use cpp_code_analysis::{FileView, OutlineSymbol};
use lsp_types::{
ClientCapabilities, DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, OneOf,
Position, Range, ServerCapabilities,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{symbol_kind, uri_to_file_path};
pub async fn on_document_symbol(
context: ServerContextSnapshot,
params: DocumentSymbolParams,
cancel_token: CancellationToken,
) -> RequestOutcome<DocumentSymbolResponse> {
let uri = params.text_document.uri;
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let view = session.view_of_the_file(&path)?;
let outline = session.outline(&view);
Some(DocumentSymbolResponse::Nested(symbols(&view, &outline)))
})
.await
}
pub fn symbols(view: &FileView, outline: &[OutlineSymbol]) -> Vec<DocumentSymbol> {
outline
.iter()
.map(|symbol| document_symbol(view, symbol))
.collect()
}
fn document_symbol(view: &FileView, symbol: &OutlineSymbol) -> DocumentSymbol {
let fact = &symbol.fact;
let whole = range_in_file(view, declaration_range(view, fact));
let name = range_in_file(view, fact.name_range);
let (range, selection_range) = match (whole, name) {
(Some(whole), Some(name)) if name.start >= whole.start && name.end <= whole.end => {
(whole, name)
}
(_, Some(name)) => (name, name),
(Some(whole), None) => (whole, whole),
(None, None) => (Range::default(), Range::default()),
};
#[allow(deprecated)]
DocumentSymbol {
name: fact.name.clone(),
detail: fact.scope.clone(),
kind: symbol_kind(fact.kind),
tags: None,
deprecated: None,
range,
selection_range,
children: Some(
symbol
.children
.iter()
.map(|child| document_symbol(view, child))
.collect(),
),
}
}
fn declaration_range(
view: &FileView,
fact: &cpp_code_analysis::DeclFact,
) -> cpp_parser::SourceRange {
let at = fact.name_range.start_offset;
let Some(at) = view
.reading_offset_of(cpp_code_analysis::file::coordinates::FileOffset::new(at))
.map(|offset| offset.get())
else {
return fact.range;
};
let Some(name_node) = innermost_holding(&view.root, at) else {
return fact.range;
};
let Some(declaration) = name_node.ancestors().find(|node| {
cpp_parser::CppSyntaxKind::from(node.kind()) == cpp_parser::CppSyntaxKind::Declaration
}) else {
return fact.range;
};
let declarators = declaration
.children()
.filter(|inner| {
matches!(
cpp_parser::CppSyntaxKind::from(inner.kind()),
cpp_parser::CppSyntaxKind::InitDeclarator | cpp_parser::CppSyntaxKind::Declarator
)
})
.count();
if declarators != 1 {
return fact.range;
}
let start = declaration_start(&declaration);
let mut end = usize::from(declaration.text_range().end());
let significant_end = declaration
.descendants_with_tokens()
.filter_map(|element| element.into_token())
.filter(|token| !cpp_parser::is_trivia(cpp_parser::CppTokenKind::from(token.kind())))
.map(|token| usize::from(token.text_range().end()))
.collect::<Vec<_>>()
.into_iter()
.next_back();
if let Some(significant_end) = significant_end
&& significant_end < end
{
end = significant_end;
}
let range = cpp_parser::SourceRange::new(start, end.saturating_sub(start));
let range = view.file_range_of(range).unwrap_or(range);
if range.start_offset <= fact.name_range.start_offset
&& range.end_offset() >= fact.name_range.end_offset()
{
range
} else {
fact.range
}
}
fn declaration_start(declaration: &cpp_parser::CppSyntaxNode) -> usize {
let mut children = declaration.children();
let Some(first) = children.next() else {
return usize::from(declaration.text_range().start());
};
if cpp_parser::CppSyntaxKind::from(first.kind()) != cpp_parser::CppSyntaxKind::TemplateDecl {
return usize::from(declaration.text_range().start());
}
children
.next()
.map(|child| usize::from(child.text_range().start()))
.unwrap_or_else(|| usize::from(declaration.text_range().start()))
}
fn innermost_holding(
root: &cpp_parser::CppSyntaxNode,
offset: usize,
) -> Option<cpp_parser::CppSyntaxNode> {
let mut node = root.clone();
while let Some(next) = node.children().find(|child| {
let range = child.text_range();
usize::from(range.start()) <= offset && offset < usize::from(range.end())
}) {
node = next;
}
Some(node)
}
fn range_in_file(view: &FileView, range: cpp_parser::SourceRange) -> Option<Range> {
let text = view.written_text().unwrap_or(&view.source);
let lines = view.written_lines();
Some(Range::new(
file_position(text, lines, range.start_offset)?,
file_position(text, lines, range.end_offset())?,
))
}
fn file_position(text: &str, lines: &cpp_parser::LineIndex, offset: usize) -> Option<Position> {
let (line, column) = lines.position_of(offset, text)?;
let body = crate::util::line_body(text, lines, line)?;
Some(Position::new(
line as u32,
crate::util::utf16_column(body, column),
))
}
pub struct DocumentSymbolCapabilities;
impl RegisterCapabilities for DocumentSymbolCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.document_symbol_provider = Some(OneOf::Left(true));
}
}
#[cfg(test)]
mod a_rendering_is_not_the_file {
use super::*;
#[test]
fn a_declaration_after_a_macro_is_placed_in_the_file() {
let text = "\
#include \"h.h\"
BEFORE
void after_the_macro() {
return;
}
void last_one() {
return;
}
";
let files = cpp_code_analysis::MemoryFiles::new()
.with_file("/p/a.cpp", text)
.with_file(
"/p/h.h",
"#define BEFORE int a; int b; int c; int d; int e;\n",
);
let providers =
cpp_code_analysis::SessionFiles::new(cpp_code_analysis::OpenDocuments::new(), files);
let mut session = cpp_code_analysis::Session::with_config(
"/p",
providers,
cpp_code_analysis::WatchFilter::new("/p"),
cpp_code_analysis::CompilerConfig::default(),
);
let path = std::path::PathBuf::from("/p/a.cpp");
session.load(&path);
session.add_project_files([path.clone()]);
session.index_everything();
session.cook(&path);
let view = session.view(&path).expect("the file is cooked");
assert!(
view.source.matches('\n').count() < text.matches('\n').count(),
"the fixture must make the rendering shorter than the file"
);
let symbols = symbols(&view, &session.outline(&view));
let find = |name: &str| {
symbols
.iter()
.find(|symbol| symbol.name == name)
.unwrap_or_else(|| panic!("`{name}` is in the outline: {symbols:?}"))
};
let after = find("after_the_macro");
assert_eq!(
(
after.range.start.line,
after.range.start.character,
after.range.end.line,
after.range.end.character
),
(4, 0, 6, 1),
"a declaration after a macro is placed in the file, not in the rendering"
);
assert_eq!(find("last_one").range.start.line, 8);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_symbols_range_is_its_whole_declaration() {
let text = "namespace ns {\r\n\tstruct Widget {\r\n\t\tint size;\r\n\t\tvoid f();\r\n\t};\r\n}\r\n";
let (session, path) = a_session(text);
let view = session
.view_of_the_file(&path)
.expect("a view of the file's own text");
let outline = session.outline(&view);
let selected = |range: Range| -> String {
let lines: Vec<&str> = text.split("\r\n").collect();
let from = lines[range.start.line as usize];
let to = lines[range.end.line as usize];
if range.start.line == range.end.line {
from[range.start.character as usize..range.end.character as usize].to_string()
} else {
format!(
"{}…{}",
&from[range.start.character as usize..],
&to[..range.end.character as usize]
)
}
};
let symbols = symbols(&view, &outline);
let ns = &symbols[0];
assert_eq!(ns.name, "ns");
assert_eq!(
selected(ns.range),
"namespace ns {…}",
"a namespace ends at its own `}}`, not at the end of the file: {:?}",
ns.range
);
assert_eq!(
selected(ns.selection_range),
"ns",
"and the name is what a client selects"
);
let members = ns
.children
.as_ref()
.expect("the class is inside the namespace");
let widget = &members[0];
assert_eq!(widget.name, "Widget");
assert_eq!(
selected(widget.range),
"struct Widget {…\t}",
"a class ends at its `}};`, indentation and all: {:?}",
widget.range
);
let body = widget.children.as_ref().expect("its members");
assert_eq!(body[0].name, "size");
assert_eq!(
selected(body[0].range),
"int size;",
"a variable's range is the declaration, type and `;` included: {:?}",
body[0].range
);
assert_eq!(
selected(body[0].selection_range),
"size",
"and its name is inside it, which the protocol requires"
);
assert_eq!(body[1].name, "f");
assert_eq!(
selected(body[1].range),
"void f();",
"and so is a member function's: {:?}",
body[1].range
);
fn check(symbols: &[DocumentSymbol]) {
for symbol in symbols {
let inside = symbol.selection_range.start >= symbol.range.start
&& symbol.selection_range.end <= symbol.range.end;
assert!(
inside,
"`{}` selects {:?} which is not inside {:?}",
symbol.name, symbol.selection_range, symbol.range
);
if let Some(children) = &symbol.children {
check(children);
}
}
}
check(&symbols);
assert_ne!(
body[0].range, widget.range,
"a member is not the class it is written in"
);
assert!(
body[0].range.start >= widget.range.start && body[0].range.end <= widget.range.end,
"and it is written inside it"
);
}
#[test]
fn a_shared_declaration_is_not_attributed_to_one_name() {
let text = "int a = 1, b = 2;\n";
let (session, path) = a_session(text);
let view = session
.view_of_the_file(&path)
.expect("a view of the file's own text");
let outline = session.outline(&view);
let symbols = symbols(&view, &outline);
let mut found: Vec<(String, Range)> = Vec::new();
collect(&symbols, &mut found);
assert_eq!(found.len(), 2, "two names: {found:?}");
let line = "int a = 1, b = 2;";
for (name, range) in &found {
let selected = &line[range.start.character as usize..range.end.character as usize];
assert!(
!selected.starts_with("int"),
"`{name}` must not claim the shared type: {selected:?}"
);
}
}
fn collect(symbols: &[DocumentSymbol], into: &mut Vec<(String, Range)>) {
for symbol in symbols {
into.push((symbol.name.clone(), symbol.range));
if let Some(children) = &symbol.children {
collect(children, into);
}
}
}
#[test]
fn every_symbols_range_encloses_its_name() {
let text = "\
template <class Scenario>
constexpr void test_triviality_of_assignment() {
using Val = TrivialityTester<int, Scenario>;
using E = ExpectedVoid<Val>;
static_assert(is_trivially_assignable_v<E, const E&>
== (is_trivially_constructible_v<Val, const Val&> && is_trivially_assignable_v<Val, const Val&>
&& is_trivially_destructible_v<Val>));
static_assert(is_trivially_assignable_v<E, E&&>
== (is_trivially_constructible_v<Val, Val&&> && is_trivially_assignable_v<Val, Val&&>
&& is_trivially_destructible_v<Val>));
E e1{};
E e2{};
e1 = e2;
}
constexpr bool test_triviality_of_assignment_all() {
test_triviality_of_assignment<TrivialityScenario1>();
return true;
}
";
let (session, path) = a_session(text);
let view = session
.view_of_the_file(&path)
.expect("a view of the file's own text");
let outline = session.outline(&view);
let symbols = symbols(&view, &outline);
let mut failures: Vec<String> = Vec::new();
fn walk(symbols: &[DocumentSymbol], into: &mut Vec<String>) {
for symbol in symbols {
let inside = symbol.selection_range.start >= symbol.range.start
&& symbol.selection_range.end <= symbol.range.end;
if !inside {
into.push(format!(
"`{}`: selects {}:{}..{}:{} which is not inside {}:{}..{}:{}",
symbol.name,
symbol.selection_range.start.line,
symbol.selection_range.start.character,
symbol.selection_range.end.line,
symbol.selection_range.end.character,
symbol.range.start.line,
symbol.range.start.character,
symbol.range.end.line,
symbol.range.end.character,
));
}
if let Some(children) = &symbol.children {
walk(children, into);
}
}
}
walk(&symbols, &mut failures);
fn report(symbols: &[DocumentSymbol], depth: usize) {
for symbol in symbols {
eprintln!(
"{:indent$}{} range={}:{}..{}:{} sel={}:{}..{}:{}",
"",
symbol.name,
symbol.range.start.line,
symbol.range.start.character,
symbol.range.end.line,
symbol.range.end.character,
symbol.selection_range.start.line,
symbol.selection_range.start.character,
symbol.selection_range.end.line,
symbol.selection_range.end.character,
indent = depth * 2
);
if let Some(children) = &symbol.children {
report(children, depth + 1);
}
}
}
report(&symbols, 0);
assert!(
failures.is_empty(),
"every range must enclose the name it is about:\n{}",
failures.join("\n")
);
}
#[test]
fn a_templated_definitions_range_excludes_its_template_head() {
let text = "\
template <class Scenario>
constexpr void test_triviality_of_assignment() {
return;
}
";
let (session, path) = a_session(text);
let view = session
.view_of_the_file(&path)
.expect("a view of the file's own text");
let outline = session.outline(&view);
let symbols = symbols(&view, &outline);
let function = symbols
.iter()
.find(|symbol| symbol.name == "test_triviality_of_assignment")
.expect("the function is in the outline");
assert_eq!(
(
function.range.start.line,
function.range.start.character,
function.range.end.line,
function.range.end.character
),
(1, 0, 3, 1),
"the range is the declaration, `template` header excluded"
);
}
#[test]
fn every_shape_of_declaration_reports_its_own_lines() {
let text = "\
template <class T>
void templated() {
return;
}
void plain() {
return;
}
class Outer {
public:
template <class T>
void member_templated() {
return;
}
void member_plain() {
return;
}
int field;
};
template <class T>
class TemplatedClass {
int inner;
};
template <class T>
using Alias = T;
template <class T>
T global_variable = T();
namespace ns {
template <class T>
void in_namespace() {
return;
}
}
class WithCtor {
public:
WithCtor(int a)
: value(a) {
}
int value;
};
";
let (session, path) = a_session(text);
let view = session
.view_of_the_file(&path)
.expect("a view of the file's own text");
let outline = session.outline(&view);
let symbols = symbols(&view, &outline);
let mut found: Vec<(String, u32, u32)> = Vec::new();
fn collect(symbols: &[DocumentSymbol], into: &mut Vec<(String, u32, u32)>) {
for symbol in symbols {
into.push((
symbol.name.clone(),
symbol.range.start.line,
symbol.range.end.line,
));
if let Some(children) = &symbol.children {
collect(children, into);
}
}
}
collect(&symbols, &mut found);
let numbers: Vec<(&str, u32, u32)> = vec![
("templated", 1, 3),
("plain", 5, 7),
("Outer", 9, 19),
("member_templated", 12, 14),
("member_plain", 15, 17),
("field", 18, 18),
("TemplatedClass", 22, 24),
("inner", 23, 23),
("Alias", 27, 27),
("global_variable", 30, 30),
("in_namespace", 34, 36),
("WithCtor", 39, 45),
("value", 44, 44),
];
for (name, first, last) in numbers {
let (found_first, found_last) = found
.iter()
.find(|(found, ..)| found == name)
.map(|(_, first, last)| (*first, *last))
.unwrap_or_else(|| panic!("`{name}` is in the outline: {found:?}"));
assert_eq!(
(found_first, found_last),
(first, last),
"`{name}` covers lines {first}..={last}"
);
}
}
#[test]
fn an_offset_at_the_start_of_a_line_is_that_line() {
let text = "line0\nline1\nline2\nline3\nvoid after_the_macro() {\n";
let start_of_line_4 = 6 * 4;
assert_eq!(
&text[start_of_line_4..start_of_line_4 + 4],
"void",
"the fixture's offset arithmetic must be right before anything else is asserted"
);
let lines = cpp_parser::LineIndex::parse(text);
let placed =
file_position(text, &lines, start_of_line_4).expect("the offset is in the text");
assert_eq!(
(placed.line, placed.character),
(4, 0),
"an offset at the start of a line is column 0 of it"
);
}
pub(super) fn a_session(
text: &str,
) -> (
cpp_code_analysis::Session<cpp_code_analysis::MemoryFiles>,
std::path::PathBuf,
) {
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
let files = MemoryFiles::new().with_file("/p/a.cpp", text);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.load("/p/a.cpp");
(session, std::path::PathBuf::from("/p/a.cpp"))
}
}