From 38cc25bbcc53b71594f42fe094af0452fecd0d10 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Thu, 10 Aug 2017 21:33:42 +0200 Subject: [PATCH] Split of server into multiple files --- src/common/messages.d | 10 + src/server/autocomplete.d | 1562 ---------------------------- src/server/autocomplete/complete.d | 573 ++++++++++ src/server/autocomplete/doc.d | 99 ++ src/server/autocomplete/localuse.d | 120 +++ src/server/autocomplete/package.d | 26 + src/server/autocomplete/symbols.d | 127 +++ src/server/autocomplete/util.d | 731 +++++++++++++ src/server/main.d | 131 +-- src/server/server.d | 4 +- 10 files changed, 1723 insertions(+), 1660 deletions(-) delete mode 100644 src/server/autocomplete.d create mode 100644 src/server/autocomplete/complete.d create mode 100644 src/server/autocomplete/doc.d create mode 100644 src/server/autocomplete/localuse.d create mode 100644 src/server/autocomplete/package.d create mode 100644 src/server/autocomplete/symbols.d create mode 100644 src/server/autocomplete/util.d diff --git a/src/common/messages.d b/src/common/messages.d index abee795..411c70b 100644 --- a/src/common/messages.d +++ b/src/common/messages.d @@ -165,6 +165,16 @@ struct AutocompleteResponse * Symbol identifier */ ulong symbolIdentifier; + + /** + * Creates an empty acknoledgement response + */ + static AutocompleteResponse ack() + { + AutocompleteResponse response; + response.completionType = "ack"; + return response; + } } /** diff --git a/src/server/autocomplete.d b/src/server/autocomplete.d deleted file mode 100644 index 435b870..0000000 --- a/src/server/autocomplete.d +++ /dev/null @@ -1,1562 +0,0 @@ -/** - * 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 . - */ - -module server.autocomplete; - -import std.algorithm; -import std.experimental.allocator; -import std.experimental.logger; -import std.array; -import std.conv; -import std.experimental.logger; -import std.file; -import std.path; -import std.range; -import std.stdio; -import std.string; -import std.typecons; -import std.uni; - -import dparse.ast; -import dparse.lexer; -import dparse.parser; -import dparse.rollback_allocator; - -import dsymbol.conversion; -import dsymbol.modulecache; -import dsymbol.string_interning; -import dsymbol.symbol; -import dsymbol.scope_; -import dsymbol.builtin.names; -import dsymbol.builtin.symbols; - -import common.constants; -import common.messages; - -import containers.hashset; - -/** - * Finds the uses of the symbol at the cursor position within a single document. - * Params: - * request = the autocompletion request. - * Returns: - * the autocompletion response. - */ -public AutocompleteResponse findLocalUse(AutocompleteRequest request, - ref ModuleCache moduleCache) -{ - AutocompleteResponse response; - RollbackAllocator rba; - auto allocator = scoped!(ASTAllocator)(); - auto cache = StringCache(StringCache.defaultBucketCount); - - // patchs the original request for the subsequent requests - request.kind = RequestKind.symbolLocation; - - // getSymbolsForCompletion() copy to avoid repetitive parsing - LexerConfig config; - config.fileName = ""; - const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, - config, &cache); - SymbolStuff getSymbolsAtCursor(size_t cursorPosition) - { - auto sortedTokens = assumeSorted(tokenArray); - auto beforeTokens = sortedTokens.lowerBound(cursorPosition); - ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, - &rba, request.cursorPosition, moduleCache); - auto expression = getExpression(beforeTokens); - return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, - cursorPosition, CompletionType.location), pair.symbol, pair.scope_); - } - - // gets the symbol matching to cursor pos - SymbolStuff stuff = getSymbolsAtCursor(cast(size_t)request.cursorPosition); - scope(exit) stuff.destroy(); - - // starts searching only if no ambiguity with the symbol - if (stuff.symbols.length == 1) - { - const(DSymbol*) sourceSymbol = stuff.symbols[0]; - response.symbolLocation = sourceSymbol.location; - response.symbolFilePath = sourceSymbol.symbolFile.idup; - - // gets the source token to avoid too much getSymbolsAtCursor() - const(Token)* sourceToken; - foreach(i, t; tokenArray) - { - if (t.type != tok!"identifier") - continue; - if (request.cursorPosition >= t.index && - request.cursorPosition < t.index + t.text.length) - { - sourceToken = tokenArray.ptr + i; - break; - } - } - - // finds the tokens that match to the source symbol - if (sourceToken != null) foreach (t; tokenArray) - { - if (t.type == tok!"identifier" && t.text == sourceToken.text) - { - size_t pos = cast(size_t) t.index + 1; // place cursor inside the token - SymbolStuff candidate = getSymbolsAtCursor(pos); - scope(exit) candidate.destroy(); - if (candidate.symbols.length == 1 && - candidate.symbols[0].location == sourceSymbol.location && - candidate.symbols[0].symbolFile == sourceSymbol.symbolFile) - { - response.locations ~= t.index; - } - } - } - else - { - warning("The source token is not an identifier"); - } - } - else - { - warning("No or ambiguous symbol for the identifier at cursor"); - } - return response; -} - -/** - * Gets documentation for the symbol at the cursor - * Params: - * request = the autocompletion request - * Returns: - * the autocompletion response - */ -public AutocompleteResponse getDoc(const AutocompleteRequest request, - ref ModuleCache moduleCache) -{ -// trace("Getting doc comments"); - AutocompleteResponse response; - RollbackAllocator rba; - auto allocator = scoped!(ASTAllocator)(); - auto cache = StringCache(StringCache.defaultBucketCount); - SymbolStuff stuff = getSymbolsForCompletion(request, CompletionType.ddoc, - allocator, &rba, cache, moduleCache); - if (stuff.symbols.length == 0) - warning("Could not find symbol"); - else - { - Appender!(char[]) app; - - bool isDitto(string s) - { - import std.uni : icmp; - if (s.length > 5) - return false; - else - return s.icmp("ditto") == 0; - } - - void putDDocChar(char c) - { - switch (c) - { - case '\\': - app.put('\\'); - app.put('\\'); - break; - case '\n': - app.put('\\'); - app.put('n'); - break; - default: - app.put(c); - break; - } - } - - void putDDocString(string s) - { - foreach (char c; s) - putDDocChar(c); - } - - foreach(ref symbol; stuff.symbols.filter!(a => !a.doc.empty && !isDitto(a.doc))) - { - app.clear; - putDDocString(symbol.doc); - response.docComments ~= app.data.idup; - } - } - return response; -} - -/** - * Finds the declaration of the symbol at the cursor position. - * Params: - * request = the autocompletion request - * Returns: - * the autocompletion response - */ -public AutocompleteResponse findDeclaration(const AutocompleteRequest request, - ref ModuleCache moduleCache) -{ - AutocompleteResponse response; - RollbackAllocator rba; - auto allocator = scoped!(ASTAllocator)(); - auto cache = StringCache(StringCache.defaultBucketCount); - SymbolStuff stuff = getSymbolsForCompletion(request, - CompletionType.location, allocator, &rba, cache, moduleCache); - scope(exit) stuff.destroy(); - if (stuff.symbols.length > 0) - { - response.symbolLocation = stuff.symbols[0].location; - response.symbolFilePath = stuff.symbols[0].symbolFile.idup; - } - else - warning("Could not find symbol declaration"); - return response; -} - -/** - * 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(StringCache.defaultBucketCount); - auto beforeTokens = getTokensBeforeCursor(request.sourceCode, - request.cursorPosition, stringCache, tokenArray); - if (beforeTokens.length >= 2) - { - if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"[") - { - return parenCompletion(beforeTokens, tokenArray, request.cursorPosition, - moduleCache); - } - else if (beforeTokens[$ - 1] == tok!",") - { - immutable size_t end = goBackToOpenParen(beforeTokens); - if (end != size_t.max) - return parenCompletion(beforeTokens[0 .. end], tokenArray, - request.cursorPosition, moduleCache); - } - else - { - ImportKind kind = determineImportKind(beforeTokens); - if (kind == ImportKind.neither) - { - if (beforeTokens.isUdaExpression) - beforeTokens = beforeTokens[$-1 .. $]; - return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, - moduleCache); - } - else - return importCompletion(beforeTokens, kind, moduleCache); - } - } - return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache); -} - -/** - * - */ -public AutocompleteResponse symbolSearch(const AutocompleteRequest request, - ref ModuleCache moduleCache) -{ - import containers.ttree : TTree; - - LexerConfig config; - config.fileName = ""; - auto cache = StringCache(StringCache.defaultBucketCount); - const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, - config, &cache); - auto allocator = scoped!(ASTAllocator)(); - RollbackAllocator rba; - ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, - &rba, request.cursorPosition, moduleCache); - scope(exit) pair.destroy(); - - static struct SearchResults - { - void put(const(DSymbol)* symbol) - { - tree.insert(SearchResult(symbol)); - } - - static struct SearchResult - { - const(DSymbol)* symbol; - - int opCmp(ref const SearchResult other) const pure nothrow - { - if (other.symbol.symbolFile < symbol.symbolFile) - return -1; - if (other.symbol.symbolFile > symbol.symbolFile) - return 1; - if (other.symbol.location < symbol.location) - return -1; - return other.symbol.location > symbol.location; - } - } - - TTree!(SearchResult) tree; - } - - SearchResults results; - HashSet!size_t visited; - foreach (symbol; pair.scope_.symbols) - symbol.getParts!SearchResults(internString(request.searchName), results, visited); - foreach (s; moduleCache.getAllSymbols()) - s.symbol.getParts!SearchResults(internString(request.searchName), results, visited); - - AutocompleteResponse response; - foreach (result; results.tree[]) - { - response.locations ~= result.symbol.location; - response.completionKinds ~= result.symbol.kind; - response.completions ~= result.symbol.symbolFile; - } - - return response; -} - -/******************************************************************************/ -private: - -enum ImportKind : ubyte -{ - selective, - normal, - neither -} - -/** - * 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]; - significantTokenType = tok!"identifier"; - beforeTokens = beforeTokens[0 .. $ - 1]; - } - else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".") - significantTokenType = beforeTokens[$ - 2].type; - else - return response; - - switch (significantTokenType) - { - case tok!"stringLiteral": - case tok!"wstringLiteral": - case tok!"dstringLiteral": - foreach (symbol; arraySymbols) - { - response.completionKinds ~= symbol.kind; - response.completions ~= symbol.name.dup; - } - response.completionType = CompletionType.identifiers; - break; - case tok!"int": - case tok!"uint": - case tok!"long": - case tok!"ulong": - case tok!"char": - case tok!"wchar": - case tok!"dchar": - case tok!"bool": - case tok!"byte": - case tok!"ubyte": - case tok!"short": - case tok!"ushort": - case tok!"cent": - case tok!"ucent": - case tok!"float": - case tok!"ifloat": - case tok!"cfloat": - case tok!"idouble": - case tok!"cdouble": - case tok!"double": - case tok!"real": - case tok!"ireal": - case tok!"creal": - case tok!"identifier": - case tok!")": - case tok!"]": - case tok!"this": - case tok!"super": - auto allocator = scoped!(ASTAllocator)(); - RollbackAllocator rba; - ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, - &rba, cursorPosition, moduleCache); - scope(exit) pair.destroy(); - response.setCompletions(pair.scope_, getExpression(beforeTokens), - cursorPosition, CompletionType.identifiers, false, partial); - break; - case tok!"(": - case tok!"{": - case tok!"[": - case tok!";": - case tok!":": - break; - default: - break; - } - return response; -} - -/** - * Params: - * sourceCode = the source code of the file being edited - * cursorPosition = the cursor position in bytes - * Returns: - * a sorted range of tokens before the cursor position - */ -auto getTokensBeforeCursor(const(ubyte[]) sourceCode, size_t cursorPosition, - ref StringCache cache, out const(Token)[] tokenArray) -{ - LexerConfig config; - config.fileName = ""; - tokenArray = getTokensForParser(cast(ubyte[]) sourceCode, config, &cache); - auto sortedTokens = assumeSorted(tokenArray); - return sortedTokens.lowerBound(cast(size_t) cursorPosition); -} - -/** - * Params: - * request = the autocompletion request - * type = type the autocompletion type - * Returns: - * all symbols that should be considered for the autocomplete list based on - * the request's source code, cursor position, and completion type. - */ -SymbolStuff getSymbolsForCompletion(const AutocompleteRequest request, - const CompletionType type, IAllocator allocator, RollbackAllocator* rba, - ref StringCache cache, ref ModuleCache moduleCache) -{ - const(Token)[] tokenArray; - auto beforeTokens = getTokensBeforeCursor(request.sourceCode, - request.cursorPosition, cache, tokenArray); - ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, - rba, request.cursorPosition, moduleCache); - auto expression = getExpression(beforeTokens); - return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, - request.cursorPosition, type), pair.symbol, pair.scope_); -} - -struct SymbolStuff -{ - void destroy() - { - typeid(DSymbol).destroy(symbol); - typeid(Scope).destroy(scope_); - } - - DSymbol*[] symbols; - DSymbol* symbol; - Scope* scope_; -} - -/** - * Handles paren 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 - */ -AutocompleteResponse parenCompletion(T)(T beforeTokens, - const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache) -{ - AutocompleteResponse response; - immutable(string)[] completions; - switch (beforeTokens[$ - 2].type) - { - 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 ~= completion; - response.completionKinds ~= CompletionKind.keyword; - } - break; - case tok!"characterLiteral": - case tok!"doubleLiteral": - case tok!"dstringLiteral": - 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!"stringLiteral": - case tok!"uintLiteral": - case tok!"ulongLiteral": - case tok!"wstringLiteral": - case tok!"this": - case tok!"super": - case tok!")": - case tok!"]": - auto allocator = scoped!(ASTAllocator)(); - RollbackAllocator rba; - ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, - &rba, cursorPosition, moduleCache); - scope(exit) pair.destroy(); - auto expression = getExpression(beforeTokens[0 .. $ - 1]); - response.setCompletions(pair.scope_, expression, - cursorPosition, CompletionType.calltips, beforeTokens[$ - 1] == tok!"["); - break; - default: - break; - } - return response; -} - -/** - * Determines if an import is selective, whole-module, or neither. - */ -ImportKind determineImportKind(T)(T tokens) -{ - assert (tokens.length > 1); - size_t i = tokens.length - 1; - if (!(tokens[i] == tok!":" || tokens[i] == tok!"," || tokens[i] == tok!"." - || tokens[i] == tok!"identifier")) - return ImportKind.neither; - bool foundColon = false; - while (true) switch (tokens[i].type) - { - case tok!":": - foundColon = true; - goto case; - case tok!"identifier": - case tok!"=": - case tok!".": - case tok!",": - if (i == 0) - return ImportKind.neither; - else - i--; - break; - case tok!"import": - return foundColon ? ImportKind.selective : ImportKind.normal; - default: - return ImportKind.neither; - } - return ImportKind.neither; -} - -unittest -{ - import std.stdio : writeln; - - Token[] t = [ - Token(tok!"import"), Token(tok!"identifier"), Token(tok!"."), - Token(tok!"identifier"), Token(tok!":"), Token(tok!"identifier"), Token(tok!",") - ]; - assert(determineImportKind(t) == ImportKind.selective); - Token[] t2; - t2 ~= Token(tok!"else"); - t2 ~= Token(tok!":"); - assert(determineImportKind(t2) == ImportKind.neither); - writeln("Unittest for determineImportKind() passed"); -} - -/** - * 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); -} -body -{ - 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.completionKinds ~= sy.kind; - response.completions ~= sy.name; - 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 ~= n; - response.completionKinds ~= CompletionKind.moduleName; - } - } - 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 ~= n; - response.completionKinds ~= CompletionKind.moduleName; - } - else if (isDir(name)) - { - if (n[0] != '.' && (partial is null || n.startsWith(partial))) - { - response.completions ~= n; - response.completionKinds ~= - exists(buildPath(name, "package.d")) || exists(buildPath(name, "package.di")) - ? CompletionKind.moduleName : CompletionKind.packageName; - } - } - } - catch(FileException) - { - warning("Cannot access import path: ", importPath); - } - } - } - if (!found) - warning("Could not find ", moduleParts); -} - -static void skip(alias O, alias C, T)(T t, ref size_t i) -{ - int depth = 1; - while (i < t.length) switch (t[i].type) - { - case O: - i++; - depth++; - break; - case C: - i++; - depth--; - if (depth <= 0) - return; - break; - default: - i++; - break; - } -} - -bool isSliceExpression(T)(T tokens, size_t index) -{ - while (index < tokens.length) switch (tokens[index].type) - { - case tok!"[": - skip!(tok!"[", tok!"]")(tokens, index); - break; - case tok!"(": - skip!(tok!"(", tok!")")(tokens, index); - break; - case tok!"]": - case tok!"}": - return false; - case tok!"..": - return true; - default: - index++; - break; - } - return false; -} - -/** - * - */ -DSymbol*[] getSymbolsByTokenChain(T)(Scope* completionScope, - T tokens, size_t cursorPosition, CompletionType completionType) -{ - //writeln(">>>"); - //dumpTokens(tokens.release); - //writeln(">>>"); - - static size_t skipEnd(T tokenSlice, size_t i, IdType open, IdType close) - { - size_t j = i + 1; - for (int depth = 1; depth > 0 && j < tokenSlice.length; j++) - { - if (tokenSlice[j].type == open) - depth++; - else if (tokenSlice[j].type == close) - { - depth--; - if (depth == 0) break; - } - } - return j; - } - - // Find the symbol corresponding to the beginning of the chain - DSymbol*[] symbols; - if (tokens.length == 0) - return []; - // Recurse in case the symbol chain starts with an expression in parens - // e.g. (a.b!c).d - if (tokens[0] == tok!"(") - { - immutable j = skipEnd(tokens, 0, tok!"(", tok!")"); - symbols = getSymbolsByTokenChain(completionScope, tokens[1 .. j], - cursorPosition, completionType); - tokens = tokens[j + 1 .. $]; - //writeln("<<<"); - //dumpTokens(tokens.release); - //writeln("<<<"); - if (tokens.length == 0) // workaround (#371) - return []; - } - else if (tokens[0] == tok!"." && tokens.length > 1) - { - tokens = tokens[1 .. $]; - if (tokens.length == 0) // workaround (#371) - return []; - symbols = completionScope.getSymbolsAtGlobalScope(stringToken(tokens[0])); - } - else - symbols = completionScope.getSymbolsByNameAndCursor(stringToken(tokens[0]), cursorPosition); - - if (symbols.length == 0) - { - //TODO: better bugfix for issue #368, see test case 52 or pull #371 - if (tokens.length) - warning("Could not find declaration of ", stringToken(tokens[0]), - " from position ", cursorPosition); - else assert(0, "internal error"); - return []; - } - - // If the `symbols` array contains functions, and one of them returns - // void and the others do not, this is a property function. For the - // purposes of chaining auto-complete we want to ignore the one that - // returns void. This is a no-op if we are getting doc comments. - void filterProperties() @nogc @safe - { - if (symbols.length == 0 || completionType == CompletionType.ddoc) - return; - if (symbols[0].kind == CompletionKind.functionName - || symbols[0].qualifier == SymbolQualifier.func) - { - int voidRets = 0; - int nonVoidRets = 0; - size_t firstNonVoidIndex = size_t.max; - foreach (i, sym; symbols) - { - if (sym.type is null) - return; - if (&sym.type.name[0] == &getBuiltinTypeName(tok!"void")[0]) - voidRets++; - else - { - nonVoidRets++; - firstNonVoidIndex = min(firstNonVoidIndex, i); - } - } - if (voidRets > 0 && nonVoidRets > 0) - symbols = symbols[firstNonVoidIndex .. $]; - } - } - - filterProperties(); - - if (shouldSwapWithType(completionType, symbols[0].kind, 0, tokens.length - 1)) - { - //trace("Swapping types"); - if (symbols.length == 0 || symbols[0].type is null || symbols[0].type is symbols[0]) - return []; - else if (symbols[0].type.kind == CompletionKind.functionName) - { - if (symbols[0].type.type is null) - symbols = []; - else - symbols = [symbols[0].type.type]; - } - else - symbols = [symbols[0].type]; - } - - loop: for (size_t i = 1; i < tokens.length; i++) - { - void skip(IdType open, IdType close) - { - i = skipEnd(tokens, i, open, close); - } - - switch (tokens[i].type) - { - case tok!"int": - case tok!"uint": - case tok!"long": - case tok!"ulong": - case tok!"char": - case tok!"wchar": - case tok!"dchar": - case tok!"bool": - case tok!"byte": - case tok!"ubyte": - case tok!"short": - case tok!"ushort": - case tok!"cent": - case tok!"ucent": - case tok!"float": - case tok!"ifloat": - case tok!"cfloat": - case tok!"idouble": - case tok!"cdouble": - case tok!"double": - case tok!"real": - case tok!"ireal": - case tok!"creal": - case tok!"this": - case tok!"super": - symbols = symbols[0].getPartsByName(internString(str(tokens[i].type))); - if (symbols.length == 0) - break loop; - break; - case tok!"identifier": - //trace(symbols[0].qualifier, " ", symbols[0].kind); - filterProperties(); - - if (symbols.length == 0) - break loop; - - // Use type instead of the symbol itself for certain symbol kinds - while (symbols[0].qualifier == SymbolQualifier.func - || symbols[0].kind == CompletionKind.functionName - || (symbols[0].kind == CompletionKind.moduleName - && symbols[0].type !is null && symbols[0].type.kind == CompletionKind.importSymbol) - || 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) - break loop; - } - - //trace("looking for ", tokens[i].text, " in ", symbols[0].name); - symbols = symbols[0].getPartsByName(internString(tokens[i].text)); - //trace("symbols: ", symbols.map!(a => a.name)); - filterProperties(); - if (symbols.length == 0) - { - //trace("Couldn't find it."); - break loop; - } - if (shouldSwapWithType(completionType, symbols[0].kind, i, tokens.length - 1)) - { - symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; - if (symbols.length == 0) - break loop; - } - if ((symbols[0].kind == CompletionKind.aliasName - || symbols[0].kind == CompletionKind.moduleName) - && (completionType == CompletionType.identifiers - || i + 1 < tokens.length)) - { - symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; - } - if (symbols.length == 0) - break loop; - if (tokens[i].type == tok!"!") - { - i++; - if (tokens[i].type == tok!"(") - goto case; - else - i++; - } - break; - case tok!"(": - skip(tok!"(", tok!")"); - break; - case tok!"[": - if (symbols[0].qualifier == SymbolQualifier.array) - { - skip(tok!"[", tok!"]"); - if (!isSliceExpression(tokens, i)) - { - symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; - if (symbols.length == 0) - break loop; - } - } - else if (symbols[0].qualifier == SymbolQualifier.assocArray) - { - symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; - skip(tok!"[", tok!"]"); - } - else - { - skip(tok!"[", tok!"]"); - DSymbol*[] overloads; - if (isSliceExpression(tokens, i)) - overloads = symbols[0].getPartsByName(internString("opSlice")); - else - overloads = symbols[0].getPartsByName(internString("opIndex")); - if (overloads.length > 0) - { - symbols = overloads[0].type is null ? [] : [overloads[0].type]; - } - else - return []; - } - break; - case tok!".": - break; - default: - break loop; - } - } - return symbols; -} - -/** - * - */ -void setCompletions(T)(ref AutocompleteResponse response, - Scope* completionScope, T tokens, size_t cursorPosition, - CompletionType completionType, bool isBracket = false, string partial = null) -{ - static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, string p, - 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(sym.name) - && sym.name[0] != '*') - { - r.completionKinds ~= sym.kind; - r.completions ~= sym.name.dup; - } - if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null) - addSymToResponse(sym.type, r, p, 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)))) - { - response.completionKinds ~= s.kind; - response.completions ~= s.name.dup; - } - response.completionType = CompletionType.identifiers; - return; - } - - if (tokens.length == 0) - return; - - DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens, - cursorPosition, completionType); - - 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); - response.completionType = CompletionType.identifiers; - } - else if (completionType == CompletionType.calltips) - { - //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 (isBracket) - { - 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) - { - 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])]; - return; - } - } - else - { - symbols = constructor; - goto setCallTips; - } - } - } - setCallTips: - response.completionType = CompletionType.calltips; - foreach (symbol; symbols) - { - if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null) - response.completions ~= symbol.callTip; - } - } -} - -string generateStructConstructorCalltip(const DSymbol* symbol) -in -{ - assert(symbol.kind == CompletionKind.structName); -} -body -{ - string generatedStructConstructorCalltip = "this("; - const(DSymbol)*[] fields = symbol.opSlice().filter!( - a => a.kind == CompletionKind.variableName).map!(a => cast(const(DSymbol)*) a).array(); - fields.sort!((a, b) => a.location < b.location); - foreach (i, field; fields) - { - if (field.kind != CompletionKind.variableName) - continue; - i++; - if (field.type !is null) - { - generatedStructConstructorCalltip ~= field.type.name; - generatedStructConstructorCalltip ~= " "; - } - generatedStructConstructorCalltip ~= field.name; - if (i < fields.length) - generatedStructConstructorCalltip ~= ", "; - } - generatedStructConstructorCalltip ~= ")"; - return generatedStructConstructorCalltip; -} - -private enum TYPE_IDENT_AND_LITERAL_CASES = q{ - case tok!"int": - case tok!"uint": - case tok!"long": - case tok!"ulong": - case tok!"char": - case tok!"wchar": - case tok!"dchar": - case tok!"bool": - case tok!"byte": - case tok!"ubyte": - case tok!"short": - case tok!"ushort": - case tok!"cent": - case tok!"ucent": - case tok!"float": - case tok!"ifloat": - case tok!"cfloat": - case tok!"idouble": - case tok!"cdouble": - case tok!"double": - case tok!"real": - case tok!"ireal": - case tok!"creal": - case tok!"this": - case tok!"super": - case tok!"identifier": - case tok!"stringLiteral": - case tok!"wstringLiteral": - case tok!"dstringLiteral": -}; - -bool isUdaExpression(T)(ref T tokens) -{ - bool result; - ptrdiff_t skip; - ptrdiff_t i = tokens.length - 2; - - if (i < 1) - return result; - - // skips the UDA ctor - if (tokens[i].type == tok!")") - { - ++skip; - --i; - while (i >= 2) - { - skip += tokens[i].type == tok!")"; - skip -= tokens[i].type == tok!"("; - --i; - if (skip == 0) - { - // @UDA!(TemplateParameters)(FunctionParameters) - if (i > 3 && tokens[i].type == tok!"!" && tokens[i-1].type == tok!")") - { - skip = 1; - i -= 2; - continue; - } - else break; - } - } - } - - if (skip == 0) - { - // @UDA!SingleTemplateParameter - if (i > 2 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"!") - { - i -= 2; - } - - // @UDA - if (i > 0 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"@") - { - result = true; - } - } - - return result; -} - -/** - * - */ -T getExpression(T)(T beforeTokens) -{ - enum EXPRESSION_LOOP_BREAK = q{ - if (i + 1 < beforeTokens.length) switch (beforeTokens[i + 1].type) - { - mixin (TYPE_IDENT_AND_LITERAL_CASES); - i++; - break expressionLoop; - default: - break; - } - }; - - if (beforeTokens.length == 0) - return beforeTokens[0 .. 0]; - size_t i = beforeTokens.length - 1; - size_t sliceEnd = beforeTokens.length; - IdType open; - IdType close; - uint skipCount = 0; - - expressionLoop: while (true) - { - switch (beforeTokens[i].type) - { - case tok!"import": - i++; - break expressionLoop; - mixin (TYPE_IDENT_AND_LITERAL_CASES); - mixin (EXPRESSION_LOOP_BREAK); - break; - case tok!".": - break; - case tok!")": - open = tok!")"; - close = tok!"("; - goto skip; - case tok!"]": - open = tok!"]"; - close = tok!"["; - skip: - mixin (EXPRESSION_LOOP_BREAK); - immutable bookmark = i; - int depth = 1; - do - { - if (depth == 0 || i == 0) - break; - else - i--; - if (beforeTokens[i].type == open) - depth++; - else if (beforeTokens[i].type == close) - depth--; - } while (true); - - skipCount++; - - // check the current token after skipping parens to the left. - // if it's a loop keyword, pretend we never skipped the parens. - if (i > 0) switch (beforeTokens[i - 1].type) - { - case tok!"scope": - case tok!"if": - case tok!"while": - case tok!"for": - case tok!"foreach": - case tok!"foreach_reverse": - case tok!"do": - case tok!"cast": - case tok!"catch": - i = bookmark + 1; - break expressionLoop; - case tok!"!": - if (skipCount == 1) - { - sliceEnd = i - 1; - i -= 2; - } - break expressionLoop; - default: - break; - } - break; - default: - i++; - break expressionLoop; - } - if (i == 0) - break; - else - i--; - } - return beforeTokens[i .. sliceEnd]; -} - -size_t goBackToOpenParen(T)(T beforeTokens) -in -{ - assert (beforeTokens.length > 0); -} -body -{ - size_t i = beforeTokens.length - 1; - IdType open; - IdType close; - while (true) switch (beforeTokens[i].type) - { - case tok!",": - case tok!".": - case tok!"*": - case tok!"&": - case tok!"doubleLiteral": - case tok!"floatLiteral": - case tok!"idoubleLiteral": - case tok!"ifloatLiteral": - case tok!"intLiteral": - case tok!"longLiteral": - case tok!"realLiteral": - case tok!"irealLiteral": - case tok!"uintLiteral": - case tok!"ulongLiteral": - case tok!"characterLiteral": - mixin(TYPE_IDENT_AND_LITERAL_CASES); - if (i == 0) - return size_t.max; - else - i--; - break; - case tok!"(": - case tok!"[": - return i + 1; - case tok!")": - open = tok!")"; - close = tok!"("; - goto skip; - case tok!"}": - open = tok!"}"; - close = tok!"{"; - goto skip; - case tok!"]": - open = tok!"]"; - close = tok!"["; - skip: - if (i == 0) - return size_t.max; - else - i--; - int depth = 1; - do - { - if (depth == 0 || i == 0) - break; - else - i--; - if (beforeTokens[i].type == open) - depth++; - else if (beforeTokens[i].type == close) - depth--; - } while (true); - break; - default: - return size_t.max; - } - return size_t.max; -} - -/** - * Params: - * completionType = the completion type being requested - * kind = the kind of the current item in the completion chain - * current = the index of the current item in the symbol chain - * max = the number of items in the symbol chain - * Returns: - * true if the symbol should be swapped with its type field - */ -bool shouldSwapWithType(CompletionType completionType, CompletionKind kind, - size_t current, size_t max) pure nothrow @safe -{ - // packages never have types, so always return false - if (kind == CompletionKind.packageName - || kind == CompletionKind.className - || kind == CompletionKind.structName - || kind == CompletionKind.interfaceName - || kind == CompletionKind.enumName - || kind == CompletionKind.unionName - || kind == CompletionKind.templateName - || kind == CompletionKind.keyword) - { - return false; - } - // Swap out every part of a chain with its type except the last part - if (current < max) - return true; - // Only swap out types for these kinds - immutable bool isInteresting = - kind == CompletionKind.variableName - || kind == CompletionKind.memberVariableName - || kind == CompletionKind.importSymbol - || kind == CompletionKind.aliasName - || kind == CompletionKind.enumMember - || kind == CompletionKind.functionName; - return isInteresting && (completionType == CompletionType.identifiers - || (completionType == completionType.calltips && kind == CompletionKind.variableName)) ; -} - -istring stringToken()(auto ref const Token a) -{ - return internString(a.text is null ? str(a.type) : a.text); -} - -//void dumpTokens(const Token[] tokens) -//{ - //foreach (t; tokens) - //writeln(t.line, ":", t.column, " ", stringToken(t)); -//} diff --git a/src/server/autocomplete/complete.d b/src/server/autocomplete/complete.d new file mode 100644 index 0000000..f3e0b73 --- /dev/null +++ b/src/server/autocomplete/complete.d @@ -0,0 +1,573 @@ +/** + * 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 . + */ + +module server.autocomplete.complete; + +import std.algorithm; +import std.array; +import std.conv; +import std.experimental.logger; +import std.file; +import std.path; +import std.string; +import std.typecons; + +import 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 common.constants; +import common.messages; + +/** + * 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(StringCache.defaultBucketCount); + auto beforeTokens = getTokensBeforeCursor(request.sourceCode, + request.cursorPosition, stringCache, tokenArray); + if (beforeTokens.length >= 2) + { + if (beforeTokens[$ - 1] == tok!"(" || beforeTokens[$ - 1] == tok!"[" + || beforeTokens[$ - 1] == tok!",") + { + immutable size_t end = goBackToOpenParen(beforeTokens); + if (end != size_t.max) + return parenCompletion(beforeTokens[0 .. end], tokenArray, + request.cursorPosition, moduleCache); + } + else + { + ImportKind kind = determineImportKind(beforeTokens); + if (kind == ImportKind.neither) + { + if (beforeTokens.isUdaExpression) + beforeTokens = beforeTokens[$ - 1 .. $]; + return dotCompletion(beforeTokens, tokenArray, request.cursorPosition, moduleCache); + } + else + 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]; + significantTokenType = tok!"identifier"; + 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.completionKinds ~= symbol.kind; + response.completions ~= symbol.name.dup; + } + response.completionType = CompletionType.identifiers; + break; + mixin(TYPE_IDENT_CASES); + case tok!")": + case tok!"]": + auto allocator = scoped!(ASTAllocator)(); + RollbackAllocator rba; + ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, + &rba, cursorPosition, moduleCache); + scope (exit) + pair.destroy(); + response.setCompletions(pair.scope_, getExpression(beforeTokens), + cursorPosition, CompletionType.identifiers, false, partial); + break; + case tok!"(": + case tok!"{": + case tok!"[": + case tok!";": + case tok!":": + break; + default: + break; + } + return response; +} + +/** + * Handles paren 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 + */ +AutocompleteResponse parenCompletion(T)(T beforeTokens, + const(Token)[] tokenArray, size_t cursorPosition, ref ModuleCache moduleCache) +{ + AutocompleteResponse response; + immutable(string)[] completions; + switch (beforeTokens[$ - 2].type) + { + 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 ~= completion; + response.completionKinds ~= CompletionKind.keyword; + } + 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); + auto allocator = scoped!(ASTAllocator)(); + RollbackAllocator rba; + ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, + &rba, cursorPosition, moduleCache); + scope (exit) + pair.destroy(); + auto expression = getExpression(beforeTokens[0 .. $ - 1]); + response.setCompletions(pair.scope_, expression, cursorPosition, + CompletionType.calltips, beforeTokens[$ - 1] == tok!"["); + break; + default: + break; + } + return response; +} + +/** + * 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); +} +body +{ + 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.completionKinds ~= sy.kind; + response.completions ~= sy.name; + 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 ~= n; + response.completionKinds ~= CompletionKind.moduleName; + } + } + 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 ~= n; + response.completionKinds ~= CompletionKind.moduleName; + } + else if (isDir(name)) + { + if (n[0] != '.' && (partial is null || n.startsWith(partial))) + { + response.completions ~= n; + response.completionKinds ~= exists(buildPath(name, "package.d")) || exists(buildPath(name, + "package.di")) ? CompletionKind.moduleName : CompletionKind.packageName; + } + } + } + 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, + bool isBracket = false, string partial = null) +{ + static void addSymToResponse(const(DSymbol)* s, ref AutocompleteResponse r, + string p, 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(sym.name) && sym.name[0] != '*') + { + r.completionKinds ~= sym.kind; + r.completions ~= sym.name.dup; + } + if (sym.kind == CompletionKind.importSymbol && !sym.skipOver && sym.type !is null) + addSymToResponse(sym.type, r, p, 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)))) + { + response.completionKinds ~= s.kind; + response.completions ~= s.name.dup; + } + response.completionType = CompletionType.identifiers; + return; + } + + if (tokens.length == 0) + return; + + DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens, + cursorPosition, completionType); + + 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); + response.completionType = CompletionType.identifiers; + } + else if (completionType == CompletionType.calltips) + { + //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 (isBracket) + { + 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) + { + 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])]; + return; + } + } + else + { + symbols = constructor; + goto setCallTips; + } + } + } + setCallTips: + response.completionType = CompletionType.calltips; + foreach (symbol; symbols) + { + if (symbol.kind != CompletionKind.aliasName && symbol.callTip !is null) + response.completions ~= symbol.callTip; + } + } +} + +string generateStructConstructorCalltip(const DSymbol* symbol) +in +{ + assert(symbol.kind == CompletionKind.structName); +} +body +{ + string generatedStructConstructorCalltip = "this("; + const(DSymbol)*[] fields = symbol.opSlice().filter!( + a => a.kind == CompletionKind.variableName).map!(a => cast(const(DSymbol)*) a).array(); + fields.sort!((a, b) => a.location < b.location); + foreach (i, field; fields) + { + if (field.kind != CompletionKind.variableName) + continue; + i++; + if (field.type !is null) + { + generatedStructConstructorCalltip ~= field.type.name; + generatedStructConstructorCalltip ~= " "; + } + generatedStructConstructorCalltip ~= field.name; + if (i < fields.length) + generatedStructConstructorCalltip ~= ", "; + } + generatedStructConstructorCalltip ~= ")"; + return generatedStructConstructorCalltip; +} diff --git a/src/server/autocomplete/doc.d b/src/server/autocomplete/doc.d new file mode 100644 index 0000000..c450580 --- /dev/null +++ b/src/server/autocomplete/doc.d @@ -0,0 +1,99 @@ +/** + * 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 . + */ + +module server.autocomplete.doc; + +import std.algorithm; +import std.array; +import std.experimental.logger; +import std.typecons; + +import server.autocomplete.util; + +import dparse.lexer; +import dparse.rollback_allocator; + +import dsymbol.modulecache; + +import common.messages; + +/** + * Gets documentation for the symbol at the cursor + * Params: + * request = the autocompletion request + * Returns: + * the autocompletion response + */ +public AutocompleteResponse getDoc(const AutocompleteRequest request, + ref ModuleCache moduleCache) +{ +// trace("Getting doc comments"); + AutocompleteResponse response; + RollbackAllocator rba; + auto allocator = scoped!(ASTAllocator)(); + auto cache = StringCache(StringCache.defaultBucketCount); + SymbolStuff stuff = getSymbolsForCompletion(request, CompletionType.ddoc, + allocator, &rba, cache, moduleCache); + if (stuff.symbols.length == 0) + warning("Could not find symbol"); + else + { + Appender!(char[]) app; + + bool isDitto(string s) + { + import std.uni : icmp; + if (s.length > 5) + return false; + else + return s.icmp("ditto") == 0; + } + + void putDDocChar(char c) + { + switch (c) + { + case '\\': + app.put('\\'); + app.put('\\'); + break; + case '\n': + app.put('\\'); + app.put('n'); + break; + default: + app.put(c); + break; + } + } + + void putDDocString(string s) + { + foreach (char c; s) + putDDocChar(c); + } + + foreach(ref symbol; stuff.symbols.filter!(a => !a.doc.empty && !isDitto(a.doc))) + { + app.clear; + putDDocString(symbol.doc); + response.docComments ~= app.data.idup; + } + } + return response; +} \ No newline at end of file diff --git a/src/server/autocomplete/localuse.d b/src/server/autocomplete/localuse.d new file mode 100644 index 0000000..3dca1bf --- /dev/null +++ b/src/server/autocomplete/localuse.d @@ -0,0 +1,120 @@ +/** + * 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 . + */ + +module server.autocomplete.localuse; + +import std.experimental.logger; +import std.range; +import std.typecons; + +import server.autocomplete.util; + +import dparse.lexer; +import dparse.rollback_allocator; + +import dsymbol.conversion; +import dsymbol.modulecache; +import dsymbol.symbol; + +import common.messages; + +/** + * Finds the uses of the symbol at the cursor position within a single document. + * Params: + * request = the autocompletion request. + * Returns: + * the autocompletion response. + */ +public AutocompleteResponse findLocalUse(AutocompleteRequest request, ref ModuleCache moduleCache) +{ + AutocompleteResponse response; + RollbackAllocator rba; + auto allocator = scoped!(ASTAllocator)(); + auto cache = StringCache(StringCache.defaultBucketCount); + + // patchs the original request for the subsequent requests + request.kind = RequestKind.symbolLocation; + + // getSymbolsForCompletion() copy to avoid repetitive parsing + LexerConfig config; + config.fileName = ""; + const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, config, &cache); + SymbolStuff getSymbolsAtCursor(size_t cursorPosition) + { + auto sortedTokens = assumeSorted(tokenArray); + auto beforeTokens = sortedTokens.lowerBound(cursorPosition); + ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, + &rba, request.cursorPosition, moduleCache); + auto expression = getExpression(beforeTokens); + return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, + cursorPosition, CompletionType.location), pair.symbol, pair.scope_); + } + + // gets the symbol matching to cursor pos + SymbolStuff stuff = getSymbolsAtCursor(cast(size_t) request.cursorPosition); + scope (exit) + stuff.destroy(); + + // starts searching only if no ambiguity with the symbol + if (stuff.symbols.length == 1) + { + const(DSymbol*) sourceSymbol = stuff.symbols[0]; + response.symbolLocation = sourceSymbol.location; + response.symbolFilePath = sourceSymbol.symbolFile.idup; + + // gets the source token to avoid too much getSymbolsAtCursor() + const(Token)* sourceToken; + foreach (i, t; tokenArray) + { + if (t.type != tok!"identifier") + continue; + if (request.cursorPosition >= t.index && request.cursorPosition < t.index + t.text.length) + { + sourceToken = tokenArray.ptr + i; + break; + } + } + + // finds the tokens that match to the source symbol + if (sourceToken != null) + foreach (t; tokenArray) + { + if (t.type == tok!"identifier" && t.text == sourceToken.text) + { + size_t pos = cast(size_t) t.index + 1; // place cursor inside the token + SymbolStuff candidate = getSymbolsAtCursor(pos); + scope (exit) + candidate.destroy(); + if (candidate.symbols.length == 1 && candidate.symbols[0].location == sourceSymbol.location + && candidate.symbols[0].symbolFile == sourceSymbol.symbolFile) + { + response.locations ~= t.index; + } + } + } + else + { + warning("The source token is not an identifier"); + } + } + else + { + warning("No or ambiguous symbol for the identifier at cursor"); + } + return response; +} diff --git a/src/server/autocomplete/package.d b/src/server/autocomplete/package.d new file mode 100644 index 0000000..502305e --- /dev/null +++ b/src/server/autocomplete/package.d @@ -0,0 +1,26 @@ +/** + * 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 . + */ + +module server.autocomplete; + +public: + +import server.autocomplete.complete; +import server.autocomplete.doc; +import server.autocomplete.localuse; +import server.autocomplete.symbols; diff --git a/src/server/autocomplete/symbols.d b/src/server/autocomplete/symbols.d new file mode 100644 index 0000000..17e5a13 --- /dev/null +++ b/src/server/autocomplete/symbols.d @@ -0,0 +1,127 @@ +/** + * 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 . + */ + +module server.autocomplete.symbols; + +import std.experimental.logger; +import std.typecons; + +import server.autocomplete.util; + +import dparse.lexer; +import dparse.rollback_allocator; + +import dsymbol.conversion; +import dsymbol.modulecache; +import dsymbol.string_interning; +import dsymbol.symbol; + +import common.messages; + +import containers.hashset; + +/** + * Finds the declaration of the symbol at the cursor position. + * Params: + * request = the autocompletion request + * Returns: + * the autocompletion response + */ +public AutocompleteResponse findDeclaration(const AutocompleteRequest request, + ref ModuleCache moduleCache) +{ + AutocompleteResponse response; + RollbackAllocator rba; + auto allocator = scoped!(ASTAllocator)(); + auto cache = StringCache(StringCache.defaultBucketCount); + SymbolStuff stuff = getSymbolsForCompletion(request, CompletionType.location, + allocator, &rba, cache, moduleCache); + scope (exit) + stuff.destroy(); + if (stuff.symbols.length > 0) + { + response.symbolLocation = stuff.symbols[0].location; + response.symbolFilePath = stuff.symbols[0].symbolFile.idup; + } + else + warning("Could not find symbol declaration"); + return response; +} + +/** + * + */ +public AutocompleteResponse symbolSearch(const AutocompleteRequest request, + ref ModuleCache moduleCache) +{ + import containers.ttree : TTree; + + LexerConfig config; + config.fileName = ""; + auto cache = StringCache(StringCache.defaultBucketCount); + const(Token)[] tokenArray = getTokensForParser(cast(ubyte[]) request.sourceCode, config, &cache); + auto allocator = scoped!(ASTAllocator)(); + RollbackAllocator rba; + ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, + &rba, request.cursorPosition, moduleCache); + scope (exit) + pair.destroy(); + + static struct SearchResults + { + void put(const(DSymbol)* symbol) + { + tree.insert(SearchResult(symbol)); + } + + static struct SearchResult + { + const(DSymbol)* symbol; + + int opCmp(ref const SearchResult other) const pure nothrow + { + if (other.symbol.symbolFile < symbol.symbolFile) + return -1; + if (other.symbol.symbolFile > symbol.symbolFile) + return 1; + if (other.symbol.location < symbol.location) + return -1; + return other.symbol.location > symbol.location; + } + } + + TTree!(SearchResult) tree; + } + + SearchResults results; + HashSet!size_t visited; + foreach (symbol; pair.scope_.symbols) + symbol.getParts!SearchResults(internString(request.searchName), results, visited); + foreach (s; moduleCache.getAllSymbols()) + s.symbol.getParts!SearchResults(internString(request.searchName), results, visited); + + AutocompleteResponse response; + foreach (result; results.tree[]) + { + response.locations ~= result.symbol.location; + response.completionKinds ~= result.symbol.kind; + response.completions ~= result.symbol.symbolFile; + } + + return response; +} diff --git a/src/server/autocomplete/util.d b/src/server/autocomplete/util.d new file mode 100644 index 0000000..c7cd966 --- /dev/null +++ b/src/server/autocomplete/util.d @@ -0,0 +1,731 @@ +/** + * 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 . + */ + +module server.autocomplete.util; + +import std.algorithm; +import std.experimental.allocator; +import std.experimental.logger; +import std.range; +import std.string; +import std.typecons; + +import common.messages; + +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; + +enum ImportKind : ubyte +{ + selective, + normal, + neither +} + +struct SymbolStuff +{ + void destroy() + { + typeid(DSymbol).destroy(symbol); + typeid(Scope).destroy(scope_); + } + + DSymbol*[] symbols; + DSymbol* symbol; + Scope* scope_; +} + +/** + * Params: + * completionType = the completion type being requested + * kind = the kind of the current item in the completion chain + * current = the index of the current item in the symbol chain + * max = the number of items in the symbol chain + * Returns: + * true if the symbol should be swapped with its type field + */ +bool shouldSwapWithType(CompletionType completionType, CompletionKind kind, + size_t current, size_t max) pure nothrow @safe +{ + // packages never have types, so always return false + if (kind == CompletionKind.packageName + || kind == CompletionKind.className + || kind == CompletionKind.structName + || kind == CompletionKind.interfaceName + || kind == CompletionKind.enumName + || kind == CompletionKind.unionName + || kind == CompletionKind.templateName + || kind == CompletionKind.keyword) + { + return false; + } + // Swap out every part of a chain with its type except the last part + if (current < max) + return true; + // Only swap out types for these kinds + immutable bool isInteresting = + kind == CompletionKind.variableName + || kind == CompletionKind.memberVariableName + || kind == CompletionKind.importSymbol + || kind == CompletionKind.aliasName + || kind == CompletionKind.enumMember + || kind == CompletionKind.functionName; + return isInteresting && (completionType == CompletionType.identifiers + || (completionType == completionType.calltips && kind == CompletionKind.variableName)) ; +} + +istring stringToken()(auto ref const Token a) +{ + return internString(a.text is null ? str(a.type) : a.text); +} + +//void dumpTokens(const Token[] tokens) +//{ + //foreach (t; tokens) + //writeln(t.line, ":", t.column, " ", stringToken(t)); +//} + +/** + * Params: + * sourceCode = the source code of the file being edited + * cursorPosition = the cursor position in bytes + * Returns: + * a sorted range of tokens before the cursor position + */ +auto getTokensBeforeCursor(const(ubyte[]) sourceCode, size_t cursorPosition, + ref StringCache cache, out const(Token)[] tokenArray) +{ + LexerConfig config; + config.fileName = ""; + tokenArray = getTokensForParser(cast(ubyte[]) sourceCode, config, &cache); + auto sortedTokens = assumeSorted(tokenArray); + return sortedTokens.lowerBound(cast(size_t) cursorPosition); +} + +/** + * Params: + * request = the autocompletion request + * type = type the autocompletion type + * Returns: + * all symbols that should be considered for the autocomplete list based on + * the request's source code, cursor position, and completion type. + */ +SymbolStuff getSymbolsForCompletion(const AutocompleteRequest request, + const CompletionType type, IAllocator allocator, RollbackAllocator* rba, + ref StringCache cache, ref ModuleCache moduleCache) +{ + const(Token)[] tokenArray; + auto beforeTokens = getTokensBeforeCursor(request.sourceCode, + request.cursorPosition, cache, tokenArray); + ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, allocator, + rba, request.cursorPosition, moduleCache); + auto expression = getExpression(beforeTokens); + return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, + request.cursorPosition, type), pair.symbol, pair.scope_); +} + +bool isSliceExpression(T)(T tokens, size_t index) +{ + while (index < tokens.length) switch (tokens[index].type) + { + case tok!"[": + tokens.skipParen(index, tok!"[", tok!"]"); + break; + case tok!"(": + tokens.skipParen(index, tok!"(", tok!")"); + break; + case tok!"]": + case tok!"}": + return false; + case tok!"..": + return true; + default: + index++; + break; + } + return false; +} + +/** + * + */ +DSymbol*[] getSymbolsByTokenChain(T)(Scope* completionScope, + T tokens, size_t cursorPosition, CompletionType completionType) +{ + //writeln(">>>"); + //dumpTokens(tokens.release); + //writeln(">>>"); + + // Find the symbol corresponding to the beginning of the chain + DSymbol*[] symbols; + if (tokens.length == 0) + return []; + // Recurse in case the symbol chain starts with an expression in parens + // e.g. (a.b!c).d + if (tokens[0] == tok!"(") + { + size_t j = 0; + tokens.skipParen(j, tok!"(", tok!")"); + symbols = getSymbolsByTokenChain(completionScope, tokens[1 .. j], + cursorPosition, completionType); + tokens = tokens[j + 1 .. $]; + //writeln("<<<"); + //dumpTokens(tokens.release); + //writeln("<<<"); + if (tokens.length == 0) // workaround (#371) + return []; + } + else if (tokens[0] == tok!"." && tokens.length > 1) + { + tokens = tokens[1 .. $]; + if (tokens.length == 0) // workaround (#371) + return []; + symbols = completionScope.getSymbolsAtGlobalScope(stringToken(tokens[0])); + } + else + symbols = completionScope.getSymbolsByNameAndCursor(stringToken(tokens[0]), cursorPosition); + + if (symbols.length == 0) + { + //TODO: better bugfix for issue #368, see test case 52 or pull #371 + if (tokens.length) + warning("Could not find declaration of ", stringToken(tokens[0]), + " from position ", cursorPosition); + else assert(0, "internal error"); + return []; + } + + // If the `symbols` array contains functions, and one of them returns + // void and the others do not, this is a property function. For the + // purposes of chaining auto-complete we want to ignore the one that + // returns void. This is a no-op if we are getting doc comments. + void filterProperties() @nogc @safe + { + if (symbols.length == 0 || completionType == CompletionType.ddoc) + return; + if (symbols[0].kind == CompletionKind.functionName + || symbols[0].qualifier == SymbolQualifier.func) + { + int voidRets = 0; + int nonVoidRets = 0; + size_t firstNonVoidIndex = size_t.max; + foreach (i, sym; symbols) + { + if (sym.type is null) + return; + if (&sym.type.name[0] == &getBuiltinTypeName(tok!"void")[0]) + voidRets++; + else + { + nonVoidRets++; + firstNonVoidIndex = min(firstNonVoidIndex, i); + } + } + if (voidRets > 0 && nonVoidRets > 0) + symbols = symbols[firstNonVoidIndex .. $]; + } + } + + filterProperties(); + + if (shouldSwapWithType(completionType, symbols[0].kind, 0, tokens.length - 1)) + { + //trace("Swapping types"); + if (symbols.length == 0 || symbols[0].type is null || symbols[0].type is symbols[0]) + return []; + else if (symbols[0].type.kind == CompletionKind.functionName) + { + if (symbols[0].type.type is null) + symbols = []; + else + symbols = [symbols[0].type.type]; + } + else + symbols = [symbols[0].type]; + } + + loop: for (size_t i = 1; i < tokens.length; i++) + { + void skip(IdType open, IdType close) + { + tokens.skipParen(i, open, close); + } + + switch (tokens[i].type) + { + case tok!"int": + case tok!"uint": + case tok!"long": + case tok!"ulong": + case tok!"char": + case tok!"wchar": + case tok!"dchar": + case tok!"bool": + case tok!"byte": + case tok!"ubyte": + case tok!"short": + case tok!"ushort": + case tok!"cent": + case tok!"ucent": + case tok!"float": + case tok!"ifloat": + case tok!"cfloat": + case tok!"idouble": + case tok!"cdouble": + case tok!"double": + case tok!"real": + case tok!"ireal": + case tok!"creal": + case tok!"this": + case tok!"super": + symbols = symbols[0].getPartsByName(internString(str(tokens[i].type))); + if (symbols.length == 0) + break loop; + break; + case tok!"identifier": + //trace(symbols[0].qualifier, " ", symbols[0].kind); + filterProperties(); + + if (symbols.length == 0) + break loop; + + // Use type instead of the symbol itself for certain symbol kinds + while (symbols[0].qualifier == SymbolQualifier.func + || symbols[0].kind == CompletionKind.functionName + || (symbols[0].kind == CompletionKind.moduleName + && symbols[0].type !is null && symbols[0].type.kind == CompletionKind.importSymbol) + || 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) + break loop; + } + + //trace("looking for ", tokens[i].text, " in ", symbols[0].name); + symbols = symbols[0].getPartsByName(internString(tokens[i].text)); + //trace("symbols: ", symbols.map!(a => a.name)); + filterProperties(); + if (symbols.length == 0) + { + //trace("Couldn't find it."); + break loop; + } + if (shouldSwapWithType(completionType, symbols[0].kind, i, tokens.length - 1)) + { + symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; + if (symbols.length == 0) + break loop; + } + if ((symbols[0].kind == CompletionKind.aliasName + || symbols[0].kind == CompletionKind.moduleName) + && (completionType == CompletionType.identifiers + || i + 1 < tokens.length)) + { + symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; + } + if (symbols.length == 0) + break loop; + if (tokens[i].type == tok!"!") + { + i++; + if (tokens[i].type == tok!"(") + goto case; + else + i++; + } + break; + case tok!"(": + skip(tok!"(", tok!")"); + break; + case tok!"[": + if (symbols[0].qualifier == SymbolQualifier.array) + { + skip(tok!"[", tok!"]"); + if (!isSliceExpression(tokens, i)) + { + symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; + if (symbols.length == 0) + break loop; + } + } + else if (symbols[0].qualifier == SymbolQualifier.assocArray) + { + symbols = symbols[0].type is null || symbols[0].type is symbols[0] ? [] : [symbols[0].type]; + skip(tok!"[", tok!"]"); + } + else + { + skip(tok!"[", tok!"]"); + DSymbol*[] overloads; + if (isSliceExpression(tokens, i)) + overloads = symbols[0].getPartsByName(internString("opSlice")); + else + overloads = symbols[0].getPartsByName(internString("opIndex")); + if (overloads.length > 0) + { + symbols = overloads[0].type is null ? [] : [overloads[0].type]; + } + else + return []; + } + break; + case tok!".": + break; + default: + break loop; + } + } + return symbols; +} + +enum TYPE_IDENT_CASES = q{ + case tok!"int": + case tok!"uint": + case tok!"long": + case tok!"ulong": + case tok!"char": + case tok!"wchar": + case tok!"dchar": + case tok!"bool": + case tok!"byte": + case tok!"ubyte": + case tok!"short": + case tok!"ushort": + case tok!"cent": + case tok!"ucent": + case tok!"float": + case tok!"ifloat": + case tok!"cfloat": + case tok!"idouble": + case tok!"cdouble": + case tok!"double": + case tok!"real": + case tok!"ireal": + case tok!"creal": + case tok!"this": + case tok!"super": + case tok!"identifier": +}; + +enum STRING_LITERAL_CASES = q{ + case tok!"stringLiteral": + case tok!"wstringLiteral": + case tok!"dstringLiteral": +}; + +enum TYPE_IDENT_AND_LITERAL_CASES = TYPE_IDENT_CASES ~ STRING_LITERAL_CASES; + +/** + * + */ +T getExpression(T)(T beforeTokens) +{ + enum EXPRESSION_LOOP_BREAK = q{ + if (i + 1 < beforeTokens.length) switch (beforeTokens[i + 1].type) + { + mixin (TYPE_IDENT_AND_LITERAL_CASES); + i++; + break expressionLoop; + default: + break; + } + }; + + if (beforeTokens.length == 0) + return beforeTokens[0 .. 0]; + size_t i = beforeTokens.length - 1; + size_t sliceEnd = beforeTokens.length; + IdType open; + IdType close; + uint skipCount = 0; + + expressionLoop: while (true) + { + switch (beforeTokens[i].type) + { + case tok!"import": + i++; + break expressionLoop; + mixin (TYPE_IDENT_AND_LITERAL_CASES); + mixin (EXPRESSION_LOOP_BREAK); + break; + case tok!".": + break; + case tok!")": + open = tok!")"; + close = tok!"("; + goto skip; + case tok!"]": + open = tok!"]"; + close = tok!"["; + skip: + mixin (EXPRESSION_LOOP_BREAK); + immutable bookmark = i; + beforeTokens.skipParenReverse(i, open, close); + + skipCount++; + + // check the current token after skipping parens to the left. + // if it's a loop keyword, pretend we never skipped the parens. + if (i > 0) switch (beforeTokens[i - 1].type) + { + case tok!"scope": + case tok!"if": + case tok!"while": + case tok!"for": + case tok!"foreach": + case tok!"foreach_reverse": + case tok!"do": + case tok!"cast": + case tok!"catch": + i = bookmark + 1; + break expressionLoop; + case tok!"!": + if (skipCount == 1) + { + sliceEnd = i - 1; + i -= 2; + } + break expressionLoop; + default: + break; + } + break; + default: + i++; + break expressionLoop; + } + if (i == 0) + break; + else + i--; + } + return beforeTokens[i .. sliceEnd]; +} + +/** + * Determines if an import is selective, whole-module, or neither. + */ +ImportKind determineImportKind(T)(T tokens) +{ + assert(tokens.length > 1); + size_t i = tokens.length - 1; + if (!(tokens[i] == tok!":" || tokens[i] == tok!"," || tokens[i] == tok!"." + || tokens[i] == tok!"identifier")) + return ImportKind.neither; + bool foundColon = false; + while (true) switch (tokens[i].type) + { + case tok!":": + foundColon = true; + goto case; + case tok!"identifier": + case tok!"=": + case tok!".": + case tok!",": + if (i == 0) + return ImportKind.neither; + else + i--; + break; + case tok!"import": + return foundColon ? ImportKind.selective : ImportKind.normal; + default: + return ImportKind.neither; + } + return ImportKind.neither; +} + +unittest +{ + import std.stdio : writeln; + + Token[] t = [ + Token(tok!"import"), Token(tok!"identifier"), Token(tok!"."), + Token(tok!"identifier"), Token(tok!":"), Token(tok!"identifier"), Token(tok!",") + ]; + assert(determineImportKind(t) == ImportKind.selective); + Token[] t2; + t2 ~= Token(tok!"else"); + t2 ~= Token(tok!":"); + assert(determineImportKind(t2) == ImportKind.neither); + writeln("Unittest for determineImportKind() passed"); +} + +bool isUdaExpression(T)(ref T tokens) +{ + bool result; + ptrdiff_t skip; + auto i = cast(ptrdiff_t) tokens.length - 2; + + if (i < 1) + return result; + + // skips the UDA ctor + if (tokens[i].type == tok!")") + { + skip++; + i--; + while (i >= 2) + { + skip += tokens[i].type == tok!")"; + skip -= tokens[i].type == tok!"("; + i--; + if (skip == 0) + { + // @UDA!(TemplateParameters)(FunctionParameters) + if (i > 3 && tokens[i].type == tok!"!" && tokens[i-1].type == tok!")") + { + skip = 1; + i -= 2; + continue; + } + else break; + } + } + } + + if (skip == 0) + { + // @UDA!SingleTemplateParameter + if (i > 2 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"!") + i -= 2; + + // @UDA + if (i > 0 && tokens[i].type == tok!"identifier" && tokens[i-1].type == tok!"@") + result = true; + } + + return result; +} + +/** + * Traverses a token slice in reverse to find the opening parentheses or square bracket + * that begins the block the last token is in. + */ +size_t goBackToOpenParen(T)(T beforeTokens) +in +{ + assert (beforeTokens.length > 0); +} +body +{ + size_t i = beforeTokens.length - 1; + + while (true) switch (beforeTokens[i].type) + { + case tok!",": + case tok!".": + case tok!"*": + case tok!"&": + case tok!"doubleLiteral": + case tok!"floatLiteral": + case tok!"idoubleLiteral": + case tok!"ifloatLiteral": + case tok!"intLiteral": + case tok!"longLiteral": + case tok!"realLiteral": + case tok!"irealLiteral": + case tok!"uintLiteral": + case tok!"ulongLiteral": + case tok!"characterLiteral": + mixin(TYPE_IDENT_AND_LITERAL_CASES); + if (i == 0) + return size_t.max; + else + i--; + break; + case tok!"(": + case tok!"[": + return i + 1; + case tok!")": + beforeTokens.skipParenReverse!true(i, tok!")", tok!"("); + break; + case tok!"}": + beforeTokens.skipParenReverse!true(i, tok!"}", tok!"{"); + break; + case tok!"]": + beforeTokens.skipParenReverse!true(i, tok!"]", tok!"["); + break; + default: + return size_t.max; + } +} + +/** + * Skips blocks of parentheses until the starting block has been closed + */ +void skipParen(T)(T tokenSlice, ref size_t i, IdType open, IdType close) +{ + if (i >= tokenSlice.length || tokenSlice.length <= 0) + return; + int depth = 1; + while (depth != 0 && i + 1 != tokenSlice.length) + { + i++; + if (tokenSlice[i].type == open) + depth++; + else if (tokenSlice[i].type == close) + depth--; + } +} + +/** + * Skips blocks of parentheses in reverse until the starting block has been opened + */ +void skipParenReverse(bool before = false, T)(T beforeTokens, ref size_t i, IdType open, IdType close) +{ + if (i == 0) + return; + int depth = 1; + while (depth != 0 && i != 0) + { + i--; + if (beforeTokens[i].type == open) + depth++; + else if (beforeTokens[i].type == close) + depth--; + } + static if (before) + if (i != 0) + i--; +} + +/// +unittest +{ + Token[] t = [ + Token(tok!"identifier"), Token(tok!"identifier"), Token(tok!"("), + Token(tok!"identifier"), Token(tok!"("), Token(tok!")"), Token(tok!",") + ]; + size_t i = t.length - 1; + skipParenReverse!false(t, i, tok!")", tok!"("); + assert(i == 2); + i = t.length - 1; + skipParenReverse!true(t, i, tok!")", tok!"("); + assert(i == 1); +} diff --git a/src/server/main.d b/src/server/main.d index 6c9be84..3a01e7c 100644 --- a/src/server/main.d +++ b/src/server/main.d @@ -113,7 +113,7 @@ int main(string[] args) return 1; } - if (serverIsRunning(useTCP, socketFile, port)) + if (serverIsRunning(useTCP, socketFile, port)) { fatal("Another instance of DCD-server is already running"); return 1; @@ -131,7 +131,7 @@ int main(string[] args) socket = new TcpSocket(AddressFamily.INET); socket.blocking = true; socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); - socket.bind(new InternetAddress("localhost", port)); + socket.bind(new InternetAddress("127.0.0.1", port)); info("Listening on port ", port); } else @@ -182,39 +182,21 @@ int main(string[] args) // No relative paths version (Posix) chdir("/"); - version (LittleEndian) - immutable expectedClient = IPv4Union([1, 0, 0, 127]); - else - immutable expectedClient = IPv4Union([127, 0, 0, 1]); - serverLoop: while (true) { auto s = socket.accept(); s.blocking = true; - if (useTCP) - { - // Only accept connections from localhost - IPv4Union actual; - InternetAddress clientAddr = cast(InternetAddress) s.remoteAddress(); - actual.i = clientAddr.addr; - // Shut down if somebody tries connecting from outside - if (actual.i != expectedClient.i) - { - fatal("Connection attempted from ", clientAddr.toAddrString()); - return 1; - } - } - scope (exit) { s.shutdown(SocketShutdown.BOTH); s.close(); } - ptrdiff_t bytesReceived = s.receive(buffer); - auto requestWatch = StopWatch(AutoStart.yes); + sw.reset(); + sw.start(); + auto bytesReceived = s.receive(buffer); size_t messageLength; // bit magic! (cast(ubyte*) &messageLength)[0..size_t.sizeof] = buffer[0..size_t.sizeof]; @@ -223,20 +205,15 @@ int main(string[] args) immutable b = s.receive(buffer[bytesReceived .. $]); if (b == Socket.ERROR) { - bytesReceived = Socket.ERROR; - break; + warning("Socket recieve failed"); + continue serverLoop; } bytesReceived += b; } - if (bytesReceived == Socket.ERROR) - { - warning("Socket recieve failed"); - break; - } - AutocompleteRequest request; msgpack.unpack(buffer[size_t.sizeof .. bytesReceived], request); + if (request.kind & RequestKind.clearCache) { info("Clearing cache."); @@ -249,101 +226,63 @@ int main(string[] args) } else if (request.kind & RequestKind.query) { - AutocompleteResponse response; - response.completionType = "ack"; - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); + s.sendResponse(AutocompleteResponse.ack); continue; } + if (request.kind & RequestKind.addImport) { cache.addImportPaths(request.importPaths); } + if (request.kind & RequestKind.listImports) { AutocompleteResponse response; response.importPaths = cache.getImportPaths().map!(a => cast() a).array(); - ubyte[] responseBytes = msgpack.pack(response); info("Returning import path list"); - s.send(responseBytes); + s.sendResponse(response); } else if (request.kind & RequestKind.autocomplete) { info("Getting completions"); - AutocompleteResponse response = complete(request, cache); - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); + s.sendResponse(complete(request, cache)); } else if (request.kind & RequestKind.doc) { info("Getting doc comment"); - try - { - AutocompleteResponse response = getDoc(request, cache); - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); - } - catch (Exception e) - { - warning("Could not get DDoc information", e.msg); - } + s.trySendResponse(getDoc(request, cache), "Could not get DDoc information"); } else if (request.kind & RequestKind.symbolLocation) - { - try - { - AutocompleteResponse response = findDeclaration(request, cache); - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); - } - catch (Exception e) - { - warning("Could not get symbol location", e.msg); - } - } + s.trySendResponse(findDeclaration(request, cache), "Could not get symbol location"); else if (request.kind & RequestKind.search) - { - AutocompleteResponse response = symbolSearch(request, cache); - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); - } - else if (request.kind & RequestKind.localUse) - { - try - { - AutocompleteResponse response = findLocalUse(request, cache); - ubyte[] responseBytes = msgpack.pack(response); - s.send(responseBytes); - } - catch (Exception e) - { - warning("Could not find local usage", e.msg); - } - } - info("Request processed in ", requestWatch.peek().to!("msecs", float), " milliseconds"); + s.sendResponse(symbolSearch(request, cache)); + else if (request.kind & RequestKind.localUse) + s.trySendResponse(findLocalUse(request, cache), "Could not find local usage"); + + sw.stop(); + info("Request processed in ", sw.peek().to!("msecs", float), " milliseconds"); } return 0; } -/// IP v4 address as bytes and a uint -union IPv4Union +/// Lazily evaluates a response with an exception handler and sends it to a socket or logs msg if evaluating response fails. +void trySendResponse(Socket socket, lazy AutocompleteResponse response, string msg) { - /// the bytes - ubyte[4] b; - /// the uint - uint i; + try + { + sendResponse(socket, response); + } + catch (Exception e) + { + warningf("%s: %s", msg, e.msg); + } } -import std.regex : ctRegex; -alias envVarRegex = ctRegex!(`\$\{([_a-zA-Z][_a-zA-Z 0-9]*)\}`); - -private unittest +/// Packs an AutocompleteResponse and sends it to a socket. +void sendResponse(Socket socket, AutocompleteResponse response) { - import std.regex : replaceAll; - - enum input = `${HOME}/aaa/${_bb_b}/ccc`; - - assert(replaceAll!(m => m[1])(input, envVarRegex) == `HOME/aaa/_bb_b/ccc`); + ubyte[] responseBytes = msgpack.pack(response); + socket.send(responseBytes); } /** diff --git a/src/server/server.d b/src/server/server.d index b01d28f..69bd51f 100644 --- a/src/server/server.d +++ b/src/server/server.d @@ -70,8 +70,8 @@ string getConfigurationLocation() */ void warnAboutOldConfigLocation() { - version (linux) if ("~/.config/dcd".expandTilde().exists() - && "~/.config/dcd".expandTilde().isFile()) + const configPath = "~/.config/dcd".expandTilde; + version (linux) if (configPath.exists && configPath.isFile) { warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); warning("!! Upgrade warning:");