diff options
Diffstat (limited to 'src/edit.zig')
| -rw-r--r-- | src/edit.zig | 72 |
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); + } +} |
