From 2f2c80f7726dfae405b58e80b77c8dcb3b751c7f Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 16 Jul 2024 18:25:20 +0800 Subject: [PATCH] nixd: `lib` + `pkgs` completion with "select", e.g. `lib.gen|`, `pkgs.stdenv.mkDerivat|` --- nixd/include/nixd/Protocol/AttrSet.h | 10 +- nixd/lib/Controller/AST.cpp | 21 ++- nixd/lib/Controller/AST.h | 28 +++ nixd/lib/Controller/Completion.cpp | 169 ++++++++++++++---- nixd/tools/nixd/test/completion-select-lib.md | 77 ++++++++ nixd/tools/nixd/test/completion-select.md | 77 ++++++++ 6 files changed, 348 insertions(+), 34 deletions(-) create mode 100644 nixd/tools/nixd/test/completion-select-lib.md create mode 100644 nixd/tools/nixd/test/completion-select.md diff --git a/nixd/include/nixd/Protocol/AttrSet.h b/nixd/include/nixd/Protocol/AttrSet.h index d304cd989..aa379045d 100644 --- a/nixd/include/nixd/Protocol/AttrSet.h +++ b/nixd/include/nixd/Protocol/AttrSet.h @@ -26,7 +26,13 @@ constexpr inline std::string_view Exit = "exit"; using EvalExprParams = std::string; using EvalExprResponse = std::optional; -using AttrPathInfoParams = std::vector; +/// \brief A list of strings that "select"s into a attribute set. +using Selector = std::vector; + +using PackageInfoParams = Selector; +using OptionInfoParams = Selector; + +using AttrPathInfoParams = Selector; struct PackageDescription { std::optional Name; @@ -45,7 +51,7 @@ bool fromJSON(const llvm::json::Value &Params, PackageDescription &R, llvm::json::Path P); struct AttrPathCompleteParams { - std::vector Scope; + Selector Scope; /// \brief Search for packages prefixed with this "prefix" std::string Prefix; }; diff --git a/nixd/lib/Controller/AST.cpp b/nixd/lib/Controller/AST.cpp index e369ff09d..d0128adad 100644 --- a/nixd/lib/Controller/AST.cpp +++ b/nixd/lib/Controller/AST.cpp @@ -124,12 +124,31 @@ bool nixd::havePackageScope(const Node &N, const VariableLookupAnalysis &VLA, continue; // Hardcoded "pkgs", even more stupid. - if (static_cast(*WithBody).id().name() == "pkgs") + if (static_cast(*WithBody).id().name() == idioms::Pkgs) return true; } return false; } +nixd::Selector nixd::mkSelector(const nixf::AttrPath &AP, + Selector BaseSelector) { + const auto &Names = AP.names(); + for (const auto &Name : Names) { + if (!Name->isStatic()) + throw DynamicNameException(); + BaseSelector.emplace_back(Name->staticName()); + } + return BaseSelector; +} + +nixd::Selector nixd::mkSelector(const nixf::ExprSelect &Select, + nixd::Selector BaseSelector) { + if (Select.path()) + return nixd::mkSelector(*static_cast(Select.path()), + std::move(BaseSelector)); + return BaseSelector; +} + std::pair, std::string> nixd::getScopeAndPrefix(const Node &N, const ParentMapAnalysis &PM) { if (N.kind() != Node::NK_Identifer) diff --git a/nixd/lib/Controller/AST.h b/nixd/lib/Controller/AST.h index 63759dcb7..6f659cb2b 100644 --- a/nixd/lib/Controller/AST.h +++ b/nixd/lib/Controller/AST.h @@ -1,11 +1,29 @@ /// \file /// \brief This file declares some common analysis (tree walk) on the AST. +#include "nixd/Protocol/AttrSet.h" + #include #include namespace nixd { +namespace idioms { + +/// \brief Hardcoded name for "pkgs.xxx", or "with pkgs;" +/// +/// Assume that the value of this variable have the same structure with `import +/// nixpkgs {} +constexpr inline std::string_view Pkgs = "pkgs"; + +/// \brief Hardcoded name for nixpkgs "lib" +/// +/// Assume that the value of this variable is "nixpkgs lib". +/// e.g. lib.genAttrs. +constexpr inline std::string_view Lib = "lib"; + +} // namespace idioms + /// \brief Search up until there are some node associated with "EnvNode". [[nodiscard]] const nixf::EnvNode * upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA, @@ -29,6 +47,16 @@ upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA, std::pair, std::string> getScopeAndPrefix(const nixf::Node &N, const nixf::ParentMapAnalysis &PM); +/// \brief The attrpath has a dynamic name, thus it cannot be trivially +/// transformed to "static" selector. +struct DynamicNameException : std::exception {}; + +/// \brief Construct a nixd::Selector from \p AP. +Selector mkSelector(const nixf::AttrPath &AP, Selector BaseSelector); + +/// \brief Construct a nixd::Selector from \p Select. +Selector mkSelector(const nixf::ExprSelect &Select, Selector BaseSelector); + enum class FindAttrPathResult { OK, Inherit, diff --git a/nixd/lib/Controller/Completion.cpp b/nixd/lib/Controller/Completion.cpp index efce25955..917ff957c 100644 --- a/nixd/lib/Controller/Completion.cpp +++ b/nixd/lib/Controller/Completion.cpp @@ -14,6 +14,7 @@ #include #include +#include #include using namespace nixd; @@ -123,7 +124,7 @@ class NixpkgsCompletionProvider { } /// \brief Ask nixpkgs provider, give us a list of names. (thunks) - void completePackages(std::vector Scope, std::string Prefix, + void completePackages(const AttrPathCompleteParams &Params, std::vector &Items) { std::binary_semaphore Ready(0); std::vector Names; @@ -138,12 +139,11 @@ class NixpkgsCompletionProvider { Ready.release(); }; // Send request. - AttrPathCompleteParams Params{std::move(Scope), std::move(Prefix)}; NixpkgsClient.attrpathComplete(Params, std::move(OnReply)); Ready.acquire(); // Now we have "Names", use these to fill "Items". for (const auto &Name : Names) { - if (Name.starts_with(Prefix)) { + if (Name.starts_with(Params.Prefix)) { addItem(Items, CompletionItem{ .label = Name, .kind = CompletionItemKind::Field, @@ -265,6 +265,98 @@ void completeAttrName(const std::vector &Scope, } } +void completeAttrPath(const Node &N, const ParentMapAnalysis &PM, + std::mutex &OptionsLock, Controller::OptionMapTy &Options, + bool Snippets, + std::vector &Items) { + std::vector Scope; + using PathResult = FindAttrPathResult; + auto R = findAttrPath(N, PM, Scope); + if (R == PathResult::OK) { + // Construct request. + std::string Prefix = Scope.back(); + Scope.pop_back(); + { + std::lock_guard _(OptionsLock); + completeAttrName(Scope, Prefix, Options, Snippets, Items); + } + } +} + +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 { + .Scope = std::move(Sel), + .Prefix = "", + }; + } + std::string Back = std::move(Sel.back()); + Sel.pop_back(); + return { + .Scope = Sel, + .Prefix = std::move(Back), + }; +} + +/// \brief Complete a "select" expression. +/// \param IsComplete Whether or not the last element of the selector is +/// effectively incomplete. +/// e.g. +/// - incomplete: `lib.gen|` +/// - complete: `lib.attrset.|` +void completeSelect(const nixf::ExprSelect &Select, AttrSetClient &Client, + bool IsComplete, std::vector &List) { + // The base expr for selecting. + const nixf::Expr &BaseExpr = Select.expr(); + + // Determine that the name is one of special names interesting + // for nix language. If it is not a simple variable, skip this + // case. + if (BaseExpr.kind() != Node::NK_ExprVar) { + return; + } + + 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{}; + try { + Selector Sel = mkSelector(Select, std::move(BaseSelector)); + NCP.completePackages(mkParams(Sel, IsComplete), List); + } catch (DynamicNameException &DE) { + lspserver::log( + "completion/select: skip because attribute path contains dynamic attr"); + return; + } +} + } // namespace void Controller::onCompletion(const CompletionParams &Params, @@ -283,38 +375,53 @@ void Controller::onCompletion(const CompletionParams &Params, Reply(CompletionList{}); return; } - CompletionList List; - try { - const ParentMapAnalysis &PM = *TU->parentMap(); - std::vector Scope; - using PathResult = FindAttrPathResult; - auto R = findAttrPath(*Desc, PM, Scope); - if (R == PathResult::OK) { - // Construct request. - std::string Prefix = Scope.back(); - Scope.pop_back(); - { - std::lock_guard _(OptionsLock); - completeAttrName(Scope, Prefix, Options, + const nixf::Node &N = *Desc; + const ParentMapAnalysis &PM = *TU->parentMap(); + const Node *MaybeUpExpr = PM.upExpr(N); + if (!MaybeUpExpr) { + // If there is no concrete expression containing the cursor + // Reply an empty list. + Reply(CompletionList{}); + return; + } + // Otherwise, construct the completion list from a set of providers. + const Node &UpExpr = *MaybeUpExpr; + Reply([&]() -> CompletionList { + CompletionList List; + 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); + break; + } + // A "select" expression. e.g. + // foo.a| + // foo.| + // foo.a.bar| + case Node::NK_ExprSelect: { + const auto &Select = + static_cast(UpExpr); + completeSelect(Select, *nixpkgsClient(), N.kind() == Node::NK_Dot, + List.items); + break; + } + case Node::NK_ExprAttrs: { + completeAttrPath(N, PM, OptionsLock, Options, ClientCaps.CompletionSnippets, List.items); + break; } - } else { - const VariableLookupAnalysis &VLA = *TU->variableLookup(); - VLACompletionProvider VLAP(VLA); - VLAP.complete(*Desc, List.items, PM); - if (havePackageScope(*Desc, VLA, PM)) { - // Append it with nixpkgs completion - // FIXME: handle null nixpkgsClient() - NixpkgsCompletionProvider NCP(*nixpkgsClient()); - auto [Scope, Prefix] = getScopeAndPrefix(*Desc, PM); - NCP.completePackages(Scope, Prefix, List.items); + default: + break; } + } catch (ExceedSizeError &Err) { + List.isIncomplete = true; } - // Next, add nixpkgs provided names. - } catch (ExceedSizeError &Err) { - List.isIncomplete = true; - } - Reply(std::move(List)); + return List; + }()); } } }; diff --git a/nixd/tools/nixd/test/completion-select-lib.md b/nixd/tools/nixd/test/completion-select-lib.md new file mode 100644 index 000000000..50172b57a --- /dev/null +++ b/nixd/tools/nixd/test/completion-select-lib.md @@ -0,0 +1,77 @@ +# RUN: nixd --lit-test \ +# RUN: --nixpkgs-expr="{ lib.hello.meta.description = \"Very Nice\"; }" \ +# RUN: < %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":"lib.hel" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///completion.nix" + }, + "position": { + "line": 0, + "character": 6 + }, + "context": { + "triggerKind": 1 + } + } +} +``` + +``` + CHECK: "isIncomplete": false, +CHECK-NEXT: "items": [ +CHECK-NEXT: { +CHECK-NEXT: "data": "{\"Prefix\":\"hel\",\"Scope\":[\"lib\"]}", +CHECK-NEXT: "kind": 5, +CHECK-NEXT: "label": "hello", +CHECK-NEXT: "score": 0 +CHECK-NEXT: } +CHECK-NEXT: ] +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/completion-select.md b/nixd/tools/nixd/test/completion-select.md new file mode 100644 index 000000000..31fe8bd18 --- /dev/null +++ b/nixd/tools/nixd/test/completion-select.md @@ -0,0 +1,77 @@ +# RUN: nixd --lit-test \ +# RUN: --nixpkgs-expr="{ hello.meta.description = \"Very Nice\"; }" \ +# RUN: < %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":"pkgs.hel" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///completion.nix" + }, + "position": { + "line": 0, + "character": 7 + }, + "context": { + "triggerKind": 1 + } + } +} +``` + +``` + CHECK: "isIncomplete": false, +CHECK-NEXT: "items": [ +CHECK-NEXT: { +CHECK-NEXT: "data": "{\"Prefix\":\"hel\",\"Scope\":[]}", +CHECK-NEXT: "kind": 5, +CHECK-NEXT: "label": "hello", +CHECK-NEXT: "score": 0 +CHECK-NEXT: } +CHECK-NEXT: ] +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +```