From f3c9dd6caa6c67e018d5f8524ac8441a8a63e017 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 25 Feb 2024 14:38:20 +0100 Subject: [PATCH] Add globbing support to the open command --- helix-term/src/commands/typed.rs | 81 +++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3d7ea3fc8..7f90c12ea 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -5,9 +5,11 @@ use crate::job::Job; use super::*; +use globset::{Glob, GlobBuilder, GlobSet}; use helix_core::fuzzy::fuzzy_match; use helix_core::indent::MAX_INDENT; use helix_core::{encoding, line_ending, shellwords::Shellwords}; +use helix_stdx::env::current_working_dir; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::editor::{Action, CloseError, ConfigEvent}; use serde_json::Value; @@ -108,16 +110,16 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> } ensure!(!args.is_empty(), "wrong argument count"); - for arg in args { - let (path, pos) = args::parse_file(arg); - let path = helix_stdx::path::expand_tilde(path); - // If the path is a directory, open a file picker on that directory and update the status - // message - if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) { + + fn open(cx: &mut compositor::Context, path: Cow, pos: Position) -> anyhow::Result<()> { + // If the path is a directory, open a file picker on that directory and update + // the status message + if path.is_dir() { + let path = path.into_owned(); let callback = async move { let call: job::Callback = job::Callback::EditorCompositor(Box::new( move |editor: &mut Editor, compositor: &mut Compositor| { - let picker = ui::file_picker(path.into_owned(), &editor.config()); + let picker = ui::file_picker(path, &editor.config()); compositor.push(Box::new(overlaid(picker))); }, )); @@ -133,6 +135,71 @@ fn open(cx: &mut compositor::Context, args: &[Cow], event: PromptEvent) -> // does not affect opening a buffer without pos align_view(doc, view, Align::Center); } + Ok(()) + } + + fn glob(path: &Path) -> anyhow::Result { + let path_str = path.to_str().context("invalid unicode")?; + GlobBuilder::new(path_str) + .literal_separator(true) + .empty_alternates(true) + .build() + .context("invalid glob") + } + + for arg in args { + let (path, pos) = args::parse_file(arg); + let path = helix_stdx::path::canonicalize(path); + + if path.exists() { + // Shortcut for opening a path without globbing + open(cx, Cow::Owned(path), pos)?; + continue; + } + + let mut glob_set_builder = GlobSet::builder(); + glob_set_builder.add(glob(&path)?); + + let mut root = None; + let mut comps = path.components(); + + // Iterate over all parents + while comps.next_back().is_some() { + let parent = comps.as_path(); + if parent.as_os_str().is_empty() { + // No parents left + break; + } + + // Add each parent as a glob for filtering in the recursive walker + glob_set_builder.add(glob(parent)?); + + if root.is_none() && parent.exists() { + // Found the first parent that exists + root = Some(parent.to_path_buf()); + } + } + + let glob_set = glob_set_builder.build().context("invalid glob")?; + let root = root.unwrap_or_else(current_working_dir); + + let mut walk = cx + .editor + .config() + .file_picker + .walk_builder(root) + .filter_entry(move |entry| glob_set.is_match(entry.path())) + .build(); + + // Ignore the root directory which is yielded first + if walk.next().is_none() { + continue; + } + + for entry in walk { + let entry = entry.context("recursive walking failed")?; + open(cx, Cow::Borrowed(entry.path()), pos)?; + } } Ok(()) }