792 lines
24 KiB
D
792 lines
24 KiB
D
/**
|
|
* This file is part of DCD, a development tool for the D programming language.
|
|
* Copyright (C) 2014 Brian Schott
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
module dcd.server.autocomplete.complete;
|
|
|
|
import std.algorithm;
|
|
import std.array;
|
|
import std.conv;
|
|
import std.experimental.allocator;
|
|
import std.experimental.logger;
|
|
import std.file;
|
|
import std.path;
|
|
import std.range : assumeSorted;
|
|
import std.string;
|
|
import std.typecons;
|
|
import std.exception : enforce;
|
|
|
|
import dcd.server.autocomplete.util;
|
|
|
|
import dparse.lexer;
|
|
import dparse.rollback_allocator;
|
|
|
|
import dsymbol.builtin.names;
|
|
import dsymbol.builtin.symbols;
|
|
import dsymbol.conversion;
|
|
import dsymbol.modulecache;
|
|
import dsymbol.scope_;
|
|
import dsymbol.string_interning;
|
|
import dsymbol.symbol;
|
|
import dsymbol.ufcs;
|
|
import dsymbol.utils;
|
|
|
|
import dcd.common.constants;
|
|
import dcd.common.messages;
|
|
|
|
enum CalltipHint {
|
|
none, // asserts false if passed into setCompletion with CompletionType.calltips
|
|
regularArguments,
|
|
templateArguments,
|
|
indexOperator,
|
|
}
|
|
|
|
/**
|
|
* Handles autocompletion
|
|
* Params:
|
|
* request = the autocompletion request
|
|
* Returns:
|
|
* the autocompletion response
|
|
*/
|
|
public AutocompleteResponse complete(const AutocompleteRequest request,
|
|
ref ModuleCache moduleCache)
|
|
{
|
|
const(Token)[] tokenArray;
|
|
auto stringCache = StringCache(request.sourceCode.length.optimalBucketCount);
|
|
auto beforeTokens = getTokensBeforeCursor(request.sourceCode,
|
|
request.cursorPosition, stringCache, tokenArray);
|
|
|
|
// allows to get completion on keyword, typically "is"
|
|
if (beforeTokens.length &&
|
|
(isKeyword(beforeTokens[$-1].type) || isBasicType(beforeTokens[$-1].type)))
|
|
{
|
|
Token* fakeIdent = cast(Token*) (&beforeTokens[$-1]);
|
|
fakeIdent.text = str(fakeIdent.type);
|
|
fakeIdent.type = tok!"identifier";
|
|
}
|
|
|
|
const bool dotId = beforeTokens.length >= 2 &&
|
|
beforeTokens[$-1] == tok!"identifier" && beforeTokens[$-2] == tok!".";
|
|
|
|
// detects if the completion request uses the current module `ModuleDeclaration`
|
|
// as access chain. In this case removes this access chain, and just keep the dot
|
|
// because within a module semantic is the same (`myModule.stuff` -> `.stuff`).
|
|
if (tokenArray.length >= 3 && tokenArray[0] == tok!"module" && beforeTokens.length &&
|
|
(beforeTokens[$-1] == tok!"." || dotId))
|
|
{
|
|
const moduleDeclEndIndex = tokenArray.countUntil!(a => a.type == tok!";");
|
|
bool beginsWithModuleName;
|
|
// enough room for the module decl and the fqn...
|
|
if (moduleDeclEndIndex != -1 && beforeTokens.length >= moduleDeclEndIndex * 2)
|
|
foreach (immutable i; 0 .. moduleDeclEndIndex)
|
|
{
|
|
const expectIdt = bool(i & 1);
|
|
const expectDot = !expectIdt;
|
|
const j = beforeTokens.length - moduleDeclEndIndex + i - 1 - ubyte(dotId);
|
|
|
|
// verify that the chain is well located after an expr or a decl
|
|
if (i == 0)
|
|
{
|
|
if (!beforeTokens[j].type.among(tok!"{", tok!"}", tok!";",
|
|
tok!"[", tok!"(", tok!",", tok!":"))
|
|
break;
|
|
}
|
|
// then compare the end of the "before tokens" (access chain)
|
|
// with the firsts (ModuleDeclaration)
|
|
else
|
|
{
|
|
// even index : must be a dot
|
|
if (expectDot &&
|
|
(tokenArray[i].type != tok!"." || beforeTokens[j].type != tok!"."))
|
|
break;
|
|
// odd index : identifiers must match
|
|
else if (expectIdt &&
|
|
(tokenArray[i].type != tok!"identifier" || beforeTokens[j].type != tok!"identifier" ||
|
|
tokenArray[i].text != beforeTokens[j].text))
|
|
break;
|
|
}
|
|
if (i == moduleDeclEndIndex - 1)
|
|
beginsWithModuleName = true;
|
|
}
|
|
|
|
|
|
// replace the "before tokens" with a pattern making the remaining
|
|
// parts of the completion process think that it's a "Module Scope Operator".
|
|
if (beginsWithModuleName)
|
|
{
|
|
if (dotId)
|
|
beforeTokens = assumeSorted([const Token(tok!"{"), const Token(tok!"."),
|
|
cast(const) beforeTokens[$-1]]);
|
|
else
|
|
beforeTokens = assumeSorted([const Token(tok!"{"), const Token(tok!".")]);
|
|
}
|
|
}
|
|
|
|
auto calltipHint = getCalltipHint(beforeTokens);
|
|
|
|
final switch (calltipHint) with (CalltipHint) {
|
|
case regularArguments:
|
|
immutable size_t end = goBackToOpenParen(beforeTokens);
|
|
if (end != size_t.max)
|
|
return calltipCompletion(beforeTokens[0 .. end], tokenArray,
|
|
request.cursorPosition, moduleCache, calltipHint);
|
|
break;
|
|
case templateArguments, indexOperator:
|
|
return calltipCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache, calltipHint);
|
|
case none:
|
|
// could be import or dot completion
|
|
if (beforeTokens.length < 2){
|
|
break;
|
|
}
|
|
|
|
ImportKind kind = determineImportKind(beforeTokens);
|
|
if (kind == ImportKind.neither)
|
|
{
|
|
if (beforeTokens.isUdaExpression)
|
|
beforeTokens = beforeTokens[$ - 1 .. $];
|
|
return dotCompletion(beforeTokens, tokenArray, request.cursorPosition,
|
|
moduleCache);
|
|
}
|
|
return importCompletion(beforeTokens, kind, moduleCache);
|
|
}
|
|
return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache);
|
|
}
|
|
|
|
/**
|
|
* Handles dot completion for identifiers and types.
|
|
* Params:
|
|
* beforeTokens = the tokens before the cursor
|
|
* tokenArray = all tokens in the file
|
|
* cursorPosition = the cursor position in bytes
|
|
* Returns:
|
|
* the autocompletion response
|
|
*/
|
|
AutocompleteResponse dotCompletion(T)(T beforeTokens, const(Token)[] tokenArray,
|
|
size_t cursorPosition, ref ModuleCache moduleCache)
|
|
{
|
|
AutocompleteResponse response;
|
|
|
|
// Partial symbol name appearing after the dot character and before the
|
|
// cursor.
|
|
string partial;
|
|
|
|
// Type of the token before the dot, or identifier if the cursor was at
|
|
// an identifier.
|
|
IdType significantTokenType;
|
|
|
|
if (beforeTokens.length >= 1 && beforeTokens[$ - 1] == tok!"identifier")
|
|
{
|
|
// Set partial to the slice of the identifier between the beginning
|
|
// of the identifier and the cursor. This improves the completion
|
|
// responses when the cursor is in the middle of an identifier instead
|
|
// of at the end
|
|
auto t = beforeTokens[$ - 1];
|
|
if (cursorPosition - t.index >= 0 && cursorPosition - t.index <= t.text.length)
|
|
{
|
|
partial = t.text[0 .. cursorPosition - t.index];
|
|
// issue 442 - prevent `partial` to start in the middle of a MBC
|
|
// since later there's a non-nothrow call to `toUpper`
|
|
import std.utf : validate, UTFException;
|
|
try validate(partial);
|
|
catch (UTFException)
|
|
{
|
|
import std.experimental.logger : warning;
|
|
warning("cursor positioned within a UTF sequence");
|
|
partial = "";
|
|
}
|
|
}
|
|
significantTokenType = partial.length ? tok!"identifier" : tok!"";
|
|
beforeTokens = beforeTokens[0 .. $ - 1];
|
|
}
|
|
else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".")
|
|
significantTokenType = beforeTokens[$ - 2].type;
|
|
else
|
|
return response;
|
|
switch (significantTokenType)
|
|
{
|
|
mixin(STRING_LITERAL_CASES);
|
|
foreach (symbol; arraySymbols)
|
|
response.completions ~= makeSymbolCompletionInfo(symbol, symbol.kind);
|
|
goto case;
|
|
mixin(TYPE_IDENT_CASES);
|
|
case tok!")":
|
|
case tok!"]":
|
|
RollbackAllocator rba;
|
|
ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, &rba, cursorPosition, moduleCache);
|
|
scope(exit) pair.destroy();
|
|
response.setCompletions(pair.scope_, getExpression(beforeTokens),
|
|
cursorPosition, CompletionType.identifiers, CalltipHint.none, partial);
|
|
if (!pair.ufcsSymbols.empty) {
|
|
response.completions ~= pair.ufcsSymbols.map!(s => makeSymbolCompletionInfo(s, CompletionKind.ufcsName)).array;
|
|
// Setting CompletionType in case of none symbols are found via setCompletions, but we have UFCS symbols.
|
|
response.completionType = CompletionType.identifiers;
|
|
}
|
|
break;
|
|
// these tokens before a "." mean "Module Scope Operator"
|
|
case tok!":":
|
|
case tok!"(":
|
|
case tok!"[":
|
|
case tok!"{":
|
|
case tok!";":
|
|
case tok!"}":
|
|
case tok!",":
|
|
RollbackAllocator rba;
|
|
ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, &rba, 1, moduleCache);
|
|
scope(exit) pair.destroy();
|
|
response.setCompletions(pair.scope_, getExpression(beforeTokens),
|
|
1, CompletionType.identifiers, CalltipHint.none, partial);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Handles calltip completion for function calls and some keywords
|
|
* Params:
|
|
* beforeTokens = the tokens before the cursor
|
|
* tokenArray = all tokens in the file
|
|
* cursorPosition = the cursor position in bytes
|
|
* Returns:
|
|
* the autocompletion response
|
|
*/
|
|
deprecated("Use `calltipCompletion` instead") alias parenCompletion = calltipCompletion;
|
|
AutocompleteResponse calltipCompletion(T)(T beforeTokens,
|
|
const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache, CalltipHint calltipHint = CalltipHint.none)
|
|
{
|
|
AutocompleteResponse response;
|
|
immutable(ConstantCompletion)[] completions;
|
|
auto significantTokenId = getSignificantTokenId(beforeTokens);
|
|
|
|
switch (significantTokenId)
|
|
{
|
|
case tok!"__traits":
|
|
completions = traits;
|
|
goto fillResponse;
|
|
case tok!"scope":
|
|
completions = scopes;
|
|
goto fillResponse;
|
|
case tok!"version":
|
|
completions = predefinedVersions;
|
|
goto fillResponse;
|
|
case tok!"extern":
|
|
completions = linkages;
|
|
goto fillResponse;
|
|
case tok!"pragma":
|
|
completions = pragmas;
|
|
fillResponse:
|
|
response.completionType = CompletionType.identifiers;
|
|
foreach (completion; completions)
|
|
{
|
|
response.completions ~= AutocompleteResponse.Completion(
|
|
completion.identifier,
|
|
CompletionKind.keyword,
|
|
null, null, 0, // definition, symbol path+location
|
|
completion.ddoc
|
|
);
|
|
}
|
|
break;
|
|
case tok!"characterLiteral":
|
|
case tok!"doubleLiteral":
|
|
case tok!"floatLiteral":
|
|
case tok!"identifier":
|
|
case tok!"idoubleLiteral":
|
|
case tok!"ifloatLiteral":
|
|
case tok!"intLiteral":
|
|
case tok!"irealLiteral":
|
|
case tok!"longLiteral":
|
|
case tok!"realLiteral":
|
|
case tok!"uintLiteral":
|
|
case tok!"ulongLiteral":
|
|
case tok!"this":
|
|
case tok!"super":
|
|
case tok!")":
|
|
case tok!"]":
|
|
mixin(STRING_LITERAL_CASES);
|
|
RollbackAllocator rba;
|
|
ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, &rba, cursorPosition, moduleCache);
|
|
scope(exit) pair.destroy();
|
|
// We remove by 2 when the calltip hint is !( else remove by 1.
|
|
auto endOffset = beforeTokens.isBangParenCalltipHint ? 2 : 1;
|
|
auto expression = getExpression(beforeTokens[0 .. $ - endOffset]);
|
|
response.setCompletions(pair.scope_, expression,
|
|
cursorPosition, CompletionType.calltips, calltipHint);
|
|
if (!pair.ufcsSymbols.empty) {
|
|
response.completions ~= pair.ufcsSymbols.map!(s => makeSymbolCompletionInfo(s, CompletionKind.ufcsName)).array;
|
|
// Setting CompletionType in case of none symbols are found via setCompletions, but we have UFCS symbols.
|
|
response.completionType = CompletionType.calltips;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return response;
|
|
}
|
|
|
|
IdType getSignificantTokenId(T)(T beforeTokens){
|
|
auto significantTokenId = beforeTokens[$ - 2].type;
|
|
if (beforeTokens.isBangParenCalltipHint) {
|
|
if(beforeTokens[$ - 3] == tok!"identifier"){
|
|
return beforeTokens[$ - 3].type;
|
|
}
|
|
}
|
|
return significantTokenId;
|
|
}
|
|
/**
|
|
* Hinting what the user expects for calltip completion
|
|
* Params:
|
|
* beforeTokens = tokens before the cursor
|
|
* Returns: calltipHint based of beforeTokens
|
|
*/
|
|
CalltipHint getCalltipHint(T)(T beforeTokens) {
|
|
if (beforeTokens.length < 2){
|
|
return CalltipHint.none;
|
|
}
|
|
if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"["
|
|
|| beforeTokens[$ - 1] == tok!",")
|
|
{
|
|
return CalltipHint.regularArguments;
|
|
}
|
|
if(beforeTokens[$ - 2] == tok!"identifier" && beforeTokens[$ - 1] == tok!"["){
|
|
return CalltipHint.indexOperator;
|
|
}
|
|
|
|
if(beforeTokens.isSingleBangCalltipHint || beforeTokens.isBangParenCalltipHint){
|
|
return CalltipHint.templateArguments;
|
|
}
|
|
|
|
return CalltipHint.none;
|
|
}
|
|
|
|
// Check if we are doing a single "!" calltip hint
|
|
private bool isSingleBangCalltipHint(T)(T beforeTokens) {
|
|
return beforeTokens.length >= 2
|
|
&& beforeTokens[$ - 2] == tok!"identifier"
|
|
&& beforeTokens[$ - 1] == tok!"!";
|
|
}
|
|
|
|
// Check if we are doing a "!(" calltip hint
|
|
private bool isBangParenCalltipHint(T)(T beforeTokens){
|
|
return beforeTokens.length >= 3
|
|
&& beforeTokens[$ - 3] == tok!"identifier"
|
|
&& beforeTokens[$ - 2] == tok!"!"
|
|
&& beforeTokens[$ - 1] == tok!"(";
|
|
}
|
|
|
|
/**
|
|
* Provides autocomplete for selective imports, e.g.:
|
|
* ---
|
|
* import std.algorithm: balancedParens;
|
|
* ---
|
|
*/
|
|
AutocompleteResponse importCompletion(T)(T beforeTokens, ImportKind kind,
|
|
ref ModuleCache moduleCache)
|
|
in
|
|
{
|
|
assert (beforeTokens.length >= 2);
|
|
}
|
|
do
|
|
{
|
|
AutocompleteResponse response;
|
|
if (beforeTokens.length <= 2)
|
|
return response;
|
|
|
|
size_t i = beforeTokens.length - 1;
|
|
|
|
if (kind == ImportKind.normal)
|
|
{
|
|
|
|
while (beforeTokens[i].type != tok!"," && beforeTokens[i].type != tok!"import"
|
|
&& beforeTokens[i].type != tok!"=" )
|
|
i--;
|
|
setImportCompletions(beforeTokens[i .. $], response, moduleCache);
|
|
return response;
|
|
}
|
|
|
|
loop: while (true) switch (beforeTokens[i].type)
|
|
{
|
|
case tok!"identifier":
|
|
case tok!"=":
|
|
case tok!",":
|
|
case tok!".":
|
|
i--;
|
|
break;
|
|
case tok!":":
|
|
i--;
|
|
while (beforeTokens[i].type == tok!"identifier" || beforeTokens[i].type == tok!".")
|
|
i--;
|
|
break loop;
|
|
default:
|
|
break loop;
|
|
}
|
|
|
|
size_t j = i;
|
|
loop2: while (j <= beforeTokens.length) switch (beforeTokens[j].type)
|
|
{
|
|
case tok!":": break loop2;
|
|
default: j++; break;
|
|
}
|
|
|
|
if (i >= j)
|
|
{
|
|
warning("Malformed import statement");
|
|
return response;
|
|
}
|
|
|
|
immutable string path = beforeTokens[i + 1 .. j]
|
|
.filter!(token => token.type == tok!"identifier")
|
|
.map!(token => cast() token.text)
|
|
.joiner(dirSeparator)
|
|
.text();
|
|
|
|
string resolvedLocation = moduleCache.resolveImportLocation(path);
|
|
if (resolvedLocation is null)
|
|
{
|
|
warning("Could not resolve location of ", path);
|
|
return response;
|
|
}
|
|
auto symbols = moduleCache.getModuleSymbol(internString(resolvedLocation));
|
|
|
|
import containers.hashset : HashSet;
|
|
HashSet!string h;
|
|
|
|
void addSymbolToResponses(const(DSymbol)* sy)
|
|
{
|
|
auto a = DSymbol(sy.name);
|
|
if (!builtinSymbols.contains(&a) && sy.name !is null && !h.contains(sy.name)
|
|
&& !sy.skipOver && sy.name != CONSTRUCTOR_SYMBOL_NAME
|
|
&& isPublicCompletionKind(sy.kind))
|
|
{
|
|
response.completions ~= makeSymbolCompletionInfo(sy, sy.kind);
|
|
h.insert(sy.name);
|
|
}
|
|
}
|
|
|
|
foreach (s; symbols.opSlice().filter!(a => !a.skipOver))
|
|
{
|
|
if (s.kind == CompletionKind.importSymbol && s.type !is null)
|
|
foreach (sy; s.type.opSlice().filter!(a => !a.skipOver))
|
|
addSymbolToResponses(sy);
|
|
else
|
|
addSymbolToResponses(s);
|
|
}
|
|
response.completionType = CompletionType.identifiers;
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Populates the response with completion information for an import statement
|
|
* Params:
|
|
* tokens = the tokens after the "import" keyword and before the cursor
|
|
* response = the response that should be populated
|
|
*/
|
|
void setImportCompletions(T)(T tokens, ref AutocompleteResponse response,
|
|
ref ModuleCache cache)
|
|
{
|
|
response.completionType = CompletionType.identifiers;
|
|
string partial = null;
|
|
if (tokens[$ - 1].type == tok!"identifier")
|
|
{
|
|
partial = tokens[$ - 1].text;
|
|
tokens = tokens[0 .. $ - 1];
|
|
}
|
|
auto moduleParts = tokens.filter!(a => a.type == tok!"identifier").map!("a.text").array();
|
|
string path = buildPath(moduleParts);
|
|
|
|
bool found = false;
|
|
|
|
foreach (importPath; cache.getImportPaths())
|
|
{
|
|
if (importPath.isFile)
|
|
{
|
|
if (!exists(importPath))
|
|
continue;
|
|
|
|
found = true;
|
|
|
|
auto n = importPath.baseName(".d").baseName(".di");
|
|
if (isFile(importPath) && (importPath.endsWith(".d") || importPath.endsWith(".di"))
|
|
&& (partial is null || n.startsWith(partial)))
|
|
response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, importPath, 0);
|
|
}
|
|
else
|
|
{
|
|
string p = buildPath(importPath, path);
|
|
if (!exists(p))
|
|
continue;
|
|
|
|
found = true;
|
|
|
|
try foreach (string name; dirEntries(p, SpanMode.shallow))
|
|
{
|
|
import std.path: baseName;
|
|
if (name.baseName.startsWith(".#"))
|
|
continue;
|
|
|
|
auto n = name.baseName(".d").baseName(".di");
|
|
if (isFile(name) && (name.endsWith(".d") || name.endsWith(".di"))
|
|
&& (partial is null || n.startsWith(partial)))
|
|
response.completions ~= AutocompleteResponse.Completion(n, CompletionKind.moduleName, null, name, 0);
|
|
else if (isDir(name))
|
|
{
|
|
if (n[0] != '.' && (partial is null || n.startsWith(partial)))
|
|
{
|
|
immutable packageDPath = buildPath(name, "package.d");
|
|
immutable packageDIPath = buildPath(name, "package.di");
|
|
immutable packageD = exists(packageDPath);
|
|
immutable packageDI = exists(packageDIPath);
|
|
immutable kind = packageD || packageDI ? CompletionKind.moduleName : CompletionKind.packageName;
|
|
immutable file = packageD ? packageDPath : packageDI ? packageDIPath : name;
|
|
response.completions ~= AutocompleteResponse.Completion(n, kind, null, file, 0);
|
|
}
|
|
}
|
|
}
|
|
catch(FileException)
|
|
{
|
|
warning("Cannot access import path: ", importPath);
|
|
}
|
|
}
|
|
}
|
|
if (!found)
|
|
warning("Could not find ", moduleParts);
|
|
}
|
|
|
|
/**
|
|
*
|
|
*/
|
|
void setCompletions(T)(ref AutocompleteResponse response,
|
|
Scope* completionScope, T tokens, size_t cursorPosition,
|
|
CompletionType completionType, CalltipHint callTipHint = CalltipHint.none, string partial = null)
|
|
{
|
|
static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, string p,
|
|
Scope* completionScope, size_t[] circularGuard = [])
|
|
{
|
|
if (circularGuard.canFind(cast(size_t) s))
|
|
return;
|
|
foreach (sym; s.opSlice())
|
|
{
|
|
if (sym.name !is null && sym.name.length > 0 && isPublicCompletionKind(sym.kind)
|
|
&& (p is null ? true : toUpper(sym.name.data).startsWith(toUpper(p)))
|
|
&& !r.completions.canFind!(a => a.identifier == sym.name)
|
|
&& sym.name[0] != '*'
|
|
&& mightBeRelevantInCompletionScope(sym, completionScope))
|
|
{
|
|
r.completions ~= makeSymbolCompletionInfo(sym, sym.kind);
|
|
}
|
|
if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null)
|
|
addSymToResponse(sym.type, r, p, completionScope, circularGuard ~ (cast(size_t) s));
|
|
}
|
|
}
|
|
|
|
// Handle the simple case where we get all symbols in scope and filter it
|
|
// based on the currently entered text.
|
|
if (partial !is null && tokens.length == 0)
|
|
{
|
|
auto currentSymbols = completionScope.getSymbolsInCursorScope(cursorPosition);
|
|
foreach (s; currentSymbols.filter!(a => isPublicCompletionKind(a.kind)
|
|
&& toUpper(a.name.data).startsWith(toUpper(partial))
|
|
&& mightBeRelevantInCompletionScope(a, completionScope)))
|
|
{
|
|
response.completions ~= makeSymbolCompletionInfo(s, s.kind);
|
|
}
|
|
response.completionType = CompletionType.identifiers;
|
|
return;
|
|
}
|
|
// "Module Scope Operator" : filter module decls
|
|
else if (tokens.length == 1 && tokens[0] == tok!".")
|
|
{
|
|
auto currentSymbols = completionScope.getSymbolsInCursorScope(cursorPosition);
|
|
foreach (s; currentSymbols.filter!(a => isPublicCompletionKind(a.kind)
|
|
// TODO: for now since "module.partial" is transformed into ".partial"
|
|
// we cant put the imported symbols that should be in the list.
|
|
&& a.kind != CompletionKind.importSymbol
|
|
&& a.kind != CompletionKind.dummy
|
|
&& a.symbolFile == "stdin"
|
|
&& (partial !is null && toUpper(a.name.data).startsWith(toUpper(partial))
|
|
|| partial is null)
|
|
&& mightBeRelevantInCompletionScope(a, completionScope)))
|
|
{
|
|
response.completions ~= makeSymbolCompletionInfo(s, s.kind);
|
|
}
|
|
response.completionType = CompletionType.identifiers;
|
|
return;
|
|
}
|
|
|
|
if (tokens.length == 0)
|
|
return;
|
|
|
|
DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens,
|
|
cursorPosition, completionType);
|
|
|
|
// If calltipHint is templateArguments we ensure that the symbol is also templated
|
|
if (callTipHint == CalltipHint.templateArguments && symbols.length >= 1 && symbols[0].qualifier != SymbolQualifier.templated){
|
|
return;
|
|
}
|
|
|
|
if (symbols.length == 0)
|
|
return;
|
|
|
|
if (completionType == CompletionType.identifiers)
|
|
{
|
|
while (symbols[0].qualifier == SymbolQualifier.func
|
|
|| symbols[0].kind == CompletionKind.functionName
|
|
|| symbols[0].kind == CompletionKind.importSymbol
|
|
|| symbols[0].kind == CompletionKind.aliasName)
|
|
{
|
|
symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? []
|
|
: [symbols[0].type];
|
|
if (symbols.length == 0)
|
|
return;
|
|
}
|
|
addSymToResponse(symbols[0], response, partial, completionScope);
|
|
response.completionType = CompletionType.identifiers;
|
|
}
|
|
else if (completionType == CompletionType.calltips)
|
|
{
|
|
enforce(callTipHint != CalltipHint.none, "Make sure to have a properly defined calltipHint!");
|
|
//trace("Showing call tips for ", symbols[0].name, " of kind ", symbols[0].kind);
|
|
if (symbols[0].kind != CompletionKind.functionName
|
|
&& symbols[0].callTip is null)
|
|
{
|
|
if (symbols[0].kind == CompletionKind.aliasName)
|
|
{
|
|
if (symbols[0].type is null || symbols[0].type is symbols[0])
|
|
return;
|
|
symbols = [symbols[0].type];
|
|
}
|
|
if (symbols[0].kind == CompletionKind.variableName)
|
|
{
|
|
auto dumb = symbols[0].type;
|
|
if (dumb !is null)
|
|
{
|
|
if (dumb.kind == CompletionKind.functionName)
|
|
{
|
|
symbols = [dumb];
|
|
goto setCallTips;
|
|
}
|
|
if (callTipHint == CalltipHint.indexOperator)
|
|
{
|
|
auto index = dumb.getPartsByName(internString("opIndex"));
|
|
if (index.length > 0)
|
|
{
|
|
symbols = index;
|
|
goto setCallTips;
|
|
}
|
|
}
|
|
auto call = dumb.getPartsByName(internString("opCall"));
|
|
if (call.length > 0)
|
|
{
|
|
symbols = call;
|
|
goto setCallTips;
|
|
}
|
|
}
|
|
}
|
|
if ((symbols[0].kind == CompletionKind.structName || symbols[0].kind == CompletionKind.className))
|
|
{
|
|
if (callTipHint == CalltipHint.templateArguments) {
|
|
response.completionType = CompletionType.calltips;
|
|
response.completions = [generateStructConstructorCalltip(symbols[0], callTipHint)];
|
|
return;
|
|
}
|
|
|
|
//Else we do calltip for regular arguments
|
|
auto constructor = symbols[0].getPartsByName(CONSTRUCTOR_SYMBOL_NAME);
|
|
if (constructor.length == 0)
|
|
{
|
|
// Build a call tip out of the struct fields
|
|
if (symbols[0].kind == CompletionKind.structName)
|
|
{
|
|
response.completionType = CompletionType.calltips;
|
|
response.completions = [generateStructConstructorCalltip(symbols[0], callTipHint)];
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
symbols = constructor;
|
|
goto setCallTips;
|
|
}
|
|
}
|
|
|
|
}
|
|
setCallTips:
|
|
response.completionType = CompletionType.calltips;
|
|
foreach (symbol; symbols)
|
|
{
|
|
if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null)
|
|
{
|
|
auto completion = makeSymbolCompletionInfo(symbol, char.init);
|
|
// TODO: put return type
|
|
response.completions ~= completion;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool mightBeRelevantInCompletionScope(const DSymbol* symbol, Scope* scope_)
|
|
{
|
|
import dparse.lexer : tok;
|
|
|
|
if (symbol.protection == tok!"private" &&
|
|
!scope_.hasSymbolRecursive(symbol))
|
|
{
|
|
// scope is the scope of the current file so if the symbol is not in there, it's not accessible
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
AutocompleteResponse.Completion generateStructConstructorCalltip(const DSymbol* symbol, CalltipHint calltipHint = CalltipHint.regularArguments)
|
|
in
|
|
{
|
|
if (calltipHint == CalltipHint.regularArguments)
|
|
{
|
|
assert(symbol.kind == CompletionKind.structName);
|
|
}
|
|
}
|
|
do
|
|
{
|
|
string generatedStructConstructorCalltip = calltipHint == CalltipHint.regularArguments ? "this(" : symbol.name ~ "(";
|
|
auto completionKindFilter = calltipHint == CalltipHint.regularArguments ? CompletionKind.variableName : CompletionKind.typeTmpParam;
|
|
const(DSymbol)*[] fields =
|
|
symbol.opSlice().filter!(a => a.kind == completionKindFilter).map!(a => cast(const(DSymbol)*) a).array();
|
|
fields.sort!((a, b) => a.location < b.location);
|
|
foreach (i, field; fields)
|
|
{
|
|
if (field.kind != completionKindFilter)
|
|
continue;
|
|
i++;
|
|
if (field.type !is null && calltipHint == CalltipHint.regularArguments)
|
|
{
|
|
generatedStructConstructorCalltip ~= field.type.name;
|
|
generatedStructConstructorCalltip ~= " ";
|
|
}
|
|
generatedStructConstructorCalltip ~= field.name;
|
|
if (i < fields.length)
|
|
generatedStructConstructorCalltip ~= ", ";
|
|
}
|
|
generatedStructConstructorCalltip ~= ")";
|
|
auto completion = makeSymbolCompletionInfo(symbol, char.init);
|
|
completion.identifier = calltipHint == CalltipHint.regularArguments ? "this" : symbol.name;
|
|
completion.definition = generatedStructConstructorCalltip;
|
|
completion.typeOf = symbol.name;
|
|
return completion;
|
|
}
|