From 2d716298d23f364cceec87fe7c15fe0319ebd9a0 Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Mon, 15 Apr 2024 15:15:27 +0800 Subject: [PATCH] nixd: support `textDocument/inlayHint` (#414) --- nixd/include/nixd/Controller/Controller.h | 4 + nixd/lib/Controller/Convert.cpp | 4 + nixd/lib/Controller/Convert.h | 2 + nixd/lib/Controller/InlayHints.cpp | 130 ++++++++++++++++++++++ nixd/lib/Controller/LifeTime.cpp | 1 + nixd/lib/Controller/Support.cpp | 1 + nixd/meson.build | 1 + nixd/tools/nixd/test/initialize.md | 1 + nixd/tools/nixd/test/inlay-hint.md | 80 +++++++++++++ 9 files changed, 224 insertions(+) create mode 100644 nixd/lib/Controller/InlayHints.cpp create mode 100644 nixd/tools/nixd/test/inlay-hint.md diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 7ebdf9b97..a11adbc94 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -100,6 +100,10 @@ class Controller : public lspserver::LSPServer { void onSemanticTokens(const lspserver::SemanticTokensParams &Params, lspserver::Callback Reply); + void + onInlayHint(const lspserver::InlayHintsParams &Params, + lspserver::Callback> Reply); + void onCompletion(const lspserver::CompletionParams &Params, lspserver::Callback Reply); diff --git a/nixd/lib/Controller/Convert.cpp b/nixd/lib/Controller/Convert.cpp index 0e4711af7..d350219f1 100644 --- a/nixd/lib/Controller/Convert.cpp +++ b/nixd/lib/Controller/Convert.cpp @@ -25,6 +25,10 @@ nixf::Position toNixfPosition(const lspserver::Position &P) { return {P.line, P.character}; } +nixf::PositionRange toNixfRange(const lspserver::Range &P) { + return {toNixfPosition(P.start), toNixfPosition(P.end)}; +} + lspserver::Range toLSPRange(const nixf::LexerCursorRange &R) { return lspserver::Range{toLSPPosition(R.lCur()), toLSPPosition(R.rCur())}; } diff --git a/nixd/lib/Controller/Convert.h b/nixd/lib/Controller/Convert.h index 9ecc0f8cb..fcb71feec 100644 --- a/nixd/lib/Controller/Convert.h +++ b/nixd/lib/Controller/Convert.h @@ -14,6 +14,8 @@ lspserver::Position toLSPPosition(const nixf::LexerCursor &P); nixf::Position toNixfPosition(const lspserver::Position &P); +nixf::PositionRange toNixfRange(const lspserver::Range &P); + lspserver::Range toLSPRange(const nixf::LexerCursorRange &R); int getLSPSeverity(nixf::Diagnostic::DiagnosticKind Kind); diff --git a/nixd/lib/Controller/InlayHints.cpp b/nixd/lib/Controller/InlayHints.cpp new file mode 100644 index 000000000..cf8da2cfb --- /dev/null +++ b/nixd/lib/Controller/InlayHints.cpp @@ -0,0 +1,130 @@ +/// \file +/// \brief Implementation of [Inlay Hints]. +/// [Inlay Hints]: +/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_inlayHint +/// +/// In nixd, "Inlay Hints" are placed after each "package" node, showing it's +/// version. +/// +/// For example +/// +/// nixd[: 1.2.3] +/// nix[: 2.19.3] +/// +/// +#include "AST.h" +#include "Convert.h" + +#include "nixd/CommandLine/Options.h" +#include "nixd/Controller/Controller.h" + +#include + +#include + +#include + +using namespace nixd; +using namespace nixf; +using namespace lspserver; +using namespace llvm::cl; + +namespace { + +opt EnableInlayHints{"inlay-hints", desc("Enable/Disable inlay-hints"), + init(true), cat(NixdCategory)}; + +/// Ask nixpkgs provider to compute package information, to get inlay-hints. +class NixpkgsInlayHintsProvider { + AttrSetClient &NixpkgsProvider; + const VariableLookupAnalysis &VLA; + const ParentMapAnalysis &PMA; + + /// Only positions contained in this range should be computed && added; + std::optional Range; + + std::vector &Hints; + + bool rangeOK(const nixf::PositionRange &R) { + if (!Range) + return true; // Always OK if there is no limitation. + return Range->contains(R); + } + +public: + NixpkgsInlayHintsProvider(AttrSetClient &NixpkgsProvider, + const VariableLookupAnalysis &VLA, + const ParentMapAnalysis &PMA, + std::optional Range, + std::vector &Hints) + : NixpkgsProvider(NixpkgsProvider), VLA(VLA), PMA(PMA), Hints(Hints) { + if (Range) + this->Range = toNixfRange(*Range); + } + + void dfs(const Node *N) { + if (!N) + return; + if (N->kind() == Node::NK_ExprVar) { + if (havePackageScope(*N, VLA, PMA)) { + if (!rangeOK(N->positionRange())) + return; + // Ask nixpkgs eval to provide it's information. + // This is relatively slow. Maybe better query a set of packages in the + // future? + std::binary_semaphore Ready(0); + const std::string &Name = static_cast(*N).id().name(); + AttrPathInfoResponse R; + auto OnReply = [&Ready, &R](llvm::Expected Resp) { + if (!Resp) { + Ready.release(); + return; + } + R = *Resp; + Ready.release(); + }; + NixpkgsProvider.attrpathInfo({Name}, std::move(OnReply)); + Ready.acquire(); + + if (R.Version) { + // Construct inlay hints. + InlayHint H{ + .position = toLSPPosition(N->rCur()), + .label = ": " + *R.Version, + .kind = InlayHintKind::Designator, + .range = toLSPRange(N->range()), + }; + Hints.emplace_back(std::move(H)); + } + } + } + // FIXME: process other node kinds. e.g. ExprSelect. + for (const Node *Ch : N->children()) + dfs(Ch); + } +}; + +} // namespace + +void Controller::onInlayHint(const InlayHintsParams &Params, + Callback> Reply) { + // If not enabled, exit early. + if (!EnableInlayHints) + return Reply(std::vector{}); + + auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri, + Range = Params.range, this]() mutable { + std::string File(URI.file()); + if (std::shared_ptr TU = getTU(File, Reply)) [[likely]] { + if (std::shared_ptr AST = getAST(*TU, Reply)) [[likely]] { + // Perform inlay hints computation on the range. + std::vector Response; + NixpkgsInlayHintsProvider NP(*nixpkgsClient(), *TU->variableLookup(), + *TU->parentMap(), Range, Response); + NP.dfs(AST.get()); + Reply(std::move(Response)); + } + } + }; + boost::asio::post(Pool, std::move(Action)); +} diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index 8874b67c9..5dd183e09 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -104,6 +104,7 @@ void Controller:: {"full", true}, }, }, + {"inlayHintProvider", true}, {"completionProvider", Object{ {"resolveProvider", true}, diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index bba233f51..69e4207e7 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -120,6 +120,7 @@ Controller::Controller(std::unique_ptr In, &Controller::onDocumentSymbol); Registry.addMethod("textDocument/semanticTokens/full", this, &Controller::onSemanticTokens); + Registry.addMethod("textDocument/inlayHint", this, &Controller::onInlayHint); Registry.addMethod("textDocument/completion", this, &Controller::onCompletion); Registry.addMethod("completionItem/resolve", this, diff --git a/nixd/meson.build b/nixd/meson.build index 17166c13d..11edf5c54 100644 --- a/nixd/meson.build +++ b/nixd/meson.build @@ -16,6 +16,7 @@ libnixd_lib = library( 'lib/Controller/EvalClient.cpp', 'lib/Controller/FindReferences.cpp', 'lib/Controller/Hover.cpp', + 'lib/Controller/InlayHints.cpp', 'lib/Controller/LifeTime.cpp', 'lib/Controller/NixTU.cpp', 'lib/Controller/Rename.cpp', diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index c146313ea..3087c8503 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -39,6 +39,7 @@ CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "documentHighlightProvider": true, CHECK-NEXT: "documentSymbolProvider": true, CHECK-NEXT: "hoverProvider": true, +CHECK-NEXT: "inlayHintProvider": true, CHECK-NEXT: "referencesProvider": true, CHECK-NEXT: "renameProvider": { CHECK-NEXT: "prepareProvider": true diff --git a/nixd/tools/nixd/test/inlay-hint.md b/nixd/tools/nixd/test/inlay-hint.md new file mode 100644 index 000000000..d8b401c24 --- /dev/null +++ b/nixd/tools/nixd/test/inlay-hint.md @@ -0,0 +1,80 @@ +# RUN: nixd --lit-test \ +# RUN: --nixpkgs-expr="{ hello.version = \"0.3.12\"; }" \ +# 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", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"with pkgs; [ hello ]" + } + } +} +``` + + + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "textDocument/inlayHint", + "params": { + "textDocument":{ + "uri":"file:///basic.nix" + }, + "range": { + "start":{ + "line": 0, + "character":0 + }, + "end":{ + "line":0, + "character":20 + } + } + } +} +``` + +``` + CHECK: "id": 1, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "label": ": 0.3.12", +CHECK-NEXT: "paddingLeft": false, +CHECK-NEXT: "paddingRight": false, +CHECK-NEXT: "position": { +CHECK-NEXT: "character": 18, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: } +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +```