diff --git a/src/dcd/server/autocomplete/calltip_utils.d b/src/dcd/server/autocomplete/calltip_utils.d new file mode 100644 index 0000000..be48b33 --- /dev/null +++ b/src/dcd/server/autocomplete/calltip_utils.d @@ -0,0 +1,107 @@ +module dcd.server.autocomplete.calltip_utils; + +import std.string; +import std.regex; +import std.range : empty; +import std.experimental.logger; +import std.algorithm : canFind; + +/** + * Extracting the first argument type + * which isn't lazy, return, scope etc + * Params: + * text = the string we want to extract from + * Returns: first type in the text + */ +string extractFirstArgType(string text) +{ + // Then match the first word that isn't lazy return scope ... etc. + auto firstWordRegex = regex(`(?!lazy|return|scope|in|out|ref|const|immutable\b)\b\w+`); + + auto matchFirstType = matchFirst(text, firstWordRegex); + string firstArgument = matchFirstType.captures.back; + return firstArgument.empty ? "" : firstArgument; + +} + +/** + * + * Params: + * callTip = the symbols calltip + * Returns: the first argument type of the calltip + */ +string getFirstArgumentOfFunction(string callTip) +{ + auto splitParentheses = callTip.split('('); + + // First match all inside the parentheses + auto insideParenthesesRegex = regex(`\((.*\))`); + auto match = matchFirst(callTip, insideParenthesesRegex); + string insideParentheses = match.captures.back; + + if (insideParentheses.empty) + { + return ""; + } + + return extractFirstArgType(insideParentheses); + +} + +string removeFirstArgumentOfFunction(string callTip) +{ + auto parentheseSplit = callTip.split('('); + // has only one argument + if (!callTip.canFind(',')) + { + return parentheseSplit[0] ~ "()"; + } + auto commaSplit = parentheseSplit[1].split(','); + string newCallTip = callTip.replace((commaSplit[0] ~ ", "), ""); + return newCallTip; + +} + +unittest +{ + auto result = getFirstArgumentOfFunction("void fooFunction(ref const(Foo) bar)"); + assert(result, "Foo"); +} + +unittest +{ + auto result = getFirstArgumentOfFunction("void fooFunction(Foo foo, string message)"); + assert(result, "Foo"); +} + +unittest +{ + auto result = getFirstArgumentOfFunction("void fooFunction(ref immutable(Foo) bar)"); + assert(result, "Foo"); +} + +unittest +{ + auto result = getFirstArgumentOfFunction("void fooFunction(const(immutable(Foo)) foo)"); + assert(result, "Foo"); +} + +unittest +{ + auto result = removeFirstArgumentOfFunction("void fooFunction(const(immutable(Foo)) foo)"); + assert(result, "void fooFunction()"); +} + +unittest +{ + auto result = removeFirstArgumentOfFunction( + "void fooFunction(const(immutable(Foo)) foo), string message"); + assert(result, "void fooFunction(string message)"); +} + +unittest +{ + auto result = removeFirstArgumentOfFunction( + "void fooFunction(const(immutable(Foo)) foo), string message, ref int age"); + assert(result, "void fooFunction(string message, ref int age)"); +} diff --git a/src/dcd/server/autocomplete/complete.d b/src/dcd/server/autocomplete/complete.d index c761bce..3086238 100644 --- a/src/dcd/server/autocomplete/complete.d +++ b/src/dcd/server/autocomplete/complete.d @@ -30,6 +30,7 @@ import std.string; import std.typecons; import dcd.server.autocomplete.util; +import dcd.server.autocomplete.ufcs; import dparse.lexer; import dparse.rollback_allocator; @@ -201,7 +202,7 @@ AutocompleteResponse dotCompletion(T)(T beforeTokens, const(Token)[] tokenArray, } else if (beforeTokens.length >= 2 && beforeTokens[$ - 1] == tok!".") significantTokenType = beforeTokens[$ - 2].type; - else + else return response; switch (significantTokenType) @@ -324,7 +325,7 @@ in { assert (beforeTokens.length >= 2); } -body +do { AutocompleteResponse response; if (beforeTokens.length <= 2) @@ -574,6 +575,7 @@ void setCompletions(T)(ref AutocompleteResponse response, } addSymToResponse(symbols[0], response, partial, completionScope); response.completionType = CompletionType.identifiers; + lookupUFCS(completionScope, symbols[0], cursorPosition, response); } else if (completionType == CompletionType.calltips) { @@ -669,7 +671,7 @@ in { assert(symbol.kind == CompletionKind.structName); } -body +do { string generatedStructConstructorCalltip = "this("; const(DSymbol)*[] fields = symbol.opSlice().filter!( diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d new file mode 100644 index 0000000..7e1108c --- /dev/null +++ b/src/dcd/server/autocomplete/ufcs.d @@ -0,0 +1,237 @@ +module dcd.server.autocomplete.ufcs; + +import dcd.server.autocomplete.util; +import dsymbol.symbol; +import dsymbol.scope_; +import dcd.common.messages; +import std.functional : unaryFun; +import std.algorithm; +import std.array; +import std.range; +import dsymbol.builtin.names; +import std.string; +import dparse.lexer : tok; +import std.regex; +import dcd.server.autocomplete.calltip_utils; +import containers.hashset : HashSet; +import std.experimental.logger; + +void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorPosition, ref AutocompleteResponse response) +{ + // UFCS completion + DSymbol*[] ufcsSymbols = getSymbolsForUFCS(completionScope, beforeDotSymbol, cursorPosition); + + foreach (const symbol; ufcsSymbols) + { + // Filtering only those that match with type of the beforeDotSymbol + // We use the calltip since we need more data from dsymbol + // hopefully this is solved in the future + if (getFirstArgumentOfFunction(symbol.callTip) == beforeDotSymbol.name) + { + response.completions ~= createCompletionForUFCS(symbol); + } + } +} + +AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) +{ + return AutocompleteResponse.Completion(symbol.name, symbol.kind, removeFirstArgumentOfFunction( + symbol.callTip), symbol + .symbolFile, symbol + .location, symbol + .doc); +} + +/** + * Get symbols suitable for UFCS. + * + * a symbol is suitable for UFCS if it satisfies the following: + * $(UL + * $(LI is global or imported) + * $(LI is callable with $(D implicitArg) as it's first argument) + * ) + * + * Params: + * completionScope = current scope + * beforeDotSymbol = the symbol before the dot (implicit first argument to UFCS function) + * cursorPosition = current position + * Returns: + * callable an array of symbols suitable for UFCS at $(D cursorPosition) + */ +DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSymbol, size_t cursorPosition) +{ + assert(beforeDotSymbol); + + if (beforeDotSymbol.name is getBuiltinTypeName(tok!"void") + || (beforeDotSymbol.type !is null + && beforeDotSymbol.type.name is getBuiltinTypeName(tok!"void"))) + { + + return null; // no UFCS for void + } + + Scope* currentScope = completionScope.getScopeByCursor(cursorPosition); + assert(currentScope); + HashSet!size_t visited; + // local imports only + FilteredAppender!(a => a.isCallableWithArg(beforeDotSymbol), DSymbol*[]) app; + while (currentScope !is null && currentScope.parent !is null) + { + auto localImports = currentScope.symbols.filter!(a => a.kind == CompletionKind.importSymbol); + foreach (sym; localImports) + { + if (sym.type is null) + continue; + if (sym.qualifier == SymbolQualifier.selectiveImport) + app.put(sym.type); + else + sym.type.getParts(internString(null), app, visited); + } + + currentScope = currentScope.parent; + } + // global symbols and global imports + assert(currentScope !is null); + assert(currentScope.parent is null); + foreach (sym; currentScope.symbols) + { + if (sym.kind != CompletionKind.importSymbol) + app.put(sym); + else if (sym.type !is null) + { + if (sym.qualifier == SymbolQualifier.selectiveImport) + app.put(sym.type); + else + sym.type.getParts(internString(null), app, visited); + } + } + return app.data; +} + +/** + Params: + symbol = the symbol to check + arg0 = the argument + Returns: + true if if $(D symbol) is callable with $(D arg0) as it's first argument + false otherwise +*/ +bool isCallableWithArg(const(DSymbol)* symbol, const(DSymbol)* arg0) +{ + // FIXME: do signature type checking? + // a lot is to be done in dsymbol for type checking to work. + // for instance, define an isSbtype function for where it is applicable + // ex: interfaces, subclasses, builtintypes ... + + // FIXME: instruct dsymbol to always save paramater symbols + // and check these instead of checking callTip + + static bool checkCallTip(string callTip) + { + assert(callTip.length); + if (callTip.endsWith("()")) + return false; // takes no arguments + else if (callTip.endsWith("(...)")) + return true; + else + return true; // FIXME: assume yes? + } + + assert(symbol); + assert(arg0); + + switch (symbol.kind) + { + case CompletionKind.dummy: + if (symbol.qualifier == SymbolQualifier.func) + return checkCallTip(symbol.callTip); + break; + case CompletionKind.importSymbol: + if (symbol.type is null) + break; + if (symbol.qualifier == SymbolQualifier.selectiveImport) + return symbol.type.isCallableWithArg(arg0); + break; + case CompletionKind.structName: + foreach (constructor; symbol.getPartsByName(CONSTRUCTOR_SYMBOL_NAME)) + { + // check user defined contructors or auto-generated constructor + if (checkCallTip(constructor.callTip)) + return true; + } + break; + case CompletionKind.variableName: + case CompletionKind.enumMember: // assuming anonymous enum member + if (symbol.type !is null) + { + if (symbol.type.qualifier == SymbolQualifier.func) + return checkCallTip(symbol.type.callTip); + foreach (functor; symbol.type.getPartsByName(internString("opCall"))) + if (checkCallTip(functor.callTip)) + return true; + } + break; + case CompletionKind.functionName: + return checkCallTip(symbol.callTip); + case CompletionKind.enumName: + case CompletionKind.aliasName: + if (symbol.type !is null && symbol.type !is symbol) + return symbol.type.isCallableWithArg(arg0); + break; + case CompletionKind.unionName: + case CompletionKind.templateName: + return true; // can we do more checks? + case CompletionKind.withSymbol: + case CompletionKind.className: + case CompletionKind.interfaceName: + case CompletionKind.memberVariableName: + case CompletionKind.keyword: + case CompletionKind.packageName: + case CompletionKind.moduleName: + case CompletionKind.mixinTemplateName: + break; + default: + break; + } + return false; +} + +/// $(D appender) with filter on $(D put) +struct FilteredAppender(alias predicate, T: + T[] = DSymbol*[]) if (__traits(compiles, unaryFun!predicate(T.init) ? 0 : 0)) +{ + alias pred = unaryFun!predicate; + private Appender!(T[]) app; + + void put(T item) + { + if (pred(item)) + app.put(item); + } + + void put(R)(R items) if (isInputRange!R && __traits(compiles, put(R.init.front))) + { + foreach (item; items) + put(item); + } + + void opOpAssign(string op : "~")(T rhs) + { + put(rhs); + } + + alias app this; +} + +@safe pure nothrow unittest +{ + FilteredAppender!("a%2", int[]) app; + app.put(iota(10)); + assert(app.data == [1, 3, 5, 7, 9]); +} + +bool doUFCSSearch(string beforeToken, string lastToken) +{ + // we do the search if they are different from eachother + return beforeToken != lastToken; +} diff --git a/src/dcd/server/autocomplete/util.d b/src/dcd/server/autocomplete/util.d index 009c59f..7ba4cbb 100644 --- a/src/dcd/server/autocomplete/util.d +++ b/src/dcd/server/autocomplete/util.d @@ -37,6 +37,7 @@ import dsymbol.modulecache; import dsymbol.scope_; import dsymbol.string_interning; import dsymbol.symbol; +import dcd.server.autocomplete.ufcs; enum ImportKind : ubyte { @@ -143,8 +144,14 @@ SymbolStuff getSymbolsForCompletion(const AutocompleteRequest request, ScopeSymbolPair pair = generateAutocompleteTrees(tokenArray, rba, request.cursorPosition, moduleCache); auto expression = getExpression(beforeTokens); - return SymbolStuff(getSymbolsByTokenChain(pair.scope_, expression, - request.cursorPosition, type), pair.symbol, pair.scope_); + auto symbols = getSymbolsByTokenChain(pair.scope_, expression, + request.cursorPosition, type); + if (symbols.length == 0 && doUFCSSearch(stringToken(beforeTokens.front), stringToken(beforeTokens.back))) { + // Let search for UFCS, since we got no hit + symbols ~= getSymbolsByTokenChain(pair.scope_, getExpression([beforeTokens.back]), + request.cursorPosition, type); + } + return SymbolStuff(symbols, pair.symbol, pair.scope_); } bool isSliceExpression(T)(T tokens, size_t index) diff --git a/tests/tc_erroneous_body_content/expected2.txt b/tests/tc_erroneous_body_content/expected2.txt index a034357..43fa6fe 100644 --- a/tests/tc_erroneous_body_content/expected2.txt +++ b/tests/tc_erroneous_body_content/expected2.txt @@ -1,6 +1,7 @@ identifiers a v alignof k +foo f init k mangleof k sizeof k diff --git a/tests/tc_locate_ufcs_function/barutils/barutils.d b/tests/tc_locate_ufcs_function/barutils/barutils.d new file mode 100644 index 0000000..a8a6fca --- /dev/null +++ b/tests/tc_locate_ufcs_function/barutils/barutils.d @@ -0,0 +1,4 @@ +module barutils; +void ufcsBar(Foo foo, string message) +{ +} diff --git a/tests/tc_locate_ufcs_function/file.d b/tests/tc_locate_ufcs_function/file.d new file mode 100644 index 0000000..154acea --- /dev/null +++ b/tests/tc_locate_ufcs_function/file.d @@ -0,0 +1,7 @@ +import barutils; + +void main() +{ + auto foo = Foo(); + foo.ufcsBar; +} diff --git a/tests/tc_locate_ufcs_function/run.sh b/tests/tc_locate_ufcs_function/run.sh new file mode 100755 index 0000000..8252303 --- /dev/null +++ b/tests/tc_locate_ufcs_function/run.sh @@ -0,0 +1,6 @@ +set -e +set -u + +../../bin/dcd-client $1 -c57 -l -I"$PWD"/barutils file.d > actual.txt +echo -e "$PWD/barutils/barutils.d\t22" > expected.txt +diff actual.txt expected.txt diff --git a/tests/tc_ufcs_completions/expected.txt b/tests/tc_ufcs_completions/expected.txt new file mode 100644 index 0000000..f18c531 --- /dev/null +++ b/tests/tc_ufcs_completions/expected.txt @@ -0,0 +1,18 @@ +identifiers +alignof k +fooHey f +hasArgname f +init k +mangleof k +sizeof k +stringof k +tupleof k +u f +ufcsBar f +ufcsBarRef f +ufcsBarRefConst f +ufcsBarRefConstWrapped f +ufcsBarRefImmuttableWrapped f +ufcsBarReturnScope f +ufcsBarScope f +ufcsHello f diff --git a/tests/tc_ufcs_completions/file.d b/tests/tc_ufcs_completions/file.d new file mode 100644 index 0000000..4c7f6b8 --- /dev/null +++ b/tests/tc_ufcs_completions/file.d @@ -0,0 +1,10 @@ +//import foodata; +import fooutils; + +void hasArgname(Foo f){ +} +void main() +{ + auto foo = Foo(); + foo. +} diff --git a/tests/tc_ufcs_completions/fooutils/fooutils.d b/tests/tc_ufcs_completions/fooutils/fooutils.d new file mode 100644 index 0000000..f16af2d --- /dev/null +++ b/tests/tc_ufcs_completions/fooutils/fooutils.d @@ -0,0 +1,28 @@ +module fooutils; + +struct Foo { + void fooHey(){ } +} + +void u(Foo foo) { +} + +void ufcsHello(ref Foo foo) +{ +} + +void ufcsBar(Foo foo, string mama) +{ +} + +void ufcsBarRef(ref Foo foo, string mama) +{ +} + +void ufcsBarRefConst(ref const Foo foo, string mama) +{ +} +void ufcsBarRefConstWrapped(ref const(Foo) foo, string mama) {} +void ufcsBarRefImmuttableWrapped(ref immutable(Foo) foo, string mama) {} +void ufcsBarScope(ref scope Foo foo, string mama) {} +void ufcsBarReturnScope(return scope Foo foo, string mama) {} \ No newline at end of file diff --git a/tests/tc_ufcs_completions/run.sh b/tests/tc_ufcs_completions/run.sh new file mode 100755 index 0000000..1532e70 --- /dev/null +++ b/tests/tc_ufcs_completions/run.sh @@ -0,0 +1,5 @@ +set -e +set -u + +../../bin/dcd-client $1 -c100 -I"$PWD"/fooutils file.d > actual.txt +diff actual.txt expected.txt