1 /**
2  * This file is part of DCD, a development tool for the D programming language.
3  * Copyright (C) 2014 Brian Schott
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 module dcd.server.autocomplete.complete;
20 
21 import std.algorithm;
22 import std.array;
23 import std.conv;
24 import std.experimental.logger;
25 import std.file;
26 import std.path;
27 import std.string;
28 import std.typecons;
29 
30 import dcd.server.autocomplete.util;
31 
32 import dparse.lexer;
33 import dparse.rollback_allocator;
34 
35 import dsymbol.builtin.names;
36 import dsymbol.builtin.symbols;
37 import dsymbol.conversion;
38 import dsymbol.modulecache;
39 import dsymbol.scope_;
40 import dsymbol.string_interning;
41 import dsymbol.symbol;
42 
43 import dcd.common.constants;
44 import dcd.common.messages;
45 
46 /**
47  * Handles autocompletion
48  * Params:
49  *     request = the autocompletion request
50  * Returns:
51  *     the autocompletion response
52  */
53 public AutocompleteResponse complete(const AutocompleteRequest request,
54 	ref ModuleCache moduleCache)
55 {
56 	const(Token)[] tokenArray;
57 	auto stringCache = StringCache(StringCache.defaultBucketCount);
58 	auto beforeTokens = getTokensBeforeCursor(request.sourceCode,
59 		request.cursorPosition, stringCache, tokenArray);
60 
61 	// allows to get completion on keyword, typically "is"
62 	if (beforeTokens.length && isKeyword(beforeTokens[$-1].type))
63 	{
64 		Token* fakeIdent = cast(Token*) (&beforeTokens[$-1]);
65 		fakeIdent.text = str(fakeIdent.type);
66 		fakeIdent.type = tok!"identifier";
67 	}
68 
69 	if (beforeTokens.length >= 2)
70 	{
71 		if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"["
72 			|| beforeTokens[$ - 1] == tok!",")
73 		{
74 			immutable size_t end = goBackToOpenParen(beforeTokens);
75 			if (end != size_t.max)
76 				return parenCompletion(beforeTokens[0 .. end], tokenArray,
77 					request.cursorPosition, moduleCache);
78 		}
79 		else
80 		{
81 			ImportKind kind = determineImportKind(beforeTokens);
82 			if (kind == ImportKind.neither)
83 			{
84 				if (beforeTokens.isUdaExpression)
85 					beforeTokens = beforeTokens[$-1 .. $];
86 				return dotCompletion(beforeTokens, tokenArray, request.cursorPosition,
87 					moduleCache);
88 			}
89 			else
90 				return importCompletion(beforeTokens, kind, moduleCache);
91 		}
92 	}
93 	return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache);
94 }
95 
96 /**
97  * Handles dot completion for identifiers and types.
98  * Params:
99  *     beforeTokens = the tokens before the cursor
100  *     tokenArray = all tokens in the file
101  *     cursorPosition = the cursor position in bytes
102  * Returns:
103  *     the autocompletion response
104  */
105 AutocompleteResponse dotCompletion(T)(T beforeTokens, const(Token)[] tokenArray,
106 	size_t cursorPosition, ref ModuleCache moduleCache)
107 {
108 	AutocompleteResponse response;
109 
110 	// Partial symbol name appearing after the dot character and before the
111 	// cursor.
112 	string partial;
113 
114 	// Type of the token before the dot, or identifier if the cursor was at
115 	// an identifier.
116 	IdType significantTokenType;
117 
118 	if (beforeTokens.length >= 1 && beforeTokens[$ - 1] == tok!"identifier")
119 	{
120 		// Set partial to the slice of the identifier between the beginning
121 		// of the identifier and the cursor. This improves the completion
122 		// responses when the cursor is in the middle of an identifier instead
123 		// of at the end
124 		auto t = beforeTokens[$ - 1];
125 		if (cursorPosition - t.index >= 0 && cursorPosition - t.index <= t.text.length)
126 		{
127 			partial = t.text[0 .. cursorPosition - t.index];
128 			// issue 442 - prevent `partial` to start in the middle of a MBC
129 			// since later there's a non-nothrow call to `toUpper`
130 			import std.utf : validate, UTFException;
131 			try validate(partial);
132 			catch (UTFException)
133 			{
134 				import std.experimental.logger : warning;
135 				warning("cursor positioned within a UTF sequence");
136 				partial = "";
137 			}
138 		}
139 		significantTokenType = partial.length ? tok!"identifier" : tok!"";
140 		beforeTokens = beforeTokens[0 .. $ - 1];
141 	}
142 	else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".")
143 		significantTokenType = beforeTokens[$ - 2].type;
144 	else
145 		return response;
146 
147 	switch (significantTokenType)
148 	{
149 	mixin(STRING_LITERAL_CASES);
150 		foreach (symbol; arraySymbols)
151 			response.completions ~= makeSymbolCompletionInfo(symbol, symbol.kind);
152 		response.completionType = CompletionType.identifiers;
153 		break;
154 	mixin(TYPE_IDENT_CASES);
155 	case tok!")":
156 	case tok!"]":
157 		auto allocator = scoped!(ASTAllocator)();
158 		RollbackAllocator rba;
159 		ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator,
160 			&rba, cursorPosition, moduleCache);
161 		scope(exit) pair.destroy();
162 		response.setCompletions(pair.scope_, getExpression(beforeTokens),
163 			cursorPosition, CompletionType.identifiers, false, partial);
164 		break;
165 	case tok!"(":
166 	case tok!"{":
167 	case tok!"[":
168 	case tok!";":
169 	case tok!":":
170 		break;
171 	default:
172 		break;
173 	}
174 	return response;
175 }
176 
177 /**
178  * Handles paren completion for function calls and some keywords
179  * Params:
180  *     beforeTokens = the tokens before the cursor
181  *     tokenArray = all tokens in the file
182  *     cursorPosition = the cursor position in bytes
183  * Returns:
184  *     the autocompletion response
185  */
186 AutocompleteResponse parenCompletion(T)(T beforeTokens,
187 	const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache)
188 {
189 	AutocompleteResponse response;
190 	immutable(ConstantCompletion)[] completions;
191 	switch (beforeTokens[$ - 2].type)
192 	{
193 	case tok!"__traits":
194 		completions = traits;
195 		goto fillResponse;
196 	case tok!"scope":
197 		completions = scopes;
198 		goto fillResponse;
199 	case tok!"version":
200 		completions = predefinedVersions;
201 		goto fillResponse;
202 	case tok!"extern":
203 		completions = linkages;
204 		goto fillResponse;
205 	case tok!"pragma":
206 		completions = pragmas;
207 	fillResponse:
208 		response.completionType = CompletionType.identifiers;
209 		foreach (completion; completions)
210 		{
211 			response.completions ~= AutocompleteResponse.Completion(
212 				completion.identifier,
213 				CompletionKind.keyword,
214 				null, null, 0, // definition, symbol path+location
215 				completion.ddoc
216 			);
217 		}
218 		break;
219 	case tok!"characterLiteral":
220 	case tok!"doubleLiteral":
221 	case tok!"floatLiteral":
222 	case tok!"identifier":
223 	case tok!"idoubleLiteral":
224 	case tok!"ifloatLiteral":
225 	case tok!"intLiteral":
226 	case tok!"irealLiteral":
227 	case tok!"longLiteral":
228 	case tok!"realLiteral":
229 	case tok!"uintLiteral":
230 	case tok!"ulongLiteral":
231 	case tok!"this":
232 	case tok!"super":
233 	case tok!")":
234 	case tok!"]":
235 	mixin(STRING_LITERAL_CASES);
236 		auto allocator = scoped!(ASTAllocator)();
237 		RollbackAllocator rba;
238 		ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator,
239 			&rba, cursorPosition, moduleCache);
240 		scope(exit) pair.destroy();
241 		auto expression = getExpression(beforeTokens[0 .. $ - 1]);
242 		response.setCompletions(pair.scope_, expression,
243 			cursorPosition, CompletionType.calltips, beforeTokens[$ - 1] == tok!"[");
244 		break;
245 	default:
246 		break;
247 	}
248 	return response;
249 }
250 
251 /**
252  * Provides autocomplete for selective imports, e.g.:
253  * ---
254  * import std.algorithm: balancedParens;
255  * ---
256  */
257 AutocompleteResponse importCompletion(T)(T beforeTokens, ImportKind kind,
258 	ref ModuleCache moduleCache)
259 in
260 {
261 	assert (beforeTokens.length >= 2);
262 }
263 body
264 {
265 	AutocompleteResponse response;
266 	if (beforeTokens.length <= 2)
267 		return response;
268 
269 	size_t i = beforeTokens.length - 1;
270 
271 	if (kind == ImportKind.normal)
272 	{
273 
274 		while (beforeTokens[i].type != tok!"," && beforeTokens[i].type != tok!"import"
275 				&& beforeTokens[i].type != tok!"=" )
276 			i--;
277 		setImportCompletions(beforeTokens[i .. $], response, moduleCache);
278 		return response;
279 	}
280 
281 	loop: while (true) switch (beforeTokens[i].type)
282 	{
283 	case tok!"identifier":
284 	case tok!"=":
285 	case tok!",":
286 	case tok!".":
287 		i--;
288 		break;
289 	case tok!":":
290 		i--;
291 		while (beforeTokens[i].type == tok!"identifier" || beforeTokens[i].type == tok!".")
292 			i--;
293 		break loop;
294 	default:
295 		break loop;
296 	}
297 
298 	size_t j = i;
299 	loop2: while (j <= beforeTokens.length) switch (beforeTokens[j].type)
300 	{
301 	case tok!":": break loop2;
302 	default: j++; break;
303 	}
304 
305 	if (i >= j)
306 	{
307 		warning("Malformed import statement");
308 		return response;
309 	}
310 
311 	immutable string path = beforeTokens[i + 1 .. j]
312 		.filter!(token => token.type == tok!"identifier")
313 		.map!(token => cast() token.text)
314 		.joiner(dirSeparator)
315 		.text();
316 
317 	string resolvedLocation = moduleCache.resolveImportLocation(path);
318 	if (resolvedLocation is null)
319 	{
320 		warning("Could not resolve location of ", path);
321 		return response;
322 	}
323 	auto symbols = moduleCache.getModuleSymbol(internString(resolvedLocation));
324 
325 	import containers.hashset : HashSet;
326 	HashSet!string h;
327 
328 	void addSymbolToResponses(const(DSymbol)* sy)
329 	{
330 		auto a = DSymbol(sy.name);
331 		if (!builtinSymbols.contains(&a) && sy.name !is null && !h.contains(sy.name)
332 				&& !sy.skipOver && sy.name != CONSTRUCTOR_SYMBOL_NAME
333 				&& isPublicCompletionKind(sy.kind))
334 		{
335 			response.completions ~= makeSymbolCompletionInfo(sy, sy.kind);
336 			h.insert(sy.name);
337 		}
338 	}
339 
340 	foreach (s; symbols.opSlice().filter!(a => !a.skipOver))
341 	{
342 		if (s.kind == CompletionKind.importSymbol && s.type !is null)
343 			foreach (sy; s.type.opSlice().filter!(a => !a.skipOver))
344 				addSymbolToResponses(sy);
345 		else
346 			addSymbolToResponses(s);
347 	}
348 	response.completionType = CompletionType.identifiers;
349 	return response;
350 }
351 
352 /**
353  * Populates the response with completion information for an import statement
354  * Params:
355  *     tokens = the tokens after the "import" keyword and before the cursor
356  *     response = the response that should be populated
357  */
358 void setImportCompletions(T)(T tokens, ref AutocompleteResponse response,
359 	ref ModuleCache cache)
360 {
361 	response.completionType = CompletionType.identifiers;
362 	string partial = null;
363 	if (tokens[$ - 1].type == tok!"identifier")
364 	{
365 		partial = tokens[$ - 1].text;
366 		tokens = tokens[0 .. $ - 1];
367 	}
368 	auto moduleParts = tokens.filter!(a => a.type == tok!"identifier").map!("a.text").array();
369 	string path = buildPath(moduleParts);
370 
371 	bool found = false;
372 
373 	foreach (importPath; cache.getImportPaths())
374 	{
375 		if (importPath.isFile)
376 		{
377 			if (!exists(importPath))
378 				continue;
379 
380 			found = true;
381 
382 			auto n = importPath.baseName(".d").baseName(".di");
383 			if (isFile(importPath) && (importPath.endsWith(".d") || importPath.endsWith(".di"))
384 					&& (partial is null || n.startsWith(partial)))
385 				response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, importPath, 0);
386 		}
387 		else
388 		{
389 			string p = buildPath(importPath, path);
390 			if (!exists(p))
391 				continue;
392 
393 			found = true;
394 
395 			try foreach (string name; dirEntries(p, SpanMode.shallow))
396 			{
397 				import std.path: baseName;
398 				if (name.baseName.startsWith(".#"))
399 					continue;
400 
401 				auto n = name.baseName(".d").baseName(".di");
402 				if (isFile(name) && (name.endsWith(".d") || name.endsWith(".di"))
403 					&& (partial is null || n.startsWith(partial)))
404 					response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, name, 0);
405 				else if (isDir(name))
406 				{
407 					if (n[0] != '.' && (partial is null || n.startsWith(partial)))
408 					{
409 						immutable packageDPath = buildPath(name, "package.d");
410 						immutable packageDIPath = buildPath(name, "package.di");
411 						immutable packageD = exists(packageDPath);
412 						immutable packageDI = exists(packageDIPath);
413 						immutable kind = packageD || packageDI ? CompletionKind.moduleName : CompletionKind.packageName;
414 						immutable file = packageD ? packageDPath : packageDI ? packageDIPath : name;
415 						response.completions ~= AutocompleteResponse.Completion(n, kind, null, file, 0);
416 					}
417 				}
418 			}
419 			catch(FileException)
420 			{
421 				warning("Cannot access import path: ", importPath);
422 			}
423 		}
424 	}
425 	if (!found)
426 		warning("Could not find ", moduleParts);
427 }
428 
429 /**
430  *
431  */
432 void setCompletions(T)(ref AutocompleteResponse response,
433 	Scope* completionScope, T tokens, size_t cursorPosition,
434 	CompletionType completionType, bool isBracket = false, string partial = null)
435 {
436 	static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, string p,
437 		size_t[] circularGuard = [])
438 	{
439 		if (circularGuard.canFind(cast(size_t) s))
440 			return;
441 		foreach (sym; s.opSlice())
442 		{
443 			if (sym.name !is null && sym.name.length > 0 && isPublicCompletionKind(sym.kind)
444 				&& (p is null ? true : toUpper(sym.name.data).startsWith(toUpper(p)))
445 				&& !r.completions.canFind!(a => a.identifier == sym.name)
446 				&& sym.name[0] != '*')
447 			{
448 				r.completions ~= makeSymbolCompletionInfo(sym, sym.kind);
449 			}
450 			if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null)
451 				addSymToResponse(sym.type, r, p, circularGuard ~ (cast(size_t) s));
452 		}
453 	}
454 
455 	// Handle the simple case where we get all symbols in scope and filter it
456 	// based on the currently entered text.
457 	if (partial !is null && tokens.length == 0)
458 	{
459 		auto currentSymbols = completionScope.getSymbolsInCursorScope(cursorPosition);
460 		foreach (s; currentSymbols.filter!(a => isPublicCompletionKind(a.kind)
461 				&& toUpper(a.name.data).startsWith(toUpper(partial))))
462 		{
463 			response.completions ~= makeSymbolCompletionInfo(s, s.kind);
464 		}
465 		response.completionType = CompletionType.identifiers;
466 		return;
467 	}
468 
469 	if (tokens.length == 0)
470 		return;
471 
472 	DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens,
473 		cursorPosition, completionType);
474 
475 	if (symbols.length == 0)
476 		return;
477 
478 	if (completionType == CompletionType.identifiers)
479 	{
480 		while (symbols[0].qualifier == SymbolQualifier.func
481 				|| symbols[0].kind == CompletionKind.functionName
482 				|| symbols[0].kind == CompletionKind.importSymbol
483 				|| symbols[0].kind == CompletionKind.aliasName)
484 		{
485 			symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? []
486 				: [symbols[0].type];
487 			if (symbols.length == 0)
488 				return;
489 		}
490 		addSymToResponse(symbols[0], response, partial);
491 		response.completionType = CompletionType.identifiers;
492 	}
493 	else if (completionType == CompletionType.calltips)
494 	{
495 		//trace("Showing call tips for ", symbols[0].name, " of kind ", symbols[0].kind);
496 		if (symbols[0].kind != CompletionKind.functionName
497 			&& symbols[0].callTip is null)
498 		{
499 			if (symbols[0].kind == CompletionKind.aliasName)
500 			{
501 				if (symbols[0].type is null || symbols[0].type is symbols[0])
502 					return;
503 				symbols = [symbols[0].type];
504 			}
505 			if (symbols[0].kind == CompletionKind.variableName)
506 			{
507 				auto dumb = symbols[0].type;
508 				if (dumb !is null)
509 				{
510 					if (dumb.kind == CompletionKind.functionName)
511 					{
512 						symbols = [dumb];
513 						goto setCallTips;
514 					}
515 					if (isBracket)
516 					{
517 						auto index = dumb.getPartsByName(internString("opIndex"));
518 						if (index.length > 0)
519 						{
520 							symbols = index;
521 							goto setCallTips;
522 						}
523 					}
524 					auto call = dumb.getPartsByName(internString("opCall"));
525 					if (call.length > 0)
526 					{
527 						symbols = call;
528 						goto setCallTips;
529 					}
530 				}
531 			}
532 			if (symbols[0].kind == CompletionKind.structName
533 				|| symbols[0].kind == CompletionKind.className)
534 			{
535 				auto constructor = symbols[0].getPartsByName(CONSTRUCTOR_SYMBOL_NAME);
536 				if (constructor.length == 0)
537 				{
538 					// Build a call tip out of the struct fields
539 					if (symbols[0].kind == CompletionKind.structName)
540 					{
541 						response.completionType = CompletionType.calltips;
542 						response.completions = [generateStructConstructorCalltip(symbols[0])];
543 						return;
544 					}
545 				}
546 				else
547 				{
548 					symbols = constructor;
549 					goto setCallTips;
550 				}
551 			}
552 		}
553 	setCallTips:
554 		response.completionType = CompletionType.calltips;
555 		foreach (symbol; symbols)
556 		{
557 			if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null)
558 			{
559 				auto completion = makeSymbolCompletionInfo(symbol, char.init);
560 				// TODO: put return type
561 				response.completions ~= completion;
562 			}
563 		}
564 	}
565 }
566 
567 AutocompleteResponse.Completion generateStructConstructorCalltip(const DSymbol* symbol)
568 in
569 {
570 	assert(symbol.kind == CompletionKind.structName);
571 }
572 body
573 {
574 	string generatedStructConstructorCalltip = "this(";
575 	const(DSymbol)*[] fields = symbol.opSlice().filter!(
576 		a => a.kind == CompletionKind.variableName).map!(a => cast(const(DSymbol)*) a).array();
577 	fields.sort!((a, b) => a.location < b.location);
578 	foreach (i, field; fields)
579 	{
580 		if (field.kind != CompletionKind.variableName)
581 			continue;
582 		i++;
583 		if (field.type !is null)
584 		{
585 			generatedStructConstructorCalltip ~= field.type.name;
586 			generatedStructConstructorCalltip ~= " ";
587 		}
588 		generatedStructConstructorCalltip ~= field.name;
589 		if (i < fields.length)
590 			generatedStructConstructorCalltip ~= ", ";
591 	}
592 	generatedStructConstructorCalltip ~= ")";
593 	auto completion = makeSymbolCompletionInfo(symbol, char.init);
594 	completion.identifier = "this";
595 	completion.definition = generatedStructConstructorCalltip;
596 	return completion;
597 }