mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-24 22:02:23 +01:00
When computing the token range of a declaration, we were using the first and last parse nodes to determine the first and last tokens. That's not correct -- the parse tree nodes can be in a different order from the tokens, so the first parse node need not be at the start of the declaration. For a malformed declaration such as `fn f` (with no terminator), the first and last parse nodes were both associated with the `fn` token for error recovery, meaning that the function name wasn't even within the symbol we handed back to the LSP client, in violation of the LSP requirements. This led to VS Code producing an error and discarding all symbols in the file. Assisted-by: Claude Code
208 lines
7.2 KiB
C++
208 lines
7.2 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 <utility>
|
|
#include <vector>
|
|
|
|
#include "common/check.h"
|
|
#include "toolchain/language_server/handle.h"
|
|
#include "toolchain/lex/token_index.h"
|
|
#include "toolchain/lex/token_kind.h"
|
|
#include "toolchain/parse/node_ids.h"
|
|
#include "toolchain/parse/node_kind.h"
|
|
#include "toolchain/parse/tree_and_subtrees.h"
|
|
|
|
namespace Carbon::LanguageServer {
|
|
|
|
// Returns the token of first child of kind IdentifierNameMaybeBeforeSignature
|
|
// or IdentifierNameNotBeforeSignature.
|
|
static auto GetSymbolIdentifier(const Parse::TreeAndSubtrees& tree_and_subtrees,
|
|
Parse::NodeId node)
|
|
-> std::optional<Lex::TokenIndex> {
|
|
const auto& tokens = tree_and_subtrees.tree().tokens();
|
|
for (auto child : tree_and_subtrees.children(node)) {
|
|
switch (tree_and_subtrees.tree().node_kind(child)) {
|
|
case Parse::NodeKind::IdentifierNameMaybeBeforeSignature:
|
|
case Parse::NodeKind::IdentifierNameNotBeforeSignature: {
|
|
auto token = tree_and_subtrees.tree().node_token(child);
|
|
if (tokens.GetKind(token) == Lex::TokenKind::Identifier) {
|
|
return token;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
// Helper class to collect all symbols during traversal of AST.
|
|
// Symbols are "open" when their signature has been declared but their body is
|
|
// still ongoing. New symbols are added to last open symbol, or top level list
|
|
// if no symbols are open.
|
|
class SymbolStore {
|
|
public:
|
|
// Adds a symbol with no children.
|
|
auto AddSymbol(clang::clangd::DocumentSymbol symbol) -> void {
|
|
if (open_symbols_.empty()) {
|
|
top_level_symbols_.push_back(std::move(symbol));
|
|
} else {
|
|
open_symbols_.back().children.push_back(symbol);
|
|
}
|
|
}
|
|
|
|
// Starts a symbol potentially with children.
|
|
auto StartSymbol(clang::clangd::DocumentSymbol symbol) -> void {
|
|
open_symbols_.push_back(std::move(symbol));
|
|
}
|
|
|
|
auto HasOpenSymbol() const -> bool { return !open_symbols_.empty(); }
|
|
|
|
// Completes a symbol, appending to parent list.
|
|
auto EndSymbol(const Lex::TokenizedBuffer* tokens = nullptr,
|
|
std::optional<Lex::TokenIndex> end_token = std::nullopt)
|
|
-> void {
|
|
CARBON_CHECK(HasOpenSymbol());
|
|
// Extend the range of the symbol to include the `end_token`, if one is
|
|
// provided. For example, this extends function definitions to the `}` or,
|
|
// for a terse function body, `;`.
|
|
if (tokens && end_token) {
|
|
auto [end_line, end_col] = tokens->GetEndLoc(*end_token);
|
|
open_symbols_.back().range.end = {.line = end_line.index,
|
|
.character = end_col - 1};
|
|
}
|
|
AddSymbol(open_symbols_.pop_back_val());
|
|
}
|
|
|
|
// Returns final top level symbols.
|
|
auto Collect() -> std::vector<clang::clangd::DocumentSymbol> {
|
|
while (HasOpenSymbol()) {
|
|
EndSymbol();
|
|
}
|
|
return std::move(top_level_symbols_);
|
|
}
|
|
|
|
private:
|
|
std::vector<clang::clangd::DocumentSymbol> top_level_symbols_;
|
|
llvm::SmallVector<clang::clangd::DocumentSymbol> open_symbols_;
|
|
};
|
|
|
|
// Constructs a Range from a closed interval of tokens [start, end].
|
|
static 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},
|
|
};
|
|
}
|
|
|
|
// Finds a spanning range for the provided parse node.
|
|
static auto GetSymbolRange(const Parse::TreeAndSubtrees& tree_and_subtrees,
|
|
const Parse::NodeId& ast_node)
|
|
-> clang::clangd::Range {
|
|
auto token_range = tree_and_subtrees.GetSubtreeTokenRange(ast_node);
|
|
return GetTokenRange(tree_and_subtrees.tree().tokens(), token_range.begin,
|
|
token_range.end);
|
|
}
|
|
|
|
auto HandleDocumentSymbol(
|
|
Context& context, const clang::clangd::DocumentSymbolParams& params,
|
|
llvm::function_ref<
|
|
auto(llvm::Expected<std::vector<clang::clangd::DocumentSymbol>>)->void>
|
|
on_done) -> void {
|
|
auto* file = context.LookupFile(params.textDocument.uri.file());
|
|
if (!file) {
|
|
return;
|
|
}
|
|
|
|
const auto& tree_and_subtrees = file->tree_and_subtrees();
|
|
const auto& tree = tree_and_subtrees.tree();
|
|
const auto& tokens = tree.tokens();
|
|
|
|
SymbolStore symbols;
|
|
for (const auto& node_id : tree.postorder()) {
|
|
auto node_kind = tree.node_kind(node_id);
|
|
clang::clangd::SymbolKind symbol_kind;
|
|
bool is_leaf = false;
|
|
switch (node_kind) {
|
|
case Parse::NodeKind::FunctionDecl:
|
|
is_leaf = true;
|
|
symbol_kind = clang::clangd::SymbolKind::Function;
|
|
break;
|
|
case Parse::NodeKind::FunctionDefinitionStart:
|
|
case Parse::NodeKind::BuiltinFunctionDefinitionStart:
|
|
symbol_kind = clang::clangd::SymbolKind::Function;
|
|
break;
|
|
case Parse::NodeKind::Namespace:
|
|
is_leaf = true;
|
|
symbol_kind = clang::clangd::SymbolKind::Namespace;
|
|
break;
|
|
case Parse::NodeKind::InterfaceDecl:
|
|
case Parse::NodeKind::NamedConstraintDecl:
|
|
is_leaf = true;
|
|
symbol_kind = clang::clangd::SymbolKind::Interface;
|
|
break;
|
|
case Parse::NodeKind::InterfaceDefinitionStart:
|
|
case Parse::NodeKind::NamedConstraintDefinitionStart:
|
|
symbol_kind = clang::clangd::SymbolKind::Interface;
|
|
break;
|
|
case Parse::NodeKind::ClassDecl:
|
|
is_leaf = true;
|
|
symbol_kind = clang::clangd::SymbolKind::Class;
|
|
break;
|
|
case Parse::NodeKind::ClassDefinitionStart:
|
|
symbol_kind = clang::clangd::SymbolKind::Class;
|
|
break;
|
|
case Parse::NodeKind::ChoiceDefinitionStart:
|
|
symbol_kind = clang::clangd::SymbolKind::Enum;
|
|
break;
|
|
|
|
case Parse::NodeKind::FunctionDefinition:
|
|
case Parse::NodeKind::FunctionTerseDefinition:
|
|
case Parse::NodeKind::BuiltinFunctionDefinition:
|
|
case Parse::NodeKind::NamedConstraintDefinition:
|
|
case Parse::NodeKind::InterfaceDefinition:
|
|
case Parse::NodeKind::ClassDefinition:
|
|
case Parse::NodeKind::ChoiceDefinition: {
|
|
if (symbols.HasOpenSymbol()) {
|
|
// Symbols definition has completed, pop it from stack and add to
|
|
// parent/root.
|
|
symbols.EndSymbol(&tokens, tree.node_token(node_id));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
if (auto identifier = GetSymbolIdentifier(tree_and_subtrees, node_id)) {
|
|
clang::clangd::DocumentSymbol symbol{
|
|
.name = std::string(tokens.GetTokenText(*identifier)),
|
|
.kind = symbol_kind,
|
|
.range = GetSymbolRange(tree_and_subtrees, node_id),
|
|
.selectionRange = GetTokenRange(tokens, *identifier, *identifier),
|
|
};
|
|
|
|
if (is_leaf) {
|
|
symbols.AddSymbol(std::move(symbol));
|
|
} else {
|
|
symbols.StartSymbol(std::move(symbol));
|
|
}
|
|
}
|
|
}
|
|
|
|
on_done(symbols.Collect());
|
|
}
|
|
|
|
} // namespace Carbon::LanguageServer
|