Files
carbon-lang/toolchain/language_server/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

135 lines
4.6 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 "toolchain/language_server/position.h"
#include <algorithm>
#include <iterator>
#include <optional>
#include <utility>
#include "llvm/ADT/STLExtras.h"
#include "toolchain/language_server/sem_ir_index.h"
#include "toolchain/parse/tree.h"
#include "toolchain/sem_ir/file.h"
#include "toolchain/sem_ir/typed_insts.h"
namespace Carbon::LanguageServer {
auto GetTokenRange(const Lex::TokenizedBuffer& tokens, Lex::TokenIndex start,
Lex::TokenIndex end) -> clang::clangd::Range {
auto start_line = tokens.GetLine(start);
auto start_col = tokens.GetColumnNumber(start);
auto [end_line, end_col] = tokens.GetEndLoc(end);
return clang::clangd::Range{
.start = {.line = start_line.index, .character = start_col - 1},
.end = {.line = end_line.index, .character = end_col - 1},
};
}
// Returns the 0-based (line, column) where a token starts, for comparison
// against an LSP position.
static auto GetTokenStart(const Lex::TokenizedBuffer& tokens,
Lex::TokenIndex token) -> std::pair<int, int> {
return {tokens.GetLine(token).index, tokens.GetColumnNumber(token) - 1};
}
auto FindToken(const Lex::TokenizedBuffer& tokens,
const clang::clangd::Position& position) -> Lex::TokenIndex {
std::pair<int, int> target = {position.line, position.character};
// Tokens are in source order, so find the first one starting after
// `position`; only the token before that can contain it. Searching on
// (line, column) avoids converting the position to a byte offset, which would
// mean rescanning the text for line boundaries.
//
// `tokens()` excludes the recovery tokens added after lexing finished, which
// matters here: those are appended rather than inserted in source order, so
// including them would break the ordering this search relies on.
auto range = tokens.tokens();
auto after = llvm::partition_point(range, [&](Lex::TokenIndex token) {
return GetTokenStart(tokens, token) <= target;
});
if (after == range.begin()) {
return Lex::TokenIndex::None;
}
// The position is in that token only if it's also before the token's end;
// otherwise it falls in whitespace or a comment.
auto token = *std::prev(after);
auto [end_line, end_col] = tokens.GetEndLoc(token);
if (target < std::pair<int, int>(end_line.index, end_col - 1)) {
return token;
}
return Lex::TokenIndex::None;
}
auto FindPositionInfo(const Context::File& file,
const clang::clangd::Position& position) -> PositionInfo {
const auto* sem_ir = file.sem_ir();
const auto* index = file.sem_ir_index();
if (!sem_ir || !index) {
return {};
}
auto token = FindToken(file.tokens(), position);
if (!token.has_value()) {
return {};
}
PositionInfo info = {.file = &file, .token = token};
auto insts = index->InstsForToken(token);
for (auto inst_id : insts) {
// Prefer a name reference: a token such as the name in `fn F()` also has
// instructions for the declaration itself, but a request at a name is about
// the name.
if (sem_ir->insts().Is<SemIR::NameRef>(inst_id)) {
info.inst_id = inst_id;
return info;
}
}
if (!insts.empty()) {
info.inst_id = insts.front();
}
return info;
}
auto GetReferencedInst(const SemIR::File& sem_ir, SemIR::InstId inst_id)
-> SemIR::InstId {
if (auto name_ref = sem_ir.insts().TryGetAs<SemIR::NameRef>(inst_id)) {
return name_ref->value_id;
}
return inst_id;
}
auto GetInstNameToken(const Context::File& file, SemIR::InstId inst_id)
-> Lex::TokenIndex {
const auto* sem_ir = file.sem_ir();
if (!sem_ir || !inst_id.has_value()) {
return Lex::TokenIndex::None;
}
auto loc_id = sem_ir->insts().GetCanonicalLocId(inst_id);
if (loc_id.kind() != SemIR::LocId::Kind::NodeId) {
// Imported from another file, which we can't yet name a location in.
return Lex::TokenIndex::None;
}
auto node_id = loc_id.node_id();
if (!node_id.has_value()) {
return Lex::TokenIndex::None;
}
return GetNameToken(file.tree_and_subtrees(), node_id);
}
auto GetInstLocation(const Context::File& file, SemIR::InstId inst_id)
-> std::optional<clang::clangd::Location> {
auto token = GetInstNameToken(file, inst_id);
if (!token.has_value()) {
return std::nullopt;
}
return clang::clangd::Location{.uri = file.uri(),
.range = GetTokenRange(file.tokens(), token)};
}
} // namespace Carbon::LanguageServer