use cpp_code_analysis::{Known, ReferenceKind, Session};
use lsp_types::{ClientCapabilities, Location, ReferenceParams, ServerCapabilities};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{path_to_uri, position_in_file, uri_to_file_path};
pub async fn on_references(
context: ServerContextSnapshot,
params: ReferenceParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<Location>> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let include_declaration = params.context.include_declaration;
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
context.analysis().catch_up(&path).await;
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let written = session.view_of_the_file(&path)?;
let in_the_file = crate::util::offset_at_position(&written, position)?;
if matches!(
session.macro_references(&written, in_the_file),
Known::Yes(_)
) {
return locations(session, &written, in_the_file, include_declaration);
}
match session.symbol_references(
&written,
in_the_file,
cpp_code_analysis::ReferenceBudget::default(),
) {
Known::Yes(found) => Some(
found
.all()
.filter(|(_, reference)| {
include_declaration || !matches!(reference.kind, ReferenceKind::Definition)
})
.filter_map(|(file, reference)| {
let held = session.files().held(file)?;
let start = position_in_file(held, reference.range.start_offset)?;
let end = position_in_file(held, reference.range.end_offset())?;
Some(Location {
uri: path_to_uri(file)?,
range: lsp_types::Range::new(start, end),
})
})
.collect(),
),
Known::No | Known::Unknown(_) => None,
}
})
.await
}
pub fn locations<F: cpp_code_analysis::FileProvider + Clone>(
session: &Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
include_declaration: bool,
) -> Option<Vec<Location>> {
if session.pending() > 0 {
log::info!(
"a reference list while {} file(s) are still queued: uses in those files are not in this answer",
session.pending()
);
}
let Known::Yes(found) = session.macro_references(view, offset) else {
return Some(name_level_locations(
session,
view,
offset,
include_declaration,
));
};
let mut locations = Vec::new();
for file in &found.files {
let Some(held) = session.files().held(&file.file) else {
continue;
};
for reference in &file.references {
if !include_declaration && matches!(reference.kind, ReferenceKind::Definition) {
continue;
}
let Some(uri) = path_to_uri(&file.file) else {
continue;
};
let (Some(start), Some(end)) = (
position_in_file(held, reference.range.start_offset),
position_in_file(held, reference.range.end_offset()),
) else {
continue;
};
locations.push(Location {
uri,
range: lsp_types::Range::new(start, end),
});
}
}
if found.not_looked_at > 0 || !found.unreadable.is_empty() {
log::warn!(
"no reference list for `{}`: {} candidate file(s) were not looked at and {} could not be read",
found.name,
found.not_looked_at,
found.unreadable.len()
);
return None;
}
Some(locations)
}
pub(super) fn macro_at<F: cpp_code_analysis::FileProvider + Clone>(
session: &Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
) -> Option<(String, cpp_parser::SourceRange)> {
let (name, range) =
cpp_code_analysis::sema::resolve::name_at_including_directives(&view.root, offset.get())?;
match session.macro_definition(view, offset) {
Known::Yes(_) => Some((name, range)),
Known::No | Known::Unknown(_) => None,
}
}
pub struct ReferencesCapabilities;
impl RegisterCapabilities for ReferencesCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.references_provider = Some(lsp_types::OneOf::Left(true));
}
}
#[cfg(test)]
mod tests {
use super::locations;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use std::path::PathBuf;
const LIMITS_H: &str = "#define MAX_ITEMS 64\n";
const ITEMS_CPP: &str = "#include \"limits.h\"\nint items[MAX_ITEMS];\n";
fn session_with_an_unread_project() -> Session<MemoryFiles> {
let files = MemoryFiles::new()
.with_file("/p/limits.h", LIMITS_H)
.with_file("/p/items.cpp", ITEMS_CPP);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/limits.h"), PathBuf::from("/p/items.cpp")]);
session.load("/p/items.cpp");
session
}
#[test]
fn a_reference_list_is_partial_while_the_index_has_work_and_complete_after() {
let mut session = session_with_an_unread_project();
let held = session
.files()
.held("/p/items.cpp")
.expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let on_the_use = ITEMS_CPP
.find("MAX_ITEMS")
.expect("the fixture uses the macro");
assert!(
session.pending() > 0,
"the fixture is the state this test is about"
);
let partial = locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(on_the_use),
true,
)
.expect("a list is answered from what the index has, not refused");
assert!(
partial.len() <= 1,
"and it holds only what an unread `items.cpp` can already show: {partial:?}"
);
session.index_everything();
assert_eq!(session.pending(), 0);
let held = session
.files()
.held("/p/items.cpp")
.expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let found = locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(on_the_use),
true,
)
.expect("the index is complete now");
assert_eq!(found.len(), 2, "the `#define` and the one use: {found:?}");
assert!(
found
.iter()
.any(|location| location.uri.as_str().ends_with("limits.h")),
"including the file that defines it: {found:?}"
);
assert_eq!(
locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(on_the_use),
false
)
.expect("complete")
.len(),
1,
"and `includeDeclaration: false` leaves the `#define` out"
);
}
}
const NAME_REFERENCE_BUDGET: usize = 2000;
pub fn name_level_offsets<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
include_declaration: bool,
) -> Vec<(std::path::PathBuf, cpp_parser::SourceRange)> {
let Known::Yes(defined) = session.definitions(view, offset) else {
return Vec::new();
};
let Some(first) = defined.found.first() else {
return Vec::new();
};
let name = first.fact.name.clone();
let owner = the_class_the_cursor_is_in(&view.scopes, &view.root, offset.get());
let defined = narrowed_to_the_cursors_class(view, offset.get(), defined);
let aimed: Vec<&cpp_code_analysis::ProjectDefinition> = defined.found.iter().collect();
let mut declaring: Vec<std::path::PathBuf> = aimed
.iter()
.map(|definition| definition.file.clone())
.collect();
declaring.sort();
declaring.dedup();
let mut found: Vec<(std::path::PathBuf, cpp_parser::SourceRange)> = Vec::new();
let push = |file: &std::path::Path,
range: cpp_parser::SourceRange,
found: &mut Vec<(std::path::PathBuf, cpp_parser::SourceRange)>| {
let key = (file.to_path_buf(), range);
if !found.contains(&key) {
found.push(key);
}
};
if include_declaration {
for definition in &aimed {
push(&definition.file, definition.fact.name_range, &mut found);
}
}
let read =
|path: &std::path::Path| session.files().held(path).map(|file| file.text.to_string());
let references = cpp_code_analysis::index::references::name_references(
session.index(),
&read,
&name,
&declaring,
NAME_REFERENCE_BUDGET,
);
let mut parsed: std::collections::HashMap<
std::path::PathBuf,
Option<cpp_code_analysis::FileView>,
> = std::collections::HashMap::new();
for (file, ranges) in references.found {
let scopes = match &owner {
Some(class) => {
let view = parsed.entry(file.clone()).or_insert_with(|| {
session.files().held(&file).map(|held| {
cpp_code_analysis::FileView::parse_with(
held,
&cpp_parser::MacroEnvironment::default(),
)
})
});
view.as_ref().map(|view| (class, view))
}
None => None,
};
for range in ranges {
if let Some((class, view)) = &scopes
&& an_occurrence_of_another_class(session, view, range.start_offset, class)
{
continue;
}
push(&file, range, &mut found);
}
}
found
}
fn name_level_locations<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
include_declaration: bool,
) -> Vec<Location> {
name_level_offsets(session, view, offset, include_declaration)
.into_iter()
.filter_map(|(file, range)| {
let uri = crate::util::path_to_uri(&file)?;
let (Some(start), Some(end)) = (
position_of(session, &file, range.start_offset),
position_of(session, &file, range.end_offset()),
) else {
return None;
};
Some(Location {
uri,
range: lsp_types::Range { start, end },
})
})
.collect()
}
fn position_of<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
file: &std::path::Path,
offset: usize,
) -> Option<lsp_types::Position> {
if let Some(held) = session.files().held(file) {
return crate::util::position_in(&held.text, &held.line_index, offset);
}
let text = std::fs::read_to_string(file).ok()?;
let index = cpp_parser::LineIndex::parse(&text);
crate::util::position_in(&text, &index, offset)
}
pub fn the_class_the_cursor_is_in(
scopes: &cpp_code_analysis::ScopeTree,
root: &cpp_parser::CppSyntaxNode,
offset: usize,
) -> Option<String> {
if let Some(id) = scopes.scope_at(offset)
&& let Some(class) = scopes.scope_chain(id).into_iter().find_map(|id| {
let scope = scopes.scope(id)?;
(scope.kind == cpp_code_analysis::ScopeKind::Class)
.then(|| scope.name.as_ref().map(|name| name.to_string()))
.flatten()
})
{
return Some(class);
}
let token = cpp_parser::token_at(root, offset)?;
let definition = token.parent_ancestors().find(|node| {
matches!(
cpp_parser::CppSyntaxKind::from(node.kind()),
cpp_parser::CppSyntaxKind::FunctionDef | cpp_parser::CppSyntaxKind::Declaration
)
})?;
let text = definition.text().to_string();
let before_the_parameters = text.split('(').next()?.trim();
let (before_the_name, _) = before_the_parameters.rsplit_once("::")?;
before_the_name
.rsplit(|character: char| !(character.is_alphanumeric() || character == '_'))
.next()
.map(str::trim)
.filter(|class| !class.is_empty())
.map(str::to_string)
}
pub(crate) fn narrowed_to_the_cursors_class(
view: &cpp_code_analysis::FileView,
offset: usize,
found: cpp_code_analysis::ProjectDefinitions,
) -> cpp_code_analysis::ProjectDefinitions {
let Some(class) = the_class_the_cursor_is_in(&view.scopes, &view.root, offset) else {
return found;
};
let inside: Vec<cpp_code_analysis::ProjectDefinition> = found
.found
.iter()
.filter(|definition| definition.fact.scope.as_deref() == Some(class.as_str()))
.cloned()
.collect();
if inside.is_empty() {
return found;
}
let mut narrowed = found;
narrowed.found = inside;
narrowed
}
fn an_occurrence_of_another_class<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
view: &cpp_code_analysis::FileView,
offset: usize,
owner: &str,
) -> bool {
use cpp_code_analysis::Known;
if let Some(access) = cpp_code_analysis::sema::resolve::member_access_at(&view.root, offset)
&& access.member_range.start_offset == offset
{
let at = cpp_code_analysis::file::coordinates::ReadingOffset::new(usize::from(
access.object.text_range().start(),
));
return match session.type_at(view, at) {
Known::Yes(found) => {
cpp_code_analysis::sema::types::parse_type_spelling(&found.type_of)
.class_name()
.is_some_and(|class| {
crate::handlers::implementation::last_segment_of(class) != owner
})
}
_ => false,
};
}
match the_class_the_cursor_is_in(&view.scopes, &view.root, offset) {
Some(class) => class != owner,
None => false,
}
}
#[cfg(test)]
mod name_level {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::name_level_locations;
const BASE_H: &str = "struct Widget {\n int size;\n};\n";
const USE_CPP: &str = "#include \"base.h\"\nvoid f(Widget w) {\n (void)w.size;\n}\n";
#[test]
fn a_class_declared_in_a_header_is_referenced_from_the_file_that_includes_it() {
let files = MemoryFiles::new()
.with_file("/p/base.h", BASE_H)
.with_file("/p/use.cpp", USE_CPP);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/base.h"), PathBuf::from("/p/use.cpp")]);
session.index_everything();
let use_path = PathBuf::from("/p/use.cpp");
let held = session.files().held(&use_path).expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let at = USE_CPP.find("Widget").expect("the fixture uses it");
let locations = name_level_locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
true,
);
assert!(
locations
.iter()
.any(|found| found.uri.path().ends_with("base.h")),
"the declaration in the header is one of the references: {locations:?}"
);
assert!(
locations
.iter()
.any(|found| found.uri.path().ends_with("use.cpp")),
"and the use in the file that included it: {locations:?}"
);
}
}
#[cfg(test)]
mod a_member_of_one_class {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::name_level_locations;
const BASE_H: &str = "\
struct First {
int _value;
void update();
};
struct Second {
int _value;
void update();
};
";
const USE_CPP: &str = "\
#include \"base.h\"
void First::update() { _value = 1; }
void Second::update() { _value = 2; }
";
#[test]
fn is_not_another_class_member_and_the_header_declaration_counts() {
let files = MemoryFiles::new()
.with_file("/p/base.h", BASE_H)
.with_file("/p/use.cpp", USE_CPP);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/base.h"), PathBuf::from("/p/use.cpp")]);
session.index_everything();
let path = PathBuf::from("/p/use.cpp");
let held = session.files().held(&path).expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let at = USE_CPP
.find("_value = 1")
.expect("the fixture writes the member in First::update");
let locations = name_level_locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
true,
);
println!(
"references: {:?}",
locations
.iter()
.map(|found| (found.uri.path().to_string(), found.range.start.line))
.collect::<Vec<_>>()
);
assert!(
locations
.iter()
.any(|found| found.uri.path().ends_with("base.h")),
"the declaration in the header is a reference: {locations:?}"
);
assert_eq!(
locations
.iter()
.filter(|found| found.uri.path().ends_with("use.cpp"))
.count(),
1,
"one use — `First::update`'s — and not `Second`'s: {locations:?}"
);
}
}
#[cfg(test)]
mod a_file_the_session_does_not_hold {
use super::position_of;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
#[test]
fn is_positioned_even_though_the_vfs_has_never_seen_it() {
let root = std::env::temp_dir().join("cpp_ls-references-disk");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch directory");
let header = root.join("only_on_disk.h");
std::fs::write(&header, "struct Widget {\n int size;\n};\n").expect("the fixture");
let files = MemoryFiles::new();
let providers = SessionFiles::new(OpenDocuments::new(), files);
let session = Session::with_config(
&root,
providers,
WatchFilter::new(&root),
CompilerConfig::default(),
);
assert!(
session.files().held(&header).is_none(),
"the fixture is the state this test is about"
);
let at = "struct Widget {\n int ".len();
let found =
position_of(&session, &header, at).expect("the file is read for its line index");
println!("{header:?} at {at} → {found:?}");
assert_eq!(found.line, 1);
assert_eq!(found.character, 8);
}
}
#[cfg(test)]
mod a_definition_in_a_class {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
#[test]
fn is_the_class_the_cursor_is_in() {
let base = "\
struct First {
int _value;
void update();
};
struct Second {
int _value;
void update();
};
";
let use_cpp = "\
#include \"base.h\"
void First::update() { _value = 1; }
";
let files = MemoryFiles::new()
.with_file("/p/base.h", base)
.with_file("/p/use.cpp", use_cpp);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/base.h"), PathBuf::from("/p/use.cpp")]);
session.index_everything();
let path = PathBuf::from("/p/use.cpp");
let held = session.files().held(&path).expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let at = use_cpp.find("_value").expect("the fixture uses it");
let cpp_code_analysis::Known::Yes(found) = session.definitions(
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
) else {
panic!("`_value` is declared");
};
println!(
"before: {:?}",
found
.found
.iter()
.map(|definition| definition.fact.qualified_name())
.collect::<Vec<_>>()
);
assert_eq!(found.found.len(), 2, "the fixture is two classes' members");
let narrowed = super::narrowed_to_the_cursors_class(&view, at, found);
let names: Vec<String> = narrowed
.found
.iter()
.map(|definition| definition.fact.qualified_name())
.collect();
println!("after: {names:?}");
assert_eq!(
names,
["First::_value"],
"the cursor is in `First::update`, so that is the declaration meant"
);
}
}
#[cfg(test)]
mod a_receiver_of_another_class {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::name_level_locations;
const BASE_H: &str = "\
struct First {
int _value;
void update();
};
struct Second {
int _value;
void update();
};
";
const USE_CPP: &str = "\
#include \"base.h\"
void First::update() { _value = 1; }
void Second::update() {
First first;
first._value = 2; // First's member, reached through a First
_value = 3; // Second's member
}
";
#[test]
fn is_the_receivers_and_a_bare_name_is_the_enclosing_class() {
let files = MemoryFiles::new()
.with_file("/p/base.h", BASE_H)
.with_file("/p/use.cpp", USE_CPP);
let providers = SessionFiles::new(OpenDocuments::new(), files);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([PathBuf::from("/p/base.h"), PathBuf::from("/p/use.cpp")]);
session.index_everything();
let path = PathBuf::from("/p/use.cpp");
let held = session.files().held(&path).expect("the file is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let at = USE_CPP
.find("_value = 1")
.expect("the fixture writes it in First::update");
let found = name_level_locations(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
true,
);
let lines: Vec<u32> = found
.iter()
.filter(|location| location.uri.path().ends_with("use.cpp"))
.map(|location| location.range.start.line)
.collect();
println!("lines in use.cpp for First::_value: {lines:?}");
assert_eq!(
lines,
[1, 4],
"the bare use in `First::update` and the receiver-qualified one — not `Second`'s own"
);
}
}