| // Copyright 2021 The Verible Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "common/lsp/lsp-text-buffer.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "common/lsp/json-rpc-dispatcher.h" | |
| #include "gtest/gtest.h" | |
| namespace verible { | |
| namespace lsp { | |
| TEST(TextBufferTest, RecreateEmptyFile) { | |
| EditTextBuffer buffer(""); | |
| EXPECT_EQ(buffer.lines(), 0); | |
| EXPECT_EQ(buffer.document_length(), 0); | |
| buffer.RequestContent([](absl::string_view s) { // | |
| EXPECT_TRUE(s.empty()); | |
| }); | |
| } | |
| TEST(TextBufferTest, RequestParticularLine) { | |
| EditTextBuffer buffer("foo\nbar\nbaz\n"); | |
| ASSERT_EQ(buffer.lines(), 3); | |
| buffer.RequestLine(0, [](absl::string_view s) { // | |
| EXPECT_EQ(std::string(s), "foo\n"); | |
| }); | |
| buffer.RequestLine(1, [](absl::string_view s) { // | |
| EXPECT_EQ(std::string(s), "bar\n"); | |
| }); | |
| // Be graceful with out-of-range requests. | |
| buffer.RequestLine(-1, [](absl::string_view s) { // | |
| EXPECT_TRUE(s.empty()); | |
| }); | |
| buffer.RequestLine(100, [](absl::string_view s) { // | |
| EXPECT_TRUE(s.empty()); | |
| }); | |
| } | |
| TEST(TextBufferTest, RecreateFileWithAndWithoutNewlineAtEOF) { | |
| static constexpr absl::string_view kBaseFile = | |
| "Hello World\n" | |
| "\n" | |
| "Foo"; | |
| for (const absl::string_view append : {"", "\n", "\r\n"}) { | |
| const std::string &content = absl::StrCat(kBaseFile, append); | |
| EditTextBuffer buffer(content); | |
| EXPECT_EQ(buffer.lines(), 3); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ(std::string(s), content); | |
| }); | |
| } | |
| } | |
| TEST(TextBufferTest, RecreateCRLFFiles) { | |
| EditTextBuffer buffer("Foo\r\nBar\r\n"); | |
| EXPECT_EQ(buffer.lines(), 2); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Foo\r\nBar\r\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplyFullContent) { | |
| EditTextBuffer buffer("Foo\nBar\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = {}, | |
| .has_range = false, | |
| .text = "NewFile", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("NewFile", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_Insert) { | |
| EditTextBuffer buffer("Hello World"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, | |
| .end = {0, 6}, | |
| }, | |
| .has_range = true, | |
| .text = "brave ", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.document_length(), 17); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hello brave World", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLineWithUTF8Characters_Insert) { | |
| EditTextBuffer buffer("Hëllö World"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, | |
| .end = {0, 6}, | |
| }, | |
| .has_range = true, | |
| .text = "brave ", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.document_length(), 19); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hëllö brave World", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_InsertFromEmptyFile) { | |
| EditTextBuffer buffer(""); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 0}, | |
| .end = {0, 0}, | |
| }, | |
| .has_range = true, | |
| .text = "New File!", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("New File!", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_Replace) { | |
| EditTextBuffer buffer("Hello World\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, | |
| .end = {0, 11}, | |
| }, | |
| .has_range = true, | |
| .text = "Planet", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hello Planet\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLineWihtUTF8Characters_Replace) { | |
| EditTextBuffer buffer("Heizölrückstoßabdämpfung\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, | |
| .end = {0, 14}, | |
| }, | |
| .has_range = true, | |
| .text = "brandgefahr", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Heizölbrandgefahrabdämpfung\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_ReplaceNotFirstLine) { | |
| // Make sure we properly access the right line. | |
| EditTextBuffer buffer("Hello World\nFoo\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {1, 0}, | |
| .end = {1, 3}, | |
| }, | |
| .has_range = true, | |
| .text = "Bar", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hello World\nBar\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_Erase) { | |
| EditTextBuffer buffer("Hello World\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 5}, | |
| .end = {0, 11}, | |
| }, | |
| .has_range = true, | |
| .text = "", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.document_length(), 6); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("Hello\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplySingleLine_ReplaceCorrectOverlongEnd) { | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, .end = {0, 42}, // Too long end shall be trimmed | |
| }, | |
| .has_range = true, | |
| .text = "Planet", | |
| }; | |
| { | |
| EditTextBuffer buffer("Hello World\n"); | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hello Planet\n", std::string(s)); | |
| }); | |
| } | |
| { | |
| EditTextBuffer buffer("Hello World"); | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("Hello Planet", std::string(s)); | |
| }); | |
| } | |
| } | |
| TEST(TextBufferTest, ChangeApplyMultiLine_EraseBetweenLines) { | |
| EditTextBuffer buffer("Hello\nWorld\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 2}, // From here to end of line | |
| .end = {1, 0}, | |
| }, | |
| .has_range = true, | |
| .text = "y ", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("Hey World\n", std::string(s)); | |
| }); | |
| EXPECT_EQ(buffer.document_length(), 10); | |
| } | |
| TEST(TextBufferTest, ChangeApplyMultiLineWithUTF8Characters_Modify) { | |
| EditTextBuffer buffer("Heizölrück-\nstoßabdämpfung\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 6}, // From here to end of line | |
| .end = {1, 4}, | |
| }, | |
| .has_range = true, | |
| .text = "brand-\ngefahr", | |
| }; | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("Heizölbrand-\ngefahrabdämpfung\n", std::string(s)); | |
| }); | |
| } | |
| TEST(TextBufferTest, ChangeApplyMultiLine_InsertMoreLines) { | |
| EditTextBuffer buffer("Hello\nbrave World\n"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 2}, // From here to end of line | |
| .end = {1, 5}, | |
| }, | |
| .has_range = true, | |
| .text = "y!\nThis will be a new line\nand more in this", | |
| }; | |
| EXPECT_EQ(buffer.lines(), 2); | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.lines(), 3); | |
| static constexpr absl::string_view kExpected = | |
| "Hey!\nThis will be a new line\nand more in this World\n"; | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ(kExpected, std::string(s)); | |
| }); | |
| EXPECT_EQ(buffer.document_length(), kExpected.length()); | |
| } | |
| TEST(TextBufferTest, ChangeApplyMultiLine_InsertFromStart) { | |
| EditTextBuffer buffer(""); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {0, 0}, | |
| .end = {0, 0}, | |
| }, | |
| .has_range = true, | |
| .text = "This is now\na multiline\nfile\n", | |
| }; | |
| EXPECT_EQ(buffer.lines(), 0); | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.lines(), 3); | |
| buffer.RequestContent([&](absl::string_view s) { | |
| EXPECT_EQ("This is now\na multiline\nfile\n", std::string(s)); | |
| }); | |
| EXPECT_EQ(buffer.document_length(), change.text.length()); | |
| } | |
| TEST(TextBufferTest, ChangeApplyMultiLine_RemoveLines) { | |
| EditTextBuffer buffer("Foo\nBar\nBaz\nQuux"); | |
| const TextDocumentContentChangeEvent change = { | |
| .range = | |
| { | |
| .start = {1, 0}, | |
| .end = {3, 0}, | |
| }, | |
| .has_range = true, | |
| .text = "", | |
| }; | |
| EXPECT_EQ(buffer.lines(), 4); | |
| EXPECT_TRUE(buffer.ApplyChange(change)); | |
| EXPECT_EQ(buffer.lines(), 2); | |
| buffer.RequestContent([&](absl::string_view s) { // | |
| EXPECT_EQ("Foo\nQuux", std::string(s)); | |
| }); | |
| EXPECT_EQ(buffer.document_length(), 8); | |
| } | |
| TEST(BufferCollection, SimulateDocumentLifecycleThroughRPC) { | |
| // Let's walk a BufferCollection through the lifecycle of a document | |
| // by sending it the JSON RPC notifications for open, change and close. | |
| // | |
| // Inspect at each step that the content and number of documents is what | |
| // we expect. | |
| // Notifications don't send any responses; write function not relevant. | |
| JsonRpcDispatcher rpc_dispatcher([](absl::string_view) {}); | |
| BufferCollection collection(&rpc_dispatcher); | |
| EXPECT_EQ(collection.documents_open(), 0); | |
| int64_t last_global_version = 0; | |
| // Let's check that the callback is called on opening and contains | |
| // the right content. | |
| int change_callback_called = 0; | |
| collection.SetChangeListener( | |
| [&](const std::string &uri, const EditTextBuffer *buffer) { | |
| EXPECT_EQ(uri, "file:///foo.cc"); | |
| buffer->RequestContent([](absl::string_view s) { | |
| EXPECT_EQ(std::string(s), "Hello\nworld"); | |
| }); | |
| change_callback_called++; | |
| }); | |
| // ------ Opening new document | |
| // Simulate an incoming event from the client | |
| rpc_dispatcher.DispatchMessage(R"({ | |
| "jsonrpc":"2.0", | |
| "method":"textDocument/didOpen", | |
| "params":{ | |
| "textDocument":{ | |
| "uri": "file:///foo.cc", | |
| "text": "Hello\nworld", | |
| "languageId": "cpp", | |
| "version": 1 | |
| } | |
| }})"); | |
| // We now expect one document to be open. | |
| EXPECT_EQ(collection.documents_open(), 1); | |
| EXPECT_EQ(change_callback_called, 1); | |
| EXPECT_GT(collection.global_version(), last_global_version); | |
| // Prepare for the next expected change | |
| collection.SetChangeListener( | |
| [&](const std::string &uri, const EditTextBuffer *buffer) { | |
| EXPECT_EQ(uri, "file:///foo.cc"); | |
| buffer->RequestContent( | |
| [](absl::string_view s) { EXPECT_EQ(std::string(s), "Hey"); }); | |
| change_callback_called++; | |
| }); | |
| // ------ Editing document: receive content changes | |
| rpc_dispatcher.DispatchMessage(R"({ | |
| "jsonrpc":"2.0", | |
| "method":"textDocument/didChange", | |
| "params":{ | |
| "textDocument": { "uri": "file:///foo.cc" }, | |
| "contentChanges": [ {"text":"Hey"} ] | |
| }})"); | |
| EXPECT_EQ(change_callback_called, 2); | |
| // Upcoming change will send us a nullptr buffer as it is removed. | |
| collection.SetChangeListener( | |
| [&](const std::string &uri, const EditTextBuffer *buffer) { | |
| EXPECT_EQ(buffer, nullptr); | |
| change_callback_called++; | |
| }); | |
| // ------ Closing document | |
| rpc_dispatcher.DispatchMessage(R"({ | |
| "jsonrpc":"2.0", | |
| "method":"textDocument/didClose", | |
| "params":{ | |
| "textDocument":{ "uri": "file:///foo.cc" } | |
| }})"); | |
| // No document open anymore | |
| EXPECT_EQ(collection.documents_open(), 0); | |
| EXPECT_EQ(change_callback_called, 3); | |
| } | |
| } // namespace lsp | |
| } // namespace verible |