Files
carbon-lang/toolchain/language_server/handle_text_document.cpp
T
Richard Smith 03939a9223 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
2026-09-23 23:44:32 +00:00

156 lines
6.4 KiB
C++

// 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 <string>
#include <tuple>
#include <vector>
#include "toolchain/language_server/handle.h"
namespace Carbon::LanguageServer {
// Implements `textDocument/didOpen`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen
auto HandleDidOpenTextDocument(
Context& context, const clang::clangd::DidOpenTextDocumentParams& params)
-> void {
llvm::StringRef filename = params.textDocument.uri.file();
if (!filename.ends_with(".carbon")) {
// Ignore non-Carbon files.
return;
}
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()) {
CARBON_DIAGNOSTIC(LanguageServerOpenDuplicateFile, Warning,
"duplicate open file request; updating content");
context.file_emitter().Emit(filename, LanguageServerOpenDuplicateFile);
}
}
// Returns the result of moving from `cursor_index` past `line_count` lines.
static auto GetNthLineIndex(llvm::StringRef contents, size_t cursor_index,
size_t line_count) -> size_t {
for ([[maybe_unused]] auto _ : llvm::seq(line_count)) {
const size_t newline_index = contents.find('\n', cursor_index);
CARBON_CHECK(newline_index != std::string::npos,
"Line number greater than number of lines in the file");
cursor_index = newline_index + 1;
}
return cursor_index;
}
// Takes start and end positions and returns a tuple with start and end
// offsets. Positions are based on row and column numbers in the source
// code. We often need to know the offsets when modifying strings, so
// this function helps us calculate the offsets. It assumes that the start
// position comes before the end position.
static auto PositionToIndex(llvm::StringRef contents,
const clang::clangd::Position& start,
const clang::clangd::Position& end)
-> std::tuple<size_t, size_t> {
size_t start_index =
GetNthLineIndex(contents, /*cursor_index=*/0, start.line);
size_t end_index = GetNthLineIndex(contents, /*cursor_index=*/start_index,
end.line - start.line);
start_index += start.character;
end_index += end.character;
CARBON_CHECK(end_index <= contents.size(),
"Position greater than source code size");
return {start_index, end_index};
}
// LSP allows full and incremental document synchronization. It sends the
// entire source code when doing full sync. However, when doing incremental
// sync, it only sends the list of changes to be performed on the source
// code. It is necessary to apply changes in the order in which they are
// received. These changes can have one of 1) full document or 2) start
// position, end position, and the text which replaces original text between
// these start and end positions. If the range property is absent, then we
// do full sync. Otherwise, we calculate start and end offsets and replace
// the contents between them with the text we received in change.text.
static auto ApplyChanges(
std::string& source,
const std::vector<clang::clangd::TextDocumentContentChangeEvent>&
content_changes) -> void {
for (const auto& change : content_changes) {
// If range is not present, then we replace entire text.
if (!change.range) {
source = change.text;
continue;
}
// TODO: Use lexer line number data but avoid re-lexing multiple times.
auto [start_index, end_index] =
PositionToIndex(source, change.range->start, change.range->end);
source.replace(start_index, end_index - start_index, change.text);
}
}
// Implements `textDocument/didChange`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange
auto HandleDidChangeTextDocument(
Context& context, const clang::clangd::DidChangeTextDocumentParams& params)
-> void {
llvm::StringRef filename = params.textDocument.uri.file();
if (!filename.ends_with(".carbon")) {
// Ignore non-Carbon files.
return;
}
if (auto* file = context.LookupFile(filename)) {
// We copy the document to a new string, apply changes to the string, and
// set the string as the new text.
std::string source = file->text().str();
ApplyChanges(source, params.contentChanges);
file->SetText(context, params.textDocument.version, source);
}
}
// Implements `textDocument/didSave`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didSave
auto HandleDidSaveTextDocument(
Context& /*context*/,
const clang::clangd::DidSaveTextDocumentParams& /*params*/) -> void {
// Saving doesn't change the content we're tracking: we always use the text
// provided by the client, which is already up-to-date from `didChange`. We
// handle this rather than warning about an unsupported notification.
// TODO: Other open files that import this one may now be stale, because they
// read it from disk rather than from the client. Re-check them here, or
// handle `workspace/didChangeWatchedFiles` instead.
}
// Implements `textDocument/didClose`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose
auto HandleDidCloseTextDocument(
Context& context, const clang::clangd::DidCloseTextDocumentParams& params)
-> void {
llvm::StringRef filename = params.textDocument.uri.file();
if (!filename.ends_with(".carbon")) {
// Ignore non-Carbon files.
return;
}
if (context.files().Erase(filename)) {
// Clear diagnostics when the document closes. Otherwise, any diagnostics
// will linger.
context.PublishDiagnostics({.uri = params.textDocument.uri});
} else {
CARBON_DIAGNOSTIC(LanguageServerCloseUnknownFile, Warning,
"tried closing unknown file; ignoring request");
context.file_emitter().Emit(filename, LanguageServerCloseUnknownFile);
}
}
} // namespace Carbon::LanguageServer