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 `.normLF;
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 }`.normLF;
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 }`.normLF;
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 }