diff --git a/libnixf/include/nixf/Basic/Nodes/Attrs.h b/libnixf/include/nixf/Basic/Nodes/Attrs.h index cb9b398c2..ec0d9f4ae 100644 --- a/libnixf/include/nixf/Basic/Nodes/Attrs.h +++ b/libnixf/include/nixf/Basic/Nodes/Attrs.h @@ -89,10 +89,14 @@ class AttrName : public Node { class AttrPath : public Node { const std::vector> Names; + // FIXME: workaround for just recording the trailing dot. + const std::shared_ptr TrailingDot; public: - AttrPath(LexerCursorRange Range, std::vector> Names) - : Node(NK_AttrPath, Range), Names(std::move(Names)) {} + AttrPath(LexerCursorRange Range, std::vector> Names, + std::shared_ptr TrailingDot) + : Node(NK_AttrPath, Range), Names(std::move(Names)), + TrailingDot(std::move(TrailingDot)) {} [[nodiscard]] const std::vector> &names() const { return Names; @@ -100,10 +104,11 @@ class AttrPath : public Node { [[nodiscard]] ChildVector children() const override { ChildVector Children; - Children.reserve(Names.size()); + Children.reserve(Names.size() + 1); for (const auto &Name : Names) { Children.push_back(Name.get()); } + Children.emplace_back(TrailingDot.get()); return Children; } }; diff --git a/libnixf/src/Parse/ParseAttrs.cpp b/libnixf/src/Parse/ParseAttrs.cpp index 01a6a9b48..bd025756b 100644 --- a/libnixf/src/Parse/ParseAttrs.cpp +++ b/libnixf/src/Parse/ParseAttrs.cpp @@ -36,6 +36,7 @@ std::shared_ptr Parser::parseAttrPath() { assert(LastToken && "LastToken should be set after valid attrname"); std::vector> AttrNames; AttrNames.emplace_back(std::move(First)); + std::shared_ptr TrailingDot; while (true) { if (Token Tok = peek(); Tok.kind() == tok_dot) { consume(); @@ -47,6 +48,7 @@ std::shared_ptr Parser::parseAttrPath() { D.fix("remove extra .").edit(TextEdit::mkRemoval(Tok.range())); D.fix("insert dummy attrname") .edit(TextEdit::mkInsertion(Tok.rCur(), R"("dummy")")); + TrailingDot = std::make_shared(Tok.range()); continue; } AttrNames.emplace_back(std::move(Next)); @@ -55,7 +57,8 @@ std::shared_ptr Parser::parseAttrPath() { break; } return std::make_shared(LexerCursorRange{Begin, LastToken->rCur()}, - std::move(AttrNames)); + std::move(AttrNames), + std::move(TrailingDot)); } std::shared_ptr Parser::parseBinding() { diff --git a/libnixf/src/Sema/SemaActions.cpp b/libnixf/src/Sema/SemaActions.cpp index 206a6915c..22546b7d2 100644 --- a/libnixf/src/Sema/SemaActions.cpp +++ b/libnixf/src/Sema/SemaActions.cpp @@ -330,7 +330,8 @@ std::shared_ptr Sema::desugarInheritExpr(std::shared_ptr Name, return std::make_shared(Range, Name->id()); auto Path = std::make_shared( - Range, std::vector>{std::move(Name)}); + Range, std::vector>{std::move(Name)}, + /*TrailingDot=*/nullptr); return std::make_shared(Range, std::move(E), std::move(Path), nullptr); } diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 27f000200..fbbbb44a8 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -14,16 +14,21 @@ namespace nixd { class Controller : public lspserver::LSPServer { +public: + using OptionMapTy = std::map>; + +private: std::unique_ptr Eval; // Use this worker for evaluating nixpkgs. std::unique_ptr NixpkgsEval; + std::mutex OptionsLock; // Map of option providers. // // e.g. "nixos" -> nixos worker // "home-manager" -> home-manager worker - std::map> Options; + OptionMapTy Options; // GUARDED_BY(OptionsLock) AttrSetClientProc &nixpkgsEval() { assert(NixpkgsEval); diff --git a/nixd/lib/Controller/AST.cpp b/nixd/lib/Controller/AST.cpp index a068a6ee8..6bc0aadf8 100644 --- a/nixd/lib/Controller/AST.cpp +++ b/nixd/lib/Controller/AST.cpp @@ -123,3 +123,30 @@ nixd::getSelectAttrPath(const nixf::AttrName &N, __builtin_unreachable(); } } + +std::optional> +nixd::findAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM) { + if (const Node *Name = PM.upTo(N, Node::NK_AttrName)) { + std::vector AttrPath; + if (const auto *Expr = PM.upExpr(N)) + AttrPath = getValueAttrPath(*Expr, PM); + auto Select = getSelectAttrPath(static_cast(*Name), PM); + AttrPath.insert(AttrPath.end(), Select.begin(), Select.end()); + assert(!AttrPath.empty()); + return AttrPath; + } + + // Consider this is an "extra" dot. + if (const Node *APNode = PM.upTo(N, Node::NK_AttrPath)) { + const auto &AP = static_cast(*APNode); + std::vector AttrPathVec; + if (const auto *Expr = PM.upExpr(N)) + AttrPathVec = getValueAttrPath(*Expr, PM); + assert(!AP.names().empty()); + auto Select = getSelectAttrPath(*AP.names().back(), PM); + AttrPathVec.insert(AttrPathVec.end(), Select.begin(), Select.end()); + AttrPathVec.emplace_back(""); + return AttrPathVec; + } + return std::nullopt; +} diff --git a/nixd/lib/Controller/AST.h b/nixd/lib/Controller/AST.h index c6bf99dbc..c3faf4e76 100644 --- a/nixd/lib/Controller/AST.h +++ b/nixd/lib/Controller/AST.h @@ -4,6 +4,8 @@ #include #include +#include + namespace nixd { /// \brief Search up until there are some node associated with "EnvNode". @@ -45,4 +47,9 @@ getValueAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM); std::vector getSelectAttrPath(const nixf::AttrName &N, const nixf::ParentMapAnalysis &PM); +/// \brief Heuristically find attrpath suitable for "attrpath" completion. +/// \returns non-empty std::vector attrpath. +std::optional> +findAttrPath(const nixf::Node &N, const nixf::ParentMapAnalysis &PM); + } // namespace nixd diff --git a/nixd/lib/Controller/Completion.cpp b/nixd/lib/Controller/Completion.cpp index 05d803179..b3e09ed97 100644 --- a/nixd/lib/Controller/Completion.cpp +++ b/nixd/lib/Controller/Completion.cpp @@ -229,6 +229,20 @@ class OptionCompletionProvider { } }; +void completeAttrName(std::vector Scope, std::string Prefix, + Controller::OptionMapTy &Options, bool CompletionSnippets, + std::vector &List) { + for (const auto &[Name, Provider] : Options) { + AttrSetClient *Client = Options.at(Name)->client(); + if (!Client) [[unlikely]] { + elog("skipped client {0} as it is dead", Name); + continue; + } + OptionCompletionProvider OCP(*Client, Name, CompletionSnippets); + OCP.completeOptions(Scope, Prefix, List); + } +} + } // namespace void Controller::onCompletion(const CompletionParams &Params, @@ -246,32 +260,19 @@ void Controller::onCompletion(const CompletionParams &Params, CompletionList List; try { const ParentMapAnalysis &PM = *TU->parentMap(); - - if (const Node *Name = PM.upTo(*Desc, Node::NK_AttrName)) { - // Complete attrpath. - std::vector AttrPath; - if (const auto *Expr = PM.upExpr(*Desc)) - AttrPath = getValueAttrPath(*Expr, PM); - auto Select = - getSelectAttrPath(static_cast(*Name), PM); - AttrPath.insert(AttrPath.end(), Select.begin(), Select.end()); - assert(!AttrPath.empty()); + if (auto AP = findAttrPath(*Desc, PM)) { + // Construct request. std::vector Scope; - Scope.reserve(AttrPath.size()); - for (std::string_view Name : AttrPath) { + Scope.reserve(AP->size()); + for (std::string_view Name : *AP) { Scope.emplace_back(Name); } std::string Prefix = Scope.back(); Scope.pop_back(); - for (const auto &[Name, Provider] : Options) { - AttrSetClient *Client = Options.at(Name)->client(); - if (!Client) [[unlikely]] { - elog("skipped client {0} as it is dead", Name); - continue; - } - OptionCompletionProvider OCP(*Client, Name, - ClientCaps.CompletionSnippets); - OCP.completeOptions(Scope, Prefix, List.items); + { + std::lock_guard _(OptionsLock); + completeAttrName(std::move(Scope), std::move(Prefix), Options, + ClientCaps.CompletionSnippets, List.items); } } else { const VariableLookupAnalysis &VLA = *TU->variableLookup(); diff --git a/nixd/lib/Controller/Configuration.cpp b/nixd/lib/Controller/Configuration.cpp index cff797801..87efa3372 100644 --- a/nixd/lib/Controller/Configuration.cpp +++ b/nixd/lib/Controller/Configuration.cpp @@ -58,6 +58,7 @@ void Controller::updateConfig(Configuration NewConfig) { } } if (!Config.options.empty()) { + std::lock_guard _(OptionsLock); // Stop option workers that are not listed in config. for (const auto &[Name, _] : Options) { if (!Config.options.contains(Name)) { diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index cfa58e25e..d81e39b3f 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -117,6 +117,7 @@ void Controller:: {"completionProvider", Object{ {"resolveProvider", true}, + {"triggerCharacters", {"."}}, }}, {"referencesProvider", true}, {"documentHighlightProvider", true}, @@ -150,11 +151,13 @@ void Controller:: } // Launch nixos worker also. - startOption("nixos", Options["nixos"]); - - if (AttrSetClient *Client = Options["nixos"]->client()) - evalExprWithProgress(*Client, DefaultNixOSOptionsExpr, "nixos options"); + { + std::lock_guard _(OptionsLock); + startOption("nixos", Options["nixos"]); + if (AttrSetClient *Client = Options["nixos"]->client()) + evalExprWithProgress(*Client, DefaultNixOSOptionsExpr, "nixos options"); + } fetchConfig(); } diff --git a/nixd/tools/nixd/test/completion-dot-select.md b/nixd/tools/nixd/test/completion-dot-select.md new file mode 100644 index 000000000..e791109a8 --- /dev/null +++ b/nixd/tools/nixd/test/completion-dot-select.md @@ -0,0 +1,74 @@ +# RUN: nixd --lit-test \ +# RUN: --nixos-options-expr="{ foo.bar = { _type = \"option\"; }; }" \ +# 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":"{ bar = 1; foo = foo.foo.; }" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///completion.nix" + }, + "position": { + "line": 0, + "character": 24 + }, + "context": { + "triggerKind": 1, + "triggerCharacter": "." + } + } +} +``` + +``` + CHECK: "id": 1, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "isIncomplete": false, +CHECK-NEXT: "items": [] +CHECK-NEXT: } +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/completion-dot.md b/nixd/tools/nixd/test/completion-dot.md new file mode 100644 index 000000000..faa1e0b98 --- /dev/null +++ b/nixd/tools/nixd/test/completion-dot.md @@ -0,0 +1,82 @@ +# RUN: nixd --lit-test \ +# RUN: --nixos-options-expr="{ foo.bar = { _type = \"option\"; }; }" \ +# 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":"{ bar = 1; foo. }" + } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/completion", + "params": { + "textDocument": { + "uri": "file:///completion.nix" + }, + "position": { + "line": 0, + "character": 14 + }, + "context": { + "triggerKind": 1, + "triggerCharacter": "." + } + } +} +``` + +``` + 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: "detail": "nixos", +CHECK-NEXT: "kind": 7, +CHECK-NEXT: "label": "foo", +CHECK-NEXT: "score": 0 +CHECK-NEXT: } +CHECK-NEXT: ] +CHECK-NEXT: } +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index 7bb4ff5e9..4406edc3f 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -33,7 +33,10 @@ CHECK-NEXT: ], CHECK-NEXT: "resolveProvider": false CHECK-NEXT: }, CHECK-NEXT: "completionProvider": { -CHECK-NEXT: "resolveProvider": true +CHECK-NEXT: "resolveProvider": true, +CHECK-NEXT: "triggerCharacters": [ +CHECK-NEXT: "." +CHECK-NEXT: ] CHECK-NEXT: }, CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "documentFormattingProvider": true,