Unnamed repository; edit this file 'description' to name the repository.
feat(helix-term): Support stack-based directory changes i.e. pushd/popd (#14932)
Karim Abdul-Samad 4 months ago
parent 3de3357 · commit 8f259df
-rw-r--r--book/src/generated/typable-cmd.md3
-rw-r--r--helix-term/src/commands/typed.rs142
-rw-r--r--helix-view/src/editor.rs7
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),
}
}