Files
carbon-lang/toolchain/language_server/handle_position.cpp
T
Richard Smith c78751338b language-server: Support simple semantic queries. (#7639)
Add support for "jump to declaration", "find references", type
information on hover. This support is strictly single-file for now; only
references and declarations within the same file are found. We could go
a bit beyond that, but to properly handle cross-file references we'll
need to build an index and a compilation database, which is beyond the
scope of this change.

On hover, we provide the type information for the instruction under the
cursor as-is. This is frequently not very useful, as the type of a
function F is simply "<type of F>", but is a starting point for richer
information.

Assisted-by: Claude Code
2026-08-19 14:35:05 +00:00

195 lines
7.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 <optional>
#include <string>
#include <vector>
#include "common/raw_string_ostream.h"
#include "toolchain/language_server/handle.h"
#include "toolchain/language_server/position.h"
#include "toolchain/language_server/sem_ir_index.h"
#include "toolchain/sem_ir/file.h"
#include "toolchain/sem_ir/stringify.h"
#include "toolchain/sem_ir/typed_insts.h"
namespace Carbon::LanguageServer {
// Returns the type of `inst_id` rendered as Carbon source, or an empty string
// if it has no type. Instructions that aren't values, such as declarations of
// namespaces, have no type to show.
//
// TODO: `StringifyConstantInst` renders some types as placeholders such as
// `<type of F>` for a function and `<pattern for i32>` for a binding pattern,
// which is unhelpful as hover text. Show the signature for a function, and the
// bound type rather than the pattern type for a binding.
static auto StringifyTypeOfInst(const SemIR::File& sem_ir,
SemIR::InstId inst_id) -> std::string {
auto type_id = sem_ir.insts().Get(inst_id).type_id();
if (!type_id.has_value()) {
return "";
}
return SemIR::StringifyConstantInst(sem_ir,
sem_ir.types().GetTypeInstId(type_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.
template <typename ResponseType>
static auto FindInstAtPositionOrFail(
Context& context, const clang::clangd::TextDocumentPositionParams& params,
llvm::function_ref<auto(llvm::Expected<ResponseType>)->void> on_done)
-> PositionInfo {
auto* file = context.LookupFile(params.textDocument.uri.file());
if (!file) {
on_done(llvm::make_error<clang::clangd::LSPError>(
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;
}
// Implements `textDocument/hover`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_hover
auto HandleHover(
Context& context, const clang::clangd::TextDocumentPositionParams& params,
llvm::function_ref<auto(llvm::Expected<clang::clangd::Hover>)->void>
on_done) -> void {
auto info = FindInstAtPositionOrFail(context, params, on_done);
if (!info.has_inst()) {
return;
}
const auto& sem_ir = *info.file->sem_ir();
RawStringOstream text;
text << "```carbon\n" << info.file->tokens().GetTokenText(info.token);
if (auto type = StringifyTypeOfInst(sem_ir, info.inst_id); !type.empty()) {
text << ": " << type;
}
text << "\n```";
on_done(clang::clangd::Hover{
.contents = {.kind = clang::clangd::MarkupKind::Markdown,
.value = text.TakeStr()},
.range = GetTokenRange(info.file->tokens(), info.token)});
}
// Shared implementation of the goto-style requests, which differ only in which
// instruction they resolve to.
static auto HandleGoto(
Context& context, const clang::clangd::TextDocumentPositionParams& params,
bool use_type,
llvm::function_ref<
auto(llvm::Expected<std::vector<clang::clangd::Location>>)->void>
on_done) -> void {
auto info = FindInstAtPositionOrFail(context, params, on_done);
if (!info.has_inst()) {
return;
}
const auto& sem_ir = *info.file->sem_ir();
auto target_id = GetReferencedInst(sem_ir, info.inst_id);
if (use_type) {
auto type_id = sem_ir.insts().Get(target_id).type_id();
if (!type_id.has_value()) {
on_done(std::vector<clang::clangd::Location>());
return;
}
target_id = sem_ir.types().GetTypeInstId(type_id);
}
std::vector<clang::clangd::Location> locations;
if (auto location = GetInstLocation(*info.file, target_id)) {
locations.push_back(*location);
}
on_done(std::move(locations));
}
// Implements `textDocument/definition` and `textDocument/declaration`:
// 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<std::vector<clang::clangd::Location>>)->void>
on_done) -> void {
HandleGoto(context, params, /*use_type=*/false, on_done);
}
// Implements `textDocument/typeDefinition`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_typeDefinition
auto HandleTypeDefinition(
Context& context, const clang::clangd::TextDocumentPositionParams& params,
llvm::function_ref<
auto(llvm::Expected<std::vector<clang::clangd::Location>>)->void>
on_done) -> void {
HandleGoto(context, params, /*use_type=*/true, on_done);
}
// Implements `textDocument/references`:
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_references
//
// Only finds references within the file being edited. Without a project-wide
// index there's no way to see other files, so results may be incomplete.
auto HandleReferences(
Context& context, const clang::clangd::ReferenceParams& params,
llvm::function_ref<
auto(llvm::Expected<std::vector<clang::clangd::Location>>)->void>
on_done) -> void {
auto info = FindInstAtPositionOrFail(context, params, on_done);
if (!info.has_inst()) {
return;
}
const auto& sem_ir = *info.file->sem_ir();
auto target_token =
GetInstNameToken(*info.file, GetReferencedInst(sem_ir, info.inst_id));
if (!target_token.has_value()) {
on_done(std::vector<clang::clangd::Location>());
return;
}
// This is the one request the token index can't serve: it needs every
// instruction referring to an entity, which is the opposite direction from
// the index. A scan is inherent, and cheap next to the compile that produced
// the IR.
std::vector<clang::clangd::Location> locations;
if (params.context.includeDeclaration) {
locations.push_back(
{.uri = info.file->uri(),
.range = GetTokenRange(info.file->tokens(), target_token)});
}
for (auto [inst_id, inst] : sem_ir.insts().enumerate()) {
auto name_ref = inst.TryAs<SemIR::NameRef>();
if (!name_ref ||
GetInstNameToken(*info.file, name_ref->value_id) != target_token) {
continue;
}
if (auto location = GetInstLocation(*info.file, inst_id)) {
locations.push_back(*location);
}
}
on_done(std::move(locations));
}
} // namespace Carbon::LanguageServer