Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions book/src/generated/static-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@
| `extend_to_column` | Extend to column | select: `` g\| `` |
| `goto_next_buffer` | Goto next buffer | normal: `` gn ``, select: `` gn `` |
| `goto_previous_buffer` | Goto previous buffer | normal: `` gp ``, select: `` gp `` |
| `goto_next_file` | Go to the next file alphabetically in the current file's directory | normal: `` ]n ``, select: `` ]n `` |
| `goto_prev_file` | Go to the previous file alphabetically in the current file's directory | normal: `` [n ``, select: `` [n `` |
| `goto_line_end_newline` | Goto newline at line end | insert: `` <end> `` |
| `goto_first_nonwhitespace` | Goto first non-blank in line | normal: `` gs ``, select: `` gs `` |
| `trim_selections` | Trim whitespace from selections | normal: `` _ ``, select: `` _ `` |
Expand Down
2 changes: 2 additions & 0 deletions book/src/generated/typable-cmd.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
| `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Force close all buffers ignoring unsaved changes without quitting. |
| `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
| `:file-next`, `:fn` | Goto next file in the current file's directory. |
| `:file-previous`, `:fp` | Goto previous file in the current file's directory. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt) |
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |
Expand Down
54 changes: 28 additions & 26 deletions book/src/keymap.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,32 +352,34 @@ Displays the signature of the selected completion item. Remapping currently not

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

| Key | Description | Command |
| ----- | ----------- | ------- |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]T` | Go to next test (**TS**) | `goto_next_test` |
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `]g` | Go to next change | `goto_next_change` |
| `[g` | Go to previous change | `goto_prev_change` |
| `]G` | Go to last change | `goto_last_change` |
| `[G` | Go to first change | `goto_first_change` |
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
| `]Space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]T` | Go to next test (**TS**) | `goto_next_test` |
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `]g` | Go to next change | `goto_next_change` |
| `[g` | Go to previous change | `goto_prev_change` |
| `]G` | Go to last change | `goto_last_change` |
| `[G` | Go to first change | `goto_first_change` |
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
| `]Space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
| `[n` | Go to the next file alphabetically in the current file's directory | `goto_next_file` |
| `]n` | Go to the previous file alphabetically in the current file's directory | `goto_prev_file` |

## Insert mode

Expand Down
62 changes: 54 additions & 8 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,8 @@ impl MappableCommand {
extend_to_column, "Extend to column",
goto_next_buffer, "Goto next buffer",
goto_previous_buffer, "Goto previous buffer",
goto_next_file, "Go to the next file alphabetically in the current file's directory",
goto_prev_file, "Go to the previous file alphabetically in the current file's directory",
goto_line_end_newline, "Goto newline at line end",
goto_first_nonwhitespace, "Goto first non-blank in line",
trim_selections, "Trim whitespace from selections",
Expand Down Expand Up @@ -3070,14 +3072,10 @@ fn file_picker(cx: &mut Context) {
}

fn file_picker_in_current_buffer_directory(cx: &mut Context) {
let doc_dir = doc!(cx.editor)
.path()
.and_then(|path| path.parent().map(|path| path.to_path_buf()));

let path = match doc_dir {
Some(path) => path,
None => {
cx.editor.set_error("current buffer has no path or parent");
let path = match parent_dir(cx.editor) {
Ok(value) => value,
Err(msg) => {
cx.editor.set_error(msg.to_string());
return;
}
};
Expand Down Expand Up @@ -6955,3 +6953,51 @@ fn lsp_or_syntax_workspace_symbol_picker(cx: &mut Context) {
syntax_workspace_symbol_picker(cx);
}
}

fn goto_next_file(cx: &mut Context) {
if let Err(err) = goto_next_file_impl(cx.editor, Direction::Forward, cx.count()) {
cx.editor.set_error(err.to_string());
}
}

fn goto_prev_file(cx: &mut Context) {
if let Err(err) = goto_next_file_impl(cx.editor, Direction::Backward, cx.count()) {
cx.editor.set_error(err.to_string());
}
}

fn goto_next_file_impl(
editor: &mut Editor,
direction: Direction,
count: usize,
) -> anyhow::Result<()> {
let path = parent_dir(editor)?;
let files: Vec<_> = WalkBuilder::new(path)
.sort_by_file_name(|name1, name2| name1.cmp(name2))
.max_depth(Some(1))
.build()
.flatten()
.map(|e| e.into_path())
.filter(|p| p.is_file())
.collect();
if let Some(path) = doc!(editor).path() {
let file = match direction {
Direction::Forward => files.iter().skip_while(|f| *f != path).nth(count),
Direction::Backward => files.iter().rev().skip_while(|f| *f != path).nth(count),
};

let file = file.ok_or_else(|| anyhow!("No more files"))?;
if let Err(e) = editor.open(file, Action::Replace) {
anyhow::bail!(format!("Open file failed: {:?}", e));
}
}
Ok(())
}

/// Get the parent dir if it exists.
fn parent_dir(editor: &mut Editor) -> anyhow::Result<PathBuf> {
doc!(editor)
.path()
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.ok_or_else(|| anyhow!("current buffer has no path or parent"))
}
40 changes: 40 additions & 0 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,24 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
}
}

fn file_next(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
goto_next_file_impl(cx.editor, Direction::Forward, 1)
}

fn file_previous(
cx: &mut compositor::Context,
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
goto_next_file_impl(cx.editor, Direction::Backward, 1)
}

#[derive(Debug, Clone, Copy)]
pub struct WriteOptions {
pub force: bool,
Expand Down Expand Up @@ -2903,6 +2921,28 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
..Signature::DEFAULT
},
},
TypableCommand {
name: "file-next",
aliases: &["fn"],
doc: "Goto next file in the current file's directory.",
fun: file_next,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand {
name: "file-previous",
aliases: &["fp"],
doc: "Goto previous file in the current file's directory.",
fun: file_previous,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
},
TypableCommand {
name: "write",
aliases: &["w"],
Expand Down
2 changes: 2 additions & 0 deletions helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"g" => goto_prev_change,
"G" => goto_first_change,
"f" => goto_prev_function,
"n" => goto_prev_file,
"t" => goto_prev_class,
"a" => goto_prev_parameter,
"c" => goto_prev_comment,
Expand All @@ -129,6 +130,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"g" => goto_next_change,
"G" => goto_last_change,
"f" => goto_next_function,
"n" => goto_next_file,
"t" => goto_next_class,
"a" => goto_next_parameter,
"c" => goto_next_comment,
Expand Down
117 changes: 117 additions & 0 deletions helix-term/tests/test/commands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use helix_term::application::Application;
use helix_view::editor::Severity;

use super::*;
use std::fs::File;
use std::ops::Deref;

mod insert;
mod movement;
Expand Down Expand Up @@ -844,3 +847,117 @@ async fn global_search_with_multibyte_chars() -> anyhow::Result<()> {

Ok(())
}

#[tokio::test(flavor = "multi_thread")]
async fn test_goto_next_file() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;

let a = dir.path().join("a");
let b = dir.path().join("b");
let c = dir.path().join("c");
let d = dir.path().join("d").join("ignored");

std::fs::create_dir(d.parent().unwrap())?;

File::create(&a)?;
File::create(&b)?;
File::create(&c)?;
File::create(&d)?;

test_key_sequence(
&mut helpers::AppBuilder::new().build()?,
Some("]n"),
Some(&|app| {
let (msg, severity) = app.editor.get_status().unwrap();
assert_eq!("current buffer has no path or parent", msg);
assert_eq!(&Severity::Error, severity);
}),
false,
)
.await?;

test_key_sequence(
&mut helpers::AppBuilder::new().build()?,
Some("[n"),
Some(&|app| {
let (msg, severity) = app.editor.get_status().unwrap();
assert_eq!("current buffer has no path or parent", msg);
assert_eq!(&Severity::Error, severity);
}),
false,
)
.await?;

fn open_buffers(app: &Application) -> Vec<String> {
app.editor
.documents()
.filter_map(|d| d.path()?.file_name())
.map(|n| n.to_string_lossy().deref().to_owned())
.collect()
}

test_key_sequence(
&mut AppBuilder::new().with_file(&b, None).build()?,
Some("[n"),
Some(&|app| {
assert_eq!(vec!["b", "a"], open_buffers(app));
assert_status_not_error(&app.editor);
}),
false,
)
.await?;

test_key_sequence(
&mut AppBuilder::new().with_file(&c, None).build()?,
Some("[n[n[n[n[n[n[n[n[n"),
Some(&|app| {
assert_eq!(vec!["c", "b", "a"], open_buffers(app));

let (msg, severity) = app.editor.get_status().unwrap();
assert_eq!("No more files", msg);
assert_eq!(&Severity::Error, severity);
}),
false,
)
.await?;

test_key_sequence(
&mut AppBuilder::new().with_file(&b, None).build()?,
Some("]n"),
Some(&|app| {
assert_eq!(vec!["b", "c"], open_buffers(app));
assert_status_not_error(&app.editor);
}),
false,
)
.await?;

test_key_sequence(
&mut AppBuilder::new().with_file(&a, None).build()?,
Some("]n]n]n]n]n]n]n]n"),
Some(&|app| {
assert_eq!(vec!["a", "b", "c"], open_buffers(app));

let (msg, severity) = app.editor.get_status().unwrap();
assert_eq!("No more files", msg);
assert_eq!(&Severity::Error, severity);
}),
false,
)
.await?;

test_key_sequence(
&mut AppBuilder::new()
.with_file(dir.path().join("not").join("there"), None)
.build()?,
Some("[n"),
Some(&|app| {
let (msg, severity) = app.editor.get_status().unwrap();
assert_eq!("No more files", msg);
assert_eq!(&Severity::Error, severity);
}),
false,
)
.await?;
Ok(())
}