use cpp_code_analysis::{FileView, ParameterHint, VfsFile};
use lsp_types::{
ClientCapabilities, InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams,
InlayHintServerCapabilities, OneOf, 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_inlay_hint(
context: ServerContextSnapshot,
params: InlayHintParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<InlayHint>> {
let uri = params.text_document.uri;
let requested = params.range;
let started = std::time::Instant::now();
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
context.analysis().catch_up(&path).await;
if cpp_code_analysis::probes::inlay() {
eprintln!("[inlay] prepared and caught up in {:?}", started.elapsed());
}
}
snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let view = session.view_of_the_file(&path)?;
let parsed = started.elapsed();
let held = session.files().held(&view.path)?;
let visible = visible_range(&view, requested)?;
let hints = session.inlay_hints(&view, visible);
let types = session.type_hints(&view, visible);
if cpp_code_analysis::probes::inlay() {
eprintln!("[inlay] {} type hint(s)", types.len());
}
if cpp_code_analysis::probes::inlay() {
eprintln!(
"[inlay] view {parsed:?}, hints {:?}, {} hint(s)",
started.elapsed() - parsed,
hints.len()
);
}
Some(
hints
.into_iter()
.filter_map(|hint| inlay_hint_of(held, hint))
.chain(
types
.into_iter()
.filter_map(|hint| type_hint_of(held, hint)),
)
.collect(),
)
})
.await
}
fn visible_range(view: &FileView, range: lsp_types::Range) -> Option<cpp_parser::SourceRange> {
let len = view.source.len();
let start = offset_at_position(view, range.start).unwrap_or(
cpp_code_analysis::file::coordinates::ReadingOffset::new(len),
);
let end = offset_at_position(view, range.end).unwrap_or(
cpp_code_analysis::file::coordinates::ReadingOffset::new(len),
);
(start <= end).then(|| cpp_parser::SourceRange::new(start.get(), end.get() - start.get()))
}
pub fn inlay_hint_of(file: &VfsFile, hint: ParameterHint) -> Option<InlayHint> {
let position = position_in_file(file, hint.offset)?;
Some(InlayHint {
position,
label: InlayHintLabel::String(format!("{}:", hint.name)),
kind: Some(InlayHintKind::PARAMETER),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
})
}
pub fn type_hint_of(file: &VfsFile, hint: cpp_code_analysis::TypeHint) -> Option<InlayHint> {
let position = position_in_file(file, hint.offset)?;
let label = match hint.kind {
cpp_code_analysis::TypeHintKind::Auto
| cpp_code_analysis::TypeHintKind::StructuredBinding => format!(": {}", hint.label),
cpp_code_analysis::TypeHintKind::DeducedReturn => format!(" -> {}", hint.label),
};
Some(InlayHint {
position,
label: InlayHintLabel::String(label),
kind: Some(InlayHintKind::TYPE),
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: Some(true),
data: None,
})
}
pub struct InlayHintCapabilities;
impl RegisterCapabilities for InlayHintCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.inlay_hint_provider = Some(OneOf::Right(
InlayHintServerCapabilities::Options(lsp_types::InlayHintOptions {
resolve_provider: Some(false),
..Default::default()
}),
));
}
}
#[cfg(test)]
mod tests {
use super::inlay_hint_of;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, ParameterHint, Session, SessionFiles,
WatchFilter,
};
fn a_held_file(text: &str) -> (Session<MemoryFiles>, std::path::PathBuf) {
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"))
}
#[test]
fn a_type_hint_becomes_a_labelled_hint_where_a_type_would_be_written() {
use super::type_hint_of;
use cpp_code_analysis::{TypeHint, TypeHintKind};
let text = "auto n = 1;\nauto f() { return 1; }\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let variable = type_hint_of(
file,
TypeHint {
offset: text.find('n').expect("the fixture") + 1,
label: "int".to_string(),
kind: TypeHintKind::Auto,
},
)
.expect("the offset is in the file");
match &variable.label {
lsp_types::InlayHintLabel::String(label) => assert_eq!(label, ": int"),
other => panic!("a plain label was expected, not label parts: {other:?}"),
}
assert_eq!(variable.kind, Some(lsp_types::InlayHintKind::TYPE));
let returned = type_hint_of(
file,
TypeHint {
offset: text.find("()").expect("the fixture") + 2,
label: "int".to_string(),
kind: TypeHintKind::DeducedReturn,
},
)
.expect("the offset is in the file");
match &returned.label {
lsp_types::InlayHintLabel::String(label) => assert_eq!(label, " -> int"),
other => panic!("a plain label was expected, not label parts: {other:?}"),
}
assert!(
type_hint_of(
file,
TypeHint {
offset: text.len() + 5,
label: "int".to_string(),
kind: TypeHintKind::Auto,
},
)
.is_none(),
"a position past the end of the file produces no hint"
);
}
#[test]
fn a_parameter_hint_becomes_a_labelled_hint_at_the_argument() {
let text = "int x = scale(3);\n";
let (session, path) = a_held_file(text);
let file = session.files().held(&path).expect("the file is held");
let hint = inlay_hint_of(
file,
ParameterHint {
offset: text.find('3').expect("the fixture"),
name: "count".to_string(),
},
)
.expect("the offset is in the file");
assert_eq!(hint.position.line, 0);
assert_eq!(hint.position.character, 14, "the hint sits on the argument");
match &hint.label {
lsp_types::InlayHintLabel::String(text) => assert_eq!(text, "count:"),
other => panic!("a plain label was expected, not label parts: {other:?}"),
}
assert_eq!(hint.kind, Some(lsp_types::InlayHintKind::PARAMETER));
assert_eq!(
hint.padding_right,
Some(true),
"the space goes after the colon"
);
assert!(
inlay_hint_of(
file,
ParameterHint {
offset: text.len() + 10,
name: "count".to_string(),
}
)
.is_none()
);
}
}