summaryrefslogtreecommitdiff
path: root/support/texlab/src/features/completion/include.rs
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/features/completion/include.rs')
-rw-r--r--support/texlab/src/features/completion/include.rs181
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());
+ }
+}