diff --git a/book/src/keymap.md b/book/src/keymap.md index e7ae6ae47..4fd073b07 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -98,6 +98,8 @@ Normal mode is the default mode when you launch helix. You can return to it from | `Ctrl-x` | Decrement object (number) under cursor | `decrement` | | `Q` | Start/stop macro recording to the selected register (experimental) | `record_macro` | | `q` | Play back a recorded macro from the selected register (experimental) | `replay_macro` | +| `@` | Insert selected file path before each selection | `insert_file_path` | +| `Alt-@` | Insert selected file path after each selection | `append_file_path` | #### Shell diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6e037a471..761738c5d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -511,6 +511,8 @@ impl MappableCommand { wonly, "Close windows except current", select_register, "Select register", insert_register, "Insert register", + insert_file_path, "Insert file path", + append_file_path, "Append file path", align_view_middle, "Align view middle", align_view_top, "Align view top", align_view_center, "Align view center", @@ -5287,6 +5289,80 @@ fn select_register(cx: &mut Context) { }) } +#[derive(Eq, PartialEq)] +enum FilePathBehavior { + Insert, + Append, +} + +fn file_path(cx: &mut Context, prompt: Cow<'static, str>, behavior: FilePathBehavior) { + ui::prompt( + cx, + prompt, + Some('|'), + ui::completers::filename, + move |cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + if input.is_empty() { + return; + } + + let config = cx.editor.config(); + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); + + let mut changes = Vec::with_capacity(selection.len()); + let mut ranges = SmallVec::with_capacity(selection.len()); + + let mut offset = 0isize; + + for range in selection.ranges() { + let input_len = input.chars().count(); + + let (from, to, deleted_len) = match behavior { + FilePathBehavior::Insert => (range.from(), range.from(), 0), + FilePathBehavior::Append => (range.to(), range.to(), 0), + }; + + // These `usize`s cannot underflow because selection ranges cannot overlap. + let anchor = to + .checked_add_signed(offset) + .expect("Selection ranges cannot overlap") + .checked_sub(deleted_len) + .expect("Selection ranges cannot overlap"); + let new_range = Range::new(anchor, anchor + input_len).with_direction(range.direction()); + ranges.push(new_range); + offset = offset + .checked_add_unsigned(input_len) + .expect("Selection ranges cannot overlap") + .checked_sub_unsigned(deleted_len) + .expect("Selection ranges cannot overlap"); + + changes.push((from, to, Some(Tendril::from(input)))); + } + + let transaction = Transaction::change(doc.text(), changes.into_iter()) + .with_selection(Selection::new(ranges, selection.primary_index())); + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view); + + // after replace cursor may be out of bounds, do this to + // make sure cursor is in view and update scroll as well + view.ensure_cursor_in_view(doc, config.scrolloff); + } + ) +} + +fn insert_file_path(cx: &mut Context) { + file_path(cx, "insert-file-path:".into(), FilePathBehavior::Insert) +} + +fn append_file_path(cx: &mut Context) { + file_path(cx, "append-file-path:".into(), FilePathBehavior::Append) +} + fn insert_register(cx: &mut Context) { cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers)); cx.on_next_key(move |cx, event| { diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5a3e8eed4..ae3f0ff65 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -328,6 +328,8 @@ pub fn default() -> HashMap { "!" => shell_insert_output, "A-!" => shell_append_output, "$" => shell_keep_pipe, + "@" => insert_file_path, + "A-@" => append_file_path, "C-z" => suspend, "C-a" => increment, diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 9f196827f..7a1e9131a 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -444,6 +444,41 @@ async fn test_delete_char_forward() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_multiselection_file_path_commands() -> anyhow::Result<()> { + // insert_file_path + test(( + indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}, + "@/some/path", + indoc! {"\ + #[|/some/path]#lorem + #(|/some/path)#ipsum + #(|/some/path)#dolor + "}, + )) + .await?; + + // append_file_path + test(( + indoc! {"\ + #[|lorem]# + #(|ipsum)# + #(|dolor)# + "}, + "/some/path", + indoc! {"\ + lorem#[|/some/path]# + ipsum#(|/some/path)# + dolor#(|/some/path)# + "}, + )) + .await?; +} + #[tokio::test(flavor = "multi_thread")] async fn test_insert_with_indent() -> anyhow::Result<()> { const INPUT: &str = indoc! { "