From 499698bcf8a776540f5607cc64ed9ce27b6a8c2a Mon Sep 17 00:00:00 2001 From: davu Date: Sun, 2 Oct 2022 10:54:20 +0200 Subject: [PATCH 1/8] Adding Simple UFCS Added UFCS navigation Adding test cases for ufcs With ufcs update expected test Updated UFCS with templating support Removing template function. --- src/dcd/server/autocomplete/calltip_utils.d | 107 ++++++++ src/dcd/server/autocomplete/complete.d | 8 +- src/dcd/server/autocomplete/ufcs.d | 237 ++++++++++++++++++ src/dcd/server/autocomplete/util.d | 11 +- tests/tc_erroneous_body_content/expected2.txt | 1 + .../barutils/barutils.d | 4 + tests/tc_locate_ufcs_function/file.d | 7 + tests/tc_locate_ufcs_function/run.sh | 6 + tests/tc_ufcs_completions/expected.txt | 18 ++ tests/tc_ufcs_completions/file.d | 10 + tests/tc_ufcs_completions/fooutils/fooutils.d | 28 +++ tests/tc_ufcs_completions/run.sh | 5 + 12 files changed, 437 insertions(+), 5 deletions(-) create mode 100644 src/dcd/server/autocomplete/calltip_utils.d create mode 100644 src/dcd/server/autocomplete/ufcs.d create mode 100644 tests/tc_locate_ufcs_function/barutils/barutils.d create mode 100644 tests/tc_locate_ufcs_function/file.d create mode 100755 tests/tc_locate_ufcs_function/run.sh create mode 100644 tests/tc_ufcs_completions/expected.txt create mode 100644 tests/tc_ufcs_completions/file.d create mode 100644 tests/tc_ufcs_completions/fooutils/fooutils.d create mode 100755 tests/tc_ufcs_completions/run.sh 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 From 1831222f4d780d60483afb5dfaec7c3ad2c57f03 Mon Sep 17 00:00:00 2001 From: davu Date: Fri, 7 Oct 2022 08:34:43 +0200 Subject: [PATCH 2/8] Removing unsused cases --- src/dcd/server/autocomplete/ufcs.d | 95 ++++-------------------------- 1 file changed, 10 insertions(+), 85 deletions(-) diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index 7e1108c..aefa75c 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -23,20 +23,14 @@ void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorP 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); - } + response.completions ~= createCompletionForUFCS(symbol); } } AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) { return AutocompleteResponse.Completion(symbol.name, symbol.kind, removeFirstArgumentOfFunction( - symbol.callTip), symbol + symbol.callTip), symbol .symbolFile, symbol .location, symbol .doc); @@ -48,7 +42,7 @@ AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) * 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) + * $(LI is callable with $(D beforeDotSymbol) as it's first argument) * ) * * Params: @@ -111,87 +105,18 @@ DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSy /** Params: symbol = the symbol to check - arg0 = the argument + firstArgumentSymbol = the first argument Returns: - true if if $(D symbol) is callable with $(D arg0) as it's first argument + true if if $(D symbol) is callable with $(D firstArgumentSymbol) as it's first argument false otherwise */ -bool isCallableWithArg(const(DSymbol)* symbol, const(DSymbol)* arg0) +bool isCallableWithArg(const(DSymbol)* beforeDotSymbol, const(DSymbol)* firstArgumentSymbol) { - // 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) + if (beforeDotSymbol.kind == CompletionKind.functionName) { - 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; + // Keeping it simple only support for functions for now + // Where beforeDotSymbol matches first argument + return getFirstArgumentOfFunction(beforeDotSymbol.callTip) == firstArgumentSymbol.name; } return false; } From e5705e85758a4e50838a55718669241cd575c915 Mon Sep 17 00:00:00 2001 From: davu Date: Sat, 8 Oct 2022 15:45:49 +0200 Subject: [PATCH 3/8] hide private functions --- src/dcd/server/autocomplete/ufcs.d | 10 ++++++++-- tests/tc_ufcs_completions/fooutils/fooutils.d | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index aefa75c..5e153fe 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -69,6 +69,8 @@ DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSy HashSet!size_t visited; // local imports only FilteredAppender!(a => a.isCallableWithArg(beforeDotSymbol), DSymbol*[]) app; + FilteredAppender!(a => a.protection != tok!"private", DSymbol*[]) globalsFunctions; + while (currentScope !is null && currentScope.parent !is null) { auto localImports = currentScope.symbols.filter!(a => a.kind == CompletionKind.importSymbol); @@ -95,8 +97,12 @@ DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSy { if (sym.qualifier == SymbolQualifier.selectiveImport) app.put(sym.type); - else - sym.type.getParts(internString(null), app, visited); + else{ + sym.type.getParts(istring(null), globalsFunctions, visited); + foreach(gSym; globalsFunctions) { + app.put(gSym); + } + } } } return app.data; diff --git a/tests/tc_ufcs_completions/fooutils/fooutils.d b/tests/tc_ufcs_completions/fooutils/fooutils.d index f16af2d..187f39d 100644 --- a/tests/tc_ufcs_completions/fooutils/fooutils.d +++ b/tests/tc_ufcs_completions/fooutils/fooutils.d @@ -25,4 +25,6 @@ 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 +void ufcsBarReturnScope(return scope Foo foo, string mama) {} +private void privateUfcsBar(Foo foo, string message) {} +void notUfcsBar(string message) {} \ No newline at end of file From 28f7d3cb2e11b3c38a2464287b88d3309b5084f8 Mon Sep 17 00:00:00 2001 From: davu Date: Sat, 15 Oct 2022 23:12:56 +0200 Subject: [PATCH 4/8] UFSC implementation using functionParameters instead and update tests for UFSC --- README.md | 3 +- src/dcd/server/autocomplete/calltip_utils.d | 67 ------------------- src/dcd/server/autocomplete/ufcs.d | 74 +++++++++++---------- tests/tc006/expected1.txt | 1 + tests/tc006/expected3.txt | 1 + 5 files changed, 44 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index e2c65a6..b6a7cd1 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ the issue.) * *alias this* * *auto* declarations (Mostly) * *with* statements + * Simple UFCS suggestions for concrete types. * Not working: - * UFCS suggestions + * UFCS completion for templates, literals, UFCS function arguments, and '.' chaining with other UFCS functions. * Autocompletion of declarations with template arguments (This will work to some extent, but it won't do things like replace T with int) * Determining the type of an enum member when no base type is specified, but the first member has an initializer * auto functions (which can then propagate the failure to auto declarations) diff --git a/src/dcd/server/autocomplete/calltip_utils.d b/src/dcd/server/autocomplete/calltip_utils.d index be48b33..200a696 100644 --- a/src/dcd/server/autocomplete/calltip_utils.d +++ b/src/dcd/server/autocomplete/calltip_utils.d @@ -1,53 +1,10 @@ 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('('); @@ -62,30 +19,6 @@ string removeFirstArgumentOfFunction(string callTip) } -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)"); diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index 5e153fe..34d4e6c 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -15,27 +15,32 @@ import std.regex; import dcd.server.autocomplete.calltip_utils; import containers.hashset : HashSet; import std.experimental.logger; +import std.algorithm.iteration : map; void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorPosition, ref AutocompleteResponse response) { // UFCS completion DSymbol*[] ufcsSymbols = getSymbolsForUFCS(completionScope, beforeDotSymbol, cursorPosition); - - foreach (const symbol; ufcsSymbols) - { - response.completions ~= createCompletionForUFCS(symbol); - } + response.completions ~= map!(s => createCompletionForUFCS(s))(ufcsSymbols).array; } AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) { - return AutocompleteResponse.Completion(symbol.name, symbol.kind, removeFirstArgumentOfFunction( + return AutocompleteResponse.Completion(symbol.name, symbol.kind, "(UFCS) " ~ removeFirstArgumentOfFunction( symbol.callTip), symbol .symbolFile, symbol .location, symbol .doc); } +// Check if beforeDotSymbol is null or void +bool isInvalidForUFCSCompletion(const(DSymbol)* beforeDotSymbol) +{ + return beforeDotSymbol is null + || beforeDotSymbol.name is getBuiltinTypeName(tok!"void") + || (beforeDotSymbol.type !is null && beforeDotSymbol.type.name is getBuiltinTypeName( + tok!"void")); +} /** * Get symbols suitable for UFCS. * @@ -54,22 +59,16 @@ AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) */ 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 + if (beforeDotSymbol.isInvalidForUFCSCompletion) { + return null; } Scope* currentScope = completionScope.getScopeByCursor(cursorPosition); assert(currentScope); HashSet!size_t visited; - // local imports only - FilteredAppender!(a => a.isCallableWithArg(beforeDotSymbol), DSymbol*[]) app; - FilteredAppender!(a => a.protection != tok!"private", DSymbol*[]) globalsFunctions; + + // local appender + FilteredAppender!(a => a.isCallableWithArg(beforeDotSymbol), DSymbol*[]) localAppender; while (currentScope !is null && currentScope.parent !is null) { @@ -79,51 +78,58 @@ DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSy if (sym.type is null) continue; if (sym.qualifier == SymbolQualifier.selectiveImport) - app.put(sym.type); + localAppender.put(sym.type); else - sym.type.getParts(internString(null), app, visited); + sym.type.getParts(internString(null), localAppender, visited); } currentScope = currentScope.parent; } + + // global appender + FilteredAppender!(a => a.isCallableWithArg(beforeDotSymbol, true), DSymbol*[]) globalAppender; + // 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); + localAppender.put(sym); else if (sym.type !is null) { if (sym.qualifier == SymbolQualifier.selectiveImport) - app.put(sym.type); - else{ - sym.type.getParts(istring(null), globalsFunctions, visited); - foreach(gSym; globalsFunctions) { - app.put(gSym); - } + localAppender.put(sym.type); + else + { + sym.type.getParts(istring(null), globalAppender, visited); } } } - return app.data; + return localAppender.opSlice ~ globalAppender.opSlice; } /** Params: symbol = the symbol to check - firstArgumentSymbol = the first argument + incomingSymbol = symbols we check on Returns: - true if if $(D symbol) is callable with $(D firstArgumentSymbol) as it's first argument + true if incomingSymbols first parameter matches beforeDotSymbol false otherwise */ -bool isCallableWithArg(const(DSymbol)* beforeDotSymbol, const(DSymbol)* firstArgumentSymbol) +bool isCallableWithArg(const(DSymbol)* incomingSymbol, const(DSymbol)* beforeDotSymbol, bool isGlobalScope = false) { - if (beforeDotSymbol.kind == CompletionKind.functionName) + if (isGlobalScope && incomingSymbol.protection == tok!"private") { - // Keeping it simple only support for functions for now - // Where beforeDotSymbol matches first argument - return getFirstArgumentOfFunction(beforeDotSymbol.callTip) == firstArgumentSymbol.name; + return false; } + + if (incomingSymbol.kind == CompletionKind.functionName && !incomingSymbol + .functionParameters.empty) + { + return incomingSymbol.functionParameters.front.type is beforeDotSymbol; + } + return false; } diff --git a/tests/tc006/expected1.txt b/tests/tc006/expected1.txt index 9a132a4..672fb58 100644 --- a/tests/tc006/expected1.txt +++ b/tests/tc006/expected1.txt @@ -1,5 +1,6 @@ identifiers alignof k +doStuff f init k mangleof k max k diff --git a/tests/tc006/expected3.txt b/tests/tc006/expected3.txt index 9a132a4..672fb58 100644 --- a/tests/tc006/expected3.txt +++ b/tests/tc006/expected3.txt @@ -1,5 +1,6 @@ identifiers alignof k +doStuff f init k mangleof k max k From 5f8658fd10a9bbcc6050ebe391bcb8288b807cc3 Mon Sep 17 00:00:00 2001 From: Vushu Date: Sun, 16 Oct 2022 19:16:00 +0200 Subject: [PATCH 5/8] update readme for UFCS description Co-authored-by: Jan Jurzitza --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b6a7cd1..612741b 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ the issue.) * Simple UFCS suggestions for concrete types. * Not working: * UFCS completion for templates, literals, UFCS function arguments, and '.' chaining with other UFCS functions. + * UFCS calltips * Autocompletion of declarations with template arguments (This will work to some extent, but it won't do things like replace T with int) * Determining the type of an enum member when no base type is specified, but the first member has an initializer * auto functions (which can then propagate the failure to auto declarations) From e222de8689235e0950ccd0e8d348988c0442ea46 Mon Sep 17 00:00:00 2001 From: davu Date: Sun, 16 Oct 2022 20:47:53 +0200 Subject: [PATCH 6/8] minor adjustments --- src/dcd/server/autocomplete/calltip_utils.d | 40 ------------------- src/dcd/server/autocomplete/complete.d | 2 +- src/dcd/server/autocomplete/ufcs.d | 9 ++--- src/dcd/server/autocomplete/util.d | 9 ++--- tests/tc_ufcs_completions/fooutils/fooutils.d | 29 ++++---------- 5 files changed, 16 insertions(+), 73 deletions(-) delete mode 100644 src/dcd/server/autocomplete/calltip_utils.d diff --git a/src/dcd/server/autocomplete/calltip_utils.d b/src/dcd/server/autocomplete/calltip_utils.d deleted file mode 100644 index 200a696..0000000 --- a/src/dcd/server/autocomplete/calltip_utils.d +++ /dev/null @@ -1,40 +0,0 @@ -module dcd.server.autocomplete.calltip_utils; -import std.string; -import std.regex; -import std.range : empty; -import std.experimental.logger; -import std.algorithm : canFind; - -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 = 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 3086238..79bfa08 100644 --- a/src/dcd/server/autocomplete/complete.d +++ b/src/dcd/server/autocomplete/complete.d @@ -202,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) diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index 34d4e6c..f48893e 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -12,10 +12,7 @@ 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; -import std.algorithm.iteration : map; void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorPosition, ref AutocompleteResponse response) { @@ -26,8 +23,7 @@ void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorP AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) { - return AutocompleteResponse.Completion(symbol.name, symbol.kind, "(UFCS) " ~ removeFirstArgumentOfFunction( - symbol.callTip), symbol + return AutocompleteResponse.Completion(symbol.name, symbol.kind, "(UFCS) " ~ symbol.callTip, symbol .symbolFile, symbol .location, symbol .doc); @@ -59,7 +55,8 @@ bool isInvalidForUFCSCompletion(const(DSymbol)* beforeDotSymbol) */ DSymbol*[] getSymbolsForUFCS(Scope* completionScope, const(DSymbol)* beforeDotSymbol, size_t cursorPosition) { - if (beforeDotSymbol.isInvalidForUFCSCompletion) { + if (beforeDotSymbol.isInvalidForUFCSCompletion) + { return null; } diff --git a/src/dcd/server/autocomplete/util.d b/src/dcd/server/autocomplete/util.d index 7ba4cbb..f03bf04 100644 --- a/src/dcd/server/autocomplete/util.d +++ b/src/dcd/server/autocomplete/util.d @@ -146,11 +146,10 @@ SymbolStuff getSymbolsForCompletion(const AutocompleteRequest request, auto expression = getExpression(beforeTokens); 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); - } + 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_); } diff --git a/tests/tc_ufcs_completions/fooutils/fooutils.d b/tests/tc_ufcs_completions/fooutils/fooutils.d index 187f39d..4b06cfc 100644 --- a/tests/tc_ufcs_completions/fooutils/fooutils.d +++ b/tests/tc_ufcs_completions/fooutils/fooutils.d @@ -1,30 +1,17 @@ module fooutils; struct Foo { - void fooHey(){ } + 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 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) {} -private void privateUfcsBar(Foo foo, string message) {} -void notUfcsBar(string message) {} \ No newline at end of file +private void ufcsBarPrivate(Foo foo, string message) {} +void helloBar(string message) {} \ No newline at end of file From c409670da80c0fa533943b8f12e185dfdad9d32b Mon Sep 17 00:00:00 2001 From: davu Date: Mon, 17 Oct 2022 01:18:11 +0200 Subject: [PATCH 7/8] UFCS using new completion kind F --- README.md | 1 + dsymbol/src/dsymbol/conversion/second.d | 1 + dsymbol/src/dsymbol/symbol.d | 3 +++ src/dcd/server/autocomplete/ufcs.d | 2 +- tests/tc006/expected1.txt | 2 +- tests/tc006/expected3.txt | 2 +- tests/tc_erroneous_body_content/expected2.txt | 2 +- tests/tc_ufcs_completions/expected.txt | 20 +++++++++---------- 8 files changed, 19 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 612741b..d56595a 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ tab character, followed by a completion kind * m - member variable name * k - keyword, built-in version, scope statement * f - function or method +* F - UFCS function * g - enum name * e - enum member * P - package name diff --git a/dsymbol/src/dsymbol/conversion/second.d b/dsymbol/src/dsymbol/conversion/second.d index bd56763..933bfef 100644 --- a/dsymbol/src/dsymbol/conversion/second.d +++ b/dsymbol/src/dsymbol/conversion/second.d @@ -47,6 +47,7 @@ void secondPass(SemanticSymbol* currentSymbol, Scope* moduleScope, ref ModuleCac case variableName: case memberVariableName: case functionName: + case ufcsName: case aliasName: // type may not be null in the case of a renamed import if (currentSymbol.acSymbol.type is null) diff --git a/dsymbol/src/dsymbol/symbol.d b/dsymbol/src/dsymbol/symbol.d index 44263e1..6f56e0f 100644 --- a/dsymbol/src/dsymbol/symbol.d +++ b/dsymbol/src/dsymbol/symbol.d @@ -75,6 +75,9 @@ enum CompletionKind : char /// function or method functionName = 'f', + /// UFCS function + ufcsName = 'F', + /// enum name enumName = 'g', diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index f48893e..8a38911 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -23,7 +23,7 @@ void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorP AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) { - return AutocompleteResponse.Completion(symbol.name, symbol.kind, "(UFCS) " ~ symbol.callTip, symbol + return AutocompleteResponse.Completion(symbol.name, CompletionKind.ufcsName, symbol.callTip, symbol .symbolFile, symbol .location, symbol .doc); diff --git a/tests/tc006/expected1.txt b/tests/tc006/expected1.txt index 672fb58..940999a 100644 --- a/tests/tc006/expected1.txt +++ b/tests/tc006/expected1.txt @@ -1,6 +1,6 @@ identifiers alignof k -doStuff f +doStuff F init k mangleof k max k diff --git a/tests/tc006/expected3.txt b/tests/tc006/expected3.txt index 672fb58..940999a 100644 --- a/tests/tc006/expected3.txt +++ b/tests/tc006/expected3.txt @@ -1,6 +1,6 @@ identifiers alignof k -doStuff f +doStuff F init k mangleof k max k diff --git a/tests/tc_erroneous_body_content/expected2.txt b/tests/tc_erroneous_body_content/expected2.txt index 43fa6fe..fb8d7dd 100644 --- a/tests/tc_erroneous_body_content/expected2.txt +++ b/tests/tc_erroneous_body_content/expected2.txt @@ -1,7 +1,7 @@ identifiers a v alignof k -foo f +foo F init k mangleof k sizeof k diff --git a/tests/tc_ufcs_completions/expected.txt b/tests/tc_ufcs_completions/expected.txt index f18c531..01eae0a 100644 --- a/tests/tc_ufcs_completions/expected.txt +++ b/tests/tc_ufcs_completions/expected.txt @@ -1,18 +1,18 @@ identifiers alignof k fooHey f -hasArgname 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 +u F +ufcsBar F +ufcsBarRef F +ufcsBarRefConst F +ufcsBarRefConstWrapped F +ufcsBarRefImmuttableWrapped F +ufcsBarReturnScope F +ufcsBarScope F +ufcsHello F From e969d2b1583e624af2b210eed887c078b909ca4e Mon Sep 17 00:00:00 2001 From: davu Date: Tue, 18 Oct 2022 14:48:35 +0200 Subject: [PATCH 8/8] ufcs function chaining --- README.md | 4 +- dsymbol/src/dsymbol/conversion/first.d | 15 ++++++++ dsymbol/src/dsymbol/symbol.d | 6 +++ src/dcd/server/autocomplete/complete.d | 4 +- src/dcd/server/autocomplete/ufcs.d | 37 ++++++++++++++++++- tests/tc_ufcs_completions/expected.txt | 2 + tests/tc_ufcs_completions/expected2.txt | 13 +++++++ tests/tc_ufcs_completions/expected3.txt | 3 ++ tests/tc_ufcs_completions/expected4.txt | 2 + tests/tc_ufcs_completions/expected5.txt | 2 + tests/tc_ufcs_completions/expected6.txt | 3 ++ tests/tc_ufcs_completions/file.d | 6 ++- tests/tc_ufcs_completions/fooutils/fooutils.d | 7 +++- tests/tc_ufcs_completions/run.sh | 12 +++++- 14 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 tests/tc_ufcs_completions/expected2.txt create mode 100644 tests/tc_ufcs_completions/expected3.txt create mode 100644 tests/tc_ufcs_completions/expected4.txt create mode 100644 tests/tc_ufcs_completions/expected5.txt create mode 100644 tests/tc_ufcs_completions/expected6.txt diff --git a/README.md b/README.md index d56595a..9b29282 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,9 @@ the issue.) * *auto* declarations (Mostly) * *with* statements * Simple UFCS suggestions for concrete types. + * UFCS '.' function chaining with concrete types except string type. * Not working: - * UFCS completion for templates, literals, UFCS function arguments, and '.' chaining with other UFCS functions. + * UFCS completion for templates, literals, UFCS function arguments. * UFCS calltips * Autocompletion of declarations with template arguments (This will work to some extent, but it won't do things like replace T with int) * Determining the type of an enum member when no base type is specified, but the first member has an initializer @@ -157,6 +158,7 @@ tab character, followed by a completion kind resolvedType v calltip v getPartByName f + ufcsFunction F #### Extended output mode diff --git a/dsymbol/src/dsymbol/conversion/first.d b/dsymbol/src/dsymbol/conversion/first.d index e488566..45cd95d 100644 --- a/dsymbol/src/dsymbol/conversion/first.d +++ b/dsymbol/src/dsymbol/conversion/first.d @@ -37,6 +37,8 @@ import std.experimental.allocator; import std.experimental.allocator.gc_allocator : GCAllocator; import std.experimental.logger; import std.typecons : Rebindable; +import std.array : appender; +import std.range : empty; /** * First Pass handles the following: @@ -944,10 +946,23 @@ private: } } + void setFunctionReturnType(const Type returnType, ref DSymbol acSymbol){ + if (returnType !is null) + { + auto app = appender!string(); + app.formatNode(returnType); + auto foundSymbols = currentScope.getSymbolsByName(istring(app.data)); + if (!foundSymbols.empty){ + acSymbol.functionReturnType = foundSymbols[0]; + } + } + } + void processParameters(SemanticSymbol* symbol, const Type returnType, string functionName, const Parameters parameters, const TemplateParameters templateParameters) { + setFunctionReturnType(returnType, *symbol.acSymbol); processTemplateParameters(symbol, templateParameters); if (parameters !is null) { diff --git a/dsymbol/src/dsymbol/symbol.d b/dsymbol/src/dsymbol/symbol.d index 6f56e0f..2f5f50a 100644 --- a/dsymbol/src/dsymbol/symbol.d +++ b/dsymbol/src/dsymbol/symbol.d @@ -386,6 +386,12 @@ struct DSymbol */ DSymbol*[] functionParameters; + /** + * + * Return type of the function + */ + DSymbol* functionReturnType; + private uint _location; /** diff --git a/src/dcd/server/autocomplete/complete.d b/src/dcd/server/autocomplete/complete.d index 79bfa08..4de2968 100644 --- a/src/dcd/server/autocomplete/complete.d +++ b/src/dcd/server/autocomplete/complete.d @@ -558,8 +558,10 @@ void setCompletions(T)(ref AutocompleteResponse response, DSymbol*[] symbols = getSymbolsByTokenChain(completionScope, tokens, cursorPosition, completionType); - if (symbols.length == 0) + if (symbols.length == 0){ + lookupUFCSForDotChaining(completionScope, tokens, cursorPosition, response); return; + } if (completionType == CompletionType.identifiers) { diff --git a/src/dcd/server/autocomplete/ufcs.d b/src/dcd/server/autocomplete/ufcs.d index 8a38911..d4bb637 100644 --- a/src/dcd/server/autocomplete/ufcs.d +++ b/src/dcd/server/autocomplete/ufcs.d @@ -14,11 +14,15 @@ import dparse.lexer : tok; import std.regex; import containers.hashset : HashSet; -void lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorPosition, ref AutocompleteResponse response) +bool lookupUFCS(Scope* completionScope, DSymbol* beforeDotSymbol, size_t cursorPosition, ref AutocompleteResponse response) { - // UFCS completion + if (beforeDotSymbol.isInvalidForUFCSCompletion) + return false; DSymbol*[] ufcsSymbols = getSymbolsForUFCS(completionScope, beforeDotSymbol, cursorPosition); + if (ufcsSymbols.empty) + return false; response.completions ~= map!(s => createCompletionForUFCS(s))(ufcsSymbols).array; + return true; } AutocompleteResponse.Completion createCompletionForUFCS(const DSymbol* symbol) @@ -169,3 +173,32 @@ bool doUFCSSearch(string beforeToken, string lastToken) // we do the search if they are different from eachother return beforeToken != lastToken; } + +void lookupUFCSForDotChaining(T)(Scope* completionScope, ref T tokens, size_t cursorPosition, ref AutocompleteResponse response) +{ + auto filteredTokens = filter!(t => !t.text.empty)(tokens); + + if (filteredTokens.empty) + return; + + auto foundTokens = filteredTokens.array; + auto possibleUFCSSymbols = completionScope.getSymbolsByNameAndCursor(istring(null), cursorPosition); + for (auto i = foundTokens.length; i > 0; i--) + { + // Finding function return value type + auto foundSymbols = filter!(s => s.name == foundTokens[i - 1].text && s.kind == CompletionKind + .functionName)(possibleUFCSSymbols); + if (!foundSymbols.empty) + { + auto foundFunctionSymbol = foundSymbols.array.back; + if (foundFunctionSymbol.functionReturnType) + { + if (lookupUFCS(completionScope, foundFunctionSymbol.functionReturnType, cursorPosition, response)) + { + response.completionType = CompletionType.identifiers; + } + } + break; + } + } +} \ No newline at end of file diff --git a/tests/tc_ufcs_completions/expected.txt b/tests/tc_ufcs_completions/expected.txt index 01eae0a..1a48285 100644 --- a/tests/tc_ufcs_completions/expected.txt +++ b/tests/tc_ufcs_completions/expected.txt @@ -15,4 +15,6 @@ ufcsBarRefConstWrapped F ufcsBarRefImmuttableWrapped F ufcsBarReturnScope F ufcsBarScope F +ufcsGetNumber F ufcsHello F +ufcsSelf F diff --git a/tests/tc_ufcs_completions/expected2.txt b/tests/tc_ufcs_completions/expected2.txt new file mode 100644 index 0000000..577bfeb --- /dev/null +++ b/tests/tc_ufcs_completions/expected2.txt @@ -0,0 +1,13 @@ +identifiers +hasArgname F +u F +ufcsBar F +ufcsBarRef F +ufcsBarRefConst F +ufcsBarRefConstWrapped F +ufcsBarRefImmuttableWrapped F +ufcsBarReturnScope F +ufcsBarScope F +ufcsGetNumber F +ufcsHello F +ufcsSelf F diff --git a/tests/tc_ufcs_completions/expected3.txt b/tests/tc_ufcs_completions/expected3.txt new file mode 100644 index 0000000..7dee382 --- /dev/null +++ b/tests/tc_ufcs_completions/expected3.txt @@ -0,0 +1,3 @@ +identifiers +helloNumber F +isEvenNumber F diff --git a/tests/tc_ufcs_completions/expected4.txt b/tests/tc_ufcs_completions/expected4.txt new file mode 100644 index 0000000..1b05e62 --- /dev/null +++ b/tests/tc_ufcs_completions/expected4.txt @@ -0,0 +1,2 @@ +identifiers +getBoolToFloat F diff --git a/tests/tc_ufcs_completions/expected5.txt b/tests/tc_ufcs_completions/expected5.txt new file mode 100644 index 0000000..046e1c5 --- /dev/null +++ b/tests/tc_ufcs_completions/expected5.txt @@ -0,0 +1,2 @@ +identifiers +multiply1000 F diff --git a/tests/tc_ufcs_completions/expected6.txt b/tests/tc_ufcs_completions/expected6.txt new file mode 100644 index 0000000..7dee382 --- /dev/null +++ b/tests/tc_ufcs_completions/expected6.txt @@ -0,0 +1,3 @@ +identifiers +helloNumber F +isEvenNumber F diff --git a/tests/tc_ufcs_completions/file.d b/tests/tc_ufcs_completions/file.d index 4c7f6b8..40bc356 100644 --- a/tests/tc_ufcs_completions/file.d +++ b/tests/tc_ufcs_completions/file.d @@ -1,4 +1,3 @@ -//import foodata; import fooutils; void hasArgname(Foo f){ @@ -7,4 +6,9 @@ void main() { auto foo = Foo(); foo. + foo.ufcsSelf. + foo.ufcsSelf.ufcsGetNumber(42). + foo.ufcsSelf.ufcsGetNumber(42).isEvenNumber. + foo.ufcsSelf.ufcsGetNumber(42).isEvenNumber.getBoolToFloat. + foo.ufcsSelf.ufcsGetNumber(42).isEvenNumber.getBoolToFloat.multiply1000. } diff --git a/tests/tc_ufcs_completions/fooutils/fooutils.d b/tests/tc_ufcs_completions/fooutils/fooutils.d index 4b06cfc..c65546c 100644 --- a/tests/tc_ufcs_completions/fooutils/fooutils.d +++ b/tests/tc_ufcs_completions/fooutils/fooutils.d @@ -5,6 +5,8 @@ struct Foo { } void u(Foo foo) {} +Foo ufcsSelf(ref Foo foo) { return foo; } +int ufcsGetNumber(ref Foo foo, int number) { return number; } void ufcsHello(ref Foo foo) {} void ufcsBar(Foo foo, string mama) {} void ufcsBarRef(ref Foo foo, string mama) {} @@ -14,4 +16,7 @@ void ufcsBarRefImmuttableWrapped(ref immutable(Foo) foo, string mama) {} void ufcsBarScope(ref scope Foo foo, string mama) {} void ufcsBarReturnScope(return scope Foo foo, string mama) {} private void ufcsBarPrivate(Foo foo, string message) {} -void helloBar(string message) {} \ No newline at end of file +void helloNumber(int message) {} +bool isEvenNumber(int number) { return number % 2 == 0; } +float getBoolToFloat(bool result) { return result ? 1.0 : 0.0; } +int multiply1000(float x) { return x * 1000; } diff --git a/tests/tc_ufcs_completions/run.sh b/tests/tc_ufcs_completions/run.sh index 1532e70..fdee570 100755 --- a/tests/tc_ufcs_completions/run.sh +++ b/tests/tc_ufcs_completions/run.sh @@ -1,5 +1,15 @@ set -e set -u -../../bin/dcd-client $1 -c100 -I"$PWD"/fooutils file.d > actual.txt +../../bin/dcd-client $1 -c82 -I"$PWD"/fooutils file.d > actual.txt +../../bin/dcd-client $1 -c97 -I"$PWD"/fooutils file.d > actual2.txt +../../bin/dcd-client $1 -c130 -I"$PWD"/fooutils file.d > actual3.txt +../../bin/dcd-client $1 -c176 -I"$PWD"/fooutils file.d > actual4.txt +../../bin/dcd-client $1 -c237 -I"$PWD"/fooutils file.d > actual5.txt +../../bin/dcd-client $1 -c311 -I"$PWD"/fooutils file.d > actual6.txt diff actual.txt expected.txt +diff actual2.txt expected2.txt +diff actual3.txt expected3.txt +diff actual4.txt expected4.txt +diff actual5.txt expected5.txt +diff actual6.txt expected6.txt