perl_lsp_file_completion/
lib.rs1#![warn(missing_docs)]
2use perl_lsp_completion_item::{CompletionItem, CompletionItemKind};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct FileCompletionContext {
13 pub prefix: String,
15 pub prefix_start: usize,
17 pub position: usize,
19}
20
21impl FileCompletionContext {
22 #[must_use]
24 pub fn new(prefix: impl Into<String>, prefix_start: usize, position: usize) -> Self {
25 Self { prefix: prefix.into(), prefix_start, position }
26 }
27}
28
29#[must_use]
31#[cfg(not(target_arch = "wasm32"))]
32pub fn complete_file_paths(
33 context: &FileCompletionContext,
34 is_cancelled: &dyn Fn() -> bool,
35) -> Vec<CompletionItem> {
36 use perl_path_security::{
37 build_completion_path, is_hidden_or_forbidden_entry_name, is_safe_completion_filename,
38 resolve_completion_base_directory, sanitize_completion_path_input,
39 split_completion_path_components,
40 };
41 use walkdir::WalkDir;
42
43 if is_cancelled() {
44 return Vec::new();
45 }
46
47 let prefix = context.prefix.trim();
48 if prefix.len() > 1024 {
49 return Vec::new();
50 }
51
52 let Some(safe_prefix) = sanitize_completion_path_input(prefix) else {
53 return Vec::new();
54 };
55
56 let (dir_part, file_part) = split_completion_path_components(&safe_prefix);
57 let Some(base_dir) = resolve_completion_base_directory(&dir_part) else {
58 return Vec::new();
59 };
60
61 let mut completions = Vec::new();
62 let mut entries_examined = 0usize;
63
64 for entry in
65 WalkDir::new(&base_dir).max_depth(1).follow_links(false).into_iter().filter_entry(|entry| {
66 !is_hidden_or_forbidden_entry_name(entry.file_name().to_string_lossy().as_ref())
67 })
68 {
69 if is_cancelled() {
70 break;
71 }
72
73 entries_examined += 1;
74 if entries_examined > 200 {
75 break;
76 }
77
78 let Ok(entry) = entry else {
79 continue;
80 };
81
82 if entry.path() == base_dir {
83 continue;
84 }
85
86 let Some(file_name) = entry.file_name().to_str() else {
87 continue;
88 };
89
90 if !file_name.starts_with(&file_part) || !is_safe_completion_filename(file_name) {
91 continue;
92 }
93
94 let completion_path =
95 build_completion_path(&dir_part, file_name, entry.file_type().is_dir());
96 let (detail, documentation) = file_completion_metadata(&entry);
97 completions.push(CompletionItem {
98 label: completion_path.clone(),
99 kind: CompletionItemKind::File,
100 detail: Some(detail),
101 documentation,
102 insert_text: Some(completion_path.clone()),
103 sort_text: Some(format!("1_{completion_path}")),
104 filter_text: Some(completion_path.clone()),
105 additional_edits: Vec::new(),
106 text_edit_range: Some((context.prefix_start, context.position)),
107 commit_characters: None,
108 });
109
110 if completions.len() >= 50 {
111 break;
112 }
113 }
114
115 completions
116}
117
118#[must_use]
120#[cfg(target_arch = "wasm32")]
121pub fn complete_file_paths(
122 _context: &FileCompletionContext,
123 _is_cancelled: &dyn Fn() -> bool,
124) -> Vec<CompletionItem> {
125 Vec::new()
126}
127
128#[cfg(not(target_arch = "wasm32"))]
129fn file_completion_metadata(entry: &walkdir::DirEntry) -> (String, Option<String>) {
130 let file_type = entry.file_type();
131 if file_type.is_dir() {
132 ("directory".to_string(), Some("Directory".to_string()))
133 } else if file_type.is_file() {
134 let extension = entry.path().extension().and_then(|ext| ext.to_str()).unwrap_or("");
135 let file_type_desc = match extension.to_ascii_lowercase().as_str() {
136 "pl" | "pm" | "t" => "Perl file",
137 "rs" => "Rust source file",
138 "js" => "JavaScript file",
139 "py" => "Python file",
140 "txt" => "Text file",
141 "md" => "Markdown file",
142 "json" => "JSON file",
143 "yaml" | "yml" => "YAML file",
144 "toml" => "TOML file",
145 _ => "file",
146 };
147 (file_type_desc.to_string(), None)
148 } else {
149 ("file".to_string(), None)
150 }
151}