use lsp_types::{
ClientCapabilities, DocumentLink, DocumentLinkOptions, DocumentLinkParams, ServerCapabilities,
};
use tokio_util::sync::CancellationToken;
use cpp_code_analysis::sema::resolve::header_name_in;
use super::RegisterCapabilities;
use crate::context::{RequestOutcome, ServerContextSnapshot, snapshot_query};
use crate::util::{path_to_uri, position_in_file, uri_to_file_path};
pub async fn on_document_link(
context: ServerContextSnapshot,
params: DocumentLinkParams,
cancel_token: CancellationToken,
) -> RequestOutcome<Vec<DocumentLink>> {
let uri = params.text_document.uri;
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 summary = session.index().summary(&view.path)?;
let headers = summary.includes.iter().filter_map(|include| {
document_link_of(held, include, header_name_in(&view.root, include.range))
});
let modules = session
.module_imports_of(&view)
.into_iter()
.filter_map(move |import| module_link_of(held, &import));
Some(headers.chain(modules).collect())
})
.await
}
pub fn document_link_of(
file: &cpp_code_analysis::VfsFile,
include: &cpp_code_analysis::IncludeFact,
name: Option<cpp_parser::SourceRange>,
) -> Option<DocumentLink> {
let resolved = include.resolved.as_ref()?;
let covered = name.unwrap_or(include.range);
let start = position_in_file(file, covered.start_offset)?;
let end = position_in_file(file, covered.end_offset())?;
Some(DocumentLink {
range: lsp_types::Range { start, end },
target: path_to_uri(resolved),
tooltip: Some(include.spelling.clone()),
data: None,
})
}
pub fn module_link_of(
file: &cpp_code_analysis::VfsFile,
import: &cpp_code_analysis::ModuleImport,
) -> Option<DocumentLink> {
let resolved = import.resolved.as_ref()?;
let start = position_in_file(file, import.range.start_offset)?;
let end = position_in_file(file, import.range.end_offset())?;
Some(DocumentLink {
range: lsp_types::Range { start, end },
target: path_to_uri(resolved),
tooltip: Some(format!("import {}", import.name)),
data: None,
})
}
pub struct DocumentLinkCapabilities;
impl RegisterCapabilities for DocumentLinkCapabilities {
fn register_capabilities(server: &mut ServerCapabilities, _client: &ClientCapabilities) {
server.document_link_provider = Some(DocumentLinkOptions {
resolve_provider: Some(false),
work_done_progress_options: Default::default(),
});
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use cpp_code_analysis::{
CompilerConfig, IncludeFact, IncludeForm, MemoryFiles, OpenDocuments, Session,
SessionFiles, WatchFilter,
};
use super::*;
fn a_held_file() -> (Session<MemoryFiles>, PathBuf, String) {
let text = "#include \"widget.h\" // the widget\n#include <missing.h>\n";
let files = MemoryFiles::new()
.with_file("/p/a.cpp", text)
.with_file("/p/widget.h", "struct Widget {};\n");
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/a.cpp")]);
session.load("/p/a.cpp");
session.index_everything();
(session, PathBuf::from("/p/a.cpp"), text.to_string())
}
fn an_include(
spelling: &str,
resolved: Option<PathBuf>,
range: cpp_parser::SourceRange,
) -> IncludeFact {
IncludeFact {
form: IncludeForm::Quote,
spelling: spelling.to_string(),
resolved,
is_next: false,
range,
guard: cpp_code_analysis::FactGuard::Unconditional,
}
}
#[test]
fn a_resolved_include_becomes_a_link_that_covers_the_path() {
let (session, path, text) = a_held_file();
let view = session
.view_of_the_file(&path)
.expect("the file's own reading is held");
let file = session.files().held(&path).expect("the file is held");
let summary = session.index().summary(&path).expect("the file is indexed");
let links: Vec<DocumentLink> = summary
.includes
.iter()
.filter_map(|include| {
document_link_of(file, include, header_name_in(&view.root, include.range))
})
.collect();
assert_eq!(links.len(), 1, "only the include that resolved is a link");
let link = &links[0];
assert_eq!(link.range.start, lsp_types::Position::new(0, 10));
assert_eq!(
link.range.end,
lsp_types::Position::new(0, 18),
"the link is the path, not the directive and not the comment after it"
);
assert!(
text[10..18].eq("widget.h"),
"the span is the name as the file writes it"
);
assert_eq!(link.tooltip.as_deref(), Some("widget.h"));
let target = link
.target
.as_ref()
.expect("the target is the resolved file");
assert!(
target.path().ends_with("widget.h"),
"the link points at the header the search found: {target:?}"
);
let unresolved = summary
.includes
.iter()
.find(|include| include.spelling == "missing.h")
.expect("the fixture includes one");
assert!(
document_link_of(
file,
unresolved,
header_name_in(&view.root, unresolved.range),
)
.is_none(),
"an include that did not resolve leads nowhere and is not a link"
);
}
#[test]
fn an_import_becomes_a_link_that_covers_the_module_name() {
let text = "import math;\nint x = 0;\n";
let files = MemoryFiles::new().with_file("/p/main.cpp", text).with_file(
"/p/math.cppm",
"export module math;\nexport int add(int, int);\n",
);
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/main.cpp")]);
session.index_everything();
let view = session
.view_of_the_file("/p/main.cpp")
.expect("the file's own reading is held");
let file = session
.files()
.held("/p/main.cpp")
.expect("the file is held");
let imports = session.module_imports_of(&view);
assert_eq!(imports.len(), 1, "the file imports one module: {imports:?}");
assert_eq!(imports[0].name, "math");
let link = module_link_of(file, &imports[0]).expect("a resolved import is a link");
assert_eq!(link.range.start, lsp_types::Position::new(0, 7));
assert_eq!(
link.range.end,
lsp_types::Position::new(0, 11),
"the link is `math`, not the declaration"
);
assert_eq!(link.tooltip.as_deref(), Some("import math"));
let target = link.target.as_ref().expect("the target is the unit");
assert!(
target.path().ends_with("math.cppm"),
"and it points at the interface unit: {target:?}"
);
}
#[test]
fn a_directive_with_no_name_in_it_falls_back_to_its_own_range() {
let (session, path, _) = a_held_file();
let file = session.files().held(&path).expect("the file is held");
let directive = cpp_parser::SourceRange::new(0, "#include \"widget.h\"".len());
let link = document_link_of(
file,
&an_include("widget.h", Some(PathBuf::from("/p/widget.h")), directive),
None,
)
.expect("a resolved include is a link");
assert_eq!(link.range.start, lsp_types::Position::new(0, 0));
assert_eq!(
link.range.end,
lsp_types::Position::new(0, 19),
"the fallback is the directive the fact recorded"
);
}
}