diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 8caf2c548..b2af1892a 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -18,7 +18,31 @@ class Controller : public lspserver::LSPServer { PublishDiagnostic; std::mutex TUsLock; - llvm::StringMap TUs; + llvm::StringMap> TUs; + + template + std::shared_ptr getTU(std::string File, lspserver::Callback &Reply, + bool Ignore = false) { + using lspserver::error; + std::lock_guard G(TUsLock); + if (!TUs.count(File)) [[unlikely]] { + if (!Ignore) + Reply(error("cannot find corresponding AST on file {0}", File)); + return nullptr; + } + return TUs[File]; + } + + template + std::shared_ptr getAST(const NixTU &TU, + lspserver::Callback &Reply) { + using lspserver::error; + if (!TU.ast()) { + Reply(error("AST is null on this unit")); + return nullptr; + } + return TU.ast(); + } boost::asio::thread_pool Pool; @@ -52,6 +76,9 @@ class Controller : public lspserver::LSPServer { void onHover(const lspserver::TextDocumentPositionParams &Params, lspserver::Callback> Reply); + void onDefinition(const lspserver::TextDocumentPositionParams &Params, + lspserver::Callback Reply); + void publishDiagnostics(lspserver::PathRef File, std::optional Version, const std::vector &Diagnostics); diff --git a/nixd/include/nixd/Controller/NixTU.h b/nixd/include/nixd/Controller/NixTU.h index 70b0ae98c..3c0bb7e83 100644 --- a/nixd/include/nixd/Controller/NixTU.h +++ b/nixd/include/nixd/Controller/NixTU.h @@ -4,6 +4,8 @@ #include "nixf/Basic/Diagnostic.h" #include "nixf/Basic/Nodes/Basic.h" +#include "nixf/Sema/ParentMap.h" +#include "nixf/Sema/VariableLookup.h" #include #include @@ -18,20 +20,29 @@ class NixTU { std::vector Diagnostics; std::shared_ptr AST; std::optional ASTByteCode; + std::unique_ptr VLA; + std::unique_ptr PMA; public: NixTU() = default; NixTU(std::vector Diagnostics, std::shared_ptr AST, - std::optional ASTByteCode) - : Diagnostics(std::move(Diagnostics)), AST(std::move(AST)), - ASTByteCode(std::move(ASTByteCode)) {} + std::optional ASTByteCode, + std::unique_ptr VLA); [[nodiscard]] const std::vector &diagnostics() const { return Diagnostics; } [[nodiscard]] const std::shared_ptr &ast() const { return AST; } + + [[nodiscard]] const nixf::ParentMapAnalysis *parentMap() const { + return PMA.get(); + } + + [[nodiscard]] const nixf::VariableLookupAnalysis *variableLookup() const { + return VLA.get(); + } }; } // namespace nixd diff --git a/nixd/lib/Controller/CodeAction.cpp b/nixd/lib/Controller/CodeAction.cpp index 2d82f6364..ec55182de 100644 --- a/nixd/lib/Controller/CodeAction.cpp +++ b/nixd/lib/Controller/CodeAction.cpp @@ -19,41 +19,39 @@ void Controller::onCodeAction(const lspserver::CodeActionParams &Params, std::string File(Params.textDocument.uri.file()); Range Range = Params.range; auto Action = [Reply = std::move(Reply), File, Range, this]() mutable { - std::vector Diagnostics; - { - std::lock_guard TU(TUsLock); - Diagnostics = TUs[File].diagnostics(); - } - std::vector Actions; - Actions.reserve(Diagnostics.size()); - for (const nixf::Diagnostic &D : Diagnostics) { - auto DRange = toLSPRange(D.range()); - if (!Range.overlap(DRange)) - continue; - - // Add fixes. - for (const nixf::Fix &F : D.fixes()) { - std::vector Edits; - Edits.reserve(F.edits().size()); - for (const nixf::TextEdit &TE : F.edits()) { - Edits.emplace_back(TextEdit{ - .range = toLSPRange(TE.oldRange()), - .newText = std::string(TE.newText()), + if (auto TU = getTU(File, Reply, /*Ignore=*/true)) { + std::vector Diagnostics = TU->diagnostics(); + std::vector Actions; + Actions.reserve(Diagnostics.size()); + for (const nixf::Diagnostic &D : Diagnostics) { + auto DRange = toLSPRange(D.range()); + if (!Range.overlap(DRange)) + continue; + + // Add fixes. + for (const nixf::Fix &F : D.fixes()) { + std::vector Edits; + Edits.reserve(F.edits().size()); + for (const nixf::TextEdit &TE : F.edits()) { + Edits.emplace_back(TextEdit{ + .range = toLSPRange(TE.oldRange()), + .newText = std::string(TE.newText()), + }); + } + using Changes = std::map>; + std::string FileURI = URIForFile::canonicalize(File, File).uri(); + WorkspaceEdit WE{.changes = Changes{ + {std::move(FileURI), std::move(Edits)}, + }}; + Actions.emplace_back(CodeAction{ + .title = F.message(), + .kind = std::string(CodeAction::QUICKFIX_KIND), + .edit = std::move(WE), }); } - using Changes = std::map>; - std::string FileURI = URIForFile::canonicalize(File, File).uri(); - WorkspaceEdit WE{.changes = Changes{ - {std::move(FileURI), std::move(Edits)}, - }}; - Actions.emplace_back(CodeAction{ - .title = F.message(), - .kind = std::string(CodeAction::QUICKFIX_KIND), - .edit = std::move(WE), - }); } + Reply(std::move(Actions)); } - Reply(std::move(Actions)); }; boost::asio::post(Pool, std::move(Action)); } diff --git a/nixd/lib/Controller/Convert.cpp b/nixd/lib/Controller/Convert.cpp index ad258e12d..0e4711af7 100644 --- a/nixd/lib/Controller/Convert.cpp +++ b/nixd/lib/Controller/Convert.cpp @@ -21,6 +21,10 @@ lspserver::Position toLSPPosition(const nixf::LexerCursor &P) { static_cast(P.column())}; } +nixf::Position toNixfPosition(const lspserver::Position &P) { + return {P.line, P.character}; +} + 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 455659d1b..9ecc0f8cb 100644 --- a/nixd/lib/Controller/Convert.h +++ b/nixd/lib/Controller/Convert.h @@ -12,6 +12,8 @@ namespace nixd { lspserver::Position toLSPPosition(const nixf::LexerCursor &P); +nixf::Position toNixfPosition(const lspserver::Position &P); + lspserver::Range toLSPRange(const nixf::LexerCursorRange &R); int getLSPSeverity(nixf::Diagnostic::DiagnosticKind Kind); diff --git a/nixd/lib/Controller/Definition.cpp b/nixd/lib/Controller/Definition.cpp new file mode 100644 index 000000000..f1e4d62ac --- /dev/null +++ b/nixd/lib/Controller/Definition.cpp @@ -0,0 +1,80 @@ +/// \file +/// \brief Implementation of [Go to Definition] +/// [Go to Definition]: +/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition + +#include "Convert.h" + +#include "nixd/Controller/Controller.h" + +#include + +#include + +using namespace nixd; +using namespace lspserver; + +namespace { + +void gotoDefinition(const NixTU &TU, const nixf::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}); + if (!N) [[unlikely]] { + Reply(error("cannot find AST node on given position")); + return; + } + + const nixf::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(); + 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 (Result.Def->isBuiltin()) { + Reply(error("this is a builtin variable")); + return; + } + + assert(Result.Def->syntax()); + + Reply(Location{ + .uri = std::move(URI), + .range = toLSPRange(Result.Def->syntax()->range()), + }); +} + +} // namespace + +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]] { + gotoDefinition(*TU, *AST, Pos, std::move(URI), Reply); + } + } + }; + boost::asio::post(Pool, std::move(Action)); +} diff --git a/nixd/lib/Controller/Hover.cpp b/nixd/lib/Controller/Hover.cpp index 4f14b21de..fd13e012a 100644 --- a/nixd/lib/Controller/Hover.cpp +++ b/nixd/lib/Controller/Hover.cpp @@ -24,23 +24,25 @@ void Controller::onHover(const TextDocumentPositionParams &Params, auto Action = [Reply = std::move(Reply), File = std::string(Params.textDocument.uri.file()), RawPos = Params.position, this]() mutable { - std::lock_guard G(TUsLock); - const std::shared_ptr &AST = TUs[File].ast(); - nixf::Position Pos{RawPos.line, RawPos.character}; - const nixf::Node *N = AST->descend({Pos, Pos}); - if (!N) { - Reply(std::nullopt); - return; + if (std::shared_ptr TU = getTU(File, Reply)) [[likely]] { + if (std::shared_ptr AST = getAST(*TU, Reply)) [[likely]] { + nixf::Position Pos{RawPos.line, RawPos.character}; + const nixf::Node *N = AST->descend({Pos, Pos}); + if (!N) { + Reply(std::nullopt); + return; + } + std::string Name = N->name(); + Reply(Hover{ + .contents = + MarkupContent{ + .kind = MarkupKind::Markdown, + .value = "`" + Name + "`", + }, + .range = toLSPRange(N->range()), + }); + } } - std::string Name = N->name(); - Reply(Hover{ - .contents = - MarkupContent{ - .kind = MarkupKind::Markdown, - .value = "`" + Name + "`", - }, - .range = toLSPRange(N->range()), - }); }; boost::asio::post(Pool, std::move(Action)); } diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index 0ce7b6166..933da78f6 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -36,6 +36,7 @@ void Controller:: {"resolveProvider", false}, }, }, + {"definitionProvider", true}, {"hoverProvider", true}}, }; diff --git a/nixd/lib/Controller/NixTU.cpp b/nixd/lib/Controller/NixTU.cpp new file mode 100644 index 000000000..72e1adefc --- /dev/null +++ b/nixd/lib/Controller/NixTU.cpp @@ -0,0 +1,15 @@ +#include "nixd/Controller/NixTU.h" + +using namespace nixd; + +NixTU::NixTU(std::vector Diagnostics, + std::shared_ptr AST, + std::optional ASTByteCode, + std::unique_ptr VLA) + : Diagnostics(std::move(Diagnostics)), AST(std::move(AST)), + ASTByteCode(std::move(ASTByteCode)), VLA(std::move(VLA)) { + if (this->AST) { + PMA = std::make_unique(); + PMA->runOnAST(*this->AST); + } +} diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index 9f1509cb0..ada4bafe5 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -42,12 +42,15 @@ void Controller::actOnDocumentAdd(PathRef File, if (!AST) { std::lock_guard G(TUsLock); publishDiagnostics(File, Version, Diagnostics); - TUs[File] = NixTU(std::move(Diagnostics), std::move(AST), std::nullopt); + TUs.insert_or_assign(File, + std::make_shared(std::move(Diagnostics), + std::move(AST), std::nullopt, + /*VLA=*/nullptr)); return; } - nixf::VariableLookupAnalysis VLA(Diagnostics); - VLA.runOnAST(*AST); + auto VLA = std::make_unique(Diagnostics); + VLA->runOnAST(*AST); publishDiagnostics(File, Version, Diagnostics); @@ -58,7 +61,9 @@ void Controller::actOnDocumentAdd(PathRef File, if (Buf.empty()) { lspserver::log("empty AST for {0}", File); std::lock_guard G(TUsLock); - TUs[File] = NixTU(std::move(Diagnostics), std::move(AST), std::nullopt); + TUs.insert_or_assign( + File, std::make_shared(std::move(Diagnostics), std::move(AST), + std::nullopt, std::move(VLA))); return; } @@ -77,8 +82,11 @@ void Controller::actOnDocumentAdd(PathRef File, Buf.size()); std::lock_guard G(TUsLock); - TUs[File] = NixTU(std::move(Diagnostics), std::move(AST), - util::OwnedRegion{std::move(Shm), std::move(Region)}); + TUs.insert_or_assign( + File, std::make_shared( + std::move(Diagnostics), std::move(AST), + util::OwnedRegion{std::move(Shm), std::move(Region)}, + std::move(VLA))); if (Eval) { Eval->RegisterBC(rpc::RegisterBCParams{ @@ -91,8 +99,8 @@ void Controller::actOnDocumentAdd(PathRef File, // So just invoke the action here. Action(); } else { - // Otherwise we may want to concurently parse & serialize the file, so post - // it to the thread pool. + // Otherwise we may want to concurently parse & serialize the file, so + // post it to the thread pool. boost::asio::post(Pool, std::move(Action)); } } @@ -115,6 +123,8 @@ Controller::Controller(std::unique_ptr In, &Controller::onDocumentDidClose); // Language Features + Registry.addMethod("textDocument/definition", this, + &Controller::onDefinition); 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 aa83f5b2e..a57320bb6 100644 --- a/nixd/meson.build +++ b/nixd/meson.build @@ -6,10 +6,12 @@ libnixd_lib = library( 'nixd', 'lib/Controller/CodeAction.cpp', 'lib/Controller/Convert.cpp', + 'lib/Controller/Definition.cpp', 'lib/Controller/Diagnostics.cpp', 'lib/Controller/EvalClient.cpp', 'lib/Controller/Hover.cpp', 'lib/Controller/LifeTime.cpp', + 'lib/Controller/NixTU.cpp', 'lib/Controller/Support.cpp', 'lib/Controller/TextDocumentSync.cpp', 'lib/Eval/EvalProvider.cpp', diff --git a/nixd/tools/nixd/test/definition-with.md b/nixd/tools/nixd/test/definition-with.md new file mode 100644 index 000000000..bcd353552 --- /dev/null +++ b/nixd/tools/nixd/test/definition-with.md @@ -0,0 +1,78 @@ +# 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 pkgs; x" + } + } +} +``` + +<-- textDocument/definition(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/definition", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":11 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 4, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 0, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: "uri": "file:///basic.nix" +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/definition.md b/nixd/tools/nixd/test/definition.md new file mode 100644 index 000000000..23b07c1b6 --- /dev/null +++ b/nixd/tools/nixd/test/definition.md @@ -0,0 +1,77 @@ +# 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; in x" + } + } +} +``` + +<-- textDocument/definition(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/definition", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":14 + } + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 5, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 4, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: }, +``` + + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index 2b2feeebd..b3f551d26 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -32,6 +32,7 @@ CHECK-NEXT: "quickfix" CHECK-NEXT: ], CHECK-NEXT: "resolveProvider": false CHECK-NEXT: }, +CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "hoverProvider": true, CHECK-NEXT: "textDocumentSync": { CHECK-NEXT: "change": 2,