diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index b7496d338c4e..28d577782133 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -9,6 +9,7 @@ | `:buffer-close-others!`, `:bco!`, `:bcloseother!` | Force close all buffers but the currently focused one. | | `:buffer-close-all`, `:bca`, `:bcloseall` | Close all buffers without quitting. | | `:buffer-close-all!`, `:bca!`, `:bcloseall!` | Force close all buffers ignoring unsaved changes without quitting. | +| `:buffer-goto`, `:b` | Goto the buffer number . | | `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. | | `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. | | `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5498a4377162..2d0f4e0d6413 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -309,6 +309,15 @@ impl MappableCommand { goto_prev_diag, "Goto previous diagnostic", goto_line_start, "Goto line start", goto_line_end, "Goto line end", + goto_first_buffer, "Goto first buffer", + goto_second_buffer, "Goto second buffer", + goto_third_buffer, "Goto third buffer", + goto_fourth_buffer, "Goto fourth buffer", + goto_fifth_buffer, "Goto fifth buffer", + goto_sixth_buffer, "Goto sixth buffer", + goto_seventh_buffer, "Goto seventh buffer", + goto_eight_buffer, "Goto eight buffer", + goto_ninth_buffer, "Goto ninth buffer", goto_next_buffer, "Goto next buffer", goto_previous_buffer, "Goto previous buffer", goto_line_end_newline, "Goto newline at line end", @@ -659,36 +668,88 @@ fn goto_line_start(cx: &mut Context) { ) } +fn goto_first_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 0); +} + +fn goto_second_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 1); +} + +fn goto_third_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 2); +} + +fn goto_fourth_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 3); +} + +fn goto_fifth_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 4); +} + +fn goto_sixth_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 5); +} + +fn goto_seventh_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 6); +} + +fn goto_eight_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 7); +} + +fn goto_ninth_buffer(cx: &mut Context) { + goto_buffer_by_index_impl(cx.editor, 8); +} + fn goto_next_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Forward); + goto_buffer_by_direction(cx.editor, Direction::Forward) } fn goto_previous_buffer(cx: &mut Context) { - goto_buffer(cx.editor, Direction::Backward); -} + goto_buffer_by_direction(cx.editor, Direction::Backward) +} + +fn goto_buffer_by_direction(editor: &mut Editor, direction: Direction) { + let doc_id = &view!(editor).doc; + let buffers_len = editor.documents.len(); + let current_index = editor + .documents + .keys() + .position(|d| d == doc_id) + .expect("current document was not in documents"); + + let new_index = match direction { + Direction::Forward if current_index < buffers_len - 1 => current_index + 1, + Direction::Forward => 0, // Would be out of bounds, wrap to front. + Direction::Backward if current_index > 0 => current_index - 1, + Direction::Backward => buffers_len - 1, // Would be out of bounds, wrap to back. + }; -fn goto_buffer(editor: &mut Editor, direction: Direction) { - let current = view!(editor).doc; + // Safety: The above logic ensures that the new index is always in bounds. + goto_buffer_by_index_impl(editor, new_index).unwrap(); +} - let id = match direction { - Direction::Forward => { - let iter = editor.documents.keys(); - let mut iter = iter.skip_while(|id| *id != ¤t); - iter.next(); // skip current item - iter.next().or_else(|| editor.documents.keys().next()) - } - Direction::Backward => { - let iter = editor.documents.keys(); - let mut iter = iter.rev().skip_while(|id| *id != ¤t); - iter.next(); // skip current item - iter.next().or_else(|| editor.documents.keys().rev().next()) - } - } - .unwrap(); +/// Goto a buffer by providing it's index. +/// +/// Note that the index starts from 0. +/// +/// # Errors +/// +/// Returns an error if the index could not be found - is out of bounds. +fn goto_buffer_by_index_impl(editor: &mut Editor, index: usize) -> anyhow::Result<()> { + let doc_id = editor + .documents + .keys() + .nth(index) + .copied() + .ok_or_else(|| anyhow!("no buffer '{}' in editor", index + 1))?; - let id = *id; + editor.switch(doc_id, Action::Replace); - editor.switch(id, Action::Replace); + Ok(()) } fn extend_to_line_start(cx: &mut Context) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index c6810f05390f..0ed68ab60457 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -247,7 +247,8 @@ fn buffer_next( return Ok(()); } - goto_buffer(cx.editor, Direction::Forward); + goto_buffer_by_direction(cx.editor, Direction::Forward); + Ok(()) } @@ -260,7 +261,38 @@ fn buffer_previous( return Ok(()); } - goto_buffer(cx.editor, Direction::Backward); + goto_buffer_by_direction(cx.editor, Direction::Backward); + + Ok(()) +} + +/// Goto a buffer by providing (index) as the argument. +/// +/// Note that the index starts from 1. +fn goto_buffer_by_index( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let index_str = args + .first() + .ok_or_else(|| anyhow!("requires an argument for "))? + .as_ref(); + let index = index_str + .parse::() + .map_err(|_| anyhow!("'{}' is not a valid index", index_str))?; + + if index < 1 { + bail!("indices bellow 1 not supported"); + } + + // Convert to zero based index + goto_buffer_by_index_impl(cx.editor, index - 1)?; + Ok(()) } @@ -1766,14 +1798,14 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ aliases: &["bc", "bclose"], doc: "Close the current buffer.", fun: buffer_close, - completer: Some(completers::buffer), + completer: Some(completers::buffer_name), }, TypableCommand { name: "buffer-close!", aliases: &["bc!", "bclose!"], doc: "Close the current buffer forcefully, ignoring unsaved changes.", fun: force_buffer_close, - completer: Some(completers::buffer), + completer: Some(completers::buffer_name), }, TypableCommand { name: "buffer-close-others", @@ -1803,6 +1835,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: force_buffer_close_all, completer: None, }, + TypableCommand { + name: "buffer-goto", + aliases: &["b"], + doc: "Goto the buffer number .", + fun: goto_buffer_by_index, + completer: Some(completers::buffer_index), + }, TypableCommand { name: "buffer-next", aliases: &["bn", "bnext"], diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 118764d97585..c8ae047ca5b4 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -73,6 +73,15 @@ pub fn default() -> HashMap { "C" => copy_selection_on_next_line, "A-C" => copy_selection_on_prev_line, + "C-1" => goto_first_buffer, + "C-2" => goto_second_buffer, + "C-3" => goto_third_buffer, + "C-4" => goto_fourth_buffer, + "C-5" => goto_fifth_buffer, + "C-6" => goto_sixth_buffer, + "C-7" => goto_seventh_buffer, + "C-8" => goto_eight_buffer, + "C-9" => goto_ninth_buffer, "s" => select_regex, "A-s" => split_selection_on_newline, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index cca9e9bf0b68..d6534c6068ab 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -251,7 +251,7 @@ pub mod completers { Vec::new() } - pub fn buffer(editor: &Editor, input: &str) -> Vec { + pub fn buffer_name(editor: &Editor, input: &str) -> Vec { let mut names: Vec<_> = editor .documents .iter() @@ -279,6 +279,30 @@ pub mod completers { names } + /// Completes the buffer indices. + /// + /// Note that the indices start from 1 instead of 0. + pub fn buffer_index(editor: &Editor, input: &str) -> Vec { + let indices = (0..editor.documents.len()).map(|index| Cow::from((index + 1).to_string())); + + let matcher = Matcher::default(); + + let mut matches: Vec<_> = indices + .filter_map(|index| { + matcher + .fuzzy_match(&index, input) + .map(|score| (index, score)) + }) + .collect(); + + matches.sort_unstable_by_key(|(_, score)| Reverse(*score)); + + matches + .into_iter() + .map(|(index, _)| ((0..), index)) + .collect() + } + pub fn theme(_editor: &Editor, input: &str) -> Vec { let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes")); names.extend(theme::Loader::read_names(