From c7ade24c78384fb92260920fde8698fc4bbc2c41 Mon Sep 17 00:00:00 2001 From: rszyma Date: Sun, 9 Nov 2025 00:06:25 +0100 Subject: [PATCH] extract to `complete_filepath` --- crates/ide/src/ide/completion.rs | 227 ++++++++++++++++--------------- 1 file changed, 115 insertions(+), 112 deletions(-) diff --git a/crates/ide/src/ide/completion.rs b/crates/ide/src/ide/completion.rs index 4f674e2..a88b978 100644 --- a/crates/ide/src/ide/completion.rs +++ b/crates/ide/src/ide/completion.rs @@ -317,116 +317,12 @@ impl Context<'_> { if literal.kind() == Some(LiteralKind::Path) && matches!(self.vfs_path, VfsPath::Path(_)) { - let curr_file_path = match &self.vfs_path { - VfsPath::Path(x) => x, + let curr_file_path = match self.vfs_path { + VfsPath::Path(ref x) => x.clone(), VfsPath::Virtual(_) => unreachable!("we just checked it's not Virtual"), }; - let mut path_completions: Vec<(SmolStr, CompletionItemKind)> = Vec::new(); - - let raw_literal = literal - .token() - .expect("path literal should be stored as token") - .to_string(); - - // NOTE: "*" is a valid character in unix path, - // but it's not supported in Nix path literals, - // so we don't have to care about escaping it for globbing. - // This applies to other glob special chars as well. - let literal_path = PathBuf::from_str(&(raw_literal + "*")) - .expect("path literal should be valid as a path"); - - let (literal_path_cleaned, home_dir_to_strip) = match literal_path - .strip_prefix("~/") - { - Ok(p) => match std::env::var("HOME").unwrap_or_default().as_str() { - "" => { - // TODO: log warning about unset $HOME? - return None; - } - home_dir => (PathBuf::from(home_dir).join(p), Some(home_dir.to_string())), - }, - Err(_) => (literal_path, None), - }; - - // Glob root should be relative to the current file dir, not the workspace root. - let glob_root = curr_file_path - .parent() - .expect("a file in a workspace should always have parent dir"); - - // (This works for absolute literal paths as well) - let glob_pattern = glob_root.join(&literal_path_cleaned).display().to_string(); - - let glob_paths = glob::glob_with( - &glob_pattern, - glob::MatchOptions { - case_sensitive: false, - require_literal_separator: true, - require_literal_leading_dot: false, - }, - ) - .expect("glob pattern should be valid"); - - for glob_path in glob_paths { - let Ok(entry) = glob_path else { - // TODO: maybe log the glob error? - continue; - }; - - // Replace absolute path with relative, if it was relative before. - let entry = if literal_path_cleaned.is_relative() { - entry.strip_prefix(glob_root).unwrap_or(&entry) - } else { - &entry - }; - - // Replace absolute home path back to `~` if it was substituted. - let entry = &match home_dir_to_strip { - Some(ref home_dir) => entry - .strip_prefix(home_dir) - .map(|p| { - PathBuf::from_str("~/") - .expect("~/ is always valid filepath") - .join(p) - }) - .unwrap_or(entry.to_path_buf()), - None => entry.to_path_buf(), - }; - - let path_cleaned: SmolStr = { - let mut pb = PathBuf::new(); - if entry.is_relative() - && !entry.starts_with(".") - && !entry.starts_with("..") - && !entry.starts_with("~") - { - // Re-add "." as first component for relative paths. - pb.push(path::Component::CurDir); - } - pb.push(entry); - pb.to_string_lossy().to_smolstr() - }; - - let kind = if entry.is_dir() { - CompletionItemKind::Folder - } else { - CompletionItemKind::File // or symlink. - }; - - path_completions.push((path_cleaned, kind)); - } - - for (path, compl_kind) in path_completions { - self.record_item(CompletionItem { - label: path.clone(), - replace_range: self.replace_range, - replace: path.clone(), - kind: compl_kind, - signature: None, - description: None, - documentation: None, - }); - } + self.complete_filepath(curr_file_path, literal)?; } return Some(()); @@ -494,6 +390,118 @@ impl Context<'_> { Some(()) } + fn complete_filepath( + &mut self, + curr_file_path: PathBuf, + filepath_literal: &ast::Literal, + ) -> Option<()> { + let mut path_completions: Vec<(SmolStr, CompletionItemKind)> = Vec::new(); + + let raw_literal = filepath_literal + .token() + .expect("path literal should be stored as token") + .to_string(); + + // NOTE: "*" is a valid character in unix path, + // but it's not supported in Nix path literals, + // so we don't have to care about escaping it for globbing. + // This applies to other glob special chars as well. + let glob_pattern_1 = PathBuf::from_str(&(raw_literal + "*")) + .expect("path literal should be valid as a path"); + + let (glob_pattern_cleaned, home_dir_to_strip) = match glob_pattern_1.strip_prefix("~/") { + Ok(p) => match std::env::var("HOME").unwrap_or_default().as_str() { + "" => { + // TODO: log warning about unset $HOME? + return None; + } + home_dir => (PathBuf::from(home_dir).join(p), Some(home_dir.to_string())), + }, + Err(_) => (glob_pattern_1, None), + }; + + // Glob root should be relative to the current file dir, not the workspace root. + let glob_root = curr_file_path + .parent() + .expect("a file in a workspace should always have parent dir"); + + // (This works for absolute literal paths as well) + let glob_pattern_abs = glob_root.join(&glob_pattern_cleaned).display().to_string(); + + let glob_results = glob::glob_with( + &glob_pattern_abs, + glob::MatchOptions { + case_sensitive: false, + require_literal_separator: true, + require_literal_leading_dot: false, + }, + ) + .expect("glob pattern should be valid"); + + for glob_result in glob_results { + let Ok(entry) = glob_result else { + // TODO: maybe log the glob error? + continue; + }; + + // Replace absolute path with relative, if it was relative before. + let entry = if glob_pattern_cleaned.is_relative() { + entry.strip_prefix(glob_root).unwrap_or(&entry) + } else { + &entry + }; + + // Replace absolute home path back to `~` if it was substituted. + let entry = &match home_dir_to_strip { + Some(ref home_dir) => entry + .strip_prefix(home_dir) + .map(|p| { + PathBuf::from_str("~/") + .expect("~/ is always valid filepath") + .join(p) + }) + .unwrap_or(entry.to_path_buf()), + None => entry.to_path_buf(), + }; + + let path_cleaned: SmolStr = { + let mut pb = PathBuf::new(); + if entry.is_relative() + && !entry.starts_with(".") + && !entry.starts_with("..") + && !entry.starts_with("~") + { + // Re-add "." as first component for relative paths. + pb.push(path::Component::CurDir); + } + pb.push(entry); + pb.to_string_lossy().to_smolstr() + }; + + let kind = if entry.is_dir() { + CompletionItemKind::Folder + } else { + CompletionItemKind::File // or symlink. + }; + + path_completions.push((path_cleaned, kind)); + } + + for (path, compl_kind) in path_completions { + self.record_item(CompletionItem { + label: path.clone(), + replace_range: self.replace_range, + replace: path.clone(), + kind: compl_kind, + signature: None, + description: None, + documentation: None, + }); + } + + None + } + /// Complete in binding position. /// Eg. `{ a = 1; | }` or `let |`. fn complete_binding(&mut self, container: ast::Expr) -> Option<()> { @@ -782,11 +790,6 @@ mod tests { check("if a th$0", "then", expect!["(Keyword) if a then"]); } - // #[test] - // fn filesystem_path() { - // todo!() - // } - #[test] fn local_binding() { check(