use std::{ convert::TryFrom, fs, path::{Path, PathBuf}, }; use cancellation::CancellationToken; use cstree::{TextRange, TextSize}; use lsp_types::CompletionParams; use crate::{ features::cursor::CursorContext, syntax::{latex, CstNode}, }; use super::types::{InternalCompletionItem, InternalCompletionItemData}; pub fn complete_includes<'a>( context: &'a CursorContext, items: &mut Vec>, cancellation_token: &CancellationToken, ) -> Option<()> { cancellation_token.result().ok()?; if context.request.main_document().uri.scheme() != "file" { return None; } let token = context.cursor.as_latex()?; let (path_text, path_range) = if token.kind() == latex::WORD { (token.text(), token.text_range()) } else { ("", TextRange::empty(context.offset)) }; let group = latex::CurlyGroupWordList::cast(token.parent()) .filter(|group| context.is_inside_latex_curly(group))?; 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)?; ( 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 path_text.is_empty() { path_range } else { let start = path_range.end() - TextSize::try_from(path_text.split('/').last()?.len()).ok()?; TextRange::new(start, path_range.end()) }; let current_dir = current_dir(context, path_text)?; for entry in fs::read_dir(current_dir).ok()?.filter_map(Result::ok) { 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 = path.file_name()?.to_str()?.into(); let data = InternalCompletionItemData::File { name }; let item = InternalCompletionItem::new(segment_range, data); items.push(item); } else if file_type.is_dir() { let name = path.file_name()?.to_str()?.into(); let data = InternalCompletionItemData::Directory { name }; let item = InternalCompletionItem::new(segment_range, data); items.push(item); } } Some(()) } fn current_dir(context: &CursorContext, path_text: &str) -> Option { let mut path = context .request .context .options .read() .unwrap() .root_directory .as_ref() .map(|root_directory| { context .request .context .current_directory .join(root_directory) }) .unwrap_or_else(|| { let mut path = context.request.main_document().uri.to_file_path().unwrap(); path.pop(); path }); path = PathBuf::from(path.to_str()?.replace('\\', "/")); if !path_text.is_empty() { path.push(&path_text); if !path_text.ends_with('/') { path.pop(); } } Some(path) } fn is_included(file: &Path, allowed_extensions: &[&str]) -> bool { allowed_extensions.is_empty() || file .extension() .and_then(|ext| ext.to_str()) .map(|ext| ext.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(|stem| stem.to_str()) .map(ToOwned::to_owned) { path.pop(); path.push(stem); } } #[cfg(test)] mod tests { use crate::features::testing::FeatureTester; use super::*; #[test] fn test_empty_latex_document() { let request = FeatureTester::builder() .files(vec![("main.tex", "")]) .main("main.tex") .line(0) .character(0) .build() .completion(); let context = CursorContext::new(request); let mut actual_items = Vec::new(); complete_includes(&context, &mut actual_items, CancellationToken::none()); assert!(actual_items.is_empty()); } #[test] fn test_empty_bibtex_document() { let request = FeatureTester::builder() .files(vec![("main.bib", "")]) .main("main.bib") .line(0) .character(0) .build() .completion(); let context = CursorContext::new(request); let mut actual_items = Vec::new(); complete_includes(&context, &mut actual_items, CancellationToken::none()); assert!(actual_items.is_empty()); } }