fix: preserve Visual selections through command entry

This commit is contained in:
Valdemar Lorenzen
2026-10-16 13:59:25 +02:00
parent 0446ec86c7
commit d67b874b87
6 changed files with 93 additions and 18 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
- [ ] we should be able to execute commands in visual mode, since some commands may relevant to selections. - [x] we should be able to execute commands in visual mode, since some commands may relevant to selections.
- [x] diff view: local review UX first, independently of GitHub integration, for reviewing local changes and delivering feedback to coding agents. - [x] diff view: local review UX first, independently of GitHub integration, for reviewing local changes and delivering feedback to coding agents.
- add comments at the caret or on selected line ranges on either diff side; compose/edit with normal editor interaction, delete drafts, navigate comments, and open a review overview. - add comments at the caret or on selected line ranges on either diff side; compose/edit with normal editor interaction, delete drafts, navigate comments, and open a review overview.
- start with gutter markers and a focused comment panel rather than inserting comment text into source rows; iterate on the interaction before adding remote operations. - start with gutter markers and a focused comment panel rather than inserting comment text into source rows; iterate on the interaction before adding remote operations.
+2 -1
View File
@@ -219,7 +219,8 @@ Normal and Visual Ctrl+u/Ctrl+d invoke `config.core.scroll_step_up` / `config.co
| --- | --- | | --- | --- |
| Normal | `insert`, `append`, `open_below`, `open_above` | | Normal | `insert`, `append`, `open_below`, `open_above` |
| Normal, Visual | `insert_first_nonblank`, `append_line_end`, `clone_above`, `clone_below` | | Normal, Visual | `insert_first_nonblank`, `append_line_end`, `clone_above`, `clone_below` |
| Normal | `join_lines`, `undo`, `redo`, `command` | | Normal | `join_lines`, `undo`, `redo` |
| Normal, Visual | `command` |
| Normal | `copy_line`, `delete_line`, `change_line` | | Normal | `copy_line`, `delete_line`, `change_line` |
| Normal | `change_to_line_end`, `delete_to_line_end`, `copy_to_line_end`, `substitute_character`, `delete_word_forward`, `delete_word_backward`, `change_word_forward`, `change_word_backward` | | Normal | `change_to_line_end`, `delete_to_line_end`, `copy_to_line_end`, `substitute_character`, `delete_word_forward`, `delete_word_backward`, `change_word_forward`, `change_word_backward` |
| Normal, Visual | `replace_character` | | Normal, Visual | `replace_character` |
+1
View File
@@ -37,6 +37,7 @@ Rust embeds the Ruby implementation at compile time: [`ruby_values.rb`](../src/r
Rust tests live alongside their modules; native LSP integration checks are in [`tests/lsp.rs`](../tests/lsp.rs). Rust tests live alongside their modules; native LSP integration checks are in [`tests/lsp.rs`](../tests/lsp.rs).
- Editor checks cover Unicode editing, multi-target state, undo/redo, saving failures, and modal paste acceptance and rejection atomicity. - Editor checks cover Unicode editing, multi-target state, undo/redo, saving failures, and modal paste acceptance and rejection atomicity.
- Visual-command checks preserve directed characterwise/linewise ranges and multiple carets through command cancellation and plugin dispatch, exercise remapped command entry, and submit `:diff.comment` on either historical side. Live PTY smoke selects lines, cancels a prompt, then runs `:diff.comment` directly and verifies the saved original range without using the palette.
- Markdown navigation checks cover inline/reference destinations, escaped/percent-encoded paths, heading markup and duplicates, same-document fragments, dirty target reuse, failed navigation atomicity, primary-only following, and `gf` prefix cancellation/configuration overrides. - Markdown navigation checks cover inline/reference destinations, escaped/percent-encoded paths, heading markup and duplicates, same-document fragments, dirty target reuse, failed navigation atomicity, primary-only following, and `gf` prefix cancellation/configuration overrides.
- Jump-history checks cover cross-buffer/same-document navigation, search and picker recording, counts and repeated visits, bounded history, branch truncation, closed buffers, edited-coordinate clamping, dirty text/undo preservation, independent Ctrl+i/Tab bindings, and custom bindings. Diff checks exercise Ctrl+i on either side in split/unified views, both control-letter and modified-Tab terminal events, and plain Tab pane cycling. Real LSP fixtures cover definition round-trips and delayed replies after leaving and returning to the same source location. - Jump-history checks cover cross-buffer/same-document navigation, search and picker recording, counts and repeated visits, bounded history, branch truncation, closed buffers, edited-coordinate clamping, dirty text/undo preservation, independent Ctrl+i/Tab bindings, and custom bindings. Diff checks exercise Ctrl+i on either side in split/unified views, both control-letter and modified-Tab terminal events, and plain Tab pane cycling. Real LSP fixtures cover definition round-trips and delayed replies after leaving and returning to the same source location.
- Tab checks cover shared edits/history with independent pane layouts, focused views, explorers and viewports; cross-tab close guards, immutable snapshots, hidden comparison refresh/result routing, and separate repository workers. Rendered checks cover sanitized/dirty titles, active-tab visibility in narrow terminals, overlay row reservation, and exclusion of hidden diff geometry from palette polling and viewport reporting. - Tab checks cover shared edits/history with independent pane layouts, focused views, explorers and viewports; cross-tab close guards, immutable snapshots, hidden comparison refresh/result routing, and separate repository workers. Rendered checks cover sanitized/dirty titles, active-tab visibility in narrow terminals, overlay row reservation, and exclusion of hidden diff geometry from palette polling and viewport reporting.
+5 -2
View File
@@ -33,7 +33,8 @@ Indicators refresh asynchronously. Typing and undo/redo retain the last complete
| Normal, Visual | Decimal prefixes repeat cursor/word/search motions: `5j`, `3k`, `2w`, `4e`, `2n`; Esc cancels a pending count | | Normal, Visual | Decimal prefixes repeat cursor/word/search motions: `5j`, `3k`, `2w`, `4e`, `2n`; Esc cancels a pending count |
| Normal, Visual | Ctrl+u/Ctrl+d scroll up/down without moving carets or selections; half a viewport by default, configurable with `config.scroll_step_lines`; counts multiply the distance | | Normal, Visual | Ctrl+u/Ctrl+d scroll up/down without moving carets or selections; half a viewport by default, configurable with `config.scroll_step_lines`; counts multiply the distance |
| Normal | `J` (Shift+j) joins each targeted line with the next line | | Normal | `J` (Shift+j) joins each targeted line with the next line |
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode | | Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo |
| Normal, Visual | `:` enters command mode; Visual selections remain available to selection-aware commands |
| Normal | `yy` copies whole lines without moving; `dd` deletes them; `cc` changes them and enters Insert; counts work (`3yy`, `2dd`, `2cc`) | | Normal | `yy` copies whole lines without moving; `dd` deletes them; `cc` changes them and enters Insert; counts work (`3yy`, `2dd`, `2cc`) |
| Normal | `D` deletes through line end; `C` changes that suffix and enters Insert; `Y` copies that suffix characterwise | | Normal | `D` deletes through line end; `C` changes that suffix and enters Insert; `Y` copies that suffix characterwise |
| Normal, Visual | `r<char>` replaces the current grapheme in Normal or every selected grapheme in Visual | | Normal, Visual | `r<char>` replaces the current grapheme in Normal or every selected grapheme in Visual |
@@ -189,6 +190,8 @@ Submitting an empty search repeats the last query. Esc cancels without moving th
`:` opens compact floating command input titled **Command**; `/` opens input titled **Search** for buffer text search. Titles sit at the top-left border, replacing the displayed leading `:` / `/`. Both input fields have one column of padding on each side. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Both windows default to the workspace's bottom-left edge, directly above the bottom bar when visible; their [anchors can be configured independently](configuration.md#command-and-search-popup-anchors). They temporarily hide operation notifications and never reserve a permanent prompt/footer row. Tiny windows omit borders/titles or padding when needed to keep input usable. `:` opens compact floating command input titled **Command**; `/` opens input titled **Search** for buffer text search. Titles sit at the top-left border, replacing the displayed leading `:` / `/`. Both input fields have one column of padding on each side. Enter submits and Escape cancels using the existing modal rules. Long Unicode input scrolls horizontally without changing buffer text. Both windows default to the workspace's bottom-left edge, directly above the bottom bar when visible; their [anchors can be configured independently](configuration.md#command-and-search-popup-anchors). They temporarily hide operation notifications and never reserve a permanent prompt/footer row. Tiny windows omit borders/titles or padding when needed to keep input usable.
Command input works in both Normal and Visual modes, including read-only diff panes. Enter dispatches with the original Visual ranges, direction, and linewise shape intact; individual commands decide whether to consume or retain them. Escape returns to the original Visual selection without executing. For example, select lines with `V`, then type `:diff.comment` and press Enter—no command palette is needed. `config.core.command` can also be bound in Visual mode.
The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible, with one column of inner padding on each side when space permits. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar. The latest reported operation result appears in a non-focusable popup at the workspace's bottom-right, just above the bottom bar when visible, with one column of inner padding on each side when space permits. It remains until dismissed or replaced, with no timer. In Normal mode, the first Escape dismisses it without collapsing carets; a subsequent Escape retains ordinary Normal behavior. Existing modal windows, focused explorers/diff selectors, and pending prefixes retain Escape priority. New operation messages reopen the popup; routine successful buffer switches do not duplicate the buffer/path information already shown in the bar.
Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands. Command-capable core actions derive canonical Ex names by removing `core.`: `core.quit` gives `:quit`, `core.save` gives `:save`, and `core.diff.close` gives `:diff.close`. Native editing and Insert-only completion/snippet actions remain action bindings, not additional Ex commands.
@@ -449,7 +452,7 @@ Collapsed folders show a green **`✓`** when every changed file beneath them is
Review marks belong only to the open comparison and survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. A new or recomputed comparison clears them. Editing a writable After file clears only that file's mark; unchanged refreshes and saves preserve it. Marks do not change document text, undo history, or repository state, and are not persisted after closing the comparison. Review marks belong only to the open comparison and survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. A new or recomputed comparison clears them. Editing a writable After file clears only that file's mark; unchanged refreshes and saves preserve it. Marks do not change document text, undo history, or repository state, and are not persisted after closing the comparison.
**Local review comments** are independent of GitHub and the file-reviewed checkmarks above. Focus either diff text pane and run **`:diff.comment`** to comment at the caret. For a range, make one Visual or Visual Line selection, then choose `diff.comment` through Ctrl+p or a configured Visual-mode binding. Multiple independent carets/ranges must be reviewed separately. **Local review comments** are independent of GitHub and the file-reviewed checkmarks above. Focus either diff text pane and run **`:diff.comment`** to comment at the caret. For a range, make one Visual or Visual Line selection, then type **`:diff.comment`** and press Enter. The palette and configured Visual-mode bindings also work. Multiple independent carets/ranges must be reviewed separately.
Comments and summaries open in a focused native split pane, with their own heading, normal modes, selections, undo/redo, and pane-focus bindings. Source rows are never replaced with comment text. Magenta **`○`** gutter markers identify current comment ranges; **`●`** identifies the selected comment. Markers preserve addition/deletion signs, appear only on the first wrapped segment, and also work in unified view. Comments and summaries open in a focused native split pane, with their own heading, normal modes, selections, undo/redo, and pane-focus bindings. Source rows are never replaced with comment text. Magenta **`○`** gutter markers identify current comment ranges; **`●`** identifies the selected comment. Markers preserve addition/deletion signs, appear only on the first wrapped segment, and also work in unified view.
+79 -10
View File
@@ -508,7 +508,11 @@ core_actions![
Action::RepeatLastEdit, Action::RepeatLastEdit,
&[Mode::Normal, Mode::Visual], &[Mode::Normal, Mode::Visual],
), ),
("core.command", Action::Command, &[Mode::Normal]), (
"core.command",
Action::Command,
&[Mode::Normal, Mode::Visual]
),
("core.search", Action::Search, &[Mode::Normal]), ("core.search", Action::Search, &[Mode::Normal]),
("core.search_word", Action::SearchWord, &[Mode::Normal]), ("core.search_word", Action::SearchWord, &[Mode::Normal]),
( (
@@ -1350,7 +1354,6 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
('s', SubstituteCharacter), ('s', SubstituteCharacter),
('u', Undo), ('u', Undo),
('U', Redo), ('U', Redo),
(':', Command),
('/', Search), ('/', Search),
('*', SearchWord), ('*', SearchWord),
('p', PasteAfter), ('p', PasteAfter),
@@ -1382,6 +1385,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
} }
for mode in [Mode::Normal, Mode::Visual] { for mode in [Mode::Normal, Mode::Visual] {
for (key, action) in [ for (key, action) in [
(':', Command),
('h', Left), ('h', Left),
('j', Down), ('j', Down),
('k', Up), ('k', Up),
@@ -1670,6 +1674,7 @@ struct ViewState {
search_highlight: bool, search_highlight: bool,
search_match: Option<SearchMatch>, search_match: Option<SearchMatch>,
command_line: String, command_line: String,
command_from_visual: bool,
caret_reveal_revision: u64, caret_reveal_revision: u64,
lsp_center: Cell<Option<(u64, u64, Cursor)>>, lsp_center: Cell<Option<(u64, u64, Cursor)>>,
viewport_scroll: Cell<(isize, isize)>, viewport_scroll: Cell<(isize, isize)>,
@@ -1691,6 +1696,7 @@ impl ViewState {
search_highlight: false, search_highlight: false,
search_match: None, search_match: None,
command_line: String::new(), command_line: String::new(),
command_from_visual: false,
caret_reveal_revision: 0, caret_reveal_revision: 0,
lsp_center: Cell::new(None), lsp_center: Cell::new(None),
viewport_scroll: Cell::new((0, 0)), viewport_scroll: Cell::new((0, 0)),
@@ -1712,6 +1718,7 @@ impl ViewState {
search_highlight: self.search_highlight, search_highlight: self.search_highlight,
search_match: self.search_match, search_match: self.search_match,
command_line: self.command_line.clone(), command_line: self.command_line.clone(),
command_from_visual: self.command_from_visual,
caret_reveal_revision: self.caret_reveal_revision, caret_reveal_revision: self.caret_reveal_revision,
visual_motions: self.visual_motions.clone(), visual_motions: self.visual_motions.clone(),
..Self::empty() ..Self::empty()
@@ -7611,6 +7618,7 @@ impl Editor {
self.pane_close_picker_origin = None; self.pane_close_picker_origin = None;
self.active.view.command_line = format!("{} ", entry.name); self.active.view.command_line = format!("{} ", entry.name);
self.palette = None; self.palette = None;
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
if self.diff.is_some() && !self.diff_interacting() { if self.diff.is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode); self.diff_prompt_mode.get_or_insert(self.active.view.mode);
} else if self.active.view.mode == Mode::Insert { } else if self.active.view.mode == Mode::Insert {
@@ -8239,14 +8247,19 @@ impl Editor {
None None
} }
fn finish_diff_prompt(&mut self) { fn finish_prompt(&mut self) {
if let Some(mode) = self.diff_prompt_mode.take() { if let Some(mode) = self.diff_prompt_mode.take() {
self.active.view.mode = mode; self.active.view.mode = mode;
self.active.view.command_line.clear(); self.active.view.command_line.clear();
} }
if std::mem::take(&mut self.active.view.command_from_visual) {
self.active.view.mode = Mode::Visual;
self.active.view.command_line.clear();
}
} }
fn open_action_prompt(&mut self, entry: &CoreAction) { fn open_action_prompt(&mut self, entry: &CoreAction) {
self.active.view.command_from_visual = self.active.view.mode == Mode::Visual;
if self.diff().is_some() && !self.diff_interacting() { if self.diff().is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode); self.diff_prompt_mode.get_or_insert(self.active.view.mode);
self.palette = None; self.palette = None;
@@ -8268,8 +8281,10 @@ impl Editor {
{ {
self.pane_close_picker_origin = None; self.pane_close_picker_origin = None;
} }
if action == Action::Normal && self.diff_prompt_mode.is_some() { if action == Action::Normal
self.finish_diff_prompt(); && (self.diff_prompt_mode.is_some() || self.active.view.command_from_visual)
{
self.finish_prompt();
self.count = None; self.count = None;
self.clear_sequence(); self.clear_sequence();
return None; return None;
@@ -8544,6 +8559,8 @@ impl Editor {
Action::Redo => self.redo(), Action::Redo => self.redo(),
Action::RepeatLastEdit => self.repeat_last_edit(), Action::RepeatLastEdit => self.repeat_last_edit(),
Action::Command | Action::Search => { Action::Command | Action::Search => {
self.active.view.command_from_visual =
action == Action::Command && self.active.view.mode == Mode::Visual;
if self.diff().is_some() && !self.diff_interacting() { if self.diff().is_some() && !self.diff_interacting() {
self.diff_prompt_mode.get_or_insert(self.active.view.mode); self.diff_prompt_mode.get_or_insert(self.active.view.mode);
} }
@@ -8978,6 +8995,7 @@ impl Editor {
self.normalize_targets(); self.normalize_targets();
} }
self.active.view.command_line.clear(); self.active.view.command_line.clear();
self.active.view.command_from_visual = false;
self.count = None; self.count = None;
self.clear_sequence(); self.clear_sequence();
self.palette = None; self.palette = None;
@@ -9252,7 +9270,7 @@ impl Editor {
pub fn execute(&mut self, command: &str) -> Option<Invocation> { pub fn execute(&mut self, command: &str) -> Option<Invocation> {
self.pending_lsp_navigation = None; self.pending_lsp_navigation = None;
self.finish_diff_prompt(); self.finish_prompt();
let invocation = self.execute_command(command); let invocation = self.execute_command(command);
self.leave_diff_on_navigation(); self.leave_diff_on_navigation();
self.refresh_live_diff(); self.refresh_live_diff();
@@ -9278,8 +9296,10 @@ impl Editor {
self.active.view.command_line.clear(); self.active.view.command_line.clear();
} else { } else {
self.prepare_command(); self.prepare_command();
if self.active.view.mode != Mode::Visual {
self.finish_command_mode(); self.finish_command_mode();
} }
}
let Some(feature) = self.owner(name) else { let Some(feature) = self.owner(name) else {
self.set_message(format!("Unknown command: {name}")); self.set_message(format!("Unknown command: {name}"));
return None; return None;
@@ -9302,6 +9322,7 @@ impl Editor {
} }
fn finish_command_mode(&mut self) { fn finish_command_mode(&mut self) {
self.active.view.command_from_visual = false;
if self.active.view.mode == Mode::Insert { if self.active.view.mode == Mode::Insert {
self.finish_insert(); self.finish_insert();
} }
@@ -9324,7 +9345,7 @@ impl Editor {
| Action::ReviewSave | Action::ReviewSave
| Action::ReviewOpen | Action::ReviewOpen
) { ) {
self.finish_diff_prompt(); self.finish_prompt();
if let Err(error) = self.local_review_command(entry.action, args) { if let Err(error) = self.local_review_command(entry.action, args) {
self.set_message(error); self.set_message(error);
} }
@@ -9408,7 +9429,7 @@ impl Editor {
if !matches!(entry.action, Action::PaneClose | Action::PaneForceClose) { if !matches!(entry.action, Action::PaneClose | Action::PaneForceClose) {
self.pane_close_picker_origin = None; self.pane_close_picker_origin = None;
} }
self.finish_diff_prompt(); self.finish_prompt();
if matches!( if matches!(
entry.action, entry.action,
Action::PaneSplitHorizontal Action::PaneSplitHorizontal
@@ -15928,9 +15949,53 @@ mod tests {
} }
#[test] #[test]
fn local_review_visual_palette_anchors_both_sides_and_preserves_stale_ranges() { fn visual_commands_preserve_directed_ranges_on_cancel_and_plugin_dispatch() {
for shape in ["vll", "vhh", "Vj"] {
let mut editor = editor();
editor
.apply(vec![Effect::Replace(
"abcdef\nsecond\nabcdef\nfourth".into(),
)])
.unwrap();
editor.register(plugin("inspect", &["inspect"])).unwrap();
set_carets(&mut editor, &[(0, 2), (2, 2)], 1);
type_text(&mut editor, shape);
let selections = editor.selections();
let carets = editor.carets();
let linewise = editor.active.view.linewise;
type_text(&mut editor, ":inspect");
assert_eq!(editor.mode(), Mode::Command);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.carets(), carets);
assert_eq!(editor.active.view.linewise, linewise);
editor
.bind("visual", ",", Some("core.command".into()))
.unwrap();
editor.handle(Input::Char(','));
type_text(&mut editor, "inspect");
editor
.handle(Input::Enter)
.expect("selection-aware plugin invocation");
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.carets(), carets);
assert_eq!(editor.active.view.linewise, linewise);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Normal);
}
}
#[test]
fn local_review_visual_commands_and_palette_preserve_both_side_ranges() {
use crate::review::Side; use crate::review::Side;
for focus in [Focus::Before, Focus::After] { for (focus, palette) in [
(Focus::Before, false),
(Focus::After, false),
(Focus::Before, true),
(Focus::After, true),
] {
let directory = Directory::new(); let directory = Directory::new();
let root = directory.0.canonicalize().unwrap(); let root = directory.0.canonicalize().unwrap();
let mut editor = editor(); let mut editor = editor();
@@ -15943,8 +16008,12 @@ mod tests {
editor.diff.as_mut().unwrap().focus = focus; editor.diff.as_mut().unwrap().focus = focus;
editor.bind_diff_buffer(); editor.bind_diff_buffer();
type_text(&mut editor, "Vj"); type_text(&mut editor, "Vj");
if palette {
editor.handle(Input::Ctrl('p')); editor.handle(Input::Ctrl('p'));
type_text(&mut editor, "diff.comment"); type_text(&mut editor, "diff.comment");
} else {
type_text(&mut editor, ":diff.comment");
}
editor.handle(Input::Enter); editor.handle(Input::Enter);
let diff = editor.workspace_diff().unwrap(); let diff = editor.workspace_diff().unwrap();
let anchor = diff.local_review.document.comments[0].anchor.clone(); let anchor = diff.local_review.document.comments[0].anchor.clone();
+2 -1
View File
@@ -39,6 +39,7 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
y/d/c/p/P use the system clipboard by default; config.clipboard_mode = \"separate\" isolates it y/d/c/p/P use the system clipboard by default; config.clipboard_mode = \"separate\" isolates it
Esc keeps only the primary caret Esc keeps only the primary caret
Visual: motions extend selections, y copy, d/x/Delete cut, c change, p/P replace selections Visual: motions extend selections, y copy, d/x/Delete cut, c change, p/P replace selections
: opens command input with the selection preserved; Esc returns to that selection
r<char> replaces every selected grapheme, preserving newlines; one undo action r<char> replaces every selected grapheme, preserving newlines; one undo action
I/A insert at each selected line's first nonblank/end I/A insert at each selected line's first nonblank/end
Ctrl+l split rows, Ctrl+n add occurrence, Ctrl+s skip, Ctrl+a all Ctrl+l split rows, Ctrl+n add occurrence, Ctrl+s skip, Ctrl+a all
@@ -82,7 +83,7 @@ Diff: :diff (Normal); choose Before then After; Working checkout After edits liv
Esc closes Before/After selectors; :diff.close exits an opened comparison explicitly Esc closes Before/After selectors; :diff.close exits an opened comparison explicitly
Remote refs/history load into temporary comparison repos without changing active refs/checkout Remote refs/history load into temporary comparison repos without changing active refs/checkout
Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths Changed files: normal picker/explorer bindings, Ctrl+w close; A/M/D/R status and rename paths
Local reviews: :diff.comment at caret; use palette or Visual binding for a selected line range Local reviews: :diff.comment at caret or on a Visual/Visual Line selection
:diff.review.edit/delete [ID], :diff.review.next/previous, :diff.review.overview :diff.review.edit/delete [ID], :diff.review.next/previous, :diff.review.overview
:diff.review.summary; :diff.review.verdict looks_good|changes_requested|none :diff.review.summary; :diff.review.verdict looks_good|changes_requested|none
:diff.review.save PATH / :diff.review.open PATH; :w in review panes saves local JSON :diff.review.save PATH / :diff.review.open PATH; :w in review panes saves local JSON