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 }