From 3b6e46fc854ebd3e6dfaa0f1d603f5165e2dcb9b Mon Sep 17 00:00:00 2001 From: Yingchi Long Date: Wed, 10 Apr 2024 00:23:42 +0800 Subject: [PATCH] nixd: support `textDocument/rename`, `textDocument/prepareRename` (#405) Fixes: #255 --- libnixf/include/nixf/Sema/VariableLookup.h | 3 + nixd/include/nixd/Controller/Controller.h | 6 ++ nixd/lib/Controller/Definition.cpp | 7 +- nixd/lib/Controller/LifeTime.cpp | 6 +- nixd/lib/Controller/Rename.cpp | 98 ++++++++++++++++++++++ nixd/lib/Controller/Support.cpp | 3 + nixd/meson.build | 1 + nixd/tools/nixd/test/initialize.md | 3 + nixd/tools/nixd/test/rename-duplicated.md | 72 ++++++++++++++++ nixd/tools/nixd/test/rename-issue-255-1.md | 97 +++++++++++++++++++++ nixd/tools/nixd/test/rename-issue-255-2.md | 97 +++++++++++++++++++++ 11 files changed, 390 insertions(+), 3 deletions(-) create mode 100644 nixd/lib/Controller/Rename.cpp create mode 100644 nixd/tools/nixd/test/rename-duplicated.md create mode 100644 nixd/tools/nixd/test/rename-issue-255-1.md create mode 100644 nixd/tools/nixd/test/rename-issue-255-2.md diff --git a/libnixf/include/nixf/Sema/VariableLookup.h b/libnixf/include/nixf/Sema/VariableLookup.h index 6954eb2bf..b56b76977 100644 --- a/libnixf/include/nixf/Sema/VariableLookup.h +++ b/libnixf/include/nixf/Sema/VariableLookup.h @@ -79,6 +79,7 @@ class VariableLookupAnalysis { Undefined, FromWith, Defined, + NoSuchVar, }; struct LookupResult { @@ -129,6 +130,8 @@ class VariableLookupAnalysis { /// \brief Query the which name/with binds to specific varaible. [[nodiscard]] LookupResult query(const ExprVar &Var) const { + if (!Results.contains(&Var)) + return {.Kind = LookupResultKind::NoSuchVar}; return Results.at(&Var); } diff --git a/nixd/include/nixd/Controller/Controller.h b/nixd/include/nixd/Controller/Controller.h index 8bb09eeee..c37438d9b 100644 --- a/nixd/include/nixd/Controller/Controller.h +++ b/nixd/include/nixd/Controller/Controller.h @@ -92,6 +92,12 @@ class Controller : public lspserver::LSPServer { std::optional Version, const std::vector &Diagnostics); + void onRename(const lspserver::RenameParams &Params, + lspserver::Callback Reply); + + void onPrepareRename(const lspserver::TextDocumentPositionParams &Params, + lspserver::Callback Reply); + public: Controller(std::unique_ptr In, std::unique_ptr Out); diff --git a/nixd/lib/Controller/Definition.cpp b/nixd/lib/Controller/Definition.cpp index 721646d25..0bc63ba86 100644 --- a/nixd/lib/Controller/Definition.cpp +++ b/nixd/lib/Controller/Definition.cpp @@ -127,11 +127,14 @@ nixd::findDefinition(const Node &N, const ParentMapAnalysis &PMA, 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.Kind == ResultKind::NoSuchVar) + return error("this varaible is not used in var lookup (duplicated attr?)"); + + assert(Result.Def); + if (Result.Def->isBuiltin()) return error("this is a builtin variable"); diff --git a/nixd/lib/Controller/LifeTime.cpp b/nixd/lib/Controller/LifeTime.cpp index d87457608..c1f345c7f 100644 --- a/nixd/lib/Controller/LifeTime.cpp +++ b/nixd/lib/Controller/LifeTime.cpp @@ -39,7 +39,11 @@ void Controller:: {"definitionProvider", true}, {"referencesProvider", true}, {"documentHighlightProvider", true}, - {"hoverProvider", true}}, + {"hoverProvider", true}, + {"renameProvider", + Object{ + {"prepareProvider", true}, + }}}, }; Object Result{{ diff --git a/nixd/lib/Controller/Rename.cpp b/nixd/lib/Controller/Rename.cpp new file mode 100644 index 000000000..a4cf4263e --- /dev/null +++ b/nixd/lib/Controller/Rename.cpp @@ -0,0 +1,98 @@ +/// \file +/// \brief This implements [Rename]. +/// [Rename]: +/// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename + +#include "Convert.h" +#include "Definition.h" + +#include "lspserver/Protocol.h" +#include "nixd/Controller/Controller.h" + +#include + +using namespace lspserver; +using namespace nixd; +using namespace llvm; +using namespace nixf; + +namespace { +llvm::Expected rename(const nixf::Node &Desc, + const std::string &NewText, + const ParentMapAnalysis &PMA, + const VariableLookupAnalysis &VLA, + const URIForFile &URI) { + using lspserver::TextEdit; + // Find "definition" + auto Def = findDefinition(Desc, PMA, VLA); + if (!Def) + return Def.takeError(); + + std::vector Edits; + + for (const auto *Use : Def->uses()) { + Edits.emplace_back(TextEdit{ + .range = toLSPRange(Use->range()), + .newText = NewText, + }); + } + + Edits.emplace_back(TextEdit{ + .range = toLSPRange(Def->syntax()->range()), + .newText = NewText, + }); + WorkspaceEdit WE; + WE.changes = std::map>{ + {URI.uri(), std::move(Edits)}}; + return WE; +} +} // namespace + +void Controller::onRename(const RenameParams &Params, + Callback Reply) { + auto Action = [Reply = std::move(Reply), URI = Params.textDocument.uri, + Pos = toNixfPosition(Params.position), + NewText = Params.newName, 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; + } + Reply(rename(*Desc, NewText, *TU->parentMap(), *TU->variableLookup(), + URI)); + return; + } + } + }; + boost::asio::post(Pool, std::move(Action)); +} + +void Controller::onPrepareRename( + const lspserver::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; + } + llvm::Expected Exp = + rename(*Desc, "", *TU->parentMap(), *TU->variableLookup(), URI); + if (Exp) { + Reply(toLSPRange(Desc->range())); + return; + } + Reply(Exp.takeError()); + return; + } + } + }; + boost::asio::post(Pool, std::move(Action)); +} diff --git a/nixd/lib/Controller/Support.cpp b/nixd/lib/Controller/Support.cpp index 957a1b544..a57e91a27 100644 --- a/nixd/lib/Controller/Support.cpp +++ b/nixd/lib/Controller/Support.cpp @@ -132,6 +132,9 @@ Controller::Controller(std::unique_ptr In, Registry.addMethod("textDocument/codeAction", this, &Controller::onCodeAction); Registry.addMethod("textDocument/hover", this, &Controller::onHover); + Registry.addMethod("textDocument/rename", this, &Controller::onRename); + Registry.addMethod("textDocument/prepareRename", this, + &Controller::onPrepareRename); } } // namespace nixd diff --git a/nixd/meson.build b/nixd/meson.build index 8bcd53629..1fb997b29 100644 --- a/nixd/meson.build +++ b/nixd/meson.build @@ -14,6 +14,7 @@ libnixd_lib = library( 'lib/Controller/Hover.cpp', 'lib/Controller/LifeTime.cpp', 'lib/Controller/NixTU.cpp', + 'lib/Controller/Rename.cpp', 'lib/Controller/Support.cpp', 'lib/Controller/TextDocumentSync.cpp', 'lib/Eval/EvalProvider.cpp', diff --git a/nixd/tools/nixd/test/initialize.md b/nixd/tools/nixd/test/initialize.md index 308f28332..1c1419de4 100644 --- a/nixd/tools/nixd/test/initialize.md +++ b/nixd/tools/nixd/test/initialize.md @@ -36,6 +36,9 @@ CHECK-NEXT: "definitionProvider": true, CHECK-NEXT: "documentHighlightProvider": true, CHECK-NEXT: "hoverProvider": true, CHECK-NEXT: "referencesProvider": true, +CHECK-NEXT: "renameProvider": { +CHECK-NEXT: "prepareProvider": true +CHECK-NEXT: }, CHECK-NEXT: "textDocumentSync": { CHECK-NEXT: "change": 2, CHECK-NEXT: "openClose": true, diff --git a/nixd/tools/nixd/test/rename-duplicated.md b/nixd/tools/nixd/test/rename-duplicated.md new file mode 100644 index 000000000..116ff091a --- /dev/null +++ b/nixd/tools/nixd/test/rename-duplicated.md @@ -0,0 +1,72 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +// https://github.com/nix-community/nixd/issues/255 + +<-- 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 { bar = 1; a = 1; a = bar; b = a; }" + } + } +} +``` + +<-- textDocument/rename(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/rename", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":28 + }, + "newName": "b" + } +} +``` + +``` + CHECK: "error": { +CHECK-NEXT: "code": -32001, +CHECK-NEXT: "message": "this varaible is not used in var lookup (duplicated attr?)" +CHECK-NEXT: }, +CHECK-NEXT: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0" +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/rename-issue-255-1.md b/nixd/tools/nixd/test/rename-issue-255-1.md new file mode 100644 index 000000000..1304b8a13 --- /dev/null +++ b/nixd/tools/nixd/test/rename-issue-255-1.md @@ -0,0 +1,97 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +// https://github.com/nix-community/nixd/issues/255 + +<-- 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":"{a ? 1}: { x = a; }" + } + } +} +``` + +<-- textDocument/rename(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/rename", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":1 + }, + "newName": "b" + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "changes": { +CHECK-NEXT: "file:///basic.nix": [ +CHECK-NEXT: { +CHECK-NEXT: "newText": "b", +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 16, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 15, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "newText": "b", +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 2, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 1, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: } +CHECK-NEXT: } +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +``` diff --git a/nixd/tools/nixd/test/rename-issue-255-2.md b/nixd/tools/nixd/test/rename-issue-255-2.md new file mode 100644 index 000000000..856c91161 --- /dev/null +++ b/nixd/tools/nixd/test/rename-issue-255-2.md @@ -0,0 +1,97 @@ +# RUN: nixd --lit-test < %s | FileCheck %s + +// https://github.com/nix-community/nixd/issues/255 + +<-- 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":"{a ? 1}: { x = a; }" + } + } +} +``` + +<-- textDocument/rename(2) + + +```json +{ + "jsonrpc":"2.0", + "id":2, + "method":"textDocument/rename", + "params":{ + "textDocument":{ + "uri":"file:///basic.nix" + }, + "position":{ + "line": 0, + "character":15 + }, + "newName": "b" + } +} +``` + +``` + CHECK: "id": 2, +CHECK-NEXT: "jsonrpc": "2.0", +CHECK-NEXT: "result": { +CHECK-NEXT: "changes": { +CHECK-NEXT: "file:///basic.nix": [ +CHECK-NEXT: { +CHECK-NEXT: "newText": "b", +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 16, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 15, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: } +CHECK-NEXT: }, +CHECK-NEXT: { +CHECK-NEXT: "newText": "b", +CHECK-NEXT: "range": { +CHECK-NEXT: "end": { +CHECK-NEXT: "character": 2, +CHECK-NEXT: "line": 0 +CHECK-NEXT: }, +CHECK-NEXT: "start": { +CHECK-NEXT: "character": 1, +CHECK-NEXT: "line": 0 +CHECK-NEXT: } +CHECK-NEXT: } +CHECK-NEXT: } +``` + +```json +{"jsonrpc":"2.0","method":"exit"} +```