diff --git a/nixd/lib/Controller/AST.cpp b/nixd/lib/Controller/AST.cpp index d0128adad..16feee11d 100644 --- a/nixd/lib/Controller/AST.cpp +++ b/nixd/lib/Controller/AST.cpp @@ -1,5 +1,9 @@ #include "AST.h" +#include "nixd/Protocol/AttrSet.h" + +#include + using namespace nixf; namespace { @@ -130,6 +134,97 @@ bool nixd::havePackageScope(const Node &N, const VariableLookupAnalysis &VLA, return false; } +// Idioms. +namespace { + +using IdiomSetT = std::unordered_set; + +IdiomSetT IdiomSet{nixd::idioms::Pkgs, nixd::idioms::Lib}; + +auto ItLib = IdiomSet.find(nixd::idioms::Lib); +auto ItPkgs = IdiomSet.find(nixd::idioms::Pkgs); + +nixd::Selector idiomItSelector(IdiomSetT::iterator It) { + // Unknown name, cannot deal with it. + if (It == IdiomSet.end()) + throw nixd::NotAnIdiomException(); + + return [&]() -> nixd::Selector { + if (It == ItLib) { + return {std::string(nixd::idioms::Lib)}; + } + if (It == ItPkgs) { + return {}; + } + assert(false && "Unhandled idiom iterator?"); + __builtin_unreachable(); + return {}; + }(); +} + +IdiomSetT::iterator varIdiomIt(const nixf::ExprVar &Var) { + return IdiomSet.find(Var.id().name()); +}; + +nixd::Selector varSelector(const nixf::ExprVar &Var) { + return idiomItSelector(varIdiomIt(Var)); +}; + +nixd::Selector withSelector(const nixf::ExprWith &With) { + if (!With.with() || With.with()->kind() != Node::NK_ExprVar) + throw nixd::NotAnIdiomException(); + return varSelector(static_cast(*With.with())); +} + +} // namespace + +nixd::Selector nixd::mkIdiomSelector(const nixf::ExprVar &Var, + const nixf::VariableLookupAnalysis &VLA, + const nixf::ParentMapAnalysis &PM) { + // Only check if the variable can be recogonized by some idiom. + + using ResultKind = VariableLookupAnalysis::LookupResultKind; + auto Result = VLA.query(Var); + switch (Result.Kind) { + case ResultKind::Undefined: + case ResultKind::Defined: + return varSelector(Var); + case ResultKind::FromWith: { + assert(Result.Def && "FromWith variables should contains definition"); + const nixf::Definition &Def = *Result.Def; + if (!Def.syntax()) + throw NotAnIdiomException(); + + // The syntax + // + // with pkgs; with lib; [ ] + // + // does provide both "pkgs" + "lib" scopes. + // + // However, in current implementation we will only consider nested "with". + // That is, only "lib" variables will be considered. + const nixf::Node &Syntax = *Def.syntax(); + const nixf::Node *With = PM.query(Syntax); + assert(With && "parent of kwWith should be the with expression"); + assert(With->kind() == nixf::Node::NK_ExprWith); + Selector WithSelector = + withSelector(static_cast(*With)); + + // Append variable name after "with" expression selector. + // e.g. + // + // with pkgs; [ fo ] + // ^ + // The result will be {pkgs, fo} + WithSelector.emplace_back(Var.id().name()); + + return WithSelector; + } + case ResultKind::NoSuchVar: + throw NoSuchVarException(); + } +} + nixd::Selector nixd::mkSelector(const nixf::AttrPath &AP, Selector BaseSelector) { const auto &Names = AP.names(); diff --git a/nixd/lib/Controller/AST.h b/nixd/lib/Controller/AST.h index 6f659cb2b..343584820 100644 --- a/nixd/lib/Controller/AST.h +++ b/nixd/lib/Controller/AST.h @@ -6,6 +6,8 @@ #include #include +#include + namespace nixd { namespace idioms { @@ -47,9 +49,40 @@ upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA, std::pair, std::string> getScopeAndPrefix(const nixf::Node &N, const nixf::ParentMapAnalysis &PM); +/// \brief Exceptions scoped in nixd::mkIdiomSelector +struct IdiomSelectorException : std::exception {}; + +/// \brief The pattern of this variable cannot be recognized by known idioms. +struct NotAnIdiomException : IdiomSelectorException { + [[nodiscard]] const char *what() const noexcept override { + return "not an idiom"; + } +}; + +/// \brief No such variable. +struct NoSuchVarException : IdiomSelectorException { + [[nodiscard]] const char *what() const noexcept override { + return "no such variable"; + } +}; + +/// \brief Construct a nixd::Selector from \p Var. +/// +/// Try to heuristically find a selector of a variable, based on some known +/// idioms. +Selector mkIdiomSelector(const nixf::ExprVar &Var, + const nixf::VariableLookupAnalysis &VLA, + const nixf::ParentMapAnalysis &PM); + +struct SelectorException : std::exception {}; + /// \brief The attrpath has a dynamic name, thus it cannot be trivially /// transformed to "static" selector. -struct DynamicNameException : std::exception {}; +struct DynamicNameException : SelectorException { + [[nodiscard]] const char *what() const noexcept override { + return "dynamic attribute path encountered"; + } +}; /// \brief Construct a nixd::Selector from \p AP. Selector mkSelector(const nixf::AttrPath &AP, Selector BaseSelector); diff --git a/nixd/lib/Controller/Completion.cpp b/nixd/lib/Controller/Completion.cpp index 917ff957c..05be65048 100644 --- a/nixd/lib/Controller/Completion.cpp +++ b/nixd/lib/Controller/Completion.cpp @@ -10,6 +10,8 @@ #include "nixd/Controller/Controller.h" +#include + #include #include @@ -78,11 +80,9 @@ class VLACompletionProvider { VLACompletionProvider(const VariableLookupAnalysis &VLA) : VLA(VLA) {} /// Perform code completion right after this node. - void complete(const nixf::Node &Desc, std::vector &Items, + void complete(const nixf::ExprVar &Desc, std::vector &Items, const ParentMapAnalysis &PM) { - std::string Prefix; // empty string, accept all prefixes - if (Desc.kind() == Node::NK_Identifer) - Prefix = static_cast(Desc).name(); + std::string Prefix = Desc.id().name(); collectDef(Items, upEnv(Desc, VLA, PM), Prefix); } }; @@ -283,20 +283,6 @@ void completeAttrPath(const Node &N, const ParentMapAnalysis &PM, } } -void completeVarName(const VariableLookupAnalysis &VLA, - const ParentMapAnalysis &PM, const nixf::Node &N, - AttrSetClient &Client, std::vector &List) { - VLACompletionProvider VLAP(VLA); - VLAP.complete(N, List, PM); - if (havePackageScope(N, VLA, PM)) { - // Append it with nixpkgs completion - // FIXME: handle null nixpkgsClient() - NixpkgsCompletionProvider NCP(Client); - auto [Scope, Prefix] = getScopeAndPrefix(N, PM); - NCP.completePackages({Scope, Prefix}, List); - } -} - AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) { if (IsComplete) { return { @@ -312,6 +298,32 @@ AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) { }; } +#define DBG DBGPREFIX ": " + +void completeVarName(const VariableLookupAnalysis &VLA, + const ParentMapAnalysis &PM, const nixf::ExprVar &N, + AttrSetClient &Client, std::vector &List) { +#define DBGPREFIX "completion/var" + + VLACompletionProvider VLAP(VLA); + VLAP.complete(N, List, PM); + + // Try to complete the name by known idioms. + try { + Selector Sel = mkIdiomSelector(N, VLA, PM); + NixpkgsCompletionProvider NCP(Client); + + // Invoke nixpkgs provider to get the completion list. + // Variable names are always incomplete. + NCP.completePackages(mkParams(Sel, /*IsComplete=*/false), List); + } catch (IdiomSelectorException &E) { + log(DBG "skipped, reason: {0}", E.what()); + return; + } + +#undef DBGPREFIX +} + /// \brief Complete a "select" expression. /// \param IsComplete Whether or not the last element of the selector is /// effectively incomplete. @@ -319,7 +331,10 @@ AttrPathCompleteParams mkParams(nixd::Selector Sel, bool IsComplete) { /// - incomplete: `lib.gen|` /// - complete: `lib.attrset.|` void completeSelect(const nixf::ExprSelect &Select, AttrSetClient &Client, - bool IsComplete, std::vector &List) { + const nixf::VariableLookupAnalysis &VLA, + const nixf::ParentMapAnalysis &PM, bool IsComplete, + std::vector &List) { +#define DBGPREFIX "completion/select" // The base expr for selecting. const nixf::Expr &BaseExpr = Select.expr(); @@ -331,30 +346,24 @@ void completeSelect(const nixf::ExprSelect &Select, AttrSetClient &Client, } const auto &Var = static_cast(BaseExpr); - - // See if the variable matches some idioms name we alreay know. - std::unordered_set S{idioms::Pkgs, idioms::Lib}; - auto It = S.find(Var.id().name()); - - // Unknown name, cannot deal with it. - if (It == S.end()) - return; - // Ask nixpkgs provider to get idioms completion. NixpkgsCompletionProvider NCP(Client); - // Construct the scope & prefix suitable for this "select". - Selector BaseSelector = It == S.find(idioms::Lib) - ? Selector{std::string(idioms::Lib)} - : Selector{}; + const auto Handler = [](std::exception &E) noexcept { + log(DBG "skipped, reason: {0}", E.what()); + }; try { - Selector Sel = mkSelector(Select, std::move(BaseSelector)); + Selector Sel = mkSelector(Select, mkIdiomSelector(Var, VLA, PM)); NCP.completePackages(mkParams(Sel, IsComplete), List); - } catch (DynamicNameException &DE) { - lspserver::log( - "completion/select: skip because attribute path contains dynamic attr"); + } catch (IdiomSelectorException &E) { + Handler(E); + return; + } catch (SelectorException &E) { + Handler(E); return; } + +#undef DBGPREFIX } } // namespace @@ -388,14 +397,14 @@ void Controller::onCompletion(const CompletionParams &Params, const Node &UpExpr = *MaybeUpExpr; Reply([&]() -> CompletionList { CompletionList List; + const VariableLookupAnalysis &VLA = *TU->variableLookup(); try { switch (UpExpr.kind()) { // In these cases, assume the cursor have "variable" scoping. - case Node::NK_ExprWith: - case Node::NK_ExprList: case Node::NK_ExprVar: { - const VariableLookupAnalysis &VLA = *TU->variableLookup(); - completeVarName(VLA, PM, N, *nixpkgsClient(), List.items); + completeVarName(VLA, PM, + static_cast(UpExpr), + *nixpkgsClient(), List.items); break; } // A "select" expression. e.g. @@ -405,8 +414,8 @@ void Controller::onCompletion(const CompletionParams &Params, case Node::NK_ExprSelect: { const auto &Select = static_cast(UpExpr); - completeSelect(Select, *nixpkgsClient(), N.kind() == Node::NK_Dot, - List.items); + completeSelect(Select, *nixpkgsClient(), VLA, PM, + N.kind() == Node::NK_Dot, List.items); break; } case Node::NK_ExprAttrs: { diff --git a/nixd/tools/nixd/test/completion-nixpkgs.md b/nixd/tools/nixd/test/completion-nixpkgs.md deleted file mode 100644 index 936851447..000000000 --- a/nixd/tools/nixd/test/completion-nixpkgs.md +++ /dev/null @@ -1,74 +0,0 @@ -# RUN: nixd --nixpkgs-expr='{ a = 1; b = 2; }' --lit-test < %s | FileCheck %s - -<-- initialize(0) - -```json -{ - "jsonrpc":"2.0", - "id":0, - "method":"initialize", - "params":{ - "processId":123, - "rootPath":"", - "capabilities":{ - }, - "trace":"off" - } -} -``` - - -<-- textDocument/didOpen - - -```json -{ - "jsonrpc":"2.0", - "method":"textDocument/didOpen", - "params":{ - "textDocument":{ - "uri":"file:///completion.nix", - "languageId":"nix", - "version":1, - "text":"with pkgs; [ ]" - } - } -} -``` - -```json -{ - "jsonrpc": "2.0", - "id": 1, - "method": "textDocument/completion", - "params": { - "textDocument": { - "uri": "file:///completion.nix" - }, - "position": { - "line": 0, - "character": 14 - }, - "context": { - "triggerKind": 1 - } - } -} -``` - -``` - CHECK: "kind": 5, -CHECK-NEXT: "label": "a", -CHECK-NEXT: "score": 0 -CHECK-NEXT: }, -CHECK-NEXT: { -CHECK-NEXT: "data": "{\"Prefix\":\"\",\"Scope\":[]}", -CHECK-NEXT: "kind": 5, -CHECK-NEXT: "label": "b", -CHECK-NEXT: "score": 0 -``` - - -```json -{"jsonrpc":"2.0","method":"exit"} -``` diff --git a/nixd/tools/nixd/test/completion.md b/nixd/tools/nixd/test/completion.md index b30528239..2ab6f89a5 100644 --- a/nixd/tools/nixd/test/completion.md +++ b/nixd/tools/nixd/test/completion.md @@ -30,7 +30,7 @@ "uri":"file:///completion.nix", "languageId":"nix", "version":1, - "text":"let xxx = 1; yy = 2 in x" + "text":"let xxx = 1; yy = 2; in x" } } } @@ -47,7 +47,7 @@ }, "position": { "line": 0, - "character": 23 + "character": 24 }, "context": { "triggerKind": 1 diff --git a/nixd/tools/nixd/test/completion-comment.md b/nixd/tools/nixd/test/completion/comment.md similarity index 100% rename from nixd/tools/nixd/test/completion-comment.md rename to nixd/tools/nixd/test/completion/comment.md diff --git a/nixd/tools/nixd/test/completion-dot-select.md b/nixd/tools/nixd/test/completion/dot-select.md similarity index 100% rename from nixd/tools/nixd/test/completion-dot-select.md rename to nixd/tools/nixd/test/completion/dot-select.md diff --git a/nixd/tools/nixd/test/completion-dot.md b/nixd/tools/nixd/test/completion/dot.md similarity index 100% rename from nixd/tools/nixd/test/completion-dot.md rename to nixd/tools/nixd/test/completion/dot.md diff --git a/nixd/tools/nixd/test/completion-nested.md b/nixd/tools/nixd/test/completion/nested.md similarity index 100% rename from nixd/tools/nixd/test/completion-nested.md rename to nixd/tools/nixd/test/completion/nested.md diff --git a/nixd/tools/nixd/test/completion/nixpkgs.md b/nixd/tools/nixd/test/completion/nixpkgs.md new file mode 100644 index 000000000..9f2208abb --- /dev/null +++ b/nixd/tools/nixd/test/completion/nixpkgs.md @@ -0,0 +1,90 @@ +# RUN: nixd --nixpkgs-expr='{ ax = 1; ay = 2; }' --lit-test < %s | FileCheck %s + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///completion.nix", + "languageId":"nix", + "version":1, + "text":"with pkgs; [ a ]" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///completion.nix" + }, + "position": { + "line": 0, + "character": 13 + }, + "context": { + "triggerKind": 1 + } + } +} +``` + +``` + CHECK: "id": 1, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "isIncomplete": false, +CHECK-NEXT: "items": [ +CHECK-NEXT: { +CHECK-NEXT: "data": "", +CHECK-NEXT: "kind": 14, +CHECK-NEXT: "label": "abort", +CHECK-NEXT: "score": 0 +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "data": "{\"Prefix\":\"a\",\"Scope\":[]}", +CHECK-NEXT: "kind": 5, +CHECK-NEXT: "label": "ax", +CHECK-NEXT: "score": 0 +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "data": "{\"Prefix\":\"a\",\"Scope\":[]}", +CHECK-NEXT: "kind": 5, +CHECK-NEXT: "label": "ay", +CHECK-NEXT: "score": 0 +CHECK-NEXT: } +CHECK-NEXT: ] +CHECK-NEXT: } +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/completion-option-stop.md b/nixd/tools/nixd/test/completion/option-stop.md similarity index 100% rename from nixd/tools/nixd/test/completion-option-stop.md rename to nixd/tools/nixd/test/completion/option-stop.md diff --git a/nixd/tools/nixd/test/completion-options-mid.md b/nixd/tools/nixd/test/completion/options-mid.md similarity index 100% rename from nixd/tools/nixd/test/completion-options-mid.md rename to nixd/tools/nixd/test/completion/options-mid.md diff --git a/nixd/tools/nixd/test/completion-options-no-snippet.md b/nixd/tools/nixd/test/completion/options-no-snippet.md similarity index 100% rename from nixd/tools/nixd/test/completion-options-no-snippet.md rename to nixd/tools/nixd/test/completion/options-no-snippet.md diff --git a/nixd/tools/nixd/test/completion-options-snippet.md b/nixd/tools/nixd/test/completion/options-snippet.md similarity index 100% rename from nixd/tools/nixd/test/completion-options-snippet.md rename to nixd/tools/nixd/test/completion/options-snippet.md diff --git a/nixd/tools/nixd/test/completion-options.md b/nixd/tools/nixd/test/completion/options.md similarity index 100% rename from nixd/tools/nixd/test/completion-options.md rename to nixd/tools/nixd/test/completion/options.md diff --git a/nixd/tools/nixd/test/completion-resolve.md b/nixd/tools/nixd/test/completion/resolve.md similarity index 100% rename from nixd/tools/nixd/test/completion-resolve.md rename to nixd/tools/nixd/test/completion/resolve.md diff --git a/nixd/tools/nixd/test/completion-select-lib.md b/nixd/tools/nixd/test/completion/select-lib.md similarity index 100% rename from nixd/tools/nixd/test/completion-select-lib.md rename to nixd/tools/nixd/test/completion/select-lib.md diff --git a/nixd/tools/nixd/test/completion-select.md b/nixd/tools/nixd/test/completion/select.md similarity index 100% rename from nixd/tools/nixd/test/completion-select.md rename to nixd/tools/nixd/test/completion/select.md