Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow LSP test_file to autofill didOpen params from previous splits #5078

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 100 additions & 30 deletions testing/file_test/test_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

#include <fstream>

#include "common/check.h"
#include "common/error.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/JSON.h"
#include "testing/base/file_helpers.h"

namespace Carbon::Testing {
Expand Down Expand Up @@ -93,9 +96,47 @@ struct SplitState {
int file_index = 0;
};

static auto ExtractFilePathFromUri(llvm::StringRef uri) -> llvm::StringRef {
static constexpr llvm::StringRef FilePrefix = "file:/";
if (uri.starts_with(FilePrefix)) {
return uri.drop_front(FilePrefix.size());
}
return uri;
}

static auto AutoFillDidOpenParams(llvm::json::Object* params,
llvm::ArrayRef<TestFile::Split> splits)
-> ErrorOr<Success> {
auto* text_document = params->getObject("textDocument");
if (text_document == nullptr) {
return Success{};
}

auto attr_it = text_document->find("text");
if (attr_it == text_document->end() || attr_it->second != "AUTOFILL") {
return Success{};
}

auto uri = text_document->getString("uri");
CARBON_CHECK(uri.has_value());

auto file_path = ExtractFilePathFromUri(*uri);
const auto* split_it =
llvm::find_if(splits, [&](const TestFile::Split& split) {
return split.filename == file_path;
});
if (split_it == splits.end()) {
return ErrorBuilder() << "No split found for uri: " << *uri;
}
attr_it->second = split_it->content;
return Success{};
}

// Reformats `[[@LSP:` and similar keyword as an LSP call with headers.
static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
int& lsp_call_id) -> ErrorOr<size_t> {
int& lsp_call_id,
llvm::ArrayRef<TestFile::Split> splits)
-> ErrorOr<size_t> {
llvm::StringRef content_at_keyword =
llvm::StringRef(*content).substr(keyword_pos);

Expand Down Expand Up @@ -133,30 +174,52 @@ static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
llvm::StringRef body = body_start.take_front(body_end);
auto [method_or_id, extra_content] = body.split(":");

// Form the JSON.
std::string json = llvm::formatv(R"({{"jsonrpc": "2.0", "{0}": "{1}")",
method_or_id_label, method_or_id);
if (use_call_id) {
// Omit quotes on the ID because we know it's an integer.
json += llvm::formatv(R"(, "id": {0})", ++lsp_call_id);
}
llvm::json::Value parsed_extra_content = nullptr;
if (!extra_content.empty()) {
json += ",";
if (extra_content_label.empty()) {
if (!extra_content.starts_with("\n")) {
json += " ";
}
json += extra_content;
} else {
json += llvm::formatv(R"( "{0}": {{{1}})", extra_content_label,
extra_content);
std::string bracketed_extra_content =
llvm::formatv("{{{0}}", extra_content);
auto parse_result = llvm::json::parse(bracketed_extra_content);
if (auto err = parse_result.takeError()) {
return ErrorBuilder() << "Error parsing extra content: " << err;
}
parsed_extra_content = std::move(*parse_result);
CARBON_CHECK(parsed_extra_content.kind() == llvm::json::Value::Object);
if (extra_content_label == "params" &&
method_or_id == "textDocument/didOpen") {
CARBON_RETURN_IF_ERROR(
AutoFillDidOpenParams(parsed_extra_content.getAsObject(), splits));
}
}
json += "}";

// Form the JSON.
std::string buffer;

{
llvm::raw_string_ostream json_sstream(buffer);
llvm::json::OStream json(json_sstream);

json.object([&] {
json.attribute("jsonrpc", "2.0");
json.attribute(method_or_id_label, method_or_id);

if (use_call_id) {
json.attribute("id", ++lsp_call_id);
}
if (parsed_extra_content != nullptr) {
if (!extra_content_label.empty()) {
json.attribute(extra_content_label, parsed_extra_content);
} else {
for (const auto& [key, value] : *parsed_extra_content.getAsObject()) {
json.attribute(key, value);
}
}
}
});
}

// 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)
llvm::formatv("Content-Length: {0}\n\n{1}\n", buffer.size() + 2, buffer)
.str();
int keyword_len =
(body_start.data() + body_end + LspEnd.size()) - keyword.data();
Expand All @@ -167,7 +230,8 @@ static auto ReplaceLspKeywordAt(std::string* content, size_t keyword_pos,
// 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_call_id)
llvm::StringRef test_name, int& lsp_call_id,
llvm::ArrayRef<TestFile::Split> splits)
-> ErrorOr<size_t> {
auto keyword = llvm::StringRef(*content).substr(keyword_pos);

Expand All @@ -186,7 +250,7 @@ static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
}

if (keyword.starts_with("[[@LSP")) {
return ReplaceLspKeywordAt(content, keyword_pos, lsp_call_id);
return ReplaceLspKeywordAt(content, keyword_pos, lsp_call_id, splits);
}

return ErrorBuilder() << "Unexpected use of `[[@` at `"
Expand All @@ -198,7 +262,9 @@ static auto ReplaceContentKeywordAt(std::string* content, size_t keyword_pos,
// TEST_NAME is the only content keyword at present, but we do validate that
// other names are reserved.
static auto ReplaceContentKeywords(llvm::StringRef filename,
std::string* content) -> ErrorOr<Success> {
std::string* content,
llvm::ArrayRef<TestFile::Split> splits)
-> ErrorOr<Success> {
static constexpr llvm::StringLiteral Prefix = "[[@";

auto keyword_pos = content->find(Prefix);
Expand Down Expand Up @@ -227,7 +293,8 @@ static auto ReplaceContentKeywords(llvm::StringRef filename,
while (keyword_pos != std::string::npos) {
CARBON_ASSIGN_OR_RETURN(
auto keyword_end,
ReplaceContentKeywordAt(content, keyword_pos, test_name, lsp_call_id));
ReplaceContentKeywordAt(content, keyword_pos, test_name, lsp_call_id,
splits));
keyword_pos = content->find(Prefix, keyword_end);
}
return Success();
Expand All @@ -237,7 +304,8 @@ static auto ReplaceContentKeywords(llvm::StringRef filename,
static auto AddSplit(llvm::StringRef filename, std::string* content,
llvm::SmallVector<TestFile::Split>* file_splits)
-> ErrorOr<Success> {
CARBON_RETURN_IF_ERROR(ReplaceContentKeywords(filename, content));
CARBON_RETURN_IF_ERROR(
ReplaceContentKeywords(filename, content, *file_splits));
file_splits->push_back(
{.filename = filename.str(), .content = std::move(*content)});
content->clear();
Expand Down Expand Up @@ -290,17 +358,18 @@ static auto TryConsumeSplit(llvm::StringRef line, llvm::StringRef line_trimmed,
if (split->filename.empty()) {
return ErrorBuilder() << "Missing filename for split.";
}
// The split line is added to non_check_lines for retention in autoupdate, but
// is not added to the test file content.
// The split line is added to non_check_lines for retention in autoupdate,
// but is not added to the test file content.
*line_index = 0;
non_check_lines->push_back(
FileTestLine(split->file_index, *line_index, line));
return true;
}

// Converts a `FileCheck`-style expectation string into a single complete regex
// string by escaping all regex characters outside of the designated `{{...}}`
// regex sequences, and switching those to a normal regex sub-pattern syntax.
// Converts a `FileCheck`-style expectation string into a single complete
// regex string by escaping all regex characters outside of the designated
// `{{...}}` regex sequences, and switching those to a normal regex
// sub-pattern syntax.
static auto ConvertExpectationStringToRegex(std::string& str) -> void {
for (int pos = 0; pos < static_cast<int>(str.size());) {
switch (str[pos]) {
Expand Down Expand Up @@ -492,7 +561,8 @@ static auto TryConsumeArgs(llvm::StringRef line, llvm::StringRef line_trimmed,
return true;
}

// Processes AUTOUPDATE lines when found. Returns true if the line is consumed.
// Processes AUTOUPDATE lines when found. Returns true if the line is
// consumed.
static auto TryConsumeAutoupdate(int line_index, llvm::StringRef line_trimmed,
bool* found_autoupdate,
std::optional<int>* autoupdate_line_number)
Expand Down
38 changes: 38 additions & 0 deletions testing/file_test/testdata/lsp_autofill.carbon
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //testing/file_test:file_test_base_test --test_arg=--file_tests=testing/file_test/testdata/lsp_autofill.carbon
// TIP: To dump output, run:
// TIP: bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/lsp_autofill.carbon

// --- foo.carbon
class Foo {
fn foo();
fn bar() {}
}

// --- STDIN
[[@LSP-NOTIFY:textDocument/didOpen:
"textDocument": {
"uri": "file:/foo.carbon",
"languageId": "carbon",
"text": "AUTOFILL"
}
]]

// --- AUTOUPDATE-SPLIT

// CHECK:STDERR: --- STDIN:
// CHECK:STDERR: Content-Length: 181
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"languageId":"carbon","text":"class Foo {\n fn foo();\n fn bar() {}\n}\n\n","uri":"file:/foo.carbon"}}}
// CHECK:STDERR:
// CHECK:STDERR:
// CHECK:STDOUT: 2 args: `default_args`, `foo.carbon`
// CHECK:STDOUT: foo.carbon:1: class Foo {
// CHECK:STDOUT: foo.carbon:2: fn foo();
// CHECK:STDOUT: foo.carbon:3: fn bar() {}
// CHECK:STDOUT: foo.carbon:4: }
51 changes: 23 additions & 28 deletions testing/file_test/testdata/lsp_keywords.carbon
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,57 @@
// --- STDIN
[[@LSP:foo:]]
[[@LSP:foo]]
[[@LSP:bar:content]]
[[@LSP:bar:"content": 0]]
[[@LSP:baz:
multi
line
"multi": 0,
"line": 1
]]
[[@LSP-CALL:bar:content]]
[[@LSP-CALL:bar:"content": 0]]
[[@LSP-CALL:baz:
multi
line]]
"multi": 0,
"line": 1]]
[[@LSP-REPLY:7]]
[[@LSP-REPLY:8:bar]]
[[@LSP-REPLY:8:"bar": 0]]
[[@LSP-NOTIFY:exit]]

// --- AUTOUPDATE-SPLIT

// CHECK:STDERR: --- STDIN:
// CHECK:STDERR: Content-Length: 37
// CHECK:STDERR: Content-Length: 34
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "foo"}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"foo"}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 37
// CHECK:STDERR: Content-Length: 34
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "foo"}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"foo"}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 46
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "bar", content}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"bar","content":0}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 50
// CHECK:STDERR: Content-Length: 53
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "baz",
// CHECK:STDERR: multi
// CHECK:STDERR: line
// CHECK:STDERR: }
// CHECK:STDERR: {"jsonrpc":"2.0","method":"baz","multi":0,"line":1}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 67
// CHECK:STDERR: Content-Length: 64
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "bar", "id": 1, "params": {content}}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"bar","id":1,"params":{"content":0}}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 71
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "baz", "id": 2, "params": {
// CHECK:STDERR: multi
// CHECK:STDERR: line}}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"baz","id":2,"params":{"line":1,"multi":0}}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 31
// CHECK:STDERR: Content-Length: 28
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "7"}
// CHECK:STDERR: {"jsonrpc":"2.0","id":"7"}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 48
// CHECK:STDERR: Content-Length: 47
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "id": "8", "result": {bar}}
// CHECK:STDERR: {"jsonrpc":"2.0","id":"8","result":{"bar":0}}
// CHECK:STDERR:
// CHECK:STDERR: Content-Length: 38
// CHECK:STDERR: Content-Length: 35
// CHECK:STDERR:
// CHECK:STDERR: {"jsonrpc": "2.0", "method": "exit"}
// CHECK:STDERR: {"jsonrpc":"2.0","method":"exit"}
// CHECK:STDERR:
// CHECK:STDERR:
// CHECK:STDOUT: 1 args: `default_args`
13 changes: 11 additions & 2 deletions toolchain/language_server/testdata/document_symbol/nested.carbon
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/document_symbol/nested.carbon

// --- class.carbon
class A {
fn F();
fn G() {}
}

// --- STDIN
[[@LSP-NOTIFY:textDocument/didOpen:
"textDocument": {"uri": "file:/class.carbon", "languageId": "carbon",
"text": "class A {\n fn F();\n fn G() {}\n}\n"}
"textDocument": {
"uri": "file:/class.carbon",
"languageId": "carbon",
"text": "AUTOFILL"
}
]]
[[@LSP-CALL:textDocument/documentSymbol:
"textDocument": {"uri": "file:/class.carbon"}
Expand Down