fix: limit linewise highlighting to copied text
This commit is contained in:
@@ -23,7 +23,7 @@ One crate; no workspace or terminal-coordinate types in the editing backend:
|
||||
## 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 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.
|
||||
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 the copied text on those rows and one cell for each copied newline, never unused viewport space or 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.
|
||||
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.
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ The status bar displays a pending count. Esc cancels it without moving, collapsi
|
||||
|
||||
## 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. `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.
|
||||
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 covers the copied text, including trailing whitespace, plus one blank cell for each copied newline; unused viewport space and the gutter remain unhighlighted. 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.
|
||||
|
||||
|
||||
+24
-9
@@ -670,9 +670,13 @@ fn draw(
|
||||
let first = (*rows.start()).max(*top);
|
||||
let last = (*rows.end()).min(top.saturating_add(height - 1));
|
||||
for row in first..=last {
|
||||
let end = display_column(&editor.lines()[row], usize::MAX)
|
||||
.saturating_add(1)
|
||||
.min(left.saturating_add(width));
|
||||
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 column in *left..end {
|
||||
frame.buffer_mut()[(viewport.x + (column - *left) as u16, y)]
|
||||
.set_style(selection_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5568,19 +5572,21 @@ end
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn linewise_selection_fills_blank_and_final_rows_without_coloring_gutter() {
|
||||
fn linewise_selection_highlights_copied_text_and_newlines_without_filling_rows() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor
|
||||
.apply(vec![crate::editor::Effect::Replace("long text\n\n".into())])
|
||||
.apply(vec![crate::editor::Effect::Replace(
|
||||
"\t界e\u{301} \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] {
|
||||
for width in [16, 3, 1] {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 6)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
terminal
|
||||
let frame = terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
frame,
|
||||
@@ -5593,8 +5599,8 @@ end
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
if width == 12 {
|
||||
let buffer = frame.buffer;
|
||||
if width == 16 {
|
||||
let status: String = (0..width).map(|x| buffer[(x, 4)].symbol()).collect();
|
||||
assert!(status.contains("VISUAL LINE"), "{status}");
|
||||
}
|
||||
@@ -5603,8 +5609,17 @@ end
|
||||
for x in 0..gutter {
|
||||
assert_ne!(buffer[(x, row)].bg, Color::Blue);
|
||||
}
|
||||
let selected_width = if row == 0 { 10 } else { 1 };
|
||||
for x in gutter..width {
|
||||
assert_eq!(buffer[(x, row)].bg, Color::Blue);
|
||||
if x - gutter < selected_width {
|
||||
assert_eq!(
|
||||
buffer[(x, row)].bg,
|
||||
Color::Blue,
|
||||
"width {width}, row {row}, x {x}"
|
||||
);
|
||||
} else {
|
||||
assert_ne!(buffer[(x, row)].bg, Color::Blue);
|
||||
}
|
||||
}
|
||||
}
|
||||
for x in 0..width {
|
||||
|
||||
Reference in New Issue
Block a user