Unnamed repository; edit this file 'description' to name the repository.
feat(helix-term): Support stack-based directory changes i.e. pushd/popd (#14932)
| -rw-r--r-- | book/src/generated/typable-cmd.md | 3 | ||||
| -rw-r--r-- | helix-term/src/commands/typed.rs | 142 | ||||
| -rw-r--r-- | helix-view/src/editor.rs | 7 |
3 files changed, 132 insertions, 20 deletions
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 958dc415..5e4624bb 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -47,6 +47,9 @@ | `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. | | `:show-clipboard-provider` | Show clipboard provider name in status bar. | | `:change-current-directory`, `:cd` | Change the current working directory. | +| `:show-directory-stack` | Show the directory stack as a <space> delimited string. | +| `:push-directory`, `:pushd` | Save and then change the current directory. | +| `:pop-directory`, `:popd` | Remove the top entry from the directory stack, and cd to the new top directory.. | | `:show-directory`, `:pwd` | Show the current working directory. | | `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. | | `:character-info`, `:char` | Get info about the character under the primary cursor. | diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 5597d586..cf9f8b8c 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1228,26 +1228,20 @@ fn show_clipboard_provider( Ok(()) } -fn change_current_directory( - cx: &mut compositor::Context, - args: Args, - event: PromptEvent, -) -> anyhow::Result<()> { - if event != PromptEvent::Validate { - return Ok(()); +/// Helper function to parse the first argument as a directory +#[inline] +fn parse_first_arg_as_dir(args: &Args, last_cwd: Option<PathBuf>) -> anyhow::Result<PathBuf> { + match args.first().map(AsRef::as_ref) { + Some("-") => last_cwd.ok_or_else(|| anyhow!("No previous working directory")), + Some(path) => Ok(helix_stdx::path::expand_tilde(Path::new(path)).into_owned()), + None => Ok(home_dir()?), } +} - let dir = match args.first().map(AsRef::as_ref) { - Some("-") => cx - .editor - .get_last_cwd() - .map(|path| Cow::Owned(path.to_path_buf())) - .ok_or_else(|| anyhow!("No previous working directory"))?, - Some(path) => helix_stdx::path::expand_tilde(Path::new(path)), - None => Cow::Owned(home_dir()?), - }; - - cx.editor.set_cwd(&dir).map_err(|err| { +/// Helper function to apply a directory change for an already-parsed Path ref +#[inline] +fn apply_directory_change(cx: &mut compositor::Context, dir: &Path) -> anyhow::Result<()> { + cx.editor.set_cwd(dir).map_err(|err| { anyhow!( "Could not change working directory to '{}': {err}", dir.display() @@ -1262,6 +1256,85 @@ fn change_current_directory( Ok(()) } +fn change_current_directory( + cx: &mut compositor::Context, + args: Args, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let dir = parse_first_arg_as_dir(&args, cx.editor.get_last_cwd().map(|p| p.to_path_buf()))?; + + apply_directory_change(cx, &dir) +} + +fn show_directory_stack( + cx: &mut compositor::Context, + _args: Args, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let serialized_stack = cx + .editor + .dir_stack + .iter() + .map(|p| p.display().to_string()) + .collect::<Vec<_>>() + .join(" "); + + if !serialized_stack.is_empty() { + cx.editor.set_status(serialized_stack); + } else { + cx.editor.set_error("Stack is empty"); + } + + Ok(()) +} + +fn push_directory( + cx: &mut compositor::Context, + args: Args, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + // avoid an unbounded directory stack and reallocs for perf + if cx.editor.dir_stack.len() == cx.editor.dir_stack.capacity() { + cx.editor.dir_stack.pop_back(); + } + + cx.editor + .dir_stack + .push_front(helix_stdx::env::current_working_dir()); + + change_current_directory(cx, args, event) +} + +fn pop_directory( + cx: &mut compositor::Context, + _args: Args, + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + if let Some(dir) = cx.editor.dir_stack.pop_front() { + apply_directory_change(cx, &dir)?; + } else { + cx.editor.set_error("Stack is empty"); + } + + Ok(()) +} + fn show_current_directory( cx: &mut compositor::Context, _args: Args, @@ -3349,6 +3422,39 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ }, }, TypableCommand { + name: "show-directory-stack", + aliases: &[], + doc: "Show the directory stack as a <space> delimited string.", + fun: show_directory_stack, + completer: CommandCompleter::none(), + signature: Signature { + positionals: (0, Some(0)), + ..Signature::DEFAULT + }, + }, + TypableCommand { + name: "push-directory", + aliases: &["pushd"], + doc: "Save and then change the current directory.", + fun: push_directory, + completer: CommandCompleter::positional(&[completers::directory]), + signature: Signature { + positionals: (1, Some(1)), + ..Signature::DEFAULT + }, + }, + TypableCommand { + name: "pop-directory", + aliases: &["popd"], + doc: "Remove the top entry from the directory stack, and cd to the new top directory..", + fun: pop_directory, + completer: CommandCompleter::none(), + signature: Signature { + positionals: (0, Some(0)), + ..Signature::DEFAULT + }, + }, + TypableCommand { name: "show-directory", aliases: &["pwd"], doc: "Show the current working directory.", diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index f559938a..d33d9c18 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -25,7 +25,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ borrow::Cow, cell::Cell, - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet, VecDeque}, fs, io::{self, stdin}, num::{NonZeroU8, NonZeroUsize}, @@ -62,6 +62,7 @@ use arc_swap::{ ArcSwap, }; +pub const DIR_STACK_CAP: usize = 10; pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000; fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error> @@ -1231,7 +1232,8 @@ pub struct Editor { redraw_timer: Pin<Box<Sleep>>, last_motion: Option<Motion>, pub last_completion: Option<CompleteAction>, - last_cwd: Option<PathBuf>, + pub last_cwd: Option<PathBuf>, + pub dir_stack: VecDeque<PathBuf>, pub exit_code: i32, @@ -1374,6 +1376,7 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + dir_stack: VecDeque::with_capacity(DIR_STACK_CAP), } } |