use cpp_code_analysis::VfsFile;
use cpp_code_analysis::sema::declarations::{LocalReferences, local_references_of};
use lsp_types::{
ClientCapabilities, DocumentHighlight, DocumentHighlightKind, DocumentHighlightParams, OneOf,
Range, ServerCapabilities,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{offset_at_position, position_in_file, uri_to_file_path};
pub async fn on_document_highlight(
context: ServerContextSnapshot,
params: DocumentHighlightParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<DocumentHighlight>> {
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 held = session.files().held(&view.path)?;
let offset = offset_at_position(&view, position)?.get();
let locals = local_references_of(&view.scopes, &view.root);
let mut found = highlights_of(held, &view.source, offset, &locals);
if found.is_empty() {
found = symbol_highlights(
session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(offset),
);
}
Some(found)
})
.await
}
fn symbol_highlights<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
) -> Vec<DocumentHighlight> {
use cpp_code_analysis::Known;
if !matches!(session.definitions(view, offset), Known::Yes(_)) {
return Vec::new();
}
let here = cpp_code_analysis::file::paths::normalize_path(&view.path, cfg!(windows));
let mut found = Vec::new();
for (file, range) in
crate::handlers::references::name_level_offsets(session, view, offset, true)
{
if cpp_code_analysis::file::paths::normalize_path(&file, cfg!(windows)) != here {
continue;
}
let Some(held) = session.files().held(&file) else {
continue;
};
let (Some(start), Some(end)) = (
position_in_file(held, range.start_offset),
position_in_file(held, range.end_offset()),
) else {
continue;
};
let kind = if is_a_declaration_here(session, view, offset, &file, range) {
DocumentHighlightKind::WRITE
} else {
DocumentHighlightKind::READ
};
found.push(DocumentHighlight {
range: Range { start, end },
kind: Some(kind),
});
}
found
}
fn is_a_declaration_here<F: cpp_code_analysis::FileProvider + Clone>(
session: &cpp_code_analysis::Session<F>,
view: &cpp_code_analysis::FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
file: &std::path::Path,
range: cpp_parser::SourceRange,
) -> bool {
let cpp_code_analysis::Known::Yes(defined) = session.definitions(view, offset) else {
return false;
};
let normalized = cpp_code_analysis::file::paths::normalize_path(file, cfg!(windows));
defined.found.iter().any(|definition| {
cpp_code_analysis::file::paths::normalize_path(&definition.file, cfg!(windows))
== normalized
&& definition.fact.name_range == range
})
}
pub fn highlights_of(
file: &VfsFile,
source: &str,
offset: usize,
locals: &[LocalReferences],
) -> Vec<DocumentHighlight> {
let Some(entry) = locals.iter().find(|entry| {
entry.declared_at as usize == offset || entry.uses.iter().any(|at| *at as usize == offset)
}) else {
return Vec::new();
};
let mut found = Vec::new();
push_highlight(
file,
source,
entry.declared_at as usize,
DocumentHighlightKind::WRITE,
&mut found,
);
for at in &entry.uses {
push_highlight(
file,
source,
*at as usize,
DocumentHighlightKind::READ,
&mut found,
);
}
found
}
fn push_highlight(
file: &VfsFile,
source: &str,
offset: usize,
kind: DocumentHighlightKind,
found: &mut Vec<DocumentHighlight>,
) {
let Some(text) = source.get(offset..) else {
return;
};
let length = text
.bytes()
.take_while(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
.count();
let (Some(start), Some(end)) = (
position_in_file(file, offset),
position_in_file(file, offset + length),
) else {
return;
};
found.push(DocumentHighlight {
range: Range { start, end },
kind: Some(kind),
});
}
pub struct DocumentHighlightCapabilities;
impl RegisterCapabilities for DocumentHighlightCapabilities {
fn register_capabilities(server: &mut ServerCapabilities, _client: &ClientCapabilities) {
server.document_highlight_provider = Some(OneOf::Left(true));
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::*;
#[test]
fn a_local_used_twice_is_one_write_and_two_reads() {
let text = "void f() {\n int value = 0;\n value = value + 1;\n}\n";
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");
let path = PathBuf::from("/p/a.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 locals = local_references_of(&view.scopes, &view.root);
let on_a_use = text.find("value = value").expect("the fixture uses it");
let found = highlights_of(held, &view.source, on_a_use, &locals);
assert_eq!(found.len(), 3, "the declaration and two uses: {found:?}");
assert_eq!(found[0].kind, Some(DocumentHighlightKind::WRITE));
assert!(
found[1..]
.iter()
.all(|highlight| highlight.kind == Some(DocumentHighlightKind::READ)),
"the uses are reads: {found:?}"
);
assert_eq!(
found[0].range.start.character, 8,
"`int value`: the name starts at column 8"
);
assert_eq!(
found[0].range.end.character, 13,
"`value` is five characters"
);
assert!(highlights_of(held, &view.source, 0, &locals).is_empty());
}
}
#[cfg(test)]
mod a_symbol {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::symbol_highlights;
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_highlighted_in_this_file_and_another_class_is_not() {
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 = symbol_highlights(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
);
println!(
"highlights: {:?}",
found
.iter()
.map(|highlight| (highlight.range.start.line, highlight.kind))
.collect::<Vec<_>>()
);
assert_eq!(
found.len(),
1,
"one use in this file, and the declaration is in the header: {found:?}"
);
assert_eq!(found[0].range.start.line, 1, "the use in `First::update`");
}
}