diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 4690e613a..798fabe3f 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -74,6 +74,13 @@ class Controller : public lspserver::LSPServer { void createWorkDoneProgress(const lspserver::WorkDoneProgressCreateParams &Params); + /// Request the client to show a document (file or URL). + /// @since LSP 3.16.0 + llvm::unique_function)> + ShowDocument; + llvm::unique_function &)> BeginWorkDoneProgress; @@ -165,6 +172,9 @@ class Controller : public lspserver::LSPServer { onCodeAction(const lspserver::CodeActionParams &Params, lspserver::Callback> Reply); + void onCodeActionResolve(const lspserver::CodeAction &Params, + lspserver::Callback Reply); + void onHover(const lspserver::TextDocumentPositionParams &Params, lspserver::Callback> Reply); diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 249098862..0a66bd079 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -9,6 +9,8 @@ #include "nixd/Controller/Controller.h" #include +#include +#include #include #include @@ -634,6 +636,85 @@ void addAttrNameActions(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, } } +/// \brief Construct noogle.dev URL for a lib.* function path. +/// Examples: +/// - {"lib", "optionalString"} -> "https://noogle.dev/f/lib/optionalString" +/// - {"lib", "strings", "optionalString"} -> +/// "https://noogle.dev/f/lib/strings/optionalString" +std::string buildNoogleUrl(const std::vector &Path) { + std::string Url = "https://noogle.dev/f"; + for (const auto &Segment : Path) { + Url += "/"; + Url += Segment; + } + return Url; +} + +/// \brief Add a code action to open noogle.dev documentation for lib.* +/// functions. +/// +/// This action is offered when the cursor is on an ExprSelect with: +/// - Base expression is ExprVar with name "lib" +/// - Path contains at least one static attribute name +/// +/// Examples that trigger: +/// - lib.optionalString +/// - lib.strings.optionalString +/// - lib.attrsets.mapAttrs +/// +/// Examples that do NOT trigger: +/// - lib (just the variable, no selection) +/// - lib.${x} (dynamic attribute) +/// - pkgs.hello (not lib.*) +void addNoogleDocAction(const nixf::Node &N, const nixf::ParentMapAnalysis &PM, + std::vector &Actions) { + // Find if we're inside an ExprSelect + const nixf::Node *SelectNode = PM.upTo(N, nixf::Node::NK_ExprSelect); + if (!SelectNode) + return; + + const auto &Sel = static_cast(*SelectNode); + + // Check base expression is ExprVar with name "lib" + if (Sel.expr().kind() != nixf::Node::NK_ExprVar) + return; + + const auto &Var = static_cast(Sel.expr()); + if (Var.id().name() != "lib") + return; + + // Check path exists and has at least one attribute + if (!Sel.path()) + return; + + const nixf::AttrPath &Path = *Sel.path(); + if (Path.names().empty()) + return; + + // Build the function path, checking all names are static + std::vector FunctionPath; + FunctionPath.reserve(Path.names().size() + 1); + FunctionPath.emplace_back("lib"); + + for (const auto &Name : Path.names()) { + if (!Name->isStatic()) + return; // Dynamic attribute, can't construct URL + FunctionPath.emplace_back(Name->staticName()); + } + + // Construct the noogle.dev URL + std::string NoogleUrl = buildNoogleUrl(FunctionPath); + + // Create a code action that will open the URL + // Note: The actual URL opening is handled by the client via + // window/showDocument + Actions.emplace_back(CodeAction{ + .title = "Open Noogle documentation for " + FunctionPath.back(), + .kind = std::string(CodeAction::REFACTOR_KIND), + .data = Object{{"noogleUrl", NoogleUrl}}, + }); +} + /// \brief Add JSON to Nix conversion action for selected JSON text. /// This is a selection-based action that works on arbitrary text, not AST /// nodes. @@ -694,7 +775,6 @@ void addJsonToNixAction(llvm::StringRef Src, const lspserver::Range &Range, "Convert JSON to Nix", CodeAction::REFACTOR_REWRITE_KIND, FileURI, Range, std::move(NixText))); } - } // namespace void Controller::onCodeAction(const lspserver::CodeActionParams &Params, @@ -746,6 +826,7 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, addFlattenAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); addPackAttrsAction(*N, *TU->parentMap(), FileURI, TU->src(), Actions); + addNoogleDocAction(*N, *TU->parentMap(), Actions); } } @@ -758,4 +839,36 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, boost::asio::post(Pool, std::move(Action)); } +void Controller::onCodeActionResolve(const lspserver::CodeAction &Params, + Callback Reply) { + auto Action = [Reply = std::move(Reply), Params, this]() mutable { + // Check if this is a Noogle documentation action + if (Params.data) { + const auto *DataObj = Params.data->getAsObject(); + if (DataObj) { + auto NoogleUrl = DataObj->getString("noogleUrl"); + if (NoogleUrl) { + // Call window/showDocument to open the URL in external browser + ShowDocumentParams ShowParams; + ShowParams.externalUri = NoogleUrl->str(); + ShowParams.external = true; + + ShowDocument( + ShowParams, [](llvm::Expected Result) { + if (!Result) { + lspserver::elog("Failed to open Noogle documentation: {0}", + Result.takeError()); + } + }); + } + } + } + + // Return the resolved code action (unchanged for Noogle actions since + // the work is done via showDocument) + Reply(Params); + }; + boost::asio::post(Pool, std::move(Action)); +} + } // namespace nixd diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index 4f365a6ec..0aac76dbc 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -98,9 +98,10 @@ void Controller:: { "codeActionProvider", Object{ - {"codeActionKinds", Array{CodeAction::QUICKFIX_KIND, - CodeAction::REFACTOR_REWRITE_KIND}}, - {"resolveProvider", false}, + {"codeActionKinds", + Array{CodeAction::QUICKFIX_KIND, CodeAction::REFACTOR_KIND, + CodeAction::REFACTOR_REWRITE_KIND}}, + {"resolveProvider", true}, }, }, {"definitionProvider", true}, diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index 2b04592c0..d9c1670b2 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -107,6 +107,8 @@ Controller::Controller(std::unique_ptr In, &Controller::onDocumentLink); Registry.addMethod("textDocument/codeAction", this, &Controller::onCodeAction); + Registry.addMethod("codeAction/resolve", this, + &Controller::onCodeActionResolve); Registry.addMethod("textDocument/hover", this, &Controller::onHover); Registry.addMethod("textDocument/formatting", this, &Controller::onFormat); Registry.addMethod("textDocument/rename", this, &Controller::onRename); @@ -125,6 +127,8 @@ Controller::Controller(std::unique_ptr In, CreateWorkDoneProgress = mkOutMethod( "window/workDoneProgress/create"); + ShowDocument = mkOutMethod( + "window/showDocument"); BeginWorkDoneProgress = mkOutNotifiction>("$/progress"); ReportWorkDoneProgress = diff --git a/nixd/lspserver/include/lspserver/Protocol.h b/nixd/lspserver/include/lspserver/Protocol.h index 8e6679e6a..c2160af0d 100644 --- a/nixd/lspserver/include/lspserver/Protocol.h +++ b/nixd/lspserver/include/lspserver/Protocol.h @@ -1072,8 +1072,14 @@ struct CodeAction { /// A command this code action executes. If a code action provides an edit /// and a command, first the edit is executed and then the command. std::optional command; + + /// A data entry field that is preserved on a code action between a + /// `textDocument/codeAction` and a `codeAction/resolve` request. + /// @since LSP 3.16.0 + std::optional data; }; llvm::json::Value toJSON(const CodeAction &); +bool fromJSON(const llvm::json::Value &, CodeAction &, llvm::json::Path); /// Represents programming constructs like variables, classes, interfaces etc. /// that appear in a document. Document symbols can be hierarchical and they @@ -1159,6 +1165,42 @@ struct ApplyWorkspaceEditResponse { bool fromJSON(const llvm::json::Value &, ApplyWorkspaceEditResponse &, llvm::json::Path); +/// Parameters for the `window/showDocument` request. +/// @since LSP 3.16.0 +struct ShowDocumentParams { + /// The document URI to show (for file:// URIs). + URIForFile uri; + + /// External URI string (for https:// or other non-file URIs). + /// When set, this takes precedence over `uri` in serialization. + std::optional externalUri; + + /// Indicates to show the resource in an external program. + /// To show, for example, `https://noogle.dev/` in the default web browser, + /// set `external` to `true`. + std::optional external; + + /// An optional property to indicate whether the editor showing the document + /// should take focus or not. Clients might ignore this property if an + /// external program is started. + std::optional takeFocus; + + /// An optional selection range if the document is a text document. + /// Clients might ignore the property if an external program is started or + /// the file is not a text file. + std::optional selection; +}; +llvm::json::Value toJSON(const ShowDocumentParams &); + +/// Result of the `window/showDocument` request. +/// @since LSP 3.16.0 +struct ShowDocumentResult { + /// A boolean indicating if the show was successful. + bool success = false; +}; +bool fromJSON(const llvm::json::Value &, ShowDocumentResult &, + llvm::json::Path); + struct TextDocumentPositionParams { /// The text document. TextDocumentIdentifier textDocument; diff --git a/nixd/lspserver/src/Protocol.cpp b/nixd/lspserver/src/Protocol.cpp index 19c2b190f..8f665e989 100644 --- a/nixd/lspserver/src/Protocol.cpp +++ b/nixd/lspserver/src/Protocol.cpp @@ -782,9 +782,29 @@ llvm::json::Value toJSON(const CodeAction &CA) { CodeAction["edit"] = *CA.edit; if (CA.command) CodeAction["command"] = *CA.command; + if (CA.data) + CodeAction["data"] = *CA.data; return CodeAction; } +bool fromJSON(const llvm::json::Value &Params, CodeAction &CA, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Params, P); + if (!O || !O.map("title", CA.title)) + return false; + O.map("kind", CA.kind); + O.map("diagnostics", CA.diagnostics); + O.map("isPreferred", CA.isPreferred); + O.map("edit", CA.edit); + O.map("command", CA.command); + // Handle data field - it's an arbitrary JSON value + if (const auto *Obj = Params.getAsObject()) { + if (const auto *Data = Obj->get("data")) + CA.data = *Data; + } + return true; +} + llvm::raw_ostream &operator<<(llvm::raw_ostream &O, const DocumentSymbol &S) { return O << S.name << " - " << toJSON(S); } @@ -847,6 +867,28 @@ bool fromJSON(const llvm::json::Value &Response, ApplyWorkspaceEditResponse &R, O.map("failureReason", R.failureReason); } +llvm::json::Value toJSON(const ShowDocumentParams &Params) { + llvm::json::Object Result; + // Use externalUri if set (for https:// etc), otherwise use uri (for file://) + if (Params.externalUri) + Result["uri"] = *Params.externalUri; + else + Result["uri"] = Params.uri; + if (Params.external) + Result["external"] = *Params.external; + if (Params.takeFocus) + Result["takeFocus"] = *Params.takeFocus; + if (Params.selection) + Result["selection"] = *Params.selection; + return Result; +} + +bool fromJSON(const llvm::json::Value &Response, ShowDocumentResult &R, + llvm::json::Path P) { + llvm::json::ObjectMapper O(Response, P); + return O && O.map("success", R.success); +} + bool fromJSON(const llvm::json::Value &Params, TextDocumentPositionParams &R, llvm::json::Path P) { llvm::json::ObjectMapper O(Params, P); diff --git a/nixd/tools/nixd/test/code-action/noogle-lib-nested.md b/nixd/tools/nixd/test/code-action/noogle-lib-nested.md new file mode 100644 index 000000000..e134d0ae0 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/noogle-lib-nested.md @@ -0,0 +1,69 @@ +# 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 + +```nix file:///noogle-lib-nested.nix +lib.strings.optionalString +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///noogle-lib-nested.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":12 + }, + "end":{ + "line":0, + "character":26 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", + CHECK: "data": { +CHECK-NEXT: "noogleUrl": "https://noogle.dev/f/lib/strings/optionalString" +CHECK-NEXT: }, +CHECK-NEXT: "kind": "refactor", +CHECK-NEXT: "title": "Open Noogle documentation for optionalString" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/noogle-lib-simple.md b/nixd/tools/nixd/test/code-action/noogle-lib-simple.md new file mode 100644 index 000000000..df3f2ff29 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/noogle-lib-simple.md @@ -0,0 +1,73 @@ +# 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 + +```nix file:///noogle-lib-simple.nix +lib.optionalString +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///noogle-lib-simple.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":0 + }, + "end":{ + "line":0, + "character":18 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "data": { +CHECK-NEXT: "noogleUrl": "https://noogle.dev/f/lib/optionalString" +CHECK-NEXT: }, +CHECK-NEXT: "kind": "refactor", +CHECK-NEXT: "title": "Open Noogle documentation for optionalString" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/noogle-lib-var-only.md b/nixd/tools/nixd/test/code-action/noogle-lib-var-only.md new file mode 100644 index 000000000..78343dd3d --- /dev/null +++ b/nixd/tools/nixd/test/code-action/noogle-lib-var-only.md @@ -0,0 +1,67 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that Noogle action is NOT offered for just "lib" variable without selection + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///noogle-lib-var-only.nix +lib +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///noogle-lib-var-only.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":0 + }, + "end":{ + "line":0, + "character":3 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/code-action/noogle-not-lib.md b/nixd/tools/nixd/test/code-action/noogle-not-lib.md new file mode 100644 index 000000000..56fb0b8f9 --- /dev/null +++ b/nixd/tools/nixd/test/code-action/noogle-not-lib.md @@ -0,0 +1,67 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +# Test that Noogle action is NOT offered for non-lib selects + +<-- initialize(0) + +```json +{ + "jsonrpc":"2.0", + "id":0, + "method":"initialize", + "params":{ + "processId":123, + "rootPath":"", + "capabilities":{ + }, + "trace":"off" + } +} +``` + + +<-- textDocument/didOpen + +```nix file:///noogle-not-lib.nix +pkgs.hello +``` + +<-- textDocument/codeAction(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/codeAction", + "params":{ + "textDocument":{ + "uri":"file:///noogle-not-lib.nix" + }, + "range":{ + "start":{ + "line": 0, + "character":5 + }, + "end":{ + "line":0, + "character":10 + } + }, + "context":{ + "diagnostics":[], + "triggerKind":2 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", + CHECK-NOT: "noogleUrl" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index 71d1a56a4..4130957a1 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -29,9 +29,10 @@ CHECK-NEXT: "capabilities": { CHECK-NEXT: "codeActionProvider": { CHECK-NEXT: "codeActionKinds": [ CHECK-NEXT: "quickfix", +CHECK-NEXT: "refactor", CHECK-NEXT: "refactor.rewrite" CHECK-NEXT: ], -CHECK-NEXT: "resolveProvider": false +CHECK-NEXT: "resolveProvider": true CHECK-NEXT: }, CHECK-NEXT: "completionProvider": { CHECK-NEXT: "resolveProvider": true, diff --git a/nixd/tools/nixd/test/semantic-tokens/initialize.md b/nixd/tools/nixd/test/semantic-tokens/initialize.md index e303bad79..939d2bf5d 100644 --- a/nixd/tools/nixd/test/semantic-tokens/initialize.md +++ b/nixd/tools/nixd/test/semantic-tokens/initialize.md @@ -29,9 +29,10 @@ CHECK-NEXT: "capabilities": { CHECK-NEXT: "codeActionProvider": { CHECK-NEXT: "codeActionKinds": [ CHECK-NEXT: "quickfix", +CHECK-NEXT: "refactor", CHECK-NEXT: "refactor.rewrite" CHECK-NEXT: ], -CHECK-NEXT: "resolveProvider": false +CHECK-NEXT: "resolveProvider": true CHECK-NEXT: }, CHECK-NEXT: "completionProvider": { CHECK-NEXT: "resolveProvider": true,