tinymist_query/analysis/completion/
path.rs

1//! Completion of paths (string literal).
2
3use tinymist_world::vfs::WorkspaceResolver;
4
5use super::*;
6impl CompletionPair<'_, '_, '_> {
7    pub fn complete_path(&mut self, preference: &PathKind) -> Option<Vec<CompletionItem>> {
8        let id = self.cursor.source.id();
9        if WorkspaceResolver::is_package_file(id) {
10            return None;
11        }
12
13        let is_in_text;
14        let text;
15        let rng;
16        // todo: the non-str case
17        if self.cursor.leaf.is::<ast::Str>() {
18            let vr = self.cursor.leaf.range();
19            rng = vr.start + 1..vr.end - 1;
20            if rng.start > rng.end
21                || (self.cursor.cursor != rng.end && !rng.contains(&self.cursor.cursor))
22            {
23                return None;
24            }
25
26            let mut w = EcoString::new();
27            w.push('"');
28            w.push_str(&self.cursor.text[rng.start..self.cursor.cursor]);
29            w.push('"');
30            let partial_str = SyntaxNode::leaf(SyntaxKind::Str, w);
31
32            text = partial_str.cast::<ast::Str>()?.get();
33            is_in_text = true;
34        } else {
35            text = EcoString::default();
36            rng = self.cursor.cursor..self.cursor.cursor;
37            is_in_text = false;
38        }
39        crate::log_debug_ct!("complete_path: is_in_text: {is_in_text:?}");
40        let path = Path::new(text.as_str());
41        let has_root = path.has_root();
42
43        let src_path = id.vpath();
44        let base = id;
45        let dst_path = src_path.join(path);
46        let mut compl_path = dst_path.as_rootless_path();
47        if !compl_path.is_dir() {
48            compl_path = compl_path.parent().unwrap_or(Path::new(""));
49        }
50        crate::log_debug_ct!("compl_path: {src_path:?} + {path:?} -> {compl_path:?}");
51
52        if compl_path.is_absolute() {
53            log::warn!(
54                "absolute path completion is not supported for security consideration {path:?}"
55            );
56            return None;
57        }
58
59        // find directory or files in the path
60        let folder_completions = vec![];
61        let mut module_completions = vec![];
62        // todo: test it correctly
63        for path in self.worker.ctx.completion_files(preference) {
64            crate::log_debug_ct!("compl_check_path: {path:?}");
65
66            // Skip self smartly
67            if *path == base {
68                continue;
69            }
70
71            let label: EcoString = if has_root {
72                // diff with root
73                unix_slash(path.vpath().as_rooted_path()).into()
74            } else {
75                let base = base
76                    .vpath()
77                    .as_rooted_path()
78                    .parent()
79                    .unwrap_or(Path::new("/"));
80                let path = path.vpath().as_rooted_path();
81                let w = tinymist_std::path::diff(path, base)?;
82                unix_slash(&w).into()
83            };
84            crate::log_debug_ct!("compl_label: {label:?}");
85
86            module_completions.push((label, CompletionKind::File));
87
88            // todo: looks like the folder completion is broken
89            // if path.is_dir() {
90            //     folder_completions.push((label, CompletionKind::Folder));
91            // }
92        }
93
94        let replace_range = self.cursor.lsp_range_of(rng);
95
96        fn is_dot_or_slash(ch: &char) -> bool {
97            matches!(*ch, '.' | '/')
98        }
99
100        let path_priority_cmp = |lhs: &str, rhs: &str| {
101            // files are more important than dot started paths
102            if lhs.starts_with('.') || rhs.starts_with('.') {
103                // compare consecutive dots and slashes
104                let a_prefix = lhs.chars().take_while(is_dot_or_slash).count();
105                let b_prefix = rhs.chars().take_while(is_dot_or_slash).count();
106                if a_prefix != b_prefix {
107                    return a_prefix.cmp(&b_prefix);
108                }
109            }
110            lhs.cmp(rhs)
111        };
112
113        module_completions.sort_by(|a, b| path_priority_cmp(&a.0, &b.0));
114        // folder_completions.sort_by(|a, b| path_priority_cmp(&a.0, &b.0));
115
116        let mut sorter = 0;
117        let digits = (module_completions.len() + folder_completions.len())
118            .to_string()
119            .len();
120        let completions = module_completions.into_iter().chain(folder_completions);
121        Some(
122            completions
123                .map(|typst_completion| {
124                    let lsp_snippet = &typst_completion.0;
125                    let text_edit = EcoTextEdit::new(
126                        replace_range,
127                        if is_in_text {
128                            lsp_snippet.clone()
129                        } else {
130                            eco_format!(r#""{lsp_snippet}""#)
131                        },
132                    );
133
134                    let sort_text = eco_format!("{sorter:0>digits$}");
135                    sorter += 1;
136
137                    // todo: no all clients support label details
138                    LspCompletion {
139                        label: typst_completion.0,
140                        kind: typst_completion.1,
141                        detail: None,
142                        text_edit: Some(text_edit),
143                        // don't sort me
144                        sort_text: Some(sort_text),
145                        filter_text: Some("".into()),
146                        insert_text_format: Some(InsertTextFormat::PLAIN_TEXT),
147                        ..Default::default()
148                    }
149                })
150                .collect_vec(),
151        )
152    }
153}