use cpp_code_analysis::{CompletionItem as AnalysisItem, DiskFiles, ItemKind, Session, detail_of};
use lsp_types::{
ClientCapabilities, CompletionItem, CompletionItemKind, CompletionItemLabelDetails,
CompletionOptions, CompletionParams, CompletionResponse, CompletionTextEdit, Documentation,
InsertTextFormat, MarkupContent, MarkupKind, ServerCapabilities, TextEdit,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::handlers::hover::documentation_text;
use crate::util::{file_offset_at_position, position_at_offset, uri_to_file_path};
pub async fn on_completion(
context: ServerContextSnapshot,
params: CompletionParams,
cancel_token: CancellationToken,
) -> RequestOutcome<CompletionResponse> {
let uri = params.text_document_position.text_document.uri;
let position = params.text_document_position.position;
let snippets = snippets_of(context.lsp_features().supports_snippets());
if let Some(path) = uri_to_file_path(&uri) {
let started = std::time::Instant::now();
context.analysis().prepare(&path).await;
let prepared = started.elapsed().as_millis();
let read = path.clone();
let started = std::time::Instant::now();
context.analysis().catch_up(&read).await;
let caught_up = started.elapsed().as_millis();
let started = std::time::Instant::now();
crate::handlers::read_the_modules(&context, &path, true).await;
let modules = started.elapsed().as_millis();
log::debug!(
"completion: prepare {prepared} ms, catch up {caught_up} ms, modules {modules} ms, {} ms before the query",
prepared + caught_up + modules
);
}
let asked_at = std::time::Instant::now();
let answer = snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let viewing = std::time::Instant::now();
let view = session.view_of_the_file(&path)?;
let viewed = viewing.elapsed().as_millis();
let offset = file_offset_at_position(&view, position)?;
if let Some(held) = session.files().held(&view.path)
&& let Some(items) = crate::handlers::format::complete::format_string_complete(
session, &view, held, offset,
)
{
log::debug!(
"completion: {} format-string item(s) at {}:{}",
items.len(),
position.line,
position.character
);
return Some(CompletionResponse::List(lsp_types::CompletionList {
is_incomplete: false,
items,
}));
}
let finding = std::time::Instant::now();
let found = session.completions(&view, offset);
log::debug!(
"completion: view {viewed} ms, completions {} ms, {} item(s)",
finding.elapsed().as_millis(),
found.items.len()
);
log_a_member_that_produced_no_members(
session,
&view,
offset.get(),
(position.line, position.character),
&found.items,
);
Some(CompletionResponse::List(lsp_types::CompletionList {
is_incomplete: found.items.is_empty()
|| !session.is_locally_ready(&path)
|| found.truncated
|| found.incomplete,
items: found
.items
.iter()
.enumerate()
.map(|(rank, item)| item_for(item, rank, &view, found.replace, snippets))
.collect(),
}))
})
.await;
log::debug!(
"completion: the query took {} ms (answer {})",
asked_at.elapsed().as_millis(),
match &answer {
RequestOutcome::Ready(_) => "ready",
RequestOutcome::Cancelled(_) => "cancelled",
RequestOutcome::Missing => "missing",
}
);
answer
}
fn log_a_member_that_produced_no_members(
session: &Session<DiskFiles>,
view: &cpp_code_analysis::FileView,
offset: usize,
asked: (u32, u32),
items: &[AnalysisItem],
) {
let has_members = items
.iter()
.any(|item| matches!(item.kind, ItemKind::Method | ItemKind::Field));
if has_members {
return;
}
let line_body = {
let before = &view.source[..offset.min(view.source.len())];
let start = before.rfind('\n').map_or(0, |at| at + 1);
let end = view.source[start..]
.find('\n')
.map_or(view.source.len(), |at| start + at);
view.source[start..end].to_string()
};
let landed_on = view
.source
.get(offset..)
.and_then(|rest| rest.chars().next())
.map_or("the end of the file".to_string(), |found| {
format!("{found:?}")
});
log::info!(
"completion at {}:{offset} (asked line {} character {}, on {landed_on}) — the line is {line_body:?} — \
the index holds {} files with {} still queued — has no members: {}",
view.path.display(),
asked.0,
asked.1,
session.index().len(),
session.pending(),
cpp_code_analysis::why_no_members(
session.index(),
&view.scopes,
&view.root,
&view.path,
cpp_code_analysis::file::coordinates::ReadingOffset::new(offset),
)
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Snippets {
Yes,
No,
}
fn snippets_of(supported: bool) -> Snippets {
if supported {
Snippets::Yes
} else {
Snippets::No
}
}
fn item_for(
item: &AnalysisItem,
rank: usize,
view: &cpp_code_analysis::FileView,
replace: cpp_parser::SourceRange,
snippets: Snippets,
) -> CompletionItem {
let insert_as_a_snippet = item.snippet && snippets == Snippets::Yes;
CompletionItem {
label: item.label.clone(),
kind: Some(completion_kind(item.kind)),
data: item
.identity
.as_ref()
.and_then(|(file, offset)| identity_of(file, *offset)),
sort_text: Some(format!("{rank:04}_{}", item.label)),
filter_text: item.filter.clone(),
insert_text_format: insert_as_a_snippet.then_some(InsertTextFormat::SNIPPET),
text_edit: text_edit(view, replace, item, insert_as_a_snippet),
..CompletionItem::default()
}
}
fn text_edit(
view: &cpp_code_analysis::FileView,
replace: cpp_parser::SourceRange,
item: &AnalysisItem,
insert_as_a_snippet: bool,
) -> Option<CompletionTextEdit> {
let new_text = if insert_as_a_snippet {
item.insert.clone()
} else if item.insert.starts_with("->") {
format!("->{}", item.label)
} else {
item.label.clone()
};
Some(CompletionTextEdit::Edit(TextEdit {
range: lsp_types::Range::new(
position_at_offset(
view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(replace.start_offset),
)?,
position_at_offset(
view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(replace.end_offset()),
)?,
),
new_text,
}))
}
fn identity_of(file: &std::path::Path, offset: usize) -> Option<lsp_types::LSPAny> {
Some(serde_json::json!({
"file": file.to_str()?,
"offset": offset,
}))
}
pub async fn on_completion_resolve(
context: ServerContextSnapshot,
mut item: CompletionItem,
cancel_token: CancellationToken,
) -> RequestOutcome<CompletionItem> {
let identity = item.data.clone();
let file = identity
.as_ref()
.and_then(|identity| identity.get("file"))
.and_then(|value| value.as_str())
.map(std::path::PathBuf::from);
let offset = identity
.as_ref()
.and_then(|identity| identity.get("offset"))
.and_then(|value| value.as_u64())
.map(|value| value as usize);
let (Some(file), Some(offset)) = (file, offset) else {
return RequestOutcome::Ready(item);
};
context.analysis().prepare(&file).await;
let looked_up = file.clone();
let resolved = snapshot_query(context.analysis(), cancel_token, move |session| {
let detail = declaration_in(session, &looked_up, offset).and_then(|fact| detail_of(&fact));
let documentation = session
.view(&file)
.and_then(|view| session.documentation(&view, &looked_up, offset))
.map(|comment| documentation_text(&comment));
Some((detail, documentation.flatten()))
})
.await;
if let RequestOutcome::Ready((detail, documentation)) = resolved {
item.label_details = detail.as_ref().map(|detail| CompletionItemLabelDetails {
detail: Some(format!(" {detail}")),
description: None,
});
item.detail = detail;
if let Some(text) = documentation {
item.documentation = Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: text,
}));
}
}
RequestOutcome::Ready(item)
}
fn declaration_in(
session: &cpp_code_analysis::Session<DiskFiles>,
file: &std::path::Path,
offset: usize,
) -> Option<cpp_code_analysis::DeclFact> {
let summary = session.index().summary(file)?;
summary
.declarations
.iter()
.find(|fact| fact.name_range.start_offset == offset)
.cloned()
}
fn completion_kind(kind: ItemKind) -> CompletionItemKind {
match kind {
ItemKind::Variable => CompletionItemKind::VARIABLE,
ItemKind::Function => CompletionItemKind::FUNCTION,
ItemKind::Method => CompletionItemKind::METHOD,
ItemKind::Field => CompletionItemKind::FIELD,
ItemKind::Class => CompletionItemKind::CLASS,
ItemKind::Enum => CompletionItemKind::ENUM,
ItemKind::EnumMember => CompletionItemKind::ENUM_MEMBER,
ItemKind::Namespace => CompletionItemKind::MODULE,
ItemKind::Macro => CompletionItemKind::CONSTANT,
ItemKind::TypeParameter => CompletionItemKind::TYPE_PARAMETER,
ItemKind::Keyword => CompletionItemKind::KEYWORD,
ItemKind::Snippet => CompletionItemKind::SNIPPET,
ItemKind::Header => CompletionItemKind::FILE,
}
}
pub struct CompletionCapabilities;
impl RegisterCapabilities for CompletionCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.completion_provider = Some(CompletionOptions {
trigger_characters: Some(vec![
".".to_string(),
">".to_string(),
":".to_string(),
"#".to_string(),
"<".to_string(),
"\"".to_string(),
]),
resolve_provider: Some(true),
..CompletionOptions::default()
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_item_carries_where_its_declaration_is() {
let identity =
identity_of(std::path::Path::new("/p/widget.h"), 120).expect("a path spells");
assert_eq!(identity["file"], serde_json::json!("/p/widget.h"));
assert_eq!(identity["offset"], serde_json::json!(120));
}
#[test]
fn every_item_kind_has_an_icon() {
assert_eq!(
completion_kind(ItemKind::Variable),
CompletionItemKind::VARIABLE
);
assert_eq!(
completion_kind(ItemKind::Function),
CompletionItemKind::FUNCTION
);
assert_eq!(
completion_kind(ItemKind::Method),
CompletionItemKind::METHOD
);
assert_eq!(completion_kind(ItemKind::Field), CompletionItemKind::FIELD);
assert_eq!(completion_kind(ItemKind::Class), CompletionItemKind::CLASS);
assert_eq!(completion_kind(ItemKind::Enum), CompletionItemKind::ENUM);
assert_eq!(
completion_kind(ItemKind::EnumMember),
CompletionItemKind::ENUM_MEMBER
);
assert_eq!(
completion_kind(ItemKind::Namespace),
CompletionItemKind::MODULE
);
assert_eq!(
completion_kind(ItemKind::Macro),
CompletionItemKind::CONSTANT
);
assert_eq!(
completion_kind(ItemKind::TypeParameter),
CompletionItemKind::TYPE_PARAMETER
);
assert_eq!(
completion_kind(ItemKind::Keyword),
CompletionItemKind::KEYWORD
);
assert_eq!(
completion_kind(ItemKind::Snippet),
CompletionItemKind::SNIPPET
);
assert_eq!(completion_kind(ItemKind::Header), CompletionItemKind::FILE);
}
}