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