use cpp_code_analysis::{DiskFiles, Known, Session};
use lsp_types::{
ClientCapabilities, GotoDefinitionParams, GotoDefinitionResponse, Location, OneOf, Range,
ServerCapabilities,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{
file_offset_at_position, offset_at_position, path_to_uri, position_in_file, uri_to_file_path,
};
pub async fn on_goto_definition_handler(
context: ServerContextSnapshot,
params: GotoDefinitionParams,
cancel_token: CancellationToken,
) -> RequestOutcome<GotoDefinitionResponse> {
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;
crate::handlers::read_the_modules(&context, &path, false).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 in_the_file = file_offset_at_position(&view, position)?;
if let Known::Yes(header) = session.header_at(&view, in_the_file) {
return location_of_a_file(session, &header).map(GotoDefinitionResponse::Scalar);
}
let Known::Yes(found) = session.definitions(&view, offset) else {
return None;
};
let found =
crate::handlers::references::narrowed_to_the_cursors_class(&view, offset.get(), found);
let found = definitions_where_there_are_bodies(session, found);
let chosen = chosen_definition(session, &view, offset, &found);
let mut ordered: Vec<(bool, Location)> = Vec::new();
for (at, declaration) in found.found.iter().enumerate() {
let Some(uri) = path_to_uri(&declaration.file) else {
continue;
};
let range =
name_range(session, &declaration.file, &declaration.fact).unwrap_or_default();
ordered.push((Some(at) == chosen, Location { uri, range }));
}
ordered.sort_by_key(|(is_the_one, _)| !*is_the_one);
let mut locations: Vec<Location> =
ordered.into_iter().map(|(_, location)| location).collect();
match locations.len() {
0 => None,
1 => Some(GotoDefinitionResponse::Scalar(locations.remove(0))),
_ => Some(GotoDefinitionResponse::Array(locations)),
}
})
.await
}
fn location_of_a_file(
session: &Session<DiskFiles>,
header: &cpp_code_analysis::HeaderTarget,
) -> Option<Location> {
let uri = path_to_uri(&header.resolved)?;
let file = session.files().held(&header.resolved)?;
let end_of_the_first_line = file
.text
.find('\n')
.map_or_else(|| file.text.len(), |newline| newline + 1);
let end = position_in_file(file, end_of_the_first_line)?;
Some(Location {
uri,
range: Range::new(lsp_types::Position::new(0, 0), end),
})
}
fn name_range(
session: &cpp_code_analysis::Session<cpp_code_analysis::DiskFiles>,
file: &std::path::Path,
fact: &cpp_code_analysis::DeclFact,
) -> Option<Range> {
let declaring = session.files().held(file)?;
Some(Range::new(
position_in_file(declaring, fact.name_range.start_offset)?,
position_in_file(declaring, fact.name_range.end_offset())?,
))
}
pub struct DefinitionCapabilities;
fn definitions_where_there_are_bodies<F: cpp_code_analysis::FileProvider + Clone>(
session: &Session<F>,
found: cpp_code_analysis::ProjectDefinitions,
) -> cpp_code_analysis::ProjectDefinitions {
if found.found.len() < 2 {
return found;
}
let written_as_a_definition: Vec<bool> = found
.found
.iter()
.map(|declaration| {
is_written_as_a_definition(session, &declaration.file, &declaration.fact)
})
.collect();
let groups_with_a_body: std::collections::HashSet<(
std::path::PathBuf,
Option<String>,
String,
)> = found
.found
.iter()
.zip(&written_as_a_definition)
.filter(|(_, definition)| **definition)
.map(|(declaration, _)| {
(
declaration.file.clone(),
declaration.fact.scope.clone(),
declaration.fact.name.clone(),
)
})
.collect();
if groups_with_a_body.is_empty() {
return found;
}
let kept: Vec<cpp_code_analysis::ProjectDefinition> = found
.found
.into_iter()
.zip(written_as_a_definition)
.filter(|(declaration, definition)| {
let group = (
declaration.file.clone(),
declaration.fact.scope.clone(),
declaration.fact.name.clone(),
);
!groups_with_a_body.contains(&group) || *definition
})
.map(|(declaration, _)| declaration)
.collect();
cpp_code_analysis::ProjectDefinitions {
found: kept,
..found
}
}
fn is_written_as_a_definition<F: cpp_code_analysis::FileProvider + Clone>(
session: &Session<F>,
file: &std::path::Path,
fact: &cpp_code_analysis::DeclFact,
) -> bool {
let Some(text) = session.text(file) else {
return false;
};
let start = fact.range.start_offset.min(text.len());
let end = fact.range.end_offset().min(text.len());
let Some(written) = text.get(start..end) else {
return false;
};
if written.trim_end().ends_with(';') {
return false;
}
if written.contains('{') {
return true;
}
for character in text.get(end..).unwrap_or_default().chars() {
match character {
'{' | ':' => return true,
';' => return false,
_ => {}
}
}
false
}
impl RegisterCapabilities for DefinitionCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.definition_provider = Some(OneOf::Left(true));
}
}
fn chosen_definition(
session: &Session<DiskFiles>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
found: &cpp_code_analysis::ProjectDefinitions,
) -> Option<usize> {
if found.found.len() < 2 {
return None;
}
let parameter_lists: Vec<Option<String>> = found
.found
.iter()
.map(|declaration| declaration.fact.parameter_list.clone())
.collect();
let template_parameters: Vec<Vec<String>> = found
.found
.iter()
.map(|declaration| {
session
.text(&declaration.file)
.map(|text| {
cpp_code_analysis::sema::deduction::template_parameters_before(
&text,
declaration.fact.name_range.start_offset,
)
})
.unwrap_or_default()
})
.collect();
session.the_declaration_a_call_means(view, offset, ¶meter_lists, &template_parameters)
}
#[cfg(test)]
mod tests {
use super::*;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
#[test]
fn a_forward_declaration_is_not_offered_beside_its_definition() {
let header = "\
class Box;
class Box { int size; };
class Box<bool>;
class Box<bool> : public Box { };
class Promised;
";
let source = "#include \"box.h\"\nBox b;\nPromised* promised;\n";
let mut memory = MemoryFiles::new()
.with_file("/p/box.h", header)
.with_file("/p/main.cpp", source);
memory = memory.with_file("/p/other.cpp", "int main() { return 0; }\n");
let providers = SessionFiles::new(OpenDocuments::new(), memory);
let mut session = Session::with_config(
"/p",
providers,
WatchFilter::new("/p"),
CompilerConfig::default(),
);
session.add_project_files([
std::path::PathBuf::from("/p/box.h"),
std::path::PathBuf::from("/p/main.cpp"),
]);
session.index_everything();
let found = session
.index()
.definitions("Box", std::path::Path::new("/p/main.cpp"));
let Known::Yes(found) = found else {
panic!("the name is declared: {found:?}");
};
assert_eq!(found.found.len(), 4, "four declarations before the filter");
let kept = definitions_where_there_are_bodies(&session, found);
let written: Vec<String> = kept
.found
.iter()
.map(|declaration| {
let text = session.text(&declaration.file).expect("held");
text[declaration.fact.range.start_offset..declaration.fact.range.end_offset()]
.to_string()
})
.collect();
assert_eq!(
written.len(),
2,
"a forward declaration is not a definition: {written:?}"
);
assert!(
written.iter().all(|declaration| declaration.contains('{')),
"what survived is what has a body: {written:?}"
);
let known = session
.index()
.definitions("Promised", std::path::Path::new("/p/main.cpp"));
let Known::Yes(known) = known else {
panic!("the name is declared: {known:?}");
};
assert_eq!(
definitions_where_there_are_bodies(&session, known)
.found
.len(),
1,
"a declaration with nothing to prefer stays"
);
}
}