use cpp_code_analysis::{FileProvider, Known, Session};
use lsp_types::{
ClientCapabilities, ImplementationProviderCapability, Location, Range, ServerCapabilities,
request::{GotoImplementationParams, GotoImplementationResponse},
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{offset_at_position, path_to_uri, position_in_file, uri_to_file_path};
pub async fn on_goto_implementation(
context: ServerContextSnapshot,
params: GotoImplementationParams,
cancel_token: CancellationToken,
) -> RequestOutcome<GotoImplementationResponse> {
let uri = params.text_document_position_params.text_document.uri;
let position = params.text_document_position_params.position;
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 offset = offset_at_position(&view, position)?;
let Known::Yes(defined) = session.definitions(&view, offset) else {
return None;
};
let name = defined.found.first()?.fact.name.clone();
let mut found = definitions_of(session, &view, offset.get());
found.extend(implementations_of(session, &name));
let mut seen: Vec<(String, String)> = Vec::new();
found.retain(|location| {
let key = (location.uri.to_string(), format!("{:?}", location.range));
if seen.contains(&key) {
false
} else {
seen.push(key);
true
}
});
if found.is_empty() {
return None;
}
Some(GotoImplementationResponse::Array(found))
})
.await
}
pub fn definitions_of<F: FileProvider + Clone>(
session: &Session<F>,
view: &cpp_code_analysis::FileView,
offset: usize,
) -> Vec<Location> {
let Known::Yes(found) = session.definitions(
view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(offset),
) else {
return Vec::new();
};
let owner =
crate::handlers::references::the_class_the_cursor_is_in(&view.scopes, &view.root, offset);
let mut wanted: Vec<(String, Option<String>)> = found
.found
.iter()
.filter(|definition| definition.fact.kind == cpp_code_analysis::DeclKind::Function)
.map(|definition| (definition.fact.name.clone(), owner.clone()))
.collect();
wanted.sort();
wanted.dedup();
if wanted.is_empty() {
return Vec::new();
}
let mut locations: Vec<Location> = Vec::new();
let summaries = session.index().all_summaries();
for (path, summary) in &summaries {
for fact in &summary.declarations {
if fact.kind != cpp_code_analysis::DeclKind::Function {
continue;
}
if !wanted.iter().any(|(name, scope)| {
fact.name == *name && the_same_class(fact.scope.as_deref(), scope.as_deref())
}) {
continue;
}
if !ends_with_a_body(session, path, fact.range) {
continue;
}
push_location(session, path, fact.name_range, &mut locations);
}
}
locations
}
fn the_same_class(left: Option<&str>, right: Option<&str>) -> bool {
match (left, right) {
(None, None) => true,
(Some(left), Some(right)) => {
left == right || left.rsplit("::").next() == right.rsplit("::").next()
}
_ => false,
}
}
fn ends_with_a_body<F: FileProvider + Clone>(
session: &Session<F>,
path: &std::path::Path,
range: cpp_parser::SourceRange,
) -> bool {
let text = session
.files()
.held(path)
.map(|held| held.text.to_string())
.or_else(|| std::fs::read_to_string(path).ok());
let Some(text) = text else {
return false;
};
text.get(range.start_offset..range.end_offset())
.is_some_and(|declaration| declaration.trim_end().ends_with('}'))
}
pub fn implementations_of<F: FileProvider + Clone>(
session: &Session<F>,
name: &str,
) -> Vec<Location> {
let summaries = session.index().all_summaries();
let mut owners: Vec<String> = Vec::new();
for (_, summary) in &summaries {
for fact in &summary.declarations {
if fact.name == name
&& let Some(scope) = fact.scope.as_deref()
{
owners.push(last_segment(scope).to_string());
}
}
}
let mut found: Vec<Location> = Vec::new();
let mut derived: Vec<String> = Vec::new();
for (path, summary) in &summaries {
for fact in &summary.declarations {
let derives_from_the_name = fact.bases.iter().any(|base| {
let segment = last_segment(base);
segment == name || owners.iter().any(|owner| owner == segment)
});
if derives_from_the_name {
derived.push(fact.name.clone());
push_location(session, path, fact.name_range, &mut found);
}
}
}
if derived.is_empty() {
return found;
}
for (path, summary) in &summaries {
for fact in &summary.declarations {
if fact.name != name {
continue;
}
let Some(scope) = fact.scope.as_deref() else {
continue;
};
if derived.iter().any(|class| class == last_segment(scope)) {
push_location(session, path, fact.name_range, &mut found);
}
}
}
found
}
pub(crate) fn last_segment_of(spelling: &str) -> &str {
last_segment(spelling)
}
fn last_segment(spelling: &str) -> &str {
spelling.rsplit("::").next().unwrap_or(spelling).trim()
}
fn push_location<F: FileProvider + Clone>(
session: &Session<F>,
path: &std::path::Path,
range: cpp_parser::SourceRange,
found: &mut Vec<Location>,
) {
let Some(uri) = path_to_uri(path) else {
return;
};
let at_the_top = Location {
uri: uri.clone(),
range: Range::default(),
};
let position = session.files().held(path).and_then(|file| {
let start = position_in_file(file, range.start_offset)?;
let end = position_in_file(file, range.end_offset())?;
Some(Range { start, end })
});
let location = match position {
Some(range) => Location { uri, range },
None => at_the_top,
};
if !found
.iter()
.any(|held| held.uri == location.uri && held.range == location.range)
{
found.push(location);
}
}
pub struct ImplementationCapabilities;
impl RegisterCapabilities for ImplementationCapabilities {
fn register_capabilities(server: &mut ServerCapabilities, _client: &ClientCapabilities) {
server.implementation_provider = Some(ImplementationProviderCapability::Simple(true));
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::*;
fn an_indexed_project() -> Session<MemoryFiles> {
let files = MemoryFiles::new()
.with_file("/p/base.h", "struct Base {\n virtual void run();\n};\n")
.with_file(
"/p/derived.h",
"struct Derived : public Base {\n void run() override;\n};\n",
);
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/derived.h")]);
session.index_everything();
session
}
#[test]
fn a_derived_class_and_its_override_are_the_implementations() {
let session = an_indexed_project();
let classes = implementations_of(&session, "Base");
assert_eq!(classes.len(), 1, "one class derives from Base: {classes:?}");
assert!(
classes[0].uri.path().ends_with("derived.h"),
"{:?}",
classes[0]
);
let overrides = implementations_of(&session, "run");
assert!(
!overrides.is_empty(),
"the member `run` of the derived class implements the base's"
);
assert!(
overrides
.iter()
.any(|found| found.uri.path().ends_with("derived.h")),
"the implementation is in the derived class: {overrides:?}"
);
}
}
#[cfg(test)]
mod a_declaration_in_a_header {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::definitions_of;
#[test]
fn answers_the_body_in_the_source_file() {
let files = MemoryFiles::new()
.with_file("/p/base.h", "struct Base {\n virtual void run();\n};\n")
.with_file("/p/base.cpp", "#include \"base.h\"\nvoid Base::run() { }\n");
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/base.cpp")]);
session.index_everything();
let path = PathBuf::from("/p/base.h");
let held = session.files().held(&path).expect("the header is held");
let view =
cpp_code_analysis::FileView::parse_with(held, &cpp_parser::MacroEnvironment::default());
let at = "struct Base {\n virtual void ".len();
let found = definitions_of(&session, &view, at);
println!(
"definitions: {:?}",
found
.iter()
.map(|location| location.uri.path().to_string())
.collect::<Vec<_>>()
);
assert_eq!(found.len(), 1, "the body, and only it: {found:?}");
assert!(
found[0].uri.path().ends_with("base.cpp"),
"the implementation is in the source file: {found:?}"
);
}
}