From 03939a922341179e995a6b64684d9565afe43c09 Mon Sep 17 00:00:00 2001 From: Richard Smith Date: Wed, 23 Sep 2026 23:44:32 +0000 Subject: [PATCH] Add language server support for SemIR in testdata. (#7803) Add hover cards and jump to declaration / definition / reference for the formatted SemIR that appears in check tests. This is done by adding a heuristic "parser" for SemIR to the language server. The cross-references are strictly best-effort, since this is just a tool for Carbon developers, not a user-facing facility. A couple of other changes made along the way: * file_test tests with an AUTOUPDATE-SPLIT no longer look for CHECK: lines outside that split. This was motivated by the tests for this new facility including CHECK: lines as part of the test input. * An agent skill for working on the language server, tracking some things that cost Claude time when working on this. Assisted-by: Claude via Antigravity --- .agents/skills/language_server/SKILL.md | 197 +++++ .agents/skills/toolchain_development/SKILL.md | 4 + .agents/skills/toolchain_tests/SKILL.md | 5 + testing/file_test/README.md | 7 + testing/file_test/test_file.cpp | 42 +- .../check_outside_autoupdate_split.carbon | 30 + toolchain/language_server/BUILD | 20 +- toolchain/language_server/context.cpp | 25 + toolchain/language_server/context.h | 31 +- toolchain/language_server/handle.h | 14 + .../handle_document_symbol.cpp | 7 + .../language_server/handle_formatting.cpp | 6 + .../language_server/handle_initialize.cpp | 1 + toolchain/language_server/handle_position.cpp | 126 ++- .../language_server/handle_sem_ir_text.cpp | 167 ++++ .../language_server/handle_sem_ir_text.h | 55 ++ .../language_server/handle_text_document.cpp | 6 +- .../language_server/incoming_messages.cpp | 3 +- toolchain/language_server/sem_ir_text.cpp | 756 ++++++++++++++++++ toolchain/language_server/sem_ir_text.h | 220 +++++ .../basics/fail_shutdown_without_exit.carbon | 3 +- .../testdata/basics/initialize.carbon | 9 +- .../testdata/basics/notify_parse_error.carbon | 3 +- .../testdata/document_symbol/basics.carbon | 3 +- .../testdata/document_symbol/choice.carbon | 3 +- .../decl_with_parse_error.carbon | 3 +- .../document_symbol/fn_definition.carbon | 3 +- .../document_symbol/incomplete.carbon | 3 +- .../testdata/document_symbol/language.carbon | 3 +- .../testdata/document_symbol/namespace.carbon | 3 +- .../testdata/document_symbol/unknown.carbon | 3 +- .../testdata/formatting/basic.carbon | 3 +- .../testdata/formatting/errors.carbon | 3 +- .../testdata/formatting/unknown.carbon | 3 +- .../testdata/position/bad_params.carbon | 3 +- .../testdata/position/hover_and_goto.carbon | 3 +- .../testdata/position/sem_ir_text.carbon | 600 ++++++++++++++ .../position/sem_ir_text_with_self.carbon | 253 ++++++ .../text_document/change_unknown.carbon | 3 +- .../text_document/close_unknown.carbon | 3 +- .../testdata/text_document/diagnostics.carbon | 3 +- .../text_document/import_prelude.carbon | 3 +- .../text_document/incremental_sync.carbon | 3 +- .../incremental_sync_multiline.carbon | 3 +- .../text_document/multiple_files.carbon | 3 +- .../text_document/open_change_close.carbon | 3 +- .../text_document/open_duplicate.carbon | 3 +- .../open_with_cpp_nonexistent.carbon | 3 +- utils/vscode/src/extension.ts | 3 + 49 files changed, 2587 insertions(+), 75 deletions(-) create mode 100644 .agents/skills/language_server/SKILL.md create mode 100644 testing/file_test/testdata/check_outside_autoupdate_split.carbon create mode 100644 toolchain/language_server/handle_sem_ir_text.cpp create mode 100644 toolchain/language_server/handle_sem_ir_text.h create mode 100644 toolchain/language_server/sem_ir_text.cpp create mode 100644 toolchain/language_server/sem_ir_text.h create mode 100644 toolchain/language_server/testdata/position/sem_ir_text.carbon create mode 100644 toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon diff --git a/.agents/skills/language_server/SKILL.md b/.agents/skills/language_server/SKILL.md new file mode 100644 index 000000000000..107157e887d3 --- /dev/null +++ b/.agents/skills/language_server/SKILL.md @@ -0,0 +1,197 @@ +--- +name: Language server +description: + Instructions for working on Carbon's LSP language server, including its + architecture, its file_test-based tests, and the VS Code extension. +--- + +# Language server + + + +## Introduction + +This skill covers [`toolchain/language_server/`](/toolchain/language_server/), +which implements `carbon language-server`, and +[`utils/vscode/`](/utils/vscode/), the VS Code extension that launches it. + +## Architecture + +The server is built on clangd's LSP transport (`clang::clangd`), not on a +Carbon-specific one. That means clangd's `Protocol.h` types (`Position`, +`Range`, `Location`, `Hover`, `MarkupContent`) are the interface currency. + +- `server.cpp` / `incoming_messages.cpp`: message dispatch. A handler must be + registered in `incoming_messages.cpp` before it can be called. +- `handle_*.cpp`: one file per request family, each declaring its entry point + in `handle.h`. +- `handle_initialize.cpp`: the advertised capabilities. **Adding a capability + changes `Content-Length` in every test that calls `initialize`**, so expect + a large autoupdate diff. +- `context.h` / `context.cpp`: `Context::File` per open document, plus the + compile driver. `Context::File::unit()` has a `CARBON_CHECK` on the compile + driver, so any handler that reaches for the parse tree must first rule out + documents that were never compiled. +- `position.h`, `sem_ir_index.h`: mapping source positions to SemIR + instructions for real Carbon files. +- `sem_ir_text.h`, `handle_sem_ir_text.h`: navigation within the *formatted + SemIR* in a test file's `// CHECK:STDOUT:` lines. This is a heuristic text + index, deliberately independent of the real SemIR data structures. See + [the SemIR text reader](#the-semir-text-reader). + +### Document kinds + +The server handles two kinds of document, distinguished by the `languageId` +from `textDocument/didOpen`, with a content sniff as a fallback: + +- `carbon`: a real Carbon file. Compiled; diagnostics published. +- `carbon-testdata`: a test file. **Not compiled**, because we lack logic to + split it into one file per `// ---` split marker. + +> [!IMPORTANT] +> A handler that assumes every file was compiled will crash on a test file. +> When adding one, give the test-file path an explicit early return. + +## Tests + +There is **no language-server-specific test target**. +`toolchain/language_server/BUILD` only declares +`filegroup(name = "testdata")`, which is pulled into +`//toolchain/testing:all_testdata` and run by `//toolchain/testing:file_test`. + +```bash +# Run just the language server tests (or any subset). +bazelisk test //toolchain/testing:file_test \ + --test_arg=--file_tests=toolchain/language_server/testdata/position/hover_and_goto.carbon + +# See the raw output, which is much easier to read than a test failure. +bazelisk run //toolchain/testing:file_test -- --dump_output \ + --file_tests=toolchain/language_server/testdata/position/hover_and_goto.carbon + +# Update expectations. Never hand-write CHECK lines. +./toolchain/autoupdate_testdata.py toolchain/language_server/testdata/... +``` + +These tests run serially, because clangd's logging is a global singleton. + +### Test file shape + +The request stream is a `// --- STDIN` split written with the `[[@LSP-*]]` +keywords, and the responses land in a trailing `// --- AUTOUPDATE-SPLIT`. +Documents come from other splits by way of `"text": "FROM_FILE_SPLIT"`, which +is substituted with the content of the split whose name matches the `uri`. + +```carbon +// --- position.carbon +fn Abs(n: i32) -> i32 { return n; } + +// --- STDIN +[[@LSP-CALL:initialize:"capabilities": {}]] +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": { + "uri": "file:/position.carbon", + "languageId": "carbon", + "text": "FROM_FILE_SPLIT" + } +]] +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/position.carbon"}, + "position": {"line": 0, "character": 3} +]] +[[@LSP-CALL:shutdown]] +[[@LSP-NOTIFY:exit]] + +// --- AUTOUPDATE-SPLIT +``` + +Full keyword documentation is in +[`testing/file_test/README.md`](/testing/file_test/README.md). + +### Traps + +> [!WARNING] +> **A blank line inside the `STDIN` split breaks the JSON transport.** It +> terminates a header block, so clangd logs a timestamped +> `Warning: Missing Content-Length header, or zero-length message.` The +> timestamp makes the test unreproducible, so it fails on the next run. +> Comment lines between messages are fine; blank lines are not. A single blank +> line immediately before `// --- AUTOUPDATE-SPLIT` is also fine. + +Other things worth knowing: + +- **`positionEncoding` is UTF-16.** A `character` is a UTF-16 code unit + offset, not a byte offset. +- **Line and character numbers in requests are 0-based**, while the `locN_M` + suffixes in SemIR output are 1-based. Off-by-ones here are silent: the + request succeeds and returns the wrong thing. +- **A split can hold a document that itself contains `// CHECK:STDOUT:` + lines**, because `CHECK` lines only form expectations inside the + `AUTOUPDATE-SPLIT`. Such a document still can't contain a literal `// ---` + line, which would split the enclosing test file; write it as + `[[@0x2f]]/ --- name.carbon`. + +## The SemIR text reader + +`sem_ir_text.cpp` indexes the formatted SemIR inside a test file's +`// CHECK:STDOUT:` lines so that hover and go-to-definition work on operand +names. It is a heuristic reader, not a parser, and its correctness rests on +facts about `toolchain/sem_ir/formatter.cpp` and +`toolchain/sem_ir/inst_namer.cpp`. Re-check these if the formatter changes: + +- There are exactly four scope keywords: `file`, `generated`, `imports`, and + `constants` (`InstNamer::GetScopeName`). Everything else is `@entityname`. +- A reference is `%name` within its own scope and `scope.%name` otherwise + (`InstNamer::GetNameFor`). Names may contain `.` and may _start_ with one, + as in `%.Self.frozen`. +- **Type annotations are printed in the `constants` scope.** + `Formatter::FormatTypeOfInst` does + `llvm::SaveAndRestore file_scope(scope_, InstNamer::ScopeId::Constants)`, + so in `%x: %foo = ...` a bare `%foo` means `constants.%foo`. This does not + apply to ordinary operands or to `[concrete = ...]` annotations. +- **`*_decl` braces hold the declared entity's scope.** The braces of + `%F.decl: ... = fn_decl @F [...] { ... } { ... }` are lexically inside + `file { }`, but their names belong to `@F`. +- **`!with Self:` switches scope without a brace**, until `!members:` + switches it back. Brace counting alone cannot see this. +- A `specific @F(args) { }` block uses `@F`'s scope and defines nothing; each + `%name => value` row references an instruction of the generic. + +### Validating a change to the reader + +The unit tests only cover a handful of cases. To check a change against the +real corpus, drive the server over a sample of check testdata, hovering on +every `%name`, and compare the resolved fraction before and after. Roughly 99% +of names resolve; the residue are names the formatter references but never +emits a definition line for, such as `%I.WithSelf.F`, which only ever appears +inside a `[symbolic = ...]` annotation. + +Two things to get right in such a harness: + +- Feed the request stream from a **file**, not a pipe. The server reads stdin + as a file and reports `error: Input/output error` on a pipe. +- Read the output as **bytes**. Python's `text=True` rewrites the `\r\n` + framing, and `Content-Length` counts bytes. + +## VS Code extension + +[`utils/vscode/`](/utils/vscode/) declares three languages in `package.json`: + +| Language id | Applies to | +| ---------------- | --------------------------------- | +| `carbon` | `*.carbon` | +| `carbon-testdata`| `**/testdata/**/*.carbon` | +| `semir` | `*.semir` | + +> [!NOTE] +> The TextMate _scope_ for SemIR is `source.carbon-semir`, but the +> _language id_ is `semir`. Markdown code fences in hover text resolve language +> ids, so a fence must say ` ```semir `. + +`extension.ts` launches the server over stdio, using the `carbonPath` setting +(default `./bazel-bin/toolchain/carbon`). Its `documentSelector` controls which +files are sent to the server at all; a new document kind has to be added there +as well as in the server. diff --git a/.agents/skills/toolchain_development/SKILL.md b/.agents/skills/toolchain_development/SKILL.md index b6d5b9ac50b9..6fb7e978a959 100644 --- a/.agents/skills/toolchain_development/SKILL.md +++ b/.agents/skills/toolchain_development/SKILL.md @@ -35,6 +35,10 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ([SKILL.md](../builtins/SKILL.md)) for guidelines on registering, mapping, constant evaluating, and lowering compiler builtin primitives (e.g. `"int.convert_float"`). +- **Language server**: Refer to the **Language server** skill + ([SKILL.md](../language_server/SKILL.md)) before working on + `toolchain/language_server/` or `utils/vscode/`. Neither follows the + patterns described here. - **Phases**: Lex -> Parse -> Check -> Lower. - **Definitions**: Many kinds (tokens, parse nodes, SemIR instructions) are defined in `.def` files and expanded by way of macros. diff --git a/.agents/skills/toolchain_tests/SKILL.md b/.agents/skills/toolchain_tests/SKILL.md index f0255ee2e398..59e6b72e50b4 100644 --- a/.agents/skills/toolchain_tests/SKILL.md +++ b/.agents/skills/toolchain_tests/SKILL.md @@ -23,6 +23,11 @@ Toolchain tests evaluate Carbon source files through Lexing, Parsing, Checking, and optionally Lowering. Output (for example SemIR dumps, Clang errors) is captured and validated using inline CHECK records. +Language server tests also use `file_test`, but with quite different +conventions (an LSP message stream, `AUTOUPDATE-SPLIT`, `FROM_FILE_SPLIT`). +Refer to the **Language server** skill +([SKILL.md](../language_server/SKILL.md)) for those. + ## Structure and Authoring ### File Layout and Headers diff --git a/testing/file_test/README.md b/testing/file_test/README.md index c18f476ea604..49ca54101551 100644 --- a/testing/file_test/README.md +++ b/testing/file_test/README.md @@ -284,6 +284,13 @@ Supported comment markers are: Output line matchers may contain `[[@LINE+offset]` and `{{regex}}` syntaxes, similar to `FileCheck`. + When the file uses an `AUTOUPDATE-SPLIT`, only `CHECK` lines in that split + are matchers; elsewhere they are ordinary content. This is what allows a + split to hold a test file that itself contains `CHECK` lines, as the + language server's SemIR tests do. Note that such a split still can't contain + a `// ---` line, which would split the enclosing file; write the `/` + characters as `[[@0x2f]]` to avoid that. + - ``` // TIP: ``` diff --git a/testing/file_test/test_file.cpp b/testing/file_test/test_file.cpp index adc35336ec80..d57116528ac9 100644 --- a/testing/file_test/test_file.cpp +++ b/testing/file_test/test_file.cpp @@ -25,6 +25,9 @@ using ::testing::Matcher; using ::testing::MatchesRegex; using ::testing::StrEq; +// The name of the trailing split that autoupdate writes `CHECK` lines into. +static constexpr llvm::StringLiteral AutoupdateSplit = "AUTOUPDATE-SPLIT"; + // Represents the different kinds of version-control conflict markers that are // relevant for the autoupdater. One key concern here is the distinction between // "snapshot" and "diff" conflict regions. Snapshot regions are the more @@ -735,6 +738,22 @@ static auto TryConsumeSetFlag(llvm::StringRef line_trimmed, return true; } +// Returns whether the file uses a `// --- AUTOUPDATE-SPLIT` split. +// +// Autoupdate writes every `CHECK` into that split, so when one is present, a +// `CHECK` line anywhere else is part of the test input rather than an +// expectation. That's what allows a split to hold a test file that itself +// contains `CHECK` lines. +static auto UsesAutoupdateSplit(llvm::StringRef content) -> bool { + for (llvm::StringRef line : llvm::split(content, '\n')) { + llvm::StringRef trimmed = line.trim(); + if (trimmed.consume_front("// ---") && trimmed.trim() == AutoupdateSplit) { + return true; + } + } + return false; +} + // Process content for either the main file (with `test_file` and // `found_autoupdate` provided) or an included file (with those arguments null). // @@ -761,6 +780,8 @@ static auto ProcessFileContent(llvm::StringRef filename, // Otherwise conflict markers are errors. auto previous_conflict_marker = MarkerKind::None; + const bool uses_autoupdate_split = UsesAutoupdateSplit(content_cursor); + SplitState split_state; while (!content_cursor.empty()) { @@ -802,13 +823,18 @@ static auto ProcessFileContent(llvm::StringRef filename, continue; } - CARBON_ASSIGN_OR_RETURN( - is_consumed, - TryConsumeCheck(running_autoupdate, line_index, line, line_trimmed, - test_file ? &test_file->expected_stdout : nullptr, - test_file ? &test_file->expected_stderr : nullptr)); - if (is_consumed) { - continue; + // `CHECK` lines are only expectations where autoupdate would write them. + // Everywhere else they're input, which is how a split can hold a test file + // that itself contains `CHECK` lines. + if (!uses_autoupdate_split || split_state.filename == AutoupdateSplit) { + CARBON_ASSIGN_OR_RETURN( + is_consumed, + TryConsumeCheck(running_autoupdate, line_index, line, line_trimmed, + test_file ? &test_file->expected_stdout : nullptr, + test_file ? &test_file->expected_stderr : nullptr)); + if (is_consumed) { + continue; + } } if (test_file) { @@ -895,8 +921,6 @@ auto ProcessTestFile(llvm::StringRef test_name, bool running_autoupdate) return ErrorBuilder() << "Missing AUTOUPDATE/NOAUTOUPDATE setting"; } - constexpr llvm::StringLiteral AutoupdateSplit = "AUTOUPDATE-SPLIT"; - // Validate AUTOUPDATE-SPLIT use, and remove it from test files if present. if (test_file.has_splits) { for (const auto& test_file : diff --git a/testing/file_test/testdata/check_outside_autoupdate_split.carbon b/testing/file_test/testdata/check_outside_autoupdate_split.carbon new file mode 100644 index 000000000000..9c092e1a5dd2 --- /dev/null +++ b/testing/file_test/testdata/check_outside_autoupdate_split.carbon @@ -0,0 +1,30 @@ +// 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/check_outside_autoupdate_split.carbon +// TIP: To dump output, run: +// TIP: bazel run //testing/file_test:file_test_base_test -- --dump_output --file_tests=testing/file_test/testdata/check_outside_autoupdate_split.carbon + +// When the file uses an `AUTOUPDATE-SPLIT`, `CHECK` lines elsewhere are +// content rather than expectations, so a split can hold a test file of its +// own. Such a split still can't contain a literal `// ---` line, so the `/` +// characters are written as `[[@0x2f]]`. +// +// The echoed line numbers below show that both lines are part of `a.carbon`: +// if the `CHECK` had been consumed it would be an unmatched expectation, and +// if the `// ---` had split, there would be a second file in the arguments. + +// --- a.carbon +// CHECK:STDOUT: not an expectation +this line follows a CHECK line +[[@0x2f]][[@0x2f]] --- not a split +this line follows a split marker + +// --- AUTOUPDATE-SPLIT + +// CHECK:STDOUT: 2 args: `default_args`, `a.carbon` +// CHECK:STDOUT: a.carbon:2: this line follows a CHECK line +// CHECK:STDOUT: a.carbon:4: this line follows a split marker diff --git a/toolchain/language_server/BUILD b/toolchain/language_server/BUILD index 7ae247a31382..243bd66442ba 100644 --- a/toolchain/language_server/BUILD +++ b/toolchain/language_server/BUILD @@ -44,12 +44,24 @@ cc_library( ], ) +cc_library( + name = "sem_ir_text", + srcs = ["sem_ir_text.cpp"], + hdrs = ["sem_ir_text.h"], + deps = [ + "//common:raw_string_ostream", + "@llvm-project//clang-tools-extra/clangd:ClangDaemon", + "@llvm-project//llvm:Support", + ], +) + cc_library( name = "context", srcs = ["context.cpp"], hdrs = ["context.h"], deps = [ ":sem_ir_index", + ":sem_ir_text", "//common:check", "//common:map", "//common:raw_string_ostream", @@ -93,12 +105,16 @@ cc_library( cc_library( name = "handle", - srcs = glob(["handle_*"]), - hdrs = ["handle.h"], + srcs = glob(["handle_*.cpp"]), + hdrs = [ + "handle.h", + "handle_sem_ir_text.h", + ], deps = [ ":context", ":position", ":sem_ir_index", + ":sem_ir_text", "//common:check", "//common:raw_string_ostream", "//toolchain/base:shared_value_stores", diff --git a/toolchain/language_server/context.cpp b/toolchain/language_server/context.cpp index 3b4b11917aca..eeed57950ea6 100644 --- a/toolchain/language_server/context.cpp +++ b/toolchain/language_server/context.cpp @@ -10,6 +10,7 @@ #include "common/check.h" #include "common/raw_string_ostream.h" +#include "llvm/ADT/StringExtras.h" #include "llvm/Support/VirtualFileSystem.h" #include "llvm/TargetParser/Host.h" #include "toolchain/base/clang_invocation.h" @@ -216,17 +217,41 @@ auto Context::File::sem_ir_index() const -> const SemIRIndex* { return &*sem_ir_index_; } +auto Context::File::sem_ir_text() const -> const SemIRText* { + if (!is_test_file_) { + return nullptr; + } + if (!sem_ir_text_) { + sem_ir_text_.emplace(text_); + } + return sem_ir_text_->empty() ? nullptr : &*sem_ir_text_; +} + auto Context::File::SetText(Context& context, std::optional version, llvm::StringRef text) -> void { // Clear state dependent on the source text. compile_driver_.reset(); sem_ir_index_.reset(); + sem_ir_text_.reset(); text_ = text.str(); + is_test_file_ = language_id_ == "carbon-testdata"; // A consumer to gather diagnostics for the file. DiagnosticConsumer consumer(&context, uri_, version); + if (is_test_file_) { + // A test file isn't a Carbon source file: it holds any number of input + // files, plus the output expected from compiling them. Compiling it as one + // source file would report errors on almost every line, so don't. Publish + // the empty diagnostic list anyway, to clear anything left over from + // before the file became a test file. + // TODO: Compile each of the file's splits separately, so that the Carbon + // source in a test file gets the same support as any other Carbon source. + context.PublishDiagnostics(consumer.params()); + return; + } + // TODO: Make the processing asynchronous, to better handle rapid text // updates. diff --git a/toolchain/language_server/context.h b/toolchain/language_server/context.h index 676d242d9a99..da5608c02889 100644 --- a/toolchain/language_server/context.h +++ b/toolchain/language_server/context.h @@ -19,6 +19,7 @@ #include "toolchain/driver/compile_driver.h" #include "toolchain/driver/compile_options.h" #include "toolchain/language_server/sem_ir_index.h" +#include "toolchain/language_server/sem_ir_text.h" #include "toolchain/lex/tokenized_buffer.h" #include "toolchain/parse/tree_and_subtrees.h" #include "toolchain/sem_ir/file.h" @@ -31,9 +32,12 @@ class Context { // Cached information for an open file. class File { public: - explicit File(clang::clangd::URIForFile uri) + // `language_id` is the client's `TextDocumentItem::languageId`, which may + // be empty for a file we were never told about in `didOpen`. + explicit File(clang::clangd::URIForFile uri, llvm::StringRef language_id) : uri_(std::move(uri)), filename_(uri_.file().str()), + language_id_(language_id.str()), options_(&codegen_options_) {} // Changes the file's text, updating dependent state. @@ -44,6 +48,13 @@ class Context { auto filename() const -> llvm::StringRef { return filename_; } auto text() const -> llvm::StringRef { return text_; } + // Returns whether this is a toolchain test file rather than a Carbon + // source file. Test files aren't compiled: their text is a script for the + // test runner, holding any number of input files plus the output expected + // from compiling them, so compiling it as a single source file would + // produce nothing but noise. + auto is_test_file() const -> bool { return is_test_file_; } + auto tree_and_subtrees() const -> const Parse::TreeAndSubtrees& { return unit().parse_tree_and_subtrees(); } @@ -53,8 +64,11 @@ class Context { } // Returns the checked IR, or null if checking didn't get far enough to - // produce one. + // produce one, including because this is a test file and wasn't compiled. auto sem_ir() const -> const SemIR::File* { + if (!compile_driver_) { + return nullptr; + } const auto& compilation_unit = unit(); return compilation_unit.has_sem_ir() ? &compilation_unit.sem_ir() : nullptr; @@ -70,6 +84,11 @@ class Context { // users actually notice. auto sem_ir_index() const -> const SemIRIndex*; + // Returns an index of the formatted SemIR in this file's expected output, + // building it on first use as `sem_ir_index` does. Returns null unless + // this is a test file that has some. + auto sem_ir_text() const -> const SemIRText*; + private: auto unit() const -> const CompilationUnit& { CARBON_CHECK(compile_driver_); @@ -80,15 +99,21 @@ class Context { clang::clangd::URIForFile uri_; std::string filename_; + // The language the client says this file is written in, which is how we + // recognize a test file when the client knows it's looking at one. + std::string language_id_; + // Current file content, and derived values. std::string text_; + bool is_test_file_ = false; CodegenOptions codegen_options_; CompileOptions options_; std::unique_ptr compile_driver_; - // Built on demand by `sem_ir_index()`, and discarded by `SetText`. + // Built on demand by their accessors, and discarded by `SetText`. mutable std::optional sem_ir_index_; + mutable std::optional sem_ir_text_; }; // `vlog_stream` is optional; other parameters are required. diff --git a/toolchain/language_server/handle.h b/toolchain/language_server/handle.h index 27b634e736c7..6b94efc1cad4 100644 --- a/toolchain/language_server/handle.h +++ b/toolchain/language_server/handle.h @@ -10,6 +10,13 @@ namespace Carbon::LanguageServer { +// Locates where the entity named at a position was declared. +auto HandleDeclaration( + Context& context, const clang::clangd::TextDocumentPositionParams& params, + llvm::function_ref< + auto(llvm::Expected>)->void> + on_done) -> void; + // Locates the entity named at a position. auto HandleDefinition( Context& context, const clang::clangd::TextDocumentPositionParams& params, @@ -57,6 +64,13 @@ auto HandleHover( llvm::function_ref)->void> on_done) -> void; +// Locates the implementations of the entity named at a position. +auto HandleImplementation( + Context& context, const clang::clangd::TextDocumentPositionParams& params, + llvm::function_ref< + auto(llvm::Expected>)->void> + on_done) -> void; + // Tells the client what features are supported, and negotiates the position // encoding. auto HandleInitialize( diff --git a/toolchain/language_server/handle_document_symbol.cpp b/toolchain/language_server/handle_document_symbol.cpp index be111582f0a3..2b46b53fda0c 100644 --- a/toolchain/language_server/handle_document_symbol.cpp +++ b/toolchain/language_server/handle_document_symbol.cpp @@ -123,6 +123,13 @@ auto HandleDocumentSymbol( if (!file) { return; } + if (file->is_test_file()) { + // A test file isn't Carbon source, so it has no parse tree to find symbols + // in. + // TODO: Report the file's splits, and the entities within each. + on_done(std::vector()); + return; + } const auto& tree_and_subtrees = file->tree_and_subtrees(); const auto& tree = tree_and_subtrees.tree(); diff --git a/toolchain/language_server/handle_formatting.cpp b/toolchain/language_server/handle_formatting.cpp index 4ea836ac0d15..e2559e1c45fe 100644 --- a/toolchain/language_server/handle_formatting.cpp +++ b/toolchain/language_server/handle_formatting.cpp @@ -32,6 +32,12 @@ auto HandleFormatting( if (!file) { return; } + if (file->is_test_file()) { + // A test file isn't Carbon source, so it has no parse tree to format from, + // and reformatting one as if it were would destroy it. + on_done(std::vector()); + return; + } RawStringOstream out; if (!Format::Format(file->tokens(), out)) { diff --git a/toolchain/language_server/handle_initialize.cpp b/toolchain/language_server/handle_initialize.cpp index 422f834aefc6..2254bd91257b 100644 --- a/toolchain/language_server/handle_initialize.cpp +++ b/toolchain/language_server/handle_initialize.cpp @@ -35,6 +35,7 @@ auto HandleInitialize( {"documentFormattingProvider", true}, {"documentSymbolProvider", true}, {"hoverProvider", true}, + {"implementationProvider", true}, {"positionEncoding", encoding}, {"referencesProvider", true}, {"textDocumentSync", /*Incremental=*/2}, diff --git a/toolchain/language_server/handle_position.cpp b/toolchain/language_server/handle_position.cpp index 9e82788dec79..4e4dd673358d 100644 --- a/toolchain/language_server/handle_position.cpp +++ b/toolchain/language_server/handle_position.cpp @@ -4,10 +4,12 @@ #include #include +#include #include #include "common/raw_string_ostream.h" #include "toolchain/language_server/handle.h" +#include "toolchain/language_server/handle_sem_ir_text.h" #include "toolchain/language_server/position.h" #include "toolchain/language_server/sem_ir_index.h" #include "toolchain/sem_ir/file.h" @@ -32,34 +34,21 @@ static auto StringifyTypeForHover(const SemIR::File& sem_ir, return SemIR::StringifyTypeOfInst(sem_ir, inst_id); } -// Given a position-based query, returns the corresponding position information. -// If the request is invalid or there is no instruction at that position, -// produces a response to the query: an InvalidParams error or a -// default-constructed reply, as appropriate. -// -// TODO: If a default-constructed response is not the correct way to handle an -// invalid location, we will need to extend this function to accept a fallback -// value. For now, it's right for all the queries we support. +// Returns the file a position-based query refers to. If the request names a +// file we don't know, produces an InvalidParams error and returns null. template -static auto FindInstAtPositionOrFail( +static auto FindFileOrFail( Context& context, const clang::clangd::TextDocumentPositionParams& params, llvm::function_ref)->void> on_done) - -> PositionInfo { + -> const Context::File* { auto* file = context.LookupFile(params.textDocument.uri.file()); if (!file) { on_done(llvm::make_error( llvm::formatv("Unknown textDocument `{0}`", params.textDocument.uri.file()), clang::clangd::ErrorCode::InvalidParams)); - return {}; } - - auto info = FindPositionInfo(*file, params.position); - if (!info.has_inst()) { - on_done(ResponseType()); - } - - return info; + return file; } // Implements `textDocument/hover`: @@ -68,8 +57,19 @@ auto HandleHover( Context& context, const clang::clangd::TextDocumentPositionParams& params, llvm::function_ref)->void> on_done) -> void { - auto info = FindInstAtPositionOrFail(context, params, on_done); + const auto* file = + FindFileOrFail(context, params, on_done); + if (!file) { + return; + } + if (auto hover = GetSemIRTextHover(*file, params.position)) { + on_done(std::move(*hover)); + return; + } + + auto info = FindPositionInfo(*file, params.position); if (!info.has_inst()) { + on_done(clang::clangd::Hover()); return; } @@ -91,12 +91,24 @@ auto HandleHover( // instruction they resolve to. static auto HandleGoto( Context& context, const clang::clangd::TextDocumentPositionParams& params, - bool use_type, + SemIRTextGoto sem_ir_text_goto, bool use_type, llvm::function_ref< auto(llvm::Expected>)->void> on_done) -> void { - auto info = FindInstAtPositionOrFail(context, params, on_done); + const auto* file = FindFileOrFail>( + context, params, on_done); + if (!file) { + return; + } + if (auto locations = + GetSemIRTextLocations(*file, params.position, sem_ir_text_goto)) { + on_done(std::move(*locations)); + return; + } + + auto info = FindPositionInfo(*file, params.position); if (!info.has_inst()) { + on_done(std::vector()); return; } @@ -118,19 +130,58 @@ static auto HandleGoto( on_done(std::move(locations)); } -// Implements `textDocument/definition` and `textDocument/declaration`: +// Implements `textDocument/definition`: // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_definition -// -// Carbon separates declaration from definition, but SemIR resolves a name to a -// single entity instruction, so both requests currently answer the same way. -// TODO: Point `definition` at the definition when an entity is declared in one -// place and defined in another. auto HandleDefinition( Context& context, const clang::clangd::TextDocumentPositionParams& params, llvm::function_ref< auto(llvm::Expected>)->void> on_done) -> void { - HandleGoto(context, params, /*use_type=*/false, on_done); + HandleGoto(context, params, SemIRTextGoto::Definition, /*use_type=*/false, + on_done); +} + +// Implements `textDocument/declaration`: +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_declaration +// +// In formatted SemIR this finds the source text the name was checked from, +// which is the closest thing an instruction has to a declaration, and is the +// more useful answer given `definition` already finds the instruction. +// +// Carbon separates declaration from definition, but SemIR resolves a name to a +// single entity instruction, so for Carbon source both requests answer alike. +// TODO: Point `definition` at the definition when an entity is declared in one +// place and defined in another. +auto HandleDeclaration( + Context& context, const clang::clangd::TextDocumentPositionParams& params, + llvm::function_ref< + auto(llvm::Expected>)->void> + on_done) -> void { + HandleGoto(context, params, SemIRTextGoto::Source, /*use_type=*/false, + on_done); +} + +// Implements `textDocument/implementation`: +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_implementation +// +// In formatted SemIR this finds the value the name takes in each `specific` of +// the generic that defines it. +// +// TODO: For Carbon source, find the `impl`s of an interface, and the functions +// implementing an interface's associated functions. +auto HandleImplementation( + Context& context, const clang::clangd::TextDocumentPositionParams& params, + llvm::function_ref< + auto(llvm::Expected>)->void> + on_done) -> void { + const auto* file = FindFileOrFail>( + context, params, on_done); + if (!file) { + return; + } + auto locations = + GetSemIRTextLocations(*file, params.position, SemIRTextGoto::Specifics); + on_done(locations.value_or(std::vector())); } // Implements `textDocument/typeDefinition`: @@ -140,7 +191,10 @@ auto HandleTypeDefinition( llvm::function_ref< auto(llvm::Expected>)->void> on_done) -> void { - HandleGoto(context, params, /*use_type=*/true, on_done); + // A SemIR name's type is written on its defining line, which `definition` + // already finds, so there's nothing extra to offer for formatted SemIR. + HandleGoto(context, params, SemIRTextGoto::Definition, /*use_type=*/true, + on_done); } // Implements `textDocument/references`: @@ -153,8 +207,20 @@ auto HandleReferences( llvm::function_ref< auto(llvm::Expected>)->void> on_done) -> void { - auto info = FindInstAtPositionOrFail(context, params, on_done); + const auto* file = FindFileOrFail>( + context, params, on_done); + if (!file) { + return; + } + if (auto locations = GetSemIRTextLocations(*file, params.position, + SemIRTextGoto::References)) { + on_done(std::move(*locations)); + return; + } + + auto info = FindPositionInfo(*file, params.position); if (!info.has_inst()) { + on_done(std::vector()); return; } diff --git a/toolchain/language_server/handle_sem_ir_text.cpp b/toolchain/language_server/handle_sem_ir_text.cpp new file mode 100644 index 000000000000..a59fbdd714bf --- /dev/null +++ b/toolchain/language_server/handle_sem_ir_text.cpp @@ -0,0 +1,167 @@ +// 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 + +#include "toolchain/language_server/handle_sem_ir_text.h" + +#include +#include +#include + +#include "common/raw_string_ostream.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringExtras.h" +#include "toolchain/language_server/sem_ir_text.h" + +namespace Carbon::LanguageServer { + +// The language id the VS Code extension uses for formatted SemIR, so that +// hover text is highlighted the same way the output it was read from is. Note +// this is the language id from `package.json`, not the TextMate scope name +// `source.carbon-semir`. +static constexpr llvm::StringLiteral SemIRLanguage = "semir"; + +// Writes `text` as a fenced code block of formatted SemIR. +static auto WriteCodeBlock(llvm::raw_ostream& out, llvm::StringRef text) + -> void { + out << "```" << SemIRLanguage << "\n" << text << "\n```\n"; +} + +// Returns the 0-based line `line` of `text`, without its terminator, or an +// empty string if the text has no such line. +static auto GetLine(llvm::StringRef text, int line) -> llvm::StringRef { + for (llvm::StringRef candidate : llvm::split(text, '\n')) { + if (line-- == 0) { + return candidate.rtrim('\r'); + } + } + return llvm::StringRef(); +} + +// Returns a definition written the way a reference to it from outside its +// scope would be, so that the scope it came from is visible. Only the first +// line is qualified: the rest is the body of the definition's brace group. +static auto Qualified(const SemIRTextDefinition& definition) -> std::string { + RawStringOstream out; + out << definition.scope << "." << definition.text; + return out.TakeStr(); +} + +// Returns `text` with every line indented, so that it reads as subordinate to +// the line before it. +static auto Indent(llvm::StringRef text) -> std::string { + RawStringOstream out; + llvm::ListSeparator newline("\n"); + for (llvm::StringRef line : llvm::split(text, '\n')) { + out << newline << " " << line; + } + return out.TakeStr(); +} + +auto GetSemIRTextHover(const Context::File& file, + const clang::clangd::Position& position) + -> std::optional { + const auto* sem_ir_text = file.sem_ir_text(); + if (!sem_ir_text) { + return std::nullopt; + } + auto name = sem_ir_text->Lookup(position); + if (!name) { + return std::nullopt; + } + if (name->definitions.empty() && !name->source_position) { + // We found a name but have nothing to say about it, so say nothing rather + // than showing an empty popup. + return std::nullopt; + } + + RawStringOstream text; + + // The definitions, written the way a reference to them from outside their + // scope would be, so that the scope each one came from is visible. + for (const auto& definition : name->definitions) { + WriteCodeBlock(text, Qualified(definition)); + } + + // What the name becomes in each specific of the generic that defines it. + if (!name->specific_values.empty()) { + text << "\nSpecific values:\n\n"; + RawStringOstream values; + llvm::ListSeparator newline("\n"); + for (const auto& value : name->specific_values) { + values << newline << value.specific << " =>"; + if (value.value_definition) { + // The value is nearly always a reference to a constant, and the name + // of the constant says little, so show what it's defined as. It goes + // on its own line because a definition can be long, or several lines. + values << "\n" << Indent(Qualified(*value.value_definition)); + } else { + values << " " << value.value; + } + } + WriteCodeBlock(text, values.TakeStr()); + } + + // The source text the name was checked from. + if (name->source_position) { + auto source = GetLine(file.text(), name->source_position->line).trim(); + if (!source.empty()) { + text << "\nSource:\n\n```carbon\n" << source << "\n```\n"; + } + } + + return clang::clangd::Hover{ + .contents = {.kind = clang::clangd::MarkupKind::Markdown, + .value = text.TakeStr()}, + .range = name->range}; +} + +auto GetSemIRTextLocations(const Context::File& file, + const clang::clangd::Position& position, + SemIRTextGoto goto_kind) + -> std::optional> { + const auto* sem_ir_text = file.sem_ir_text(); + if (!sem_ir_text) { + return std::nullopt; + } + auto name = sem_ir_text->Lookup(position); + if (!name) { + return std::nullopt; + } + + std::vector locations; + auto add = [&](clang::clangd::Range range) { + locations.push_back({.uri = file.uri(), .range = range}); + }; + + switch (goto_kind) { + case SemIRTextGoto::Definition: + for (const auto& definition : name->definitions) { + add(definition.range); + } + break; + + case SemIRTextGoto::Source: + if (name->source_position) { + // We know where the source text starts but not how far it runs, so + // point at it rather than selecting it. + add({.start = *name->source_position, .end = *name->source_position}); + } + break; + + case SemIRTextGoto::Specifics: + for (const auto& value : name->specific_values) { + add(value.range); + } + break; + + case SemIRTextGoto::References: + for (const auto& occurrence : name->occurrences) { + add(occurrence); + } + break; + } + return locations; +} + +} // namespace Carbon::LanguageServer diff --git a/toolchain/language_server/handle_sem_ir_text.h b/toolchain/language_server/handle_sem_ir_text.h new file mode 100644 index 000000000000..cd55fbfca933 --- /dev/null +++ b/toolchain/language_server/handle_sem_ir_text.h @@ -0,0 +1,55 @@ +// 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 + +#ifndef CARBON_TOOLCHAIN_LANGUAGE_SERVER_HANDLE_SEM_IR_TEXT_H_ +#define CARBON_TOOLCHAIN_LANGUAGE_SERVER_HANDLE_SEM_IR_TEXT_H_ + +#include +#include + +#include "clang-tools-extra/clangd/Protocol.h" +#include "toolchain/language_server/context.h" + +namespace Carbon::LanguageServer { + +// Answers `textDocument/hover` from the formatted SemIR in a test file's +// expected output. Returns `nullopt` if `position` isn't within a SemIR name, +// leaving the request to be answered from the file's Carbon source. +auto GetSemIRTextHover(const Context::File& file, + const clang::clangd::Position& position) + -> std::optional; + +// What a goto-style request should find for a name in formatted SemIR. +enum class SemIRTextGoto : int8_t { + // The lines that define the name, for `textDocument/definition`. + Definition, + + // The source text that the name's `.loc_` suffix points at, + // for `textDocument/declaration`. A SemIR name is derived from the source it + // was checked from, so this is where the name came from, in the input file + // the enclosing block of output was compiled from. + Source, + + // The rows of `specific` blocks that give the name its value, for + // `textDocument/implementation`. A name defined in a generic is symbolic, + // and each specific of that generic is one way of making it concrete, which + // is as close to an implementation as SemIR has. + Specifics, + + // Everywhere the name is written in the same block of output, for + // `textDocument/references`. + References, +}; + +// Answers a goto-style request from the formatted SemIR in a test file's +// expected output. Returns `nullopt` if `position` isn't within a SemIR name, +// leaving the request to be answered from the file's Carbon source. +auto GetSemIRTextLocations(const Context::File& file, + const clang::clangd::Position& position, + SemIRTextGoto goto_kind) + -> std::optional>; + +} // namespace Carbon::LanguageServer + +#endif // CARBON_TOOLCHAIN_LANGUAGE_SERVER_HANDLE_SEM_IR_TEXT_H_ diff --git a/toolchain/language_server/handle_text_document.cpp b/toolchain/language_server/handle_text_document.cpp index bf80be3e2651..9e5de35fe13f 100644 --- a/toolchain/language_server/handle_text_document.cpp +++ b/toolchain/language_server/handle_text_document.cpp @@ -21,8 +21,10 @@ auto HandleDidOpenTextDocument( return; } - auto insert_result = context.files().Insert( - filename, [&] { return Context::File(params.textDocument.uri); }); + auto insert_result = context.files().Insert(filename, [&] { + return Context::File(params.textDocument.uri, + params.textDocument.languageId); + }); insert_result.value().SetText(context, params.textDocument.version, params.textDocument.text); if (!insert_result.is_inserted()) { diff --git a/toolchain/language_server/incoming_messages.cpp b/toolchain/language_server/incoming_messages.cpp index 807a38de9725..1a8d660774cd 100644 --- a/toolchain/language_server/incoming_messages.cpp +++ b/toolchain/language_server/incoming_messages.cpp @@ -73,11 +73,12 @@ auto IncomingMessages::AddNotificationHandler( IncomingMessages::IncomingMessages(clang::clangd::Transport* transport, Context* context) : transport_(transport), context_(context) { - AddCallHandler("textDocument/declaration", &HandleDefinition); + AddCallHandler("textDocument/declaration", &HandleDeclaration); AddCallHandler("textDocument/definition", &HandleDefinition); AddCallHandler("textDocument/documentSymbol", &HandleDocumentSymbol); AddCallHandler("textDocument/formatting", &HandleFormatting); AddCallHandler("textDocument/hover", &HandleHover); + AddCallHandler("textDocument/implementation", &HandleImplementation); AddCallHandler("textDocument/references", &HandleReferences); AddCallHandler("textDocument/typeDefinition", &HandleTypeDefinition); AddCallHandler("initialize", &HandleInitialize); diff --git a/toolchain/language_server/sem_ir_text.cpp b/toolchain/language_server/sem_ir_text.cpp new file mode 100644 index 000000000000..c9120a400dd2 --- /dev/null +++ b/toolchain/language_server/sem_ir_text.cpp @@ -0,0 +1,756 @@ +// 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 + +#include "toolchain/language_server/sem_ir_text.h" + +#include +#include +#include +#include +#include + +#include "common/raw_string_ostream.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/Twine.h" + +namespace Carbon::LanguageServer { + +namespace { + +// A line of formatted SemIR recovered from a `// CHECK:STDOUT:` line, and +// where it was written in the document. +struct ContentLine { + // 0-based document line. + int line; + + // 0-based document column where `text` begins. + int column; + + // The SemIR, with the check prefix removed. + llvm::StringRef text; +}; + +// The source location encoded in a name's `.loc[_]` suffix. +struct SourceLoc { + // 1-based line number within the input file the SemIR was compiled from. + int line; + + // 0-based column number. + int column; +}; + +} // namespace + +// The prefix on a line of expected standard output in a test file. +static constexpr llvm::StringLiteral CheckPrefix = "// CHECK:STDOUT:"; + +// The prefix on a line that starts a new input file within a test file. +static constexpr llvm::StringLiteral SplitPrefix = "// ---"; + +// The prefix on the line that starts a new file's output within formatted +// SemIR. Note this is the same marker as `SplitPrefix` without the comment, +// because the output names the input files it came from. +static constexpr llvm::StringLiteral BlockPrefix = "--- "; + +// The scope that type annotations are written in, whatever scope encloses them. +static constexpr llvm::StringLiteral ConstantsScope = "constants"; + +// Stands in for the scope of a top-level construct we didn't recognize, so +// that its names are still findable by the whole-block fallback. +static constexpr llvm::StringLiteral UnknownScope = ""; + +// The text introducing the braces that hold a declared entity's names, as in +// `%F.decl: %F.type = fn_decl @F [concrete = constants.%F] { ... }`. +static constexpr llvm::StringLiteral DeclMarker = "_decl @"; + +// Separates the name from the value in a row of a `specific` block. +static constexpr llvm::StringLiteral SpecificSeparator = " => "; + +// Starts the part of an interface or named constraint body that is written in +// the entity's `WithSelf` scope. +static constexpr llvm::StringLiteral WithSelfLabel = "!with Self:"; + +// Ends a `WithSelfLabel` region, returning to the entity's own scope. +static constexpr llvm::StringLiteral MembersLabel = "!members:"; + +// Appended to an entity's scope name to get the scope of its `!with Self:` +// region. +static constexpr llvm::StringLiteral WithSelfSuffix = ".WithSelf"; + +// Returns whether `c` can appear in a SemIR name or scope name. Note `.` is +// included: names are built from dot-separated segments, and a name can start +// with one, as in `%.loc12_34.1` and `%.Self`. +static auto IsNameChar(char c) -> bool { + return llvm::isAlnum(c) || c == '_' || c == '.'; +} + +// Returns the end of the `%name` whose `%` is at `text[percent]`, or +// `percent + 1` if no name follows it. +static auto FindNameEnd(llvm::StringRef text, int percent) -> int { + int end = percent + 1; + while (end < static_cast(text.size()) && IsNameChar(text[end])) { + ++end; + } + // A trailing `.` belongs to the surrounding text rather than to the name. + while (end > percent + 1 && text[end - 1] == '.') { + --end; + } + return end; +} + +// Returns the source location a name's `.loc[_]` suffix refers +// to, or `nullopt` if it has no such suffix. +static auto ParseLocSuffix(llvm::StringRef name) -> std::optional { + auto [rest, last] = name.rsplit('.'); + + // An all-digits final segment is a disambiguating counter, such as the `.1` + // of `%T.loc5_16.1`, so the location is the segment before it. + if (!last.empty() && llvm::all_of(last, llvm::isDigit)) { + std::tie(rest, last) = rest.rsplit('.'); + } + if (!last.consume_front("loc")) { + return std::nullopt; + } + + auto [line_text, column_text] = last.split('_'); + int line = 0; + if (line_text.getAsInteger(10, line) || line <= 0) { + return std::nullopt; + } + // A location with no column, such as `%i32.loc5`, refers to the whole line. + int column = 1; + if (!column_text.empty() && + (column_text.getAsInteger(10, column) || column <= 0)) { + return std::nullopt; + } + return SourceLoc{.line = line, .column = column - 1}; +} + +// Returns the `@name` starting at `text[at]`, or an empty string if there +// isn't one there. +static auto ParseEntityName(llvm::StringRef text, size_t at) + -> llvm::StringRef { + size_t end = at + 1; + while (end < text.size() && IsNameChar(text[end])) { + ++end; + } + if (end == at + 1) { + return llvm::StringRef(); + } + return text.substr(at, end - at); +} + +// Returns the scope that a top-level line introduces: a scope keyword for +// `constants {` and friends, and otherwise the entity name. +// +// The entity name is the first `@name` on the line, which works because every +// construct names itself before it mentions anything else: +// `generic fn @F(%T: type) {` introduces `@F` however its parameters are +// spelled, and `specific @F(constants.%i32) {` is written in `@F` too, because +// the names it maps are `@F`'s. +static auto ScopeNameFor(llvm::StringRef line) -> llvm::StringRef { + for (llvm::StringRef keyword : + {"constants", "imports", "generated", "file"}) { + if (line.starts_with(keyword) && + line.drop_front(keyword.size()).ltrim(' ').starts_with("{")) { + return keyword; + } + } + + size_t at = line.find('@'); + if (at == llvm::StringRef::npos) { + return UnknownScope; + } + auto name = ParseEntityName(line, at); + return name.empty() ? llvm::StringRef(UnknownScope) : name; +} + +// Returns the entity scope that the braces on `line` hold, or an empty string +// if they hold the same scope as the line itself. +// +// The only lines that change scope without saying so are the declarations: +// the braces of `%F.decl: %F.type = fn_decl @F [...] { ... } { ... }` are +// lexically inside `file`, but the names they define belong to `@F`. +static auto DeclScopeFor(llvm::StringRef line) -> llvm::StringRef { + size_t decl = line.find(DeclMarker); + if (decl == llvm::StringRef::npos) { + return llvm::StringRef(); + } + return ParseEntityName(line, decl + DeclMarker.size() - 1); +} + +// Returns whether a block's output looks like formatted SemIR, rather than +// some other part of the toolchain's output such as a lowering test's LLVM IR +// or a parse test's YAML. +static auto IsSemIR(llvm::ArrayRef lines) -> bool { + for (const auto& line : lines) { + llvm::StringRef text = line.text; + if (text == "constants {" || text == "imports {" || text == "generated {" || + text == "file {") { + return true; + } + // A file whose top-level scopes are all empty still prints its entities. + for (llvm::StringRef keyword : + {"generic ", "specific ", "fn @", "class @", "interface @", + "constraint @", "impl @", "final impl @", "vtable @"}) { + if (text.starts_with(keyword)) { + return true; + } + } + } + return false; +} + +// Reads the formatted SemIR of one `--- file.carbon` block, and adds the +// definitions and references it finds to the index. +class SemIRText::BlockReader { + public: + explicit BlockReader(SemIRText& index, int32_t block) + : index_(&index), block_(block) {} + + auto Read(llvm::ArrayRef lines) -> void { + lines_ = lines; + for (int index = 0, size = lines.size(); index < size; ++index) { + ReadLine(index); + } + // The output can be truncated mid-definition, so close whatever is still + // open at the end of the block rather than dropping it. + while (!open_definitions_.empty()) { + FinishDefinition(open_definitions_.pop_back_val(), lines.size() - 1); + } + // References are resolved only once the whole block has been read, because + // a name can be used before the line that defines it. + Resolve(); + } + + private: + // Where the parts of a line that need separate treatment are, as offsets + // into the line's body. + struct LineParts { + // The `` of a `%name: = ...`, which is written in the + // `constants` scope rather than in the line's own scope. `-1` for both if + // the line has no type annotation. + int type_begin = -1; + int type_end = -1; + + // The `specific` row this line is, as an index into `specific_values_`, or + // -1 if it isn't one. + int32_t specific_value = -1; + + // Where the value of a `specific` row starts, or -1. + int value_begin = -1; + }; + + // A definition whose brace group is still open. + struct OpenDefinition { + // Index into `definitions_`. + int32_t definition; + + // Index into `lines_` of the line the definition starts on. + int start_line; + + // The brace depth just before that line, which the depth returns to when + // the definition's last brace group closes. + int depth; + }; + + auto ReadLine(int index) -> void; + auto ReadNames(const ContentLine& line, int body_offset, llvm::StringRef body, + llvm::StringRef scope, const LineParts& parts) -> void; + auto AddDefinition(llvm::StringRef name, llvm::StringRef scope, + clang::clangd::Range range, llvm::StringRef text) + -> int32_t; + auto FinishDefinition(const OpenDefinition& open, int end_line) -> void; + auto Resolve() -> void; + auto LookupIn(llvm::StringRef scope, llvm::StringRef name) const + -> std::optional; + + // Updates `scope_stack_` for the braces on `text`, pushing `nested_scope` + // for each `{`. Text inside a string literal is skipped, so that a brace in + // a name or path doesn't unbalance the stack. + auto UpdateScopeStack(llvm::StringRef text, llvm::StringRef nested_scope) + -> void { + bool in_string = false; + for (size_t i = 0; i < text.size(); ++i) { + char c = text[i]; + if (in_string) { + if (c == '\\') { + ++i; + } else if (c == '"') { + in_string = false; + } + } else if (c == '"') { + in_string = true; + } else if (c == '{') { + scope_stack_.push_back({.scope = nested_scope}); + } else if (c == '}' && !scope_stack_.empty()) { + scope_stack_.pop_back(); + } + } + } + + // Handles the labels that change the scope of the rest of the brace group + // they're in. An interface or named constraint body switches to the + // `@Entity.WithSelf` scope at `!with Self:`, and back at `!members:`. + auto UpdateScopeForLabel(llvm::StringRef body) -> void { + if (scope_stack_.empty()) { + return; + } + OpenScope& open = scope_stack_.back(); + if (body == WithSelfLabel) { + open.saved_scope = open.scope; + open.scope = + index_->strings_.save(llvm::Twine(open.scope) + WithSelfSuffix); + } else if (body == MembersLabel && !open.saved_scope.empty()) { + open.scope = open.saved_scope; + open.saved_scope = llvm::StringRef(); + } + } + + SemIRText* index_; + int32_t block_; + + // The content lines of the block being read. + llvm::ArrayRef lines_; + + // An open brace group and the scope its contents are written in. + struct OpenScope { + llvm::StringRef scope; + + // The scope to return to at `!members:`, or empty if we haven't passed a + // `!with Self:` in this brace group. + llvm::StringRef saved_scope; + }; + + // The scope of each open brace. Empty at the top level of the block, where + // the next line introduces a scope of its own. + llvm::SmallVector scope_stack_; + + // Definitions whose brace groups haven't closed yet, innermost last. + llvm::SmallVector open_definitions_; + + // The header of the `specific` block being read, such as + // `specific @F(constants.%i32)`, or empty if we aren't in one. + llvm::StringRef specific_; + + llvm::SmallVector pending_; +}; + +auto SemIRText::BlockReader::ReadLine(int index) -> void { + const ContentLine& line = lines_[index]; + llvm::StringRef text = line.text; + llvm::StringRef body = text.ltrim(' '); + int body_offset = text.size() - body.size(); + + // A label can change the scope of the rest of the brace group it's in, and + // has no names or braces of its own, so handle it before anything else. + if (body.starts_with("!")) { + UpdateScopeForLabel(body); + return; + } + + // The scope the names on this line are written in, and the scope that any + // braces on it open. + llvm::StringRef scope; + llvm::StringRef nested_scope; + if (scope_stack_.empty()) { + if (body.empty()) { + return; + } + // A construct at the top level of the block introduces its own scope. + scope = ScopeNameFor(body); + nested_scope = scope; + specific_ = + body.starts_with("specific ") ? body.rtrim(" {") : llvm::StringRef(); + } else { + scope = scope_stack_.back().scope; + nested_scope = DeclScopeFor(body); + if (nested_scope.empty()) { + nested_scope = scope; + } + } + + // A line that starts with a name either defines it or, in a `specific` + // block, gives it a value. + LineParts parts; + int32_t definition = -1; + if (body.starts_with("%")) { + int name_end = FindNameEnd(body, 0); + llvm::StringRef name = body.substr(1, name_end - 1); + llvm::StringRef rest = body.substr(name_end); + clang::clangd::Range range = { + .start = {.line = line.line, .character = line.column + body_offset}, + .end = {.line = line.line, + .character = line.column + body_offset + name_end}}; + + if (!name.empty() && !specific_.empty() && + rest.starts_with(SpecificSeparator)) { + // A `specific` block doesn't define names; it maps its generic's names + // to the values they take, so this is a reference into the generic. + parts.specific_value = index_->specific_values_.size(); + parts.value_begin = name_end + SpecificSeparator.size(); + index_->specific_values_.push_back( + {.info = {.range = range, + .specific = specific_, + .value = body.substr(parts.value_begin)}}); + } else if (!name.empty()) { + definition = AddDefinition(name, scope, range, body); + + // In `%name: = ...`, the type is written in the `constants` + // scope: the formatter switches scope to print it, because a type is + // nearly always a constant. + if (rest.starts_with(":")) { + size_t equals = body.find(" = ", name_end); + if (equals != llvm::StringRef::npos) { + parts.type_begin = name_end + 1; + parts.type_end = equals; + } + } + } + } + + ReadNames(line, body_offset, body, scope, parts); + + int depth = scope_stack_.size(); + UpdateScopeStack(text, nested_scope); + + // A definition that leaves a brace group open continues onto later lines, + // and isn't complete until the depth comes back to where it started. Note a + // `fn_decl` has two brace groups, and the line between them dips to the + // starting depth and back; because the depth is only checked once per line, + // that doesn't end the definition. + if (definition >= 0 && static_cast(scope_stack_.size()) > depth) { + open_definitions_.push_back( + {.definition = definition, .start_line = index, .depth = depth}); + } + while (!open_definitions_.empty() && static_cast(scope_stack_.size()) <= + open_definitions_.back().depth) { + FinishDefinition(open_definitions_.pop_back_val(), index); + } +} + +auto SemIRText::BlockReader::FinishDefinition(const OpenDefinition& open, + int end_line) -> void { + llvm::StringRef first = lines_[open.start_line].text; + size_t indent = first.size() - first.ltrim(' ').size(); + + // Each line is a separate `// CHECK:STDOUT:` line in the document, so the + // text has to be rebuilt rather than pointed at. + RawStringOstream text; + llvm::ListSeparator newline("\n"); + for (int i = open.start_line; i <= end_line; ++i) { + llvm::StringRef line = lines_[i].text; + text << newline + << (line.size() >= indent ? line.drop_front(indent) : line.ltrim(' ')); + } + index_->definitions_[open.definition].info.text = + index_->strings_.save(text.TakeStr()); +} + +auto SemIRText::BlockReader::ReadNames(const ContentLine& line, int body_offset, + llvm::StringRef body, + llvm::StringRef scope, + const LineParts& parts) -> void { + int size = body.size(); + for (int i = 0; i < size; ++i) { + if (body[i] != '%') { + continue; + } + int name_end = FindNameEnd(body, i); + if (name_end == i + 1) { + continue; + } + + // A name from another scope is written `scope.%name`, where the scope is + // either a keyword or an `@`-prefixed entity name. + int begin = i; + llvm::StringRef explicit_scope; + if (i > 0 && body[i - 1] == '.') { + int scope_begin = i - 1; + while (scope_begin > 0 && IsNameChar(body[scope_begin - 1])) { + --scope_begin; + } + if (scope_begin > 0 && body[scope_begin - 1] == '@') { + --scope_begin; + } + if (scope_begin < i - 1) { + explicit_scope = body.substr(scope_begin, i - 1 - scope_begin); + begin = scope_begin; + } + } + + int column = line.column + body_offset; + // A `specific` row's value gets linked to the row only when this name + // makes up the whole of it, so that a compound value such as + // `%A.type (%A)` isn't reported as the definition of the row. + bool is_whole_value = + begin == parts.value_begin && name_end == static_cast(body.size()); + pending_.push_back( + {.ref = {.line = line.line, + .column_begin = column + begin, + .column_end = column + name_end, + .block = block_, + .name = body.substr(i + 1, name_end - i - 1), + .definitions_begin = 0, + .definitions_size = 0}, + .explicit_scope = explicit_scope, + .context_scope = scope, + .in_type = i >= parts.type_begin && name_end <= parts.type_end, + .specific_value = i == 0 ? parts.specific_value : -1, + .value_of_specific = is_whole_value ? parts.specific_value : -1}); + + i = name_end - 1; + } +} + +auto SemIRText::BlockReader::AddDefinition(llvm::StringRef name, + llvm::StringRef scope, + clang::clangd::Range range, + llvm::StringRef text) -> int32_t { + int32_t index = index_->definitions_.size(); + index_->definitions_.push_back( + {.info = {.range = range, .scope = scope, .text = text}}); + // Names are unique within a scope, but the output can be truncated or + // regex-substituted, so keep the first of any duplicates. + index_->blocks_[block_].scopes[scope].insert({name, index}); + return index; +} + +auto SemIRText::BlockReader::LookupIn(llvm::StringRef scope, + llvm::StringRef name) const + -> std::optional { + const auto& scopes = index_->blocks_[block_].scopes; + auto scope_it = scopes.find(scope); + if (scope_it == scopes.end()) { + return std::nullopt; + } + auto name_it = scope_it->second.find(name); + if (name_it == scope_it->second.end()) { + return std::nullopt; + } + return name_it->second; +} + +auto SemIRText::BlockReader::Resolve() -> void { + for (auto& pending : pending_) { + llvm::StringRef name = pending.ref.name; + llvm::SmallVector definitions; + + if (!pending.explicit_scope.empty()) { + if (auto index = LookupIn(pending.explicit_scope, name)) { + definitions.push_back(*index); + } + } else { + // A bare name usually comes from the scope of the line it's written on, + // except in a type annotation, where it comes from `constants`. Try the + // likely scope first, then the other one. + llvm::StringRef scopes[] = {pending.context_scope, ConstantsScope}; + if (pending.in_type) { + std::swap(scopes[0], scopes[1]); + } + for (llvm::StringRef scope : scopes) { + if (auto index = LookupIn(scope, name)) { + definitions.push_back(*index); + break; + } + } + } + + if (definitions.empty()) { + // The formatter has a few more places where it switches scope without + // saying so, such as the constant value of a symbolic binding, which is + // written in the generic that owns it, and it doesn't always print the + // contents of a scope it names. Rather than model each of them, search + // the whole block and report everything that matches. + for (const auto& scope : index_->blocks_[block_].scopes) { + auto it = scope.second.find(name); + if (it != scope.second.end()) { + definitions.push_back(it->second); + } + } + // `StringMap` iteration order is unspecified; sorting puts the + // definitions back into the order they were written in. + llvm::sort(definitions); + } + + pending.ref.definitions_begin = index_->ref_definitions_.size(); + pending.ref.definitions_size = definitions.size(); + llvm::append_range(index_->ref_definitions_, definitions); + index_->refs_.push_back(pending.ref); + + if (pending.specific_value >= 0) { + for (int32_t index : definitions) { + index_->definitions_[index].specific_values.push_back( + pending.specific_value); + } + } + + // An ambiguous value would be misleading to show as *the* definition, so + // only record one we resolved to exactly one place. + if (pending.value_of_specific >= 0 && definitions.size() == 1) { + index_->specific_values_[pending.value_of_specific].value_definition = + definitions.front(); + } + } +} + +// Recovers the formatted SemIR from the `// CHECK:STDOUT:` lines of `text`, +// and records the line each `// --- file.carbon` split was written on. +static auto ExtractContent(llvm::StringRef text, + llvm::SmallVectorImpl& content, + llvm::StringMap& split_lines) -> void { + llvm::SmallVector lines; + text.split(lines, '\n'); + + for (auto [index, line] : llvm::enumerate(lines)) { + llvm::StringRef body = line.rtrim('\r'); + int indent = body.size(); + body = body.ltrim(' '); + indent -= body.size(); + + if (body.consume_front(CheckPrefix)) { + int column = indent + CheckPrefix.size(); + // An empty line of output has no space after the prefix. + if (body.consume_front(" ")) { + ++column; + } + content.push_back( + {.line = static_cast(index), .column = column, .text = body}); + } else if (body.consume_front(SplitPrefix)) { + split_lines.insert({body.trim(), static_cast(index)}); + } + } +} + +// Returns what to add to a SemIR line number to get the 0-based document line +// of the corresponding source text. +static auto FindSourceLineBase(llvm::StringRef name, + const llvm::StringMap& split_lines) + -> std::optional { + // Line 1 of a split is the line after its `// --- file.carbon` marker. + if (auto it = split_lines.find(name); it != split_lines.end()) { + return it->second; + } + if (split_lines.empty()) { + // The test file isn't split, so it is itself the input file, and SemIR + // line numbers are 1-based document line numbers. + return -1; + } + // The input was pulled in by `// INCLUDE-FILE`, so its text is in a + // different document, which we have no way to refer to from here. + return std::nullopt; +} + +SemIRText::SemIRText(llvm::StringRef text) { + llvm::SmallVector content; + llvm::StringMap split_lines; + ExtractContent(text, content, split_lines); + + // Each `--- file.carbon` block is a separate compilation, so names in one + // never refer to names in another. Read them independently. + for (size_t begin = 0; begin < content.size();) { + llvm::StringRef name; + if (content[begin].text.starts_with(BlockPrefix)) { + name = content[begin].text.drop_front(BlockPrefix.size()).trim(); + ++begin; + } + size_t end = begin; + while (end < content.size() && + !content[end].text.starts_with(BlockPrefix)) { + ++end; + } + + auto lines = llvm::ArrayRef(content).slice(begin, end - begin); + if (IsSemIR(lines)) { + blocks_.push_back( + {.source_line_base = FindSourceLineBase(name, split_lines)}); + BlockReader(*this, blocks_.size() - 1).Read(lines); + } + begin = end; + } + + // Blocks and lines are read in order, so this is nearly always a no-op, but + // `Lookup` depends on it. + llvm::stable_sort(refs_, [](const Ref& lhs, const Ref& rhs) { + return std::pair(lhs.line, lhs.column_begin) < + std::pair(rhs.line, rhs.column_begin); + }); +} + +auto SemIRText::FindRef(clang::clangd::Position position) const -> const Ref* { + // References are sorted by position, so only the last one starting at or + // before `position` can contain it. + auto after = llvm::partition_point(refs_, [&](const Ref& ref) { + return std::pair(ref.line, ref.column_begin) <= + std::pair(position.line, position.character); + }); + if (after == refs_.begin()) { + return nullptr; + } + + const Ref& ref = *std::prev(after); + if (ref.line != position.line || position.character >= ref.column_end) { + return nullptr; + } + return &ref; +} + +auto SemIRText::Lookup(clang::clangd::Position position) const + -> std::optional { + const Ref* ref = FindRef(position); + if (!ref) { + return std::nullopt; + } + + auto range_of = [](const Ref& ref) -> clang::clangd::Range { + return {.start = {.line = ref.line, .character = ref.column_begin}, + .end = {.line = ref.line, .character = ref.column_end}}; + }; + auto definitions_of = [&](const Ref& ref) -> llvm::ArrayRef { + return llvm::ArrayRef(ref_definitions_) + .slice(ref.definitions_begin, ref.definitions_size); + }; + + SemIRTextNameRef result = {.range = range_of(*ref)}; + for (int32_t index : definitions_of(*ref)) { + const Definition& definition = definitions_[index]; + result.definitions.push_back(definition.info); + for (int32_t value : definition.specific_values) { + const SpecificValue& specific_value = specific_values_[value]; + result.specific_values.push_back(specific_value.info); + if (specific_value.value_definition >= 0) { + result.specific_values.back().value_definition = + definitions_[specific_value.value_definition].info; + } + } + } + + // A name's `.loc_` suffix names the source text the + // instruction was checked from, in the input file this block was compiled + // from. + if (auto loc = ParseLocSuffix(ref->name)) { + if (auto base = blocks_[ref->block].source_line_base) { + result.source_position = {.line = *base + loc->line, + .character = loc->column}; + } + } + + for (const Ref& other : refs_) { + // Two names denote the same thing when they resolve to the same + // definitions. When neither resolves, fall back to matching the text, + // which is still better than reporting nothing. + bool same = definitions_of(*ref).empty() && definitions_of(other).empty() + ? ref->name == other.name + : definitions_of(*ref) == definitions_of(other); + if (other.block == ref->block && same) { + result.occurrences.push_back(range_of(other)); + } + } + + return result; +} + +} // namespace Carbon::LanguageServer diff --git a/toolchain/language_server/sem_ir_text.h b/toolchain/language_server/sem_ir_text.h new file mode 100644 index 000000000000..ca2a5e6b65c1 --- /dev/null +++ b/toolchain/language_server/sem_ir_text.h @@ -0,0 +1,220 @@ +// 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 + +#ifndef CARBON_TOOLCHAIN_LANGUAGE_SERVER_SEM_IR_TEXT_H_ +#define CARBON_TOOLCHAIN_LANGUAGE_SERVER_SEM_IR_TEXT_H_ + +#include +#include + +#include "clang-tools-extra/clangd/Protocol.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringMap.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/Allocator.h" +#include "llvm/Support/StringSaver.h" + +namespace Carbon::LanguageServer { + +// The formatted SemIR that introduces a name, such as +// `%i32: type = class_type @Int, @Int(%int_32) [concrete]`. +struct SemIRTextDefinition { + // The range of the `%name` the definition introduces. + clang::clangd::Range range; + + // The scope the name belongs to: a keyword such as `constants`, or an entity + // name such as `@F`. + llvm::StringRef scope; + + // The defining text, with the indentation of its first line removed. This is + // more than one line when the definition opens a brace group, as + // `%foo: = namespace [concrete] { ... }` does, in which case it + // runs to the matching closing brace. + llvm::StringRef text; +}; + +// A `%name => value` row of a `specific` block, giving the value that one name +// from a generic takes in one particular specific. +struct SemIRTextSpecificValue { + // The range of the row's `%name`. + clang::clangd::Range range; + + // The header of the enclosing `specific` block, such as + // `specific @F(constants.%i32)`. + llvm::StringRef specific; + + // The text to the right of the `=>`. + llvm::StringRef value; + + // Where `value` is defined, when it is a single name reference that resolves + // unambiguously. A specific's value is nearly always `constants.%something`, + // and the name on its own says little, so this is what a reader wants to see. + std::optional value_definition; +}; + +// What a `%name` written at some position in formatted SemIR refers to. +struct SemIRTextNameRef { + // The range of the reference, including any `scope.` prefix. + clang::clangd::Range range; + + // The definitions the name resolves to. Normally one; none if we couldn't + // resolve it, and more than one if the name is ambiguous. + llvm::SmallVector definitions; + + // The value this name takes in each `specific` of the generic that defines + // it. Empty unless the name is defined in a generic that has specifics. + llvm::SmallVector specific_values; + + // Where in this document the source text named by the name's + // `.loc_` suffix lives. `nullopt` if the name has no such + // suffix, or if we couldn't work out where the input file it refers to was + // written. + std::optional source_position; + + // Every place this name is written within the same output block, including + // its definition and this reference itself. + llvm::SmallVector occurrences; +}; + +// An index of the formatted SemIR that a toolchain test file carries in its +// `// CHECK:STDOUT:` lines. +// +// This is a heuristic reader, not a parser: there is no grammar for formatted +// SemIR, and the format is free to change. It recognizes just enough structure +// to answer position-based requests -- which names exist, which scope each one +// belongs to, and where each is written -- and silently ignores anything it +// doesn't understand. A test file whose expected output isn't SemIR at all, +// such as a lowering test's LLVM IR, is skipped by `--- file` block rather +// than producing nonsense. +// +// The interesting structure, and the reason a plain text search isn't enough, +// is scoping. A name is written `scope.%name` when it comes from another scope +// and bare `%name` when it comes from the current one, so resolving a name +// means knowing which scope the line it's written on belongs to. That isn't +// simply the innermost `{`: the braces of a `fn_decl @F` sit inside `file` but +// hold `@F`'s names, and a type annotation is written in the `constants` scope +// whatever scope encloses it. Where the scope is still ambiguous, we fall back +// to searching the whole output block, and report every match. +class SemIRText { + public: + // Indexes the formatted SemIR in `text`, which must outlive this object. + explicit SemIRText(llvm::StringRef text); + + // Returns whether any formatted SemIR was found. + auto empty() const -> bool { return refs_.empty(); } + + // Returns what the name written at `position` refers to, or `nullopt` if + // `position` isn't within a `%name` in formatted SemIR. + auto Lookup(clang::clangd::Position position) const + -> std::optional; + + private: + // A name defined within one scope of one output block. + struct Definition { + SemIRTextDefinition info; + + // Indices into `specific_values_` of the rows giving this name's value in + // each `specific` of the generic that defines it. + llvm::SmallVector specific_values; + }; + + // A `%name => value` row of a `specific` block. + struct SpecificValue { + SemIRTextSpecificValue info; + + // Where `info.value` is defined, as an index into `definitions_`, or -1 if + // the value isn't a single name that resolved unambiguously. + int32_t value_definition = -1; + }; + + // One `--- file.carbon` section of the output. Sections are independent: + // each is a separate compilation with its own names, so a name in one never + // refers to a name in another. + struct Block { + // Definitions by scope name, then by name. Indices into `definitions_`. + llvm::StringMap> scopes; + + // Added to a 1-based SemIR line number to get the 0-based document line of + // the corresponding source text. `nullopt` if we couldn't find where this + // block's input file was written, which happens when it came from an + // `// INCLUDE-FILE` rather than from a `// --- file.carbon` split. + std::optional source_line_base; + }; + + // A `%name`, or `scope.%name`, written somewhere in the output. + struct Ref { + // Position in the document. `refs_` is sorted by these. + int32_t line; + int32_t column_begin; + int32_t column_end; + + // The enclosing `--- file.carbon` section, as an index into `blocks_`. + int32_t block; + + // The name, without the `%`. + llvm::StringRef name; + + // The definitions this resolves to, as the range + // `ref_definitions_[definitions_begin ..][0 .. definitions_size)` of + // indices into `definitions_`. + int32_t definitions_begin; + int32_t definitions_size; + }; + + // A reference found while reading, kept until the whole block has been read + // so that it can be resolved against names defined later in the block. + struct PendingRef { + Ref ref; + + // The scope written before the `%`, or empty if the name was unqualified. + llvm::StringRef explicit_scope; + + // The scope of the line the name is written on, used to resolve a name + // that has no `explicit_scope`. + llvm::StringRef context_scope; + + // Whether the name sits in a type annotation, which is written in the + // `constants` scope rather than in `context_scope`. + bool in_type; + + // The specific-value row this name labels, as an index into + // `specific_values_`, or -1. Set for the `%name` of a `%name => value` row + // of a `specific` block, which names an instruction of the generic. + int32_t specific_value = -1; + + // The specific-value row this name is the value of, as an index into + // `specific_values_`, or -1. Set when a name makes up the whole of the + // right side of a `%name => value` row, so that resolving the name also + // tells us where that row's value is defined. + int32_t value_of_specific = -1; + }; + + // Reads the SemIR of one `--- file.carbon` section, and appends what it + // finds to the index. `lines` is that section's content lines, excluding the + // `--- file.carbon` line itself. + class BlockReader; + + auto FindRef(clang::clangd::Position position) const -> const Ref*; + + std::vector blocks_; + std::vector definitions_; + std::vector specific_values_; + + // References, sorted by position, so that a lookup is a binary search. + std::vector refs_; + + // The definitions each `Ref` resolves to; see `Ref::definitions_begin`. + std::vector ref_definitions_; + + // Holds text that we build rather than point at in the document: the + // `@Entity.WithSelf` scope name of a `!with Self:` region, and the text of a + // definition that spans more than one line, which has to have each line's + // `// CHECK:STDOUT:` prefix removed before they can be joined. + llvm::BumpPtrAllocator allocator_; + llvm::StringSaver strings_ = llvm::StringSaver(allocator_); +}; + +} // namespace Carbon::LanguageServer + +#endif // CARBON_TOOLCHAIN_LANGUAGE_SERVER_SEM_IR_TEXT_H_ diff --git a/toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon b/toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon index 3077121e0447..f7b6c722fd8f 100644 --- a/toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon +++ b/toolchain/language_server/testdata/basics/fail_shutdown_without_exit.carbon @@ -15,7 +15,7 @@ // CHECK:STDERR: error: Input/output error [LanguageServerTransportError] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -27,6 +27,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/basics/initialize.carbon b/toolchain/language_server/testdata/basics/initialize.carbon index ca52a4fc7a57..9e31e4121459 100644 --- a/toolchain/language_server/testdata/basics/initialize.carbon +++ b/toolchain/language_server/testdata/basics/initialize.carbon @@ -20,7 +20,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 393{{\r}} +// CHECK:STDOUT: Content-Length: 431{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -32,13 +32,14 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-8", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, // CHECK:STDOUT: "typeDefinitionProvider": true // CHECK:STDOUT: } // CHECK:STDOUT: } -// CHECK:STDOUT: }Content-Length: 394{{\r}} +// CHECK:STDOUT: }Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 2, @@ -50,13 +51,14 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, // CHECK:STDOUT: "typeDefinitionProvider": true // CHECK:STDOUT: } // CHECK:STDOUT: } -// CHECK:STDOUT: }Content-Length: 394{{\r}} +// CHECK:STDOUT: }Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 3, @@ -68,6 +70,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/basics/notify_parse_error.carbon b/toolchain/language_server/testdata/basics/notify_parse_error.carbon index 2ad6f8484df4..3439a1d97bf5 100644 --- a/toolchain/language_server/testdata/basics/notify_parse_error.carbon +++ b/toolchain/language_server/testdata/basics/notify_parse_error.carbon @@ -19,7 +19,7 @@ // CHECK:STDERR: warning: -32602: in call to `textDocument/didOpen`, JSON parse failed: missing value at (root).textDocument [LanguageServerNotificationParseError] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -31,6 +31,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/basics.carbon b/toolchain/language_server/testdata/document_symbol/basics.carbon index 5bd33bbe8bc1..c21ad10c953d 100644 --- a/toolchain/language_server/testdata/document_symbol/basics.carbon +++ b/toolchain/language_server/testdata/document_symbol/basics.carbon @@ -36,7 +36,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -48,6 +48,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/choice.carbon b/toolchain/language_server/testdata/document_symbol/choice.carbon index 4367b27fdfe2..2ae343db0a9c 100644 --- a/toolchain/language_server/testdata/document_symbol/choice.carbon +++ b/toolchain/language_server/testdata/document_symbol/choice.carbon @@ -30,7 +30,7 @@ choice Colors { [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -42,6 +42,7 @@ choice Colors { // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/decl_with_parse_error.carbon b/toolchain/language_server/testdata/document_symbol/decl_with_parse_error.carbon index 6faa37819ef5..612b937ab37e 100644 --- a/toolchain/language_server/testdata/document_symbol/decl_with_parse_error.carbon +++ b/toolchain/language_server/testdata/document_symbol/decl_with_parse_error.carbon @@ -33,7 +33,7 @@ fn G() {} [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -45,6 +45,7 @@ fn G() {} // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/fn_definition.carbon b/toolchain/language_server/testdata/document_symbol/fn_definition.carbon index 9035ae515a15..d24872fef54f 100644 --- a/toolchain/language_server/testdata/document_symbol/fn_definition.carbon +++ b/toolchain/language_server/testdata/document_symbol/fn_definition.carbon @@ -29,7 +29,7 @@ fn Builtin() = "int.make_type_32"; [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -41,6 +41,7 @@ fn Builtin() = "int.make_type_32"; // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/incomplete.carbon b/toolchain/language_server/testdata/document_symbol/incomplete.carbon index 307a7ace01d9..70624263df3e 100644 --- a/toolchain/language_server/testdata/document_symbol/incomplete.carbon +++ b/toolchain/language_server/testdata/document_symbol/incomplete.carbon @@ -27,7 +27,7 @@ class Incomplete { [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -39,6 +39,7 @@ class Incomplete { // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/language.carbon b/toolchain/language_server/testdata/document_symbol/language.carbon index 1b830c40bb32..332d62725fc3 100644 --- a/toolchain/language_server/testdata/document_symbol/language.carbon +++ b/toolchain/language_server/testdata/document_symbol/language.carbon @@ -20,7 +20,7 @@ // CHECK:STDERR: /test.cpp: warning: non-Carbon file requested [LanguageServerFileUnsupported] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -32,6 +32,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/namespace.carbon b/toolchain/language_server/testdata/document_symbol/namespace.carbon index dadd15c8aaca..cfe3a33d1e14 100644 --- a/toolchain/language_server/testdata/document_symbol/namespace.carbon +++ b/toolchain/language_server/testdata/document_symbol/namespace.carbon @@ -28,7 +28,7 @@ fn Bar() {} [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -40,6 +40,7 @@ fn Bar() {} // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/document_symbol/unknown.carbon b/toolchain/language_server/testdata/document_symbol/unknown.carbon index f215fcc86464..be62276e2da2 100644 --- a/toolchain/language_server/testdata/document_symbol/unknown.carbon +++ b/toolchain/language_server/testdata/document_symbol/unknown.carbon @@ -20,7 +20,7 @@ // CHECK:STDERR: /test.carbon: warning: unknown file requested [LanguageServerFileUnknown] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -32,6 +32,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/formatting/basic.carbon b/toolchain/language_server/testdata/formatting/basic.carbon index 02c13e92f272..37dd007b7dc8 100644 --- a/toolchain/language_server/testdata/formatting/basic.carbon +++ b/toolchain/language_server/testdata/formatting/basic.carbon @@ -37,7 +37,7 @@ [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -49,6 +49,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/formatting/errors.carbon b/toolchain/language_server/testdata/formatting/errors.carbon index fbb9c4c68c39..b57f7fb914ca 100644 --- a/toolchain/language_server/testdata/formatting/errors.carbon +++ b/toolchain/language_server/testdata/formatting/errors.carbon @@ -21,7 +21,7 @@ [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -33,6 +33,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/formatting/unknown.carbon b/toolchain/language_server/testdata/formatting/unknown.carbon index c5b28cfb7fde..d551ccd8e39e 100644 --- a/toolchain/language_server/testdata/formatting/unknown.carbon +++ b/toolchain/language_server/testdata/formatting/unknown.carbon @@ -19,7 +19,7 @@ [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -31,6 +31,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/position/bad_params.carbon b/toolchain/language_server/testdata/position/bad_params.carbon index 8fffdb21072b..19074dbf5997 100644 --- a/toolchain/language_server/testdata/position/bad_params.carbon +++ b/toolchain/language_server/testdata/position/bad_params.carbon @@ -51,7 +51,7 @@ [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -63,6 +63,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/position/hover_and_goto.carbon b/toolchain/language_server/testdata/position/hover_and_goto.carbon index 739fd5030c35..f5b22efd0aaa 100644 --- a/toolchain/language_server/testdata/position/hover_and_goto.carbon +++ b/toolchain/language_server/testdata/position/hover_and_goto.carbon @@ -59,7 +59,7 @@ fn Run() { [[@LSP-CALL:shutdown]] [[@LSP-NOTIFY:exit]] -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -71,6 +71,7 @@ fn Run() { // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/position/sem_ir_text.carbon b/toolchain/language_server/testdata/position/sem_ir_text.carbon new file mode 100644 index 000000000000..e874d0423923 --- /dev/null +++ b/toolchain/language_server/testdata/position/sem_ir_text.carbon @@ -0,0 +1,600 @@ +// 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 //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/position/sem_ir_text.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/position/sem_ir_text.carbon + +// Navigation within the SemIR in a test file's `// CHECK:STDOUT:` lines. +// +// The split below is the document under test, not part of this test: `CHECK` +// lines only form expectations inside the `AUTOUPDATE-SPLIT`, and the +// document's own `// ---` marker is written as `[[@0x2f]]/ ---` so that it +// doesn't split this file. + +// --- sem_ir_text.carbon +[[@0x2f]]/ --- a.carbon +library "a"; + +class C {} +class D {} + +//@dump-sem-ir-begin +fn F[T: type](x: T) {} +//@dump-sem-ir-end + +fn Run(c: C, d: D) { + F(c); + F(d); +} + +// CHECK:STDOUT: --- a.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %C: type = class_type @C [concrete] +// CHECK:STDOUT: %empty_struct_type: type = struct_type {} [concrete] +// CHECK:STDOUT: %complete_type: = complete_type_witness %empty_struct_type [concrete] +// CHECK:STDOUT: %D: type = class_type @D [concrete] +// CHECK:STDOUT: %type: type = facet_type [concrete] +// CHECK:STDOUT: %.Self.frozen: %type = symbolic_binding .Self [symbolic_self] +// CHECK:STDOUT: %pattern_type.98f: type = pattern_type type [concrete] +// CHECK:STDOUT: %T.patt: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic] +// CHECK:STDOUT: %T: type = symbolic_binding T, 0 [symbolic] +// CHECK:STDOUT: %pattern_type.51d: type = pattern_type %T [symbolic] +// CHECK:STDOUT: %x.param_patt.91d: %pattern_type.51d = value_param_pattern [symbolic] +// CHECK:STDOUT: %x.patt.260: %pattern_type.51d = wrapper_binding_pattern x, %x.param_patt.91d [symbolic] +// CHECK:STDOUT: %F.type: type = fn_type @F [concrete] +// CHECK:STDOUT: %F: %F.type = struct_value () [concrete] +// CHECK:STDOUT: %require_complete: = require_complete_type %T [symbolic] +// CHECK:STDOUT: %pattern_type.98b: type = pattern_type %C [concrete] +// CHECK:STDOUT: %pattern_type.d8d: type = pattern_type %D [concrete] +// CHECK:STDOUT: %x.param_patt.0f9: %pattern_type.98b = value_param_pattern [concrete] +// CHECK:STDOUT: %x.patt.953: %pattern_type.98b = wrapper_binding_pattern x, %x.param_patt.0f9 [concrete] +// CHECK:STDOUT: %x.param_patt.23b: %pattern_type.d8d = value_param_pattern [concrete] +// CHECK:STDOUT: %x.patt.c51: %pattern_type.d8d = wrapper_binding_pattern x, %x.param_patt.23b [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] { +// CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +// CHECK:STDOUT: %x.param_patt.loc7_16.1: @F.%pattern_type (%pattern_type.51d) = value_param_pattern [symbolic = %x.param_patt.loc7_16.2 (constants.%x.param_patt.91d)] +// CHECK:STDOUT: %x.patt.loc7_16.1: @F.%pattern_type (%pattern_type.51d) = wrapper_binding_pattern x, %x.param_patt.loc7_16.1 [symbolic = %x.patt.loc7_16.2 (constants.%x.patt.260)] +// CHECK:STDOUT: } { +// CHECK:STDOUT: %.loc7_9.1: type = splice_block %.loc7_9.2 [concrete = type] { +// CHECK:STDOUT: %.Self.frozen: %type = symbolic_binding .Self [symbolic_self = constants.%.Self.frozen] +// CHECK:STDOUT: %.loc7_9.2: type = type_literal type [concrete = type] +// CHECK:STDOUT: } +// CHECK:STDOUT: %T.loc7_7.2: type = symbolic_binding T, 0 [symbolic = %T.loc7_7.1 (constants.%T)] +// CHECK:STDOUT: %x.param: @F.%T.loc7_7.1 (%T) = value_param call_param0 +// CHECK:STDOUT: %T.ref: type = name_ref T, %T.loc7_7.2 [symbolic = %T.loc7_7.1 (constants.%T)] +// CHECK:STDOUT: %x: @F.%T.loc7_7.1 (%T) = wrapper_binding x, %x.param +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: generic fn @F(%T.loc7_7.2: type) { +// CHECK:STDOUT: %T.patt.loc7_7.2: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +// CHECK:STDOUT: %T.loc7_7.1: type = symbolic_binding T, 0 [symbolic = %T.loc7_7.1 (constants.%T)] +// CHECK:STDOUT: %pattern_type: type = pattern_type %T.loc7_7.1 [symbolic = %pattern_type (constants.%pattern_type.51d)] +// CHECK:STDOUT: %x.param_patt.loc7_16.2: @F.%pattern_type (%pattern_type.51d) = value_param_pattern [symbolic = %x.param_patt.loc7_16.2 (constants.%x.param_patt.91d)] +// CHECK:STDOUT: %x.patt.loc7_16.2: @F.%pattern_type (%pattern_type.51d) = wrapper_binding_pattern x, %x.param_patt.loc7_16.2 [symbolic = %x.patt.loc7_16.2 (constants.%x.patt.260)] +// CHECK:STDOUT: +// CHECK:STDOUT: !definition: +// CHECK:STDOUT: %require_complete: = require_complete_type %T.loc7_7.1 [symbolic = %require_complete (constants.%require_complete)] +// CHECK:STDOUT: +// CHECK:STDOUT: fn(%x.param: @F.%T.loc7_7.1 (%T)) { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: specific @F(constants.%T) { +// CHECK:STDOUT: %T.patt.loc7_7.2 => constants.%T.patt +// CHECK:STDOUT: %T.loc7_7.1 => constants.%T +// CHECK:STDOUT: %pattern_type => constants.%pattern_type.51d +// CHECK:STDOUT: %x.param_patt.loc7_16.2 => constants.%x.param_patt.91d +// CHECK:STDOUT: %x.patt.loc7_16.2 => constants.%x.patt.260 +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: specific @F(constants.%C) { +// CHECK:STDOUT: %T.patt.loc7_7.2 => constants.%T.patt +// CHECK:STDOUT: %T.loc7_7.1 => constants.%C +// CHECK:STDOUT: %pattern_type => constants.%pattern_type.98b +// CHECK:STDOUT: %x.param_patt.loc7_16.2 => constants.%x.param_patt.0f9 +// CHECK:STDOUT: %x.patt.loc7_16.2 => constants.%x.patt.953 +// CHECK:STDOUT: +// CHECK:STDOUT: !definition: +// CHECK:STDOUT: %require_complete => constants.%complete_type +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: specific @F(constants.%D) { +// CHECK:STDOUT: %T.patt.loc7_7.2 => constants.%T.patt +// CHECK:STDOUT: %T.loc7_7.1 => constants.%D +// CHECK:STDOUT: %pattern_type => constants.%pattern_type.d8d +// CHECK:STDOUT: %x.param_patt.loc7_16.2 => constants.%x.param_patt.23b +// CHECK:STDOUT: %x.patt.loc7_16.2 => constants.%x.patt.c51 +// CHECK:STDOUT: +// CHECK:STDOUT: !definition: +// CHECK:STDOUT: %require_complete => constants.%complete_type +// CHECK:STDOUT: } + +// --- STDIN +[[@LSP-CALL:initialize:"capabilities": {}]] +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": { + "uri": "file:/sem_ir_text.carbon", + "languageId": "carbon-testdata", + "text": "FROM_FILE_SPLIT" + } +]] +// An operand with an explicit scope resolves in that scope. +// Line 42, character 71 is the `constants.%F` in: +// // CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] { +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 42, "character": 71} +]] +// A type annotation resolves in `constants`, not the enclosing `file` scope. +// Line 42, character 29 is the `%F.type` in: +// // CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] { +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 42, "character": 29} +]] +// Inside a `fn_decl @F` the scope is `@F`, not the lexically enclosing `file`. +// This name is also defined in a generic and has a `locN_M` suffix, so it has +// specific values and a source location too. Each specific value is shown as the +// definition of the constant it names, not just the name. +// Line 43, character 102 is the `%T.patt.loc7_7.2` in: +// // CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 43, "character": 102} +]] +// Line 43, character 102 is the `%T.patt.loc7_7.2` in: +// // CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +[[@LSP-CALL:textDocument/definition: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 43, "character": 102} +]] +// Line 43, character 102 is the `%T.patt.loc7_7.2` in: +// // CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +[[@LSP-CALL:textDocument/declaration: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 43, "character": 102} +]] +// Line 43, character 102 is the `%T.patt.loc7_7.2` in: +// // CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +[[@LSP-CALL:textDocument/implementation: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 43, "character": 102} +]] +// Line 43, character 102 is the `%T.patt.loc7_7.2` in: +// // CHECK:STDOUT: %T.patt.loc7_7.1: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)] +[[@LSP-CALL:textDocument/references: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 43, "character": 102}, + "context": {"includeDeclaration": true} +]] +// The left side of a `specific` row names an instruction of the generic. +// Line 76, character 20 is the `%T.loc7_7.1` in: +// // CHECK:STDOUT: %T.loc7_7.1 => constants.%T +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 76, "character": 20} +]] +// Line 76, character 20 is the `%T.loc7_7.1` in: +// // CHECK:STDOUT: %T.loc7_7.1 => constants.%T +[[@LSP-CALL:textDocument/definition: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 76, "character": 20} +]] +// A definition that opens a brace group runs to its closing brace, and is shown +// in full. +// Line 47, character 22 is the `%.loc7_9.1` in: +// // CHECK:STDOUT: %.loc7_9.1: type = splice_block %.loc7_9.2 [concrete = type] { +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 47, "character": 22} +]] +// A scope keyword is not an operand. +// Line 17, character 18 is the `constants` in: +// // CHECK:STDOUT: constants { +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 17, "character": 18} +]] +// Test files aren't compiled, so the Carbon source has no information. +// Line 7, character 3 is the `F[` in: +// fn F[T: type](x: T) {} +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 7, "character": 3} +]] +// Line 7, character 3 is the `F[` in: +// fn F[T: type](x: T) {} +[[@LSP-CALL:textDocument/definition: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "position": {"line": 7, "character": 3} +]] +// Test files have no parse tree, so these produce empty results rather than +// crashing. +[[@LSP-CALL:textDocument/documentSymbol: + "textDocument": {"uri": "file:/sem_ir_text.carbon"} +]] +[[@LSP-CALL:textDocument/formatting: + "textDocument": {"uri": "file:/sem_ir_text.carbon"}, + "options": {"tabSize": 2, "insertSpaces": true} +]] +[[@LSP-CALL:shutdown]] +[[@LSP-NOTIFY:exit]] + +// --- AUTOUPDATE-SPLIT + +// CHECK:STDOUT: Content-Length: 432{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 1, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "capabilities": { +// CHECK:STDOUT: "declarationProvider": true, +// CHECK:STDOUT: "definitionProvider": true, +// CHECK:STDOUT: "documentFormattingProvider": true, +// CHECK:STDOUT: "documentSymbolProvider": true, +// CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, +// CHECK:STDOUT: "positionEncoding": "utf-16", +// CHECK:STDOUT: "referencesProvider": true, +// CHECK:STDOUT: "textDocumentSync": 2, +// CHECK:STDOUT: "typeDefinitionProvider": true +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 151{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [], +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 346{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 2, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\nconstants.%F: %F.type = struct_value () [concrete]\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 73, +// CHECK:STDOUT: "line": 42 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 61, +// CHECK:STDOUT: "line": 42 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 343{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 3, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\nconstants.%F.type: type = fn_type @F [concrete]\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 42 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 28, +// CHECK:STDOUT: "line": 42 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 847{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 4, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\n@F.%T.patt.loc7_7.2: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic = %T.patt.loc7_7.2 (constants.%T.patt)]\n```\n\nSpecific values:\n\n```semir\nspecific @F(constants.%T) =>\n constants.%T.patt: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic]\nspecific @F(constants.%C) =>\n constants.%T.patt: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic]\nspecific @F(constants.%D) =>\n constants.%T.patt: %pattern_type.98f = symbolic_binding_pattern T, 0 [symbolic]\n```\n\nSource:\n\n```carbon\nfn F[T: type](x: T) {}\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 117, +// CHECK:STDOUT: "line": 43 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 101, +// CHECK:STDOUT: "line": 43 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 285{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 5, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 281{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 6, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 6, +// CHECK:STDOUT: "line": 7 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 6, +// CHECK:STDOUT: "line": 7 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 753{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 7, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 75 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 75 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 83 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 83 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 94 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 94 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 1458{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 8, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 117, +// CHECK:STDOUT: "line": 43 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 101, +// CHECK:STDOUT: "line": 43 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 115, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 99, +// CHECK:STDOUT: "line": 59 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 75 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 75 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 83 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 83 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: }, +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 35, +// CHECK:STDOUT: "line": 94 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 94 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 715{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 9, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\n@F.%T.loc7_7.1: type = symbolic_binding T, 0 [symbolic = %T.loc7_7.1 (constants.%T)]\n```\n\nSpecific values:\n\n```semir\nspecific @F(constants.%T) =>\n constants.%T: type = symbolic_binding T, 0 [symbolic]\nspecific @F(constants.%C) =>\n constants.%C: type = class_type @C [concrete]\nspecific @F(constants.%D) =>\n constants.%D: type = class_type @D [concrete]\n```\n\nSource:\n\n```carbon\nfn F[T: type](x: T) {}\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 30, +// CHECK:STDOUT: "line": 76 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 76 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 286{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 10, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 30, +// CHECK:STDOUT: "line": 60 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 60 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///sem_ir_text.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 567{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 11, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\n@F.%.loc7_9.1: type = splice_block %.loc7_9.2 [concrete = type] {\n %.Self.frozen: %type = symbolic_binding .Self [symbolic_self = constants.%.Self.frozen]\n %.loc7_9.2: type = type_literal type [concrete = type]\n}\n```\n\nSource:\n\n```carbon\nfn F[T: type](x: T) {}\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 31, +// CHECK:STDOUT: "line": 47 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 21, +// CHECK:STDOUT: "line": 47 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 74{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 12, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": null +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 74{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 13, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": null +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 50{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 14, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [] +// CHECK:STDOUT: }Content-Length: 50{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 15, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [] +// CHECK:STDOUT: }Content-Length: 50{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 16, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [] +// CHECK:STDOUT: }Content-Length: 52{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 17, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": null +// CHECK:STDOUT: } diff --git a/toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon b/toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon new file mode 100644 index 000000000000..64d40f2185f1 --- /dev/null +++ b/toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon @@ -0,0 +1,253 @@ +// 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 //toolchain/testing:file_test --test_arg=--file_tests=toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/language_server/testdata/position/sem_ir_text_with_self.carbon + +// A `!with Self:` label switches an interface body into the interface's +// `WithSelf` scope until the `!members:` label switches it back, without +// opening a brace. See the `SemIRText` reader's label handling. + +// --- with_self.carbon +[[@0x2f]]/ --- a.carbon +library "a"; + +interface I { + fn G(); +} + +// CHECK:STDOUT: --- a.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %I.type: type = facet_type <@I> [concrete] +// CHECK:STDOUT: %Self: %I.type = symbolic_binding Self, 0 [symbolic] +// CHECK:STDOUT: %I.WithSelf.G.type: type = fn_type @I.WithSelf.G, @I.WithSelf(%Self) [symbolic] +// CHECK:STDOUT: %I.WithSelf.G: %I.WithSelf.G.type = struct_value () [symbolic] +// CHECK:STDOUT: %I.assoc_type: type = assoc_entity_type @I [concrete] +// CHECK:STDOUT: %assoc0: %I.assoc_type = assoc_entity element0, @I.WithSelf.%I.WithSelf.G.decl [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace [concrete] { +// CHECK:STDOUT: .I = %I.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: %I.decl: type = interface_decl @I [concrete = constants.%I.type] {} {} +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: interface @I { +// CHECK:STDOUT: %Self: %I.type = symbolic_binding Self, 0 [symbolic = constants.%Self] +// CHECK:STDOUT: %I.WithSelf.decl = interface_with_self_decl @I [concrete] +// CHECK:STDOUT: +// CHECK:STDOUT: !with Self: +// CHECK:STDOUT: %I.WithSelf.G.decl: @I.WithSelf.%I.WithSelf.G.type (%I.WithSelf.G.type) = fn_decl @I.WithSelf.G [symbolic = @I.WithSelf.%I.WithSelf.G (constants.%I.WithSelf.G)] {} {} +// CHECK:STDOUT: %assoc0: %I.assoc_type = assoc_entity element0, %I.WithSelf.G.decl [concrete = constants.%assoc0] +// CHECK:STDOUT: +// CHECK:STDOUT: !members: +// CHECK:STDOUT: .Self = %Self +// CHECK:STDOUT: .G = @I.WithSelf.%assoc0 +// CHECK:STDOUT: witness = (@I.WithSelf.%I.WithSelf.G.decl) +// CHECK:STDOUT: +// CHECK:STDOUT: !requires: +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: generic fn @I.WithSelf.G(@I.%Self: %I.type) { +// CHECK:STDOUT: fn(); +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: specific @I.WithSelf(constants.%Self) {} +// CHECK:STDOUT: +// CHECK:STDOUT: specific @I.WithSelf.G(constants.%Self) {} + +// --- STDIN +[[@LSP-CALL:initialize:"capabilities": {}]] +[[@LSP-NOTIFY:textDocument/didOpen: + "textDocument": { + "uri": "file:/with_self.carbon", + "languageId": "carbon-testdata", + "text": "FROM_FILE_SPLIT" + } +]] +// A bare name after `!with Self:` resolves in `@I.WithSelf`. +// Line 31, character 68 is the `%I.WithSelf.G.decl` in: +// // CHECK:STDOUT: %assoc0: %I.assoc_type = assoc_entity element0, %I.WithSelf.G.decl [concrete = constants.%assoc0] +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/with_self.carbon"}, + "position": {"line": 31, "character": 68} +]] +// `!members:` switches back to `@I`, so an explicit `@I.WithSelf` prefix is +// needed to reach the same name again. +// Line 35, character 37 is the `%assoc0` in: +// // CHECK:STDOUT: .G = @I.WithSelf.%assoc0 +[[@LSP-CALL:textDocument/definition: + "textDocument": {"uri": "file:/with_self.carbon"}, + "position": {"line": 35, "character": 37} +]] +// The `constants` block reaches into `@I.WithSelf` the same way. +// Line 15, character 80 is the `%I.WithSelf.G.decl` in: +// // CHECK:STDOUT: %assoc0: %I.assoc_type = assoc_entity element0, @I.WithSelf.%I.WithSelf.G.decl [concrete] +[[@LSP-CALL:textDocument/definition: + "textDocument": {"uri": "file:/with_self.carbon"}, + "position": {"line": 15, "character": 80} +]] +// A bare name after `!members:` resolves in `@I`, not `@I.WithSelf`. +// Line 34, character 28 is the `%Self` in: +// // CHECK:STDOUT: .Self = %Self +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/with_self.carbon"}, + "position": {"line": 34, "character": 28} +]] +// `@I.WithSelf` is named here but the formatter only ever defines this name in +// `constants`, so the whole-block fallback finds it there. +// Line 30, character 52 is the `%I.WithSelf.G.type` in: +// // CHECK:STDOUT: %I.WithSelf.G.decl: @I.WithSelf.%I.WithSelf.G.type (%I.WithSelf.G.type) = fn_decl @I.WithSelf.G [symbolic = @I.WithSelf.%I.WithSelf.G (constants.%I.WithSelf.G)] {} {} +[[@LSP-CALL:textDocument/hover: + "textDocument": {"uri": "file:/with_self.carbon"}, + "position": {"line": 30, "character": 52} +]] +[[@LSP-CALL:shutdown]] +[[@LSP-NOTIFY:exit]] + +// --- AUTOUPDATE-SPLIT + +// CHECK:STDOUT: Content-Length: 432{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 1, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "capabilities": { +// CHECK:STDOUT: "declarationProvider": true, +// CHECK:STDOUT: "definitionProvider": true, +// CHECK:STDOUT: "documentFormattingProvider": true, +// CHECK:STDOUT: "documentSymbolProvider": true, +// CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, +// CHECK:STDOUT: "positionEncoding": "utf-16", +// CHECK:STDOUT: "referencesProvider": true, +// CHECK:STDOUT: "textDocumentSync": 2, +// CHECK:STDOUT: "typeDefinitionProvider": true +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 149{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "method": "textDocument/publishDiagnostics", +// CHECK:STDOUT: "params": { +// CHECK:STDOUT: "diagnostics": [], +// CHECK:STDOUT: "uri": "file:///with_self.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 474{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 2, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\n@I.WithSelf.%I.WithSelf.G.decl: @I.WithSelf.%I.WithSelf.G.type (%I.WithSelf.G.type) = fn_decl @I.WithSelf.G [symbolic = @I.WithSelf.%I.WithSelf.G (constants.%I.WithSelf.G)] {} {}\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 85, +// CHECK:STDOUT: "line": 31 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 67, +// CHECK:STDOUT: "line": 31 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 283{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 3, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 26, +// CHECK:STDOUT: "line": 31 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 31 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///with_self.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 283{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 4, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": [ +// CHECK:STDOUT: { +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 37, +// CHECK:STDOUT: "line": 30 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 19, +// CHECK:STDOUT: "line": 30 +// CHECK:STDOUT: } +// CHECK:STDOUT: }, +// CHECK:STDOUT: "uri": "file:///with_self.carbon" +// CHECK:STDOUT: } +// CHECK:STDOUT: ] +// CHECK:STDOUT: }Content-Length: 369{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 5, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\n@I.%Self: %I.type = symbolic_binding Self, 0 [symbolic = constants.%Self]\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 32, +// CHECK:STDOUT: "line": 34 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 27, +// CHECK:STDOUT: "line": 34 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 385{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 6, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": { +// CHECK:STDOUT: "contents": { +// CHECK:STDOUT: "kind": "markdown", +// CHECK:STDOUT: "value": "```semir\nconstants.%I.WithSelf.G.type: type = fn_type @I.WithSelf.G, @I.WithSelf(%Self) [symbolic]\n```\n" +// CHECK:STDOUT: }, +// CHECK:STDOUT: "range": { +// CHECK:STDOUT: "end": { +// CHECK:STDOUT: "character": 69, +// CHECK:STDOUT: "line": 30 +// CHECK:STDOUT: }, +// CHECK:STDOUT: "start": { +// CHECK:STDOUT: "character": 39, +// CHECK:STDOUT: "line": 30 +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: }Content-Length: 51{{\r}} +// CHECK:STDOUT: {{\r}} +// CHECK:STDOUT: { +// CHECK:STDOUT: "id": 7, +// CHECK:STDOUT: "jsonrpc": "2.0", +// CHECK:STDOUT: "result": null +// CHECK:STDOUT: } diff --git a/toolchain/language_server/testdata/text_document/change_unknown.carbon b/toolchain/language_server/testdata/text_document/change_unknown.carbon index 0042117bedff..3e1763851db2 100644 --- a/toolchain/language_server/testdata/text_document/change_unknown.carbon +++ b/toolchain/language_server/testdata/text_document/change_unknown.carbon @@ -21,7 +21,7 @@ // CHECK:STDERR: /test.carbon: warning: unknown file requested [LanguageServerFileUnknown] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -33,6 +33,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/close_unknown.carbon b/toolchain/language_server/testdata/text_document/close_unknown.carbon index 8bc8e2b186e8..27a2ef1e12c3 100644 --- a/toolchain/language_server/testdata/text_document/close_unknown.carbon +++ b/toolchain/language_server/testdata/text_document/close_unknown.carbon @@ -20,7 +20,7 @@ // CHECK:STDERR: /test.carbon: warning: tried closing unknown file; ignoring request [LanguageServerCloseUnknownFile] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -32,6 +32,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/diagnostics.carbon b/toolchain/language_server/testdata/text_document/diagnostics.carbon index e4f0d06359c9..cc5cf2b1e891 100644 --- a/toolchain/language_server/testdata/text_document/diagnostics.carbon +++ b/toolchain/language_server/testdata/text_document/diagnostics.carbon @@ -26,7 +26,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -38,6 +38,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/import_prelude.carbon b/toolchain/language_server/testdata/text_document/import_prelude.carbon index 714f3d8281ed..4ce070001cec 100644 --- a/toolchain/language_server/testdata/text_document/import_prelude.carbon +++ b/toolchain/language_server/testdata/text_document/import_prelude.carbon @@ -28,7 +28,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -40,6 +40,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/incremental_sync.carbon b/toolchain/language_server/testdata/text_document/incremental_sync.carbon index 99d8db499e30..4a0336b5f840 100644 --- a/toolchain/language_server/testdata/text_document/incremental_sync.carbon +++ b/toolchain/language_server/testdata/text_document/incremental_sync.carbon @@ -107,7 +107,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -119,6 +119,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/incremental_sync_multiline.carbon b/toolchain/language_server/testdata/text_document/incremental_sync_multiline.carbon index 434c3bea986b..f750e8f7f816 100644 --- a/toolchain/language_server/testdata/text_document/incremental_sync_multiline.carbon +++ b/toolchain/language_server/testdata/text_document/incremental_sync_multiline.carbon @@ -35,7 +35,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -47,6 +47,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/multiple_files.carbon b/toolchain/language_server/testdata/text_document/multiple_files.carbon index c79b491a528e..ff321df2698a 100644 --- a/toolchain/language_server/testdata/text_document/multiple_files.carbon +++ b/toolchain/language_server/testdata/text_document/multiple_files.carbon @@ -25,7 +25,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -37,6 +37,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/open_change_close.carbon b/toolchain/language_server/testdata/text_document/open_change_close.carbon index 2cfb9c5fa5a2..56adfdd253b6 100644 --- a/toolchain/language_server/testdata/text_document/open_change_close.carbon +++ b/toolchain/language_server/testdata/text_document/open_change_close.carbon @@ -26,7 +26,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -38,6 +38,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/open_duplicate.carbon b/toolchain/language_server/testdata/text_document/open_duplicate.carbon index 76639f2c00ac..3d2fd8312d7f 100644 --- a/toolchain/language_server/testdata/text_document/open_duplicate.carbon +++ b/toolchain/language_server/testdata/text_document/open_duplicate.carbon @@ -25,7 +25,7 @@ // CHECK:STDERR: /test.carbon: warning: duplicate open file request; updating content [LanguageServerOpenDuplicateFile] // CHECK:STDERR: -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -37,6 +37,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/toolchain/language_server/testdata/text_document/open_with_cpp_nonexistent.carbon b/toolchain/language_server/testdata/text_document/open_with_cpp_nonexistent.carbon index 8a122c62cb4b..5328a084d599 100644 --- a/toolchain/language_server/testdata/text_document/open_with_cpp_nonexistent.carbon +++ b/toolchain/language_server/testdata/text_document/open_with_cpp_nonexistent.carbon @@ -22,7 +22,7 @@ // --- AUTOUPDATE-SPLIT -// CHECK:STDOUT: Content-Length: 394{{\r}} +// CHECK:STDOUT: Content-Length: 432{{\r}} // CHECK:STDOUT: {{\r}} // CHECK:STDOUT: { // CHECK:STDOUT: "id": 1, @@ -34,6 +34,7 @@ // CHECK:STDOUT: "documentFormattingProvider": true, // CHECK:STDOUT: "documentSymbolProvider": true, // CHECK:STDOUT: "hoverProvider": true, +// CHECK:STDOUT: "implementationProvider": true, // CHECK:STDOUT: "positionEncoding": "utf-16", // CHECK:STDOUT: "referencesProvider": true, // CHECK:STDOUT: "textDocumentSync": 2, diff --git a/utils/vscode/src/extension.ts b/utils/vscode/src/extension.ts index e53e3f5d9ccf..8a464e983010 100644 --- a/utils/vscode/src/extension.ts +++ b/utils/vscode/src/extension.ts @@ -291,6 +291,9 @@ export function activate(context: ExtensionContext) { const clientOptions: LanguageClientOptions = { documentSelector: [ { scheme: 'file', language: 'carbon' }, + // Test files are sent to the server so that it can provide navigation + // within the SemIR in their `// CHECK:STDOUT:` lines. + { scheme: 'file', language: 'carbon-testdata' }, ], };