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.snippets;
13 import workspaced.coms;
14 
15 import std.algorithm : among, any, canFind, chunkBy, endsWith, filter, map, min,
16 	reverse, sort, startsWith, uniq;
17 import std.array : appender, array;
18 import std.conv : text, to;
19 import std.experimental.logger;
20 import std.json : JSONType, JSONValue;
21 import std..string : indexOf, join, lastIndexOf, lineSplitter, strip,
22 	stripLeft, stripRight, toLower;
23 import std.utf : decodeFront;
24 
25 import dparse.lexer : Token;
26 
27 import painlessjson : fromJSON, toJSON;
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 => CompletionItem(a.name,
385 				CompletionItemKind.field.opt, Optional!string.init,
386 				MarkupContent(a.documentation).opt, Optional!bool.init, Optional!bool.init,
387 				Optional!string.init, Optional!string.init, (a.name ~ '=').opt)).array);
388 		if (!line.length)
389 			return defaultList;
390 		if (line[0] == '[')
391 			return CompletionList(false, [
392 					CompletionItem("analysis.config.StaticAnalysisConfig",
393 						CompletionItemKind.keyword.opt),
394 					CompletionItem("analysis.config.ModuleFilters", CompletionItemKind.keyword.opt, Optional!string.init,
395 						MarkupContent("In this optional section a comma-separated list of inclusion and exclusion"
396 						~ " selectors can be specified for every check on which selective filtering"
397 						~ " should be applied. These given selectors match on the module name and"
398 						~ " partial matches (std. or .foo.) are possible. Moreover, every selectors"
399 						~ " must begin with either + (inclusion) or - (exclusion). Exclusion selectors"
400 						~ " take precedence over all inclusion operators.").opt)
401 					]);
402 		auto eqIndex = line.indexOf('=');
403 		auto quotIndex = line.lastIndexOf('"');
404 		if (quotIndex != -1 && params.position.character >= quotIndex)
405 			return CompletionList.init;
406 		if (params.position.character < eqIndex)
407 			return defaultList;
408 		else
409 			return CompletionList(false, [
410 					CompletionItem(`"disabled"`, CompletionItemKind.value.opt,
411 						"Check is disabled".opt),
412 					CompletionItem(`"enabled"`, CompletionItemKind.value.opt,
413 						"Check is enabled".opt),
414 					CompletionItem(`"skip-unittest"`, CompletionItemKind.value.opt,
415 						"Check is enabled but not operated in the unittests".opt)
416 					]);
417 	}
418 	else
419 	{
420 		if (!instance)
421 		{
422 			trace("Providing no completion because no instance");
423 			return CompletionList.init;
424 		}
425 
426 		if (document.getLanguageId == "d")
427 			return provideDSourceComplete(params, instance, document);
428 		else if (document.getLanguageId == "diet")
429 			return provideDietSourceComplete(params, instance, document);
430 		else if (document.getLanguageId == "dml")
431 			return provideDMLSourceComplete(params, instance, document);
432 		else
433 		{
434 			tracef("Providing no completion for unknown language ID %s.", document.getLanguageId);
435 			return CompletionList.init;
436 		}
437 	}
438 }
439 
440 CompletionList provideDMLSourceComplete(TextDocumentPositionParams params,
441 		WorkspaceD.Instance instance, ref Document document)
442 {
443 	import workspaced.com.dlangui : DlanguiComponent, CompletionType;
444 
445 	CompletionList ret;
446 
447 	auto items = backend.get!DlanguiComponent.complete(document.rawText,
448 			cast(int) document.positionToBytes(params.position)).getYield();
449 	ret.items.length = items.length;
450 	foreach (i, item; items)
451 	{
452 		CompletionItem translated;
453 
454 		translated.sortText = ((item.type == CompletionType.Class ? "1." : "0.") ~ item.value).opt;
455 		translated.label = item.value;
456 		if (item.documentation.length)
457 			translated.documentation = MarkupContent(item.documentation).opt;
458 		if (item.enumName.length)
459 			translated.detail = item.enumName.opt;
460 
461 		switch (item.type)
462 		{
463 		case CompletionType.Class:
464 			translated.insertTextFormat = InsertTextFormat.snippet;
465 			translated.insertText = item.value ~ ` {$0}`;
466 			break;
467 		case CompletionType.Color:
468 			translated.insertTextFormat = InsertTextFormat.snippet;
469 			translated.insertText = item.value ~ `: ${0:#000000}`;
470 			break;
471 		case CompletionType.String:
472 			translated.insertTextFormat = InsertTextFormat.snippet;
473 			translated.insertText = item.value ~ `: "$0"`;
474 			break;
475 		case CompletionType.EnumDefinition:
476 			translated.insertTextFormat = InsertTextFormat.plainText;
477 			translated.insertText = item.enumName ~ "." ~ item.value;
478 			break;
479 		case CompletionType.Rectangle:
480 		case CompletionType.Number:
481 			translated.insertTextFormat = InsertTextFormat.snippet;
482 			translated.insertText = item.value ~ `: ${0:0}`;
483 			break;
484 		case CompletionType.Keyword:
485 			// don't set, inherit from label
486 			break;
487 		default:
488 			translated.insertTextFormat = InsertTextFormat.plainText;
489 			translated.insertText = item.value ~ ": ";
490 			break;
491 		}
492 
493 		switch (item.type)
494 		{
495 		case CompletionType.Class:
496 			translated.kind = CompletionItemKind.class_;
497 			break;
498 		case CompletionType.String:
499 			translated.kind = CompletionItemKind.value;
500 			break;
501 		case CompletionType.Number:
502 			translated.kind = CompletionItemKind.value;
503 			break;
504 		case CompletionType.Color:
505 			translated.kind = CompletionItemKind.color;
506 			break;
507 		case CompletionType.EnumDefinition:
508 			translated.kind = CompletionItemKind.enum_;
509 			break;
510 		case CompletionType.EnumValue:
511 			translated.kind = CompletionItemKind.enumMember;
512 			break;
513 		case CompletionType.Rectangle:
514 			translated.kind = CompletionItemKind.typeParameter;
515 			break;
516 		case CompletionType.Boolean:
517 			translated.kind = CompletionItemKind.constant;
518 			break;
519 		case CompletionType.Keyword:
520 			translated.kind = CompletionItemKind.keyword;
521 			break;
522 		default:
523 		case CompletionType.Undefined:
524 			break;
525 		}
526 
527 		ret.items[i] = translated;
528 	}
529 
530 	return ret;
531 }
532 
533 CompletionList provideDietSourceComplete(TextDocumentPositionParams params,
534 		WorkspaceD.Instance instance, ref Document document)
535 {
536 	import served.utils.diet;
537 	import dc = dietc.complete;
538 
539 	auto completion = updateDietFile(document.uri.uriToFile, document.rawText.idup);
540 
541 	size_t offset = document.positionToBytes(params.position);
542 	auto raw = completion.completeAt(offset);
543 	CompletionItem[] ret;
544 
545 	if (raw is dc.Completion.completeD)
546 	{
547 		auto d = workspace(params.textDocument.uri).config.d;
548 		string code;
549 		contextExtractD(completion, offset, code, offset, d.dietContextCompletion);
550 		if (offset <= code.length && instance.has!DCDComponent)
551 		{
552 			info("DCD Completing Diet for ", code, " at ", offset);
553 			auto dcd = instance.get!DCDComponent.listCompletion(code, cast(int) offset).getYield;
554 			if (dcd.type == DCDCompletions.Type.identifiers)
555 			{
556 				ret = dcd.identifiers.convertDCDIdentifiers(d.argumentSnippets, d.completeNoDupes);
557 			}
558 		}
559 	}
560 	else
561 		ret = raw.map!((a) {
562 			CompletionItem ret;
563 			ret.label = a.text;
564 			ret.kind = a.type.mapToCompletionItemKind.opt;
565 			if (a.definition.length)
566 				ret.detail = a.definition.opt;
567 			if (a.documentation.length)
568 				ret.documentation = MarkupContent(a.documentation).opt;
569 			if (a.preselected)
570 				ret.preselect = true.opt;
571 			return ret;
572 		}).array;
573 
574 	return CompletionList(false, ret);
575 }
576 
577 CompletionList provideDSourceComplete(TextDocumentPositionParams params,
578 		WorkspaceD.Instance instance, ref Document document)
579 {
580 	auto lineRange = document.lineByteRangeAt(params.position.line);
581 	auto byteOff = cast(int) document.positionToBytes(params.position);
582 
583 	string line = document.rawText[lineRange[0] .. lineRange[1]].idup;
584 	string prefix = line[0 .. min($, params.position.character)].strip;
585 	CompletionItem[] completion;
586 	Token commentToken;
587 	if (document.rawText.isInComment(byteOff, backend, &commentToken))
588 		if (commentToken.text.startsWith("///", "/**", "/++"))
589 		{
590 			trace("Providing comment completion");
591 			int prefixLen = prefix[0] == '/' ? 3 : 1;
592 			auto remaining = prefix[prefixLen .. $].stripLeft;
593 
594 			foreach (compl; import("ddocs.txt").lineSplitter)
595 			{
596 				if (compl.startsWith(remaining))
597 				{
598 					auto item = CompletionItem(compl, CompletionItemKind.snippet.opt);
599 					item.insertText = compl ~ ": ";
600 					completion ~= item;
601 				}
602 			}
603 
604 			// make the comment one "line" so provide doc complete shows complete
605 			// after a /** */ comment block if you are on the first line.
606 			lineRange[1] = commentToken.index + commentToken.text.length;
607 			provideDocComplete(params, instance, document, completion, line, lineRange);
608 
609 			return CompletionList(false, completion);
610 		}
611 
612 	bool completeDCD = instance.has!DCDComponent;
613 	bool completeDoc = instance.has!DscannerComponent;
614 	bool completeSnippets = doCompleteSnippets && instance.has!SnippetsComponent;
615 
616 	tracef("Performing regular D comment completion (DCD=%s, Documentation=%s, Snippets=%s)",
617 			completeDCD, completeDoc, completeSnippets);
618 	const config = workspace(params.textDocument.uri).config;
619 	DCDCompletions result = DCDCompletions.empty;
620 	joinAll({
621 		if (completeDCD)
622 			result = instance.get!DCDComponent.listCompletion(document.rawText, byteOff).getYield;
623 	}, {
624 		if (completeDoc)
625 			provideDocComplete(params, instance, document, completion, line, lineRange);
626 	}, {
627 		if (completeSnippets)
628 			provideSnippetComplete(params, instance, document, config, completion, byteOff);
629 	});
630 
631 	if (completeDCD && result != DCDCompletions.init)
632 	{
633 		if (result.type == DCDCompletions.Type.identifiers)
634 		{
635 			auto d = config.d;
636 			completion ~= convertDCDIdentifiers(result.identifiers, d.argumentSnippets, d.completeNoDupes);
637 		}
638 		else if (result.type != DCDCompletions.Type.calltips)
639 		{
640 			trace("Unexpected result from DCD: ", result);
641 		}
642 	}
643 	return CompletionList(false, completion);
644 }
645 
646 private void provideDocComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance,
647 		ref Document document, ref CompletionItem[] completion, string line, size_t[2] lineRange)
648 {
649 	string lineStripped = line.strip;
650 	if (lineStripped.among!("", "/", "/*", "/+", "//", "///", "/**", "/++"))
651 	{
652 		auto defs = instance.get!DscannerComponent.listDefinitions(uriToFile(
653 				params.textDocument.uri), document.rawText[lineRange[1] .. $]).getYield;
654 		ptrdiff_t di = -1;
655 		FuncFinder: foreach (i, def; defs)
656 		{
657 			if (def.line >= 0 && def.line <= 5)
658 			{
659 				di = i;
660 				break FuncFinder;
661 			}
662 		}
663 		if (di == -1)
664 			return;
665 		auto def = defs[di];
666 		auto sig = "signature" in def.attributes;
667 		if (!sig)
668 		{
669 			CompletionItem doc = CompletionItem("///");
670 			doc.kind = CompletionItemKind.snippet;
671 			doc.insertTextFormat = InsertTextFormat.snippet;
672 			auto eol = document.eolAt(params.position.line).toString;
673 			doc.insertText = "/// ";
674 			CompletionItem doc2 = doc;
675 			CompletionItem doc3 = doc;
676 			doc2.label = "/**";
677 			doc2.insertText = "/** " ~ eol ~ " * $0" ~ eol ~ " */";
678 			doc3.label = "/++";
679 			doc3.insertText = "/++ " ~ eol ~ " * $0" ~ eol ~ " +/";
680 
681 			completion.addDocComplete(doc, lineStripped);
682 			completion.addDocComplete(doc2, lineStripped);
683 			completion.addDocComplete(doc3, lineStripped);
684 			return;
685 		}
686 		auto funcArgs = extractFunctionParameters(*sig);
687 		string[] docs = ["$0"];
688 		int argNo = 1;
689 		foreach (arg; funcArgs)
690 		{
691 			auto space = arg.stripRight.lastIndexOf(' ');
692 			if (space == -1)
693 				continue;
694 			auto identifier = arg[space + 1 .. $];
695 			if (!identifier.isValidDIdentifier)
696 				continue;
697 			if (argNo == 1)
698 				docs ~= "Params:";
699 			docs ~= text("  ", identifier, " = $", argNo.to!string);
700 			argNo++;
701 		}
702 		auto retAttr = "return" in def.attributes;
703 		if (retAttr && *retAttr != "void")
704 		{
705 			docs ~= "Returns: $" ~ argNo.to!string;
706 			argNo++;
707 		}
708 		auto depr = "deprecation" in def.attributes;
709 		if (depr)
710 		{
711 			docs ~= "Deprecated: $" ~ argNo.to!string ~ *depr;
712 			argNo++;
713 		}
714 		CompletionItem doc = CompletionItem("///");
715 		doc.kind = CompletionItemKind.snippet;
716 		doc.insertTextFormat = InsertTextFormat.snippet;
717 		auto eol = document.eolAt(params.position.line).toString;
718 		doc.insertText = docs.map!(a => "/// " ~ a).join(eol);
719 		CompletionItem doc2 = doc;
720 		CompletionItem doc3 = doc;
721 		doc2.label = "/**";
722 		doc2.insertText = "/** " ~ eol ~ docs.map!(a => " * " ~ a ~ eol).join() ~ " */";
723 		doc3.label = "/++";
724 		doc3.insertText = "/++ " ~ eol ~ docs.map!(a => " + " ~ a ~ eol).join() ~ " +/";
725 
726 		doc.sortText = opt(sortPrefixDoc ~ "0");
727 		doc2.sortText = opt(sortPrefixDoc ~ "1");
728 		doc3.sortText = opt(sortPrefixDoc ~ "2");
729 
730 		completion.addDocComplete(doc, lineStripped);
731 		completion.addDocComplete(doc2, lineStripped);
732 		completion.addDocComplete(doc3, lineStripped);
733 	}
734 }
735 
736 private void provideSnippetComplete(TextDocumentPositionParams params, WorkspaceD.Instance instance,
737 		ref Document document, ref const UserConfiguration config,
738 		ref CompletionItem[] completion, int byteOff)
739 {
740 	if (byteOff > 0 && document.rawText[byteOff - 1 .. $].startsWith("."))
741 		return; // no snippets after '.' character
742 
743 	auto snippets = instance.get!SnippetsComponent;
744 	auto ret = snippets.getSnippetsYield(document.uri.uriToFile, document.rawText, byteOff);
745 	trace("got ", ret.snippets.length, " snippets fitting in this context: ",
746 			ret.snippets.map!"a.shortcut");
747 	auto eol = document.eolAt(0);
748 	foreach (Snippet snippet; ret.snippets)
749 	{
750 		auto item = snippet.snippetToCompletionItem;
751 		item.data["level"] = JSONValue(ret.info.level.to!string);
752 		if (!snippet.unformatted)
753 			item.data["format"] = toJSON(generateDfmtArgs(config, eol));
754 		item.data["params"] = toJSON(params);
755 		completion ~= item;
756 	}
757 }
758 
759 private void addDocComplete(ref CompletionItem[] completion, CompletionItem doc, string prefix)
760 {
761 	if (!doc.label.startsWith(prefix))
762 		return;
763 	if (prefix.length > 0)
764 		doc.insertText = doc.insertText[prefix.length .. $];
765 	completion ~= doc;
766 }
767 
768 private bool isInComment(scope const(char)[] code, size_t at, WorkspaceD backend, Token* outToken = null)
769 {
770 	if (!backend)
771 		return false;
772 
773 	import dparse.lexer : DLexer, LexerConfig, StringBehavior, tok;
774 
775 	// TODO: does this kind of token parsing belong in serve-d?
776 
777 	LexerConfig config;
778 	config.fileName = "stdin";
779 	config.stringBehavior = StringBehavior.source;
780 	auto lexer = DLexer(code, config, &backend.stringCache);
781 
782 	while (!lexer.empty) switch (lexer.front.type)
783 	{
784 	case tok!"comment":
785 		auto t = lexer.front;
786 
787 		auto commentEnd = t.index + t.text.length;
788 		if (t.text.startsWith("//"))
789 			commentEnd++;
790 
791 		if (t.index <= at && at < commentEnd)
792 		{
793 			if (outToken !is null)
794 				*outToken = t;
795 			return true;
796 		}
797 
798 		lexer.popFront();
799 		break;
800 	case tok!"__EOF__":
801 		return false;
802 	default:
803 		lexer.popFront();
804 		break;
805 	}
806 	return false;
807 }
808 
809 @protocolMethod("completionItem/resolve")
810 CompletionItem resolveCompletionItem(CompletionItem item)
811 {
812 	auto data = item.data;
813 
814 	if (item.insertTextFormat.get == InsertTextFormat.snippet
815 			&& item.kind.get == CompletionItemKind.snippet && data.type == JSONType.object)
816 	{
817 		const resolved = "resolved" in data.object;
818 		if (resolved.type != JSONType.true_)
819 		{
820 			TextDocumentPositionParams params = data.object["params"]
821 				.fromJSON!TextDocumentPositionParams;
822 
823 			Document document = documents[params.textDocument.uri];
824 			auto f = document.uri.uriToFile;
825 			auto instance = backend.getBestInstance(f);
826 
827 			if (instance.has!SnippetsComponent)
828 			{
829 				auto snippets = instance.get!SnippetsComponent;
830 				auto snippet = snippetFromCompletionItem(item);
831 				snippet = snippets.resolveSnippet(f, document.rawText,
832 						cast(int) document.positionToBytes(params.position), snippet).getYield;
833 				item = snippetToCompletionItem(snippet);
834 			}
835 		}
836 
837 		if (const format = "format" in data.object)
838 		{
839 			auto args = (*format).fromJSON!(string[]);
840 			if (item.insertTextFormat.get == InsertTextFormat.snippet)
841 			{
842 				SnippetLevel level = SnippetLevel.global;
843 				if (const levelStr = "level" in data.object)
844 					level = levelStr.str.to!SnippetLevel;
845 				item.insertText = formatSnippet(item.insertText.get, args, level).opt;
846 			}
847 			else
848 			{
849 				item.insertText = formatCode(item.insertText.get, args).opt;
850 			}
851 		}
852 
853 		// TODO: format code
854 		return item;
855 	}
856 	else
857 	{
858 		return item;
859 	}
860 }
861 
862 CompletionItem snippetToCompletionItem(Snippet snippet)
863 {
864 	CompletionItem item;
865 	item.label = snippet.shortcut;
866 	item.sortText = opt(sortPrefixSnippets ~ snippet.shortcut);
867 	item.detail = snippet.title.opt;
868 	item.kind = CompletionItemKind.snippet.opt;
869 	item.documentation = MarkupContent(MarkupKind.markdown,
870 			snippet.documentation ~ "\n\n```d\n" ~ snippet.snippet ~ "\n```\n");
871 	item.filterText = snippet.shortcut.opt;
872 	if (capabilities.textDocument.completion.completionItem.snippetSupport)
873 	{
874 		item.insertText = snippet.snippet.opt;
875 		item.insertTextFormat = InsertTextFormat.snippet.opt;
876 	}
877 	else
878 		item.insertText = snippet.plain.opt;
879 
880 	item.data = JSONValue([
881 			"resolved": JSONValue(snippet.resolved),
882 			"id": JSONValue(snippet.id),
883 			"providerId": JSONValue(snippet.providerId),
884 			"data": snippet.data
885 			]);
886 	return item;
887 }
888 
889 Snippet snippetFromCompletionItem(CompletionItem item)
890 {
891 	Snippet snippet;
892 	snippet.shortcut = item.label;
893 	snippet.title = item.detail.get;
894 	snippet.documentation = item.documentation.get.value;
895 	auto end = snippet.documentation.lastIndexOf("\n\n```d\n");
896 	if (end != -1)
897 		snippet.documentation = snippet.documentation[0 .. end];
898 
899 	if (capabilities.textDocument.completion.completionItem.snippetSupport)
900 		snippet.snippet = item.insertText.get;
901 	else
902 		snippet.plain = item.insertText.get;
903 
904 	snippet.resolved = item.data["resolved"].boolean;
905 	snippet.id = item.data["id"].str;
906 	snippet.providerId = item.data["providerId"].str;
907 	snippet.data = item.data["data"];
908 	return snippet;
909 }
910 
911 unittest
912 {
913 	auto backend = new WorkspaceD();
914 	assert(isInComment(`hello /** world`, 10, backend));
915 	assert(!isInComment(`hello /** world`, 3, backend));
916 	assert(isInComment(`hello /* world */ bar`, 8, backend));
917 	assert(isInComment(`hello /* world */ bar`, 16, backend));
918 	assert(!isInComment(`hello /* world */ bar`, 17, backend));
919 	assert(!isInComment("int x;\n// line comment\n", 6, backend));
920 	assert(isInComment("int x;\n// line comment\n", 7, backend));
921 	assert(isInComment("int x;\n// line comment\n", 9, backend));
922 	assert(isInComment("int x;\n// line comment\n", 21, backend));
923 	assert(isInComment("int x;\n// line comment\n", 22, backend));
924 	assert(!isInComment("int x;\n// line comment\n", 23, backend));
925 }
926 
927 auto convertDCDIdentifiers(DCDIdentifier[] identifiers, bool argumentSnippets, bool completeNoDupes)
928 {
929 	CompletionItem[] completion;
930 	foreach (identifier; identifiers)
931 	{
932 		CompletionItem item;
933 		item.label = identifier.identifier;
934 		item.kind = identifier.type.convertFromDCDType;
935 		if (identifier.documentation.length)
936 			item.documentation = MarkupContent(identifier.documentation.ddocToMarked);
937 		if (identifier.definition.length)
938 		{
939 			item.detail = identifier.definition;
940 			if (!completeNoDupes)
941 				item.sortText = identifier.definition;
942 			// TODO: only add arguments when this is a function call, eg not on template arguments
943 			if (identifier.type == "f" && argumentSnippets)
944 			{
945 				item.insertTextFormat = InsertTextFormat.snippet;
946 				string args;
947 				auto parts = identifier.definition.extractFunctionParameters;
948 				if (parts.length)
949 				{
950 					int numRequired;
951 					foreach (i, part; parts)
952 					{
953 						ptrdiff_t equals = part.indexOf('=');
954 						if (equals != -1)
955 						{
956 							part = part[0 .. equals].stripRight;
957 							// remove default value from autocomplete
958 						}
959 						auto space = part.lastIndexOf(' ');
960 						if (space != -1)
961 							part = part[space + 1 .. $];
962 
963 						if (args.length)
964 							args ~= ", ";
965 						args ~= "${" ~ (i + 1).to!string ~ ":" ~ part ~ "}";
966 						numRequired++;
967 					}
968 					item.insertText = identifier.identifier ~ "(${0:" ~ args ~ "})";
969 				}
970 			}
971 		}
972 
973 		if (item.sortText.isNull)
974 			item.sortText = item.label.opt;
975 
976 		item.sortText = opt(sortPrefixDCD ~ identifier.type.sortFromDCDType ~ item.sortText.get);
977 
978 		completion ~= item;
979 	}
980 
981 	// sort only for duplicate detection (use sortText for UI sorting)
982 	completion.sort!"a.label < b.label";
983 	if (completeNoDupes)
984 		return completion.chunkBy!((a, b) => a.label == b.label && a.kind == b.kind)
985 			.map!((a) {
986 				CompletionItem ret = a.front;
987 				auto details = a.map!"a.detail"
988 					.filter!"!a.isNull && a.value.length"
989 					.uniq
990 					.array;
991 				auto docs = a.map!"a.documentation"
992 					.filter!"!a.isNull && a.value.value.length"
993 					.uniq
994 					.array;
995 				if (docs.length)
996 					ret.documentation = MarkupContent(MarkupKind.markdown,
997 						docs.map!"a.value.value".join("\n\n"));
998 				if (details.length)
999 					ret.detail = details.map!"a.value".join("\n");
1000 				return ret;
1001 			})
1002 			.array;
1003 	else
1004 		return completion.chunkBy!((a, b) => a.label == b.label && a.detail == b.detail
1005 				&& a.kind == b.kind)
1006 			.map!((a) {
1007 				CompletionItem ret = a.front;
1008 				auto docs = a.map!"a.documentation"
1009 					.filter!"!a.isNull && a.value.value.length"
1010 					.uniq
1011 					.array;
1012 				if (docs.length)
1013 					ret.documentation = MarkupContent(MarkupKind.markdown,
1014 						docs.map!"a.value.value".join("\n\n"));
1015 				return ret;
1016 			})
1017 			.array;
1018 }
1019 
1020 // === Protocol Notifications starting here ===
1021 
1022 /// Restarts all DCD servers started by this serve-d instance. Returns `true` once done.
1023 @protocolMethod("served/restartServer")
1024 bool restartServer()
1025 {
1026 	Future!void[] fut;
1027 	foreach (instance; backend.instances)
1028 		if (instance.has!DCDComponent)
1029 			fut ~= instance.get!DCDComponent.restartServer();
1030 	joinAll(fut);
1031 	return true;
1032 }
1033 
1034 /// Kills all DCD servers started by this serve-d instance.
1035 @protocolNotification("served/killServer")
1036 void killServer()
1037 {
1038 	foreach (instance; backend.instances)
1039 		if (instance.has!DCDComponent)
1040 			instance.get!DCDComponent.killServer();
1041 }
1042 
1043 /// Registers a snippet across the whole serve-d application which may be limited to given grammatical scopes.
1044 /// Requires `--provide context-snippets`
1045 /// Returns: `false` if SnippetsComponent hasn't been loaded yet, otherwise `true`.
1046 @protocolMethod("served/addDependencySnippet")
1047 bool addDependencySnippet(AddDependencySnippetParams params)
1048 {
1049 	if (!backend.has!SnippetsComponent)
1050 		return false;
1051 	PlainSnippet snippet;
1052 	foreach (i, ref v; snippet.tupleof)
1053 	{
1054 		static assert(__traits(identifier, snippet.tupleof[i]) == __traits(identifier,
1055 				params.snippet.tupleof[i]),
1056 				"struct definition changed without updating SerializablePlainSnippet");
1057 		// convert enums
1058 		v = cast(typeof(v)) params.snippet.tupleof[i];
1059 	}
1060 	backend.get!SnippetsComponent.addDependencySnippet(params.requiredDependencies, snippet);
1061 	return true;
1062 }