texlab 4.3.2

LaTeX Language Server
Documentation
use std::{
    convert::TryFrom,
    fs,
    path::{Path, PathBuf},
};

use lsp_types::CompletionParams;
use rowan::{ast::AstNode, TextRange, TextSize};

use crate::{features::cursor::CursorContext, syntax::latex};

use super::types::{InternalCompletionItem, InternalCompletionItemData};

pub fn complete_includes<'a>(
    context: &'a CursorContext<CompletionParams>,
    items: &mut Vec<InternalCompletionItem<'a>>,
) -> Option<()> {
    if context.request.main_document().uri().scheme() != "file" {
        return None;
    }

    let (path_text, path_range, group) = context.find_curly_group_word_list()?;

    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 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 mut dirs = vec![current_dir(context, &path_text, None)];
    if include.kind() == latex::GRAPHICS_INCLUDE {
        for document in context.request.workspace.iter() {
            if let Some(data) = document.data().as_latex() {
                for graphics_path in &data.extras.graphics_paths {
                    dirs.push(current_dir(context, &path_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 = 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,
    graphics_path: Option<&str>,
) -> Option<PathBuf> {
    let mut path = context
        .request
        .workspace
        .environment
        .options
        .root_directory
        .as_ref()
        .map_or_else(
            || {
                let mut path = context
                    .request
                    .main_document()
                    .uri()
                    .to_file_path()
                    .unwrap();
                path.pop();
                path
            },
            |root_directory| {
                context
                    .request
                    .workspace
                    .environment
                    .current_directory
                    .join(root_directory)
            },
        );

    path = PathBuf::from(path.to_str()?.replace('\\', "/"));
    if !path_text.is_empty() {
        if let Some(graphics_path) = graphics_path {
            path.push(graphics_path);
        }

        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(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);
    }
}