diff --git a/nixd/include/nixd/Server/Controller.h b/nixd/include/nixd/Server/Controller.h index 06410ae01..daa5d33d8 100644 --- a/nixd/include/nixd/Server/Controller.h +++ b/nixd/include/nixd/Server/Controller.h @@ -248,7 +248,7 @@ class Controller : public lspserver::LSPServer { lspserver::Callback); void onCompletion(const lspserver::CompletionParams &, - lspserver::Callback); + lspserver::Callback); void onFormat(const lspserver::DocumentFormattingParams &, lspserver::Callback>); diff --git a/nixd/lib/AST/ParseAST.cpp b/nixd/lib/AST/ParseAST.cpp index 50bbe8e0c..cff8113f9 100644 --- a/nixd/lib/AST/ParseAST.cpp +++ b/nixd/lib/AST/ParseAST.cpp @@ -2,8 +2,8 @@ #include "lspserver/Protocol.h" #include "nixd/Expr/Expr.h" -#include "nixexpr.hh" +#include #include #include @@ -327,13 +327,10 @@ ParseAST::LocationContext ParseAST::getContext(lspserver::Position Pos) const { V.traverseExpr(root()); if (V.InAttrDef) return LocationContext::AttrName; - if (auto *E = lookupContainMin(Pos)) { - auto *EAttrs = dynamic_cast(E); - try { - if (lspserver::Range(nRange(E)).contains(Pos) && !EAttrs) - return LocationContext::Value; - } catch (std::out_of_range &) { - } + if (const auto *E = lookupContainMin(Pos)) { + const auto *EAttrs = dynamic_cast(E); + if (!EAttrs) + return LocationContext::Value; } return LocationContext::Unknown; } diff --git a/nixd/lib/Server/Controller.cpp b/nixd/lib/Server/Controller.cpp index 6a8c4a832..6c5d2d287 100644 --- a/nixd/lib/Server/Controller.cpp +++ b/nixd/lib/Server/Controller.cpp @@ -544,76 +544,91 @@ void Controller::onHover(const lspserver::TextDocumentPositionParams &Params, boost::asio::post(Pool, std::move(Task)); } -void Controller::onCompletion( - const lspserver::CompletionParams &Params, - lspserver::Callback Reply) { - auto EnableOption = Config.options.enable; - using RTy = lspserver::CompletionList; - using namespace lspserver; - constexpr auto Method = "nixd/ipc/textDocument/completion"; - auto Task = [=, Reply = std::move(Reply), this]() mutable { - auto Resp = askWC(Method, Params, {EvalWorkers, EvalWorkerLock, 2e6}); - std::optional R; - if (!Resp.empty()) - R = std::move(Resp.back()); - else { - // Statically construct the completion list. - std::binary_semaphore Smp(0); - try { - auto Path = Params.textDocument.uri.file(); - auto Action = [&Smp, &Resp, Pos = Params.position]( - const ParseAST &AST, ASTManager::VersionTy Version) { - auto Items = AST.completion(Pos); - Resp.emplace_back(CompletionList{false, Items}); - Smp.release(); - }; - if (auto Draft = DraftMgr.getDraft(Path)) { - auto Version = - EvalDraftStore::decodeVersion(Draft->Version).value_or(0); - ASTMgr.withAST(Path.str(), Version, std::move(Action)); - Smp.acquire(); - } - } catch (std::exception &E) { - lspserver::elog("completion/parseAST: {0}", stripANSI(E.what())); - } catch (...) { +void Controller::onCompletion(const lspserver::CompletionParams &Params, + lspserver::Callback Reply) { + // Statically construct the completion list. + std::binary_semaphore Smp(0); + auto Path = Params.textDocument.uri.file(); + auto Action = [&Smp, &Params, &Reply, + this](const ParseAST &AST, + ASTManager::VersionTy Version) mutable { + using PL = ParseAST::LocationContext; + using List = lspserver::CompletionList; + ReplyRAII RR(std::move(Reply)); + + // TODO: split this in AST, use AST-based attrpath construction. + auto CompletionFromOptions = [&, this]() -> std::optional { + // Requested completion on an attribute name, not values. + bool OptionsEnabled; + { + std::lock_guard _(ConfigLock); + OptionsEnabled = Config.options.enable; } - } + if (OptionsEnabled) { + ipc::AttrPathParams APParams; - if (EnableOption) { - ipc::AttrPathParams APParams; + if (Params.context.triggerCharacter == ".") { + // Get nixpkgs options + auto Code = llvm::StringRef( + *DraftMgr.getDraft(Params.textDocument.uri.file())->Contents); + auto ExpectedPosition = positionToOffset(Code, Params.position); - if (Params.context.triggerCharacter == ".") { - // Get nixpkgs options - auto Code = llvm::StringRef( - *DraftMgr.getDraft(Params.textDocument.uri.file())->Contents); - auto ExpectedPosition = positionToOffset(Code, Params.position); + // get the attr path + auto TruncateBackCode = Code.substr(0, ExpectedPosition.get()); - // get the attr path - auto TruncateBackCode = Code.substr(0, ExpectedPosition.get()); + auto [_, AttrPath] = TruncateBackCode.rsplit(" "); + APParams.Path = AttrPath.str(); + } - auto [_, AttrPath] = TruncateBackCode.rsplit(" "); - APParams.Path = AttrPath.str(); + auto Resp = askWC( + "nixd/ipc/textDocument/completion/options", APParams, + {OptionWorkers, OptionWorkerLock, 1e5}); + if (!Resp.empty()) + return Resp.back(); } + return std::nullopt; + }; - auto RespOption = askWC( - "nixd/ipc/textDocument/completion/options", APParams, - {OptionWorkers, OptionWorkerLock, 1e5}); - if (!RespOption.empty()) { - if (R) { - // Merge response and option response. - auto O = RespOption.back().items; - R->items.insert(R->items.end(), O.begin(), O.end()); - } else { - R = std::move(RespOption.back()); - } - } + auto CompletionFromEval = [&, this]() -> std::optional { + constexpr auto Method = "nixd/ipc/textDocument/completion"; + auto Resp = + askWC(Method, Params, {EvalWorkers, EvalWorkerLock, 2e6}); + std::optional R; + if (!Resp.empty()) + return std::move(Resp.back()); + return std::nullopt; + }; + + switch (AST.getContext(Params.position)) { + case (PL::AttrName): { + RR.Response = CompletionFromOptions(); + break; } - if (R) - Reply(std::move(*R)); - else - Reply(RTy{}); + case (PL::Value): { + RR.Response = CompletionFromEval(); + break; + } + case (PL::Unknown): + List L; + if (auto LOptions = CompletionFromOptions()) + L.items.insert(L.items.end(), (*LOptions).items.begin(), + (*LOptions).items.end()); + if (auto LEval = CompletionFromEval()) + L.items.insert(L.items.end(), (*LEval).items.begin(), + (*LEval).items.end()); + L.isIncomplete = true; + RR.Response = std::move(L); + } + Smp.release(); }; - boost::asio::post(Pool, std::move(Task)); + + if (auto Draft = DraftMgr.getDraft(Path)) { + auto Version = EvalDraftStore::decodeVersion(Draft->Version).value_or(0); + ASTMgr.withAST(Path.str(), Version, std::move(Action)); + Smp.acquire(); + } else { + Reply(lspserver::error("requested completion list on unknown draft path")); + } } void Controller::onRename(const lspserver::RenameParams &Params, diff --git a/nixd/tools/nixd/test/completion-attrset.md b/nixd/tools/nixd/test/completion-attrset.md index ba15b80a3..52ab8ffc9 100644 --- a/nixd/tools/nixd/test/completion-attrset.md +++ b/nixd/tools/nixd/test/completion-attrset.md @@ -100,7 +100,7 @@ Complete this file: CHECK: "id": 1, CHECK-NEXT: "jsonrpc": "2.0", CHECK-NEXT: "result": { -CHECK-NEXT: "isIncomplete": false, +CHECK-NEXT: "isIncomplete": true, CHECK-NEXT: "items": [ CHECK-NEXT: { CHECK-NEXT: "kind": 5,