#![warn(missing_docs)]
use perl_lsp_completion_item::{CompletionItem, CompletionItemKind};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileCompletionContext {
pub prefix: String,
pub prefix_start: usize,
pub position: usize,
}
impl FileCompletionContext {
#[must_use]
pub fn new(prefix: impl Into<String>, prefix_start: usize, position: usize) -> Self {
Self { prefix: prefix.into(), prefix_start, position }
}
}
#[must_use]
#[cfg(not(target_arch = "wasm32"))]
pub fn complete_file_paths(
context: &FileCompletionContext,
is_cancelled: &dyn Fn() -> bool,
) -> Vec<CompletionItem> {
use perl_path_security::{
build_completion_path, is_hidden_or_forbidden_entry_name, is_safe_completion_filename,
resolve_completion_base_directory, sanitize_completion_path_input,
split_completion_path_components,
};
use walkdir::WalkDir;
if is_cancelled() {
return Vec::new();
}
let prefix = context.prefix.trim();
if prefix.len() > 1024 {
return Vec::new();
}
let Some(safe_prefix) = sanitize_completion_path_input(prefix) else {
return Vec::new();
};
let (dir_part, file_part) = split_completion_path_components(&safe_prefix);
let Some(base_dir) = resolve_completion_base_directory(&dir_part) else {
return Vec::new();
};
let mut completions = Vec::new();
let mut entries_examined = 0usize;
for entry in
WalkDir::new(&base_dir).max_depth(1).follow_links(false).into_iter().filter_entry(|entry| {
!is_hidden_or_forbidden_entry_name(entry.file_name().to_string_lossy().as_ref())
})
{
if is_cancelled() {
break;
}
entries_examined += 1;
if entries_examined > 200 {
break;
}
let Ok(entry) = entry else {
continue;
};
if entry.path() == base_dir {
continue;
}
let Some(file_name) = entry.file_name().to_str() else {
continue;
};
if !file_name.starts_with(&file_part) || !is_safe_completion_filename(file_name) {
continue;
}
let completion_path =
build_completion_path(&dir_part, file_name, entry.file_type().is_dir());
let (detail, documentation) = file_completion_metadata(&entry);
completions.push(CompletionItem {
label: completion_path.clone(),
kind: CompletionItemKind::File,
detail: Some(detail),
documentation,
insert_text: Some(completion_path.clone()),
sort_text: Some(format!("1_{completion_path}")),
filter_text: Some(completion_path.clone()),
additional_edits: Vec::new(),
text_edit_range: Some((context.prefix_start, context.position)),
commit_characters: None,
});
if completions.len() >= 50 {
break;
}
}
completions
}
#[must_use]
#[cfg(target_arch = "wasm32")]
pub fn complete_file_paths(
_context: &FileCompletionContext,
_is_cancelled: &dyn Fn() -> bool,
) -> Vec<CompletionItem> {
Vec::new()
}
#[cfg(not(target_arch = "wasm32"))]
fn file_completion_metadata(entry: &walkdir::DirEntry) -> (String, Option<String>) {
let file_type = entry.file_type();
if file_type.is_dir() {
("directory".to_string(), Some("Directory".to_string()))
} else if file_type.is_file() {
let extension = entry.path().extension().and_then(|ext| ext.to_str()).unwrap_or("");
let file_type_desc = match extension.to_ascii_lowercase().as_str() {
"pl" | "pm" | "t" => "Perl file",
"rs" => "Rust source file",
"js" => "JavaScript file",
"py" => "Python file",
"txt" => "Text file",
"md" => "Markdown file",
"json" => "JSON file",
"yaml" | "yml" => "YAML file",
"toml" => "TOML file",
_ => "file",
};
(file_type_desc.to_string(), None)
} else {
("file".to_string(), None)
}
}