1 /// Component for adding imports to a file, reading imports at a location of code and sorting imports. 2 module workspaced.com.importer; 3 4 import dparse.ast; 5 import dparse.lexer; 6 import dparse.parser; 7 import dparse.rollback_allocator; 8 9 import std.algorithm; 10 import std.array; 11 import std.functional; 12 import std.stdio; 13 import std.string; 14 import std.uni : sicmp; 15 16 import workspaced.api; 17 import workspaced.helpers : determineIndentation, indexOfKeyword, stripLineEndingLength; 18 19 /// ditto 20 @component("importer") 21 class ImporterComponent : ComponentWrapper 22 { 23 mixin DefaultComponentWrapper; 24 25 protected void load() 26 { 27 config.stringBehavior = StringBehavior.source; 28 } 29 30 /// Returns all imports available at some code position. 31 ImportInfo[] get(scope const(char)[] code, int pos) 32 { 33 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 34 auto mod = parseModule(tokens, "code", &rba); 35 auto reader = new ImporterReaderVisitor(pos); 36 reader.visit(mod); 37 return reader.imports; 38 } 39 40 /// Returns a list of code patches for adding an import. 41 /// If `insertOutermost` is false, the import will get added to the innermost block. 42 ImportModification add(string importName, scope const(char)[] code, int pos, bool insertOutermost = true) 43 { 44 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 45 auto mod = parseModule(tokens, "code", &rba); 46 auto reader = new ImporterReaderVisitor(pos); 47 reader.visit(mod); 48 foreach (i; reader.imports) 49 { 50 if (i.name.join('.') == importName) 51 { 52 if (i.selectives.length == 0) 53 return ImportModification(i.rename, []); 54 else 55 insertOutermost = false; 56 } 57 } 58 string indentation = ""; 59 if (insertOutermost) 60 { 61 indentation = reader.outerImportLocation == 0 ? "" : (cast(ubyte[]) code) 62 .getIndentation(reader.outerImportLocation); 63 if (reader.isModule) 64 indentation = '\n' ~ indentation; 65 return ImportModification("", [CodeReplacement([reader.outerImportLocation, reader.outerImportLocation], 66 indentation ~ "import " ~ importName ~ ";" ~ (reader.outerImportLocation == 0 ? "\n" : ""))]); 67 } 68 else 69 { 70 indentation = (cast(ubyte[]) code).getIndentation(reader.innermostBlockStart); 71 if (reader.isModule) 72 indentation = '\n' ~ indentation; 73 return ImportModification("", [CodeReplacement([reader.innermostBlockStart, 74 reader.innermostBlockStart], indentation ~ "import " ~ importName ~ ";")]); 75 } 76 } 77 78 /// Sorts the imports in a whitespace separated group of code 79 /// Returns `ImportBlock.init` if no changes would be done. 80 ImportBlock sortImports(scope const(char)[] code, int pos) 81 { 82 bool startBlock = true; 83 string indentation; 84 size_t start, end; 85 // find block of code separated by empty lines 86 foreach (line; code.lineSplitter!(KeepTerminator.yes)) 87 { 88 if (startBlock) 89 start = end; 90 startBlock = line.strip.length == 0; 91 if (startBlock && end >= pos) 92 break; 93 end += line.length; 94 } 95 if (start >= end || end > code.length) 96 return ImportBlock.init; 97 auto part = code[start .. end]; 98 99 // then filter out the proper indentation 100 bool inCorrectIndentationBlock; 101 size_t acc; 102 bool midImport; 103 foreach (line; part.lineSplitter!(KeepTerminator.yes)) 104 { 105 const indent = line.determineIndentation; 106 bool marksNewRegion; 107 bool leavingMidImport; 108 109 const importStart = line.indexOfKeyword("import"); 110 const importEnd = line.indexOf(';'); 111 if (importStart != -1) 112 { 113 acc += importStart; 114 line = line[importStart .. $]; 115 116 if (importEnd == -1) 117 midImport = true; 118 else 119 midImport = importEnd < importStart; 120 } 121 else if (importEnd != -1 && midImport) 122 leavingMidImport = true; 123 else if (!midImport) 124 { 125 // got no "import" and wasn't in an import here 126 marksNewRegion = true; 127 } 128 129 if ((marksNewRegion || indent != indentation) && !midImport) 130 { 131 if (inCorrectIndentationBlock) 132 { 133 end = start + acc - line.stripLineEndingLength; 134 break; 135 } 136 start += acc; 137 acc = 0; 138 indentation = indent; 139 } 140 141 if (leavingMidImport) 142 midImport = false; 143 144 if (start + acc <= pos && start + acc + line.length - 1 >= pos) 145 inCorrectIndentationBlock = true; 146 acc += line.length; 147 } 148 149 part = code[start .. end]; 150 151 auto tokens = getTokensForParser(cast(ubyte[]) part, config, &workspaced.stringCache); 152 auto mod = parseModule(tokens, "code", &rba); 153 auto reader = new ImporterReaderVisitor(-1); 154 reader.visit(mod); 155 156 auto imports = reader.imports; 157 if (!imports.length) 158 return ImportBlock.init; 159 160 foreach (ref imp; imports) 161 imp.start += start; 162 163 start = imports.front.start; 164 end = code.indexOf(';', imports.back.start) + 1; 165 166 auto sorted = imports.map!(a => ImportInfo(a.name, a.rename, 167 a.selectives.dup.sort!((c, d) => sicmp(c.effectiveName, d.effectiveName) < 0).array, 168 a.start)) 169 .array 170 .sort!((a, b) => sicmp(a.effectiveName, b.effectiveName) < 0) 171 .array; 172 if (sorted == imports) 173 return ImportBlock.init; 174 return ImportBlock(cast(int) start, cast(int) end, sorted, indentation); 175 } 176 177 private: 178 RollbackAllocator rba; 179 LexerConfig config; 180 } 181 182 unittest 183 { 184 import std.conv : to; 185 186 void assertEqual(ImportBlock a, ImportBlock b) 187 { 188 assert(a.sameEffectAs(b), a.to!string ~ " is not equal to " ~ b.to!string); 189 } 190 191 scope backend = new WorkspaceD(); 192 auto workspace = makeTemporaryTestingWorkspace; 193 auto instance = backend.addInstance(workspace.directory); 194 backend.register!ImporterComponent; 195 196 string code = `import std.stdio; 197 import std.algorithm; 198 import std.array; 199 import std.experimental.logger; 200 import std.regex; 201 import std.functional; 202 import std.file; 203 import std.path; 204 205 import core.thread; 206 import core.sync.mutex; 207 208 import gtk.HBox, gtk.VBox, gtk.MainWindow, gtk.Widget, gtk.Button, gtk.Frame, 209 gtk.ButtonBox, gtk.Notebook, gtk.CssProvider, gtk.StyleContext, gtk.Main, 210 gdk.Screen, gtk.CheckButton, gtk.MessageDialog, gtk.Window, gtkc.gtk, 211 gtk.Label, gdk.Event; 212 213 import already; 214 import sorted; 215 216 import std.stdio : writeln, File, stdout, err = stderr; 217 218 version(unittest) 219 import std.traits; 220 import std.stdio; 221 import std.algorithm; 222 223 void main() 224 { 225 import std.stdio; 226 import std.algorithm; 227 228 writeln("foo"); 229 } 230 231 void main() 232 { 233 import std.stdio; 234 import std.algorithm; 235 } 236 237 void main() 238 { 239 import std.stdio; 240 import std.algorithm; 241 string midImport; 242 import std.string; 243 import std.array; 244 } 245 246 import workspaced.api; 247 import workspaced.helpers : determineIndentation, stripLineEndingLength, indexOfKeyword; 248 `; 249 250 //dfmt off 251 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 0), ImportBlock(0, 164, [ 252 ImportInfo(["std", "algorithm"]), 253 ImportInfo(["std", "array"]), 254 ImportInfo(["std", "experimental", "logger"]), 255 ImportInfo(["std", "file"]), 256 ImportInfo(["std", "functional"]), 257 ImportInfo(["std", "path"]), 258 ImportInfo(["std", "regex"]), 259 ImportInfo(["std", "stdio"]) 260 ])); 261 262 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 192), ImportBlock(166, 209, [ 263 ImportInfo(["core", "sync", "mutex"]), 264 ImportInfo(["core", "thread"]) 265 ])); 266 267 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 238), ImportBlock(211, 457, [ 268 ImportInfo(["gdk", "Event"]), 269 ImportInfo(["gdk", "Screen"]), 270 ImportInfo(["gtk", "Button"]), 271 ImportInfo(["gtk", "ButtonBox"]), 272 ImportInfo(["gtk", "CheckButton"]), 273 ImportInfo(["gtk", "CssProvider"]), 274 ImportInfo(["gtk", "Frame"]), 275 ImportInfo(["gtk", "HBox"]), 276 ImportInfo(["gtk", "Label"]), 277 ImportInfo(["gtk", "Main"]), 278 ImportInfo(["gtk", "MainWindow"]), 279 ImportInfo(["gtk", "MessageDialog"]), 280 ImportInfo(["gtk", "Notebook"]), 281 ImportInfo(["gtk", "StyleContext"]), 282 ImportInfo(["gtk", "VBox"]), 283 ImportInfo(["gtk", "Widget"]), 284 ImportInfo(["gtk", "Window"]), 285 ImportInfo(["gtkc", "gtk"]) 286 ])); 287 288 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 467), ImportBlock.init); 289 290 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 546), ImportBlock(491, 546, [ 291 ImportInfo(["std", "stdio"], "", [ 292 SelectiveImport("stderr", "err"), 293 SelectiveImport("File"), 294 SelectiveImport("stdout"), 295 SelectiveImport("writeln"), 296 ]) 297 ])); 298 299 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 593), ImportBlock(586, 625, [ 300 ImportInfo(["std", "algorithm"]), 301 ImportInfo(["std", "stdio"]) 302 ])); 303 304 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 650), ImportBlock(642, 682, [ 305 ImportInfo(["std", "algorithm"]), 306 ImportInfo(["std", "stdio"]) 307 ], "\t")); 308 309 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 730), ImportBlock(719, 759, [ 310 ImportInfo(["std", "algorithm"]), 311 ImportInfo(["std", "stdio"]) 312 ], "\t")); 313 314 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 850), ImportBlock(839, 876, [ 315 ImportInfo(["std", "array"]), 316 ImportInfo(["std", "string"]) 317 ], "\t")); 318 319 assertEqual(backend.get!ImporterComponent(workspace.directory).sortImports(code, 897), ImportBlock(880, 991, [ 320 ImportInfo(["workspaced", "api"]), 321 ImportInfo(["workspaced", "helpers"], "", [ 322 SelectiveImport("determineIndentation"), 323 SelectiveImport("indexOfKeyword"), 324 SelectiveImport("stripLineEndingLength") 325 ]) 326 ])); 327 //dfmt on 328 } 329 330 /// Information about how to add an import 331 struct ImportModification 332 { 333 /// Set if there was already an import which was renamed. (for example import io = std.stdio; would be "io") 334 string rename; 335 /// Array of replacements to add the import to the code 336 CodeReplacement[] replacements; 337 } 338 339 /// Name and (if specified) rename of a symbol 340 struct SelectiveImport 341 { 342 /// Original name (always available) 343 string name; 344 /// Rename if specified 345 string rename; 346 347 /// Returns rename if set, otherwise name 348 string effectiveName() const 349 { 350 return rename.length ? rename : name; 351 } 352 353 /// Returns a D source code part 354 string toString() const 355 { 356 return (rename.length ? rename ~ " = " : "") ~ name; 357 } 358 } 359 360 /// Information about one import statement 361 struct ImportInfo 362 { 363 /// Parts of the imported module. (std.stdio -> ["std", "stdio"]) 364 string[] name; 365 /// Available if the module has been imported renamed 366 string rename; 367 /// Array of selective imports or empty if the entire module has been imported 368 SelectiveImport[] selectives; 369 /// Index where the import starts in code 370 size_t start; 371 372 /// Returns the rename if available, otherwise the name joined with dots 373 string effectiveName() const 374 { 375 return rename.length ? rename : name.join('.'); 376 } 377 378 /// Returns D source code for this import 379 string toString() const 380 { 381 import std.conv : to; 382 383 return "import " ~ (rename.length ? rename ~ " = " 384 : "") ~ name.join('.') ~ (selectives.length 385 ? " : " ~ selectives.to!(string[]).join(", ") : "") ~ ';'; 386 } 387 388 bool sameEffectAs(in ImportInfo other) const 389 { 390 return name == other.name && rename == other.rename && selectives == other.selectives; 391 } 392 } 393 394 /// A block of imports generated by the sort-imports command 395 struct ImportBlock 396 { 397 /// Start & end byte index 398 int start, end; 399 /// 400 ImportInfo[] imports; 401 /// 402 string indentation; 403 404 bool sameEffectAs(in ImportBlock other) const 405 { 406 if (!(start == other.start && end == other.end && indentation == other.indentation)) 407 return false; 408 409 if (imports.length != other.imports.length) 410 return false; 411 412 foreach (i; 0 .. imports.length) 413 if (!imports[i].sameEffectAs(other.imports[i])) 414 return false; 415 416 return true; 417 } 418 } 419 420 private: 421 422 string getIndentation(ubyte[] code, size_t index) 423 { 424 import std.ascii : isWhite; 425 426 bool atLineEnd = false; 427 if (index < code.length && code[index] == '\n') 428 { 429 for (size_t i = index; i < code.length; i++) 430 if (!code[i].isWhite) 431 break; 432 atLineEnd = true; 433 } 434 while (index > 0) 435 { 436 if (code[index - 1] == cast(ubyte) '\n') 437 break; 438 index--; 439 } 440 size_t end = index; 441 while (end < code.length) 442 { 443 if (!code[end].isWhite) 444 break; 445 end++; 446 } 447 auto indent = cast(string) code[index .. end]; 448 if (!indent.length && index == 0 && !atLineEnd) 449 return " "; 450 return "\n" ~ indent.stripLeft('\n'); 451 } 452 453 unittest 454 { 455 auto code = cast(ubyte[]) "void foo() {\n\tfoo();\n}"; 456 auto indent = getIndentation(code, 20); 457 assert(indent == "\n\t", '"' ~ indent ~ '"'); 458 459 code = cast(ubyte[]) "void foo() { foo(); }"; 460 indent = getIndentation(code, 19); 461 assert(indent == " ", '"' ~ indent ~ '"'); 462 463 code = cast(ubyte[]) "import a;\n\nvoid foo() {\n\tfoo();\n}"; 464 indent = getIndentation(code, 9); 465 assert(indent == "\n", '"' ~ indent ~ '"'); 466 } 467 468 class ImporterReaderVisitor : ASTVisitor 469 { 470 this(int pos) 471 { 472 this.pos = pos; 473 inBlock = false; 474 } 475 476 alias visit = ASTVisitor.visit; 477 478 override void visit(const ModuleDeclaration decl) 479 { 480 if (pos != -1 && (decl.endLocation + 1 < outerImportLocation || inBlock)) 481 return; 482 isModule = true; 483 outerImportLocation = decl.endLocation + 1; 484 } 485 486 override void visit(const ImportDeclaration decl) 487 { 488 if (pos != -1 && decl.startIndex >= pos) 489 return; 490 isModule = false; 491 if (inBlock) 492 innermostBlockStart = decl.endIndex; 493 else 494 outerImportLocation = decl.endIndex; 495 foreach (i; decl.singleImports) 496 imports ~= ImportInfo(i.identifierChain.identifiers.map!(tok => tok.text.idup) 497 .array, i.rename.text, null, decl.tokens[0].index); 498 if (decl.importBindings) 499 { 500 ImportInfo info; 501 if (!decl.importBindings.singleImport) 502 return; 503 info.name = decl.importBindings.singleImport.identifierChain.identifiers.map!( 504 tok => tok.text.idup).array; 505 info.rename = decl.importBindings.singleImport.rename.text; 506 foreach (bind; decl.importBindings.importBinds) 507 { 508 if (bind.right.text) 509 info.selectives ~= SelectiveImport(bind.right.text, bind.left.text); 510 else 511 info.selectives ~= SelectiveImport(bind.left.text); 512 } 513 info.start = decl.tokens[0].index; 514 if (info.selectives.length) 515 imports ~= info; 516 } 517 } 518 519 override void visit(const BlockStatement content) 520 { 521 if (pos == -1 || content && pos >= content.startLocation && pos < content.endLocation) 522 { 523 if (content.startLocation + 1 >= innermostBlockStart) 524 innermostBlockStart = content.startLocation + 1; 525 inBlock = true; 526 return content.accept(this); 527 } 528 } 529 530 private int pos; 531 private bool inBlock; 532 ImportInfo[] imports; 533 bool isModule; 534 size_t outerImportLocation; 535 size_t innermostBlockStart; 536 } 537 538 unittest 539 { 540 import std.conv; 541 542 scope backend = new WorkspaceD(); 543 auto workspace = makeTemporaryTestingWorkspace; 544 auto instance = backend.addInstance(workspace.directory); 545 backend.register!ImporterComponent; 546 auto imports = backend.get!ImporterComponent(workspace.directory).get("import std.stdio; void foo() { import fs = std.file; import std.algorithm : map, each2 = each; writeln(\"hi\"); } void bar() { import std.string; import std.regex : ctRegex; }", 547 81); 548 bool equalsImport(ImportInfo i, string s) 549 { 550 return i.name.join('.') == s; 551 } 552 553 void assertEquals(T)(T a, T b) 554 { 555 assert(a == b, "'" ~ a.to!string ~ "' != '" ~ b.to!string ~ "'"); 556 } 557 558 assertEquals(imports.length, 3); 559 assert(equalsImport(imports[0], "std.stdio")); 560 assert(equalsImport(imports[1], "std.file")); 561 assertEquals(imports[1].rename, "fs"); 562 assert(equalsImport(imports[2], "std.algorithm")); 563 assertEquals(imports[2].selectives.length, 2); 564 assertEquals(imports[2].selectives[0].name, "map"); 565 assertEquals(imports[2].selectives[1].name, "each"); 566 assertEquals(imports[2].selectives[1].rename, "each2"); 567 568 string code = "void foo() { import std.stdio : stderr; writeln(\"hi\"); }"; 569 auto mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 570 assertEquals(mod.rename, ""); 571 assertEquals(mod.replacements.length, 1); 572 assertEquals(mod.replacements[0].apply(code), 573 "void foo() { import std.stdio : stderr; import std.stdio; writeln(\"hi\"); }"); 574 575 code = "void foo() {\n\timport std.stdio : stderr;\n\twriteln(\"hi\");\n}"; 576 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 577 assertEquals(mod.rename, ""); 578 assertEquals(mod.replacements.length, 1); 579 assertEquals(mod.replacements[0].apply(code), 580 "void foo() {\n\timport std.stdio : stderr;\n\timport std.stdio;\n\twriteln(\"hi\");\n}"); 581 582 code = "void foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"; 583 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 584 assertEquals(mod.rename, ""); 585 assertEquals(mod.replacements.length, 1); 586 assertEquals(mod.replacements[0].apply(code), 587 "import std.stdio;\nvoid foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}"); 588 589 code = "void foo() { import io = std.stdio; io.writeln(\"hi\"); }"; 590 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 591 assertEquals(mod.rename, "io"); 592 assertEquals(mod.replacements.length, 0); 593 594 code = "import std.file : readText;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 595 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 45); 596 assertEquals(mod.rename, ""); 597 assertEquals(mod.replacements.length, 1); 598 assertEquals(mod.replacements[0].apply(code), 599 "import std.file : readText;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 600 601 code = "import std.file;\nimport std.regex;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 602 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 54); 603 assertEquals(mod.rename, ""); 604 assertEquals(mod.replacements.length, 1); 605 assertEquals(mod.replacements[0].apply(code), 606 "import std.file;\nimport std.regex;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 607 608 code = "module a;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"; 609 mod = backend.get!ImporterComponent(workspace.directory).add("std.stdio", code, 30); 610 assertEquals(mod.rename, ""); 611 assertEquals(mod.replacements.length, 1); 612 assertEquals(mod.replacements[0].apply(code), 613 "module a;\n\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}"); 614 }