tinymist_query/analysis/completion/
path.rs1use 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 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 let folder_completions = vec![];
61 let mut module_completions = vec![];
62 for path in self.worker.ctx.completion_files(preference) {
64 crate::log_debug_ct!("compl_check_path: {path:?}");
65
66 if *path == base {
68 continue;
69 }
70
71 let label: EcoString = if has_root {
72 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 }
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 if lhs.starts_with('.') || rhs.starts_with('.') {
103 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 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 LspCompletion {
139 label: typst_completion.0,
140 kind: typst_completion.1,
141 detail: None,
142 text_edit: Some(text_edit),
143 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}