Skip to content

Commit 8f4b979

Browse files
committed
Go to next/prev file in same dir as current file
`[n` and `]n` implement the unimpaired mappings: > [f Go to the file preceding the current one alphabetically in the current file's directory. > ]f Go to the file succeeding the current one alphabetically in the current file's directory. that are already taken.
1 parent 663c9d4 commit 8f4b979

File tree

7 files changed

+251
-36
lines changed

7 files changed

+251
-36
lines changed

book/src/generated/static-cmd.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@
161161
| `extend_to_column` | Extend to column | select: `` g\| `` |
162162
| `goto_next_buffer` | Goto next buffer | normal: `` gn ``, select: `` gn `` |
163163
| `goto_previous_buffer` | Goto previous buffer | normal: `` gp ``, select: `` gp `` |
164+
| `goto_next_file` | Go to the next file alphabetically in the current file's directory | normal: `` ]n ``, select: `` ]n `` |
165+
| `goto_prev_file` | Go to the previous file alphabetically in the current file's directory | normal: `` [n ``, select: `` [n `` |
164166
| `goto_line_end_newline` | Goto newline at line end | insert: `` <end> `` |
165167
| `goto_first_nonwhitespace` | Goto first non-blank in line | normal: `` gs ``, select: `` gs `` |
166168
| `trim_selections` | Trim whitespace from selections | normal: `` _ ``, select: `` _ `` |

book/src/generated/typable-cmd.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Force close all buffers ignoring unsaved changes without quitting. |
1414
| `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
1515
| `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
16+
| `:file-next`, `:fn` | Goto next file in the current file's directory. |
17+
| `:file-previous`, `:fp` | Goto previous file in the current file's directory. |
1618
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
1719
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt) |
1820
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |

book/src/keymap.md

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -352,32 +352,34 @@ Displays the signature of the selected completion item. Remapping currently not
352352

353353
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
354354

355-
| Key | Description | Command |
356-
| ----- | ----------- | ------- |
357-
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
358-
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
359-
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
360-
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
361-
| `]f` | Go to next function (**TS**) | `goto_next_function` |
362-
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
363-
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
364-
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
365-
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
366-
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
367-
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
368-
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
369-
| `]T` | Go to next test (**TS**) | `goto_next_test` |
370-
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
371-
| `]p` | Go to next paragraph | `goto_next_paragraph` |
372-
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
373-
| `]g` | Go to next change | `goto_next_change` |
374-
| `[g` | Go to previous change | `goto_prev_change` |
375-
| `]G` | Go to last change | `goto_last_change` |
376-
| `[G` | Go to first change | `goto_first_change` |
377-
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
378-
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
379-
| `]Space` | Add newline below | `add_newline_below` |
380-
| `[Space` | Add newline above | `add_newline_above` |
355+
| Key | Description | Command |
356+
| ----- | ----------- | ------- |
357+
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
358+
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
359+
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
360+
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
361+
| `]f` | Go to next function (**TS**) | `goto_next_function` |
362+
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
363+
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
364+
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
365+
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
366+
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
367+
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
368+
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
369+
| `]T` | Go to next test (**TS**) | `goto_next_test` |
370+
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
371+
| `]p` | Go to next paragraph | `goto_next_paragraph` |
372+
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
373+
| `]g` | Go to next change | `goto_next_change` |
374+
| `[g` | Go to previous change | `goto_prev_change` |
375+
| `]G` | Go to last change | `goto_last_change` |
376+
| `[G` | Go to first change | `goto_first_change` |
377+
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
378+
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
379+
| `]Space` | Add newline below | `add_newline_below` |
380+
| `[Space` | Add newline above | `add_newline_above` |
381+
| `[n` | Go to the next file alphabetically in the current file's directory | `goto_next_file` |
382+
| `]n` | Go to the previous file alphabetically in the current file's directory | `goto_prev_file` |
381383

382384
## Insert mode
383385

helix-term/src/commands.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,8 @@ impl MappableCommand {
464464
extend_to_column, "Extend to column",
465465
goto_next_buffer, "Goto next buffer",
466466
goto_previous_buffer, "Goto previous buffer",
467+
goto_next_file, "Go to the next file alphabetically in the current file's directory",
468+
goto_prev_file, "Go to the previous file alphabetically in the current file's directory",
467469
goto_line_end_newline, "Goto newline at line end",
468470
goto_first_nonwhitespace, "Goto first non-blank in line",
469471
trim_selections, "Trim whitespace from selections",
@@ -3070,16 +3072,9 @@ fn file_picker(cx: &mut Context) {
30703072
}
30713073

30723074
fn file_picker_in_current_buffer_directory(cx: &mut Context) {
3073-
let doc_dir = doc!(cx.editor)
3074-
.path()
3075-
.and_then(|path| path.parent().map(|path| path.to_path_buf()));
3076-
3077-
let path = match doc_dir {
3078-
Some(path) => path,
3079-
None => {
3080-
cx.editor.set_error("current buffer has no path or parent");
3081-
return;
3082-
}
3075+
let path = match parent_dir(cx.editor) {
3076+
Some(value) => value,
3077+
None => return,
30833078
};
30843079

30853080
let picker = ui::file_picker(cx.editor, path);
@@ -6955,3 +6950,54 @@ fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) {
69556950
syntax_workspace_symbol_picker(cx);
69566951
}
69576952
}
6953+
6954+
fn goto_next_file(cx: &mut Context) {
6955+
goto_next_file_impl(cx.editor, Direction::Forward, cx.count());
6956+
}
6957+
6958+
fn goto_prev_file(cx: &mut Context) {
6959+
goto_next_file_impl(cx.editor, Direction::Backward, cx.count());
6960+
}
6961+
6962+
fn goto_next_file_impl(editor: &mut Editor, direction: Direction, count: usize) {
6963+
let path = match parent_dir(editor) {
6964+
Some(value) => value,
6965+
None => return,
6966+
};
6967+
let files: Vec<_> = WalkBuilder::new(path)
6968+
.sort_by_file_name(|name1, name2| name1.cmp(name2))
6969+
.max_depth(Some(1))
6970+
.build()
6971+
.flatten()
6972+
.map(|e| e.into_path())
6973+
.filter(|p| p.is_file())
6974+
.collect();
6975+
if let Some(path) = doc!(editor).path() {
6976+
let file = match direction {
6977+
Direction::Forward => files.iter().skip_while(|f| *f != path).nth(count),
6978+
Direction::Backward => files.iter().rev().skip_while(|f| *f != path).nth(count),
6979+
};
6980+
6981+
if let Some(file) = file {
6982+
if let Err(e) = editor.open(file, Action::Replace) {
6983+
editor.set_error(format!("Open file failed: {:?}", e));
6984+
}
6985+
} else {
6986+
editor.set_error("No more files");
6987+
}
6988+
}
6989+
}
6990+
6991+
/// Get the parent dir if it exists.
6992+
fn parent_dir(editor: &mut Editor) -> Option<PathBuf> {
6993+
let doc_dir = doc!(editor)
6994+
.path()
6995+
.and_then(|path| path.parent().map(|path| path.to_path_buf()));
6996+
match doc_dir {
6997+
None => {
6998+
editor.set_error("current buffer has no path or parent");
6999+
None
7000+
}
7001+
e => e,
7002+
}
7003+
}

helix-term/src/commands/typed.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,28 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
481481
}
482482
}
483483

484+
fn file_next(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
485+
if event != PromptEvent::Validate {
486+
return Ok(());
487+
}
488+
489+
goto_next_file_impl(cx.editor, Direction::Forward, 1);
490+
Ok(())
491+
}
492+
493+
fn file_previous(
494+
cx: &mut compositor::Context,
495+
_args: Args,
496+
event: PromptEvent,
497+
) -> anyhow::Result<()> {
498+
if event != PromptEvent::Validate {
499+
return Ok(());
500+
}
501+
502+
goto_next_file_impl(cx.editor, Direction::Backward, 1);
503+
Ok(())
504+
}
505+
484506
#[derive(Debug, Clone, Copy)]
485507
pub struct WriteOptions {
486508
pub force: bool,
@@ -2903,6 +2925,28 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
29032925
..Signature::DEFAULT
29042926
},
29052927
},
2928+
TypableCommand {
2929+
name: "file-next",
2930+
aliases: &["fn"],
2931+
doc: "Goto next file in the current file's directory.",
2932+
fun: file_next,
2933+
completer: CommandCompleter::none(),
2934+
signature: Signature {
2935+
positionals: (0, Some(0)),
2936+
..Signature::DEFAULT
2937+
},
2938+
},
2939+
TypableCommand {
2940+
name: "file-previous",
2941+
aliases: &["fp"],
2942+
doc: "Goto previous file in the current file's directory.",
2943+
fun: file_previous,
2944+
completer: CommandCompleter::none(),
2945+
signature: Signature {
2946+
positionals: (0, Some(0)),
2947+
..Signature::DEFAULT
2948+
},
2949+
},
29062950
TypableCommand {
29072951
name: "write",
29082952
aliases: &["w"],

helix-term/src/keymap/default.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
114114
"g" => goto_prev_change,
115115
"G" => goto_first_change,
116116
"f" => goto_prev_function,
117+
"n" => goto_prev_file,
117118
"t" => goto_prev_class,
118119
"a" => goto_prev_parameter,
119120
"c" => goto_prev_comment,
@@ -129,6 +130,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
129130
"g" => goto_next_change,
130131
"G" => goto_last_change,
131132
"f" => goto_next_function,
133+
"n" => goto_next_file,
132134
"t" => goto_next_class,
133135
"a" => goto_next_parameter,
134136
"c" => goto_next_comment,

helix-term/tests/test/commands.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use helix_term::application::Application;
2+
use helix_view::editor::Severity;
23

34
use super::*;
5+
use std::fs::File;
6+
use std::ops::Deref;
47

58
mod insert;
69
mod movement;
@@ -844,3 +847,117 @@ async fn global_search_with_multibyte_chars() -> anyhow::Result<()> {
844847

845848
Ok(())
846849
}
850+
851+
#[tokio::test(flavor = "multi_thread")]
852+
async fn test_goto_next_file() -> anyhow::Result<()> {
853+
let dir = tempfile::tempdir()?;
854+
855+
let a = dir.path().join("a");
856+
let b = dir.path().join("b");
857+
let c = dir.path().join("c");
858+
let d = dir.path().join("d").join("ignored");
859+
860+
std::fs::create_dir(d.parent().unwrap())?;
861+
862+
File::create(&a)?;
863+
File::create(&b)?;
864+
File::create(&c)?;
865+
File::create(&d)?;
866+
867+
test_key_sequence(
868+
&mut helpers::AppBuilder::new().build()?,
869+
Some("]n"),
870+
Some(&|app| {
871+
let (msg, severity) = app.editor.get_status().unwrap();
872+
assert_eq!("current buffer has no path or parent", msg);
873+
assert_eq!(&Severity::Error, severity);
874+
}),
875+
false,
876+
)
877+
.await?;
878+
879+
test_key_sequence(
880+
&mut helpers::AppBuilder::new().build()?,
881+
Some("[n"),
882+
Some(&|app| {
883+
let (msg, severity) = app.editor.get_status().unwrap();
884+
assert_eq!("current buffer has no path or parent", msg);
885+
assert_eq!(&Severity::Error, severity);
886+
}),
887+
false,
888+
)
889+
.await?;
890+
891+
fn open_buffers(app: &Application) -> Vec<String> {
892+
app.editor
893+
.documents()
894+
.filter_map(|d| d.path()?.file_name())
895+
.map(|n| n.to_string_lossy().deref().to_owned())
896+
.collect()
897+
}
898+
899+
test_key_sequence(
900+
&mut AppBuilder::new().with_file(&b, None).build()?,
901+
Some("[n"),
902+
Some(&|app| {
903+
assert_eq!(vec!["b", "a"], open_buffers(app));
904+
assert_status_not_error(&app.editor);
905+
}),
906+
false,
907+
)
908+
.await?;
909+
910+
test_key_sequence(
911+
&mut AppBuilder::new().with_file(&c, None).build()?,
912+
Some("[n[n[n[n[n[n[n[n[n"),
913+
Some(&|app| {
914+
assert_eq!(vec!["c", "b", "a"], open_buffers(app));
915+
916+
let (msg, severity) = app.editor.get_status().unwrap();
917+
assert_eq!("No more files", msg);
918+
assert_eq!(&Severity::Error, severity);
919+
}),
920+
false,
921+
)
922+
.await?;
923+
924+
test_key_sequence(
925+
&mut AppBuilder::new().with_file(&b, None).build()?,
926+
Some("]n"),
927+
Some(&|app| {
928+
assert_eq!(vec!["b", "c"], open_buffers(app));
929+
assert_status_not_error(&app.editor);
930+
}),
931+
false,
932+
)
933+
.await?;
934+
935+
test_key_sequence(
936+
&mut AppBuilder::new().with_file(&a, None).build()?,
937+
Some("]n]n]n]n]n]n]n]n"),
938+
Some(&|app| {
939+
assert_eq!(vec!["a", "b", "c"], open_buffers(app));
940+
941+
let (msg, severity) = app.editor.get_status().unwrap();
942+
assert_eq!("No more files", msg);
943+
assert_eq!(&Severity::Error, severity);
944+
}),
945+
false,
946+
)
947+
.await?;
948+
949+
test_key_sequence(
950+
&mut AppBuilder::new()
951+
.with_file(dir.path().join("not").join("there"), None)
952+
.build()?,
953+
Some("[n"),
954+
Some(&|app| {
955+
let (msg, severity) = app.editor.get_status().unwrap();
956+
assert_eq!("No more files", msg);
957+
assert_eq!(&Severity::Error, severity);
958+
}),
959+
false,
960+
)
961+
.await?;
962+
Ok(())
963+
}

0 commit comments

Comments
 (0)