diff --git a/common/lsp/lsp-protocol.yaml b/common/lsp/lsp-protocol.yaml index a843c7933..24735bca4 100644 --- a/common/lsp/lsp-protocol.yaml +++ b/common/lsp/lsp-protocol.yaml @@ -202,3 +202,17 @@ DocumentLinkParams: DocumentLink: range: Range target?: string # DocumentUri + +# -- textDocument/prepareRename +PrepareRenameParams: + <: TextDocumentPositionParams + +# Response: Range + +# -- textDocument/rename +RenameParams: + <: TextDocumentPositionParams + newName: string + +# Response: Range[] + diff --git a/verilog/tools/ls/symbol-table-handler.cc b/verilog/tools/ls/symbol-table-handler.cc index 7bb81f179..b34b48724 100644 --- a/verilog/tools/ls/symbol-table-handler.cc +++ b/verilog/tools/ls/symbol-table-handler.cc @@ -16,11 +16,13 @@ #include "verilog/tools/ls/symbol-table-handler.h" #include +#include #include "absl/flags/flag.h" #include "absl/time/clock.h" #include "absl/time/time.h" #include "common/lsp/lsp-file-utils.h" +#include "common/lsp/lsp-protocol.h" #include "common/strings/line_column_map.h" #include "common/util/file_util.h" #include "common/util/range.h" @@ -221,6 +223,29 @@ void SymbolTableHandler::Prepare() { } } +std::optional +SymbolTableHandler::GetTokenInfoAtTextDocumentPosition( + const verible::lsp::TextDocumentPositionParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers) { + const verilog::BufferTracker *tracker = + parsed_buffers.FindBufferTrackerOrNull(params.textDocument.uri); + if (!tracker) { + VLOG(1) << "Could not find buffer with URI " << params.textDocument.uri; + return {}; + } + std::shared_ptr parsedbuffer = tracker->current(); + if (!parsedbuffer) { + VLOG(1) << "Buffer not found among opened buffers: " + << params.textDocument.uri; + return {}; + } + const verible::LineColumn cursor{params.position.line, + params.position.character}; + const verible::TextStructureView &text = parsedbuffer->parser().Data(); + const verible::TokenInfo cursor_token = text.FindTokenAt(cursor); + return cursor_token; +} + absl::string_view SymbolTableHandler::GetTokenAtTextDocumentPosition( const verible::lsp::TextDocumentPositionParams ¶ms, const verilog::BufferTrackerContainer &parsed_buffers) { @@ -244,6 +269,30 @@ absl::string_view SymbolTableHandler::GetTokenAtTextDocumentPosition( return cursor_token.text(); } +verible::LineColumnRange +SymbolTableHandler::GetTokenRangeAtTextDocumentPosition( + const verible::lsp::TextDocumentPositionParams &document_cursor, + const verilog::BufferTrackerContainer &parsed_buffers) { + const verilog::BufferTracker *tracker = + parsed_buffers.FindBufferTrackerOrNull(document_cursor.textDocument.uri); + if (!tracker) { + VLOG(1) << "Could not find buffer with URI " + << document_cursor.textDocument.uri; + return {}; + } + std::shared_ptr parsedbuffer = tracker->current(); + if (!parsedbuffer) { + VLOG(1) << "Buffer not found among opened buffers: " + << document_cursor.textDocument.uri; + return {}; + } + const verible::LineColumn cursor{document_cursor.position.line, + document_cursor.position.character}; + const verible::TextStructureView &text = parsedbuffer->parser().Data(); + + const verible::TokenInfo cursor_token = text.FindTokenAt(cursor); + return text.GetRangeForToken(cursor_token); +} std::optional SymbolTableHandler::GetLocationFromSymbolName( absl::string_view symbol_name, const VerilogSourceFile *file_origin) { @@ -317,6 +366,79 @@ std::vector SymbolTableHandler::FindReferencesLocations( return locations; } +std::optional +SymbolTableHandler::FindRenameableRangeAtCursor( + const verible::lsp::PrepareRenameParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers) { + Prepare(); + if (files_dirty_) { + BuildProjectSymbolTable(); + } + std::optional symbol = + GetTokenInfoAtTextDocumentPosition(params, parsed_buffers); + if (symbol) { + verible::TokenInfo token = symbol.value(); + const SymbolTableNode &root = symbol_table_->Root(); + const SymbolTableNode *node = + ScanSymbolTreeForDefinition(&root, token.text()); + if (!node) return {}; + return RangeFromLineColumn( + GetTokenRangeAtTextDocumentPosition(params, parsed_buffers)); + } + return {}; +} + +verible::lsp::WorkspaceEdit +SymbolTableHandler::FindRenameLocationsAndCreateEdits( + const verible::lsp::RenameParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers) { + if (files_dirty_) { + BuildProjectSymbolTable(); + } + Prepare(); + absl::string_view symbol = + GetTokenAtTextDocumentPosition(params, parsed_buffers); + const SymbolTableNode &root = symbol_table_->Root(); + const SymbolTableNode *node = ScanSymbolTreeForDefinition(&root, symbol); + if (!node) return {}; + std::optional location = + GetLocationFromSymbolName(*node->Key(), node->Value().file_origin); + if (!location) return {}; + std::vector locations; + locations.push_back(location.value()); + std::vector textedits; + CollectReferences(&root, node, &locations); + if (locations.empty()) return {}; + std::map> + file_edit_pairs; + for (const auto &loc : locations) { + file_edit_pairs[loc.uri].reserve(locations.size()); + } + for (const auto &loc : locations) { + // TODO(jbylicki): Remove this band-aid fix once #1678 is merged - it should + // fix + // duplicate definition/references appending in modules and remove the need + // for adding the definition location above. + if (std::none_of( + file_edit_pairs[loc.uri].begin(), file_edit_pairs[loc.uri].end(), + [&loc](const verible::lsp::TextEdit &it) { + return loc.range.start.character == it.range.start.character && + loc.range.start.line == it.range.end.line; + })) { + file_edit_pairs[loc.uri].push_back(verible::lsp::TextEdit({ + .range = loc.range, + .newText = params.newName, + })); + } + } + files_dirty_ = true; + verible::lsp::WorkspaceEdit edit = verible::lsp::WorkspaceEdit{ + .changes = {}, + }; + edit.changes = file_edit_pairs; + std::cerr << "SIZE: " << edit.changes[locations[0].uri].size() << std::endl; + return edit; +} void SymbolTableHandler::CollectReferencesReferenceComponents( const ReferenceComponentNode *ref, const SymbolTableNode *ref_origin, const SymbolTableNode *definition_node, diff --git a/verilog/tools/ls/symbol-table-handler.h b/verilog/tools/ls/symbol-table-handler.h index b80889a2b..d010d171a 100644 --- a/verilog/tools/ls/symbol-table-handler.h +++ b/verilog/tools/ls/symbol-table-handler.h @@ -65,11 +65,18 @@ class SymbolTableHandler { const verible::lsp::ReferenceParams ¶ms, const verilog::BufferTrackerContainer &parsed_buffers); + std::optional FindRenameableRangeAtCursor( + const verible::lsp::PrepareRenameParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers); // Provide new parsed content for the given path. If "content" is nullptr, // opens the given file instead. void UpdateFileContent(absl::string_view path, const verible::TextStructureView *content); + verible::lsp::WorkspaceEdit FindRenameLocationsAndCreateEdits( + const verible::lsp::RenameParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers); + // Creates a symbol table for entire project (public: needed in unit-test) std::vector BuildProjectSymbolTable(); @@ -88,6 +95,20 @@ class SymbolTableHandler { const verible::lsp::TextDocumentPositionParams ¶ms, const verilog::BufferTrackerContainer &parsed_buffers); + // Returns a range in which a token exists in the file by the LSP request + // based on TextDocumentPositionParams. If text is not found, + // empty-initialized LineColumnRange is returned. + verible::LineColumnRange GetTokenRangeAtTextDocumentPosition( + const verible::lsp::TextDocumentPositionParams &document_cursor, + const verilog::BufferTrackerContainer &parsed_buffers); + + // Returns a TokenInfo of a token pointed at by the cursor in the file by the + // LSP request based on TextDocumentPositionParams. If text is not found, + // nullptr is returned. + std::optional GetTokenInfoAtTextDocumentPosition( + const verible::lsp::TextDocumentPositionParams ¶ms, + const verilog::BufferTrackerContainer &parsed_buffers); + // Returns the Location of the symbol name in source file // pointed by the file_origin. // If given symbol name is not found, std::nullopt is returned. @@ -112,9 +133,8 @@ class SymbolTableHandler { const SymbolTableNode *definition_node, std::vector *references); - // Looks for verible.filelist file down in directory structure and loads data - // to project. - // It is meant to be executed once per VerilogProject setup + // Looks for verible.filelist file down in directory structure and loads + // data to project. It is meant to be executed once per VerilogProject setup bool LoadProjectFileList(absl::string_view current_dir); // Parse all the files in the project. @@ -123,8 +143,8 @@ class SymbolTableHandler { // Path to the filelist file for the project std::string filelist_path_; - // Last timestamp of filelist file - used to check whether SymbolTable should - // be updated + // Last timestamp of filelist file - used to check whether SymbolTable + // should be updated absl::optional last_filelist_update_; // tells that symbol table should be rebuilt due to changes in files diff --git a/verilog/tools/ls/symbol-table-handler_test.cc b/verilog/tools/ls/symbol-table-handler_test.cc index 2a40026d4..cb01b8d05 100644 --- a/verilog/tools/ls/symbol-table-handler_test.cc +++ b/verilog/tools/ls/symbol-table-handler_test.cc @@ -17,6 +17,7 @@ #include #include "absl/strings/string_view.h" +#include "common/lsp/lsp-file-utils.h" #include "common/util/file_util.h" #include "gtest/gtest.h" #include "verilog/analysis/verilog_project.h" @@ -210,6 +211,322 @@ TEST(SymbolTableHandlerTest, DefinitionNotTrackedFile) { EXPECT_EQ(location.size(), 0); } +TEST(SymbolTableHandlerTest, + FindRenamableRangeAtCursorReturnsNullUntrackedFile) { + const auto tempdir = ::testing::TempDir(); + const std::string sources_dir = + verible::file::JoinPath(tempdir, __FUNCTION__); + ASSERT_TRUE(verible::file::CreateDir(sources_dir).ok()); + + absl::string_view filelist_content = "b.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + sources_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_a(sources_dir, + kSampleModuleA, "a.sv"); + const verible::file::testing::ScopedTestFile module_b(sources_dir, + kSampleModuleB, "b.sv"); + verible::lsp::PrepareRenameParams parameters; + parameters.textDocument.uri = + verible::lsp::PathToLSPUri(sources_dir + "/c.sv"); + parameters.position.line = 1; + parameters.position.character = 11; + + std::filesystem::absolute({sources_dir.begin(), sources_dir.end()}).string(); + std::shared_ptr project = std::make_shared( + sources_dir, std::vector(), ""); + SymbolTableHandler symbol_table_handler; + symbol_table_handler.SetProject(project); + + verilog::BufferTrackerContainer parsed_buffers; + parsed_buffers.AddChangeListener( + [&symbol_table_handler, parameters]( + const std::string &uri, + const verilog::BufferTracker *buffer_tracker) { + if (!buffer_tracker) { + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), nullptr); + return; + } + if (!buffer_tracker->last_good()) return; + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), + &buffer_tracker->last_good()->parser().Data()); + }); + + // Add trackers for the files we're going to process - normally done by the + // LSP but we don't have one + auto a_buffer = verible::lsp::EditTextBuffer(kSampleModuleA); + parsed_buffers.GetSubscriptionCallback()( + verible::lsp::PathToLSPUri(sources_dir + "/a.sv"), &a_buffer); + symbol_table_handler.BuildProjectSymbolTable(); + ASSERT_FALSE(parsed_buffers.FindBufferTrackerOrNull( + parameters.textDocument.uri) != nullptr); + + std::optional edit_range = + symbol_table_handler.FindRenameableRangeAtCursor(parameters, + parsed_buffers); + ASSERT_FALSE(edit_range.has_value()); +} + +TEST(SymbolTableHandlerTest, + FindRenamableRangeAtCursorReturnsNullDefinitionUnknown) { + const auto tempdir = ::testing::TempDir(); + const std::string sources_dir = + verible::file::JoinPath(tempdir, __FUNCTION__); + ASSERT_TRUE(verible::file::CreateDir(sources_dir).ok()); + + absl::string_view filelist_content = "b.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + sources_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_a(sources_dir, + kSampleModuleA, "a.sv"); + const verible::file::testing::ScopedTestFile module_b(sources_dir, + kSampleModuleB, "b.sv"); + verible::lsp::PrepareRenameParams parameters; + parameters.textDocument.uri = + verible::lsp::PathToLSPUri(sources_dir + "/b.sv"); + parameters.position.line = 3; + parameters.position.character = 5; + + std::filesystem::absolute({sources_dir.begin(), sources_dir.end()}).string(); + std::shared_ptr project = std::make_shared( + sources_dir, std::vector(), ""); + SymbolTableHandler symbol_table_handler; + symbol_table_handler.SetProject(project); + + verilog::BufferTrackerContainer parsed_buffers; + parsed_buffers.AddChangeListener( + [&symbol_table_handler, parameters]( + const std::string &uri, + const verilog::BufferTracker *buffer_tracker) { + if (!buffer_tracker) { + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), nullptr); + return; + } + if (!buffer_tracker->last_good()) return; + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), + &buffer_tracker->last_good()->parser().Data()); + }); + + // Add trackers for the files we're going to process - normally done by the + // LSP but we don't have one + auto b_buffer = verible::lsp::EditTextBuffer(kSampleModuleB); + parsed_buffers.GetSubscriptionCallback()(parameters.textDocument.uri, + &b_buffer); + symbol_table_handler.BuildProjectSymbolTable(); + ASSERT_TRUE(parsed_buffers.FindBufferTrackerOrNull( + parameters.textDocument.uri) != nullptr); + + std::optional edit_range = + symbol_table_handler.FindRenameableRangeAtCursor(parameters, + parsed_buffers); + ASSERT_FALSE(edit_range.has_value()); +} + +TEST(SymbolTableHandlerTest, FindRenamableRangeAtCursorReturnsLocation) { + const auto tempdir = ::testing::TempDir(); + const std::string sources_dir = + verible::file::JoinPath(tempdir, __FUNCTION__); + ASSERT_TRUE(verible::file::CreateDir(sources_dir).ok()); + + absl::string_view filelist_content = + "a.sv\n" + "b.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + sources_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_a(sources_dir, + kSampleModuleA, "a.sv"); + const verible::file::testing::ScopedTestFile module_b(sources_dir, + kSampleModuleB, "b.sv"); + verible::lsp::PrepareRenameParams parameters; + parameters.textDocument.uri = + verible::lsp::PathToLSPUri(sources_dir + "/a.sv"); + parameters.position.line = 1; + parameters.position.character = 11; + + std::filesystem::absolute({sources_dir.begin(), sources_dir.end()}).string(); + std::shared_ptr project = std::make_shared( + sources_dir, std::vector(), ""); + SymbolTableHandler symbol_table_handler; + symbol_table_handler.SetProject(project); + + verilog::BufferTrackerContainer parsed_buffers; + parsed_buffers.AddChangeListener( + [&symbol_table_handler, parameters]( + const std::string &uri, + const verilog::BufferTracker *buffer_tracker) { + if (!buffer_tracker) { + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), nullptr); + return; + } + if (!buffer_tracker->last_good()) return; + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), + &buffer_tracker->last_good()->parser().Data()); + }); + + // Add trackers for the files we're going to process - normally done by the + // LSP but we don't have one + auto a_buffer = verible::lsp::EditTextBuffer(kSampleModuleA); + parsed_buffers.GetSubscriptionCallback()(parameters.textDocument.uri, + &a_buffer); + symbol_table_handler.BuildProjectSymbolTable(); + ASSERT_TRUE(parsed_buffers.FindBufferTrackerOrNull( + parameters.textDocument.uri) != nullptr); + + std::optional edit_range = + symbol_table_handler.FindRenameableRangeAtCursor(parameters, + parsed_buffers); + ASSERT_TRUE(edit_range.has_value()); + EXPECT_EQ(edit_range.value().start.line, 1); + EXPECT_EQ(edit_range.value().start.character, 9); +} +TEST(SymbolTableHandlerTest, + FindRenameLocationsAndCreateEditsReturnsLocationsTest) { + const auto tempdir = ::testing::TempDir(); + const std::string sources_dir = + verible::file::JoinPath(tempdir, __FUNCTION__); + ASSERT_TRUE(verible::file::CreateDir(sources_dir).ok()); + + absl::string_view filelist_content = + "a.sv\n" + "b.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + sources_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_a(sources_dir, + kSampleModuleA, "a.sv"); + const verible::file::testing::ScopedTestFile module_b(sources_dir, + kSampleModuleB, "b.sv"); + verible::lsp::RenameParams parameters; + parameters.textDocument.uri = + verible::lsp::PathToLSPUri(sources_dir + "/a.sv"); + parameters.position.line = 1; + parameters.position.character = 11; + parameters.newName = "aaa"; + + std::filesystem::absolute({sources_dir.begin(), sources_dir.end()}).string(); + std::shared_ptr project = std::make_shared( + sources_dir, std::vector(), ""); + SymbolTableHandler symbol_table_handler; + symbol_table_handler.SetProject(project); + + verilog::BufferTrackerContainer parsed_buffers; + parsed_buffers.AddChangeListener( + [&symbol_table_handler, parameters]( + const std::string &uri, + const verilog::BufferTracker *buffer_tracker) { + if (!buffer_tracker) { + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), nullptr); + return; + } + if (!buffer_tracker->last_good()) return; + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), + &buffer_tracker->last_good()->parser().Data()); + }); + + // Add trackers for the files we're going to process - normally done by the + // LSP but we don't have one + auto a_buffer = verible::lsp::EditTextBuffer(kSampleModuleA); + parsed_buffers.GetSubscriptionCallback()(parameters.textDocument.uri, + &a_buffer); + symbol_table_handler.BuildProjectSymbolTable(); + ASSERT_TRUE(parsed_buffers.FindBufferTrackerOrNull( + parameters.textDocument.uri) != nullptr); + + verible::lsp::WorkspaceEdit edit_range = + symbol_table_handler.FindRenameLocationsAndCreateEdits(parameters, + parsed_buffers); + EXPECT_EQ(edit_range.changes[parameters.textDocument.uri].size(), 2); + EXPECT_EQ( + edit_range.changes[verible::lsp::PathToLSPUri(sources_dir + "/b.sv")] + .size(), + 1); +} + +TEST(SymbolTableHandlerTest, + FindRenameLocationsAndCreateEditsReturnsLocationsOnDirtyFilesTest) { + const auto tempdir = ::testing::TempDir(); + const std::string sources_dir = + verible::file::JoinPath(tempdir, __FUNCTION__); + ASSERT_TRUE(verible::file::CreateDir(sources_dir).ok()); + + absl::string_view filelist_content = + "a.sv\n" + "b.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + sources_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_a(sources_dir, + kSampleModuleA, "a.sv"); + const verible::file::testing::ScopedTestFile module_b(sources_dir, + kSampleModuleB, "b.sv"); + verible::lsp::RenameParams parameters; + parameters.textDocument.uri = + verible::lsp::PathToLSPUri(sources_dir + "/a.sv"); + parameters.position.line = 1; + parameters.position.character = 11; + parameters.newName = "aaa"; + + std::filesystem::absolute({sources_dir.begin(), sources_dir.end()}).string(); + std::shared_ptr project = std::make_shared( + sources_dir, std::vector(), ""); + SymbolTableHandler symbol_table_handler; + symbol_table_handler.SetProject(project); + + verilog::BufferTrackerContainer parsed_buffers; + parsed_buffers.AddChangeListener( + [&symbol_table_handler, parameters]( + const std::string &uri, + const verilog::BufferTracker *buffer_tracker) { + if (!buffer_tracker) { + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), nullptr); + return; + } + if (!buffer_tracker->last_good()) return; + symbol_table_handler.UpdateFileContent( + verible::lsp::LSPUriToPath(parameters.textDocument.uri), + &buffer_tracker->last_good()->parser().Data()); + }); + + // Add trackers for the files we're going to process - normally done by the + // LSP but we don't have one + auto a_buffer = verible::lsp::EditTextBuffer(kSampleModuleA); + parsed_buffers.GetSubscriptionCallback()(parameters.textDocument.uri, + &a_buffer); + symbol_table_handler.BuildProjectSymbolTable(); + ASSERT_TRUE(parsed_buffers.FindBufferTrackerOrNull( + parameters.textDocument.uri) != nullptr); + + verible::lsp::WorkspaceEdit edit_range = + symbol_table_handler.FindRenameLocationsAndCreateEdits(parameters, + parsed_buffers); + EXPECT_EQ(edit_range.changes[parameters.textDocument.uri].size(), 2); + EXPECT_EQ( + edit_range.changes[verible::lsp::PathToLSPUri(sources_dir + "/b.sv")] + .size(), + 1); + + parameters.newName = "bbb"; + + edit_range = symbol_table_handler.FindRenameLocationsAndCreateEdits( + parameters, parsed_buffers); + EXPECT_EQ(edit_range.changes[parameters.textDocument.uri].size(), 2); + EXPECT_EQ( + edit_range.changes[verible::lsp::PathToLSPUri(sources_dir + "/b.sv")] + .size(), + 1); +} + TEST(SymbolTableHandlerTest, MissingVerilogProject) { SymbolTableHandler symbol_table_handler; std::vector diagnostics = diff --git a/verilog/tools/ls/verilog-language-server.cc b/verilog/tools/ls/verilog-language-server.cc index 0af2d0f08..d725de192 100644 --- a/verilog/tools/ls/verilog-language-server.cc +++ b/verilog/tools/ls/verilog-language-server.cc @@ -69,6 +69,7 @@ verible::lsp::InitializeResult VerilogLanguageServer::GetCapabilities() { {"documentHighlightProvider", true}, // Highlight same symbol {"definitionProvider", true}, // Provide going to definition {"referencesProvider", true}, // Provide going to references + {"renameProvider", true}, // Provide symbol renaming {"diagnosticProvider", // Pull model of diagnostics. { {"interFileDependencies", false}, @@ -138,6 +139,19 @@ void VerilogLanguageServer::SetRequestHandlers() { return symbol_table_handler_.FindReferencesLocations(p, parsed_buffers_); }); + dispatcher_.AddRequestHandler( + "textDocument/prepareRename", + [this](const verible::lsp::PrepareRenameParams &p) -> nlohmann::json { + auto range = symbol_table_handler_.FindRenameableRangeAtCursor( + p, parsed_buffers_); + if (range.has_value()) return range.value(); + return nullptr; + }); + dispatcher_.AddRequestHandler( + "textDocument/rename", [this](const verible::lsp::RenameParams &p) { + return symbol_table_handler_.FindRenameLocationsAndCreateEdits( + p, parsed_buffers_); + }); // The client sends a request to shut down. Use that to exit our loop. dispatcher_.AddRequestHandler("shutdown", [this](const nlohmann::json &) { shutdown_requested_ = true; diff --git a/verilog/tools/ls/verilog-language-server_test.cc b/verilog/tools/ls/verilog-language-server_test.cc index 39a77bd26..24efe8271 100644 --- a/verilog/tools/ls/verilog-language-server_test.cc +++ b/verilog/tools/ls/verilog-language-server_test.cc @@ -1459,6 +1459,242 @@ TEST_F(VerilogLanguageServerSymbolTableTest, CheckReferenceUnknownSymbol) { ASSERT_EQ(response_b["result"].size(), 0); } +std::string RenameRequest(verible::lsp::RenameParams params) { + json request = {{"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/rename"}, + {"params", params}}; + return request.dump(); +} +std::string PrepareRenameRequest(verible::lsp::PrepareRenameParams params) { + json request = {{"jsonrpc", "2.0"}, + {"id", 2}, + {"method", "textDocument/prepareRename"}, + {"params", params}}; + return request.dump(); +} +// Runs tests for textDocument/rangeFormatting requests +TEST_F(VerilogLanguageServerSymbolTableTest, + PrepareRenameReturnsRangeOfEditableSymbol) { + // Create sample file and make sure diagnostics do not have errors + std::string file_uri = PathToLSPUri(absl::string_view(root_dir + "/fmt.sv")); + verible::lsp::PrepareRenameParams params; + params.position.line = 2; + params.position.character = 1; + params.textDocument.uri = file_uri; + + const std::string mini_module = + DidOpenRequest(file_uri, + "module fmt();\nfunction automatic " + "bar();\nbar();\nbar();\nendfunction;\nendmodule\n"); + ASSERT_OK(SendRequest(mini_module)); + + const json diagnostics = json::parse(GetResponse()); + EXPECT_EQ(diagnostics["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + EXPECT_EQ(diagnostics["params"]["uri"], file_uri) + << "Diagnostics for invalid file"; + + EXPECT_EQ(diagnostics["params"]["diagnostics"].size(), 0) + << "The test file has errors"; + ASSERT_OK(SendRequest(PrepareRenameRequest(params))); + + const json response = json::parse(GetResponse()); + EXPECT_EQ(response["result"]["start"]["line"], 2) + << "Invalid result for id: "; + EXPECT_EQ(response["result"]["start"]["character"], 0) + << "Invalid result for id: "; + EXPECT_EQ(response["result"]["end"]["line"], 2) << "Invalid result for id: "; + EXPECT_EQ(response["result"]["end"]["character"], 3) + << "Invalid result for id: "; +} + +TEST_F(VerilogLanguageServerSymbolTableTest, PrepareRenameReturnsNull) { + // Create sample file and make sure diagnostics do not have errors + std::string file_uri = PathToLSPUri(absl::string_view(root_dir + "/fmt.sv")); + verible::lsp::PrepareRenameParams params; + params.position.line = 1; + params.position.character = 1; + params.textDocument.uri = file_uri; + + const std::string mini_module = + DidOpenRequest(file_uri, + "module fmt();\nfunction automatic " + "bar();\nbar();\nbar();\nendfunction;\nendmodule\n"); + ASSERT_OK(SendRequest(mini_module)); + + const json diagnostics = json::parse(GetResponse()); + EXPECT_EQ(diagnostics["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + EXPECT_EQ(diagnostics["params"]["uri"], file_uri) + << "Diagnostics for invalid file"; + + EXPECT_EQ(diagnostics["params"]["diagnostics"].size(), 0) + << "The test file has errors"; + ASSERT_OK(SendRequest(PrepareRenameRequest(params))); + + const json response = json::parse(GetResponse()); + EXPECT_EQ(response["result"], nullptr) << "Invalid result for id: "; +} + +TEST_F(VerilogLanguageServerSymbolTableTest, RenameTestSymbolSingleFile) { + // Create sample file and make sure diagnostics do not have errors + std::string file_uri = + PathToLSPUri(absl::string_view(root_dir + "/rename.sv")); + verible::lsp::RenameParams params; + params.position.line = 2; + params.position.character = 1; + params.textDocument.uri = file_uri; + params.newName = "foo"; + + absl::string_view filelist_content = "rename.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + root_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_foo( + root_dir, + "module rename();\nfunction automatic " + "bar();\nbar();\nbar();\nendfunction;\nendmodule\n", + "rename.sv"); + + const std::string mini_module = + DidOpenRequest(file_uri, + "module rename();\nfunction automatic " + "bar();\nbar();\nbar();\nendfunction;\nendmodule\n"); + + ASSERT_OK(SendRequest(mini_module)); + + const json diagnostics = json::parse(GetResponse()); + EXPECT_EQ(diagnostics["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + + EXPECT_EQ(diagnostics["params"]["uri"], + PathToLSPUri(verible::lsp::LSPUriToPath(file_uri))) + << "Diagnostics for invalid file"; + EXPECT_EQ(diagnostics["params"]["diagnostics"].size(), 0) + << "The test file has errors"; + std::string request = RenameRequest(params); + ASSERT_OK(SendRequest(request)); + + const json response = json::parse(GetResponse()); + EXPECT_EQ(response["result"]["changes"].size(), 1) + << "Invalid result size for id: "; + EXPECT_EQ(response["result"]["changes"][file_uri].size(), 3) + << "Invalid result size for id: "; +} + +TEST_F(VerilogLanguageServerSymbolTableTest, RenameTestSymbolMultipleFiles) { + // Create sample file and make sure diagnostics do not have errors + std::string top_uri = PathToLSPUri(absl::string_view(root_dir + "/top.sv")); + std::string foo_uri = PathToLSPUri(absl::string_view(root_dir + "/foo.sv")); + verible::lsp::RenameParams params; + params.position.line = 2; + params.position.character = 9; + params.textDocument.uri = top_uri; + params.newName = "foobaz"; + std::string foosv = + "package foo;\n" + " class foobar;\n" + " endclass;\n" + "endpackage;\n"; + std::string topsv = + "import foo::*;\n" + "module top;\n" + " foo::foobar bar;\n" + "endmodule;\n"; + absl::string_view filelist_content = "./foo.sv\n./top.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + root_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_foo(root_dir, foosv, + "foo.sv"); + + const verible::file::testing::ScopedTestFile module_top(root_dir, topsv, + "top.sv"); + const std::string top_request = DidOpenRequest(top_uri, topsv); + ASSERT_OK(SendRequest(top_request)); + + const json diagnostics = json::parse(GetResponse()); + EXPECT_EQ(diagnostics["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + EXPECT_EQ(diagnostics["params"]["uri"], + PathToLSPUri(verible::lsp::LSPUriToPath(top_uri))) + << "Diagnostics for invalid file"; + + const std::string foo_request = DidOpenRequest(foo_uri, foosv); + ASSERT_OK(SendRequest(foo_request)); + + const json diagnostics_foo = json::parse(GetResponse()); + EXPECT_EQ(diagnostics_foo["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + EXPECT_EQ(diagnostics_foo["params"]["uri"], + PathToLSPUri(verible::lsp::LSPUriToPath(foo_uri))) + << "Diagnostics for invalid file"; + + // Complaints about package and file names + EXPECT_EQ(diagnostics["params"]["diagnostics"].size(), 0) + << "The test file has errors"; + std::string request = RenameRequest(params); + ASSERT_OK(SendRequest(request)); + + const json response = json::parse(GetResponse()); + EXPECT_EQ(response["result"]["changes"].size(), 2) + << "Invalid result size for id: "; + EXPECT_EQ(response["result"]["changes"][top_uri].size(), 1) + << "Invalid result size for id: "; + EXPECT_EQ(response["result"]["changes"][foo_uri].size(), 1) + << "Invalid result size for id: "; +} + +TEST_F(VerilogLanguageServerSymbolTableTest, RenameTestPackageDistinction) { + // Create sample file and make sure diagnostics do not have errors + std::string file_uri = + PathToLSPUri(absl::string_view(root_dir + "/rename.sv")); + verible::lsp::RenameParams params; + params.position.line = 7; + params.position.character = 15; + params.textDocument.uri = file_uri; + params.newName = "foobaz"; + std::string renamesv = + "package foo;\n" + " class foobar;\n" + " bar::foobar baz;\n" + " endclass;\n" + "endpackage;\n" + "package bar;\n" + " class foobar;\n" + " foo::foobar baz;\n" + " endclass;\n" + "endpackage;\n"; + absl::string_view filelist_content = "rename.sv\n"; + + const verible::file::testing::ScopedTestFile filelist( + root_dir, filelist_content, "verible.filelist"); + const verible::file::testing::ScopedTestFile module_foo(root_dir, renamesv, + "rename.sv"); + + const std::string mini_module = DidOpenRequest(file_uri, renamesv); + ASSERT_OK(SendRequest(mini_module)); + + const json diagnostics = json::parse(GetResponse()); + EXPECT_EQ(diagnostics["method"], "textDocument/publishDiagnostics") + << "textDocument/publishDiagnostics not received"; + EXPECT_EQ(diagnostics["params"]["uri"], + PathToLSPUri(verible::lsp::LSPUriToPath(file_uri))) + << "Diagnostics for invalid file"; + + // Complaints about package and file names + EXPECT_EQ(diagnostics["params"]["diagnostics"].size(), 2) + << "The test file has errors"; + std::string request = RenameRequest(params); + ASSERT_OK(SendRequest(request)); + + const json response = json::parse(GetResponse()); + EXPECT_EQ(response["result"]["changes"].size(), 1) + << "Invalid result size for id: "; + EXPECT_EQ(response["result"]["changes"][file_uri].size(), 2) + << "Invalid result size for id: "; +} // Tests correctness of Language Server shutdown request TEST_F(VerilogLanguageServerTest, ShutdownTest) { const absl::string_view shutdown_request =