diff options
Diffstat (limited to 'support/texlab/src/features/completion/include.rs')
-rw-r--r-- | support/texlab/src/features/completion/include.rs | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/support/texlab/src/features/completion/include.rs b/support/texlab/src/features/completion/include.rs new file mode 100644 index 0000000000..c711ea5ce7 --- /dev/null +++ b/support/texlab/src/features/completion/include.rs @@ -0,0 +1,181 @@ +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<CompletionParams>, + items: &mut Vec<InternalCompletionItem<'a>>, + 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<CompletionParams>, path_text: &str) -> Option<PathBuf> { + 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()); + } +} |