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