1 module workspaced.com.importer;
2 
3 import dparse.parser;
4 import dparse.lexer;
5 import dparse.ast;
6 import dparse.rollback_allocator;
7 
8 import std.algorithm;
9 import std.array;
10 import std.stdio;
11 
12 import workspaced.api;
13 
14 @component("importer") :
15 
16 /// Initializes the import parser. Call with `{"cmd": "load", "components": ["importer"]}`
17 @load void start()
18 {
19 	config.stringBehavior = StringBehavior.source;
20 	cache = new StringCache(StringCache.defaultBucketCount);
21 }
22 
23 /// Has no purpose right now.
24 @unload void stop()
25 {
26 }
27 
28 /// Returns all imports available at some code position.
29 /// Call_With: `{"subcmd": "get"}`
30 @arguments("subcmd", "get")
31 ImportInfo[] get(string code, int pos)
32 {
33 	auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache);
34 	auto mod = parseModule(tokens, "code", &rba, &doNothing);
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 /// Call_With: `{"subcmd": "add"}`
43 @arguments("subcmd", "add")
44 ImportModification add(string importName, string code, int pos, bool insertOutermost = true)
45 {
46 	auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache);
47 	auto mod = parseModule(tokens, "code", &rba, &doNothing);
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("", [CodeReplacement([reader.outerImportLocation, reader.outerImportLocation],
68 				indentation ~ "import " ~ importName ~ ";" ~ (reader.outerImportLocation == 0 ? "\n" : ""))]);
69 	}
70 	else
71 	{
72 		indentation = (cast(ubyte[]) code).getIndentation(reader.innermostBlockStart);
73 		if (reader.isModule)
74 			indentation = '\n' ~ indentation;
75 		return ImportModification("", [CodeReplacement([reader.innermostBlockStart,
76 				reader.innermostBlockStart], indentation ~ "import " ~ importName ~ ";")]);
77 	}
78 }
79 
80 /// Information about how to add an import
81 struct ImportModification
82 {
83 	/// Set if there was already an import which was renamed. (for example import io = std.stdio; would be "io")
84 	string rename;
85 	/// Array of replacements to add the import to the code
86 	CodeReplacement[] replacements;
87 }
88 
89 /// Describes what to insert/replace/delete to do something
90 struct CodeReplacement
91 {
92 	/// Range what to replace. If both indices are the same its inserting.
93 	size_t[2] range;
94 	/// Content to replace it with. Empty means remove.
95 	string content;
96 
97 	string apply(string code)
98 	{
99 		size_t min = range[0];
100 		size_t max = range[1];
101 		if (min > max)
102 		{
103 			min = range[1];
104 			max = range[0];
105 		}
106 		if (min >= code.length)
107 			return code ~ content;
108 		if (max >= code.length)
109 			return code[0 .. min] ~ content;
110 		return code[0 .. min] ~ content ~ code[max .. $];
111 	}
112 }
113 
114 /// Name and (if specified) rename of a symbol
115 struct SelectiveImport
116 {
117 	/// Original name (always available)
118 	string name;
119 	/// Rename if specified
120 	string rename;
121 }
122 
123 /// Information about one import statement
124 struct ImportInfo
125 {
126 	/// Parts of the imported module. (std.stdio -> ["std", "stdio"])
127 	string[] name;
128 	/// Available if the module has been imported renamed
129 	string rename;
130 	/// Array of selective imports or empty if the entire module has been imported
131 	SelectiveImport[] selectives;
132 }
133 
134 private __gshared:
135 RollbackAllocator rba;
136 LexerConfig config;
137 StringCache* cache;
138 
139 string getIndentation(ubyte[] code, size_t index)
140 {
141 	import std.ascii : isWhite;
142 
143 	bool atLineEnd = false;
144 	if (index < code.length && code[index] == '\n')
145 	{
146 		for (size_t i = index; i < code.length; i++)
147 			if (!code[i].isWhite)
148 				break;
149 		atLineEnd = true;
150 	}
151 	while (index > 0)
152 	{
153 		if (code[index - 1] == cast(ubyte) '\n')
154 			break;
155 		index--;
156 	}
157 	size_t end = index;
158 	while (end < code.length)
159 	{
160 		if (!code[end].isWhite)
161 			break;
162 		end++;
163 	}
164 	auto indent = cast(string) code[index .. end];
165 	if (!indent.length && index == 0 && !atLineEnd)
166 		return " ";
167 	return "\n" ~ indent.stripLeft('\n');
168 }
169 
170 unittest
171 {
172 	auto code = cast(ubyte[]) "void foo() {\n\tfoo();\n}";
173 	auto indent = getIndentation(code, 20);
174 	assert(indent == "\n\t", '"' ~ indent ~ '"');
175 
176 	code = cast(ubyte[]) "void foo() { foo(); }";
177 	indent = getIndentation(code, 19);
178 	assert(indent == " ", '"' ~ indent ~ '"');
179 
180 	code = cast(ubyte[]) "import a;\n\nvoid foo() {\n\tfoo();\n}";
181 	indent = getIndentation(code, 9);
182 	assert(indent == "\n", '"' ~ indent ~ '"');
183 }
184 
185 class ImporterReaderVisitor : ASTVisitor
186 {
187 	this(int pos)
188 	{
189 		this.pos = pos;
190 		inBlock = false;
191 	}
192 
193 	alias visit = ASTVisitor.visit;
194 
195 	override void visit(const ModuleDeclaration decl)
196 	{
197 		if (decl.endLocation + 1 < outerImportLocation || inBlock)
198 			return;
199 		isModule = true;
200 		outerImportLocation = decl.endLocation + 1;
201 	}
202 
203 	override void visit(const ImportDeclaration decl)
204 	{
205 		if (decl.startIndex >= pos)
206 			return;
207 		isModule = false;
208 		if (inBlock)
209 			innermostBlockStart = decl.endIndex;
210 		else
211 			outerImportLocation = decl.endIndex;
212 		foreach (i; decl.singleImports)
213 			imports ~= ImportInfo(i.identifierChain.identifiers.map!(tok => tok.text.idup)
214 					.array, i.rename.text);
215 		if (decl.importBindings)
216 		{
217 			ImportInfo info;
218 			if (!decl.importBindings.singleImport)
219 				return;
220 			info.name = decl.importBindings.singleImport.identifierChain.identifiers.map!(
221 					tok => tok.text.idup).array;
222 			info.rename = decl.importBindings.singleImport.rename.text;
223 			foreach (bind; decl.importBindings.importBinds)
224 			{
225 				if (bind.right.text)
226 					info.selectives ~= SelectiveImport(bind.right.text, bind.left.text);
227 				else
228 					info.selectives ~= SelectiveImport(bind.left.text);
229 			}
230 			if (info.selectives.length)
231 				imports ~= info;
232 		}
233 	}
234 
235 	override void visit(const BlockStatement content)
236 	{
237 		if (content && pos >= content.startLocation && pos < content.endLocation)
238 		{
239 			if (content.startLocation + 1 >= innermostBlockStart)
240 				innermostBlockStart = content.startLocation + 1;
241 			inBlock = true;
242 			return content.accept(this);
243 		}
244 	}
245 
246 	private int pos;
247 	private bool inBlock;
248 	ImportInfo[] imports;
249 	bool isModule;
250 	size_t outerImportLocation;
251 	size_t innermostBlockStart;
252 }
253 
254 void doNothing(string, size_t, size_t, string, bool)
255 {
256 }
257 
258 unittest
259 {
260 	import std.conv;
261 
262 	start();
263 	auto imports = 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; }",
264 			81);
265 	bool equalsImport(ImportInfo i, string s)
266 	{
267 		return i.name.join('.') == s;
268 	}
269 
270 	void assertEquals(T)(T a, T b)
271 	{
272 		assert(a == b, "'" ~ a.to!string ~ "' != '" ~ b.to!string ~ "'");
273 	}
274 
275 	assertEquals(imports.length, 3);
276 	assert(equalsImport(imports[0], "std.stdio"));
277 	assert(equalsImport(imports[1], "std.file"));
278 	assertEquals(imports[1].rename, "fs");
279 	assert(equalsImport(imports[2], "std.algorithm"));
280 	assertEquals(imports[2].selectives.length, 2);
281 	assertEquals(imports[2].selectives[0].name, "map");
282 	assertEquals(imports[2].selectives[1].name, "each");
283 	assertEquals(imports[2].selectives[1].rename, "each2");
284 
285 	string code = "void foo() { import std.stdio : stderr; writeln(\"hi\"); }";
286 	auto mod = add("std.stdio", code, 45);
287 	assertEquals(mod.rename, "");
288 	assertEquals(mod.replacements.length, 1);
289 	assertEquals(mod.replacements[0].apply(code),
290 			"void foo() { import std.stdio : stderr; import std.stdio; writeln(\"hi\"); }");
291 
292 	code = "void foo() {\n\timport std.stdio : stderr;\n\twriteln(\"hi\");\n}";
293 	mod = add("std.stdio", code, 45);
294 	assertEquals(mod.rename, "");
295 	assertEquals(mod.replacements.length, 1);
296 	assertEquals(mod.replacements[0].apply(code),
297 			"void foo() {\n\timport std.stdio : stderr;\n\timport std.stdio;\n\twriteln(\"hi\");\n}");
298 
299 	code = "void foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}";
300 	mod = add("std.stdio", code, 45);
301 	assertEquals(mod.rename, "");
302 	assertEquals(mod.replacements.length, 1);
303 	assertEquals(mod.replacements[0].apply(code),
304 			"import std.stdio;\nvoid foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}");
305 
306 	code = "void foo() { import io = std.stdio; io.writeln(\"hi\"); }";
307 	mod = add("std.stdio", code, 45);
308 	assertEquals(mod.rename, "io");
309 	assertEquals(mod.replacements.length, 0);
310 
311 	code = "import std.file : readText;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
312 	mod = add("std.stdio", code, 45);
313 	assertEquals(mod.rename, "");
314 	assertEquals(mod.replacements.length, 1);
315 	assertEquals(mod.replacements[0].apply(code),
316 			"import std.file : readText;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
317 
318 	code = "import std.file;\nimport std.regex;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
319 	mod = add("std.stdio", code, 54);
320 	assertEquals(mod.rename, "");
321 	assertEquals(mod.replacements.length, 1);
322 	assertEquals(mod.replacements[0].apply(code),
323 			"import std.file;\nimport std.regex;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
324 
325 	code = "module a;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
326 	mod = add("std.stdio", code, 30);
327 	assertEquals(mod.rename, "");
328 	assertEquals(mod.replacements.length, 1);
329 	assertEquals(mod.replacements[0].apply(code),
330 			"module a;\n\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
331 
332 	stop();
333 }