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 /// Name and (if specified) rename of a symbol
90 struct SelectiveImport
91 {
92 	/// Original name (always available)
93 	string name;
94 	/// Rename if specified
95 	string rename;
96 }
97 
98 /// Information about one import statement
99 struct ImportInfo
100 {
101 	/// Parts of the imported module. (std.stdio -> ["std", "stdio"])
102 	string[] name;
103 	/// Available if the module has been imported renamed
104 	string rename;
105 	/// Array of selective imports or empty if the entire module has been imported
106 	SelectiveImport[] selectives;
107 }
108 
109 private __gshared:
110 RollbackAllocator rba;
111 LexerConfig config;
112 StringCache* cache;
113 
114 string getIndentation(ubyte[] code, size_t index)
115 {
116 	import std.ascii : isWhite;
117 
118 	bool atLineEnd = false;
119 	if (index < code.length && code[index] == '\n')
120 	{
121 		for (size_t i = index; i < code.length; i++)
122 			if (!code[i].isWhite)
123 				break;
124 		atLineEnd = true;
125 	}
126 	while (index > 0)
127 	{
128 		if (code[index - 1] == cast(ubyte) '\n')
129 			break;
130 		index--;
131 	}
132 	size_t end = index;
133 	while (end < code.length)
134 	{
135 		if (!code[end].isWhite)
136 			break;
137 		end++;
138 	}
139 	auto indent = cast(string) code[index .. end];
140 	if (!indent.length && index == 0 && !atLineEnd)
141 		return " ";
142 	return "\n" ~ indent.stripLeft('\n');
143 }
144 
145 unittest
146 {
147 	auto code = cast(ubyte[]) "void foo() {\n\tfoo();\n}";
148 	auto indent = getIndentation(code, 20);
149 	assert(indent == "\n\t", '"' ~ indent ~ '"');
150 
151 	code = cast(ubyte[]) "void foo() { foo(); }";
152 	indent = getIndentation(code, 19);
153 	assert(indent == " ", '"' ~ indent ~ '"');
154 
155 	code = cast(ubyte[]) "import a;\n\nvoid foo() {\n\tfoo();\n}";
156 	indent = getIndentation(code, 9);
157 	assert(indent == "\n", '"' ~ indent ~ '"');
158 }
159 
160 class ImporterReaderVisitor : ASTVisitor
161 {
162 	this(int pos)
163 	{
164 		this.pos = pos;
165 		inBlock = false;
166 	}
167 
168 	alias visit = ASTVisitor.visit;
169 
170 	override void visit(const ModuleDeclaration decl)
171 	{
172 		if (decl.endLocation + 1 < outerImportLocation || inBlock)
173 			return;
174 		isModule = true;
175 		outerImportLocation = decl.endLocation + 1;
176 	}
177 
178 	override void visit(const ImportDeclaration decl)
179 	{
180 		if (decl.startIndex >= pos)
181 			return;
182 		isModule = false;
183 		if (inBlock)
184 			innermostBlockStart = decl.endIndex;
185 		else
186 			outerImportLocation = decl.endIndex;
187 		foreach (i; decl.singleImports)
188 			imports ~= ImportInfo(i.identifierChain.identifiers.map!(tok => tok.text.idup)
189 					.array, i.rename.text);
190 		if (decl.importBindings)
191 		{
192 			ImportInfo info;
193 			if (!decl.importBindings.singleImport)
194 				return;
195 			info.name = decl.importBindings.singleImport.identifierChain.identifiers.map!(
196 					tok => tok.text.idup).array;
197 			info.rename = decl.importBindings.singleImport.rename.text;
198 			foreach (bind; decl.importBindings.importBinds)
199 			{
200 				if (bind.right.text)
201 					info.selectives ~= SelectiveImport(bind.right.text, bind.left.text);
202 				else
203 					info.selectives ~= SelectiveImport(bind.left.text);
204 			}
205 			if (info.selectives.length)
206 				imports ~= info;
207 		}
208 	}
209 
210 	override void visit(const BlockStatement content)
211 	{
212 		if (content && pos >= content.startLocation && pos < content.endLocation)
213 		{
214 			if (content.startLocation + 1 >= innermostBlockStart)
215 				innermostBlockStart = content.startLocation + 1;
216 			inBlock = true;
217 			return content.accept(this);
218 		}
219 	}
220 
221 	private int pos;
222 	private bool inBlock;
223 	ImportInfo[] imports;
224 	bool isModule;
225 	size_t outerImportLocation;
226 	size_t innermostBlockStart;
227 }
228 
229 void doNothing(string, size_t, size_t, string, bool)
230 {
231 }
232 
233 unittest
234 {
235 	import std.conv;
236 
237 	start();
238 	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; }",
239 			81);
240 	bool equalsImport(ImportInfo i, string s)
241 	{
242 		return i.name.join('.') == s;
243 	}
244 
245 	void assertEquals(T)(T a, T b)
246 	{
247 		assert(a == b, "'" ~ a.to!string ~ "' != '" ~ b.to!string ~ "'");
248 	}
249 
250 	assertEquals(imports.length, 3);
251 	assert(equalsImport(imports[0], "std.stdio"));
252 	assert(equalsImport(imports[1], "std.file"));
253 	assertEquals(imports[1].rename, "fs");
254 	assert(equalsImport(imports[2], "std.algorithm"));
255 	assertEquals(imports[2].selectives.length, 2);
256 	assertEquals(imports[2].selectives[0].name, "map");
257 	assertEquals(imports[2].selectives[1].name, "each");
258 	assertEquals(imports[2].selectives[1].rename, "each2");
259 
260 	string code = "void foo() { import std.stdio : stderr; writeln(\"hi\"); }";
261 	auto mod = add("std.stdio", code, 45);
262 	assertEquals(mod.rename, "");
263 	assertEquals(mod.replacements.length, 1);
264 	assertEquals(mod.replacements[0].apply(code),
265 			"void foo() { import std.stdio : stderr; import std.stdio; writeln(\"hi\"); }");
266 
267 	code = "void foo() {\n\timport std.stdio : stderr;\n\twriteln(\"hi\");\n}";
268 	mod = add("std.stdio", code, 45);
269 	assertEquals(mod.rename, "");
270 	assertEquals(mod.replacements.length, 1);
271 	assertEquals(mod.replacements[0].apply(code),
272 			"void foo() {\n\timport std.stdio : stderr;\n\timport std.stdio;\n\twriteln(\"hi\");\n}");
273 
274 	code = "void foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}";
275 	mod = add("std.stdio", code, 45);
276 	assertEquals(mod.rename, "");
277 	assertEquals(mod.replacements.length, 1);
278 	assertEquals(mod.replacements[0].apply(code),
279 			"import std.stdio;\nvoid foo() {\n\timport std.file : readText;\n\twriteln(\"hi\");\n}");
280 
281 	code = "void foo() { import io = std.stdio; io.writeln(\"hi\"); }";
282 	mod = add("std.stdio", code, 45);
283 	assertEquals(mod.rename, "io");
284 	assertEquals(mod.replacements.length, 0);
285 
286 	code = "import std.file : readText;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
287 	mod = add("std.stdio", code, 45);
288 	assertEquals(mod.rename, "");
289 	assertEquals(mod.replacements.length, 1);
290 	assertEquals(mod.replacements[0].apply(code),
291 			"import std.file : readText;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
292 
293 	code = "import std.file;\nimport std.regex;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
294 	mod = add("std.stdio", code, 54);
295 	assertEquals(mod.rename, "");
296 	assertEquals(mod.replacements.length, 1);
297 	assertEquals(mod.replacements[0].apply(code),
298 			"import std.file;\nimport std.regex;\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
299 
300 	code = "module a;\n\nvoid foo() {\n\twriteln(\"hi\");\n}";
301 	mod = add("std.stdio", code, 30);
302 	assertEquals(mod.rename, "");
303 	assertEquals(mod.replacements.length, 1);
304 	assertEquals(mod.replacements[0].apply(code),
305 			"module a;\n\nimport std.stdio;\n\nvoid foo() {\n\twriteln(\"hi\");\n}");
306 
307 	stop();
308 }