fix: route normal-mode cuts through clipboard pipeline
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
- [x] syntax highlighting for Honey (honeylang)
|
- [x] syntax highlighting for Honey (honeylang)
|
||||||
- honeylang treesitter project located at `/Users/valdemar/Developer/Personal/Languages/tree-sitter-honey`.
|
- honeylang treesitter project located at `/Users/valdemar/Developer/Personal/Languages/tree-sitter-honey`.
|
||||||
- [ ] bug: `x` cut action does not copy the hovered character if not in selection mode, though should always copy.
|
- [x] bug: `x` cut action does not copy the hovered character if not in selection mode, though should always copy.
|
||||||
- [ ] diff view: GitHub code review integration after the local review UX is usable.
|
- [ ] diff view: GitHub code review integration after the local review UX is usable.
|
||||||
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
|
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
|
||||||
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
|
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ Rust tests live under [`tests/`](../tests/). Private unit tests are in [`tests/u
|
|||||||
- Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state. Rendered checks cover longer/wider documentation visibility, Markdown heading and fenced-code colors, scrolled color coordinates, and replacement content without stale highlighting.
|
- Hover regression checks cover content forms, sanitization, cancelled/superseded replies, invalid results, scrolling/dismissal, Unicode clipping, and preserved editing state. Rendered checks cover longer/wider documentation visibility, Markdown heading and fenced-code colors, scrolled color coordinates, and replacement content without stale highlighting.
|
||||||
- Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, ASCII tab and Unicode column bounds, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets. Retention checks cover typing/undo/redo in editable diffs, column/newline/Unicode remapping, overlapping-token removal, and session polling with delayed/stale server replies. Live PTY smoke holds semantic replies while typing and undoing/redoing, checking that untouched colored text is not repainted in the fallback color.
|
- Semantic-token checks cover full/range requests, legend/modifier remapping, UTF-16 boundaries and grapheme rounding, ASCII tab and Unicode column bounds, refresh/change coalescing, stale rename/close replies, null/empty/error fallback, primary/sibling retirement, and rendered cell precedence with syntax, diagnostics, search, selections, and carets. Retention checks cover typing/undo/redo in editable diffs, column/newline/Unicode remapping, overlapping-token removal, and session polling with delayed/stale server replies. Live PTY smoke holds semantic replies while typing and undoing/redoing, checking that untouched colored text is not repainted in the fallback color.
|
||||||
|
|
||||||
Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard.
|
Clipboard checks use isolated providers rather than reading or overwriting the desktop clipboard. Normal `x`/Delete regressions cover Unicode graphemes, remapped actions, multiple carets with empty rows, characterwise paste, undo/redo, dot-repeat publication and failure atomicity, and unchanged Insert Delete behavior. Live PTY smoke cuts a combining grapheme, repeats over a ZWJ emoji, pastes, undoes/redoes, and checks exact saved bytes using Separate mode.
|
||||||
|
|
||||||
Diff checks cover staged category/filter navigation, stale request cancellation, row alignment, read-only buffer isolation, and rendered panes. Repository integration fixtures exercise Git and Git-backed jj, advertised but unfetched remote branches/bookmarks/tags, imported historical commit/change IDs, conflicts, unusual paths, binary/mode/link/submodule changes, and unchanged active metadata. Remote fixtures are local bare copies; they never push. Git fixture identities and commit dates are fixed.
|
Diff checks cover staged category/filter navigation, stale request cancellation, row alignment, read-only buffer isolation, and rendered panes. Repository integration fixtures exercise Git and Git-backed jj, advertised but unfetched remote branches/bookmarks/tags, imported historical commit/change IDs, conflicts, unusual paths, binary/mode/link/submodule changes, and unchanged active metadata. Remote fixtures are local bare copies; they never push. Git fixture identities and commit dates are fixed.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -33,7 +33,7 @@ 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 |
|
| Normal | `x`/Delete cut the hovered grapheme at every caret; `u` undo; `U` (Shift+u) redo |
|
||||||
| Normal, Visual | `:` enters command mode; Visual selections remain available to selection-aware commands |
|
| 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 |
|
||||||
@@ -144,6 +144,8 @@ Clipboard policy is session-wide and defaults to the system clipboard. Copy/cut
|
|||||||
|
|
||||||
## Clipboard policy
|
## Clipboard policy
|
||||||
|
|
||||||
|
Normal `x`/Delete (`config.core.delete`) copies each hovered grapheme to the configured clipboard before removing it. Combining sequences and emoji stay intact; empty lines are unchanged, and an entirely empty cut preserves the clipboard. Dot-repeat cuts and copies the current graphemes. Insert-mode Delete and its replay do not copy.
|
||||||
|
|
||||||
`config.clipboard_mode = "system"` is the default: ordinary Visual `y`/`d`/`x`/`c` and Normal/Visual `p`/`P` use the OS clipboard. External clipboard changes are read on each paste. Set `"separate"` to keep ordinary operations on vamp's internal fragment register instead. Both registers remain independent across buffers; changing the policy does not overwrite either.
|
`config.clipboard_mode = "system"` is the default: ordinary Visual `y`/`d`/`x`/`c` and Normal/Visual `p`/`P` use the OS clipboard. External clipboard changes are read on each paste. Set `"separate"` to keep ordinary operations on vamp's internal fragment register instead. Both registers remain independent across buffers; changing the policy does not overwrite either.
|
||||||
|
|
||||||
Explicit `config.core.copy_system` (Visual) and `config.core.paste_system_before` / `paste_system_after` (Normal/Visual) always use the system clipboard in either policy. They appear in the command palette and can be bound without replacing ordinary internal operations:
|
Explicit `config.core.copy_system` (Visual) and `config.core.paste_system_before` / `paste_system_after` (Normal/Visual) always use the system clipboard in either policy. They appear in the command palette and can be bound without replacing ordinary internal operations:
|
||||||
|
|||||||
+6
-16
@@ -368,7 +368,6 @@ enum EditStart {
|
|||||||
target: char,
|
target: char,
|
||||||
selector: SelectionIntent,
|
selector: SelectionIntent,
|
||||||
},
|
},
|
||||||
Delete,
|
|
||||||
Join,
|
Join,
|
||||||
Paste {
|
Paste {
|
||||||
after: bool,
|
after: bool,
|
||||||
@@ -8657,21 +8656,16 @@ impl Editor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
Action::Delete => {
|
Action::Delete => {
|
||||||
|
if self.active.view.mode == Mode::Normal {
|
||||||
|
self.edit_normal(action, 1);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
if self.active.view.mode == Mode::Insert && self.edit_snippet("", Some(action)) {
|
if self.active.view.mode == Mode::Insert && self.edit_snippet("", Some(action)) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let before = (self.active.view.mode != Mode::Insert).then(|| self.snapshot());
|
|
||||||
let mask = self.deletion_mask(None);
|
let mask = self.deletion_mask(None);
|
||||||
self.delete_forward(&mask);
|
self.delete_forward(&mask);
|
||||||
if let Some(before) = before {
|
if mask.iter().any(|active| *active) {
|
||||||
self.record_native(
|
|
||||||
before,
|
|
||||||
EditRecipe {
|
|
||||||
start: EditStart::Delete,
|
|
||||||
steps: Vec::new(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if mask.iter().any(|active| *active) {
|
|
||||||
self.push_insert_step(InsertStep::Delete(mask));
|
self.push_insert_step(InsertStep::Delete(mask));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10689,7 +10683,7 @@ impl Editor {
|
|||||||
Action::DeleteWordBackward | Action::ChangeWordBackward => {
|
Action::DeleteWordBackward | Action::ChangeWordBackward => {
|
||||||
self.word_destination(cursor, Action::DeleteWordBackward, count)
|
self.word_destination(cursor, Action::DeleteWordBackward, count)
|
||||||
}
|
}
|
||||||
Action::SubstituteCharacter => Cursor {
|
Action::Delete | Action::SubstituteCharacter => Cursor {
|
||||||
row: cursor.row,
|
row: cursor.row,
|
||||||
column: cursor
|
column: cursor
|
||||||
.column
|
.column
|
||||||
@@ -11537,10 +11531,6 @@ impl Editor {
|
|||||||
self.normalize_targets();
|
self.normalize_targets();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditStart::Delete => {
|
|
||||||
self.active.view.mode = Mode::Normal;
|
|
||||||
self.delete_forward(&[true]);
|
|
||||||
}
|
|
||||||
EditStart::Join => {
|
EditStart::Join => {
|
||||||
self.active.view.mode = Mode::Normal;
|
self.active.view.mode = Mode::Normal;
|
||||||
self.join_lines();
|
self.join_lines();
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ Files: Space o floating tree INSERT; Space e docked tree NORMAL (right by defaul
|
|||||||
i filters; Esc retains filter and returns NORMAL, then editor; Ctrl+u clears in either mode
|
i filters; Esc retains filter and returns NORMAL, then editor; Ctrl+u clears in either mode
|
||||||
Ctrl+t view, Ctrl+w close, Ctrl+r refresh
|
Ctrl+t view, Ctrl+w close, Ctrl+r refresh
|
||||||
Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||||
h/j/k/l move, 0/$ line ends, x delete, J join lines
|
h/j/k/l move, 0/$ line ends, x/Delete cut hovered graphemes, J join lines
|
||||||
w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs
|
w/b/e word next/previous/end, W/B/E whitespace-delimited WORDs
|
||||||
Normal/Visual: f<char>/F<char> seek next/previous on current line; 2fx seeks second x
|
Normal/Visual: f<char>/F<char> seek next/previous on current line; 2fx seeks second x
|
||||||
gf follows Markdown file links/reference links and heading fragments (no LSP)
|
gf follows Markdown file links/reference links and heading fragments (no LSP)
|
||||||
@@ -43,7 +43,7 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
|||||||
Linewise p/P paste below/above the current row; characterwise p/P paste after/before the grapheme
|
Linewise p/P paste below/above the current row; characterwise p/P paste after/before the grapheme
|
||||||
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 characterwise select, V linewise 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/x/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
|
: opens command input with the selection preserved; Esc returns to that selection
|
||||||
|
|||||||
@@ -42,6 +42,72 @@ fn keys(editor: &mut Editor, text: &str) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_x_cuts_the_hovered_grapheme_for_paste() {
|
||||||
|
for grapheme in ["b", "e\u{301}", "👩🚀"] {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut editor = fixture.editor(&format!("a{grapheme}z\n"));
|
||||||
|
keys(&mut editor, "lx");
|
||||||
|
assert_eq!(editor.context().text, "az\n");
|
||||||
|
assert_eq!(editor.mode(), Mode::Normal);
|
||||||
|
keys(&mut editor, "P");
|
||||||
|
assert_eq!(editor.context().text, format!("a{grapheme}z\n"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_delete_and_remapped_action_copy_characterwise() {
|
||||||
|
for input in [Input::Delete, Input::Char('q')] {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut editor = fixture.editor("abz\n");
|
||||||
|
editor
|
||||||
|
.bind("normal", "q", Some("core.delete".into()))
|
||||||
|
.unwrap();
|
||||||
|
keys(&mut editor, "yyl");
|
||||||
|
editor.handle(input);
|
||||||
|
assert_eq!(editor.context().text, "az\n");
|
||||||
|
keys(&mut editor, "uUP");
|
||||||
|
assert_eq!(editor.context().text, "abz\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_x_repeat_copies_current_graphemes_and_preserves_empty_line_clipboard() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut editor = fixture.editor("abc\n\nxyz\n");
|
||||||
|
keys(&mut editor, "x.");
|
||||||
|
assert_eq!(editor.context().text, "c\n\nxyz\n");
|
||||||
|
keys(&mut editor, "jxjP");
|
||||||
|
assert_eq!(editor.context().text, "c\n\nbxyz\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_x_cuts_at_every_caret_without_crossing_empty_lines() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let source = "a👩🚀z\n\nbe\u{301}y\n";
|
||||||
|
let mut editor = fixture.editor(source);
|
||||||
|
keys(&mut editor, "l");
|
||||||
|
editor.handle(Input::Ctrl('e'));
|
||||||
|
editor.handle(Input::Ctrl('e'));
|
||||||
|
keys(&mut editor, "x");
|
||||||
|
assert_eq!(editor.context().text, "az\n\nby\n");
|
||||||
|
keys(&mut editor, "P");
|
||||||
|
assert_eq!(editor.context().text, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn insert_delete_and_its_repeat_do_not_overwrite_clipboard() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut editor = fixture.editor("abc\nxyz\n");
|
||||||
|
keys(&mut editor, "vyi");
|
||||||
|
editor.handle(Input::Delete);
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
keys(&mut editor, "j.");
|
||||||
|
assert_eq!(editor.context().text, "bc\nyz\n");
|
||||||
|
keys(&mut editor, "P");
|
||||||
|
assert_eq!(editor.context().text, "bc\nayz\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn visual_r_replaces_selected_graphemes_as_one_undo_edit() {
|
fn visual_r_replaces_selected_graphemes_as_one_undo_edit() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
|
|||||||
+24
-2
@@ -6992,7 +6992,7 @@ mod system_clipboard {
|
|||||||
let revision = editor.text_revision();
|
let revision = editor.text_revision();
|
||||||
fixture.fail("fail-write", true);
|
fixture.fail("fail-write", true);
|
||||||
for keys in [
|
for keys in [
|
||||||
"yy", "2y3y", "2dd", "cc", "C", "D", "Y", "s", "d2w", "d2b", "c2w", "c2b",
|
"x", "yy", "2y3y", "2dd", "cc", "C", "D", "Y", "s", "d2w", "d2b", "c2w", "c2b",
|
||||||
] {
|
] {
|
||||||
type_text(&mut editor, keys);
|
type_text(&mut editor, keys);
|
||||||
assert_eq!(editor.context().text, "Xone\ntwo\nthree");
|
assert_eq!(editor.context().text, "Xone\ntwo\nthree");
|
||||||
@@ -7007,6 +7007,29 @@ mod system_clipboard {
|
|||||||
assert_eq!(editor.context().text, "XXone\ntXwo\nthree");
|
assert_eq!(editor.context().text, "XXone\ntXwo\nthree");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_x_publishes_graphemes_and_repeat_failure_is_atomic() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
let mut editor = fixture.editor();
|
||||||
|
editor
|
||||||
|
.apply(vec![Effect::Replace("e\u{301}👩🚀z".into())])
|
||||||
|
.unwrap();
|
||||||
|
type_text(&mut editor, "x");
|
||||||
|
assert_eq!(editor.context().text, "👩🚀z");
|
||||||
|
assert_eq!(fixture.bytes("state"), "e\u{301}".as_bytes());
|
||||||
|
fixture.fail("fail-write", true);
|
||||||
|
let before = state(&editor);
|
||||||
|
type_text(&mut editor, ".");
|
||||||
|
assert_eq!(state(&editor), before);
|
||||||
|
assert_eq!(fixture.bytes("state"), "e\u{301}".as_bytes());
|
||||||
|
fixture.fail("fail-write", false);
|
||||||
|
type_text(&mut editor, ".");
|
||||||
|
assert_eq!(editor.context().text, "z");
|
||||||
|
assert_eq!(fixture.bytes("state"), "👩🚀".as_bytes());
|
||||||
|
type_text(&mut editor, "uUP");
|
||||||
|
assert_eq!(editor.context().text, "👩🚀z");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn copy_line_publication_failure_keeps_owned_shape_register_and_full_state() {
|
fn copy_line_publication_failure_keeps_owned_shape_register_and_full_state() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
@@ -7707,7 +7730,6 @@ mod system_clipboard {
|
|||||||
editor.set_clipboard_mode("system").unwrap();
|
editor.set_clipboard_mode("system").unwrap();
|
||||||
editor.apply(vec![Effect::Replace("plain".into())]).unwrap();
|
editor.apply(vec![Effect::Replace("plain".into())]).unwrap();
|
||||||
set_carets(&mut editor, &[(0, 0)], 0);
|
set_carets(&mut editor, &[(0, 0)], 0);
|
||||||
type_text(&mut editor, "x.");
|
|
||||||
type_text(&mut editor, "iX");
|
type_text(&mut editor, "iX");
|
||||||
editor.handle(Input::Backspace);
|
editor.handle(Input::Backspace);
|
||||||
editor.handle(Input::Delete);
|
editor.handle(Input::Delete);
|
||||||
|
|||||||
Reference in New Issue
Block a user