From f510da27f8f36d40a7633017a88fd0522f327577 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Mon, 15 Apr 2024 03:51:05 +0800 Subject: [PATCH] nixd: complete nixpkgs (#411) --- default.nix | 2 +- lspserver/include/lspserver/Protocol.h | 2 + lspserver/src/Protocol.cpp | 12 ++ nixd/include/nixd/CommandLine/Options.h | 7 ++ nixd/include/nixd/Controller/Controller.h | 16 +++ nixd/include/nixd/Eval/AttrSetClient.h | 71 +++++++++++ nixd/include/nixd/Support/ForkPiped.h | 11 +- nixd/include/nixd/Support/StreamProc.h | 25 ++++ nixd/lib/CommandLine/Options.cpp | 3 + nixd/lib/Controller/AST.cpp | 52 ++++++++ nixd/lib/Controller/AST.h | 32 +++++ nixd/lib/Controller/Completion.cpp | 132 ++++++++++++++++++--- nixd/lib/Controller/LifeTime.cpp | 62 +++++++--- nixd/lib/Controller/Support.cpp | 2 + nixd/lib/Eval/AttrSetClient.cpp | 33 ++++++ nixd/lib/Support/ForkPiped.cpp | 18 +-- nixd/lib/Support/StreamProc.cpp | 29 +++++ nixd/meson.build | 41 ++++--- nixd/tools/nixd.cpp | 3 +- nixd/tools/nixd/test/completion-nixpkgs.md | 74 ++++++++++++ nixd/tools/nixd/test/completion-resolve.md | 50 ++++++++ nixd/tools/nixd/test/completion.md | 1 + nixd/tools/nixd/test/initialize.md | 4 +- nixd/tools/nixd/test/lit.cfg.py | 3 + 24 files changed, 623 insertions(+), 62 deletions(-) create mode 100644 nixd/include/nixd/CommandLine/Options.h create mode 100644 nixd/include/nixd/Eval/AttrSetClient.h create mode 100644 nixd/include/nixd/Support/StreamProc.h create mode 100644 nixd/lib/CommandLine/Options.cpp create mode 100644 nixd/lib/Controller/AST.cpp create mode 100644 nixd/lib/Controller/AST.h create mode 100644 nixd/lib/Eval/AttrSetClient.cpp create mode 100644 nixd/lib/Support/StreamProc.cpp create mode 100644 nixd/tools/nixd/test/completion-nixpkgs.md create mode 100644 nixd/tools/nixd/test/completion-resolve.md diff --git a/default.nix b/default.nix index 8f2da0aa6..dce264b2a 100644 --- a/default.nix +++ b/default.nix @@ -66,7 +66,7 @@ stdenv.mkDerivation { # Disable nixd regression tests, because it uses some features provided by # nix, and does not correctly work in the sandbox - meson test --print-errorlogs unit/libnixf/Basic unit/libnixf/Parse unit/libnixt regression/nixd + meson test --print-errorlogs unit/libnixf/Basic unit/libnixf/Parse unit/libnixt runHook postCheck ''; diff --git a/lspserver/include/lspserver/Protocol.h b/lspserver/include/lspserver/Protocol.h index f620a3bae..da862ce2a 100644 --- a/lspserver/include/lspserver/Protocol.h +++ b/lspserver/include/lspserver/Protocol.h @@ -1291,8 +1291,10 @@ struct CompletionItem { // // data?: any - A data entry field that is preserved on a completion item // between a completion and a completion resolve request. + std::string data; }; llvm::json::Value toJSON(const CompletionItem &); +bool fromJSON(const llvm::json::Value &, CompletionItem &, llvm::json::Path); llvm::raw_ostream &operator<<(llvm::raw_ostream &, const CompletionItem &); bool operator<(const CompletionItem &, const CompletionItem &); diff --git a/lspserver/src/Protocol.cpp b/lspserver/src/Protocol.cpp index ec00b2564..dd33890e0 100644 --- a/lspserver/src/Protocol.cpp +++ b/lspserver/src/Protocol.cpp @@ -987,9 +987,21 @@ llvm::json::Value toJSON(const CompletionItem &CI) { if (CI.deprecated) Result["deprecated"] = CI.deprecated; Result["score"] = CI.score; + Result["data"] = CI.data; return Result; } +bool fromJSON(const llvm::json::Value &Params, CompletionItem &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + int Kind; + O.mapOptional("kind", Kind); + R.kind = static_cast(Kind); + return O // + && O.mapOptional("label", R.label) // + && O.mapOptional("data", R.data); // +} + llvm::raw_ostream &operator<<(llvm::raw_ostream &O, const CompletionItem &I) { O << I.label << " - " << toJSON(I); return O; diff --git a/nixd/include/nixd/CommandLine/Options.h b/nixd/include/nixd/CommandLine/Options.h new file mode 100644 index 000000000..eea5e19e7 --- /dev/null +++ b/nixd/include/nixd/CommandLine/Options.h @@ -0,0 +1,7 @@ +#include + +namespace nixd { + +extern llvm::cl::OptionCategory NixdCategory; + +} // namespace nixd diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 1b4305d02..7ebdf9b97 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -6,6 +6,7 @@ #include "lspserver/DraftStore.h" #include "lspserver/LSPServer.h" #include "lspserver/Protocol.h" +#include "nixd/Eval/AttrSetClient.h" #include @@ -13,6 +14,17 @@ namespace nixd { class Controller : public lspserver::LSPServer { std::unique_ptr Eval; + + // Use this worker for evaluating nixpkgs. + std::unique_ptr NixpkgsEval; + + AttrSetClientProc &nixpkgsEval() { + assert(NixpkgsEval); + return *NixpkgsEval; + } + + AttrSetClient *nixpkgsClient() { return nixpkgsEval().client(); } + lspserver::DraftStore Store; llvm::unique_function @@ -91,6 +103,10 @@ class Controller : public lspserver::LSPServer { void onCompletion(const lspserver::CompletionParams &Params, lspserver::Callback Reply); + void + onCompletionItemResolve(const lspserver::CompletionItem &Params, + lspserver::Callback Reply); + void onDefinition(const lspserver::TextDocumentPositionParams &Params, lspserver::Callback Reply); diff --git a/nixd/include/nixd/Eval/AttrSetClient.h b/nixd/include/nixd/Eval/AttrSetClient.h new file mode 100644 index 000000000..f10aa652c --- /dev/null +++ b/nixd/include/nixd/Eval/AttrSetClient.h @@ -0,0 +1,71 @@ +#pragma once + +#include "nixd/Protocol/AttrSet.h" +#include "nixd/Support/StreamProc.h" + +#include + +#include + +namespace nixd { + +class AttrSetClient : public lspserver::LSPServer { + + llvm::unique_function Reply)> + EvalExpr; + + llvm::unique_function Reply)> + AttrPathInfo; + + llvm::unique_function Reply)> + AttrPathComplete; + +public: + AttrSetClient(std::unique_ptr In, + std::unique_ptr Out); + + /// \brief Request eval some expression. + /// The expression should be evaluted to attrset. + void evalExpr(const EvalExprParams &Params, + lspserver::Callback Reply) { + return EvalExpr(Params, std::move(Reply)); + } + + void attrpathInfo(const AttrPathInfoParams &Params, + lspserver::Callback Reply) { + AttrPathInfo(Params, std::move(Reply)); + } + + void attrpathComplete(const AttrPathCompleteParams &Params, + lspserver::Callback Reply) { + AttrPathComplete(Params, std::move(Reply)); + } + + /// Get executable path for launching the server. + /// \returns null terminated string. + static const char *getExe(); +}; + +class AttrSetClientProc { + StreamProc Proc; + AttrSetClient Client; + std::thread Input; + +public: + /// \brief Check if the process is still alive + /// \returns nullptr if it has been dead. + AttrSetClient *client(); + ~AttrSetClientProc() { + Client.closeInbound(); + Input.join(); + } + + /// \see StreamProc::StreamProc + AttrSetClientProc(const std::function &Action); +}; + +} // namespace nixd diff --git a/nixd/include/nixd/Support/ForkPiped.h b/nixd/include/nixd/Support/ForkPiped.h index c2b2da6a9..c802dc7c6 100644 --- a/nixd/include/nixd/Support/ForkPiped.h +++ b/nixd/include/nixd/Support/ForkPiped.h @@ -1,7 +1,14 @@ #pragma once -namespace nixd::util { +namespace nixd { +/// \brief fork this process and create some pipes connected to the new process. +/// +/// stdin, stdout, stderr in the new process will be closed, and these fds could +/// be used to communicate with it. +/// +/// \returns pid of child process, in parent. +/// \returns 0 in child. int forkPiped(int &In, int &Out, int &Err); -} // namespace nixd::util +} // namespace nixd diff --git a/nixd/include/nixd/Support/StreamProc.h b/nixd/include/nixd/Support/StreamProc.h new file mode 100644 index 000000000..bbe66e6ab --- /dev/null +++ b/nixd/include/nixd/Support/StreamProc.h @@ -0,0 +1,25 @@ +#pragma once + +#include "nixd/Support/PipedProc.h" + +#include +#include + +namespace nixd { + +struct StreamProc { + std::unique_ptr Proc; + std::unique_ptr Stream; + + /// \brief Launch a streamed process with \p Action. + /// + /// The value returned by \p Action will be interpreted as process's exit + /// value. + StreamProc(const std::function &Action); + + [[nodiscard]] std::unique_ptr mkIn() const; + + [[nodiscard]] std::unique_ptr mkOut() const; +}; + +} // namespace nixd diff --git a/nixd/lib/CommandLine/Options.cpp b/nixd/lib/CommandLine/Options.cpp new file mode 100644 index 000000000..4333d2e3d --- /dev/null +++ b/nixd/lib/CommandLine/Options.cpp @@ -0,0 +1,3 @@ +#include "nixd/CommandLine/Options.h" + +llvm::cl::OptionCategory nixd::NixdCategory("nixd library options"); diff --git a/nixd/lib/Controller/AST.cpp b/nixd/lib/Controller/AST.cpp new file mode 100644 index 000000000..af4a98222 --- /dev/null +++ b/nixd/lib/Controller/AST.cpp @@ -0,0 +1,52 @@ +#include "AST.h" + +using namespace nixf; + +[[nodiscard]] const EnvNode *nixd::upEnv(const nixf::Node &Desc, + const VariableLookupAnalysis &VLA, + const ParentMapAnalysis &PM) { + const nixf::Node *N = &Desc; // @nonnull + while (!VLA.env(N) && PM.query(*N) && !PM.isRoot(*N)) + N = PM.query(*N); + assert(N); + return VLA.env(N); +} + +bool nixd::havePackageScope(const Node &N, const VariableLookupAnalysis &VLA, + const ParentMapAnalysis &PM) { + // Firstly find the first "env" enclosed with this variable. + const EnvNode *Env = upEnv(N, VLA, PM); + if (!Env) + return false; + + // Then, search up until there are some `with`. + for (; Env; Env = Env->parent()) { + if (!Env->isWith()) + continue; + // this env is "with" expression. + const Node *With = Env->syntax(); + assert(With && With->kind() == Node::NK_ExprWith); + const Node *WithBody = static_cast(*With).with(); + if (!WithBody) + continue; // skip incomplete with epxression. + + // Se if it is "ExprVar“. Stupid. + if (WithBody->kind() != Node::NK_ExprVar) + continue; + + // Hardcoded "pkgs", even more stupid. + if (static_cast(*WithBody).id().name() == "pkgs") + return true; + } + return false; +} + +std::pair, std::string> +nixd::getScopeAndPrefix(const Node &N, const ParentMapAnalysis &PM) { + if (N.kind() != Node::NK_Identifer) + return {}; + + // FIXME: impl scoped packages + std::string Prefix = static_cast(N).name(); + return {{}, Prefix}; +} diff --git a/nixd/lib/Controller/AST.h b/nixd/lib/Controller/AST.h new file mode 100644 index 000000000..c4ff3e22e --- /dev/null +++ b/nixd/lib/Controller/AST.h @@ -0,0 +1,32 @@ +/// \file +/// \brief This file declares some common analysis (tree walk) on the AST. + +#include +#include + +namespace nixd { + +/// \brief Search up until there are some node associated with "EnvNode". +[[nodiscard]] const nixf::EnvNode * +upEnv(const nixf::Node &Desc, const nixf::VariableLookupAnalysis &VLA, + const nixf::ParentMapAnalysis &PM); + +/// \brief Determine whether or not some node has enclosed "with pkgs; [ ]" +/// +/// Yes, this evaluation isn't flawless. What if the identifier isn't "pkgs"? We +/// can't dynamically evaluate everything each time and invalidate them +/// immediately after document updates. Therefore, this heuristic method +/// represents a trade-off between performance considerations. +[[nodiscard]] bool havePackageScope(const nixf::Node &N, + const nixf::VariableLookupAnalysis &VLA, + const nixf::ParentMapAnalysis &PM); + +/// \brief get variable scope, and it's prefix name. +/// +/// Nixpkgs has some packages scoped in "nested" attrs. +/// e.g. llvmPackages, pythonPackages. +/// Try to find these name as a pre-selected scope, the last value is "prefix". +std::pair, std::string> +getScopeAndPrefix(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 7db7dccdf..b8d702e8a 100644 --- a/nixd/lib/Controller/Completion.cpp +++ b/nixd/lib/Controller/Completion.cpp @@ -3,12 +3,16 @@ /// [Code Completion]: /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion +#include "AST.h" #include "Convert.h" #include "nixd/Controller/Controller.h" #include +#include +#include + using namespace nixd; using namespace lspserver; using namespace nixf; @@ -67,23 +71,80 @@ class VLACompletionProvider { VLACompletionProvider(const VariableLookupAnalysis &VLA) : VLA(VLA) {} /// Perform code completion right after this node. - /// \returns true if the completion list is in-complete - bool complete(const nixf::Node &Desc, std::vector &Items, + void complete(const nixf::Node &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(); - try { - const nixf::Node *N = &Desc; // @nonnull - while (!VLA.env(N) && PM.query(*N) && !PM.isRoot(*N)) - N = PM.query(*N); - - assert(N); - collectDef(Items, VLA.env(N), Prefix); - } catch (const ExceedSizeError &Err) { - return true; + collectDef(Items, upEnv(Desc, VLA, PM), Prefix); + } +}; + +/// \brief Provide completions by IPC. Asking nixpkgs provider. +/// We simply select nixpkgs in separate process, thus this value does not need +/// to be cached. (It is already cached in separate process.) +/// +/// Currently, this procedure is explicitly blocked (synchronized). Because +/// query nixpkgs value is relatively fast. In the future there might be nixd +/// index, for performance. +class NixpkgsCompletionProvider { + + AttrSetClient &NixpkgsClient; + +public: + NixpkgsCompletionProvider(AttrSetClient &NixpkgsClient) + : NixpkgsClient(NixpkgsClient) {} + + void resolvePackage(std::vector Scope, std::string Name, + CompletionItem &Item) { + std::binary_semaphore Ready(0); + PackageDescription Desc; + auto OnReply = [&Ready, &Desc](llvm::Expected Resp) { + if (Resp) + Desc = *Resp; + Ready.release(); + }; + Scope.emplace_back(std::move(Name)); + NixpkgsClient.attrpathInfo(Scope, std::move(OnReply)); + Ready.acquire(); + // Format "detail" and document. + Item.documentation = MarkupContent{ + .kind = MarkupKind::Markdown, + .value = Desc.Description.value_or("") + "\n\n" + + Desc.LongDescription.value_or(""), + }; + Item.detail = Desc.Version.value_or("?"); + } + + /// \brief Ask nixpkgs provider, give us a list of names. (thunks) + void completePackages(std::vector Scope, std::string Prefix, + std::vector &Items) { + std::binary_semaphore Ready(0); + std::vector Names; + auto OnReply = [&Ready, + &Names](llvm::Expected Resp) { + if (!Resp) { + lspserver::elog("nixpkgs evaluator reported: {0}", Resp.takeError()); + Ready.release(); + return; + } + Names = *Resp; // Copy response to waiting thread. + 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)) { + addItem(Items, CompletionItem{ + .label = Name, + .kind = CompletionItemKind::Field, + .data = llvm::formatv("{0}", toJSON(Params)), + }); + } } - return false; } }; @@ -102,11 +163,54 @@ void Controller::onCompletion(const CompletionParams &Params, return; } CompletionList List; - VLACompletionProvider VLAP(*TU->variableLookup()); - List.isIncomplete = VLAP.complete(*Desc, List.items, *TU->parentMap()); + try { + const ParentMapAnalysis &PM = *TU->parentMap(); + 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); + } + // Next, add nixpkgs provided names. + } catch (ExceedSizeError &Err) { + List.isIncomplete = true; + } Reply(std::move(List)); } } }; boost::asio::post(Pool, std::move(Action)); } + +void Controller::onCompletionItemResolve(const CompletionItem &Params, + Callback Reply) { + + auto Action = [Params, Reply = std::move(Reply), this]() mutable { + if (Params.data.empty()) { + Reply(Params); + return; + } + AttrPathCompleteParams Req; + auto EV = llvm::json::parse(Params.data); + if (!EV) { + // If the json value cannot be parsed, this is very unlikely to happen. + Reply(EV.takeError()); + return; + } + + llvm::json::Path::Root Root; + fromJSON(*EV, Req, Root); + + // FIXME: handle null nixpkgsClient() + NixpkgsCompletionProvider NCP(*nixpkgsClient()); + CompletionItem Resp = Params; + NCP.resolvePackage(Req.Scope, Params.label, Resp); + + Reply(std::move(Resp)); + }; + boost::asio::post(Pool, std::move(Action)); +} diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index 8f4d43675..8874b67c9 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -5,18 +5,50 @@ #include "nixd-config.h" +#include "nixd/CommandLine/Options.h" #include "nixd/Controller/Controller.h" -#include "nixd/Controller/EvalClient.h" -#include "nixd/Support/PipedProc.h" - -#include - -namespace nixd { +using namespace nixd; using namespace util; using namespace llvm::json; +using namespace llvm::cl; using namespace lspserver; +namespace { + +opt DefaultNixpkgsExpr{ + "nixpkgs-expr", + desc("Default expression intrepreted as `import { }`"), + cat(NixdCategory), init("import { }")}; + +opt NixpkgsWorkerStderr{ + "nixpkgs-worker-stderr", + desc("Writable file path for nixpkgs worker stderr (debugging)"), + cat(NixdCategory), init("/dev/null")}; + +void notifyNixpkgsEval(AttrSetClient &NixpkgsProvider) { + lspserver::log("launched nixd attrs eval for nixpkgs"); + auto Action = [](llvm::Expected Resp) { + if (!Resp) { + lspserver::elog("error on nixpkgs attrs worker eval: {0}", + Resp.takeError()); + return; + } + lspserver::log("evaluated nixpkgs entries"); + }; + // Tell nixpkgs worker to eval + NixpkgsProvider.evalExpr(DefaultNixpkgsExpr, std::move(Action)); +} + +void startNixpkgs(std::unique_ptr &NixpkgsEval) { + NixpkgsEval = std::make_unique([]() { + freopen(NixpkgsWorkerStderr.c_str(), "w", stderr); + return execl(AttrSetClient::getExe(), "nixd-attrset-eval", nullptr); + }); +} + +} // namespace + void Controller:: onInitialize( // NOLINT(readability-convert-member-functions-to-static) [[maybe_unused]] const InitializeParams &Params, @@ -72,7 +104,10 @@ void Controller:: {"full", true}, }, }, - {"completionProvider", Object{}}, + {"completionProvider", + Object{ + {"resolveProvider", true}, + }}, {"referencesProvider", true}, {"documentHighlightProvider", true}, {"hoverProvider", true}, @@ -96,14 +131,7 @@ void Controller:: PublishDiagnostic = mkOutNotifiction( "textDocument/publishDiagnostics"); - int Fail; - Eval = OwnedEvalClient::create(Fail); - if (Fail != 0) { - lspserver::elog("failed to create nix-node-eval worker: {0}", - strerror(-Fail)); - } else { - lspserver::log("launched nix-node-eval instance: {0}", Eval->proc().PID); - } + startNixpkgs(NixpkgsEval); + if (auto *Provider = nixpkgsEval().client()) + notifyNixpkgsEval(*Provider); } - -} // namespace nixd diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index 9318b3697..bba233f51 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -122,6 +122,8 @@ Controller::Controller(std::unique_ptr In, &Controller::onSemanticTokens); Registry.addMethod("textDocument/completion", this, &Controller::onCompletion); + Registry.addMethod("completionItem/resolve", this, + &Controller::onCompletionItemResolve); Registry.addMethod("textDocument/references", this, &Controller::onReferences); Registry.addMethod("textDocument/documentHighlight", this, diff --git a/nixd/lib/Eval/AttrSetClient.cpp b/nixd/lib/Eval/AttrSetClient.cpp new file mode 100644 index 000000000..34ae1e551 --- /dev/null +++ b/nixd/lib/Eval/AttrSetClient.cpp @@ -0,0 +1,33 @@ +#include "nixd-config.h" + +#include "nixd/Eval/AttrSetClient.h" + +using namespace nixd; +using namespace lspserver; + +AttrSetClient::AttrSetClient(std::unique_ptr In, + std::unique_ptr Out) + : LSPServer(std::move(In), std::move(Out)) { + EvalExpr = mkOutMethod("attrset/evalExpr"); + AttrPathInfo = mkOutMethod( + "attrset/attrpathInfo"); + AttrPathComplete = + mkOutMethod( + "attrset/attrpathComplete"); +} + +const char *AttrSetClient::getExe() { + if (const char *Env = std::getenv("NIXD_ATTRSET_EVAL")) + return Env; + return NIXD_LIBEXEC "/nixd-attrset-eval"; +} + +AttrSetClientProc::AttrSetClientProc(const std::function &Action) + : Proc(Action), Client(Proc.mkIn(), Proc.mkOut()), + Input([this]() { Client.run(); }) {} + +AttrSetClient *AttrSetClientProc::client() { + if (!kill(Proc.Proc->PID, 0)) + return &Client; + return nullptr; +} diff --git a/nixd/lib/Support/ForkPiped.cpp b/nixd/lib/Support/ForkPiped.cpp index df68f3303..7e5122aaa 100644 --- a/nixd/lib/Support/ForkPiped.cpp +++ b/nixd/lib/Support/ForkPiped.cpp @@ -2,19 +2,17 @@ #include +#include #include -namespace nixd::util { - -int forkPiped(int &In, int &Out, int &Err) { +int nixd::forkPiped(int &In, int &Out, int &Err) { static constexpr int READ = 0; static constexpr int WRITE = 1; int PipeIn[2]; int PipeOut[2]; int PipeErr[2]; - if (pipe(PipeIn) == -1 || pipe(PipeOut) == -1 || pipe(PipeErr) == -1) { - return -errno; - } + if (pipe(PipeIn) == -1 || pipe(PipeOut) == -1 || pipe(PipeErr) == -1) + throw std::system_error(errno, std::generic_category()); pid_t Child = fork(); @@ -27,15 +25,11 @@ int forkPiped(int &In, int &Out, int &Err) { return 0; } - if (Child == -1) { - // Error. - return -errno; - } + if (Child == -1) + throw std::system_error(errno, std::generic_category()); In = PipeIn[WRITE]; Out = PipeOut[READ]; Err = PipeOut[READ]; return Child; } - -} // namespace nixd::util diff --git a/nixd/lib/Support/StreamProc.cpp b/nixd/lib/Support/StreamProc.cpp new file mode 100644 index 000000000..6963b0d58 --- /dev/null +++ b/nixd/lib/Support/StreamProc.cpp @@ -0,0 +1,29 @@ +#include "nixd/Support/StreamProc.h" +#include "nixd/Support/ForkPiped.h" + +using namespace nixd; +using namespace util; +using namespace lspserver; + +std::unique_ptr StreamProc::mkIn() const { + return std::make_unique(Proc->Stdout.get(), + JSONStreamStyle::Standard); +} + +std::unique_ptr StreamProc::mkOut() const { + return std::make_unique(*Stream); +} + +StreamProc::StreamProc(const std::function &Action) { + int In; + int Out; + int Err; + + pid_t Child = forkPiped(In, Out, Err); + if (Child == 0) + exit(Action()); + + // Parent process. + Proc = std::make_unique(Child, In, Out, Err); + Stream = std::make_unique(In, false); +} diff --git a/nixd/meson.build b/nixd/meson.build index 6cb124f36..17166c13d 100644 --- a/nixd/meson.build +++ b/nixd/meson.build @@ -4,6 +4,8 @@ libnixd_deps = [ nixd_lsp_server, nixf, llvm, nixt ] libnixd_lib = library( 'nixd', + 'lib/CommandLine/Options.cpp', + 'lib/Controller/AST.cpp', 'lib/Controller/CodeAction.cpp', 'lib/Controller/Completion.cpp', 'lib/Controller/Convert.cpp', @@ -20,6 +22,7 @@ libnixd_lib = library( 'lib/Controller/SemanticTokens.cpp', 'lib/Controller/Support.cpp', 'lib/Controller/TextDocumentSync.cpp', + 'lib/Eval/AttrSetClient.cpp', 'lib/Eval/AttrSetProvider.cpp', 'lib/Eval/EvalProvider.cpp', 'lib/Protocol/AttrSet.cpp', @@ -27,6 +30,7 @@ libnixd_lib = library( 'lib/Support/AutoCloseFD.cpp', 'lib/Support/AutoRemoveShm.cpp', 'lib/Support/ForkPiped.cpp', + 'lib/Support/StreamProc.cpp', dependencies: libnixd_deps, include_directories: libnixd_include, install: true @@ -45,23 +49,39 @@ nixd = executable( dependencies: libnixd ) -regression_env = environment() -regression_env.append('PATH', meson.current_build_dir()) -regression_env.set('MESON_BUILD_ROOT', meson.current_build_dir()) +nixd_attrset_eval = executable( + 'nixd-attrset-eval', + 'tools/nixd-attrset-eval.cpp', + install: true, + dependencies: libnixd, + install_dir: get_option('libexecdir'), +) + +regression_controller_env = environment() + +regression_controller_env.append('PATH', meson.current_build_dir()) +regression_controller_env.set('MESON_BUILD_ROOT', meson.current_build_dir()) +regression_controller_env.set('NIXD_ATTRSET_EVAL', nixd_attrset_eval.path()) + if lit.found() test( 'regression/nixd', lit, - env: regression_env, + env: regression_controller_env, args: [ '-vv', meson.current_source_dir() + '/tools/nixd/test' ], - depends: [ nixd ] ) + depends: [ nixd, nixd_attrset_eval ] ) endif +regression_worker_env = environment() + +regression_worker_env.append('PATH', meson.current_build_dir()) +regression_worker_env.set('MESON_BUILD_ROOT', meson.current_build_dir()) +regression_worker_env.set('ASAN_OPTIONS', 'detect_leaks=0') nix_node_eval = executable( 'nix-node-eval', @@ -71,22 +91,15 @@ nix_node_eval = executable( install_dir: get_option('libexecdir'), ) -nixd_attrset_eval = executable( - 'nixd-attrset-eval', - 'tools/nixd-attrset-eval.cpp', - install: true, - dependencies: libnixd, - install_dir: get_option('libexecdir'), -) -regression_env.set('ASAN_OPTIONS', 'detect_leaks=0') + if lit.found() test( 'regression/nixd-attrset-eval', lit, - env: regression_env, + env: regression_worker_env, args: [ '-vv', meson.current_source_dir() + '/tools/nixd-attrset-eval/test' diff --git a/nixd/tools/nixd.cpp b/nixd/tools/nixd.cpp index 95bce6523..5a7b1fb98 100644 --- a/nixd/tools/nixd.cpp +++ b/nixd/tools/nixd.cpp @@ -3,6 +3,7 @@ #include "lspserver/Connection.h" #include "lspserver/Logger.h" +#include "nixd/CommandLine/Options.h" #include "nixd/Controller/Controller.h" #include @@ -17,7 +18,7 @@ using namespace llvm::cl; OptionCategory Misc("miscellaneous options"); OptionCategory Debug("debug-only options (for developers)"); -const OptionCategory *NixdCatogories[] = {&Misc, &Debug}; +const OptionCategory *NixdCatogories[] = {&Misc, &Debug, &nixd::NixdCategory}; opt InputStyle{ "input-style", diff --git a/nixd/tools/nixd/test/completion-nixpkgs.md b/nixd/tools/nixd/test/completion-nixpkgs.md new file mode 100644 index 000000000..71343ea39 --- /dev/null +++ b/nixd/tools/nixd/test/completion-nixpkgs.md @@ -0,0 +1,74 @@ +# RUN: nixd --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: "label": "AMB-plugins", +CHECK-NEXT: "score": 0 +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "data": "{\"Prefix\":\"\",\"Scope\":[]}", +CHECK-NEXT: "kind": 5, +CHECK-NEXT: "label": "ArchiSteamFarm", +CHECK-NEXT: "score": 0 +CHECK-NEXT: }, +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/completion-resolve.md b/nixd/tools/nixd/test/completion-resolve.md new file mode 100644 index 000000000..02f0875c5 --- /dev/null +++ b/nixd/tools/nixd/test/completion-resolve.md @@ -0,0 +1,50 @@ +# 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" + } +} +``` + + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "completionItem/resolve", + "params": { + "label": "hello", + "data": "{ \"Scope\": [ ], \"Prefix\": \"hel\" }" + } +} +``` + +``` + CHECK: "id": 1, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "data": "{ \"Scope\": [ ], \"Prefix\": \"hel\" }", +CHECK-NEXT: "detail": "{{.*}}", +CHECK-NEXT: "documentation": { +CHECK-NEXT: "kind": "markdown", +CHECK-NEXT: "value": "Very Nice\n\n" +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/completion.md b/nixd/tools/nixd/test/completion.md index 6325bb127..b30528239 100644 --- a/nixd/tools/nixd/test/completion.md +++ b/nixd/tools/nixd/test/completion.md @@ -63,6 +63,7 @@ CHECK-NEXT: "result": { CHECK-NEXT: "isIncomplete": false, CHECK-NEXT: "items": [ CHECK-NEXT: { +CHECK-NEXT: "data": "", CHECK-NEXT: "kind": 6, CHECK-NEXT: "label": "xxx", CHECK-NEXT: "score": 0 diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index 3b656d0ae..c146313ea 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -32,7 +32,9 @@ CHECK-NEXT: "quickfix" CHECK-NEXT: ], CHECK-NEXT: "resolveProvider": false CHECK-NEXT: }, -CHECK-NEXT: "completionProvider": {}, +CHECK-NEXT: "completionProvider": { +CHECK-NEXT: "resolveProvider": true +CHECK-NEXT: }, CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "documentHighlightProvider": true, CHECK-NEXT: "documentSymbolProvider": true, diff --git a/nixd/tools/nixd/test/lit.cfg.py b/nixd/tools/nixd/test/lit.cfg.py index 4a577e27b..b8350bb8d 100644 --- a/nixd/tools/nixd/test/lit.cfg.py +++ b/nixd/tools/nixd/test/lit.cfg.py @@ -12,6 +12,9 @@ build_dir = os.getenv('MESON_BUILD_ROOT', default = '/dev/null') +config.environment['NIXD_ATTRSET_EVAL'] = os.getenv('NIXD_ATTRSET_EVAL') +config.environment['NIX_PATH'] = os.getenv('NIX_PATH') + # test_source_root: The root path where tests are located. config.test_source_root = test_root