diff --git a/nixd/include/nixd/Protocol/AttrSet.h b/nixd/include/nixd/Protocol/AttrSet.h index 8d69f5a0e..a69831d94 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,25 @@ 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; + std::vector Args; +}; + +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/Controller/Hover.cpp b/nixd/lib/Controller/Hover.cpp index 87015d5b0..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,12 +87,15 @@ class NixpkgsHoverProvider { return OS.str(); } + static std::string mkValueMarkdown(const ValueDescription &ValueDesc) { + return ValueDesc.Doc; + } + public: 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,14 +105,114 @@ 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) return std::nullopt; - return mkMarkdown(Desc->PackageDesc); + if (const auto ValueDesc = Desc->ValueDesc) { + return ValueDesc->Doc; + } + + return mkPackageMarkdown(Desc->PackageDesc); + } +}; + +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; } }; @@ -130,65 +233,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)); diff --git a/nixd/lib/Eval/AttrSetProvider.cpp b/nixd/lib/Eval/AttrSetProvider.cpp index 139c5a6f7..ceb815c97 100644 --- a/nixd/lib/Eval/AttrSetProvider.cpp +++ b/nixd/lib/Eval/AttrSetProvider.cpp @@ -160,6 +160,60 @@ 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) { + 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 = + [&]() { + const auto DocComment = Lambda->docComment; + if (DocComment) { + return DocComment.getInnerText(State.positions); + } + return std::string(); + }(), + .Arity = 0, + .Args = {}, + }; + } + + return std::nullopt; +} + } // namespace AttrSetProvider::AttrSetProvider(std::unique_ptr In, @@ -206,9 +260,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 +287,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..cf3dc2e8a 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,20 @@ 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}, + {"args", Params.Args}, + }; +} +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) // + && 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 new file mode 100644 index 000000000..1e2fbdc29 --- /dev/null +++ b/nixd/tools/nixd-attrset-eval/test/attrs-info-doc.md @@ -0,0 +1,51 @@ +# 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: "args": [], +CHECK-NEXT: "arity": 0, +CHECK-NEXT: "doc": "some markdown docs \n" +CHECK-NEXT: } +CHECK-NEXT: } +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` +