1 module served.commands.complete;
2 
3 import served.commands.format : formatCode, formatSnippet;
4 import served.extension;
5 import served.types;
6 import served.utils.ddoc;
7 import served.utils.fibermanager;
8 
9 import workspaced.api;
10 import workspaced.com.dfmt : DfmtComponent;
11 import workspaced.com.dcd;
12 import workspaced.com.dcdext;
13 import workspaced.com.snippets;
14 import workspaced.com.importer;
15 import workspaced.coms;
16 
17 import std.algorithm : among, any, canFind, chunkBy, endsWith, filter, findSplit,
18 	map, min, reverse, sort, startsWith, uniq;
19 import std.array : appender, array, split;
20 import std.conv : text, to;
21 import std.experimental.logger;
22 import std.format : format;
23 import std.string : indexOf, join, lastIndexOf, lineSplitter, strip, stripLeft,
24 	stripRight, toLower;
25 import std.utf : decodeFront;
26 
27 import dparse.lexer : Token;
28 
29 import fs = std.file;
30 import io = std.stdio;
31 
32 static immutable sortPrefixDoc = "0_";
33 static immutable sortPrefixSnippets = "2_5_";
34 // dcd additionally sorts inside with sortFromDCDType return value (appends to this)
35 static immutable sortPrefixDCD = "2_";
36 
37 CompletionItemKind convertFromDCDType(string type)
38 {
39 	if (type.length != 1)
40 		return CompletionItemKind.text;
41 
42 	switch (type[0])
43 	{
44 	case 'c': // class name
45 		return CompletionItemKind.class_;
46 	case 'i': // interface name
47 		return CompletionItemKind.interface_;
48 	case 's': // struct name
49 	case 'u': // union name
50 		return CompletionItemKind.struct_;
51 	case 'a': // array
52 	case 'A': // associative array
53 	case 'v': // variable name
54 		return CompletionItemKind.variable;
55 	case 'm': // member variable
56 		return CompletionItemKind.field;
57 	case 'e': // enum member
58 		return CompletionItemKind.enumMember;
59 	case 'k': // keyword
60 		return CompletionItemKind.keyword;
61 	case 'f': // function
62 		return CompletionItemKind.function_;
63 	case 'g': // enum name
64 		return CompletionItemKind.enum_;
65 	case 'P': // package name
66 	case 'M': // module name
67 		return CompletionItemKind.module_;
68 	case 'l': // alias name
69 		return CompletionItemKind.reference;
70 	case 't': // template name
71 	case 'T': // mixin template name
72 		return CompletionItemKind.property;
73 	case 'h': // template type parameter
74 	case 'p': // template variadic parameter
75 		return CompletionItemKind.typeParameter;
76 	default:
77 		return CompletionItemKind.text;
78 	}
79 }
80 
81 string sortFromDCDType(string type)
82 {
83 	if (type.length != 1)
84 		return "9_";
85 
86 	switch (type[0])
87 	{
88 	case 'v': // variable name
89 		return "2_";
90 	case 'm': // member variable
91 		return "3_";
92 	case 'f': // function
93 		return "4_";
94 	case 'k': // keyword
95 	case 'e': // enum member
96 		return "5_";
97 	case 'c': // class name
98 	case 'i': // interface name
99 	case 's': // struct name
100 	case 'u': // union name
101 	case 'a': // array
102 	case 'A': // associative array
103 	case 'g': // enum name
104 	case 'P': // package name
105 	case 'M': // module name
106 	case 'l': // alias name
107 	case 't': // template name
108 	case 'T': // mixin template name
109 	case 'h': // template type parameter
110 	case 'p': // template variadic parameter
111 		return "6_";
112 	default:
113 		return "9_";
114 	}
115 }
116 
117 SymbolKind convertFromDCDSearchType(string type)
118 {
119 	if (type.length != 1)
120 		return cast(SymbolKind) 0;
121 	switch (type[0])
122 	{
123 	case 'c':
124 		return SymbolKind.class_;
125 	case 'i':
126 		return SymbolKind.interface_;
127 	case 's':
128 	case 'u':
129 		return SymbolKind.package_;
130 	case 'a':
131 	case 'A':
132 	case 'v':
133 		return SymbolKind.variable;
134 	case 'm':
135 	case 'e':
136 		return SymbolKind.field;
137 	case 'f':
138 	case 'l':
139 		return SymbolKind.function_;
140 	case 'g':
141 		return SymbolKind.enum_;
142 	case 'P':
143 	case 'M':
144 		return SymbolKind.namespace;
145 	case 't':
146 	case 'T':
147 		return SymbolKind.property;
148 	case 'k':
149 	default:
150 		return cast(SymbolKind) 0;
151 	}
152 }
153 
154 SymbolKind convertFromDscannerType(string type, string name = null)
155 {
156 	if (type.length != 1)
157 		return cast(SymbolKind) 0;
158 	switch (type[0])
159 	{
160 	case 'c':
161 		return SymbolKind.class_;
162 	case 's':
163 		return SymbolKind.struct_;
164 	case 'i':
165 		return SymbolKind.interface_;
166 	case 'T':
167 		return SymbolKind.property;
168 	case 'f':
169 	case 'U':
170 	case 'Q':
171 	case 'W':
172 	case 'P':
173 		if (name == "this")
174 			return SymbolKind.constructor;
175 		else
176 			return SymbolKind.function_;
177 	case 'C':
178 	case 'S':
179 		return SymbolKind.constructor;
180 	case 'g':
181 		return SymbolKind.enum_;
182 	case 'u':
183 		return SymbolKind.struct_;
184 	case 'D':
185 	case 'V':
186 	case 'e':
187 		return SymbolKind.constant;
188 	case 'v':
189 		return SymbolKind.variable;
190 	case 'a':
191 		return SymbolKind.field;
192 	default:
193 		return cast(SymbolKind) 0;
194 	}
195 }
196 
197 SymbolKindEx convertExtendedFromDscannerType(string type)
198 {
199 	if (type.length != 1)
200 		return cast(SymbolKindEx) 0;
201 	switch (type[0])
202 	{
203 	case 'U':
204 		return SymbolKindEx.test;
205 	case 'D':
206 		return SymbolKindEx.debugSpec;
207 	case 'V':
208 		return SymbolKindEx.versionSpec;
209 	case 'C':
210 		return SymbolKindEx.staticCtor;
211 	case 'S':
212 		return SymbolKindEx.sharedStaticCtor;
213 	case 'Q':
214 		return SymbolKindEx.staticDtor;
215 	case 'W':
216 		return SymbolKindEx.sharedStaticDtor;
217 	case 'P':
218 		return SymbolKindEx.postblit;
219 	default:
220 		return cast(SymbolKindEx) 0;
221 	}
222 }
223 
224 C[] substr(C, T)(C[] s, T start, T end)
225 {
226 	if (!s.length)
227 		return s;
228 	if (start < 0)
229 		start = 0;
230 	if (start >= s.length)
231 		start = s.length - 1;
232 	if (end > s.length)
233 		end = s.length;
234 	if (end < start)
235 		return s[start .. start];
236 	return s[start .. end];
237 }
238 
239 /// Extracts all function parameters for a given declaration string.
240 /// Params:
241 ///   sig = the function signature such as `string[] example(string sig, bool exact = false)`
242 ///   exact = set to true to make the returned values include the closing paren at the end (if exists)
243 const(char)[][] extractFunctionParameters(scope const(char)[] sig, bool exact = false)
244 {
245 	if (!sig.length)
246 		return [];
247 	auto params = appender!(const(char)[][]);
248 	ptrdiff_t i = sig.length - 1;
249 
250 	if (sig[i] == ')' && !exact)
251 		i--;
252 
253 	ptrdiff_t paramEnd = i + 1;
254 
255 	void skipStr()
256 	{
257 		i--;
258 		if (sig[i + 1] == '\'')
259 			for (; i >= 0; i--)
260 				if (sig[i] == '\'')
261 					return;
262 		bool escapeNext = false;
263 		while (i >= 0)
264 		{
265 			if (sig[i] == '\\')
266 				escapeNext = false;
267 			if (escapeNext)
268 				break;
269 			if (sig[i] == '"')
270 				escapeNext = true;
271 			i--;
272 		}
273 	}
274 
275 	void skip(char open, char close)
276 	{
277 		i--;
278 		int depth = 1;
279 		while (i >= 0 && depth > 0)
280 		{
281 			if (sig[i] == '"' || sig[i] == '\'')
282 				skipStr();
283 			else
284 			{
285 				if (sig[i] == close)
286 					depth++;
287 				else if (sig[i] == open)
288 					depth--;
289 				i--;
290 			}
291 		}
292 	}
293 
294 	while (i >= 0)
295 	{
296 		switch (sig[i])
297 		{
298 		case ',':
299 			params.put(sig.substr(i + 1, paramEnd).strip);
300 			paramEnd = i;
301 			i--;
302 			break;
303 		case ';':
304 		case '(':
305 			auto param = sig.substr(i + 1, paramEnd).strip;
306 			if (param.length)
307 				params.put(param);
308 			auto ret = params.data;
309 			reverse(ret);
310 			return ret;
311 		case ')':
312 			skip('(', ')');
313 			break;
314 		case '}':
315 			skip('{', '}');
316 			break;
317 		case ']':
318 			skip('[', ']');
319 			break;
320 		case '"':
321 		case '\'':
322 			skipStr();
323 			break;
324 		default:
325 			i--;
326 			break;
327 		}
328 	}
329 	auto ret = params.data;
330 	reverse(ret);
331 	return ret;
332 }
333 
334 unittest
335 {
336 	void assertEqual(A, B)(A a, B b)
337 	{
338 		import std.conv : to;
339 
340 		assert(a == b, a.to!string ~ " is not equal to " ~ b.to!string);
341 	}
342 
343 	assertEqual(extractFunctionParameters("void foo()"), cast(string[])[]);
344 	assertEqual(extractFunctionParameters(`auto bar(int foo, Button, my.Callback cb)`),
345 			["int foo", "Button", "my.Callback cb"]);
346 	assertEqual(extractFunctionParameters(`SomeType!(int, "int_") foo(T, Args...)(T a, T b, string[string] map, Other!"(" stuff1, SomeType!(double, ")double") myType, Other!"(" stuff, Other!")")`),
347 			[
348 				"T a", "T b", "string[string] map", `Other!"(" stuff1`,
349 				`SomeType!(double, ")double") myType`, `Other!"(" stuff`, `Other!")"`
350 			]);
351 	assertEqual(extractFunctionParameters(`SomeType!(int,"int_")foo(T,Args...)(T a,T b,string[string] map,Other!"(" stuff1,SomeType!(double,")double")myType,Other!"(" stuff,Other!")")`),
352 			[
353 				"T a", "T b", "string[string] map", `Other!"(" stuff1`,
354 				`SomeType!(double,")double")myType`, `Other!"(" stuff`, `Other!")"`
355 			]);
356 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4`,
357 			true), [`4`]);
358 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, f(4)`,
359 			true), [`4`, `f(4)`]);
360 	assertEqual(extractFunctionParameters(`some_garbage(code); before(this); funcCall(4, ["a"], JSONValue(["b": JSONValue("c")]), recursive(func, call!s()), "texts )\"(too"`,
361 			true), [
362 			`4`, `["a"]`, `JSONValue(["b": JSONValue("c")])`,
363 			`recursive(func, call!s())`, `"texts )\"(too"`
364 			]);
365 }
366 
367 /// Provide snippets in auto-completion
368 __gshared bool doCompleteSnippets = false;
369 
370 // === Protocol Methods starting here ===
371 
372 @protocolMethod("textDocument/completion")
373 CompletionList provideComplete(TextDocumentPositionParams params)
374 {
375 	Document document = documents[params.textDocument.uri];
376 	auto instance = activeInstance = backend.getBestInstance(document.uri.uriToFile);
377 	trace("Completing from instance ", instance ? instance.cwd : "null");
378 
379 	if (document.uri.toLower.endsWith("dscanner.ini"))
380 	{
381 		trace("Providing dscanner.ini completion");
382 		auto possibleFields = backend.get!DscannerComponent.listAllIniFields;
383 		scope line = document.lineAtScope(params.position).strip;
384 		auto defaultList = CompletionList(false, possibleFields.map!((a) {
385 			CompletionItem ret = {
386 				label: a.name,
387 				kind: CompletionItemKind.field,
388 				documentation: MarkupContent(a.documentation),
389 				insertText: a.name ~ '='
390 			};
391 			return ret;
392 		}).array);
393 
394 		if (!line.length)
395 			return defaultList;
396 
397 		if (line[0] == '[')
398 		{
399 			// ini section
400 			CompletionItem staticAnalysisConfig = {
401 				label: "analysis.config.StaticAnalysisConfig",
402 				kind: CompletionItemKind.keyword
403 			};
404 			CompletionItem moduleFilters = {
405 				label: "analysis.config.ModuleFilters",
406 				kind: CompletionItemKind.keyword,
407 				documentation: MarkupContent(
408 					"In this optional section a comma-separated list of inclusion and exclusion"
409 					~ " selectors can be specified for every check on which selective filtering"
410 					~ " should be applied. These given selectors match on the module name and"
411 					~ " partial matches (std. or .foo.) are possible. Moreover, every selectors"
412 					~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors"
413 					~ " take precedence over all inclusion operators.")
414 			};
415 			return CompletionList(false, [staticAnalysisConfig, moduleFilters]);
416 		}
417 
418 		auto eqIndex = line.indexOf('=');
419 		auto quotIndex = line.lastIndexOf('"');
420 		if (quotIndex != -1 && params.position.character >= quotIndex)
421 			return CompletionList.init;
422 		if (params.position.character < eqIndex)
423 			return defaultList;
424 		else
425 		{
426 			CompletionItem disabled = {
427 				label: `"disabled"`,
428 				kind: CompletionItemKind.value,
429 				detail: "Check is disabled"
430 			};
431 			CompletionItem enabled = {
432 				label: `"enabled"`,
433 				kind: CompletionItemKind.value,
434 				detail: "Check is enabled"
435 			};
436 			CompletionItem skipUnittest = {
437 				label: `"skip-unittest"`,
438 				kind: CompletionItemKind.value,
439 				detail: "Check is enabled but not operated in the unittests"
440 			};
441 			return CompletionList(false, [disabled, enabled, skipUnittest]);
442 		}
443 	}
444 	else
445 	{
446 		if (!instance)
447 		{
448 			trace("Providing no completion because no instance");
449 			return CompletionList.init;
450 		}
451 
452 		if (document.getLanguageId == "d")
453 			return provideDSourceComplete(params, instance, document);
454 		else if (document.getLanguageId == "diet")
455 			return provideDietSourceComplete(params, instance, document);
456 		else if (document.getLanguageId == "dml")
457 			return provideDMLSourceComplete(params, instance, document);
458 		else
459 		{
460 			tracef("Providing no completion for unknown language ID %s.", document.getLanguageId);
461 			return CompletionList.init;
462 		}
463 	}
464 }
465 
466 CompletionList provideDMLSourceComplete(TextDocumentPositionParams params,
467 		WorkspaceD.Instance instance, ref Document document)
468 {
469 	import workspaced.com.dlangui : DlanguiComponent, CompletionType;
470 
471 	CompletionList ret;
472 
473 	auto items = backend.get!DlanguiComponent.complete(document.rawText,
474 			cast(int) document.positionToBytes(params.position)).getYield();
475 	ret.items.length = items.length;
476 	foreach (i, item; items)
477 	{
478 		CompletionItem translated;
479 
480 		translated.sortText = ((item.type == CompletionType.Class ? "1." : "0.") ~ item.value).opt;
481 		translated.label = item.value;
482 		if (item.documentation.length)
483 			translated.documentation = MarkupContent(item.documentation);
484 		if (item.enumName.length)
485 			translated.detail = item.enumName.opt;
486 
487 		switch (item.type)
488 		{
489 		case CompletionType.Class:
490 			translated.insertTextFormat = InsertTextFormat.snippet;
491 			translated.insertText = item.value ~ ` {$0}`;
492 			break;
493 		case CompletionType.Color:
494 			translated.insertTextFormat = InsertTextFormat.snippet;
495 			translated.insertText = item.value ~ `: ${0:#000000}`;
496 			break;
497 		case CompletionType.String:
498 			translated.insertTextFormat = InsertTextFormat.snippet;
499 			translated.insertText = item.value ~ `: "$0"`;
500 			break;
501 		case CompletionType.EnumDefinition:
502 			translated.insertTextFormat = InsertTextFormat.plainText;
503 			translated.insertText = item.enumName ~ "." ~ item.value;
504 			break;
505 		case CompletionType.Rectangle:
506 		case CompletionType.Number:
507 			translated.insertTextFormat = InsertTextFormat.snippet;
508 			translated.insertText = item.value ~ `: ${0:0}`;
509 			break;
510 		case CompletionType.Keyword:
511 			// don't set, inherit from label
512 			break;
513 		default:
514 			translated.insertTextFormat = InsertTextFormat.plainText;
515 			translated.insertText = item.value ~ ": ";
516 			break;
517 		}
518 
519 		switch (item.type)
520 		{
521 		case CompletionType.Class:
522 			translated.kind = CompletionItemKind.class_;
523 			break;
524 		case CompletionType.String:
525 			translated.kind = CompletionItemKind.value;
526 			break;
527 		case CompletionType.Number:
528 			translated.kind = CompletionItemKind.value;
529 			break;
530 		case CompletionType.Color:
531 			translated.kind = CompletionItemKind.color;
532 			break;
533 		case CompletionType.EnumDefinition:
534 			translated.kind = CompletionItemKind.enum_;
535 			break;
536 		case CompletionType.EnumValue:
537 			translated.kind = CompletionItemKind.enumMember;
538 			break;
539 		case CompletionType.Rectangle:
540 			translated.kind = CompletionItemKind.typeParameter;
541 			break;
542 		case CompletionType.Boolean:
543 			translated.kind = CompletionItemKind.constant;
544 			break;
545 		case CompletionType.Keyword:
546 			translated.kind = CompletionItemKind.keyword;
547 			break;
548 		default:
549 		case CompletionType.Undefined:
550 			break;
551 		}
552 
553 		ret.items[i] = translated;
554 	}
555 
556 	return ret;
557 }
558 
559 CompletionList provideDietSourceComplete(TextDocumentPositionParams params,
560 		WorkspaceD.Instance instance, ref Document document)
561 {
562 	import served.utils.diet;
563 	import dc = dietc.complete;
564 
565 	auto completion = updateDietFile(document.uri.uriToFile, document.rawText.idup);
566 
567 	auto dcdext = instance.has!DCDExtComponent ? instance.get!DCDExtComponent : null;
568 
569 	size_t offset = document.positionToBytes(params.position);
570 	auto raw = completion.completeAt(offset);
571 	CompletionItem[] ret;
572 
573 	if (raw is dc.Completion.completeD)
574 	{
575 		auto d = workspace(params.textDocument.uri).config.d;
576 		string code;
577 		contextExtractD(completion, offset, code, offset, d.dietContextCompletion);
578 		if (offset <= code.length && instance.has!DCDComponent)
579 		{
580 			info("DCD Completing Diet for ", code, " at ", offset);
581 			auto dcd = instance.get!DCDComponent.listCompletion(code, cast(int) offset).getYield;
582 			if (dcd.type == DCDCompletions.Type.identifiers)
583 			{
584 				ret = dcd.identifiers.convertDCDIdentifiers(d.argumentSnippets, dcdext);
585 			}
586 		}
587 	}
588 	else
589 		ret = raw.map!((a) {
590 			CompletionItem ret;
591 			ret.label = a.text;
592 			if (a.text.among!(`''`, `""`, "``", `{}`, `()`, `[]`, `<>`))
593 			{
594 				ret.insertTextFormat = InsertTextFormat.snippet;
595 				ret.insertText = a.text[0] ~ "$0" ~ a.text[1];
596 			}
597 			ret.kind = a.type.mapToCompletionItemKind.opt;
598 			if (a.definition.length)
599 			{
600 				ret.detail = a.definition.opt;
601 				if (capabilities
602 					.textDocument.orDefault
603 					.completion.orDefault
604 					.completionItem.orDefault
605 					.labelDetailsSupport.orDefault)
606 					ret.labelDetails = CompletionItemLabelDetails(ret.detail);
607 			}
608 			if (a.documentation.length)
609 				ret.documentation = MarkupContent(a.documentation);
610 			if (a.preselected)
611 				ret.preselect = true;
612 			return ret;
613 		}).array;
614 
615 	return CompletionList(false, ret);
616 }
617 
618 CompletionList provideDSourceComplete(TextDocumentPositionParams params,
619 		WorkspaceD.Instance instance, ref Document document)
620 {
621 	auto lineRange = document.lineByteRangeAt(params.position.line);
622 	auto byteOff = cast(int) document.positionToBytes(params.position);
623 
624 	auto dcdext = instance.has!DCDExtComponent ? instance.get!DCDExtComponent : null;
625 
626 	string line = document.rawText[lineRange[0] .. lineRange[1]].idup;
627 	string prefix = line[0 .. min($, params.position.character)].strip;
628 	CompletionItem[] completion;
629 	Token commentToken;
630 	if (document.rawText.isInComment(byteOff, backend, &commentToken))
631 	{
632 		import dparse.lexer : tok;
633 		if (commentToken.type == tok!"__EOF__")
634 			return CompletionList.init;
635 
636 		if (commentToken.text.startsWith("///", "/**", "/++"))
637 		{
638 			trace("Providing comment completion");
639 			int prefixLen = (prefix.length > 0 && prefix[0] == '/') ? 3 : 1;
640 			auto remaining = prefix[min($, prefixLen) .. $].stripLeft;
641 
642 			foreach (compl; import("ddocs.txt").lineSplitter)
643 			{
644 				if (compl.startsWith(remaining))
645 				{
646 					CompletionItem item = {
647 						label: compl,
648 						kind: CompletionItemKind.snippet,
649 						insertText: compl ~ ": "
650 					};
651 					completion ~= item;
652 				}
653 			}
654 
655 			// make the comment one "line" so provide doc complete shows complete
656 			// after a /** */ comment block if you are on the first line.
657 			lineRange[1] = commentToken.index + commentToken.text.length;
658 			provideDocComplete(params, instance, document, completion, line, lineRange);
659 
660 			return CompletionList(false, completion);
661 		}
662 	}
663 
664 	bool completeDCD = instance.has!DCDComponent;
665 	bool completeDoc = instance.has!DscannerComponent;
666 	bool completeSnippets = doCompleteSnippets && instance.has!SnippetsComponent;
667 
668 	tracef("Performing regular D comment completion (DCD=%s, Documentation=%s, Snippets=%s)",
669 			completeDCD, completeDoc, completeSnippets);
670 	const config = workspace(params.textDocument.uri).config;
671 	DCDCompletions result = DCDCompletions.empty;
672 	SnippetInfo snippetInfo;
673 	joinAll({
674 		if (completeDCD)
675 			result = instance.get!DCDComponent.listCompletion(document.rawText, byteOff).getYield;
676 	}, {
677 		if (completeDoc)
678 			provideDocComplete(params, instance, document, completion, line, lineRange);
679 	}, {
680 		if (completeSnippets)
681 			snippetInfo = provideSnippetComplete(params, instance, document, config, completion, byteOff);
682 		else
683 			snippetInfo = getSnippetInfo(instance, document, byteOff);
684 	});
685 
686 	if (completeDCD && result != DCDCompletions.init)
687 	{
688 		if (result.type == DCDCompletions.Type.identifiers)
689 		{
690 			auto d = config.d;
691 			completion ~= convertDCDIdentifiers(result.identifiers, d.argumentSnippets, dcdext, snippetInfo);
692 		}
693 		else if (result.type != DCDCompletions.Type.calltips)
694 		{
695 			trace("Unexpected result from DCD: ", result);
696 		}
697 	}
698 	return CompletionList(false, completion);
699 }
700 
701 private void provideDocComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance,
702 		ref Document document, ref CompletionItem[] completion, string line, size_t[2] lineRange)
703 {
704 	string lineStripped = line.strip;
705 	if (lineStripped.among!("", "/", "/*", "/+", "//", "///", "/**", "/++"))
706 	{
707 		auto defs = instance.get!DscannerComponent.listDefinitions(uriToFile(
708 				params.textDocument.uri), document.rawText[lineRange[1] .. $]).getYield;
709 		ptrdiff_t di = -1;
710 		FuncFinder: foreach (i, def; defs)
711 		{
712 			if (def.line >= 0 && def.line <= 5)
713 			{
714 				di = i;
715 				break FuncFinder;
716 			}
717 		}
718 		if (di == -1)
719 			return;
720 		auto def = defs[di];
721 		auto sig = "signature" in def.attributes;
722 		if (!sig)
723 		{
724 			CompletionItem doc = CompletionItem("///");
725 			doc.kind = CompletionItemKind.snippet;
726 			doc.insertTextFormat = InsertTextFormat.snippet;
727 			auto eol = document.eolAt(params.position.line).toString;
728 			doc.insertText = "/// ";
729 			CompletionItem doc2 = doc;
730 			CompletionItem doc3 = doc;
731 			doc2.label = "/**";
732 			doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */";
733 			doc3.label = "/++";
734 			doc3.insertText = "/++ " ~ eol ~ " * $0" ~ eol ~ " +/";
735 
736 			completion.addDocComplete(doc, lineStripped);
737 			completion.addDocComplete(doc2, lineStripped);
738 			completion.addDocComplete(doc3, lineStripped);
739 			return;
740 		}
741 		auto funcArgs = extractFunctionParameters(*sig);
742 		string[] docs = ["$0"];
743 		int argNo = 1;
744 		foreach (arg; funcArgs)
745 		{
746 			auto space = arg.stripRight.lastIndexOf(' ');
747 			if (space == -1)
748 				continue;
749 			auto identifier = arg[space + 1 .. $];
750 			if (!identifier.isValidDIdentifier)
751 				continue;
752 			if (argNo == 1)
753 				docs ~= "Params:";
754 			docs ~= text("  ", identifier, " = $", argNo.to!string);
755 			argNo++;
756 		}
757 		auto retAttr = "return" in def.attributes;
758 		if (retAttr && *retAttr != "void")
759 		{
760 			docs ~= "Returns: $" ~ argNo.to!string;
761 			argNo++;
762 		}
763 		auto depr = "deprecation" in def.attributes;
764 		if (depr)
765 		{
766 			docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr;
767 			argNo++;
768 		}
769 		CompletionItem doc = CompletionItem("///");
770 		doc.kind = CompletionItemKind.snippet;
771 		doc.insertTextFormat = InsertTextFormat.snippet;
772 		auto eol = document.eolAt(params.position.line).toString;
773 		doc.insertText = docs.map!(a => "/// " ~ a).join(eol);
774 		CompletionItem doc2 = doc;
775 		CompletionItem doc3 = doc;
776 		doc2.label = "/**";
777 		doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */";
778 		doc3.label = "/++";
779 		doc3.insertText = "/++ " ~ eol ~ docs.map!(a => " + " ~ a ~ eol).join() ~ " +/";
780 
781 		doc.sortText = opt(sortPrefixDoc ~ "0");
782 		doc2.sortText = opt(sortPrefixDoc ~ "1");
783 		doc3.sortText = opt(sortPrefixDoc ~ "2");
784 
785 		completion.addDocComplete(doc, lineStripped);
786 		completion.addDocComplete(doc2, lineStripped);
787 		completion.addDocComplete(doc3, lineStripped);
788 	}
789 }
790 
791 private SnippetInfo provideSnippetComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance,
792 		ref Document document, ref const UserConfiguration config,
793 		ref CompletionItem[] completion, int byteOff)
794 {
795 	if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith("."))
796 		return SnippetInfo.init; // no snippets after '.' character
797 
798 	auto snippets = instance.get!SnippetsComponent;
799 	auto ret = snippets.getSnippetsYield(document.uri.uriToFile, document.rawText, byteOff);
800 	trace("got ", ret.snippets.length, " snippets fitting in this context: ",
801 			ret.snippets.map!"a.shortcut");
802 	auto eol = document.eolAt(0);
803 	foreach (Snippet snippet; ret.snippets)
804 	{
805 		CompletionItem item = snippet.snippetToCompletionItem;
806 		JsonValue[string] data;
807 		data["level"] = JsonValue(ret.info.level.to!string);
808 		if (!snippet.unformatted)
809 			data["format"] = JsonValue(generateDfmtArgs(config, eol).join("\t"));
810 		data["uri"] = JsonValue(params.textDocument.uri);
811 		data["line"] = JsonValue(params.position.line);
812 		data["column"] = JsonValue(params.position.character);
813 		if (snippet.imports.length)
814 			data["imports"] = JsonValue(snippet.imports.map!(i => JsonValue(i)).array);
815 		item.data = JsonValue(data);
816 		completion ~= item;
817 	}
818 
819 	return ret.info;
820 }
821 
822 private SnippetInfo getSnippetInfo(WorkspaceD.Instance instance, ref Document document, int byteOff)
823 {
824 	if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith("."))
825 		return SnippetInfo.init; // no snippets after '.' character
826 
827 	auto snippets = instance.get!SnippetsComponent;
828 	return snippets.determineSnippetInfo(document.uri.uriToFile, document.rawText, byteOff);
829 }
830 
831 private void addDocComplete(ref CompletionItem[] completion, CompletionItem doc, string prefix)
832 in(!doc.insertText.isNone)
833 {
834 	if (!doc.label.startsWith(prefix))
835 		return;
836 	if (prefix.length > 0)
837 		doc.insertText = doc.insertText.deref[prefix.length .. $];
838 	completion ~= doc;
839 }
840 
841 private bool isInComment(scope const(char)[] code, size_t at, WorkspaceD backend, Token* outToken = null)
842 {
843 	if (!backend)
844 		return false;
845 
846 	import dparse.lexer : DLexer, LexerConfig, StringBehavior, tok;
847 
848 	// TODO: does this kind of token parsing belong in serve-d?
849 
850 	LexerConfig config;
851 	config.fileName = "stdin";
852 	config.stringBehavior = StringBehavior.source;
853 	auto lexer = DLexer(code, config, &backend.stringCache);
854 
855 	while (!lexer.empty)
856 	{
857 		if (lexer.front.index > at)
858 			return false;
859 
860 		switch (lexer.front.type)
861 		{
862 		case tok!"comment":
863 			auto t = lexer.front;
864 
865 			auto commentEnd = t.index + t.text.length;
866 			if (t.text.startsWith("//"))
867 				commentEnd++;
868 
869 			if (t.index <= at && at < commentEnd)
870 			{
871 				if (outToken !is null)
872 					*outToken = t;
873 				return true;
874 			}
875 
876 			lexer.popFront();
877 			break;
878 		case tok!"__EOF__":
879 			if (outToken !is null)
880 				*outToken = lexer.front;
881 			return true;
882 		default:
883 			lexer.popFront();
884 			break;
885 		}
886 	}
887 	return false;
888 }
889 
890 @protocolMethod("completionItem/resolve")
891 CompletionItem resolveCompletionItem(CompletionItem item)
892 {
893 	auto data = item.data;
894 
895 	if (!data.isNone)
896 	{
897 		auto object = data.deref.get!(StringMap!JsonValue);
898 		const resolved = "resolved" in object;
899 		const uriObj = "uri" in object;
900 		const lineObj = "line" in object;
901 		const columnObj = "column" in object;
902 
903 		if (uriObj && lineObj && columnObj)
904 		{
905 			auto uri = uriObj.get!string;
906 			auto line = cast(uint)lineObj.get!long;
907 			auto column = cast(uint)columnObj.get!long;
908 
909 			Document document = documents[uri];
910 			auto f = document.uri.uriToFile;
911 			auto instance = backend.getBestInstance(f);
912 
913 			if (resolved && !resolved.get!bool)
914 			{
915 				if (instance.has!SnippetsComponent)
916 				{
917 					auto snippets = instance.get!SnippetsComponent;
918 					auto snippet = snippetFromCompletionItem(item);
919 					snippet = snippets.resolveSnippet(f, document.rawText,
920 							cast(int) document.positionToBytes(Position(line, column)), snippet).getYield;
921 					item = snippetToCompletionItem(snippet);
922 				}
923 			}
924 
925 			if (const importsJson = "imports" in object)
926 			{
927 				if (instance.has!ImporterComponent)
928 				{
929 					auto importer = instance.get!ImporterComponent;
930 					TextEdit[] additionalEdits = item.additionalTextEdits.orDefault;
931 					auto imports = importsJson.get!(JsonValue[]);
932 					foreach (importJson; imports)
933 					{
934 						string importName = importJson.get!string;
935 						auto importInfo = importer.add(importName, document.rawText,
936 								cast(int) document.positionToBytes(Position(line, column)));
937 						// TODO: use renamed imports properly
938 						foreach (edit; importInfo.replacements)
939 							additionalEdits ~= edit.toTextEdit(document);
940 					}
941 					additionalEdits.sort!"a.range.start>b.range.start";
942 					item.additionalTextEdits = additionalEdits;
943 				}
944 			}
945 		}
946 
947 		if (const format = "format" in object)
948 		{
949 			auto args = format.get!string.split("\t");
950 			if (item.insertTextFormat.orDefault == InsertTextFormat.snippet)
951 			{
952 				SnippetLevel level = SnippetLevel.global;
953 				if (const levelStr = "level" in object)
954 					level = levelStr.get!string.to!SnippetLevel;
955 				item.insertText = formatSnippet(item.insertText.deref, args, level);
956 			}
957 			else
958 			{
959 				item.insertText = formatCode(item.insertText.deref, args);
960 			}
961 		}
962 
963 		return item;
964 	}
965 	else
966 	{
967 		return item;
968 	}
969 }
970 
971 CompletionItem snippetToCompletionItem(Snippet snippet)
972 {
973 	CompletionItem item;
974 	item.label = snippet.shortcut;
975 	item.sortText = opt(sortPrefixSnippets ~ snippet.shortcut);
976 	item.detail = snippet.title;
977 	item.kind = CompletionItemKind.snippet;
978 	item.documentation = MarkupContent(MarkupKind.markdown,
979 			snippet.documentation ~ "\n\n```d\n" ~ snippet.snippet ~ "\n```\n");
980 	item.filterText = snippet.shortcut;
981 	if (capabilities
982 		.textDocument.orDefault
983 		.completion.orDefault
984 		.completionItem.orDefault
985 		.snippetSupport.orDefault)
986 	{
987 		item.insertText = snippet.snippet;
988 		item.insertTextFormat = InsertTextFormat.snippet;
989 	}
990 	else
991 		item.insertText = snippet.plain;
992 
993 	item.data = JsonValue([
994 		"resolved": JsonValue(snippet.resolved),
995 		"id": JsonValue(snippet.id),
996 		"providerId": JsonValue(snippet.providerId),
997 		"data": snippet.data.toJsonValue
998 	]);
999 	return item;
1000 }
1001 
1002 Snippet snippetFromCompletionItem(CompletionItem item)
1003 {
1004 	Snippet snippet;
1005 	snippet.shortcut = item.label;
1006 	snippet.title = item.detail.deref;
1007 	snippet.documentation = item.documentation.match!(
1008 		() => cast(string)(null),
1009 		(string s) => s,
1010 		(MarkupContent c) => c.value
1011 	);
1012 	auto end = snippet.documentation.lastIndexOf("\n\n```d\n");
1013 	if (end != -1)
1014 		snippet.documentation = snippet.documentation[0 .. end];
1015 
1016 	if (capabilities
1017 		.textDocument.orDefault
1018 		.completion.orDefault
1019 		.completionItem.orDefault
1020 		.snippetSupport.orDefault)
1021 		snippet.snippet = item.insertText.deref;
1022 	else
1023 		snippet.plain = item.insertText.deref;
1024 
1025 	auto itemData = item.data.deref.get!(StringMap!JsonValue);
1026 	snippet.resolved = itemData["resolved"].get!bool;
1027 	snippet.id = itemData["id"].get!string;
1028 	snippet.providerId = itemData["providerId"].get!string;
1029 	snippet.data = itemData["data"];
1030 	return snippet;
1031 }
1032 
1033 unittest
1034 {
1035 	auto backend = new WorkspaceD();
1036 	assert(isInComment(`hello /** world`, 10, backend));
1037 	assert(!isInComment(`hello /** world`, 3, backend));
1038 	assert(isInComment(`hello /* world */ bar`, 8, backend));
1039 	assert(isInComment(`hello /* world */ bar`, 16, backend));
1040 	assert(!isInComment(`hello /* world */ bar`, 17, backend));
1041 	assert(!isInComment("int x;\n// line comment\n", 6, backend));
1042 	assert(isInComment("int x;\n// line comment\n", 7, backend));
1043 	assert(isInComment("int x;\n// line comment\n", 9, backend));
1044 	assert(isInComment("int x;\n// line comment\n", 21, backend));
1045 	assert(isInComment("int x;\n// line comment\n", 22, backend));
1046 	assert(!isInComment("int x;\n// line comment\n", 23, backend));
1047 }
1048 
1049 auto convertDCDIdentifiers(DCDIdentifier[] identifiers, bool argumentSnippets, DCDExtComponent dcdext,
1050 	SnippetInfo info = SnippetInfo.init)
1051 {
1052 	CompletionItem[] completion;
1053 	foreach (identifier; identifiers)
1054 	{
1055 		CompletionItem item;
1056 		string detailDetail, detailDescription;
1057 		item.label = identifier.identifier;
1058 		item.kind = identifier.type.convertFromDCDType;
1059 		if (identifier.documentation.length)
1060 			item.documentation = MarkupContent(identifier.documentation.ddocToMarked);
1061 		
1062 		if (identifier.definition.length == 0)
1063 		{
1064 			if (identifier.type.length == 1)
1065 			{
1066 				switch (identifier.type[0])
1067 				{
1068 				case 'c':
1069 					detailDescription = "Class";
1070 					break;
1071 				case 'i':
1072 					detailDescription = "Interface";
1073 					break;
1074 				case 's':
1075 					detailDescription = "Struct";
1076 					break;
1077 				case 'u':
1078 					detailDescription = "Union";
1079 					break;
1080 				case 'a':
1081 					detailDescription = "Array";
1082 					break;
1083 				case 'A':
1084 					detailDescription = "AA";
1085 					break;
1086 				case 'v':
1087 					detailDescription = "Variable";
1088 					break;
1089 				case 'm':
1090 					detailDescription = "Member";
1091 					break;
1092 				case 'e':
1093 					// lowercare to differentiate member from enum name
1094 					detailDescription = "enum";
1095 					break;
1096 				case 'k':
1097 					detailDescription = "Keyword";
1098 					break;
1099 				case 'f':
1100 					detailDescription = "Function";
1101 					break;
1102 				case 'g':
1103 					detailDescription = "Enum";
1104 					break;
1105 				case 'P':
1106 					detailDescription = "Package";
1107 					break;
1108 				case 'M':
1109 					detailDescription = "Module";
1110 					break;
1111 				case 't':
1112 				case 'T':
1113 					detailDescription = "Template";
1114 					break;
1115 				case 'h':
1116 					detailDescription = "<T>";
1117 					break;
1118 				case 'p':
1119 					detailDescription = "<T...>";
1120 					break;
1121 				case 'l': // Alias (eventually should show what it aliases to)
1122 				default:
1123 					break;
1124 				}
1125 			}
1126 		}
1127 		else
1128 		{
1129 			item.detail = identifier.definition;
1130 
1131 			// check if that's actually a proper completion item to process
1132 			auto definitionSpace = identifier.definition.indexOf(' ');
1133 			if (definitionSpace != -1)
1134 			{
1135 				detailDescription = identifier.definition[0 .. definitionSpace];
1136 				
1137 				// if function, only show the parenthesis content
1138 				if (identifier.type == "f")
1139 				{
1140 					auto paren = identifier.definition.indexOf('(');
1141 					if (paren != -1)
1142 						detailDetail = " " ~ identifier.definition[paren .. $];
1143 				}
1144 			}
1145 
1146 
1147 			// handle special cases
1148 			if (identifier.type == "e")
1149 			{
1150 				// enum definitions are the enum identifiers (not the type)
1151 				detailDescription = "enum";
1152 			}
1153 			else if (identifier.type == "f" && dcdext)
1154 			{
1155 				CalltipsSupport funcParams = dcdext.extractCallParameters(
1156 					identifier.definition, cast(int) identifier.definition.length - 1, true);
1157 
1158 				// if definition doesn't contains a return type, then it is a function that returns auto
1159 				// it could be 'enum', but that's typically the same, and there is no way to get that info right now
1160 				// need to check on DCD's part, auto/enum are removed from the definition
1161 				auto nameEnd = funcParams.templateArgumentsRange[0];
1162 				if (!nameEnd) nameEnd = funcParams.functionParensRange[0];
1163 				if (!nameEnd) nameEnd = cast(int) identifier.definition.length;
1164 				auto retTypeEnd = identifier.definition.lastIndexOf(' ', nameEnd);
1165 				if (retTypeEnd != -1)
1166 					detailDescription = identifier.definition[0 .. retTypeEnd].strip;
1167 				else
1168 					detailDescription = "auto";
1169 
1170 				detailDetail = " " ~ identifier.definition[nameEnd .. $];
1171 			}
1172 
1173 			item.sortText = identifier.definition;
1174 
1175 			// TODO: only add arguments when this is a function call, eg not on template arguments
1176 			if (identifier.type == "f" && argumentSnippets
1177 				&& info.level.among!(SnippetLevel.method, SnippetLevel.value))
1178 			{
1179 				item.insertTextFormat = InsertTextFormat.snippet;
1180 				string args;
1181 				auto parts = identifier.definition.extractFunctionParameters;
1182 				if (parts.length)
1183 				{
1184 					int numRequired;
1185 					foreach (i, part; parts)
1186 					{
1187 						ptrdiff_t equals = part.indexOf('=');
1188 						if (equals != -1)
1189 						{
1190 							part = part[0 .. equals].stripRight;
1191 							// remove default value from autocomplete
1192 						}
1193 						auto space = part.lastIndexOf(' ');
1194 						if (space != -1)
1195 							part = part[space + 1 .. $];
1196 
1197 						if (args.length)
1198 							args ~= ", ";
1199 						args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}";
1200 						numRequired++;
1201 					}
1202 					item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})";
1203 				}
1204 			}
1205 		}
1206 
1207 		if (item.sortText.isNone)
1208 			item.sortText = item.label;
1209 
1210 		item.sortText = sortPrefixDCD ~ identifier.type.sortFromDCDType ~ item.sortText.deref;
1211 
1212 		if (detailDescription.length || detailDetail.length)
1213 		{
1214 			CompletionItemLabelDetails d;
1215 			if (detailDetail.length)
1216 				d.detail = detailDetail;
1217 			if (detailDescription.length)
1218 				d.description = detailDescription;
1219 
1220 			item.labelDetails = d;
1221 		}
1222 
1223 		completion ~= item;
1224 	}
1225 
1226 	// sort only for duplicate detection (use sortText for UI sorting)
1227 	completion.sort!"a.effectiveInsertText < b.effectiveInsertText";
1228 	return completion.chunkBy!(
1229 			(a, b) =>
1230 				a.effectiveInsertText == b.effectiveInsertText
1231 				&& a.kind == b.kind
1232 		).map!((a) {
1233 			CompletionItem ret = a.front;
1234 			auto details = a.map!"a.detail"
1235 				.filter!(a => !a.isNone && a.deref.length)
1236 				.uniq
1237 				.array;
1238 			auto docs = a.map!"a.documentation"
1239 				.filter!(a => a.match!(
1240 					() => false,
1241 					(string s) => s.length > 0,
1242 					(MarkupContent s) => s.value.length > 0,
1243 				))
1244 				.uniq
1245 				.array;
1246 			auto labelDetails = a.map!"a.labelDetails"
1247 				.filter!(a => !a.isNone)
1248 				.uniq
1249 				.array;
1250 			if (docs.length)
1251 				ret.documentation = MarkupContent(MarkupKind.markdown,
1252 					docs.map!(a => a.match!(
1253 						() => assert(false),
1254 						(string s) => s,
1255 						(MarkupContent s) => s.value,
1256 					)).join("\n\n"));
1257 			if (details.length)
1258 				ret.detail = details.map!(a => a.deref).join("\n");
1259 
1260 			if (labelDetails.length == 1)
1261 			{
1262 				ret.labelDetails = labelDetails[0];
1263 			}
1264 			else if (labelDetails.length > 1)
1265 			{
1266 				auto descriptions = labelDetails
1267 					.filter!(a => !a.deref.description.isNone)
1268 					.map!(a => a.deref.description.deref)
1269 					.array
1270 					.sort!"a<b"
1271 					.uniq
1272 					.array;
1273 				auto detailDetails = labelDetails
1274 					.filter!(a => !a.deref.detail.isNone)
1275 					.map!(a => a.deref.detail.deref)
1276 					.array
1277 					.sort!"a<b"
1278 					.uniq
1279 					.array;
1280 
1281 				CompletionItemLabelDetails detail;
1282 				if (descriptions.length == 1)
1283 					detail.description = descriptions[0];
1284 				else if (descriptions.length)
1285 					detail.description = descriptions.join(" | ");
1286 
1287 				if (detailDetails.length == 1)
1288 					detail.detail = detailDetails[0];
1289 				else if (detailDetails.length && detailDetails[0].endsWith(")"))
1290 					detail.detail = format!" (*%d overloads*)"(detailDetails.length);
1291 				else if (detailDetails.length) // dunno when/if this can even happen
1292 					detail.description = detailDetails.join(" |");
1293 
1294 				ret.labelDetails = detail;
1295 			}
1296 
1297 			migrateLabelDetailsSupport(ret);
1298 			return ret;
1299 		})
1300 		.array;
1301 }
1302 
1303 private void migrateLabelDetailsSupport(ref CompletionItem item)
1304 {
1305 	if (!capabilities
1306 		.textDocument.orDefault
1307 		.completion.orDefault
1308 		.completionItem.orDefault
1309 		.labelDetailsSupport.orDefault
1310 		&& !item.labelDetails.isNone)
1311 	{
1312 		// labelDetails is not supported, but let's use what we computed, it's
1313 		// still very useful
1314 		CompletionItemLabelDetails detail = item.labelDetails.deref;
1315 
1316 		// don't overwrite `detail`, it may be used to show full definition in a
1317 		// documentation popup.
1318 
1319 		// if we got a detailed detail, use that and properly set the insertText
1320 		if (!detail.detail.isNone)
1321 		{
1322 			if (item.insertText.isNone)
1323 				item.insertText = item.label;
1324 			item.label ~= detail.detail.deref;
1325 		}
1326 
1327 		item.labelDetails = item.labelDetails._void;
1328 	}
1329 }
1330 
1331 // === Protocol Notifications starting here ===
1332 
1333 /// Restarts all DCD servers started by this serve-d instance. Returns `true` once done.
1334 @protocolMethod("served/restartServer")
1335 bool restartServer()
1336 {
1337 	Future!void[] fut;
1338 	foreach (instance; backend.instances)
1339 		if (instance.has!DCDComponent)
1340 			fut ~= instance.get!DCDComponent.restartServer();
1341 	joinAll(fut);
1342 	return true;
1343 }
1344 
1345 /// Kills all DCD servers started by this serve-d instance.
1346 @protocolNotification("served/killServer")
1347 void killServer()
1348 {
1349 	foreach (instance; backend.instances)
1350 		if (instance.has!DCDComponent)
1351 			instance.get!DCDComponent.killServer();
1352 }
1353 
1354 /// Registers a snippet across the whole serve-d application which may be limited to given grammatical scopes.
1355 /// Requires `--provide context-snippets`
1356 /// Returns: `false` if SnippetsComponent hasn't been loaded yet, otherwise `true`.
1357 @protocolMethod("served/addDependencySnippet")
1358 bool addDependencySnippet(AddDependencySnippetParams params)
1359 {
1360 	if (!backend.has!SnippetsComponent)
1361 		return false;
1362 	PlainSnippet snippet;
1363 	foreach (i, ref v; snippet.tupleof)
1364 	{
1365 		static assert(__traits(identifier, snippet.tupleof[i]) == __traits(identifier,
1366 				params.snippet.tupleof[i]),
1367 				"struct definition changed without updating SerializablePlainSnippet");
1368 		// convert enums
1369 		v = cast(typeof(v)) params.snippet.tupleof[i];
1370 	}
1371 	backend.get!SnippetsComponent.addDependencySnippet(params.requiredDependencies, snippet);
1372 	return true;
1373 }