1 module workspaced.com.snippets;
2 
3 import dparse.lexer;
4 import dparse.parser;
5 import dparse.rollback_allocator;
6 
7 import workspaced.api;
8 import workspaced.com.dfmt : DfmtComponent;
9 import workspaced.com.snippets.generator;
10 
11 import std.algorithm;
12 import std.array;
13 import std.ascii;
14 import std.conv;
15 import std.json;
16 import std.string;
17 import std.typecons;
18 
19 public import workspaced.com.snippets.plain;
20 public import workspaced.com.snippets.smart;
21 public import workspaced.com.snippets.dependencies;
22 
23 /// Component for auto completing snippets with context information and formatting these snippets with dfmt.
24 @component("snippets")
25 class SnippetsComponent : ComponentWrapper
26 {
27 	mixin DefaultComponentWrapper;
28 
29 	static PlainSnippetProvider plainSnippets;
30 	static SmartSnippetProvider smartSnippets;
31 	static DependencyBasedSnippetProvider dependencySnippets;
32 
33 	protected SnippetProvider[] providers;
34 
35 	protected void load()
36 	{
37 		if (!plainSnippets)
38 			plainSnippets = new PlainSnippetProvider();
39 		if (!smartSnippets)
40 			smartSnippets = new SmartSnippetProvider();
41 		if (!dependencySnippets)
42 			dependencySnippets = new DependencyBasedSnippetProvider();
43 
44 		config.stringBehavior = StringBehavior.source;
45 		providers.reserve(16);
46 		providers ~= plainSnippets;
47 		providers ~= smartSnippets;
48 		providers ~= dependencySnippets;
49 	}
50 
51 	/** 
52 	 * Params:
53 	 *   file = Filename to resolve dependencies relatively from.
54 	 *   code = Code to complete snippet in.
55 	 *   position = Byte offset of where to find scope in.
56 	 *
57 	 * Returns: a `SnippetInfo` object for all snippet information.
58 	 *
59 	 * `.loopScope` is set if a loop can be inserted at this position, Optionally
60 	 * with information about close ranges. Contains `SnippetLoopScope.init` if
61 	 * this is not a location where a loop can be inserted.
62 	 */
63 	SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position)
64 	{
65 		// each variable is 1
66 		// maybe more expensive lookups with DCD in the future
67 		enum LoopVariableAnalyzeMaxCost = 90;
68 
69 		scope tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
70 		// TODO: binary search
71 		size_t loc;
72 		foreach (i, tok; tokens)
73 			if (tok.index >= position)
74 			{
75 				loc = i;
76 				break;
77 			}
78 
79 		auto leading = tokens[0 .. loc];
80 		foreach_reverse (t; leading)
81 		{
82 			if (t.type == tok!";")
83 				break;
84 
85 			// test for tokens semicolon closed statements where we should abort to avoid incomplete syntax
86 			if (t.type.among!(tok!"import", tok!"module"))
87 			{
88 				return SnippetInfo([SnippetLevel.global, SnippetLevel.other]);
89 			}
90 			else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/",
91 					tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<",
92 					tok!">>", tok!">>>", tok!"~", tok!"in"))
93 			{
94 				return SnippetInfo([SnippetLevel.global, SnippetLevel.value]);
95 			}
96 		}
97 
98 		scope parsed = parseModule(tokens, cast(string) file, &rba);
99 
100 		trace("determineSnippetInfo at ", position);
101 
102 		scope gen = new SnippetInfoGenerator(position);
103 		gen.variableStack.reserve(64);
104 		gen.visit(parsed);
105 
106 		gen.value.loopScope.supported = gen.value.level == SnippetLevel.method;
107 		if (gen.value.loopScope.supported)
108 		{
109 			int cost = 0;
110 			foreach_reverse (v; gen.variableStack)
111 			{
112 				if (fillLoopScopeInfo(gen.value.loopScope, v))
113 					break;
114 				if (++cost > LoopVariableAnalyzeMaxCost)
115 					break;
116 			}
117 		}
118 
119 		return gen.value;
120 	}
121 
122 	Future!SnippetList getSnippets(scope const(char)[] file, scope const(char)[] code, int position)
123 	{
124 		mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`);
125 	}
126 
127 	SnippetList getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position)
128 	{
129 		auto futures = collectSnippets(file, code, position);
130 
131 		auto ret = appender!(Snippet[]);
132 		foreach (fut; futures[1])
133 			ret.put(fut.getBlocking());
134 		return SnippetList(futures[0], ret.data);
135 	}
136 
137 	SnippetList getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position)
138 	{
139 		auto futures = collectSnippets(file, code, position);
140 
141 		auto ret = appender!(Snippet[]);
142 		foreach (fut; futures[1])
143 			ret.put(fut.getYield());
144 		return SnippetList(futures[0], ret.data);
145 	}
146 
147 	Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code,
148 			int position, Snippet snippet)
149 	{
150 		foreach (provider; providers)
151 		{
152 			if (typeid(provider).name == snippet.providerId)
153 			{
154 				const info = determineSnippetInfo(file, code, position);
155 				return provider.resolveSnippet(instance, file, code, position, info, snippet);
156 			}
157 		}
158 
159 		return Future!Snippet.fromResult(snippet);
160 	}
161 
162 	Future!string format(scope const(char)[] snippet, string[] arguments = [],
163 			SnippetLevel level = SnippetLevel.global)
164 	{
165 		mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments, level)`);
166 	}
167 
168 	/// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents.
169 	/// Returns: the formatted code as string or unchanged if dfmt is not active
170 	string formatSync(scope const(char)[] snippet, string[] arguments = [],
171 			SnippetLevel level = SnippetLevel.global)
172 	{
173 		if (!has!DfmtComponent)
174 			return snippet.idup;
175 
176 		auto dfmt = get!DfmtComponent;
177 
178 		auto tmp = appender!string;
179 
180 		final switch (level)
181 		{
182 		case SnippetLevel.global:
183 		case SnippetLevel.other:
184 			break;
185 		case SnippetLevel.type:
186 			tmp.put("struct FORMAT_HELPER {\n");
187 			break;
188 		case SnippetLevel.method:
189 			tmp.put("void FORMAT_HELPER() {\n");
190 			break;
191 		case SnippetLevel.value:
192 			tmp.put("int FORMAT_HELPER() = ");
193 			break;
194 		}
195 
196 		scope const(char)[][string] tokens;
197 
198 		ptrdiff_t dollar, last;
199 		while (true)
200 		{
201 			dollar = snippet.indexOfAny(`$\`, last);
202 			if (dollar == -1)
203 			{
204 				tmp ~= snippet[last .. $];
205 				break;
206 			}
207 
208 			tmp ~= snippet[last .. dollar];
209 			last = dollar + 1;
210 			if (last >= snippet.length)
211 				break;
212 			if (snippet[dollar] == '\\')
213 			{
214 				tmp ~= snippet[dollar + 1];
215 				last = dollar + 2;
216 			}
217 			else
218 			{
219 				string key = "__WspD_Snp_" ~ dollar.to!string;
220 				const(char)[] str;
221 
222 				bool startOfBlock = snippet[0 .. dollar].stripRight.endsWith("{");
223 				bool endOfBlock;
224 
225 				bool makeWrappingIfMayBeDelegate()
226 				{
227 					endOfBlock = snippet[last .. $].stripLeft.startsWith("}");
228 					if (startOfBlock && endOfBlock)
229 					{
230 						// make extra long to make dfmt definitely wrap this (in case this is a delegate, otherwise this doesn't hurt either)
231 						key.reserve(key.length + 200);
232 						foreach (i; 0 .. 200)
233 							key ~= "_";
234 						return true;
235 					}
236 					else
237 						return false;
238 				}
239 
240 				if (snippet[dollar + 1] == '{')
241 				{
242 					ptrdiff_t i = dollar + 2;
243 					int depth = 1;
244 					while (true)
245 					{
246 						auto next = snippet.indexOfAny(`\{}`, i);
247 						if (next == -1)
248 						{
249 							i = snippet.length;
250 							break;
251 						}
252 
253 						if (snippet[next] == '\\')
254 							i = next + 2;
255 						else
256 						{
257 							if (snippet[next] == '{')
258 								depth++;
259 							else if (snippet[next] == '}')
260 								depth--;
261 							else
262 								assert(false);
263 
264 							i = next + 1;
265 						}
266 
267 						if (depth == 0)
268 							break;
269 					}
270 					str = snippet[dollar .. i];
271 					last = i;
272 
273 					const wrapped = makeWrappingIfMayBeDelegate();
274 
275 					const placeholderMightBeIdentifier = str.length > 5
276 						|| snippet[last .. $].stripLeft.startsWith(";", ".", "{");
277 
278 					if (wrapped || placeholderMightBeIdentifier)
279 					{
280 						// let's insert some token in here instead of a comment because there is probably some default content
281 						// if there is a semicolon at the end we probably need to insert a semicolon here too
282 						// if this is a comment placeholder let's insert a semicolon to make dfmt wrap
283 						if (str[0 .. $ - 1].endsWith(';') || str[0 .. $ - 1].canFind("//"))
284 							key ~= ';';
285 					}
286 					else if (level != SnippetLevel.value)
287 					{
288 						// empty default, put in comment
289 						key = "/+++" ~ key ~ "+++/";
290 					}
291 				}
292 				else
293 				{
294 					size_t end = dollar + 1;
295 
296 					if (snippet[dollar + 1].isDigit)
297 					{
298 						while (end < snippet.length && snippet[end].isDigit)
299 							end++;
300 					}
301 					else
302 					{
303 						while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_'))
304 							end++;
305 					}
306 
307 					str = snippet[dollar .. end];
308 					last = end;
309 
310 					makeWrappingIfMayBeDelegate();
311 
312 					const placeholderMightBeIdentifier = snippet[last .. $].stripLeft.startsWith(";",
313 							".", "{");
314 
315 					if (placeholderMightBeIdentifier)
316 					{
317 						// keep value thing as simple identifier as we don't have any placeholder text
318 					}
319 					else if (level != SnippetLevel.value)
320 					{
321 						// primitive placeholder as comment
322 						key = "/+++" ~ key ~ "+++/";
323 					}
324 				}
325 
326 				tokens[key] = str;
327 				tmp ~= key;
328 			}
329 		}
330 
331 		final switch (level)
332 		{
333 		case SnippetLevel.global:
334 		case SnippetLevel.other:
335 			break;
336 		case SnippetLevel.type:
337 		case SnippetLevel.method:
338 			tmp.put("}");
339 			break;
340 		case SnippetLevel.value:
341 			tmp.put(";");
342 			break;
343 		}
344 
345 		auto res = dfmt.formatSync(tmp.data, arguments);
346 
347 		string chompStr;
348 		char del;
349 		final switch (level)
350 		{
351 		case SnippetLevel.global:
352 		case SnippetLevel.other:
353 			break;
354 		case SnippetLevel.type:
355 		case SnippetLevel.method:
356 			chompStr = "}";
357 			del = '{';
358 			break;
359 		case SnippetLevel.value:
360 			chompStr = ";";
361 			del = '=';
362 			break;
363 		}
364 
365 		if (chompStr.length)
366 			res = res.stripRight.chomp(chompStr);
367 
368 		if (del != char.init)
369 		{
370 			auto start = res.indexOf(del);
371 			if (start != -1)
372 			{
373 				res = res[start + 1 .. $];
374 
375 				while (true)
376 				{
377 					// delete empty lines before first line
378 					auto nl = res.indexOf('\n');
379 					if (nl != -1 && res[0 .. nl].all!isWhite)
380 						res = res[nl + 1 .. $];
381 					else
382 						break;
383 				}
384 
385 				auto indent = res[0 .. res.length - res.stripLeft.length];
386 				if (indent.length)
387 				{
388 					// remove indentation of whole block
389 					assert(indent.all!isWhite);
390 					res = res.splitLines.map!(a => a.startsWith(indent)
391 							? a[indent.length .. $] : a.stripRight).join("\n");
392 				}
393 			}
394 		}
395 
396 		foreach (key, value; tokens)
397 		{
398 			// TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos
399 			res = res.replace(key, value);
400 		}
401 
402 		if (res.endsWith("\r\n") && !snippet.endsWith('\n'))
403 			res.length -= 2;
404 		else if (res.endsWith('\n') && !snippet.endsWith('\n'))
405 			res.length--;
406 
407 		if (res.endsWith(";\n\n$0"))
408 			res = res[0 .. $ - "\n$0".length] ~ "$0";
409 		else if (res.endsWith(";\r\n\r\n$0"))
410 			res = res[0 .. $ - "\r\n$0".length] ~ "$0";
411 
412 		return res;
413 	}
414 
415 	/// Adds snippets which complete conditionally based on dub dependencies being present.
416 	/// This function affects the global configuration of all instances.
417 	/// Params:
418 	///   requiredDependencies = The dependencies which must be present in order for this snippet to show up.
419 	///   snippet = The snippet to suggest when the required dependencies are matched.
420 	void addDependencySnippet(string[] requiredDependencies, PlainSnippet snippet)
421 	{
422 		// maybe application global change isn't such a good idea? Current config system seems too inefficient for this.
423 		dependencySnippets.addSnippet(requiredDependencies, snippet);
424 	}
425 
426 private:
427 	Tuple!(SnippetInfo, Future!(Snippet[])[]) collectSnippets(scope const(char)[] file,
428 			scope const(char)[] code, int position)
429 	{
430 		const inst = instance;
431 		auto info = determineSnippetInfo(file, code, position);
432 		auto futures = appender!(Future!(Snippet[])[]);
433 		foreach (provider; providers)
434 			futures.put(provider.provideSnippets(inst, file, code, position, info));
435 		return tuple(info, futures.data);
436 	}
437 
438 	RollbackAllocator rba;
439 	LexerConfig config;
440 }
441 
442 ///
443 enum SnippetLevel
444 {
445 	/// Outside of functions or types, possibly inside templates
446 	global,
447 	/// Inside interfaces, classes, structs or unions
448 	type,
449 	/// Inside method body
450 	method,
451 	/// inside a variable value, argument call, default value or similar
452 	value,
453 	/// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place)
454 	other
455 }
456 
457 ///
458 struct SnippetLoopScope
459 {
460 	/// true if an loop expression can be inserted at this point
461 	bool supported;
462 	/// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise
463 	bool stringIterator;
464 	/// Explicit type to use when iterating or null if none is known
465 	string type;
466 	/// Best variable to iterate over or null if none was found
467 	string iterator;
468 	/// Number of keys to iterate over
469 	int numItems = 1;
470 }
471 
472 ///
473 struct SnippetInfo
474 {
475 	/// Levels this snippet location has gone through, latest one being the last
476 	SnippetLevel[] stack = [SnippetLevel.global];
477 	/// Information about snippets using loop context
478 	SnippetLoopScope loopScope;
479 
480 	/// Current snippet scope level of the location
481 	SnippetLevel level() const @property
482 	{
483 		return stack.length ? stack[$ - 1] : SnippetLevel.other;
484 	}
485 }
486 
487 /// A list of snippets resolved at a given position.
488 struct SnippetList
489 {
490 	/// The info where this snippet is completing at.
491 	SnippetInfo info;
492 	/// The list of snippets that got returned.
493 	Snippet[] snippets;
494 }
495 
496 ///
497 interface SnippetProvider
498 {
499 	Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance,
500 			scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info);
501 
502 	Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance,
503 			scope const(char)[] file, scope const(char)[] code, int position,
504 			const SnippetInfo info, Snippet snippet);
505 }
506 
507 /// Snippet to insert
508 struct Snippet
509 {
510 	/// Internal ID for resolving this snippet
511 	string id, providerId;
512 	/// User-defined data for helping resolving this snippet
513 	JSONValue data;
514 	/// Label for this snippet
515 	string title;
516 	/// Shortcut to type for this snippet
517 	string shortcut;
518 	/// Markdown documentation for this snippet
519 	string documentation;
520 	/// Plain text to insert assuming global level indentation.
521 	string plain;
522 	/// Text with interactive snippet locations to insert assuming global indentation.
523 	string snippet;
524 	/// true if this snippet can be used as-is
525 	bool resolved;
526 }
527 
528 unittest
529 {
530 	scope backend = new WorkspaceD();
531 	auto workspace = makeTemporaryTestingWorkspace;
532 	auto instance = backend.addInstance(workspace.directory);
533 	backend.register!SnippetsComponent;
534 	backend.register!DfmtComponent;
535 	SnippetsComponent snippets = backend.get!SnippetsComponent(workspace.directory);
536 
537 	auto args = ["--indent_style", "tab"];
538 
539 	auto res = snippets.formatSync("void main(${1:string[] args}) {\n\t$0\n}", args);
540 	shouldEqual(res, "void main(${1:string[] args})\n{\n\t$0\n}");
541 
542 	res = snippets.formatSync("class ${1:MyClass} {\n\t$0\n}", args);
543 	shouldEqual(res, "class ${1:MyClass}\n{\n\t$0\n}");
544 
545 	res = snippets.formatSync("enum ${1:MyEnum} = $2;\n$0", args);
546 	shouldEqual(res, "enum ${1:MyEnum} = $2;\n$0");
547 
548 	res = snippets.formatSync("import ${1:std};\n$0", args);
549 	shouldEqual(res, "import ${1:std};\n$0");
550 
551 	res = snippets.formatSync("import ${1:std};\n$0", args, SnippetLevel.method);
552 	shouldEqual(res, "import ${1:std};\n$0");
553 
554 	res = snippets.formatSync("foo(delegate() {\n${1:// foo}\n});", args, SnippetLevel.method);
555 	shouldEqual(res, "foo(delegate() {\n\t${1:// foo}\n});");
556 }