crates/completion/src/providers/include.rs
use std::{
fs::{self},
path::{Path, PathBuf},
};
use base_db::{
deps::{self, ProjectRoot},
util, DocumentData, FeatureParams,
};
use rowan::{ast::AstNode, TextLen, TextRange};
use syntax::latex;
use crate::{
util::{find_curly_group_word_list, CompletionBuilder},
CompletionItem, CompletionItemData, CompletionParams,
};
pub fn complete_includes<'a>(
params: &'a CompletionParams<'a>,
builder: &mut CompletionBuilder<'a>,
) -> Option<()> {
params.feature.document.path.as_ref()?;
let (cursor, group) = find_curly_group_word_list(params)?;
let include = group.syntax().parent()?;
let (include_extension, extensions): (bool, &[&str]) = match include.kind() {
latex::PACKAGE_INCLUDE => (false, &["sty"]),
latex::CLASS_INCLUDE => (false, &["cls"]),
latex::LATEX_INCLUDE => {
let include = latex::Include::cast(include.clone())?;
(
matches!(include.command()?.text(), "\\input" | "\\subfile"),
&["tex"],
)
}
latex::BIBLATEX_INCLUDE => (true, &["bib"]),
latex::BIBTEX_INCLUDE => (false, &["bib"]),
latex::GRAPHICS_INCLUDE => (true, &["pdf", "png", "jpg", "jpeg", "bmp"]),
latex::SVG_INCLUDE => (true, &["svg"]),
latex::INKSCAPE_INCLUDE => (true, &["pdf", "eps", "ps", "png"]),
latex::VERBATIM_INCLUDE => (true, &[]),
_ => return None,
};
let segment_range = if cursor.text.is_empty() {
cursor.range
} else {
let start = cursor.range.end() - cursor.text.split('/').last()?.text_len();
TextRange::new(start, cursor.range.end())
};
let segment_text = ¶ms.feature.document.text[std::ops::Range::from(segment_range)];
let mut dirs = vec![current_dir(¶ms.feature, &cursor.text, None)];
if include.kind() == latex::GRAPHICS_INCLUDE {
for document in ¶ms.feature.project.documents {
let DocumentData::Tex(data) = &document.data else {
continue;
};
for graphics_path in &data.semantics.graphics_paths {
dirs.push(current_dir(
¶ms.feature,
&cursor.text,
Some(graphics_path),
));
}
}
}
for entry in dirs
.into_iter()
.flatten()
.filter_map(|dir| fs::read_dir(dir).ok())
.flatten()
.flatten()
{
let mut path = entry.path();
let file_type = entry.file_type().ok()?;
if file_type.is_file() && is_included(&path, extensions) {
if !include_extension {
remove_extension(&mut path);
}
let name = String::from(path.file_name()?.to_str()?);
if let Some(score) = builder.matcher.score(&name, segment_text) {
builder.items.push(CompletionItem::new_simple(
score,
segment_range,
CompletionItemData::File(name),
));
}
} else if file_type.is_dir() {
let name = String::from(path.file_name()?.to_str()?);
if let Some(score) = builder.matcher.score(&name, segment_text) {
builder.items.push(CompletionItem::new_simple(
score,
segment_range,
CompletionItemData::Directory(name),
));
}
}
}
Some(())
}
fn current_dir(
params: &FeatureParams,
path_text: &str,
graphics_path: Option<&str>,
) -> Option<PathBuf> {
let workspace = ¶ms.workspace;
let parent = deps::parents(workspace, params.document)
.iter()
.next()
.map_or(params.document, Clone::clone);
let root = ProjectRoot::walk_and_find(workspace, &parent.dir);
let mut path = PathBuf::new();
if let Some(graphics_path) = graphics_path {
path.push(graphics_path);
}
if !path_text.is_empty() {
path.push(path_text);
if !path_text.ends_with('/') {
path.pop();
}
}
let current_dir =
util::expand_relative_path(path.to_str()?, &root.src_dir, workspace.folders()).ok()?;
current_dir.to_file_path().ok()
}
fn is_included(file: &Path, allowed_extensions: &[&str]) -> bool {
allowed_extensions.is_empty()
|| file
.extension()
.and_then(std::ffi::OsStr::to_str)
.map(str::to_lowercase)
.map(|ext| allowed_extensions.contains(&ext.as_str()))
.unwrap_or_default()
}
fn remove_extension(path: &mut PathBuf) {
if let Some(stem) = path
.file_stem()
.and_then(std::ffi::OsStr::to_str)
.map(ToOwned::to_owned)
{
path.pop();
path.push(stem);
}
}