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 import workspaced.dparseext;
11 
12 import std.algorithm;
13 import std.array;
14 import std.ascii;
15 import std.conv;
16 import std.json;
17 import std.string;
18 import std.typecons;
19 
20 public import workspaced.com.snippets.control_flow;
21 public import workspaced.com.snippets.dependencies;
22 public import workspaced.com.snippets.plain;
23 public import workspaced.com.snippets.smart;
24 
25 // ugly, but works for now
26 import mir.algebraic_alias.json : JsonValue = JsonAlgebraic;
27 
28 /// Component for auto completing snippets with context information and formatting these snippets with dfmt.
29 @component("snippets")
30 class SnippetsComponent : ComponentWrapper
31 {
32 	mixin DefaultComponentWrapper;
33 
34 	static PlainSnippetProvider plainSnippets;
35 	static SmartSnippetProvider smartSnippets;
36 	static DependencyBasedSnippetProvider dependencySnippets;
37 	static ControlFlowSnippetProvider controlFlowSnippets;
38 
39 	protected SnippetProvider[] providers;
40 
41 	protected void load()
42 	{
43 		if (!plainSnippets)
44 			plainSnippets = new PlainSnippetProvider();
45 		if (!smartSnippets)
46 			smartSnippets = new SmartSnippetProvider();
47 		if (!dependencySnippets)
48 			dependencySnippets = new DependencyBasedSnippetProvider();
49 		if (!controlFlowSnippets)
50 			controlFlowSnippets = new ControlFlowSnippetProvider();
51 
52 		config.stringBehavior = StringBehavior.source;
53 		providers.reserve(16);
54 		providers ~= plainSnippets;
55 		providers ~= smartSnippets;
56 		providers ~= dependencySnippets;
57 		providers ~= controlFlowSnippets;
58 	}
59 
60 	/**
61 	 * Params:
62 	 *   file = Filename to resolve dependencies relatively from.
63 	 *   code = Code to complete snippet in.
64 	 *   position = Byte offset of where to find scope in.
65 	 *
66 	 * Returns: a `SnippetInfo` object for all snippet information.
67 	 *
68 	 * `.loopScope` is set if a loop can be inserted at this position, Optionally
69 	 * with information about close ranges. Contains `SnippetLoopScope.init` if
70 	 * this is not a location where a loop can be inserted.
71 	 */
72 	SnippetInfo determineSnippetInfo(scope const(char)[] file, scope const(char)[] code, int position)
73 	{
74 		// each variable is 1
75 		// maybe more expensive lookups with DCD in the future
76 		enum LoopVariableAnalyzeMaxCost = 90;
77 
78 		scope tokens = getTokensForParser(cast(const(ubyte)[]) code, config, &workspaced.stringCache);
79 		auto loc = tokens.tokenIndexAtByteIndex(position);
80 
81 		// first check if at end of identifier, move current location to that
82 		// identifier.
83 		if (loc > 0
84 			&& loc < tokens.length
85 			&& tokens[loc - 1].isLikeIdentifier
86 			&& tokens[loc - 1].index <= position
87 			&& tokens[loc - 1].index + tokens[loc - 1].textLength >= position)
88 			loc--;
89 		// also determine info from before start of identifier (so you can start
90 		// typing something and it still finds a snippet scope)
91 		// > double decrement when at end of identifier, start of other token!
92 		if (loc > 0
93 			&& loc < tokens.length
94 			&& tokens[loc].isLikeIdentifier
95 			&& tokens[loc].index <= position
96 			&& tokens[loc].index + tokens[loc].textLength >= position)
97 			loc--;
98 
99 		// nudge in next token if position is after this token
100 		if (loc < tokens.length && tokens[loc].isLikeIdentifier
101 			&& position > tokens[loc].index + tokens[loc].textLength)
102 		{
103 			// cursor must not be glued to the end of identifiers
104 			loc++;
105 		}
106 		else if (loc < tokens.length && !tokens[loc].isLikeIdentifier
107 			&& position >= tokens[loc].index + tokens[loc].textLength)
108 		{
109 			// but next token if end of non-identifiers (eg `""`, `;`, `.`, `(`)
110 			loc++;
111 		}
112 
113 		int contextIndex;
114 		int checkLocation = position;
115 		if (loc >= 0 && loc < tokens.length)
116 		{
117 			contextIndex = cast(int) tokens[loc].index;
118 			if (tokens[loc].index < position)
119 				checkLocation = contextIndex;
120 		}
121 
122 		if (loc == 0 || loc == tokens.length)
123 			return SnippetInfo(contextIndex, [SnippetLevel.global]);
124 
125 		auto leading = tokens[0 .. loc];
126 
127 		if (leading.length)
128 		{
129 			auto last = leading[$ - 1];
130 			switch (last.type)
131 			{
132 			case tok!".":
133 			case tok!")":
134 			case tok!"characterLiteral":
135 			case tok!"dstringLiteral":
136 			case tok!"wstringLiteral":
137 			case tok!"stringLiteral":
138 				// no snippets immediately after these tokens (needs some other
139 				// token inbetween)
140 				return SnippetInfo(contextIndex, [SnippetLevel.other]);
141 			case tok!"(":
142 				// current token is something like `)`, check for previous
143 				// tokens like `__traits` `(`
144 				if (leading.length >= 2)
145 				{
146 					switch (leading[$ - 2].type)
147 					{
148 					case tok!"__traits":
149 					case tok!"version":
150 					case tok!"debug":
151 						return SnippetInfo(contextIndex, [SnippetLevel.other]);
152 					default: break;
153 					}
154 				}
155 				break;
156 			case tok!"__traits":
157 			case tok!"version":
158 			case tok!"debug":
159 				return SnippetInfo(contextIndex, [SnippetLevel.other]);
160 			case tok!"typeof":
161 			case tok!"if":
162 			case tok!"while":
163 			case tok!"for":
164 			case tok!"foreach":
165 			case tok!"foreach_reverse":
166 			case tok!"switch":
167 			case tok!"with":
168 			case tok!"catch":
169 				// immediately after these tokens, missing opening parentheses
170 				if (tokens[loc].type != tok!"(")
171 					return SnippetInfo(contextIndex, [SnippetLevel.other]);
172 				break;
173 			default:
174 				break;
175 			}
176 		}
177 
178 		auto current = tokens[loc];
179 		switch (current.type)
180 		{
181 		case tok!"comment":
182 			size_t len = max(0, cast(ptrdiff_t)position
183 				- cast(ptrdiff_t)current.index);
184 			// TODO: currently never called because we would either need to
185 			// use the DLexer struct as parser immediately or wait until
186 			// libdparse >=0.15.0 which contains trivia, where this switch
187 			// needs to be modified to check the exact trivia token instead
188 			// of the associated token with it.
189 			if (current.text[0 .. len].startsWith("///", "/++", "/**"))
190 				return SnippetInfo(contextIndex, [SnippetLevel.docComment]);
191 			else if (len >= 2)
192 				return SnippetInfo(contextIndex, [SnippetLevel.comment]);
193 			else
194 				break;
195 		case tok!"characterLiteral":
196 		case tok!"dstringLiteral":
197 		case tok!"wstringLiteral":
198 		case tok!"stringLiteral":
199 			if (position <= current.index)
200 				break;
201 
202 			auto textSoFar = current.text[1 .. position - current.index];
203 			// no string complete if we are immediately after escape or
204 			// quote character
205 			// TODO: properly check if this is an unescaped escape
206 			if (textSoFar.endsWith('\\', current.text[0]))
207 				return SnippetInfo(contextIndex, [SnippetLevel.strings, SnippetLevel.other]);
208 			else
209 				return SnippetInfo(contextIndex, [SnippetLevel.strings]);
210 		default:
211 			break;
212 		}
213 
214 		foreach_reverse (t; leading)
215 		{
216 			if (t.type == tok!";")
217 				break;
218 
219 			// test for tokens semicolon closed statements where we should abort to avoid incomplete syntax
220 			if (t.type.among!(tok!"import", tok!"module"))
221 			{
222 				return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.other]);
223 			}
224 			else if (t.type.among!(tok!"=", tok!"+", tok!"-", tok!"*", tok!"/",
225 					tok!"%", tok!"^^", tok!"&", tok!"|", tok!"^", tok!"<<",
226 					tok!">>", tok!">>>", tok!"~", tok!"in"))
227 			{
228 				return SnippetInfo(contextIndex, [SnippetLevel.global, SnippetLevel.value]);
229 			}
230 		}
231 
232 		RollbackAllocator rba;
233 		scope parsed = parseModule(tokens, cast(string) file, &rba);
234 
235 		//trace("determineSnippetInfo at ", contextIndex);
236 
237 		scope gen = new SnippetInfoGenerator(checkLocation);
238 		gen.value.contextTokenIndex = contextIndex;
239 		gen.variableStack.reserve(64);
240 		gen.visit(parsed);
241 
242 		gen.value.loopScope.supported = gen.value.level == SnippetLevel.method;
243 		if (gen.value.loopScope.supported)
244 		{
245 			int cost = 0;
246 			foreach_reverse (v; gen.variableStack)
247 			{
248 				if (fillLoopScopeInfo(gen.value.loopScope, v))
249 					break;
250 				if (++cost > LoopVariableAnalyzeMaxCost)
251 					break;
252 			}
253 		}
254 
255 		if (gen.lastStatement)
256 		{
257 			import dparse.ast;
258 
259 			LastStatementInfo info;
260 			auto nodeType = gen.lastStatement.findDeepestNonBlockNode;
261 			if (gen.lastStatement.tokens.length)
262 				info.location = cast(int) nodeType.tokens[0].index;
263 			info.type = typeid(nodeType).name;
264 			auto lastDot = info.type.lastIndexOf('.');
265 			if (lastDot != -1)
266 				info.type = info.type[lastDot + 1 .. $];
267 			if (auto ifStmt = cast(IfStatement)nodeType)
268 			{
269 				auto elseStmt = getIfElse(ifStmt);
270 				if (cast(IfStatement)elseStmt)
271 					info.ifHasElse = false;
272 				else
273 					info.ifHasElse = elseStmt !is null;
274 			}
275 			else if (auto ifStmt = cast(ConditionalDeclaration)nodeType)
276 				info.ifHasElse = ifStmt.hasElse;
277 			// if (auto ifStmt = cast(ConditionalStatement)nodeType)
278 			// 	info.ifHasElse = !!getIfElse(ifStmt);
279 
280 			gen.value.lastStatement = info;
281 		}
282 
283 		return gen.value;
284 	}
285 
286 	Future!SnippetList getSnippets(scope const(char)[] file, scope const(char)[] code, int position)
287 	{
288 		mixin(gthreadsAsyncProxy!`getSnippetsBlocking(file, code, position)`);
289 	}
290 
291 	SnippetList getSnippetsBlocking(scope const(char)[] file, scope const(char)[] code, int position)
292 	{
293 		auto futures = collectSnippets(file, code, position);
294 
295 		auto ret = appender!(Snippet[]);
296 		foreach (fut; futures[1])
297 			ret.put(fut.getBlocking());
298 		return SnippetList(futures[0], ret.data);
299 	}
300 
301 	SnippetList getSnippetsYield(scope const(char)[] file, scope const(char)[] code, int position)
302 	{
303 		auto futures = collectSnippets(file, code, position);
304 
305 		auto ret = appender!(Snippet[]);
306 		foreach (fut; futures[1])
307 			ret.put(fut.getYield());
308 		return SnippetList(futures[0], ret.data);
309 	}
310 
311 	Future!Snippet resolveSnippet(scope const(char)[] file, scope const(char)[] code,
312 			int position, Snippet snippet)
313 	{
314 		foreach (provider; providers)
315 		{
316 			if (typeid(provider).name == snippet.providerId)
317 			{
318 				const info = determineSnippetInfo(file, code, position);
319 				return provider.resolveSnippet(instance, file, code, position, info, snippet);
320 			}
321 		}
322 
323 		return typeof(return).fromResult(snippet);
324 	}
325 
326 	Future!string format(scope const(char)[] snippet, string[] arguments = [],
327 			SnippetLevel level = SnippetLevel.global)
328 	{
329 		mixin(gthreadsAsyncProxy!`formatSync(snippet, arguments, level)`);
330 	}
331 
332 	/// Will format the code passed in synchronously using dfmt. Might take a short moment on larger documents.
333 	/// Returns: the formatted code as string or unchanged if dfmt is not active
334 	string formatSync(scope const(char)[] snippet, string[] arguments = [],
335 			SnippetLevel level = SnippetLevel.global)
336 	{
337 		if (!has!DfmtComponent)
338 			return snippet.idup;
339 
340 		auto dfmt = get!DfmtComponent;
341 
342 		auto tmp = appender!string;
343 
344 		final switch (level)
345 		{
346 		case SnippetLevel.global:
347 		case SnippetLevel.other:
348 		case SnippetLevel.comment:
349 		case SnippetLevel.docComment:
350 		case SnippetLevel.strings:
351 		case SnippetLevel.mixinTemplate:
352 		case SnippetLevel.newMethod:
353 		case SnippetLevel.loop:
354 		case SnippetLevel.switch_:
355 			break;
356 		case SnippetLevel.type:
357 			tmp.put("struct FORMAT_HELPER {\n");
358 			break;
359 		case SnippetLevel.method:
360 			tmp.put("void FORMAT_HELPER() {\n");
361 			break;
362 		case SnippetLevel.value:
363 			tmp.put("int FORMAT_HELPER() = ");
364 			break;
365 		}
366 
367 		scope const(char)[][string] tokens;
368 
369 		ptrdiff_t dollar, last;
370 		while (true)
371 		{
372 			dollar = snippet.indexOfAny(`$\`, last);
373 			if (dollar == -1)
374 			{
375 				tmp ~= snippet[last .. $];
376 				break;
377 			}
378 
379 			tmp ~= snippet[last .. dollar];
380 			last = dollar + 1;
381 			if (last >= snippet.length)
382 				break;
383 			if (snippet[dollar] == '\\')
384 			{
385 				tmp ~= snippet[dollar + 1];
386 				last = dollar + 2;
387 			}
388 			else
389 			{
390 				string key = "__WspD_Snp_" ~ dollar.to!string ~ "_";
391 				const(char)[] str;
392 
393 				bool startOfBlock = snippet[0 .. dollar].stripRight.endsWith("{");
394 				bool endOfBlock;
395 
396 				bool makeWrappingIfMayBeDelegate()
397 				{
398 					endOfBlock = snippet[last .. $].stripLeft.startsWith("}");
399 					if (startOfBlock && endOfBlock)
400 					{
401 						// make extra long to make dfmt definitely wrap this (in case this is a delegate, otherwise this doesn't hurt either)
402 						key.reserve(key.length + 200);
403 						foreach (i; 0 .. 200)
404 							key ~= "_";
405 						return true;
406 					}
407 					else
408 						return false;
409 				}
410 
411 				if (snippet[dollar + 1] == '{')
412 				{
413 					ptrdiff_t i = dollar + 2;
414 					int depth = 1;
415 					while (true)
416 					{
417 						auto next = snippet.indexOfAny(`\{}`, i);
418 						if (next == -1)
419 						{
420 							i = snippet.length;
421 							break;
422 						}
423 
424 						if (snippet[next] == '\\')
425 							i = next + 2;
426 						else
427 						{
428 							if (snippet[next] == '{')
429 								depth++;
430 							else if (snippet[next] == '}')
431 								depth--;
432 							else
433 								assert(false);
434 
435 							i = next + 1;
436 						}
437 
438 						if (depth == 0)
439 							break;
440 					}
441 					str = snippet[dollar .. i];
442 					last = i;
443 
444 					const wrapped = makeWrappingIfMayBeDelegate();
445 
446 					const placeholderMightBeIdentifier = str.length > 5
447 						|| snippet[last .. $].stripLeft.startsWith(";", ".", "{", "(", "[");
448 
449 					if (wrapped || placeholderMightBeIdentifier)
450 					{
451 						// let's insert some token in here instead of a comment because there is probably some default content
452 						// if there is a semicolon at the end we probably need to insert a semicolon here too
453 						// if this is a comment placeholder let's insert a semicolon to make dfmt wrap
454 						if (str[0 .. $ - 1].endsWith(';') || str[0 .. $ - 1].canFind("//"))
455 							key ~= ';';
456 					}
457 					else if (level != SnippetLevel.value)
458 					{
459 						// empty default, put in comment
460 						key = "/+++" ~ key ~ "+++/";
461 					}
462 				}
463 				else
464 				{
465 					size_t end = dollar + 1;
466 
467 					if (snippet[dollar + 1].isDigit)
468 					{
469 						while (end < snippet.length && snippet[end].isDigit)
470 							end++;
471 					}
472 					else
473 					{
474 						while (end < snippet.length && (snippet[end].isAlphaNum || snippet[end] == '_'))
475 							end++;
476 					}
477 
478 					str = snippet[dollar .. end];
479 					last = end;
480 
481 					makeWrappingIfMayBeDelegate();
482 
483 					const placeholderMightBeIdentifier = snippet[last .. $].stripLeft.startsWith(";",
484 							".", "{", "(", "[");
485 
486 					if (placeholderMightBeIdentifier)
487 					{
488 						// keep value thing as simple identifier as we don't have any placeholder text
489 					}
490 					else if (level != SnippetLevel.value)
491 					{
492 						// primitive placeholder as comment
493 						key = "/+++" ~ key ~ "+++/";
494 					}
495 				}
496 
497 				tokens[key] = str;
498 				tmp ~= key;
499 			}
500 		}
501 
502 		final switch (level)
503 		{
504 		case SnippetLevel.global:
505 		case SnippetLevel.other:
506 		case SnippetLevel.comment:
507 		case SnippetLevel.docComment:
508 		case SnippetLevel.strings:
509 		case SnippetLevel.mixinTemplate:
510 		case SnippetLevel.newMethod:
511 		case SnippetLevel.loop:
512 		case SnippetLevel.switch_:
513 			break;
514 		case SnippetLevel.type:
515 		case SnippetLevel.method:
516 			tmp.put("}");
517 			break;
518 		case SnippetLevel.value:
519 			tmp.put(";");
520 			break;
521 		}
522 
523 		auto res = dfmt.formatSync(tmp.data, arguments);
524 
525 		string chompStr;
526 		char del;
527 		final switch (level)
528 		{
529 		case SnippetLevel.global:
530 		case SnippetLevel.other:
531 		case SnippetLevel.comment:
532 		case SnippetLevel.docComment:
533 		case SnippetLevel.strings:
534 		case SnippetLevel.mixinTemplate:
535 		case SnippetLevel.newMethod:
536 		case SnippetLevel.loop:
537 		case SnippetLevel.switch_:
538 			break;
539 		case SnippetLevel.type:
540 		case SnippetLevel.method:
541 			chompStr = "}";
542 			del = '{';
543 			break;
544 		case SnippetLevel.value:
545 			chompStr = ";";
546 			del = '=';
547 			break;
548 		}
549 
550 		if (chompStr.length)
551 			res = res.stripRight.chomp(chompStr);
552 
553 		if (del != char.init)
554 		{
555 			auto start = res.indexOf(del);
556 			if (start != -1)
557 			{
558 				res = res[start + 1 .. $];
559 
560 				while (true)
561 				{
562 					// delete empty lines before first line
563 					auto nl = res.indexOf('\n');
564 					if (nl != -1 && res[0 .. nl].all!isWhite)
565 						res = res[nl + 1 .. $];
566 					else
567 						break;
568 				}
569 
570 				auto indent = res[0 .. res.length - res.stripLeft.length];
571 				if (indent.length)
572 				{
573 					// remove indentation of whole block
574 					assert(indent.all!isWhite);
575 					res = res.splitLines.map!(a => a.startsWith(indent)
576 							? a[indent.length .. $] : a.stripRight).join("\n");
577 				}
578 			}
579 		}
580 
581 		foreach (key, value; tokens)
582 		{
583 			// TODO: replacing using aho-corasick would be far more efficient but there is nothing like that in phobos
584 			res = res.replace(key, value);
585 		}
586 
587 		if (res.endsWith("\r\n") && !snippet.endsWith('\n'))
588 			res.length -= 2;
589 		else if (res.endsWith('\n') && !snippet.endsWith('\n'))
590 			res.length--;
591 
592 		if (res.endsWith(";\n\n$0"))
593 			res = res[0 .. $ - "\n$0".length] ~ "$0";
594 		else if (res.endsWith(";\r\n\r\n$0"))
595 			res = res[0 .. $ - "\r\n$0".length] ~ "$0";
596 
597 		return res;
598 	}
599 
600 	/// Adds snippets which complete conditionally based on dub dependencies being present.
601 	/// This function affects the global configuration of all instances.
602 	/// Params:
603 	///   requiredDependencies = The dependencies which must be present in order for this snippet to show up.
604 	///   snippet = The snippet to suggest when the required dependencies are matched.
605 	void addDependencySnippet(scope const string[] requiredDependencies, const PlainSnippet snippet)
606 	{
607 		// maybe application global change isn't such a good idea? Current config system seems too inefficient for this.
608 		dependencySnippets.addSnippet(requiredDependencies, snippet);
609 	}
610 
611 	/// Registers snippets for a variety of popular DUB dependencies.
612 	void registerBuiltinDependencySnippets()
613 	{
614 		import workspaced.com.snippets.external_builtin;
615 
616 		foreach (group; builtinDependencySnippets)
617 			foreach (snippet; group.snippets)
618 				addDependencySnippet(group.requiredDependencies, snippet);
619 	}
620 
621 private:
622 	Tuple!(SnippetInfo, Future!(Snippet[])[]) collectSnippets(scope const(char)[] file,
623 			scope const(char)[] code, int position)
624 	{
625 		const inst = instance;
626 		auto info = determineSnippetInfo(file, code, position);
627 		auto futures = appender!(Future!(Snippet[])[]);
628 		foreach (provider; providers)
629 			futures.put(provider.provideSnippets(inst, file, code, position, info));
630 		return tuple(info, futures.data);
631 	}
632 
633 	LexerConfig config;
634 }
635 
636 ///
637 enum SnippetLevel
638 {
639 	/// Outside of functions or types, possibly inside templates
640 	global,
641 	/// Inside interfaces, classes, structs or unions
642 	type,
643 	/// Inside method body
644 	method,
645 	/// inside a variable value, argument call, default value or similar
646 	value,
647 	/// Other scope types (for example outside of braces but after a function definition or some other invalid syntax place)
648 	other,
649 	/// Inside a string literal.
650 	strings,
651 	/// Inside a normal comment
652 	comment,
653 	/// Inside a documentation comment
654 	docComment,
655 	/// Inside explicitly declared mixin templates
656 	mixinTemplate,
657 
658 	/// Inserted at the start of any method, meaning the scope has cleared or at least is logically separated.
659 	newMethod,
660 	/// a breakable loop (while, for, foreach, etc.)
661 	/// This type is usually not the trailing type and will repeat method afterwards.
662 	loop,
663 	/// a `switch` statement
664 	/// This type is usually not the trailing type and will repeat method afterwards.
665 	switch_,
666 }
667 
668 ///
669 struct SnippetLoopScope
670 {
671 	/// true if an loop expression can be inserted at this point
672 	bool supported;
673 	/// true if we know we are iterating over a string (possibly needing unicode decoding) or false otherwise
674 	bool stringIterator;
675 	/// Explicit type to use when iterating or null if none is known
676 	string type;
677 	/// Best variable to iterate over or null if none was found
678 	string iterator;
679 	/// Number of keys to iterate over
680 	int numItems = 1;
681 }
682 
683 ///
684 struct SnippetInfo
685 {
686 	/// Index in code which token was used to determine this snippet info.
687 	int contextTokenIndex;
688 	/// Levels this snippet location has gone through, latest one being the last
689 	SnippetLevel[] stack = [SnippetLevel.global];
690 	/// Information about snippets using loop context
691 	SnippetLoopScope loopScope;
692 	/// Information about the last parsable statement before the cursor. May be
693 	/// `LastStatementInfo.init` at start of function or block.
694 	LastStatementInfo lastStatement;
695 
696 	/// Current snippet scope level of the location
697 	SnippetLevel level() const @property
698 	{
699 		return stack.length ? stack[$ - 1] : SnippetLevel.other;
700 	}
701 
702 	/// Checks in reverse if the given snippet level is in the stack, up until
703 	/// the last newMethod level.
704 	SnippetLevel findInLocalScope(SnippetLevel[] levels...) const
705 	{
706 		foreach_reverse (s; stack)
707 		{
708 			if (levels.canFind(s))
709 				return s;
710 			if (s == SnippetLevel.newMethod)
711 				break;
712 		}
713 		return SnippetLevel.init;
714 	}
715 }
716 
717 struct LastStatementInfo
718 {
719 	/// The libdparse class name (typeid) of the last parsable statement before
720 	/// the cursor, stripped of module name.
721 	string type;
722 	/// If type is set, this is the start location in bytes where
723 	/// the first token was.
724 	int location;
725 	/// True if the type is (`IfStatement`, `ConditionalDeclaration` or
726 	/// `ConditionalStatement`) and has a final `else` block defined.
727 	bool ifHasElse;
728 }
729 
730 /// A list of snippets resolved at a given position.
731 struct SnippetList
732 {
733 	/// The info where this snippet is completing at.
734 	SnippetInfo info;
735 	/// The list of snippets that got returned.
736 	Snippet[] snippets;
737 }
738 
739 ///
740 interface SnippetProvider
741 {
742 	Future!(Snippet[]) provideSnippets(scope const WorkspaceD.Instance instance,
743 			scope const(char)[] file, scope const(char)[] code, int position, const SnippetInfo info);
744 
745 	Future!Snippet resolveSnippet(scope const WorkspaceD.Instance instance,
746 			scope const(char)[] file, scope const(char)[] code, int position,
747 			const SnippetInfo info, Snippet snippet);
748 }
749 
750 /// Snippet to insert
751 struct Snippet
752 {
753 	/// Internal ID for resolving this snippet
754 	string id, providerId;
755 	/// User-defined data for helping resolving this snippet
756 	JsonValue data;
757 	/// Label for this snippet
758 	string title;
759 	/// Shortcut to type for this snippet
760 	string shortcut;
761 	/// Markdown documentation for this snippet
762 	string documentation;
763 	/// Plain text to insert assuming global level indentation.
764 	string plain;
765 	/// Text with interactive snippet locations to insert assuming global indentation.
766 	string snippet;
767 	/// true if this snippet can be used as-is
768 	bool resolved;
769 	/// true if this snippet shouldn't be formatted.
770 	bool unformatted;
771 	/// List of imports that should be imported when using this snippet.
772 	string[] imports;
773 }