diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index b2af1892a..1f6a7b24c 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -79,6 +79,10 @@ class Controller : public lspserver::LSPServer { void onDefinition(const lspserver::TextDocumentPositionParams &Params, lspserver::Callback Reply); + void + onReferences(const lspserver::TextDocumentPositionParams &Params, + lspserver::Callback> Reply); + void publishDiagnostics(lspserver::PathRef File, std::optional Version, const std::vector &Diagnostics); diff --git a/nixd/lib/Controller/Definition.cpp b/nixd/lib/Controller/Definition.cpp index f1e4d62ac..621b394f3 100644 --- a/nixd/lib/Controller/Definition.cpp +++ b/nixd/lib/Controller/Definition.cpp @@ -3,75 +3,102 @@ /// [Go to Definition]: /// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition +#include "Definition.h" #include "Convert.h" #include "nixd/Controller/Controller.h" +#include "nixf/Sema/VariableLookup.h" #include #include using namespace nixd; +using namespace nixf; using namespace lspserver; +using namespace llvm; + +using LookupResult = VariableLookupAnalysis::LookupResult; +using ResultKind = VariableLookupAnalysis::LookupResultKind; namespace { -void gotoDefinition(const NixTU &TU, const nixf::Node &AST, nixf::Position Pos, +void gotoDefinition(const NixTU &TU, const Node &AST, nixf::Position Pos, URIForFile URI, Callback &Reply) { - using LookupResult = nixf::VariableLookupAnalysis::LookupResult; - using ResultKind = nixf::VariableLookupAnalysis::LookupResultKind; - - const nixf::Node *N = AST.descend({Pos, Pos}); + const Node *N = AST.descend({Pos, Pos}); if (!N) [[unlikely]] { Reply(error("cannot find AST node on given position")); return; } - const nixf::ParentMapAnalysis *PMA = TU.parentMap(); + const ParentMapAnalysis *PMA = TU.parentMap(); assert(PMA && "ParentMap should not be null as AST is not null"); - const nixf::Node *Var = PMA->upTo(*N, nixf::Node::NK_ExprVar); - if (!Var) [[unlikely]] { - Reply(error("cannot find variable on given position")); - return; - } - assert(Var->kind() == nixf::Node::NK_ExprVar); - // OK, this is an variable. Lookup it in VLA entries. - const nixf::VariableLookupAnalysis *VLA = TU.variableLookup(); + const VariableLookupAnalysis *VLA = TU.variableLookup(); if (!VLA) [[unlikely]] { Reply(error("cannot get variable analysis for nix unit")); return; } - LookupResult Result = VLA->query(static_cast(*Var)); - if (Result.Kind == ResultKind::Undefined) { - Reply(error("this varaible is undefined")); - return; + if (Expected ExpDef = findDefinition(*N, *PMA, *VLA)) { + assert(ExpDef->syntax()); + Reply(Location{ + .uri = std::move(URI), + .range = toLSPRange(ExpDef->syntax()->range()), + }); + } else { + Reply(ExpDef.takeError()); } +} - if (Result.Def->isBuiltin()) { - Reply(error("this is a builtin variable")); - return; - } +const Definition *findSelfDefinition(const Node &N, + const ParentMapAnalysis &PMA, + const VariableLookupAnalysis &VLA) { + // If "N" is a definition itself, just return it. + if (const Definition *Def = VLA.toDef(N)) + return Def; - assert(Result.Def->syntax()); + // If N is inside an attrset, it maybe an "AttrName", let's look for it. + const Node *Parent = PMA.query(N); + if (Parent && Parent->kind() == Node::NK_AttrName) + return VLA.toDef(*Parent); - Reply(Location{ - .uri = std::move(URI), - .range = toLSPRange(Result.Def->syntax()->range()), - }); + return nullptr; } } // namespace +Expected +nixd::findDefinition(const Node &N, const ParentMapAnalysis &PMA, + const VariableLookupAnalysis &VLA) { + const Node *Var = PMA.upTo(N, Node::NK_ExprVar); + if (!Var) [[unlikely]] { + if (const Definition *Def = findSelfDefinition(N, PMA, VLA)) + return *Def; + return error("cannot find variable on given position"); + } + assert(Var->kind() == Node::NK_ExprVar); + LookupResult Result = VLA.query(static_cast(*Var)); + + assert(Result.Def); + + if (Result.Kind == ResultKind::Undefined) + return error("this varaible is undefined"); + + if (Result.Def->isBuiltin()) + return error("this is a builtin variable"); + + return *Result.Def; +} + void Controller::onDefinition(const TextDocumentPositionParams &Params, Callback Reply) { auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri, Pos = toNixfPosition(Params.position), 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]] { + if (std::shared_ptr AST = getAST(*TU, Reply)) [[likely]] { gotoDefinition(*TU, *AST, Pos, std::move(URI), Reply); } } diff --git a/nixd/lib/Controller/Definition.h b/nixd/lib/Controller/Definition.h new file mode 100644 index 000000000..0b8e09c61 --- /dev/null +++ b/nixd/lib/Controller/Definition.h @@ -0,0 +1,15 @@ +#pragma once + +#include "nixf/Sema/ParentMap.h" +#include "nixf/Sema/VariableLookup.h" + +#include + +namespace nixd { + +/// \brief Heuristically find definition on some node +llvm::Expected +findDefinition(const nixf::Node &N, const nixf::ParentMapAnalysis &PMA, + const nixf::VariableLookupAnalysis &VLA); + +} // namespace nixd diff --git a/nixd/lib/Controller/FindReferences.cpp b/nixd/lib/Controller/FindReferences.cpp new file mode 100644 index 000000000..391e4ebd8 --- /dev/null +++ b/nixd/lib/Controller/FindReferences.cpp @@ -0,0 +1,71 @@ +/// \file +/// \brief This implements [Find References]. +/// [Find References]: +/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references + +#include "Convert.h" +#include "Definition.h" + +#include "nixd/Controller/Controller.h" + +#include +#include +#include +#include + +using namespace lspserver; +using namespace nixd; +using namespace llvm; +using namespace nixf; + +namespace { + +Error findReferences(const nixf::Node &Desc, const ParentMapAnalysis &PMA, + const VariableLookupAnalysis &VLA, const URIForFile &URI, + std::vector &Locations) { + + // Two steps. + // 1. Find some "definition" for this node. + // 2. Find all "uses", and construct the vector. + + // Find "definition" + if (auto Def = findDefinition(Desc, PMA, VLA)) { + // OK, iterate all uses. + for (const auto *Use : Def->uses()) { + assert(Use); + Locations.emplace_back(Location{ + .uri = URI, + .range = toLSPRange(Use->range()), + }); + } + return Error::success(); + } + return error("Cannot find definition of this node"); +} + +} // namespace + +void Controller::onReferences(const TextDocumentPositionParams &Params, + Callback> Reply) { + auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri, + Pos = toNixfPosition(Params.position), 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]] { + const nixf::Node *Desc = AST->descend({Pos, Pos}); + if (!Desc) { + Reply(error("cannot find corresponding node on given position")); + return; + } + std::vector Locations; + if (auto Err = findReferences(*Desc, *TU->parentMap(), + *TU->variableLookup(), URI, Locations)) { + Reply(std::move(Err)); + return; + } + Reply(std::move(Locations)); + } + } + }; + boost::asio::post(Pool, std::move(Action)); +} diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index 933da78f6..2a4750ecb 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -37,6 +37,7 @@ void Controller:: }, }, {"definitionProvider", true}, + {"referencesProvider", true}, {"hoverProvider", true}}, }; diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index ada4bafe5..fbaa3574a 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -125,6 +125,8 @@ Controller::Controller(std::unique_ptr In, // Language Features Registry.addMethod("textDocument/definition", this, &Controller::onDefinition); + Registry.addMethod("textDocument/references", this, + &Controller::onReferences); Registry.addMethod("textDocument/codeAction", this, &Controller::onCodeAction); Registry.addMethod("textDocument/hover", this, &Controller::onHover); diff --git a/nixd/meson.build b/nixd/meson.build index a57320bb6..448889250 100644 --- a/nixd/meson.build +++ b/nixd/meson.build @@ -9,6 +9,7 @@ libnixd_lib = library( 'lib/Controller/Definition.cpp', 'lib/Controller/Diagnostics.cpp', 'lib/Controller/EvalClient.cpp', + 'lib/Controller/FindReferences.cpp', 'lib/Controller/Hover.cpp', 'lib/Controller/LifeTime.cpp', 'lib/Controller/NixTU.cpp', diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index b3f551d26..1b4cc230e 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -34,6 +34,7 @@ CHECK-NEXT: "resolveProvider": false CHECK-NEXT: }, CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "hoverProvider": true, +CHECK-NEXT: "referencesProvider": true, CHECK-NEXT: "textDocumentSync": { CHECK-NEXT: "change": 2, CHECK-NEXT: "openClose": true, diff --git a/nixd/tools/nixd/test/references-lambda.md b/nixd/tools/nixd/test/references-lambda.md new file mode 100644 index 000000000..bcda6756e --- /dev/null +++ b/nixd/tools/nixd/test/references-lambda.md @@ -0,0 +1,93 @@ +# 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 + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"arg @ { foo, bar }: arg + foo + bar + arg" + } + } +} +``` + +<-- textDocument/references(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/references", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":0 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 23, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 20, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 41, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 38, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/references-let.md b/nixd/tools/nixd/test/references-let.md new file mode 100644 index 000000000..ca6904965 --- /dev/null +++ b/nixd/tools/nixd/test/references-let.md @@ -0,0 +1,80 @@ +# 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 + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"let x = 1; y = 2; in x + y" + } + } +} +``` + +<-- textDocument/references(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/references", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":4 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 22, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 21, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/references-rec.md b/nixd/tools/nixd/test/references-rec.md new file mode 100644 index 000000000..c79ca9d08 --- /dev/null +++ b/nixd/tools/nixd/test/references-rec.md @@ -0,0 +1,80 @@ +# 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 + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"rec { x = 1; y = x; }" + } + } +} +``` + +<-- textDocument/references(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/references", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":6 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 18, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 17, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/references-var.md b/nixd/tools/nixd/test/references-var.md new file mode 100644 index 000000000..4603ea4f8 --- /dev/null +++ b/nixd/tools/nixd/test/references-var.md @@ -0,0 +1,93 @@ +# 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 + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"arg @ { foo, bar }: arg + foo + bar + arg" + } + } +} +``` + +<-- textDocument/references(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/references", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":21 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 23, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 20, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 41, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 38, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/references-with.md b/nixd/tools/nixd/test/references-with.md new file mode 100644 index 000000000..d8799c835 --- /dev/null +++ b/nixd/tools/nixd/test/references-with.md @@ -0,0 +1,94 @@ +# 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 + +```json +{ + "jsonrpc":"2.0", + "method":"textDocument/didOpen", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix", + "languageId":"nix", + "version":1, + "text":"with builtins; foo + bar" + } + } +} +``` + +<-- textDocument/references(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/references", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":3 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": [ +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 18, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 15, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 24, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 21, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +CHECK-NEXT: } +CHECK-NEXT: ] +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +```