use cpp_code_analysis::{FileProvider, FileView, Known, Session};
use lsp_types::{
ClientCapabilities, CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams,
CodeActionProviderCapability, Position, Range, ServerCapabilities, TextEdit, Uri,
WorkspaceEdit,
};
use tokio_util::sync::CancellationToken;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::uri_to_file_path;
pub async fn on_code_action(
context: ServerContextSnapshot,
params: CodeActionParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<CodeActionOrCommand>> {
let uri = params.text_document.uri;
let position = params.range.start;
let trace = cpp_code_analysis::probes::code_action();
let started = std::time::Instant::now();
if let Some(path) = uri_to_file_path(&uri) {
context.analysis().prepare(&path).await;
}
let prepared = started.elapsed();
let answer = snapshot_query(context.analysis(), cancel_token, move |session| {
let path = uri_to_file_path(&uri)?;
let written = session.view_of_the_file(&path)?;
let after_the_file = std::time::Instant::now();
let in_the_file = crate::util::file_offset_at_position(&written, position)?;
let view = session.view(&path)?;
let after_the_rendering = std::time::Instant::now();
let offset = view.reading_offset_of(
cpp_code_analysis::file::coordinates::FileOffset::new(in_the_file.get()),
)?;
let fixes = fixes_of(session, &view, offset);
if trace {
eprintln!(
"[codeAction] file {:?}, rendering {:?}, fixes {:?}, {} action(s)",
after_the_file - started,
after_the_rendering - after_the_file,
after_the_rendering.elapsed(),
fixes.len()
);
}
Some(
fixes
.into_iter()
.map(CodeActionOrCommand::CodeAction)
.collect(),
)
})
.await;
if trace {
eprintln!(
"[codeAction] prepare {:?}, total {:?}",
prepared,
started.elapsed()
);
}
answer
}
pub struct FixContext<'a, F: FileProvider + Clone> {
pub session: &'a Session<F>,
pub view: &'a FileView,
pub offset: cpp_code_analysis::file::coordinates::ReadingOffset,
}
impl<F: FileProvider + Clone> FixContext<'_, F> {
pub fn name_at_the_cursor(&self) -> Option<&str> {
let source: &str = &self.view.source;
let bytes = source.as_bytes();
let offset = self.offset.get();
if offset > bytes.len() {
return None;
}
let is_identifier_byte = |byte: u8| byte.is_ascii_alphanumeric() || byte == b'_';
let mut start = offset;
while start > 0 && is_identifier_byte(bytes[start - 1]) {
start -= 1;
}
let mut end = offset;
while end < bytes.len() && is_identifier_byte(bytes[end]) {
end += 1;
}
if start == end || bytes[start].is_ascii_digit() {
return None;
}
source.get(start..end)
}
pub fn resolves(&self) -> bool {
matches!(
self.session.definitions(self.view, self.offset),
Known::Yes(_)
)
}
pub fn after_the_includes(&self) -> Position {
let mut last = None;
for (number, line) in self.view.source.lines().enumerate() {
if line.trim_start().starts_with("#include") {
last = Some(number);
}
}
match last {
Some(number) => Position {
line: (number + 1) as u32,
character: 0,
},
None => Position {
line: 0,
character: 0,
},
}
}
pub fn insert_at(
&self,
title: String,
kind: CodeActionKind,
at: Position,
text: String,
) -> Option<CodeAction> {
let uri: Uri = crate::util::path_to_uri(&self.view.path)?;
Some(CodeAction {
title,
kind: Some(kind),
diagnostics: None,
edit: Some(WorkspaceEdit {
changes: None,
document_changes: Some(lsp_types::DocumentChanges::Edits(vec![
lsp_types::TextDocumentEdit {
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
uri: uri.clone(),
version: None,
},
edits: vec![lsp_types::OneOf3::Left(TextEdit {
range: Range { start: at, end: at },
new_text: text,
})],
},
])),
change_annotations: None,
}),
command: None,
is_preferred: Some(false),
disabled: None,
data: None,
})
}
}
pub fn fixes_of<F: FileProvider + Clone>(
session: &Session<F>,
view: &FileView,
offset: cpp_code_analysis::file::coordinates::ReadingOffset,
) -> Vec<CodeAction> {
let context = FixContext {
session,
view,
offset,
};
let mut fixes = Vec::new();
fixes.extend(include_fix(&context));
fixes
}
fn include_fix<F: FileProvider + Clone>(context: &FixContext<'_, F>) -> Vec<CodeAction> {
if context.resolves() {
return Vec::new();
}
let Some(name) = context.name_at_the_cursor() else {
return Vec::new();
};
let name = name.to_string();
const MOST_OFFERED: usize = 5;
let at = context.after_the_includes();
let mut offered: Vec<std::path::PathBuf> = Vec::new();
let mut fixes = Vec::new();
for (file, _) in context.session.index().every_fact_named(&name) {
if file == context.view.path || offered.contains(&file) {
continue;
}
offered.push(file.clone());
if offered.len() > MOST_OFFERED {
break;
}
let header = file.to_string_lossy().replace('\\', "/");
if let Some(fix) = context.insert_at(
format!("add `#include` for `{name}` from {header}"),
CodeActionKind::QUICKFIX,
at,
format!("#include \"{header}\"\n"),
) {
fixes.push(fix);
}
}
fixes
}
pub struct CodeActionCapabilities;
impl RegisterCapabilities for CodeActionCapabilities {
fn register_capabilities(server: &mut ServerCapabilities, _client: &ClientCapabilities) {
server.code_action_provider = Some(CodeActionProviderCapability::Simple(true));
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, MemoryFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use super::*;
fn a_project(use_cpp: &str) -> Session<MemoryFiles> {
let files = MemoryFiles::new()
.with_file("/p/base.h", "struct Widget {\n int size;\n};\n")
.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();
session
}
#[test]
fn an_unresolved_name_offers_the_header_that_declares_it() {
let source = "void f(Widget w) {\n (void)w;\n}\n";
let session = a_project(source);
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 = source.find("Widget").expect("the fixture names it");
let fixes = fixes_of(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at),
);
assert_eq!(fixes.len(), 1, "one header declares it: {fixes:?}");
assert!(fixes[0].title.contains("base.h"), "{:?}", fixes[0].title);
let edit = fixes[0]
.edit
.as_ref()
.and_then(|edit| edit.document_changes.as_ref())
.expect("the action carries an edit");
let lsp_types::DocumentChanges::Edits(edits) = edit else {
panic!("expected text edits");
};
let lsp_types::OneOf3::Left(insert) = &edits[0].edits[0] else {
panic!("expected a plain text edit");
};
assert_eq!(
insert.range.start, insert.range.end,
"an insertion, not a replacement"
);
assert!(insert.new_text.contains("base.h"), "{:?}", insert.new_text);
}
#[test]
fn a_name_that_resolves_offers_nothing() {
let source = "#include \"base.h\"\nvoid f(Widget w) {\n (void)w;\n}\n";
let session = a_project(source);
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 = source.find("Widget").expect("the fixture names it");
assert!(
fixes_of(
&session,
&view,
cpp_code_analysis::file::coordinates::ReadingOffset::new(at)
)
.is_empty(),
"a name that resolves is not a missing include"
);
}
}