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