Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions nixd/include/nixd/Controller/Controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(
const lspserver::ShowDocumentParams &,
lspserver::Callback<lspserver::ShowDocumentResult>)>
ShowDocument;

llvm::unique_function<void(
const lspserver::ProgressParams<lspserver::WorkDoneProgressBegin> &)>
BeginWorkDoneProgress;
Expand Down Expand Up @@ -165,6 +172,9 @@ class Controller : public lspserver::LSPServer {
onCodeAction(const lspserver::CodeActionParams &Params,
lspserver::Callback<std::vector<lspserver::CodeAction>> Reply);

void onCodeActionResolve(const lspserver::CodeAction &Params,
lspserver::Callback<lspserver::CodeAction> Reply);

void onHover(const lspserver::TextDocumentPositionParams &Params,
lspserver::Callback<std::optional<lspserver::Hover>> Reply);

Expand Down
115 changes: 114 additions & 1 deletion nixd/lib/Controller/CodeAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include "nixd/Controller/Controller.h"

#include <nixf/Basic/Nodes/Attrs.h>
#include <nixf/Basic/Nodes/Expr.h>
#include <nixf/Basic/Nodes/Simple.h>
#include <nixf/Sema/ParentMap.h>

#include <boost/asio/post.hpp>
Expand Down Expand Up @@ -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<std::string> &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<CodeAction> &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<const nixf::ExprSelect &>(*SelectNode);

// Check base expression is ExprVar with name "lib"
if (Sel.expr().kind() != nixf::Node::NK_ExprVar)
return;

const auto &Var = static_cast<const nixf::ExprVar &>(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<std::string> 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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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<CodeAction> 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<ShowDocumentResult> 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
7 changes: 4 additions & 3 deletions nixd/lib/Controller/LifeTime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
4 changes: 4 additions & 0 deletions nixd/lib/Controller/Support.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ Controller::Controller(std::unique_ptr<lspserver::InboundPort> 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);
Expand All @@ -125,6 +127,8 @@ Controller::Controller(std::unique_ptr<lspserver::InboundPort> In,
CreateWorkDoneProgress =
mkOutMethod<WorkDoneProgressCreateParams, std::nullptr_t>(
"window/workDoneProgress/create");
ShowDocument = mkOutMethod<ShowDocumentParams, ShowDocumentResult>(
"window/showDocument");
BeginWorkDoneProgress =
mkOutNotifiction<ProgressParams<WorkDoneProgressBegin>>("$/progress");
ReportWorkDoneProgress =
Expand Down
42 changes: 42 additions & 0 deletions nixd/lspserver/include/lspserver/Protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -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> 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<llvm::json::Value> 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
Expand Down Expand Up @@ -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<std::string> 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<bool> 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<bool> 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<Range> 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;
Expand Down
42 changes: 42 additions & 0 deletions nixd/lspserver/src/Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
69 changes: 69 additions & 0 deletions nixd/tools/nixd/test/code-action/noogle-lib-nested.md
Original file line number Diff line number Diff line change
@@ -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"}
```
Loading