From 7d2958ad37571d2a76deb5554daa8672cea518de Mon Sep 17 00:00:00 2001 From: Jon Ross-Perkins Date: Wed, 29 Jan 2025 13:31:10 -0800 Subject: [PATCH] Broaden file_test support for LSP requests (#4854) Adds `@LSP-NOTIFY` and `@LSP-REPLY` to capture the slightly different formats versus a typical call. Removes special-casing for `exit`. --- testing/file_test/README.md | 6 +- testing/file_test/file_test_base.cpp | 119 ++++++++++-------- testing/file_test/testdata/lsp_calls.carbon | 12 +- .../language_server/testdata/exit.carbon | 2 +- .../testdata/initialize.carbon | 2 +- 5 files changed, 85 insertions(+), 56 deletions(-) diff --git a/testing/file_test/README.md b/testing/file_test/README.md index f1231d21ed42..5e806572698e 100644 --- a/testing/file_test/README.md +++ b/testing/file_test/README.md @@ -88,9 +88,13 @@ Some keywords can be inserted for content: - ``` [[@LSP::]] + [[@LSP-NOTIFY::]] + [[@LSP-REPLY::]] ``` - Produces JSON for an LSP method call, complete with `Content-Length` header. + Produces JSON for an LSP method call, notification, or reply. Each includes + the `Content-Length` header. The `:` is optional, and may be + omitted. - ``` [[@TEST_NAME]] diff --git a/testing/file_test/file_test_base.cpp b/testing/file_test/file_test_base.cpp index fe589227c2ef..99d254bb45d6 100644 --- a/testing/file_test/file_test_base.cpp +++ b/testing/file_test/file_test_base.cpp @@ -517,10 +517,62 @@ struct SplitState { int file_index = 0; }; +// Reformats `[[@LSP:` and `[[LSP-NOTIFY:` as an LSP call with headers. For +// notifications, `lsp_call_id` is null. +static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos, + int* lsp_call_id, llvm::StringLiteral keyword, + llvm::StringLiteral method_or_id_label) + -> ErrorOr { + auto method_or_id_start = keyword_pos + keyword.size(); + + static constexpr llvm::StringLiteral LspEnd = "]]"; + auto keyword_end = content->find("]]", method_or_id_start); + if (keyword_end == std::string::npos) { + return ErrorBuilder() << "Missing `" << LspEnd << "` after `" << keyword + << "`"; + } + + auto method_or_id_end = content->find(":", method_or_id_start); + auto extra_content_start = method_or_id_end + 1; + if (method_or_id_end == std::string::npos || method_or_id_end > keyword_end) { + method_or_id_end = keyword_end; + extra_content_start = keyword_end; + } + auto method_or_id = + llvm::StringRef(*content).slice(method_or_id_start, method_or_id_end); + + auto extra_content = + llvm::StringRef(*content).slice(extra_content_start, keyword_end); + std::string extra_content_sep; + if (!extra_content.empty()) { + extra_content_sep = ","; + if (!extra_content.starts_with("\n")) { + extra_content_sep += " "; + } + } + + // Form the JSON. + std::string json = R"({"jsonrpc": "2.0", )"; + if (lsp_call_id) { + json += llvm::formatv(R"("id": "{0}", )", ++(*lsp_call_id)); + } + json += llvm::formatv(R"("{0}": "{1}"{2}{3}})", method_or_id_label, + method_or_id, extra_content_sep, extra_content); + + // Add the Content-Length header. The `2` accounts for extra newlines. + auto json_with_header = + llvm::formatv("Content-Length: {0}\n\n{1}\n", json.size() + 2, json) + .str(); + // Insert the content. + content->replace(keyword_pos, keyword_end + 2 - keyword_pos, + json_with_header); + return keyword_pos + json_with_header.size(); +} + // Replaces the keyword at the given position. Returns the position to start a // find for the next keyword. static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos, - llvm::StringRef test_name, int* lsp_id) + llvm::StringRef test_name, int* lsp_call_id) -> ErrorOr { auto keyword = llvm::StringRef(*content).substr(keyword_pos); @@ -538,57 +590,20 @@ static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos, return keyword_pos + test_name.size(); } - // Reformatted as an LSP call with headers. static constexpr llvm::StringLiteral Lsp = "[[@LSP:"; if (keyword.starts_with(Lsp)) { - auto method_start = keyword_pos + Lsp.size(); - - static constexpr llvm::StringLiteral LspEnd = "]]"; - auto keyword_end = content->find("]]", method_start); - if (keyword_end == std::string::npos) { - return ErrorBuilder() - << "Missing `" << LspEnd << "` after `" << Lsp << "`"; - } - - auto method_end = content->find(":", method_start); - auto extra_content_start = method_end + 1; - if (method_end == std::string::npos || method_end > keyword_end) { - method_end = keyword_end; - extra_content_start = keyword_end; - } - auto method = content->substr(method_start, method_end - method_start); - - auto extra_content = - content->substr(extra_content_start, keyword_end - extra_content_start); - std::string extra_content_sep; - if (!extra_content.empty()) { - extra_content_sep = ","; - if (!extra_content.starts_with("\n")) { - extra_content_sep += " "; - } - } - - // Form the JSON. - std::string json; - if (method == "exit") { - if (!extra_content.empty()) { - return Error("`[[@LSP:exit:` cannot include extra content"); - } - json = R"({"jsonrpc": "2.0", "method": "exit"})"; - } else { - json = llvm::formatv( - R"({{"jsonrpc": "2.0", "id": "{0}", "method": "{1}"{2}{3}})", - ++(*lsp_id), method, extra_content_sep, extra_content) - .str(); - } - // Add the Content-Length header. The `2` accounts for extra newlines. - auto json_with_header = - llvm::formatv("Content-Length: {0}\n\n{1}\n", json.size() + 2, json) - .str(); - // Insert the content. - content->replace(keyword_pos, keyword_end + 2 - keyword_pos, - json_with_header); - return keyword_pos + json_with_header.size(); + return ReplaceLspKeywordAt(content, keyword_pos, lsp_call_id, Lsp, + "method"); + } + static constexpr llvm::StringLiteral LspNotify = "[[@LSP-NOTIFY:"; + if (keyword.starts_with(LspNotify)) { + return ReplaceLspKeywordAt(content, keyword_pos, + /*lsp_call_id=*/nullptr, LspNotify, "method"); + } + static constexpr llvm::StringLiteral LspReply = "[[@LSP-REPLY:"; + if (keyword.starts_with(LspReply)) { + return ReplaceLspKeywordAt(content, keyword_pos, + /*lsp_call_id=*/nullptr, LspReply, "id"); } return ErrorBuilder() << "Unexpected use of `[[@` at `" @@ -625,11 +640,11 @@ static auto ReplaceContentKeywords(llvm::StringRef filename, test_name.consume_front("todo_"); // A counter for LSP calls. - int lsp_id = 0; + int lsp_call_id = 0; while (keyword_pos != std::string::npos) { CARBON_ASSIGN_OR_RETURN( auto keyword_end, - ReplaceContentKeywordAt(content, keyword_pos, test_name, &lsp_id)); + ReplaceContentKeywordAt(content, keyword_pos, test_name, &lsp_call_id)); keyword_pos = content->find(Prefix, keyword_end); } return Success(); diff --git a/testing/file_test/testdata/lsp_calls.carbon b/testing/file_test/testdata/lsp_calls.carbon index d3bb7b80e3bf..8edcd223e0e9 100644 --- a/testing/file_test/testdata/lsp_calls.carbon +++ b/testing/file_test/testdata/lsp_calls.carbon @@ -16,7 +16,9 @@ multi line ]] -[[@LSP:exit]] +[[@LSP-REPLY:7]] +[[@LSP-REPLY:8:bar]] +[[@LSP-NOTIFY:exit]] // --- AUTOUPDATE-SPLIT @@ -40,6 +42,14 @@ line // CHECK:STDERR: line // CHECK:STDERR: } // CHECK:STDERR: +// CHECK:STDERR: Content-Length: 31 +// CHECK:STDERR: +// CHECK:STDERR: {"jsonrpc": "2.0", "id": "7"} +// CHECK:STDERR: +// CHECK:STDERR: Content-Length: 36 +// CHECK:STDERR: +// CHECK:STDERR: {"jsonrpc": "2.0", "id": "8", bar} +// CHECK:STDERR: // CHECK:STDERR: Content-Length: 38 // CHECK:STDERR: // CHECK:STDERR: {"jsonrpc": "2.0", "method": "exit"} diff --git a/toolchain/language_server/testdata/exit.carbon b/toolchain/language_server/testdata/exit.carbon index c35774965b28..164ed5dd54ba 100644 --- a/toolchain/language_server/testdata/exit.carbon +++ b/toolchain/language_server/testdata/exit.carbon @@ -9,7 +9,7 @@ // TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/exit.carbon // --- STDIN -[[@LSP:exit]] +[[@LSP-NOTIFY:exit]] // --- AUTOUPDATE-SPLIT diff --git a/toolchain/language_server/testdata/initialize.carbon b/toolchain/language_server/testdata/initialize.carbon index 7c4afb344995..d344234b59c9 100644 --- a/toolchain/language_server/testdata/initialize.carbon +++ b/toolchain/language_server/testdata/initialize.carbon @@ -10,7 +10,7 @@ // --- STDIN [[@LSP:initialize]] -[[@LSP:exit]] +[[@LSP-NOTIFY:exit]] // --- AUTOUPDATE-SPLIT