feat: add linewise Visual selection and paste
This commit is contained in:
@@ -24,8 +24,8 @@ Vamp starts in Normal mode. Press `i`, type text, then Esc to return to Normal.
|
|||||||
| Save in Normal | Space then `w`, or `:w` then Enter |
|
| Save in Normal | Space then `w`, or `:w` then Enter |
|
||||||
| Undo/redo in Normal | `u` / `U` (Shift+u) |
|
| Undo/redo in Normal | `u` / `U` (Shift+u) |
|
||||||
| Search in Normal | `/`, type a literal query, Enter; `n`/`N` for next/previous |
|
| Search in Normal | `/`, type a literal query, Enter; `n`/`N` for next/previous |
|
||||||
| Select/copy/cut | `v` in Normal, move to extend, then `y`/`d` in Visual |
|
| Select/copy/cut | `v` characterwise or `V` whole rows, move to extend, then `y`/`d` in Visual |
|
||||||
| Paste in Normal | `p`/`P` after/before the caret; system clipboard by default |
|
| Paste in Normal | `p`/`P` after/before the caret or row for linewise fragments; system clipboard by default |
|
||||||
| Command palette | Ctrl+p; type to filter, arrows select, Enter accepts, Esc cancels |
|
| Command palette | Ctrl+p; type to filter, arrows select, Enter accepts, Esc cancels |
|
||||||
| Floating/docked explorer in Normal or Visual | Space then `o` / Space then `e` |
|
| Floating/docked explorer in Normal or Visual | Space then `o` / Space then `e` |
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ Ruby configuration and plugins run with your account's filesystem/process permis
|
|||||||
## Current limits
|
## Current limits
|
||||||
|
|
||||||
- One visible buffer at a time; no splits/tabs or graphical frontend.
|
- One visible buffer at a time; no splits/tabs or graphical frontend.
|
||||||
- Modal editing, not full Vim compatibility: characterwise Visual selections, but no line/block selection, named registers, or Vim operators. Search is literal, case-sensitive, and single-line.
|
- Modal editing, not full Vim compatibility: characterwise and linewise Visual selections, but no block selection, named registers, or Vim operators. Search is literal, case-sensitive, and single-line.
|
||||||
- Native opt-in LSP supports diagnostics, go-to-definition, plain-text hover, and automatic semantic highlighting—not completion, rename, formatting, or code actions.
|
- Native opt-in LSP supports diagnostics, go-to-definition, plain-text hover, and automatic semantic highlighting—not completion, rename, formatting, or code actions.
|
||||||
- System clipboard access supports macOS and Linux. Separate clipboard mode keeps internal editing available on unsupported/headless environments.
|
- System clipboard access supports macOS and Linux. Separate clipboard mode keeps internal editing available on unsupported/headless environments.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ One crate; no workspace or terminal-coordinate types in the editing backend:
|
|||||||
## Frontend interface
|
## Frontend interface
|
||||||
|
|
||||||
Frontends drive `EditorSession::handle` or `execute`, periodically call `poll()` even without input, and inspect `editor()`; there is no mutable editor getter that could independently alter plugin registration. `poll()` reports whether background activity requires redrawing. Pasted text uses `Input::Paste(String)` through the same input seam as keys. Plugin effects are applied internally so a rejected result also retires its owner; there is no generic frontend effects entry point.
|
Frontends drive `EditorSession::handle` or `execute`, periodically call `poll()` even without input, and inspect `editor()`; there is no mutable editor getter that could independently alter plugin registration. `poll()` reports whether background activity requires redrawing. Pasted text uses `Input::Paste(String)` through the same input seam as keys. Plugin effects are applied internally so a rejected result also retires its owner; there is no generic frontend effects entry point.
|
||||||
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. Selection normalization, clipboard contents, edit planning, and history remain inside the editor module.
|
Frontends inspect `selections()` for document-ordered, end-exclusive Visual ranges of grapheme coordinates (empty outside Visual), `carets()` for all active cursors in document order, `cursor()` for the primary, and `selection_count()` for the total target count. `visual_linewise()` is true only for the whole-row shape in `Mode::Visual`; it changes neither binding modes nor Ruby mode names. Linewise ranges expand to complete rows. A range ending at the next row's column 0 includes the previous row's newline; an empty final-row range contains no text. `linewise_rows()` exposes inclusive selected row ranges only in linewise Visual, including an empty final row without changing `selections()` cardinality. The TUI highlights those rows across the text viewport, never the gutter, and labels their mode VISUAL LINE. Selection normalization, clipboard shape and contents, edit planning, and history remain inside the editor module.
|
||||||
The TUI also observes the editor's logical caret-reveal generation. Core motion/navigation dispatch advances it even when the resulting position is unchanged, so a detached viewport can resume following without duplicating key-binding policy or changing text revisions and LSP freshness.
|
The TUI also observes the editor's logical caret-reveal generation. Core motion/navigation dispatch advances it even when the resulting position is unchanged, so a detached viewport can resume following without duplicating key-binding policy or changing text revisions and LSP freshness.
|
||||||
Frontends inspect `palette()` for the optional read-only query, filtered command entries, selected index, effective bindings, and eligibility. Palette input handling and acceptance stay in the editor; registered command execution stays in the session.
|
Frontends inspect `palette()` for the optional read-only query, filtered command entries, selected index, effective bindings, and eligibility. Palette input handling and acceptance stay in the editor; registered command execution stays in the session.
|
||||||
Frontends use cached `selection_navigation_hint()` for command lists and `file_navigation_hint()` for the active file view/context, without reconstructing input policy.
|
Frontends use cached `selection_navigation_hint()` for command lists and `file_navigation_hint()` for the active file view/context, without reconstructing input policy.
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ The built-in Normal `gf` sequence follows Markdown file links. It is not a confi
|
|||||||
| Normal | `lsp.definition`, `lsp.diagnostics`, `lsp.hover` |
|
| Normal | `lsp.definition`, `lsp.diagnostics`, `lsp.hover` |
|
||||||
| Normal | `search`, `paste_before`, `paste_after` |
|
| Normal | `search`, `paste_before`, `paste_after` |
|
||||||
| Normal | `paste_system_before`, `paste_system_after` |
|
| Normal | `paste_system_before`, `paste_system_after` |
|
||||||
| Normal, Visual | `select`, `search_next`, `search_previous` |
|
| Normal, Visual | `select`, `select_linewise`, `search_next`, `search_previous` |
|
||||||
| Normal, Visual | `repeat_last_edit` |
|
| Normal, Visual | `repeat_last_edit` |
|
||||||
| Normal, Visual | `file_picker`, `file_explorer` |
|
| Normal, Visual | `file_picker`, `file_explorer` |
|
||||||
| Normal, Insert, Visual | `line_start`, `line_end` |
|
| Normal, Insert, Visual | `line_start`, `line_end` |
|
||||||
@@ -86,6 +86,8 @@ The built-in Normal `gf` sequence follows Markdown file links. It is not a confi
|
|||||||
| Insert | `newline` |
|
| Insert | `newline` |
|
||||||
| Command, Search | `submit` |
|
| Command, Search | `submit` |
|
||||||
|
|
||||||
|
`core.select` (`v`) and `core.select_linewise` (`V`) select characterwise and whole-row Visual shapes. Switching selectors retains endpoints; pressing the active shape's selector again cancels. Both use `config.modes.visual`; there is no separate linewise binding mode. `core.select_lines` (Visual Ctrl+l) still splits touched rows into Normal carets.
|
||||||
|
|
||||||
## Binding overrides and failures
|
## Binding overrides and failures
|
||||||
|
|
||||||
Adding a binding keeps the original key. Pass `nil` to disable a key, including literal typing in Insert/Command mode:
|
Adding a binding keeps the original key. Pass `nil` to disable a key, including literal typing in Insert/Command mode:
|
||||||
|
|||||||
+10
-10
@@ -23,9 +23,9 @@ Default controls and editing behavior. For custom bindings, see [configuration](
|
|||||||
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
| Normal | `x`/Delete delete; `u` undo; `U` (Shift+u) redo; `:` enter command mode |
|
||||||
| Normal, Visual | `.` repeats the last successful native edit against the current targets; one undo group |
|
| Normal, Visual | `.` repeats the last successful native edit against the current targets; one undo group |
|
||||||
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
| Normal | `/` opens search; `n` finds the next match; `N` (Shift+n) finds the previous match |
|
||||||
| Normal | `v` starts Visual selections; `p`/`P` paste the configured clipboard after/before each caret (system by default); Esc keeps only the primary caret |
|
| Normal | `v` starts characterwise Visual; `V` starts linewise Visual; `p`/`P` paste the configured clipboard after/before each caret or row for linewise fragments (system by default); Esc keeps only the primary caret |
|
||||||
| Normal, Visual | Ctrl+k clones above; Ctrl+e clones below |
|
| Normal, Visual | Ctrl+k clones above; Ctrl+e clones below |
|
||||||
| Visual | Cursor/word motions and `n`/`N` extend every selection; `y` copies; `d`/`x`/Delete cuts; `c` changes selections; `v`/Esc cancels highlighting |
|
| Visual | Cursor/word motions and `n`/`N` extend every selection; `y` copies; `d`/`x`/Delete cuts; `c` changes; `v`/`V` switches shape, the same selector or Esc cancels |
|
||||||
| Visual | Ctrl+l splits touched rows into Normal carets at first nonblank; Ctrl+n adds the next occurrence; Ctrl+s skips the primary occurrence; Ctrl+a selects all occurrences |
|
| Visual | Ctrl+l splits touched rows into Normal carets at first nonblank; Ctrl+n adds the next occurrence; Ctrl+s skips the primary occurrence; Ctrl+a selects all occurrences |
|
||||||
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End at every caret; Esc returns to Normal, retaining carets |
|
| Insert | Type, Enter, Tab, Backspace/Delete, arrows, Home/End at every caret; Esc returns to Normal, retaining carets |
|
||||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||||
@@ -51,17 +51,17 @@ The status bar displays a pending count. Esc cancels it without moving, collapsi
|
|||||||
|
|
||||||
## Selections and multi-caret editing
|
## Selections and multi-caret editing
|
||||||
|
|
||||||
Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. Visual selection is characterwise and inclusive of the endpoints; its read-only ranges are end-exclusive. `$`/End can select the newline after a non-final row; empty-line newlines are selectable too. Selected newlines appear as a blue blank cell after the line. Blue highlighting overrides yellow search highlighting; secondary carets are magenta, including on empty lines and at line ends. Only the primary owns the hardware cursor and viewport. The status line counts all targets, including offscreen targets.
|
Editing uses an ordered selection set with one primary target. Cursor and word motions apply to every target; Visual motions keep each target's anchor and move its active endpoint. Whole combining sequences and ZWJ emoji remain intact. `v` selects characterwise, inclusive of endpoints; `V` selects every complete row touched by the endpoints, including blank and empty final rows. Switching between `v` and `V` retains endpoints; pressing the current shape's selector again cancels. Read-only ranges are end-exclusive. Characterwise `$`/End can select the newline after a non-final row; selected newlines appear as a blue blank cell after the line. Linewise highlighting fills the text viewport width, never the gutter, and the status reads VISUAL LINE. Blue highlighting overrides yellow search highlighting; secondary carets are magenta, including on empty lines and at line ends. Only the primary owns the hardware cursor and viewport. The status line counts all targets, including offscreen targets.
|
||||||
|
|
||||||
Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual `I`/`A` similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.
|
Ctrl+k/Ctrl+e clone each target to the adjacent row, preserving preferred terminal-cell columns (tabs use four-cell stops); Visual cloning preserves both endpoints. Duplicate carets and truly overlapping selections merge; adjacent ranges remain distinct. Visual Ctrl+l creates one Normal caret at the first nonblank of each touched row. Visual `I`/`A` similarly create one Insert caret per touched row at first nonblank/end. Occurrence actions use the primary selection as a literal, potentially multiline, grapheme-aligned query: Ctrl+n adds the next non-overlapping occurrence and wraps; Ctrl+s moves only the primary to that occurrence; Ctrl+a selects all occurrences.
|
||||||
|
|
||||||
Copy and cut return to Normal at each selection start; Visual cancellation keeps each active endpoint and removes highlighting. Normal Esc collapses to the primary. `p`/`P` paste after/before each current grapheme, including multiline text. Each caret lands on its last inserted grapheme, or the new row's start when its fragment ends in a newline. Every multi-target edit is planned against the original buffer and committed as one undo action, not one per target. Copy, movement, and cancellation do not add history or clear redo. Undo/redo restore text, the full caret set, and primary identity in Normal mode, not Visual highlighting or the clipboard.
|
Copy and cut return to Normal at each selection start; Visual cancellation keeps each active endpoint and removes highlighting. Normal Esc collapses to the primary. Characterwise `p`/`P` paste after/before each current grapheme, including multiline text. Linewise clipboard fragments paste complete rows below/above each target row, including at end of file. Internal fragments retain their shape; system clipboard shape is retained only while its bytes match the editor's last publication, and unrelated external clipboard text is characterwise. Characterwise paste lands on its last inserted grapheme, or the new row's start when its fragment ends in a newline; linewise paste lands at the first nonblank of the first inserted row. Every multi-target edit is planned against the original buffer and committed as one undo action, not one per target. Copy, movement, and cancellation do not add history or clear redo. Undo/redo restore text, the full caret set, and primary identity in Normal mode, not Visual highlighting or the clipboard.
|
||||||
|
|
||||||
Visual `c` removes all selected text into the configured clipboard and enters Insert at each selection start. In the default System policy, clipboard publication must succeed before any deletion; failures leave text and selections intact. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only empty final-row selections enters Insert without overwriting either clipboard.
|
Visual `c` removes all selected text into the configured clipboard and enters Insert at each selection start. In the default System policy, clipboard publication must succeed before any deletion; failures leave text and selections intact. Deletion and replacement typing form one undo action when Insert mode is exited, including through explicit command execution. Esc without typing commits the deletion alone. An identical replacement creates no history entry and preserves redo. Changing only characterwise empty final-row selections enters Insert without overwriting either clipboard; linewise empty final rows are genuine rows and publish a newline.
|
||||||
|
|
||||||
Bracketed paste broadcasts the same literal text to all Insert carets in a separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal-mode operator grammar such as `cw` is not supported; `c` types literal text in Insert mode.
|
Bracketed paste broadcasts the same literal text to all Insert carets in a separate undo group, distinct from typing before/after it. A rejected paste leaves the pending change intact. Normal-mode operator grammar such as `cw`, `dd`, `yy`, or `cc` and blockwise Visual are not supported; `c` types literal text in Insert mode.
|
||||||
|
|
||||||
Clipboard policy is session-wide and defaults to the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing editing state. Empty clipboard paste does not edit text; copying/cutting only empty final-row selections does not overwrite either register. Open a new search or enter Ex commands after leaving Visual mode. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.
|
Clipboard policy is session-wide and defaults to the system clipboard. Copy/cut store fragments in document order: one fragment broadcasts to every caret; matching fragment/caret counts pair in document order; any other count refuses paste without changing editing state. Empty clipboard paste does not edit text; copying/cutting only characterwise empty final-row selections does not overwrite either register. Linewise copy/cut retain whole-row shape, including a newline fragment for an empty final row. Open a new search or enter Ex commands after leaving Visual mode. Plugin commands execute once, not once per target: successful text effects collapse to the primary and apply once; rejected or message-only effects preserve the selection set. There is no selection-aware plugin API yet.
|
||||||
|
|
||||||
## Clipboard policy
|
## Clipboard policy
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ config.bind "ctrl+s", config.core.copy_system, mode: config.modes.visual
|
|||||||
config.bind "ctrl+g", config.core.paste_system_before
|
config.bind "ctrl+g", config.core.paste_system_before
|
||||||
```
|
```
|
||||||
|
|
||||||
For other applications, multi-selection exports join fragments with LF. Vamp retains their original boundaries privately while the OS text exactly matches its export, so its own copies still paste pairwise. Different external text invalidates that metadata and becomes one literal fragment—even when it contains newlines—broadcasting to all targets. Identical external replacement text is inherently indistinguishable from an unchanged export. No empty-system fallback to stale internal text occurs.
|
For other applications, characterwise multi-selection exports join fragments with LF; linewise exports concatenate whole-row fragments without extra separators because each fragment already ends in LF. Vamp retains their original boundaries and shape privately while the OS text exactly matches its export, so its own copies still paste pairwise. Different external text invalidates that metadata and becomes one characterwise literal fragment—even when it contains newlines—broadcasting to all targets. Identical external replacement text is inherently indistinguishable from an unchanged export. No empty-system fallback to stale internal text occurs.
|
||||||
|
|
||||||
System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.
|
System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.
|
||||||
|
|
||||||
@@ -85,9 +85,9 @@ System clipboard access supports macOS through `osascript`, Linux Wayland throug
|
|||||||
|
|
||||||
`.` repeats the session's last successful native edit in Normal/Visual. Insertion placements (`i`/`I`/`a`/`A`/`o`/`O`), Insert typing and edit-context motions/newline/backspace/delete, Visual change/cut, Normal deletion, clipboard/literal paste, and line joining are recorded as resolved edit operations—not keystrokes, command invocations, or old byte offsets. Remapping keys afterward does not change the recorded operation. Insert `.` and popup query `.` remain literal. A count before dot is consumed and repeats once, consistent with other non-motion commands.
|
`.` repeats the session's last successful native edit in Normal/Visual. Insertion placements (`i`/`I`/`a`/`A`/`o`/`O`), Insert typing and edit-context motions/newline/backspace/delete, Visual change/cut, Normal deletion, clipboard/literal paste, and line joining are recorded as resolved edit operations—not keystrokes, command invocations, or old byte offsets. Remapping keys afterward does not change the recorded operation. Insert `.` and popup query `.` remain literal. A count before dot is consumed and repeats once, consistent with other non-motion commands.
|
||||||
|
|
||||||
In Normal, Visual change/cut targeting replays its resolved motions and counts at each current caret: `vecitem` followed later by `.` changes a fresh whole word, even if its length differs. If targeting was rebuilt by cloning, occurrence gathering, search, or overlap merging, repeat uses relative whole-grapheme spans instead. Equal spans broadcast; unequal spans require matching target counts. In Visual, the current explicit ranges replace the stored targeting. Target-building commands themselves do not recreate old cursors.
|
In Normal, characterwise Visual change/cut targeting replays its resolved motions and counts at each current caret: `vecitem` followed later by `.` changes a fresh whole word, even if its length differs. If characterwise targeting was rebuilt by cloning, occurrence gathering, search, or overlap merging, repeat uses relative whole-grapheme spans instead. Linewise targeting always uses signed relative row spans, preserving whole-row shape rather than replaying motions. Equal spans broadcast; unequal spans require matching target counts. In Visual, the current explicit ranges and selection shape replace the stored targeting. Target-building commands themselves do not recreate old cursors.
|
||||||
|
|
||||||
Clipboard paste captures the fragments actually pasted, so a later yank or external clipboard change does not change repeat's payload; dot paste never rereads the OS. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. Dot cut/change follows the current clipboard policy and stages publication until replay validation finishes. A refusal or provider error preserves editor text, caret/primary state, register caches, history, and recipe; already-published OS data cannot be transactionally restored.
|
Clipboard paste captures the fragments and their characterwise/linewise shape actually pasted, so a later yank or external clipboard change does not change repeat's payload; dot paste never rereads the OS. One fragment broadcasts; equal fragment/target counts pair; mismatches refuse atomically. Other per-target edit data, such as partially effective Insert deletion, obeys the same conservative distribution policy. Dot cut/change follows the current clipboard policy and stages publication until replay validation finishes. A refusal or provider error preserves editor text, caret/primary state, register caches, history, and recipe; already-published OS data cannot be transactionally restored.
|
||||||
|
|
||||||
Each repeat returns to Normal and creates one undo entry restoring the full caret set and primary. Original typing/paste/typing boundaries remain separate; dot replays the selected committed group as one transaction. Typed text retains scalar insertion boundaries, preserving combining/ZWJ interactions with surrounding graphemes; bracketed paste remains one bulk literal operation. Trailing Insert movement is not repeated.
|
Each repeat returns to Normal and creates one undo entry restoring the full caret set and primary. Original typing/paste/typing boundaries remain separate; dot replays the selected committed group as one transaction. Typed text retains scalar insertion boundaries, preserving combining/ZWJ interactions with surrounding graphemes; bracketed paste remains one bulk literal operation. Trailing Insert movement is not repeated.
|
||||||
|
|
||||||
|
|||||||
+846
-33
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -22,13 +22,13 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
|||||||
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
||||||
u undo, U redo, : command, / search, n/N next/previous match
|
u undo, U redo, : command, / search, n/N next/previous match
|
||||||
Normal/Visual: . repeats last native edit at current targets (one undo group)
|
Normal/Visual: . repeats last native edit at current targets (one undo group)
|
||||||
v select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
v characterwise select, V linewise select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||||
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
|
Visual: motions extend selections, y copy, d/x/Delete cut, c change
|
||||||
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
|
||||||
v/Esc returns to Normal retaining carets
|
v/V switches selection shape; same selector or Esc cancels retaining carets
|
||||||
Insert: Esc returns to Normal retaining carets
|
Insert: Esc returns to Normal retaining carets
|
||||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
||||||
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
Commands: :w [PATH], :q, :q!, :wq [PATH], :plugins, :PLUGIN [ARGS]
|
||||||
|
|||||||
@@ -1527,6 +1527,39 @@ end
|
|||||||
assert_eq!(session.editor().lines(), &["base"]);
|
assert_eq!(session.editor().lines(), &["base"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn configured_linewise_change_and_paste_preserve_rows_and_one_replacement_history() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
fixture.file("buffer.txt", " alpha\n界beta\ntail");
|
||||||
|
let mut session = fixture.open(
|
||||||
|
r#"
|
||||||
|
Vamp.configure do |config|
|
||||||
|
config.clipboard_mode = "separate"
|
||||||
|
config.bind "V", nil
|
||||||
|
config.bind "s", config.core.select_linewise
|
||||||
|
config.bind "s", config.core.select_linewise, mode: config.modes.visual
|
||||||
|
config.bind "r", config.core.change, mode: config.modes.visual
|
||||||
|
end
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
for key in "llsjrnew".chars() {
|
||||||
|
session.handle(Input::Char(key));
|
||||||
|
}
|
||||||
|
session.handle(Input::Escape);
|
||||||
|
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||||
|
session.handle(Input::Char('p'));
|
||||||
|
assert_eq!(
|
||||||
|
session.editor().lines(),
|
||||||
|
&["new", " alpha", "界beta", "tail"]
|
||||||
|
);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||||
|
session.handle(Input::Char('u'));
|
||||||
|
assert_eq!(session.editor().lines(), &[" alpha", "界beta", "tail"]);
|
||||||
|
session.handle(Input::Char('U'));
|
||||||
|
assert_eq!(session.editor().lines(), &["new", "tail"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn configured_selection_actions_edit_together_and_invoke_plugins_once() {
|
fn configured_selection_actions_edit_together_and_invoke_plugins_once() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|||||||
+93
-2
@@ -665,7 +665,18 @@ fn draw(
|
|||||||
buffer[(viewport.x + (column - *left) as u16, y)].set_style(style);
|
buffer[(viewport.x + (column - *left) as u16, y)].set_style(style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for selection in &selections {
|
let selection_style = Style::default().fg(Color::White).bg(Color::Blue);
|
||||||
|
for rows in editor.linewise_rows() {
|
||||||
|
let first = (*rows.start()).max(*top);
|
||||||
|
let last = (*rows.end()).min(top.saturating_add(height - 1));
|
||||||
|
for row in first..=last {
|
||||||
|
let y = viewport.y + (row - *top) as u16;
|
||||||
|
for x in viewport.x..viewport.x + viewport.width {
|
||||||
|
frame.buffer_mut()[(x, y)].set_style(selection_style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for selection in selections.iter().filter(|_| !editor.visual_linewise()) {
|
||||||
let first = selection.start.row.max(*top);
|
let first = selection.start.row.max(*top);
|
||||||
let last = selection.end.row.min(top.saturating_add(height - 1));
|
let last = selection.end.row.min(top.saturating_add(height - 1));
|
||||||
let style = Style::default().fg(Color::White).bg(Color::Blue);
|
let style = Style::default().fg(Color::White).bg(Color::Blue);
|
||||||
@@ -712,7 +723,11 @@ fn draw(
|
|||||||
.chars()
|
.chars()
|
||||||
.map(|c| if c.is_control() { '�' } else { c })
|
.map(|c| if c.is_control() { '�' } else { c })
|
||||||
.collect();
|
.collect();
|
||||||
let mut status = format!(" {}", editor.mode());
|
let mut status = if editor.visual_linewise() {
|
||||||
|
" VISUAL LINE".into()
|
||||||
|
} else {
|
||||||
|
format!(" {}", editor.mode())
|
||||||
|
};
|
||||||
if let Some(sequence) = editor.leader_pending() {
|
if let Some(sequence) = editor.leader_pending() {
|
||||||
write!(status, " leader: {sequence}").unwrap();
|
write!(status, " leader: {sequence}").unwrap();
|
||||||
}
|
}
|
||||||
@@ -5552,6 +5567,82 @@ end
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linewise_selection_fills_blank_and_final_rows_without_coloring_gutter() {
|
||||||
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
editor
|
||||||
|
.apply(vec![crate::editor::Effect::Replace("long text\n\n".into())])
|
||||||
|
.unwrap();
|
||||||
|
editor.handle(Input::Char('V'));
|
||||||
|
editor.handle(Input::Down);
|
||||||
|
editor.handle(Input::Down);
|
||||||
|
let before = (editor.lines().to_vec(), editor.cursor(), editor.carets());
|
||||||
|
for width in [12, 3, 1] {
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 6)).unwrap();
|
||||||
|
let (mut top, mut left) = (0, 0);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
draw(
|
||||||
|
frame,
|
||||||
|
&editor,
|
||||||
|
&mut top,
|
||||||
|
&mut left,
|
||||||
|
&mut SyntaxCache::new(),
|
||||||
|
&mut DocumentViewport::default(),
|
||||||
|
&mut WheelPanels::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
if width == 12 {
|
||||||
|
let status: String = (0..width).map(|x| buffer[(x, 4)].symbol()).collect();
|
||||||
|
assert!(status.contains("VISUAL LINE"), "{status}");
|
||||||
|
}
|
||||||
|
let gutter = if width > 2 { 2 } else { 0 };
|
||||||
|
for row in 0..3 {
|
||||||
|
for x in 0..gutter {
|
||||||
|
assert_ne!(buffer[(x, row)].bg, Color::Blue);
|
||||||
|
}
|
||||||
|
for x in gutter..width {
|
||||||
|
assert_eq!(buffer[(x, row)].bg, Color::Blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for x in 0..width {
|
||||||
|
assert_ne!(buffer[(x, 3)].bg, Color::Blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
before,
|
||||||
|
(editor.lines().to_vec(), editor.cursor(), editor.carets())
|
||||||
|
);
|
||||||
|
editor.handle(Input::Up);
|
||||||
|
editor.handle(Input::Up);
|
||||||
|
editor.handle(Input::End);
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(5, 3)).unwrap();
|
||||||
|
let (mut top, mut left) = (0, 0);
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
draw(
|
||||||
|
frame,
|
||||||
|
&editor,
|
||||||
|
&mut top,
|
||||||
|
&mut left,
|
||||||
|
&mut SyntaxCache::new(),
|
||||||
|
&mut DocumentViewport::default(),
|
||||||
|
&mut WheelPanels::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert!(left > 0);
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
for x in 0..2 {
|
||||||
|
assert_ne!(buffer[(x, 0)].bg, Color::Blue);
|
||||||
|
}
|
||||||
|
for x in 2..5 {
|
||||||
|
assert_eq!(buffer[(x, 0)].bg, Color::Blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visual_selection_highlights_graphemes_newlines_and_clipped_ranges_over_search() {
|
fn visual_selection_highlights_graphemes_newlines_and_clipped_ranges_over_search() {
|
||||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user