From 9fbb2f75d07f2a0f4ae21f62790f8fc6b69d720e Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Tue, 19 Nov 2024 16:43:02 +0800 Subject: [PATCH 1/4] nixd/tools/nixd-attrset-eval: provide `getDoc` information by C++ nix --- libnixt/include/nixt/Value.h | 6 ++ nixd/include/nixd/Protocol/AttrSet.h | 14 ++++ nixd/lib/Eval/AttrSetProvider.cpp | 67 ++++++++++++------- nixd/lib/Protocol/AttrSet.cpp | 18 +++++ .../nixd-attrset-eval/test/attrs-info-doc.md | 50 ++++++++++++++ 5 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md diff --git a/libnixt/include/nixt/Value.h b/libnixt/include/nixt/Value.h index 49086926d..fa957e8ca 100644 --- a/libnixt/include/nixt/Value.h +++ b/libnixt/include/nixt/Value.h @@ -70,4 +70,10 @@ selectStringViews(nix::EvalState &State, nix::Value &V, return selectSymbols(State, V, toSymbols(State.symbols, AttrPath)); } +/// TODO: use https://github.com/NixOS/nix/pull/11914 on nix version bump +/// \brief Get nix's `builtins` constant +inline nix::Value &getBuiltins(const nix::EvalState &State) { + return *State.baseEnv.values[0]; +} + } // namespace nixt diff --git a/nixd/include/nixd/Protocol/AttrSet.h b/nixd/include/nixd/Protocol/AttrSet.h index 8d69f5a0e..f7b675d43 100644 --- a/nixd/include/nixd/Protocol/AttrSet.h +++ b/nixd/include/nixd/Protocol/AttrSet.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -31,6 +32,7 @@ constexpr inline std::string_view AttrPathInfo = "attrset/attrpathInfo"; constexpr inline std::string_view AttrPathComplete = "attrset/attrpathComplete"; constexpr inline std::string_view OptionInfo = "attrset/optionInfo"; constexpr inline std::string_view OptionComplete = "attrset/optionComplete"; + constexpr inline std::string_view Exit = "exit"; } // namespace rpcMethod @@ -78,12 +80,24 @@ llvm::json::Value toJSON(const ValueMeta &Params); bool fromJSON(const llvm::json::Value &Params, ValueMeta &R, llvm::json::Path P); +/// \brief Using nix's ":doc" method to retrive value's additional information. +struct ValueDescription { + std::string Doc; + std::int64_t Arity; +}; + +llvm::json::Value toJSON(const ValueDescription &Params); +bool fromJSON(const llvm::json::Value &Params, ValueDescription &R, + llvm::json::Path P); + struct AttrPathInfoResponse { /// \brief General value description ValueMeta Meta; /// \brief Package description of the attribute path, if available. PackageDescription PackageDesc; + + std::optional ValueDesc; }; llvm::json::Value toJSON(const AttrPathInfoResponse &Params); diff --git a/nixd/lib/Eval/AttrSetProvider.cpp b/nixd/lib/Eval/AttrSetProvider.cpp index 139c5a6f7..a5072a5ef 100644 --- a/nixd/lib/Eval/AttrSetProvider.cpp +++ b/nixd/lib/Eval/AttrSetProvider.cpp @@ -160,6 +160,43 @@ void fillOptionDescription(nix::EvalState &State, nix::Value &V, } } +std::vector completeNames(nix::Value &Scope, + const nix::EvalState &State, + std::string_view Prefix) { + int Num = 0; + std::vector Names; + + // FIXME: we may want to use "Trie" to speedup the string searching. + // However as my (roughtly) profiling the critical in this loop is + // evaluating package details. + // "Trie"s may not beneficial because it cannot speedup eval. + for (const auto *AttrPtr : Scope.attrs()->lexicographicOrder(State.symbols)) { + const nix::Attr &Attr = *AttrPtr; + const std::string_view Name = State.symbols[Attr.name]; + if (Name.starts_with(Prefix)) { + ++Num; + Names.emplace_back(Name); + // We set this a very limited number as to speedup + if (Num > MaxItems) + break; + } + } + return Names; +} + +std::optional describeValue(nix::EvalState &State, + nix::Value &V) { + const auto Doc = State.getDoc(V); + if (!Doc) { + return std::nullopt; + } else { + return ValueDescription{ + .Doc = Doc->doc, + .Arity = static_cast(Doc->arity), + }; + } +} + } // namespace AttrSetProvider::AttrSetProvider(std::unique_ptr In, @@ -206,9 +243,11 @@ void AttrSetProvider::onAttrPathInfo( nix::Value &V = nixt::selectStrings(state(), Nixpkgs, AttrPath); state().forceValue(V, nix::noPos); + return RespT{ .Meta = metadataOf(state(), V), .PackageDesc = describePackage(state(), V), + .ValueDesc = describeValue(state(), V), }; } catch (const nix::BaseError &Err) { return error(Err.info().msg.str()); @@ -231,33 +270,11 @@ void AttrSetProvider::onAttrPathComplete( return; } - std::vector Names; - int Num = 0; - - // FIXME: we may want to use "Trie" to speedup the string searching. - // However as my (roughtly) profiling the critical in this loop is - // evaluating package details. - // "Trie"s may not beneficial becausae it cannot speedup eval. - for (const auto *AttrPtr : - Scope.attrs()->lexicographicOrder(state().symbols)) { - const nix::Attr &Attr = *AttrPtr; - const std::string_view Name = state().symbols[Attr.name]; - if (Name.starts_with(Params.Prefix)) { - ++Num; - Names.emplace_back(Name); - // We set this a very limited number as to speedup - if (Num > MaxItems) - break; - } - } - Reply(std::move(Names)); - return; + return Reply(completeNames(Scope, state(), Params.Prefix)); } catch (const nix::BaseError &Err) { - Reply(error(Err.info().msg.str())); - return; + return Reply(error(Err.info().msg.str())); } catch (const std::exception &Err) { - Reply(error(Err.what())); - return; + return Reply(error(Err.what())); } } diff --git a/nixd/lib/Protocol/AttrSet.cpp b/nixd/lib/Protocol/AttrSet.cpp index b7220ddc4..7eb139cbe 100644 --- a/nixd/lib/Protocol/AttrSet.cpp +++ b/nixd/lib/Protocol/AttrSet.cpp @@ -97,6 +97,7 @@ Value nixd::toJSON(const AttrPathInfoResponse &Params) { return Object{ {"Meta", Params.Meta}, {"PackageDesc", Params.PackageDesc}, + {"ValueDesc", Params.ValueDesc}, }; } @@ -106,6 +107,7 @@ bool nixd::fromJSON(const llvm::json::Value &Params, AttrPathInfoResponse &R, return O // && O.map("Meta", R.Meta) // && O.mapOptional("PackageDesc", R.PackageDesc) // + && O.mapOptional("ValueDesc", R.ValueDesc) // ; } @@ -120,3 +122,19 @@ bool nixd::fromJSON(const llvm::json::Value &Params, AttrPathCompleteParams &R, && O.map("Prefix", R.Prefix) // ; } + +llvm::json::Value nixd::toJSON(const ValueDescription &Params) { + return Object{ + {"arity", Params.Arity}, + {"doc", Params.Doc}, + }; +} +bool nixd::fromJSON(const llvm::json::Value &Params, ValueDescription &R, + llvm::json::Path P) { + + ObjectMapper O(Params, P); + return O // + && O.map("arity", R.Arity) // + && O.map("doc", R.Doc) // + ; +} diff --git a/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md b/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md new file mode 100644 index 000000000..1855cce55 --- /dev/null +++ b/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md @@ -0,0 +1,50 @@ +# RUN: nixd-attrset-eval --lit-test < %s | FileCheck %s + + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"attrset/evalExpr", + "params": "{ hello = /** some markdown docs */x: y: x; }" +} +``` + + +```json +{ + "jsonrpc":"2.0", + "id":1, + "method":"attrset/attrpathInfo", + "params": [ "hello" ] +} +``` + +``` + CHECK: "id": 1, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "Meta": { +CHECK-NEXT: "Location": null, +CHECK-NEXT: "Type": 9 +CHECK-NEXT: }, +CHECK-NEXT: "PackageDesc": { +CHECK-NEXT: "Description": null, +CHECK-NEXT: "Homepage": null, +CHECK-NEXT: "LongDescription": null, +CHECK-NEXT: "Name": null, +CHECK-NEXT: "PName": null, +CHECK-NEXT: "Position": null, +CHECK-NEXT: "Version": null +CHECK-NEXT: }, +CHECK-NEXT: "ValueDesc": { +CHECK-NEXT: "arity": 0, +CHECK-NEXT: "doc": "Function `hello`\\\n … defined at «string»:1:36\n\nsome markdown docs \n" +CHECK-NEXT: } +CHECK-NEXT: } +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` + From 8b7fc6f09962ceec6adf1c4bb924d6877194efe2 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Sun, 24 Nov 2024 01:25:59 +0800 Subject: [PATCH 2/4] draft: hover by "select" expressions. --- nixd/lib/Controller/Hover.cpp | 188 +++++++++++++++++++++++----------- 1 file changed, 128 insertions(+), 60 deletions(-) diff --git a/nixd/lib/Controller/Hover.cpp b/nixd/lib/Controller/Hover.cpp index 87015d5b0..31eeb004f 100644 --- a/nixd/lib/Controller/Hover.cpp +++ b/nixd/lib/Controller/Hover.cpp @@ -91,8 +91,7 @@ class NixpkgsHoverProvider { NixpkgsHoverProvider(AttrSetClient &NixpkgsClient) : NixpkgsClient(NixpkgsClient) {} - std::optional resolvePackage(std::vector Scope, - std::string Name) { + std::optional resolvePackage(const Selector &Sel) { std::binary_semaphore Ready(0); std::optional Desc; auto OnReply = [&Ready, &Desc](llvm::Expected Resp) { @@ -102,8 +101,7 @@ class NixpkgsHoverProvider { elog("nixpkgs provider: {0}", Resp.takeError()); Ready.release(); }; - Scope.emplace_back(std::move(Name)); - NixpkgsClient.attrpathInfo(Scope, std::move(OnReply)); + NixpkgsClient.attrpathInfo(Sel, std::move(OnReply)); Ready.acquire(); if (!Desc) @@ -113,6 +111,103 @@ class NixpkgsHoverProvider { } }; +class HoverProvider { + const NixTU &TU; + const VariableLookupAnalysis &VLA; + const ParentMapAnalysis &PM; + + [[nodiscard]] std::optional + mkHover(std::optional Doc, nixf::LexerCursorRange Range) const { + if (!Doc) + return std::nullopt; + return Hover{ + .contents = + MarkupContent{ + .kind = MarkupKind::Markdown, + .value = std::move(*Doc), + }, + .range = toLSPRange(TU.src(), Range), + }; + } + +public: + HoverProvider(const NixTU &TU, const VariableLookupAnalysis &VLA, + const ParentMapAnalysis &PM) + : TU(TU), VLA(VLA), PM(PM) {} + + std::optional hoverVar(const nixf::Node &N, + AttrSetClient &Client) const { + if (havePackageScope(N, VLA, PM)) { + // Ask nixpkgs client what's current package documentation. + auto NHP = NixpkgsHoverProvider(Client); + auto [Scope, Name] = getScopeAndPrefix(N, PM); + Scope.emplace_back(Name); + return mkHover(NHP.resolvePackage(Scope), N.range()); + } + + return std::nullopt; + } + + std::optional hoverSelect(const nixf::ExprSelect &Select, + AttrSetClient &Client) const { + // The base expr for selecting. + const nixf::Expr &BaseExpr = Select.expr(); + + if (BaseExpr.kind() != Node::NK_ExprVar) { + return std::nullopt; + } + + const auto &Var = static_cast(BaseExpr); + try { + Selector Sel = + idioms::mkSelector(Select, idioms::mkVarSelector(Var, VLA, PM)); + auto NHP = NixpkgsHoverProvider(Client); + return mkHover(NHP.resolvePackage(Sel), Select.range()); + } catch (std::exception &E) { + log("hover/select skipped, reason: {0}", E.what()); + } + return std::nullopt; + } + + std::optional + hoverAttrPath(const nixf::Node &N, std::mutex &OptionsLock, + const Controller::OptionMapTy &Options) const { + auto Scope = std::vector(); + const auto R = findAttrPath(N, PM, Scope); + if (R == FindAttrPathResult::OK) { + std::lock_guard _(OptionsLock); + for (const auto &[_, Client] : Options) { + if (AttrSetClient *C = Client->client()) { + OptionsHoverProvider OHP(*C); + std::optional Desc = OHP.resolveHover(Scope); + std::string Docs; + if (Desc) { + if (Desc->Type) { + std::string TypeName = Desc->Type->Name.value_or(""); + std::string TypeDesc = Desc->Type->Description.value_or(""); + Docs += llvm::formatv("{0} ({1})", TypeName, TypeDesc); + } else { + Docs += "? (missing type)"; + } + if (Desc->Description) { + Docs += "\n\n" + Desc->Description.value_or(""); + } + return Hover{ + .contents = + MarkupContent{ + .kind = MarkupKind::Markdown, + .value = std::move(Docs), + }, + .range = toLSPRange(TU.src(), N.range()), + }; + } + } + } + } + return std::nullopt; + } +}; + } // namespace void Controller::onHover(const TextDocumentPositionParams &Params, @@ -130,65 +225,38 @@ void Controller::onHover(const TextDocumentPositionParams &Params, const auto Name = std::string(N.name()); const auto &VLA = *TU->variableLookup(); const auto &PM = *TU->parentMap(); - if (havePackageScope(N, VLA, PM) && nixpkgsClient()) { - // Ask nixpkgs client what's current package documentation. - auto NHP = NixpkgsHoverProvider(*nixpkgsClient()); - const auto [Scope, Name] = getScopeAndPrefix(N, PM); - if (std::optional Doc = NHP.resolvePackage(Scope, Name)) { - return Hover{ - .contents = - MarkupContent{ - .kind = MarkupKind::Markdown, - .value = std::move(*Doc), - }, - .range = toLSPRange(TU->src(), N.range()), - }; - } - } + const auto &UpExpr = *CheckDefault(PM.upExpr(N)); - auto Scope = std::vector(); - const auto R = findAttrPath(N, PM, Scope); - if (R == FindAttrPathResult::OK) { - std::lock_guard _(OptionsLock); - for (const auto &[_, Client] : Options) { - if (AttrSetClient *C = Client->client()) { - OptionsHoverProvider OHP(*C); - std::optional Desc = OHP.resolveHover(Scope); - std::string Docs; - if (Desc) { - if (Desc->Type) { - std::string TypeName = Desc->Type->Name.value_or(""); - std::string TypeDesc = Desc->Type->Description.value_or(""); - Docs += llvm::formatv("{0} ({1})", TypeName, TypeDesc); - } else { - Docs += "? (missing type)"; - } - if (Desc->Description) { - Docs += "\n\n" + Desc->Description.value_or(""); - } - return Hover{ - .contents = - MarkupContent{ - .kind = MarkupKind::Markdown, - .value = std::move(Docs), - }, - .range = toLSPRange(TU->src(), N.range()), - }; - } - } + const auto Provider = HoverProvider(*TU, VLA, PM); + + const auto HoverByCase = [&]() -> std::optional { + switch (UpExpr.kind()) { + case Node::NK_ExprVar: + return Provider.hoverVar(N, *nixpkgsClient()); + case Node::NK_ExprSelect: + return Provider.hoverSelect( + static_cast(UpExpr), *nixpkgsClient()); + case Node::NK_ExprAttrs: + return Provider.hoverAttrPath(N, OptionsLock, Options); + default: + return std::nullopt; } - } + }(); - // Reply it's kind by static analysis - // FIXME: support more. - return Hover{ - .contents = - MarkupContent{ - .kind = MarkupKind::Markdown, - .value = "`" + Name + "`", - }, - .range = toLSPRange(TU->src(), N.range()), - }; + if (HoverByCase) { + return HoverByCase.value(); + } else { + // Reply it's kind by static analysis + // FIXME: support more. + return Hover{ + .contents = + MarkupContent{ + .kind = MarkupKind::Markdown, + .value = "`" + Name + "`", + }, + .range = toLSPRange(TU->src(), N.range()), + }; + } }()); }; boost::asio::post(Pool, std::move(Action)); From 00a38f1ae61a9aceba6f29364a86d93b7819d233 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Sun, 24 Nov 2024 01:41:17 +0800 Subject: [PATCH 3/4] draft: reply value-doc --- nixd/lib/Controller/Hover.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nixd/lib/Controller/Hover.cpp b/nixd/lib/Controller/Hover.cpp index 31eeb004f..835fa743b 100644 --- a/nixd/lib/Controller/Hover.cpp +++ b/nixd/lib/Controller/Hover.cpp @@ -56,7 +56,7 @@ class NixpkgsHoverProvider { /// /// FIXME: there are many markdown generation in language server. /// Maybe we can add structured generating first? - static std::string mkMarkdown(const PackageDescription &Package) { + static std::string mkPackageMarkdown(const PackageDescription &Package) { std::ostringstream OS; // Make each field a new section @@ -87,6 +87,10 @@ class NixpkgsHoverProvider { return OS.str(); } + static std::string mkValueMarkdown(const ValueDescription &ValueDesc) { + return ValueDesc.Doc; + } + public: NixpkgsHoverProvider(AttrSetClient &NixpkgsClient) : NixpkgsClient(NixpkgsClient) {} @@ -107,7 +111,11 @@ class NixpkgsHoverProvider { if (!Desc) return std::nullopt; - return mkMarkdown(Desc->PackageDesc); + if (const auto ValueDesc = Desc->ValueDesc) { + return ValueDesc->Doc; + } + + return mkPackageMarkdown(Desc->PackageDesc); } }; From 6bb44d0c0c893a5214f726ab1293117f246343d0 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Sun, 27 Apr 2025 00:19:20 +0800 Subject: [PATCH 4/4] fixup: don't use `getDoc()`, provide our own describe() method instead --- libnixt/include/nixt/Value.h | 6 ---- nixd/include/nixd/Protocol/AttrSet.h | 1 + nixd/lib/Eval/AttrSetProvider.cpp | 29 +++++++++++++++---- nixd/lib/Protocol/AttrSet.cpp | 3 +- .../nixd-attrset-eval/test/attrs-info-doc.md | 3 +- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/libnixt/include/nixt/Value.h b/libnixt/include/nixt/Value.h index fa957e8ca..49086926d 100644 --- a/libnixt/include/nixt/Value.h +++ b/libnixt/include/nixt/Value.h @@ -70,10 +70,4 @@ selectStringViews(nix::EvalState &State, nix::Value &V, return selectSymbols(State, V, toSymbols(State.symbols, AttrPath)); } -/// TODO: use https://github.com/NixOS/nix/pull/11914 on nix version bump -/// \brief Get nix's `builtins` constant -inline nix::Value &getBuiltins(const nix::EvalState &State) { - return *State.baseEnv.values[0]; -} - } // namespace nixt diff --git a/nixd/include/nixd/Protocol/AttrSet.h b/nixd/include/nixd/Protocol/AttrSet.h index f7b675d43..a69831d94 100644 --- a/nixd/include/nixd/Protocol/AttrSet.h +++ b/nixd/include/nixd/Protocol/AttrSet.h @@ -84,6 +84,7 @@ bool fromJSON(const llvm::json::Value &Params, ValueMeta &R, struct ValueDescription { std::string Doc; std::int64_t Arity; + std::vector Args; }; llvm::json::Value toJSON(const ValueDescription &Params); diff --git a/nixd/lib/Eval/AttrSetProvider.cpp b/nixd/lib/Eval/AttrSetProvider.cpp index a5072a5ef..ceb815c97 100644 --- a/nixd/lib/Eval/AttrSetProvider.cpp +++ b/nixd/lib/Eval/AttrSetProvider.cpp @@ -186,15 +186,32 @@ std::vector completeNames(nix::Value &Scope, std::optional describeValue(nix::EvalState &State, nix::Value &V) { - const auto Doc = State.getDoc(V); - if (!Doc) { - return std::nullopt; - } else { + if (V.isPrimOp()) { + const auto *PrimOp = V.primOp(); + assert(PrimOp); + return ValueDescription{ + .Doc = PrimOp->doc ? std::string(PrimOp->doc) : "", + .Arity = static_cast(PrimOp->arity), + .Args = PrimOp->args, + }; + } else if (V.isLambda()) { + auto *Lambda = V.payload.lambda.fun; + assert(Lambda); return ValueDescription{ - .Doc = Doc->doc, - .Arity = static_cast(Doc->arity), + .Doc = + [&]() { + const auto DocComment = Lambda->docComment; + if (DocComment) { + return DocComment.getInnerText(State.positions); + } + return std::string(); + }(), + .Arity = 0, + .Args = {}, }; } + + return std::nullopt; } } // namespace diff --git a/nixd/lib/Protocol/AttrSet.cpp b/nixd/lib/Protocol/AttrSet.cpp index 7eb139cbe..cf3dc2e8a 100644 --- a/nixd/lib/Protocol/AttrSet.cpp +++ b/nixd/lib/Protocol/AttrSet.cpp @@ -127,6 +127,7 @@ llvm::json::Value nixd::toJSON(const ValueDescription &Params) { return Object{ {"arity", Params.Arity}, {"doc", Params.Doc}, + {"args", Params.Args}, }; } bool nixd::fromJSON(const llvm::json::Value &Params, ValueDescription &R, @@ -136,5 +137,5 @@ bool nixd::fromJSON(const llvm::json::Value &Params, ValueDescription &R, return O // && O.map("arity", R.Arity) // && O.map("doc", R.Doc) // - ; + && O.map("args", R.Args); } diff --git a/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md b/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md index 1855cce55..1e2fbdc29 100644 --- a/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md +++ b/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md @@ -38,8 +38,9 @@ CHECK-NEXT: "Position": null, CHECK-NEXT: "Version": null CHECK-NEXT: }, CHECK-NEXT: "ValueDesc": { +CHECK-NEXT: "args": [], CHECK-NEXT: "arity": 0, -CHECK-NEXT: "doc": "Function `hello`\\\n … defined at «string»:1:36\n\nsome markdown docs \n" +CHECK-NEXT: "doc": "some markdown docs \n" CHECK-NEXT: } CHECK-NEXT: } ```