summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/edit.zig72
1 files changed, 56 insertions, 16 deletions
diff --git a/src/edit.zig b/src/edit.zig
index a194ef48..183d6325 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -487,6 +487,19 @@ fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText {
/// install a rewritten editable text (frees the old one)
pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
+ setEditTextEnd(p, t, new, null);
+}
+
+/// Where text `s` put in at `c` ends: `g.`'s place after typing it.
+fn insertedEnd(c: modal.Cursor, s: []const u8) modal.Cursor {
+ const nl = std.mem.lastIndexOfScalar(u8, s, '\n') orelse return .{ .row = c.row, .col = c.col + s.len };
+ return .{ .row = c.row + std.mem.count(u8, s, "\n"), .col = s.len - nl - 1 };
+}
+
+/// `setEditText` with where the edit ended in `new` (in the edit text's
+/// rows) when the caller knows it, as typing does: then nothing scans the
+/// whole text to find it. Null, it is found by comparing the texts.
+pub fn setEditTextEnd(p: *Pardes, t: *Text, new: []u8, known_end: ?modal.Cursor) void {
if (t.what == .tag) {
// A tag is edited as it is shown, but only what follows its
// prefix is its own: an edit that changed the prefix is refused.
@@ -512,10 +525,14 @@ pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
const pane = t.pane() orelse return retireEditText(p, new);
// what changed ended where the texts agree again from their ends
const old: []const u8, const row0: i32 = if (pane.file) |f| .{ f.content, 0 } else if (pane.ovl) |o| .{ o.text, o.row } else .{ "", 0 };
- const front = std.mem.indexOfDiff(u8, old, new) orelse @min(old.len, new.len);
- var back: usize = 0;
- while (back < @min(old.len, new.len) - front and old[old.len - 1 - back] == new[new.len - 1 - back]) back += 1;
- const end = modal.positionAt(new, new.len - back);
+ const end = known_end orelse found: {
+ // ponytail: O(file), for the edits that do not say where they ended
+ // (paste, a pipe, s): typing passes its end and never comes here.
+ const front = std.mem.indexOfDiff(u8, old, new) orelse @min(old.len, new.len);
+ var back: usize = 0;
+ while (back < @min(old.len, new.len) - front and old[old.len - 1 - back] == new[new.len - 1 - back]) back += 1;
+ break :found modal.positionAt(new, new.len - back);
+ };
t.last_edit = .{ .row = @as(i32, @intCast(end.row)) + row0, .col = @intCast(end.col) };
if (pane.file) |*f| return panes.File.setContent(p, f, new);
panes.Terminal.setEditText(p, pane, new);
@@ -813,7 +830,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const c = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) };
if (key.text.len > 0 and !key.ctrl and !key.alt) {
const new = modal.insertAt(p.gpa, text, c, key.text) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, insertedEnd(c, key.text));
for (key.text) |ch| {
if (ch == '\n') {
t.cur_row += 1;
@@ -837,7 +854,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const from = @min(wr.anchor, wr.head);
const fc = modal.positionAt(e2.text, from);
const new = modal.deleteSpan(p.gpa, e2.text, fc, c2) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, fc);
t.cur_row = @as(i32, @intCast(fc.row)) + e2.row0;
t.cur_col = @intCast(fc.col);
t.cur_pinned = true;
@@ -855,13 +872,13 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const to = @max(wr.anchor, wr.head);
if (to <= g) return;
const new = modal.deleteSpan(p.gpa, e2.text, c2, modal.positionAt(e2.text, to)) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c2);
t.cur_pinned = true;
return;
}
if (hit(key, config.kill_to_line_start)) {
const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = 0 }, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = 0 });
t.cur_col = 0;
t.cur_pinned = true;
t.show();
@@ -870,7 +887,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
if (hit(key, config.kill_to_line_end)) {
const llen = modal.lineSlice(text, c.row).len;
const new = modal.deleteSpan(p.gpa, text, c, .{ .row = c.row, .col = llen }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c);
t.cur_pinned = true;
return;
}
@@ -883,7 +900,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
// cursor go and the new line takes the indent
if (std.mem.indexOfNone(u8, before, " \t") == null) {
const new = modal.insertAt(p.gpa, text, .{ .row = c.row, .col = 0 }, "\n") catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row + 1, .col = 0 });
t.cur_row += 1;
t.cur_pinned = true;
t.show();
@@ -900,7 +917,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const cut = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = kept }, c) catch return;
defer p.gpa.free(cut);
const new = modal.insertAt(p.gpa, cut, .{ .row = c.row, .col = kept }, block) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row + 1, .col = indent });
t.cur_row += 1;
t.cur_col = @intCast(indent);
t.cur_pinned = true;
@@ -922,7 +939,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
while (drop > 0 and prev > 0 and line[prev - 1] == ' ') : (drop -= 1) prev -= 1;
}
const new = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = prev }, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = prev });
t.cur_col = @intCast(prev);
t.cur_pinned = true;
t.show();
@@ -936,7 +953,7 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
if (row == 0) return;
const prevlen = modal.lineSlice(e2.text, row - 1).len;
const new = modal.deleteSpan(p.gpa, e2.text, .{ .row = row - 1, .col = prevlen }, .{ .row = row, .col = 0 }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = row - 1, .col = prevlen });
t.cur_row -= 1;
t.cur_col = @intCast(prevlen);
t.cur_pinned = true;
@@ -948,13 +965,13 @@ fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
const line = modal.lineSlice(text, c.row);
if (c.col < line.len) {
const new = modal.deleteChar(p.gpa, text, c) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, c);
} else {
const e2 = editText(p, t, t.cur_row, t.cur_row + 1, t.cur_col) orelse return;
const row: usize = @intCast(@max(0, t.cur_row - e2.row0));
if (row + 1 >= modal.lineCount(e2.text)) return;
const new = modal.deleteSpan(p.gpa, e2.text, .{ .row = row, .col = c.col }, .{ .row = row + 1, .col = 0 }) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = row, .col = c.col });
}
t.cur_pinned = true;
},
@@ -993,7 +1010,7 @@ pub fn insertTab(p: *Pardes, t: *Text) void {
};
const pad = modal.INDENT_W - (c.col % modal.INDENT_W);
const new = modal.insertAt(p.gpa, eb.text, c, " "[0..pad]) catch return;
- setEditText(p, t, new);
+ setEditTextEnd(p, t, new, .{ .row = c.row, .col = c.col + pad });
t.cur_col += @intCast(pad);
t.cur_pinned = true;
t.show();
@@ -2365,3 +2382,26 @@ test "double-click selects as acme's does" {
const e = doubleClickRange(lines, 7);
try std.testing.expectEqualStrings("two\n", lines[e[0]..e[1]]);
}
+
+test "a typed edit's known end is where the texts' comparison would find it" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 20, .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("one\ntwo three\nfour\n");
+ const t = &pane.body;
+ const cases = [_]struct { at: modal.Cursor, s: []const u8 }{
+ .{ .at = .{ .row = 1, .col = 3 }, .s = "xy" },
+ .{ .at = .{ .row = 0, .col = 0 }, .s = "a\nbc" },
+ .{ .at = .{ .row = 2, .col = 4 }, .s = "\n" },
+ };
+ for (cases) |c| {
+ const base = try p.gpa.dupe(u8, pane.file.?.content);
+ defer p.gpa.free(base);
+ const scanned = try modal.insertAt(p.gpa, base, c.at, c.s);
+ setEditTextEnd(p, t, scanned, null);
+ const by_scan = t.last_edit.?;
+ const known = try modal.insertAt(p.gpa, base, c.at, c.s);
+ setEditTextEnd(p, t, known, insertedEnd(c.at, c.s));
+ try std.testing.expectEqual(by_scan.row, t.last_edit.?.row);
+ try std.testing.expectEqual(by_scan.col, t.last_edit.?.col);
+ }
+}