summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/helix-keys.md2
-rw-r--r--src/edit.zig29
-rw-r--r--test/hxcases/cases.jsonl2
-rw-r--r--test/hxcases/goldens.jsonl2
4 files changed, 26 insertions, 9 deletions
diff --git a/docs/helix-keys.md b/docs/helix-keys.md
index 0b0da2ae..816010a6 100644
--- a/docs/helix-keys.md
+++ b/docs/helix-keys.md
@@ -182,7 +182,7 @@ found. Pure text math in `modal.zig`: `findChar`, `matchBracket`,
| Key | Behavior | Notes | Status |
| --- | --- | --- | --- |
| `r<ch>` | replace selection/char with `<ch>`, newlines kept | terminals: replaces the typed-run byte under the cursor if any, else no-op — one code path for both kinds, since `normalReplaceChar` goes through `Pardes.editText`/`setEditText`, which route to a file's content or to `term_pane`'s typed-run overlay | helix-verified (phase 5) |
-| `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection, head on its last char | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) |
+| `R` | replace selection (or cursor char) with the yank register; pasted text becomes the selection in the direction the replaced one had (head on its last char, or its first when backward) | uses the INTERNAL yank (`p.yank`), no clipboard round trip — `SPC R` is the system-clipboard twin; empty register = no-op; terminals: no-op | helix-verified (phase 5) |
| `~` | switch case of selection/char, selection kept | terminals: run byte only | helix-verified (phase 5) |
| `` ` `` | selection to lowercase | terminals: run byte only | helix-verified (phase 5) |
| ``Alt-` `` | selection to uppercase | terminals: run byte only | helix-verified (phase 5) |
diff --git a/src/edit.zig b/src/edit.zig
index 56d537bf..fb8681d8 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -1125,27 +1125,40 @@ pub fn normalReplaceYank(p: *Pardes, t: *Text) void {
replaceWithText(p, t, p.yank orelse return);
}
-/// The pasted text becomes the selection, head on its last char (which for
-/// a trailing-newline `y` is the '\n' cell of the last full line).
+/// The pasted text becomes the selection, in the direction the replaced one
+/// had: a forward head lands on its last char (which for a trailing-newline
+/// `y` is the '\n' cell of the last full line), a backward one on its first.
fn replaceWithText(p: *Pardes, t: *Text, y: []const u8) void {
if (y.len == 0) return;
t.select = false;
const eb = editTextEol(p, t, selRows(t)) orelse return;
const r = selRange(t, eb.text, eb.row0);
+ const back = t.vsel.active and (t.cur_row < t.vsel.row or (t.cur_row == t.vsel.row and t.cur_col < t.vsel.col));
pushUndo(p, t);
const new = modal.replaceRange(p.gpa, eb.text, r.a, r.b, y) catch return;
setEditText(p, t, new);
t.msel.active = false;
- t.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false };
const end = modal.advanceBy(r.a, y);
+ var last_row: i32 = undefined;
+ var last_col: i32 = undefined;
if (end.col > 0) {
- t.cur_row = @as(i32, @intCast(end.row)) + eb.row0;
- t.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col));
+ last_row = @as(i32, @intCast(end.row)) + eb.row0;
+ last_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col));
} else {
- // the yank ended in '\n': the cursor lands ON that newline
- t.cur_row = @as(i32, @intCast(end.row -| 1)) + eb.row0;
- t.cur_col = @intCast(modal.lineSlice(new, end.row -| 1).len);
+ // the yank ended in '\n': its last cell is that newline
+ last_row = @as(i32, @intCast(end.row -| 1)) + eb.row0;
+ last_col = @intCast(modal.lineSlice(new, end.row -| 1).len);
}
+ const first_row = @as(i32, @intCast(r.a.row)) + eb.row0;
+ const first_col: i32 = @intCast(r.a.col);
+ t.vsel = .{
+ .active = modal.nextGrapheme(y, 0) < y.len,
+ .row = if (back) last_row else first_row,
+ .col = if (back) last_col else first_col,
+ .explicit = false,
+ };
+ t.cur_row = if (back) first_row else last_row;
+ t.cur_col = if (back) first_col else last_col;
t.cur_pinned = true;
t.sticky_col = -1;
t.show();
diff --git a/test/hxcases/cases.jsonl b/test/hxcases/cases.jsonl
index d7f76912..6e1b8188 100644
--- a/test/hxcases/cases.jsonl
+++ b/test/hxcases/cases.jsonl
@@ -287,6 +287,8 @@
{"name": "r-cancel-esc", "pane": "file", "text": "abc\n", "keys": "r<esc>l"}
{"name": "R-inline", "pane": "file", "text": "alpha beta\n", "keys": "wyggR"}
{"name": "R-linewise", "pane": "file", "text": "one\ntwo\n", "keys": "xyjR"}
+{"name": "R-backward", "pane": "file", "text": "alpha beta\n", "keys": "wyebR"}
+{"name": "R-backward-linewise", "pane": "file", "text": "one\ntwo three\n", "keys": "xyjxbR"}
{"name": "tilde-sel-keeps", "pane": "file", "text": "abc\n", "keys": "vll~"}
{"name": "tilde-bare", "pane": "file", "text": "aBc\n", "keys": "~"}
{"name": "backtick-lower", "pane": "file", "text": "ABC\n", "keys": "vll`"}
diff --git a/test/hxcases/goldens.jsonl b/test/hxcases/goldens.jsonl
index cab7393b..8ac110e6 100644
--- a/test/hxcases/goldens.jsonl
+++ b/test/hxcases/goldens.jsonl
@@ -287,6 +287,8 @@
{"name":"r-cancel-esc","text":"abc\n","mode":"normal","cursor":{"row":0,"col":1},"anchor":{"row":0,"col":1}}
{"name":"R-inline","text":"alpha lpha beta\n","mode":"normal","cursor":{"row":0,"col":5},"anchor":{"row":0,"col":0}}
{"name":"R-linewise","text":"one\ntwoone\n","mode":"normal","cursor":{"row":1,"col":6},"anchor":{"row":1,"col":3}}
+{"name":"R-backward","text":"alpha alpha \n","mode":"normal","cursor":{"row":0,"col":6},"anchor":{"row":0,"col":11}}
+{"name":"R-backward-linewise","text":"one\ntwo one\n\n","mode":"normal","cursor":{"row":1,"col":4},"anchor":{"row":1,"col":7}}
{"name":"tilde-sel-keeps","text":"ABC\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}}
{"name":"tilde-bare","text":"ABc\n","mode":"normal","cursor":{"row":0,"col":0},"anchor":{"row":0,"col":0}}
{"name":"backtick-lower","text":"abc\n","mode":"normal","cursor":{"row":0,"col":2},"anchor":{"row":0,"col":0}}