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