From 95a3a1bb822e583988bfe0f312d0ef219b27a454 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 27 Sep 2026 23:06:11 -0300 Subject: Give a pane's body its own Text holding the cursor, selections, mode and undo acme keeps what edits a text in its Text (dat.h:171-190) and the window holds a body and a tag of that type. The cursor, the selections, the modal state and the edit-buffer undo move off Pane into Text.zig, Pane holds them as its body, and the edit and normal-mode operations take the Text they edit. Nothing changes in behaviour; this is the step that lets the tag become a second Text. Co-Authored-By: Claude Opus 5.5 --- src/edit.zig | 1020 +++++++++++++++++++++++++++++----------------------------- 1 file changed, 518 insertions(+), 502 deletions(-) (limited to 'src/edit.zig') diff --git a/src/edit.zig b/src/edit.zig index 04416b67..f1b381d6 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -14,6 +14,7 @@ const pdf_enabled = panes.Pdf.enabled; const lsp = @import("lsp/lsp.zig"); const platform = pardes.platform; const Pane = panes.Pane; +const Text = panes.Text; const TagHit = @import("tag_layer.zig").Hit; const BOX_H = pardes.BOX_H; const TAG_TEXT_INSET = pardes.TAG_TEXT_INSET; @@ -91,11 +92,11 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split); return; } - if (pane.isTerminal() and pane.mode == .tty) return typeToTty(p, id, pane, bytes); + if (pane.isTerminal() and pane.body.mode == .tty) return typeToTty(p, id, pane, bytes); switch (if (req) |r| r.mode else .after) { - .after => pasteText(p, pane, bytes, false, 1), - .before => pasteText(p, pane, bytes, true, 1), - .replace => replaceWithText(p, pane, bytes), + .after => pasteText(p, &pane.body, bytes, false, 1), + .before => pasteText(p, &pane.body, bytes, true, 1), + .replace => replaceWithText(p, &pane.body, bytes), } } @@ -104,13 +105,13 @@ pub fn clipYank(p: *Pardes, pane: *Pane, main_only: bool) void { if (pv.selection_text.len > 0) setClipboard(p, pv.selection_text); return; }; - if (pane.nsel > 0 and !main_only) { - normal.replaySels(p, pane, .{ .normal = .{ .edit = .{ .kind = .yank, .count = 1 } } }); + if (pane.body.nsel > 0 and !main_only) { + normal.replaySels(p, &pane.body, .{ .normal = .{ .edit = .{ .kind = .yank, .count = 1 } } }); } else { - const others = pane.nsel; - pane.nsel = 0; - normalYank(p, pane); - pane.nsel = others; + const others = pane.body.nsel; + pane.body.nsel = 0; + normalYank(p, &pane.body); + pane.body.nsel = others; } p.emit(.{ .set_clipboard = {} }); } @@ -135,7 +136,7 @@ pub fn capturePointerSelection(p: *Pardes, pane: *Pane, slot: usize) !void { if (pane.pointerSelection(gesture) != null) return; pane.clearPointerSelection(slot); // Refresh the source map before translating the rendered rectangle. - const raw = pane.isTerminal() and pane.mode == .tty; + const raw = pane.isTerminal() and pane.body.mode == .tty; const body = try body_layer.bodyText(p, p.scratch.allocator(), pane); const terminal_lines = if (pane.file == null and !raw) try panes.Terminal.cursorLines(p, pane) else &.{}; const first = @max(0, @min(gesture.r0, gesture.r1) - BOX_H); @@ -253,28 +254,28 @@ pub fn spanHas(r: i32, c: i32, ar: i32, ac: i32, br: i32, bc: i32) bool { pub fn currentSelText(p: *Pardes, pane: *Pane) ?[]const u8 { if (comptime pdf_enabled) if (p.native_images) if (pane.pdf) |pv| if (pv.selection != null and pv.selection_text.len > 0) return pv.selection_text; - if (pane.vsel.active) return vselText(p, pane); - if (!pane.msel.active) return null; - return yankRows(p, pane, @min(pane.msel.r0, pane.msel.r1), @max(pane.msel.r0, pane.msel.r1)); + if (pane.body.vsel.active) return vselText(p, &pane.body); + if (!pane.body.msel.active) return null; + return yankRows(p, &pane.body, @min(pane.body.msel.r0, pane.body.msel.r1), @max(pane.body.msel.r0, pane.body.msel.r1)); } -pub fn exitInsert(p: *Pardes, pane: *Pane) void { - pane.mode = .normal; - pane.msel.active = false; - pane.normal.clear(); - if (pane.append_at) |aa| { - pane.append_at = null; - const text = flatSurface(p, pane) catch return; - const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }); +pub fn exitInsert(p: *Pardes, t: *Text) void { + t.mode = .normal; + t.msel.active = false; + t.normal.clear(); + if (t.append_at) |aa| { + t.append_at = null; + const text = flatSurface(p, t) catch return; + const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) }); const back = @max(a_off, modal.prevGrapheme(text, gap)); const bc = modal.positionAt(text, back); - pane.cur_row = @intCast(bc.row); - pane.cur_col = @intCast(bc.col); - pane.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; - pane.cur_pinned = true; - pane.ensureCursorVisible(); - for (pane.sels[0..pane.nsel]) |*s| { + t.cur_row = @intCast(bc.row); + t.cur_col = @intCast(bc.col); + t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false }; + t.cur_pinned = true; + t.pane().ensureCursorVisible(); + for (t.sels[0..t.nsel]) |*s| { const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) }); const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap); const bp = modal.positionAt(text, b2); @@ -286,7 +287,8 @@ pub fn exitInsert(p: *Pardes, pane: *Pane) void { } } -pub fn paneCursorLines(p: *Pardes, pane: *Pane) ![]const []const u8 { +pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { + const pane = t.pane(); const arena = p.scratch.allocator(); if (pane.file) |*f| return panes.File.cursorLines(arena, pane, f); if (pane.hasPdf()) { @@ -296,10 +298,11 @@ pub fn paneCursorLines(p: *Pardes, pane: *Pane) ![]const []const u8 { return panes.Terminal.cursorLines(p, pane); } -pub fn paneByteAtDisplay(p: *Pardes, pane: *Pane, row: i32, from_raw: i32, display_col: i32) i32 { +pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_col: i32) i32 { + const pane = t.pane(); if (pane.file != null) return panes.File.byteAtRowDisplay(pane, row, from_raw, display_col); - const lines = paneCursorLines(p, pane) catch return @max(0, from_raw + display_col); + const lines = paneCursorLines(p, t) catch return @max(0, from_raw + display_col); const local = row; if (local < 0 or @as(usize, @intCast(local)) >= lines.len) return @max(0, from_raw + display_col); @@ -312,14 +315,16 @@ pub fn paneByteAtDisplay(p: *Pardes, pane: *Pane, row: i32, from_raw: i32, displ /// Freeze the live terminal cursor into the modal coordinate space. The /// emulator reports screen cells; editing state stores UTF-8 byte offsets. -pub fn pinPaneCursor(p: *Pardes, pane: *Pane) void { - if (pane.cur_pinned) return; +pub fn pinPaneCursor(p: *Pardes, t: *Text) void { + const pane = t.pane(); + if (t.cur_pinned) return; pane.pinCursor(); if (pane.file == null and !pane.hasPdf()) - pane.cur_col = paneByteAtDisplay(p, pane, pane.cur_row, 0, pane.cur_col); + t.cur_col = paneByteAtDisplay(p, t, t.cur_row, 0, t.cur_col); } -pub fn flatSurface(p: *Pardes, pane: *Pane) ![]const u8 { +pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { + const pane = t.pane(); if (pane.file) |f| return f.content; if (pane.hasPdf()) { if (comptime pdf_enabled) return pane.pdf.?.ensureText(p.pdf_gpa); @@ -329,28 +334,31 @@ pub fn flatSurface(p: *Pardes, pane: *Pane) ![]const u8 { return panes.Terminal.flatSurface(p, pane, lines); } -pub fn paneWrapWidth(p: *const Pardes, pane: *const Pane) usize { +pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize { + const pane = t.pane(); if (pane.file == null) return 0; return panes.File.wrapWidth(pane, p.settings.wrap); } /// Route shared edit operations to a file's content or a terminal overlay. -fn editText(p: *Pardes, pane: *Pane, lo: i32, hi: i32, col: i32) ?panes.EditText { +fn editText(p: *Pardes, t: *Text, lo: i32, hi: i32, col: i32) ?panes.EditText { + const pane = t.pane(); if (pane.file) |f| return .{ .text = f.content, .row0 = 0 }; if (pane.image != null or pane.hasPdf()) return null; return panes.Terminal.editText(p, pane, lo, hi, col); } -fn editTextEol(p: *Pardes, pane: *Pane, b: Bounds) ?panes.EditText { - const eb = editText(p, pane, b.lo_row, b.hi_row, -1) orelse return null; +fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText { + const eb = editText(p, t, b.lo_row, b.hi_row, -1) orelse return null; const r: usize = @intCast(@max(0, b.hi_row - eb.row0)); if (r + 1 < modal.lineCount(eb.text)) return eb; if (b.hi_col < @as(i32, @intCast(modal.lineSlice(eb.text, r).len))) return eb; - return editText(p, pane, b.lo_row, b.hi_row + 1, -1); + return editText(p, t, b.lo_row, b.hi_row + 1, -1); } /// install a rewritten editable text (frees the old one) -fn setEditText(p: *Pardes, pane: *Pane, new: []u8) void { +fn setEditText(p: *Pardes, t: *Text, new: []u8) void { + const pane = t.pane(); if (pane.file) |*f| return panes.File.setContent(p, f, new); panes.Terminal.setEditText(p, pane, new); } @@ -359,73 +367,75 @@ pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_a /// Files insert only inside existing rows and at grapheme boundaries. /// Terminal overlays can materialize virtual rows/columns, so keep those. -pub fn clampFileCursor(pane: *Pane) bool { +pub fn clampFileCursor(t: *Text) bool { + const pane = t.pane(); const file = if (pane.file) |*file| file else return false; - const old_row = pane.cur_row; - const old_col = pane.cur_col; + const old_row = t.cur_row; + const old_col = t.cur_col; const last = panes.File.nlines(pane.gpa, file) -| 1; - pane.cur_row = @intCast(@min(last, @as(usize, @intCast(@max(0, pane.cur_row))))); - const line = panes.File.sourceLine(pane, pane.cur_row); - pane.cur_col = @intCast(modal.graphemeStart(line, @min(line.len, @as(usize, @intCast(@max(0, pane.cur_col)))))); - return old_row != pane.cur_row or old_col != pane.cur_col; + t.cur_row = @intCast(@min(last, @as(usize, @intCast(@max(0, t.cur_row))))); + const line = panes.File.sourceLine(pane, t.cur_row); + t.cur_col = @intCast(modal.graphemeStart(line, @min(line.len, @as(usize, @intCast(@max(0, t.cur_col)))))); + return old_row != t.cur_row or old_col != t.cur_col; } -pub fn enterInsert(p: *Pardes, pane: *Pane, where: InsertAt, cnt: usize) void { +pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { + const pane = t.pane(); if (pane.hasPdf()) return; - pinPaneCursor(p, pane); - _ = clampFileCursor(pane); + pinPaneCursor(p, t); + _ = clampFileCursor(t); // snapshot once per insert session (WITH the pre-insert selection) so // `u` undoes the whole session and restores what was selected - pushUndo(p, pane); - pane.select = false; - pane.append_at = null; - pane.sticky_col = -1; - const text = flatSurface(p, pane) catch { - pane.mode = .insert; - pane.msel.active = false; - pane.vsel.active = false; - pane.normal.clear(); + pushUndo(p, t); + t.select = false; + t.append_at = null; + t.sticky_col = -1; + const text = flatSurface(p, t) catch { + t.mode = .insert; + t.msel.active = false; + t.vsel.active = false; + t.normal.clear(); return; }; - const cur = pane.toModalCursor(); - const line = panes.File.textLine(pane, text, cur.row); - const b: ?Bounds = if (pane.vsel.active) vselBounds(pane) else null; + const cur = t.toModalCursor(); + const line = panes.File.textLine(t, text, cur.row); + const b: ?Bounds = if (t.vsel.active) vselBounds(t) else null; switch (where) { .at => { if (b) |bb| { - pane.cur_row = bb.lo_row; - pane.cur_col = bb.lo_col; - pane.vsel = .{ .active = true, .row = bb.hi_row, .col = bb.hi_col, .explicit = false }; - pane.cur_pinned = true; + t.cur_row = bb.lo_row; + t.cur_col = bb.lo_col; + t.vsel = .{ .active = true, .row = bb.hi_row, .col = bb.hi_col, .explicit = false }; + t.cur_pinned = true; } }, .append => { - const lo_row = if (b) |bb| bb.lo_row else pane.cur_row; - const lo_col = if (b) |bb| bb.lo_col else pane.cur_col; - const hi_row = if (b) |bb| bb.hi_row else pane.cur_row; - const hi_col = if (b) |bb| bb.hi_col else pane.cur_col; - pane.append_at = .{ .row = lo_row, .col = lo_col }; + const lo_row = if (b) |bb| bb.lo_row else t.cur_row; + const lo_col = if (b) |bb| bb.lo_col else t.cur_col; + const hi_row = if (b) |bb| bb.hi_row else t.cur_row; + const hi_col = if (b) |bb| bb.hi_col else t.cur_col; + t.append_at = .{ .row = lo_row, .col = lo_col }; const gap = modal.nextGrapheme(text, modal.offsetAt(text, .{ .row = @intCast(@max(0, hi_row)), .col = @intCast(@max(0, hi_col)) })); const gc = modal.positionAt(text, gap); - pane.cur_row = @intCast(gc.row); - pane.cur_col = @intCast(gc.col); - pane.vsel = .{ .active = b != null, .row = lo_row, .col = lo_col, .explicit = false }; - pane.cur_pinned = true; + t.cur_row = @intCast(gc.row); + t.cur_col = @intCast(gc.col); + t.vsel = .{ .active = b != null, .row = lo_row, .col = lo_col, .explicit = false }; + t.cur_pinned = true; }, .line_start => { - pane.fromModalCursor(.{ .row = cur.row, .col = modal.firstNonWs(line) }); - pane.vsel.active = false; + t.fromModalCursor(.{ .row = cur.row, .col = modal.firstNonWs(line) }); + t.vsel.active = false; }, .line_end => { - pane.cur_col = @intCast(line.len); - pane.cur_pinned = true; - pane.vsel.active = false; + t.cur_col = @intCast(line.len); + t.cur_pinned = true; + t.vsel.active = false; }, .open_below, .open_above => { - pane.vsel.active = false; + t.vsel.active = false; const below = where == .open_below; - const abs: i32 = if (b) |bb| (if (below) bb.hi_row else bb.lo_row) else pane.cur_row; - const eb = editText(p, pane, abs, abs, -1) orelse return; + const abs: i32 = if (b) |bb| (if (below) bb.hi_row else bb.lo_row) else t.cur_row; + const eb = editText(p, t, abs, abs, -1) orelse return; const row: usize = @intCast(@max(0, abs - eb.row0)); const ind = modal.indentText(modal.lineSlice(eb.text, row)); const arena = p.scratch.allocator(); @@ -449,146 +459,147 @@ pub fn enterInsert(p: *Pardes, pane: *Pane, where: InsertAt, cnt: usize) void { else .{ .row = row, .col = 0 }; const new = modal.insertAt(p.gpa, eb.text, at, block) catch return; - setEditText(p, pane, new); - pane.cur_row = @as(i32, @intCast(if (below) row + 1 else row)) + eb.row0; - pane.cur_col = @intCast(ind.len); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_row = @as(i32, @intCast(if (below) row + 1 else row)) + eb.row0; + t.cur_col = @intCast(ind.len); + t.cur_pinned = true; if (cnt > 1 and !p.multi_on) opened: { - const t2 = flatSurface(p, pane) catch break :opened; - var rs: [Pane.max_selections]modal.Selection = undefined; - const m = @min(cnt, Pane.max_selections); + const t2 = flatSurface(p, t) catch break :opened; + var rs: [Text.max_selections]modal.Selection = undefined; + const m = @min(cnt, Text.max_selections); for (0..m) |k| { - const o = modal.offsetAt(t2, .{ .row = @intCast(@max(0, pane.cur_row) + @as(i32, @intCast(k))), .col = ind.len }); + const o = modal.offsetAt(t2, .{ .row = @intCast(@max(0, t.cur_row) + @as(i32, @intCast(k))), .col = ind.len }); rs[k] = .{ .anchor = o, .head = o }; } - pane.setRanges(t2, rs[0..m], &.{}, 0, false); + t.setRanges(t2, rs[0..m], &.{}, 0, false); } }, } - pane.mode = .insert; - pane.msel.active = false; - pane.normal.clear(); + t.mode = .insert; + t.msel.active = false; + t.normal.clear(); pane.ensureCursorVisible(); } -pub fn handleInsert(p: *Pardes, pane: *Pane, key: Key) void { - if (pane.nsel == 0) return insertKey(p, pane, key); - normal.replaySels(p, pane, .{ .insert = key }); +pub fn handleInsert(p: *Pardes, t: *Text, key: Key) void { + if (t.nsel == 0) return insertKey(p, t, key); + normal.replaySels(p, t, .{ .insert = key }); } -pub fn insertKey(p: *Pardes, pane: *Pane, key: Key) void { +pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { + const pane = t.pane(); // helix aliases: normalize to the base key and re-dispatch - if (hit(key, config.insert_backspace_alias)) return insertKey(p, pane, .{ .cp = Key.backspace }); - if (hit(key, config.insert_enter_alias)) return insertKey(p, pane, .{ .cp = Key.enter }); - if (hit(key, config.insert_delete_alias)) return insertKey(p, pane, .{ .cp = Key.delete }); - pane.vsel.active = false; - if (!pane.cur_pinned) pinPaneCursor(p, pane); - _ = clampFileCursor(pane); + if (hit(key, config.insert_backspace_alias)) return insertKey(p, t, .{ .cp = Key.backspace }); + if (hit(key, config.insert_enter_alias)) return insertKey(p, t, .{ .cp = Key.enter }); + if (hit(key, config.insert_delete_alias)) return insertKey(p, t, .{ .cp = Key.delete }); + t.vsel.active = false; + if (!t.cur_pinned) pinPaneCursor(p, t); + _ = clampFileCursor(t); switch (key.cp) { Key.left, Key.right, Key.up, Key.down => { - const lines = paneCursorLines(p, pane) catch return; - const cur0 = pane.toModalCursor(); + const lines = paneCursorLines(p, t) catch return; + const cur0 = t.toModalCursor(); const nc = switch (key.cp) { Key.left => modal.charLeft(lines, cur0), Key.right => modal.charRight(lines, cur0), - Key.up => Pane.insertVerticalCursor(lines, cur0, false), - Key.down => Pane.insertVerticalCursor(lines, cur0, true), + Key.up => Text.insertVerticalCursor(lines, cur0, false), + Key.down => Text.insertVerticalCursor(lines, cur0, true), else => cur0, }; - pane.fromModalCursor(nc); + t.fromModalCursor(nc); pane.ensureCursorVisible(); return; }, Key.page_up, Key.page_down => { // helix binds insert pageup/pagedown to the same view // scroll + cursor snap as normal mode - const flat = flatSurface(p, pane) catch return; - const range = pane.primaryRange(flat, 0); + const flat = flatSurface(p, t) catch return; + const range = t.primaryRange(flat, 0); const step: i32 = @intCast(@max(1, pane.rows)); - pane.scrollViewMove(flat, range, if (key.cp == Key.page_down) step else -step); + t.scrollViewMove(flat, range, if (key.cp == Key.page_down) step else -step); return; }, Key.home => { - pane.cur_col = 0; - pane.cur_pinned = true; + t.cur_col = 0; + t.cur_pinned = true; pane.ensureCursorVisible(); return; }, Key.end => { // helix insert End: past the last char (goto_line_end_newline) - const text = flatSurface(p, pane) catch return; - const cur0 = pane.toModalCursor(); - pane.cur_col = @intCast(panes.File.textLine(pane, text, cur0.row).len); - pane.cur_pinned = true; + const text = flatSurface(p, t) catch return; + const cur0 = t.toModalCursor(); + t.cur_col = @intCast(panes.File.textLine(t, text, cur0.row).len); + t.cur_pinned = true; pane.ensureCursorVisible(); return; }, else => {}, } - const eb = editText(p, pane, pane.cur_row, pane.cur_row, pane.cur_col) orelse return; + const eb = editText(p, t, t.cur_row, t.cur_row, t.cur_col) orelse return; const text = eb.text; - const c = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - eb.row0)), .col = @intCast(@max(0, pane.cur_col)) }; + 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, pane, new); + setEditText(p, t, new); for (key.text) |ch| { if (ch == '\n') { - pane.cur_row += 1; - pane.cur_col = 0; - } else pane.cur_col += 1; + t.cur_row += 1; + t.cur_col = 0; + } else t.cur_col += 1; } - pane.cur_pinned = true; + t.cur_pinned = true; pane.ensureCursorVisible(); return; } // helix insert-mode kills (word/line; deleteSpan is exclusive) if (hit(key, config.delete_word_backward)) { const e2 = if (c.row == 0 and c.col == 0) - (editText(p, pane, pane.cur_row - 1, pane.cur_row, 0) orelse return) + (editText(p, t, t.cur_row - 1, t.cur_row, 0) orelse return) else eb; - const c2 = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - e2.row0)), .col = c.col }; + const c2 = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - e2.row0)), .col = c.col }; const g = modal.offsetAt(e2.text, c2); if (g == 0) return; const wr = modal.moveWord(e2.text, .{ .anchor = g, .head = g }, 1, .prev_word_start); 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, pane, new); - pane.cur_row = @as(i32, @intCast(fc.row)) + e2.row0; - pane.cur_col = @intCast(fc.col); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_row = @as(i32, @intCast(fc.row)) + e2.row0; + t.cur_col = @intCast(fc.col); + t.cur_pinned = true; pane.ensureCursorVisible(); return; } if (hit(key, config.delete_word_forward)) { const e2 = if (c.col >= modal.lineSlice(text, c.row).len and c.row + 1 >= modal.lineCount(text)) - (editText(p, pane, pane.cur_row, pane.cur_row + 1, pane.cur_col) orelse return) + (editText(p, t, t.cur_row, t.cur_row + 1, t.cur_col) orelse return) else eb; - const c2 = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - e2.row0)), .col = c.col }; + const c2 = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - e2.row0)), .col = c.col }; const g = modal.offsetAt(e2.text, c2); const wr = modal.moveWord(e2.text, .{ .anchor = g, .head = g }, 1, .next_word_end); 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, pane, new); - pane.cur_pinned = true; + setEditText(p, t, new); + 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, pane, new); - pane.cur_col = 0; - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_col = 0; + t.cur_pinned = true; pane.ensureCursorVisible(); return; } 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, pane, new); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_pinned = true; return; } switch (key.cp) { @@ -600,35 +611,35 @@ pub fn insertKey(p: *Pardes, pane: *Pane, key: Key) void { block[0] = '\n'; @memset(block[1..], ' '); const new = modal.insertAt(p.gpa, text, c, block) catch return; - setEditText(p, pane, new); - pane.cur_row += 1; - pane.cur_col = @intCast(indent); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_row += 1; + t.cur_col = @intCast(indent); + t.cur_pinned = true; pane.ensureCursorVisible(); }, Key.backspace => { - if (pane.cur_col > 0) { + if (t.cur_col > 0) { const line = modal.lineSlice(text, c.row); const prev = modal.prevGrapheme(line, c.col); const new = modal.deleteChar(p.gpa, text, .{ .row = c.row, .col = prev }) catch return; - setEditText(p, pane, new); - pane.cur_col = @intCast(prev); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_col = @intCast(prev); + t.cur_pinned = true; pane.ensureCursorVisible(); return; } // at col 0: eat the newline above (join up) — a terminal grows // its buffer over the row it is joining with - if (pane.cur_row <= 0) return; - const e2 = editText(p, pane, pane.cur_row - 1, pane.cur_row, 0) orelse return; - const row: usize = @intCast(@max(0, pane.cur_row - e2.row0)); + if (t.cur_row <= 0) return; + const e2 = editText(p, t, t.cur_row - 1, t.cur_row, 0) orelse return; + const row: usize = @intCast(@max(0, t.cur_row - e2.row0)); 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, pane, new); - pane.cur_row -= 1; - pane.cur_col = @intCast(prevlen); - pane.cur_pinned = true; + setEditText(p, t, new); + t.cur_row -= 1; + t.cur_col = @intCast(prevlen); + t.cur_pinned = true; pane.ensureCursorVisible(); }, Key.delete => { @@ -637,15 +648,15 @@ pub fn insertKey(p: *Pardes, pane: *Pane, 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, pane, new); + setEditText(p, t, new); } else { - const e2 = editText(p, pane, pane.cur_row, pane.cur_row + 1, pane.cur_col) orelse return; - const row: usize = @intCast(@max(0, pane.cur_row - e2.row0)); + 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, pane, new); + setEditText(p, t, new); } - pane.cur_pinned = true; + t.cur_pinned = true; }, Key.tab => { const ln = modal.lineSlice(text, c.row); @@ -656,51 +667,51 @@ pub fn insertKey(p: *Pardes, pane: *Pane, key: Key) void { p.lspRequest(p.active, .completion, ""); if (p.lsp_seq != seq) return; } - insertTab(p, pane); + insertTab(p, t); }, else => {}, } } -pub fn insertTab(p: *Pardes, pane: *Pane) void { - const eb = editText(p, pane, pane.cur_row, pane.cur_row, pane.cur_col) orelse return; +pub fn insertTab(p: *Pardes, t: *Text) void { + const eb = editText(p, t, t.cur_row, t.cur_row, t.cur_col) orelse return; const c: modal.Cursor = .{ - .row = @intCast(@max(0, pane.cur_row - eb.row0)), - .col = @intCast(@max(0, pane.cur_col)), + .row = @intCast(@max(0, t.cur_row - eb.row0)), + .col = @intCast(@max(0, t.cur_col)), }; 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, pane, new); - pane.cur_col += @intCast(pad); - pane.cur_pinned = true; - pane.ensureCursorVisible(); + setEditText(p, t, new); + t.cur_col += @intCast(pad); + t.cur_pinned = true; + t.pane().ensureCursorVisible(); } const Bounds = struct { lo_row: i32, lo_col: i32, hi_row: i32, hi_col: i32 }; /// a range's two cells, normalized to document order -pub fn cellBounds(s: Pane.SelRange) Bounds { +pub fn cellBounds(s: Text.SelRange) Bounds { if (s.row < s.arow or (s.row == s.arow and s.col < s.acol)) return .{ .lo_row = s.row, .lo_col = s.col, .hi_row = s.arow, .hi_col = s.acol }; return .{ .lo_row = s.arow, .lo_col = s.acol, .hi_row = s.row, .hi_col = s.col }; } /// the char selection [anchor, cursor] normalized to document order -fn vselBounds(pane: *Pane) Bounds { - return cellBounds(.{ .row = pane.cur_row, .col = pane.cur_col, .arow = pane.vsel.row, .acol = pane.vsel.col }); +fn vselBounds(t: *Text) Bounds { + return cellBounds(.{ .row = t.cur_row, .col = t.cur_col, .arow = t.vsel.row, .acol = t.vsel.col }); } -fn vselText(p: *Pardes, pane: *Pane) []const u8 { +fn vselText(p: *Pardes, t: *Text) []const u8 { const arena = p.scratch.allocator(); - const b = vselBounds(pane); - const text = flatSurface(p, pane) catch return ""; + const b = vselBounds(t); + const text = flatSurface(p, t) catch return ""; return modal.rangeText(arena, text, .{ .row = @intCast(@max(0, b.lo_row)), .col = @intCast(@max(0, b.lo_col)) }, .{ .row = @intCast(@max(0, b.hi_row)), .col = @intCast(@max(0, b.hi_col)) }) catch ""; } /// join surface rows [r0, r1] (absolute) with '\n'; scratch-owned -fn yankRows(p: *Pardes, pane: *Pane, r0: i32, r1: i32) []const u8 { +fn yankRows(p: *Pardes, t: *Text, r0: i32, r1: i32) []const u8 { const arena = p.scratch.allocator(); - const lines = paneCursorLines(p, pane) catch return ""; + const lines = paneCursorLines(p, t) catch return ""; const rows_count: usize = @intCast(@max(0, r1 - r0 + 1)); var total: usize = rows_count -| 1; var i = r0; @@ -725,54 +736,54 @@ fn yankRows(p: *Pardes, pane: *Pane, r0: i32, r1: i32) []const u8 { return out; } -pub fn normalDelete(p: *Pardes, pane: *Pane, yank: bool) void { - pane.select = false; // helix delete exits select mode - pane.sticky_col = -1; - if (pane.vsel.active) { - const b = vselBounds(pane); - if (yank) setYank(p, vselText(p, pane)); // read the range before the move - pushUndo(p, pane); - const eb = editTextEol(p, pane, b) orelse { - pane.vsel.active = false; +pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void { + t.select = false; // helix delete exits select mode + t.sticky_col = -1; + if (t.vsel.active) { + const b = vselBounds(t); + if (yank) setYank(p, vselText(p, t)); // read the range before the move + pushUndo(p, t); + const eb = editTextEol(p, t, b) orelse { + t.vsel.active = false; return; }; const d = modal.deleteRange(p.gpa, eb.text, .{ .row = @intCast(@max(0, b.lo_row - eb.row0)), .col = @intCast(@max(0, b.lo_col)) }, .{ .row = @intCast(@max(0, b.hi_row - eb.row0)), .col = @intCast(@max(0, b.hi_col)) }) catch { - pane.vsel.active = false; + t.vsel.active = false; return; }; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); - pane.cur_row = b.lo_row; - pane.cur_col = b.lo_col; - pane.clampCursor(d.content, eb.row0); + t.cur_row = b.lo_row; + t.cur_col = b.lo_col; + t.clampCursor(d.content, eb.row0); return; } - if (pane.msel.active) { - const r0 = @min(pane.msel.r0, pane.msel.r1); - const r1 = @max(pane.msel.r0, pane.msel.r1); - pushUndo(p, pane); + if (t.msel.active) { + const r0 = @min(t.msel.r0, t.msel.r1); + const r1 = @max(t.msel.r0, t.msel.r1); + pushUndo(p, t); // linewise: a line's newline lives on the row below it - const eb = editText(p, pane, r0, r1 + 1, -1) orelse { - pane.msel.active = false; + const eb = editText(p, t, r0, r1 + 1, -1) orelse { + t.msel.active = false; return; }; const d = modal.deleteLines(p.gpa, eb.text, @intCast(@max(0, r0 - eb.row0)), @intCast(@max(0, r1 - eb.row0))) catch { - pane.msel.active = false; + t.msel.active = false; return; }; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); if (yank) setYank(p, d.deleted); p.gpa.free(d.deleted); const n = modal.lineCount(d.content); - pane.cur_row = @as(i32, @intCast(@min(@as(usize, @intCast(@max(0, r0 - eb.row0))), if (n == 0) 0 else n - 1))) + eb.row0; - pane.cur_col = 0; - pane.msel.active = false; - pane.cur_pinned = true; - pane.ensureCursorVisible(); + t.cur_row = @as(i32, @intCast(@min(@as(usize, @intCast(@max(0, r0 - eb.row0))), if (n == 0) 0 else n - 1))) + eb.row0; + t.cur_col = 0; + t.msel.active = false; + t.cur_pinned = true; + t.pane().ensureCursorVisible(); } else { - pushUndo(p, pane); - const eb = editTextEol(p, pane, .{ .lo_row = pane.cur_row, .lo_col = pane.cur_col, .hi_row = pane.cur_row, .hi_col = pane.cur_col }) orelse return; - const c = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - eb.row0)), .col = @intCast(@max(0, pane.cur_col)) }; + pushUndo(p, t); + const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return; + const c = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) }; if (yank) { const arena = p.scratch.allocator(); setYank(p, modal.rangeText(arena, eb.text, c, c) catch return); @@ -780,38 +791,39 @@ pub fn normalDelete(p: *Pardes, pane: *Pane, yank: bool) void { // deleteRange (not deleteChar): the cursor may sit ON a '\n' // cell, where a delete joins the lines (helix 1-wide selection) const d = modal.deleteRange(p.gpa, eb.text, c, c) catch return; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); const llen = modal.lineSlice(d.content, c.row).len; - pane.cur_col = @min(pane.cur_col, @as(i32, @intCast(llen))); + t.cur_col = @min(t.cur_col, @as(i32, @intCast(llen))); } } -pub fn normalYank(p: *Pardes, pane: *Pane) void { - pane.select = false; +pub fn normalYank(p: *Pardes, t: *Text) void { + const pane = t.pane(); + t.select = false; if (pane.rawPointerText(pane.sel[sel_slot])) |text| { setYank(p, text); return; } - if (pane.vsel.active) { - setYank(p, vselText(p, pane)); + if (t.vsel.active) { + setYank(p, vselText(p, t)); return; } - if (pane.msel.active) { - setYank(p, yankRows(p, pane, @min(pane.msel.r0, pane.msel.r1), @max(pane.msel.r0, pane.msel.r1))); + if (t.msel.active) { + setYank(p, yankRows(p, t, @min(t.msel.r0, t.msel.r1), @max(t.msel.r0, t.msel.r1))); } else { - const save = pane.vsel; - pane.vsel = .{ .active = true, .row = pane.cur_row, .col = pane.cur_col, .explicit = false }; - setYank(p, vselText(p, pane)); - pane.vsel = save; + const save = t.vsel; + t.vsel = .{ .active = true, .row = t.cur_row, .col = t.cur_col, .explicit = false }; + setYank(p, vselText(p, t)); + t.vsel = save; } } -pub fn pasteText(p: *Pardes, pane: *Pane, y0: []const u8, before: bool, count: usize) void { +pub fn pasteText(p: *Pardes, t: *Text, y0: []const u8, before: bool, count: usize) void { if (y0.len == 0) return; - pushUndo(p, pane); - pane.select = false; - pane.sticky_col = -1; + pushUndo(p, t); + t.select = false; + t.sticky_col = -1; const arena = p.scratch.allocator(); var y: []const u8 = y0; if (count > 1) { @@ -820,11 +832,11 @@ pub fn pasteText(p: *Pardes, pane: *Pane, y0: []const u8, before: bool, count: u for (0..count) |i| @memcpy(buf[i * y0.len ..][0..y0.len], y0); y = buf; } - const b: Bounds = if (pane.vsel.active) - vselBounds(pane) + const b: Bounds = if (t.vsel.active) + vselBounds(t) else - .{ .lo_row = pane.cur_row, .lo_col = pane.cur_col, .hi_row = pane.cur_row, .hi_col = pane.cur_col }; - const eb = editText(p, pane, b.lo_row, b.hi_row, -1) orelse return; + .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }; + const eb = editText(p, t, b.lo_row, b.hi_row, -1) orelse return; const row0 = eb.row0; if (y[y.len - 1] == '\n') { const block_text = y[0 .. y.len - 1]; @@ -834,17 +846,17 @@ pub fn pasteText(p: *Pardes, pane: *Pane, y0: []const u8, before: bool, count: u const row: usize = @intCast(@max(0, b.lo_row - row0)); const block = std.fmt.allocPrint(arena, "{s}\n", .{block_text}) catch return; out = modal.insertAt(p.gpa, eb.text, .{ .row = row, .col = 0 }, block) catch return; - pane.cur_row = @as(i32, @intCast(row + n - 1)) + row0; + t.cur_row = @as(i32, @intCast(row + n - 1)) + row0; } else { const row: usize = @intCast(@max(0, b.hi_row - row0)); out = modal.pasteLineBelow(p.gpa, eb.text, row, block_text) catch return; - pane.cur_row = @as(i32, @intCast(row + n)) + row0; + t.cur_row = @as(i32, @intCast(row + n)) + row0; } - setEditText(p, pane, out); - pane.vsel = .{ .active = true, .row = pane.cur_row - @as(i32, @intCast(n - 1)), .col = 0, .explicit = false }; + setEditText(p, t, out); + t.vsel = .{ .active = true, .row = t.cur_row - @as(i32, @intCast(n - 1)), .col = 0, .explicit = false }; // cursor ON the last pasted line's '\n' (helix) - const llen = modal.lineSlice(out, @intCast(@max(0, pane.cur_row - row0))).len; - pane.cur_col = @intCast(llen); + const llen = modal.lineSlice(out, @intCast(@max(0, t.cur_row - row0))).len; + t.cur_col = @intCast(llen); } else { const at: modal.Cursor = if (before) .{ .row = @intCast(@max(0, b.lo_row - row0)), .col = @intCast(@max(0, b.lo_col)) } @@ -857,34 +869,35 @@ pub fn pasteText(p: *Pardes, pane: *Pane, y0: []const u8, before: bool, count: u break :blk .{ .row = hrow, .col = gcol }; }; const out = modal.insertAt(p.gpa, eb.text, at, y) catch return; - setEditText(p, pane, out); - pane.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @as(i32, @intCast(at.row)) + row0, .col = @intCast(at.col), .explicit = false }; + setEditText(p, t, out); + t.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @as(i32, @intCast(at.row)) + row0, .col = @intCast(at.col), .explicit = false }; const end = modal.advanceBy(at, y); if (end.col > 0) { - pane.cur_row = @as(i32, @intCast(end.row)) + row0; - pane.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(out, end.row), end.col)); + t.cur_row = @as(i32, @intCast(end.row)) + row0; + t.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(out, end.row), end.col)); } else { - pane.cur_row = @as(i32, @intCast(end.row -| 1)) + row0; - pane.cur_col = @intCast(modal.lineSlice(out, end.row -| 1).len); + t.cur_row = @as(i32, @intCast(end.row -| 1)) + row0; + t.cur_col = @intCast(modal.lineSlice(out, end.row -| 1).len); } } - pane.msel.active = false; - pane.cur_pinned = true; - pane.ensureCursorVisible(); + t.msel.active = false; + t.cur_pinned = true; + t.pane().ensureCursorVisible(); } -pub fn normalChange(p: *Pardes, pane: *Pane) void { - pane.select = false; - pane.sticky_col = -1; - if (pane.vsel.active) { - const b = vselBounds(pane); - const eb = editTextEol(p, pane, b) orelse return; +pub fn normalChange(p: *Pardes, t: *Text) void { + const pane = t.pane(); + t.select = false; + t.sticky_col = -1; + if (t.vsel.active) { + const b = vselBounds(t); + const eb = editTextEol(p, t, b) orelse return; const hi_len = modal.lineSlice(eb.text, @intCast(@max(0, b.hi_row - eb.row0))).len; if (b.lo_col == 0 and @as(usize, @intCast(@max(0, b.hi_col))) >= hi_len) { - setYank(p, vselText(p, pane)); - pushUndo(p, pane); + setYank(p, vselText(p, t)); + pushUndo(p, t); const d = modal.deleteLines(p.gpa, eb.text, @intCast(@max(0, b.lo_row - eb.row0)), @intCast(@max(0, b.hi_row - eb.row0))) catch return; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); const n = modal.lineCount(d.content); const row: usize = @min(@as(usize, @intCast(@max(0, b.lo_row - eb.row0))), if (n == 0) 0 else n - 1); @@ -892,37 +905,37 @@ pub fn normalChange(p: *Pardes, pane: *Pane) void { const arena = p.scratch.allocator(); const block = std.fmt.allocPrint(arena, "{s}\n", .{ind}) catch return; const new = modal.insertAt(p.gpa, d.content, .{ .row = row, .col = 0 }, block) catch return; - setEditText(p, pane, new); - pane.cur_row = @as(i32, @intCast(row)) + eb.row0; - pane.cur_col = @intCast(ind.len); - pane.vsel.active = false; - pane.msel.active = false; - pane.cur_pinned = true; - pane.mode = .insert; - pane.normal.clear(); + setEditText(p, t, new); + t.cur_row = @as(i32, @intCast(row)) + eb.row0; + t.cur_col = @intCast(ind.len); + t.vsel.active = false; + t.msel.active = false; + t.cur_pinned = true; + t.mode = .insert; + t.normal.clear(); pane.ensureCursorVisible(); return; } - setYank(p, vselText(p, pane)); - pushUndo(p, pane); + setYank(p, vselText(p, t)); + pushUndo(p, t); const d = modal.deleteRange(p.gpa, eb.text, .{ .row = @intCast(@max(0, b.lo_row - eb.row0)), .col = @intCast(@max(0, b.lo_col)) }, .{ .row = @intCast(@max(0, b.hi_row - eb.row0)), .col = @intCast(@max(0, b.hi_col)) }) catch { - pane.vsel.active = false; + t.vsel.active = false; return; }; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); - pane.cur_row = b.lo_row; - pane.cur_col = b.lo_col; - pane.clampCursor(d.content, eb.row0); - pane.mode = .insert; - pane.normal.clear(); + t.cur_row = b.lo_row; + t.cur_col = b.lo_col; + t.clampCursor(d.content, eb.row0); + t.mode = .insert; + t.normal.clear(); return; } - if (pane.msel.active) { - const r0 = @min(pane.msel.r0, pane.msel.r1); - const r1 = @max(pane.msel.r0, pane.msel.r1); - pushUndo(p, pane); - const eb = editText(p, pane, r0, r1 + 1, -1) orelse return; + if (t.msel.active) { + const r0 = @min(t.msel.r0, t.msel.r1); + const r1 = @max(t.msel.r0, t.msel.r1); + pushUndo(p, t); + const eb = editText(p, t, r0, r1 + 1, -1) orelse return; const arena = p.scratch.allocator(); const lo: usize = @intCast(@max(0, r0 - eb.row0)); const hi: usize = @intCast(@max(0, r1 - eb.row0)); @@ -943,77 +956,77 @@ pub fn normalChange(p: *Pardes, pane: *Pane) void { var body: []u8 = eb.text; if (hi > lo) { const d = modal.deleteLines(p.gpa, body, lo + 1, hi) catch { - pane.msel.active = false; + t.msel.active = false; return; }; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); body = d.content; } const cl = modal.clearLine(p.gpa, body, lo) catch { - pane.msel.active = false; + t.msel.active = false; return; }; - setEditText(p, pane, cl); - pane.cur_row = r0; - pane.cur_col = 0; - pane.msel.active = false; - pane.cur_pinned = true; - pane.mode = .insert; - pane.normal.clear(); + setEditText(p, t, cl); + t.cur_row = r0; + t.cur_col = 0; + t.msel.active = false; + t.cur_pinned = true; + t.mode = .insert; + t.normal.clear(); pane.ensureCursorVisible(); } else { - pushUndo(p, pane); - const eb = editTextEol(p, pane, .{ .lo_row = pane.cur_row, .lo_col = pane.cur_col, .hi_row = pane.cur_row, .hi_col = pane.cur_col }) orelse return; - const c = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - eb.row0)), .col = @intCast(@max(0, pane.cur_col)) }; + pushUndo(p, t); + const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return; + const c = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) }; const arena = p.scratch.allocator(); setYank(p, modal.rangeText(arena, eb.text, c, c) catch return); const d = modal.deleteRange(p.gpa, eb.text, c, c) catch return; - setEditText(p, pane, d.content); + setEditText(p, t, d.content); p.gpa.free(d.deleted); const llen = modal.lineSlice(d.content, c.row).len; - pane.cur_col = @min(pane.cur_col, @as(i32, @intCast(llen))); - pane.mode = .insert; - pane.normal.clear(); + t.cur_col = @min(t.cur_col, @as(i32, @intCast(llen))); + t.mode = .insert; + t.normal.clear(); } } -fn selRange(pane: *Pane, content: []const u8, row0: i32) modal.Range { - if (pane.vsel.active) { - const b = vselBounds(pane); +fn selRange(t: *Text, content: []const u8, row0: i32) modal.Range { + if (t.vsel.active) { + const b = vselBounds(t); return .{ .a = .{ .row = @intCast(@max(0, b.lo_row - row0)), .col = @intCast(@max(0, b.lo_col)) }, .b = .{ .row = @intCast(@max(0, b.hi_row - row0)), .col = @intCast(@max(0, b.hi_col)) }, }; } - if (pane.msel.active) { - const r0: usize = @intCast(@max(0, @min(pane.msel.r0, pane.msel.r1) - row0)); - const r1: usize = @intCast(@max(0, @max(pane.msel.r0, pane.msel.r1) - row0)); + if (t.msel.active) { + const r0: usize = @intCast(@max(0, @min(t.msel.r0, t.msel.r1) - row0)); + const r1: usize = @intCast(@max(0, @max(t.msel.r0, t.msel.r1) - row0)); const llen = modal.lineSlice(content, r1).len; return .{ .a = .{ .row = r0, .col = 0 }, .b = .{ .row = r1, .col = if (llen == 0) 0 else modal.prevGrapheme(modal.lineSlice(content, r1), llen) } }; } - const c = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row - row0)), .col = @intCast(@max(0, pane.cur_col)) }; + const c = modal.Cursor{ .row = @intCast(@max(0, t.cur_row - row0)), .col = @intCast(@max(0, t.cur_col)) }; return .{ .a = c, .b = c }; } /// the rows an op's selection spans, absolute (what editText must cover) -fn selRows(pane: *Pane) Bounds { - if (pane.vsel.active) return vselBounds(pane); - if (pane.msel.active) return .{ - .lo_row = @min(pane.msel.r0, pane.msel.r1), +fn selRows(t: *Text) Bounds { + if (t.vsel.active) return vselBounds(t); + if (t.msel.active) return .{ + .lo_row = @min(t.msel.r0, t.msel.r1), .lo_col = 0, - .hi_row = @max(pane.msel.r0, pane.msel.r1), + .hi_row = @max(t.msel.r0, t.msel.r1), .hi_col = 0, }; - return .{ .lo_row = pane.cur_row, .lo_col = pane.cur_col, .hi_row = pane.cur_row, .hi_col = pane.cur_col }; + return .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }; } /// `r`: overwrite the selection (or the cursor char) with ch — /// newlines included (helix), so `xrz` joins the selected lines -pub fn normalReplaceChar(p: *Pardes, pane: *Pane, ch: u21) void { - pane.select = false; - const eb = editTextEol(p, pane, selRows(pane)) orelse return; - const before = pane.primaryRange(eb.text, eb.row0); +pub fn normalReplaceChar(p: *Pardes, t: *Text, ch: u21) void { + t.select = false; + const eb = editTextEol(p, t, selRows(t)) orelse return; + const before = t.primaryRange(eb.text, eb.row0); const lo = @min(before.anchor, before.head); const hi = @max(before.anchor, before.head); var graphemes: usize = 0; @@ -1021,59 +1034,59 @@ pub fn normalReplaceChar(p: *Pardes, pane: *Pane, ch: u21) void { while (at < hi) : (graphemes += 1) at = modal.nextGrapheme(eb.text, at); var encoded: [4]u8 = undefined; const encoded_len = std.unicode.utf8Encode(ch, &encoded) catch return; - const r = selRange(pane, eb.text, eb.row0); - pushUndo(p, pane); + const r = selRange(t, eb.text, eb.row0); + pushUndo(p, t); const new = modal.replaceChars(p.gpa, eb.text, r.a, r.b, ch) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); const end = lo + graphemes * encoded_len; const mapped: modal.Selection = if (before.head < before.anchor) .{ .anchor = end, .head = lo } else .{ .anchor = lo, .head = end }; - pane.setRange(new, eb.row0, mapped, pane.vsel.explicit); + t.setRange(new, eb.row0, mapped, t.vsel.explicit); } /// `R`: replace the selection (or the cursor char) with the DEFAULT /// register. `SPC R` is the same verb over the system clipboard. -pub fn normalReplaceYank(p: *Pardes, pane: *Pane) void { - replaceWithText(p, pane, p.yank orelse return); +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). -fn replaceWithText(p: *Pardes, pane: *Pane, y: []const u8) void { +fn replaceWithText(p: *Pardes, t: *Text, y: []const u8) void { if (y.len == 0) return; - pane.select = false; - const eb = editTextEol(p, pane, selRows(pane)) orelse return; - const r = selRange(pane, eb.text, eb.row0); - pushUndo(p, pane); + t.select = false; + const eb = editTextEol(p, t, selRows(t)) orelse return; + const r = selRange(t, eb.text, eb.row0); + pushUndo(p, t); const new = modal.replaceRange(p.gpa, eb.text, r.a, r.b, y) catch return; - setEditText(p, pane, new); - pane.msel.active = false; - pane.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; + 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); if (end.col > 0) { - pane.cur_row = @as(i32, @intCast(end.row)) + eb.row0; - pane.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col)); + t.cur_row = @as(i32, @intCast(end.row)) + eb.row0; + t.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(new, end.row), end.col)); } else { // the yank ended in '\n': the cursor lands ON that newline - pane.cur_row = @as(i32, @intCast(end.row -| 1)) + eb.row0; - pane.cur_col = @intCast(modal.lineSlice(new, end.row -| 1).len); + t.cur_row = @as(i32, @intCast(end.row -| 1)) + eb.row0; + t.cur_col = @intCast(modal.lineSlice(new, end.row -| 1).len); } - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.ensureCursorVisible(); + t.cur_pinned = true; + t.sticky_col = -1; + t.pane().ensureCursorVisible(); } /// `~` / `` ` `` / ``Alt-` ``: case-map the selection (or the cursor char), /// keeping the selection (helix) -pub fn normalCase(p: *Pardes, pane: *Pane, op: modal.CaseOp) void { - pane.select = false; - const eb = editTextEol(p, pane, selRows(pane)) orelse return; - const r = selRange(pane, eb.text, eb.row0); - pushUndo(p, pane); +pub fn normalCase(p: *Pardes, t: *Text, op: modal.CaseOp) void { + t.select = false; + const eb = editTextEol(p, t, selRows(t)) orelse return; + const r = selRange(t, eb.text, eb.row0); + pushUndo(p, t); const new = modal.changeCase(p.gpa, eb.text, r.a, r.b, op) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); } /// a change to apply to file content: delete [from, to), insert `ins` at @@ -1115,14 +1128,14 @@ fn applyChanges(p: *Pardes, text: []const u8, chs: []const TextChange) ![]u8 { return out; } -pub fn normalJoin(p: *Pardes, pane: *Pane) void { +pub fn normalJoin(p: *Pardes, t: *Text) void { // a join always eats the newline of its last line, so the buffer has // to reach one row PAST the selection - const sr = selRows(pane); - const eb = editText(p, pane, sr.lo_row, sr.hi_row + 1, -1) orelse return; + const sr = selRows(t); + const eb = editText(p, t, sr.lo_row, sr.hi_row + 1, -1) orelse return; const text = eb.text; - const range = pane.primaryRange(text, eb.row0); - const span = Pane.rangeLineSpan(text, range); + const range = t.primaryRange(text, eb.row0); + const span = Text.rangeLineSpan(text, range); const nlines = modal.cursorLineCount(text); var end = span.end; if (span.start == end) end = @min(end + 1, nlines - 1); @@ -1140,37 +1153,37 @@ pub fn normalJoin(p: *Pardes, pane: *Pane) void { chs_len += 1; } if (chs_len == 0) return; - pushUndo(p, pane); - const cur_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row - eb.row0)), .col = @intCast(@max(0, pane.cur_col)) }); - const anc_off = if (pane.vsel.active) - modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.vsel.row - eb.row0)), .col = @intCast(@max(0, pane.vsel.col)) }) + pushUndo(p, t); + const cur_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) }); + const anc_off = if (t.vsel.active) + modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row - eb.row0)), .col = @intCast(@max(0, t.vsel.col)) }) else cur_off; const new = applyChanges(p, text, chs[0..chs_len]) catch return; const nc = mapThroughChanges(chs[0..chs_len], cur_off); const na = mapThroughChanges(chs[0..chs_len], anc_off); - setEditText(p, pane, new); + setEditText(p, t, new); const cc = modal.positionAt(new, nc); const ac = modal.positionAt(new, na); - pane.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; - pane.cur_col = @intCast(cc.col); - if (pane.vsel.active) { - pane.vsel.row = @as(i32, @intCast(ac.row)) + eb.row0; - pane.vsel.col = @intCast(ac.col); + t.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; + t.cur_col = @intCast(cc.col); + if (t.vsel.active) { + t.vsel.row = @as(i32, @intCast(ac.row)) + eb.row0; + t.vsel.col = @intCast(ac.col); } - pane.msel.active = false; - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.ensureCursorVisible(); + t.msel.active = false; + t.cur_pinned = true; + t.sticky_col = -1; + t.pane().ensureCursorVisible(); } -pub fn normalIndent(p: *Pardes, pane: *Pane, cnt: usize, add: bool) void { - pane.select = false; - const sr = selRows(pane); - const eb = editText(p, pane, sr.lo_row, sr.hi_row, -1) orelse return; +pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void { + t.select = false; + const sr = selRows(t); + const eb = editText(p, t, sr.lo_row, sr.hi_row, -1) orelse return; const text = eb.text; - const range = pane.primaryRange(text, eb.row0); - const span = Pane.rangeLineSpan(text, range); + const range = t.primaryRange(text, eb.row0); + const span = Text.rangeLineSpan(text, range); const arena = p.scratch.allocator(); // one run of spaces, sliced per line: `>` never inserts more than this const pad = arena.alloc(u8, modal.INDENT_W * cnt) catch return; @@ -1208,45 +1221,45 @@ pub fn normalIndent(p: *Pardes, pane: *Pane, cnt: usize, add: bool) void { } } if (chs_len == 0) return; - pushUndo(p, pane); - const cur_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row - eb.row0)), .col = @intCast(@max(0, pane.cur_col)) }); - const anc_off = if (pane.vsel.active) - modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.vsel.row - eb.row0)), .col = @intCast(@max(0, pane.vsel.col)) }) + pushUndo(p, t); + const cur_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row - eb.row0)), .col = @intCast(@max(0, t.cur_col)) }); + const anc_off = if (t.vsel.active) + modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row - eb.row0)), .col = @intCast(@max(0, t.vsel.col)) }) else cur_off; const new = applyChanges(p, text, chs[0..chs_len]) catch return; const nc = mapThroughChanges(chs[0..chs_len], cur_off); const na = mapThroughChanges(chs[0..chs_len], anc_off); - setEditText(p, pane, new); + setEditText(p, t, new); const cc = modal.positionAt(new, nc); const ac = modal.positionAt(new, na); - pane.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; - pane.cur_col = @intCast(cc.col); - if (pane.vsel.active) { - pane.vsel.row = @as(i32, @intCast(ac.row)) + eb.row0; - pane.vsel.col = @intCast(ac.col); + t.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; + t.cur_col = @intCast(cc.col); + if (t.vsel.active) { + t.vsel.row = @as(i32, @intCast(ac.row)) + eb.row0; + t.vsel.col = @intCast(ac.col); } - pane.msel.active = false; - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.ensureCursorVisible(); + t.msel.active = false; + t.cur_pinned = true; + t.sticky_col = -1; + t.pane().ensureCursorVisible(); } -pub fn normalToggleComment(p: *Pardes, pane: *Pane) void { - const expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; - pane.select = false; // helix exit_select_mode +pub fn normalToggleComment(p: *Pardes, t: *Text) void { + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + t.select = false; // helix exit_select_mode // the buffer must cover every cursor's rows, not just the primary's - var b = selRows(pane); - for (pane.sels[0..pane.nsel]) |s| { + var b = selRows(t); + for (t.sels[0..t.nsel]) |s| { b.lo_row = @min(b.lo_row, @min(s.row, s.arow)); b.hi_row = @max(b.hi_row, @max(s.row, s.arow)); } - const surface = flatSurface(p, pane) catch return; - const last_row = @as(i32, @intCast(panes.File.textLineCount(pane, surface))) - 1; - const eb = editText(p, pane, b.lo_row, @min(b.hi_row + 1, last_row), -1) orelse return; + const surface = flatSurface(p, t) catch return; + const last_row = @as(i32, @intCast(panes.File.textLineCount(t, surface))) - 1; + const eb = editText(p, t, b.lo_row, @min(b.hi_row + 1, last_row), -1) orelse return; const text = eb.text; - var rs: [Pane.max_selections]modal.Selection = undefined; - const got = pane.ranges(text, eb.row0, &rs); + var rs: [Text.max_selections]modal.Selection = undefined; + const got = t.ranges(text, eb.row0, &rs); const nlines = modal.cursorLineCount(text); const arena = p.scratch.allocator(); // the lines the ranges cover, each ONE ONCE and in order (helix's @@ -1255,7 +1268,7 @@ pub fn normalToggleComment(p: *Pardes, pane: *Pane) void { var lines_len: usize = 0; var next: usize = 0; for (rs[0..got.n]) |r| { - const span = Pane.rangeLineSpan(text, r); + const span = Text.rangeLineSpan(text, r); var l = @max(span.start, next); const end = @min(span.end + 1, nlines); while (l < end) : (l += 1) { @@ -1264,7 +1277,7 @@ pub fn normalToggleComment(p: *Pardes, pane: *Pane) void { } next = @max(next, end); } - const ext = if (pane.file) |f| std.fs.path.extension(f.path) else ""; + const ext = if (t.pane().file) |f| std.fs.path.extension(f.path) else ""; var token: []const u8 = config.comment_token_default; lang: for (config.comment_tokens) |row| { for (row.exts) |e| if (std.ascii.eqlIgnoreCase(ext, e)) { @@ -1303,13 +1316,13 @@ pub fn normalToggleComment(p: *Pardes, pane: *Pane) void { .{ .from = at, .to = at, .ins = ins }; chs_len += 1; } - pushUndo(p, pane); + pushUndo(p, t); // one edit, and the WHOLE selection rides through it (helix maps the // selection with the transaction) - var cells: [Pane.max_selections]Pane.SelRange = undefined; + var cells: [Text.max_selections]Text.SelRange = undefined; const new = applyChanges(p, text, chs[0..chs_len]) catch return; for (rs[0..got.n], 0..) |r, i| { - const c = Pane.rangeCells(new, .{ + const c = Text.rangeCells(new, .{ .anchor = mapThroughChanges(chs[0..chs_len], r.anchor), .head = mapThroughChanges(chs[0..chs_len], r.head), }); @@ -1322,53 +1335,53 @@ pub fn normalToggleComment(p: *Pardes, pane: *Pane) void { .acol = @intCast(ac.col), }; } - setEditText(p, pane, new); - const t2 = flatSurface(p, pane) catch return; - for (cells[0..got.n], 0..) |s, i| rs[i] = Pane.cellRange(t2, s.arow, s.acol, s.row, s.col); - pane.setRanges(t2, rs[0..got.n], &.{}, got.pri, expl); + setEditText(p, t, new); + const t2 = flatSurface(p, t) catch return; + for (cells[0..got.n], 0..) |s, i| rs[i] = Text.cellRange(t2, s.arow, s.acol, s.row, s.col); + t.setRanges(t2, rs[0..got.n], &.{}, got.pri, expl); } -pub fn normalAdjustNumber(p: *Pardes, pane: *Pane, delta: i64) void { - const eb = editTextEol(p, pane, selRows(pane)) orelse return; - const r = selRange(pane, eb.text, eb.row0); +pub fn normalAdjustNumber(p: *Pardes, t: *Text, delta: i64) void { + const eb = editTextEol(p, t, selRows(t)) orelse return; + const r = selRange(t, eb.text, eb.row0); const arena = p.scratch.allocator(); const frag = modal.rangeText(arena, eb.text, r.a, r.b) catch return; const rep = (modal.incrementDecimal(arena, frag, delta) catch null) orelse return; - pushUndo(p, pane); - pane.select = false; + pushUndo(p, t); + t.select = false; const new = modal.replaceRange(p.gpa, eb.text, r.a, r.b, rep) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); const start = modal.lineStartOffset(new, r.a.row) + r.a.col; const cc = modal.positionAt(new, modal.prevGrapheme(new, start + rep.len)); - pane.vsel = .{ .active = modal.nextGrapheme(rep, 0) < rep.len, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; - pane.msel.active = false; - pane.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; - pane.cur_col = @intCast(cc.col); - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.ensureCursorVisible(); + t.vsel = .{ .active = modal.nextGrapheme(rep, 0) < rep.len, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; + t.msel.active = false; + t.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; + t.cur_col = @intCast(cc.col); + t.cur_pinned = true; + t.sticky_col = -1; + t.pane().ensureCursorVisible(); } /// `]Space` / `[Space`: add blank lines below/above, the cursor staying /// on its text line (the selection rides along) -pub fn addNewline(p: *Pardes, pane: *Pane, below: bool, cnt: usize) void { - const eb = editText(p, pane, pane.cur_row, pane.cur_row, -1) orelse return; - pushUndo(p, pane); - const row: usize = @intCast(@max(0, pane.cur_row - eb.row0)); +pub fn addNewline(p: *Pardes, t: *Text, below: bool, cnt: usize) void { + const eb = editText(p, t, t.cur_row, t.cur_row, -1) orelse return; + pushUndo(p, t); + const row: usize = @intCast(@max(0, t.cur_row - eb.row0)); const arena = p.scratch.allocator(); const nl = arena.alloc(u8, cnt) catch return; @memset(nl, '\n'); const llen = modal.lineSlice(eb.text, row).len; const new = modal.insertAt(p.gpa, eb.text, .{ .row = row, .col = if (below) llen else 0 }, nl) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); if (!below) { - pane.cur_row += @intCast(cnt); // the line moved down; stay on it - if (pane.vsel.active and pane.vsel.row >= @as(i32, @intCast(row))) pane.vsel.row += @intCast(cnt); - } else if (pane.vsel.active and pane.vsel.row > @as(i32, @intCast(row))) { - pane.vsel.row += @intCast(cnt); + t.cur_row += @intCast(cnt); // the line moved down; stay on it + if (t.vsel.active and t.vsel.row >= @as(i32, @intCast(row))) t.vsel.row += @intCast(cnt); + } else if (t.vsel.active and t.vsel.row > @as(i32, @intCast(row))) { + t.vsel.row += @intCast(cnt); } - pane.cur_pinned = true; - pane.ensureCursorVisible(); + t.cur_pinned = true; + t.pane().ensureCursorVisible(); } /// pair mapping for m-mode: either bracket names the pair; anything else @@ -1383,20 +1396,20 @@ fn pairFor(ch: u21) ?struct { o: u8, c: u8 } { }; } -pub fn textobjectSelect(p: *Pardes, pane: *Pane, text: []const u8, obj: u21, around: bool) void { - const range = pane.primaryRange(text, 0); +pub fn textobjectSelect(p: *Pardes, t: *Text, text: []const u8, obj: u21, around: bool) void { + const range = t.primaryRange(text, 0); switch (obj) { 'w', 'W' => { const r = modal.selectWord(text, range, around, obj == 'W'); - return pane.setRange(text, 0, r, false); + return t.setRange(text, 0, r, false); }, 'p' => { const r = modal.selectParagraph(text, range, around, 1); - return pane.setRange(text, 0, r, false); + return t.setRange(text, 0, r, false); }, else => {}, } - const lines = paneCursorLines(p, pane) catch return; + const lines = paneCursorLines(p, t) catch return; const cur = modal.positionAt(text, modal.selectionCursor(text, range)); const pair: ?modal.Range = switch (obj) { '\'', '"', '`' => modal.enclosingQuote(lines, cur, @intCast(obj)), @@ -1415,102 +1428,105 @@ pub fn textobjectSelect(p: *Pardes, pane: *Pane, text: []const u8, obj: u21, aro a = modal.nextGrapheme(text, a); head = modal.prevGrapheme(text, head); } - pane.setRange(text, 0, .{ .anchor = a, .head = head }, false); + t.setRange(text, 0, .{ .anchor = a, .head = head }, false); } /// `ms`: wrap the selection (or the cursor char) in a pair; the wrap /// including the pair becomes the selection -pub fn surroundAdd(p: *Pardes, pane: *Pane, ch: u21) void { +pub fn surroundAdd(p: *Pardes, t: *Text, ch: u21) void { const pr = pairFor(ch) orelse return; - pane.select = false; - const eb = editTextEol(p, pane, selRows(pane)) orelse return; - const r = selRange(pane, eb.text, eb.row0); - pushUndo(p, pane); + t.select = false; + const eb = editTextEol(p, t, selRows(t)) orelse return; + const r = selRange(t, eb.text, eb.row0); + pushUndo(p, t); const close_col = modal.nextGrapheme(modal.lineSlice(eb.text, r.b.row), r.b.col); var new = modal.insertAt(p.gpa, eb.text, .{ .row = r.b.row, .col = close_col }, &[1]u8{pr.c}) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); new = modal.insertAt(p.gpa, new, .{ .row = r.a.row, .col = r.a.col }, &[1]u8{pr.o}) catch return; - setEditText(p, pane, new); - pane.msel.active = false; - pane.vsel = .{ .active = true, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; - pane.cur_row = @as(i32, @intCast(r.b.row)) + eb.row0; - pane.cur_col = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0)); - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.ensureCursorVisible(); + setEditText(p, t, new); + t.msel.active = false; + t.vsel = .{ .active = true, .row = @as(i32, @intCast(r.a.row)) + eb.row0, .col = @intCast(r.a.col), .explicit = false }; + t.cur_row = @as(i32, @intCast(r.b.row)) + eb.row0; + t.cur_col = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0)); + t.cur_pinned = true; + t.sticky_col = -1; + t.pane().ensureCursorVisible(); } /// `md`: delete the enclosing pair's chars; the cursor maps through -pub fn surroundDelete(p: *Pardes, pane: *Pane, ch: u21) void { +pub fn surroundDelete(p: *Pardes, t: *Text, ch: u21) void { const pr = pairFor(ch) orelse return; - const lines = paneCursorLines(p, pane) catch return; - const cur = pane.toModalCursor(); + const lines = paneCursorLines(p, t) catch return; + const cur = t.toModalCursor(); const r = (if (pr.o == pr.c) modal.enclosingQuote(lines, cur, pr.o) else modal.enclosingPair(lines, cur, pr.o, pr.c)) orelse return; const lo: i32 = @intCast(@min(r.a.row, cur.row)); const hi: i32 = @intCast(@max(r.b.row, cur.row)); - const eb = editText(p, pane, lo, hi, -1) orelse return; + const eb = editText(p, t, lo, hi, -1) orelse return; const text = eb.text; const drow = -eb.row0; // surface row -> buffer row const ra: modal.Cursor = .{ .row = @intCast(@as(i32, @intCast(r.a.row)) + drow), .col = r.a.col }; const rb: modal.Cursor = .{ .row = @intCast(@as(i32, @intCast(r.b.row)) + drow), .col = r.b.col }; - pushUndo(p, pane); + pushUndo(p, t); const a_off = modal.offsetAt(text, ra); const b_off = modal.offsetAt(text, rb); const cur_off = modal.offsetAt(text, .{ .row = @intCast(@as(i32, @intCast(cur.row)) + drow), .col = cur.col }); // the close first, so the open's position stays valid var new = modal.deleteChar(p.gpa, text, rb) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); new = modal.deleteChar(p.gpa, new, ra) catch return; - setEditText(p, pane, new); + setEditText(p, t, new); var nc = cur_off; if (nc > b_off) nc -= 1; if (nc > a_off) nc -= 1; const cc = modal.positionAt(new, nc); - pane.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; - pane.cur_col = @intCast(cc.col); - pane.vsel.active = false; - pane.msel.active = false; - pane.cur_pinned = true; - pane.ensureCursorVisible(); + t.cur_row = @as(i32, @intCast(cc.row)) + eb.row0; + t.cur_col = @intCast(cc.col); + t.vsel.active = false; + t.msel.active = false; + t.cur_pinned = true; + t.pane().ensureCursorVisible(); } /// `mr`: swap the enclosing pair's chars for 's -pub fn surroundReplace(p: *Pardes, pane: *Pane, from: u21, to: u21) void { +pub fn surroundReplace(p: *Pardes, t: *Text, from: u21, to: u21) void { const fp = pairFor(from) orelse return; const tp = pairFor(to) orelse return; - const lines = paneCursorLines(p, pane) catch return; - const cur = pane.toModalCursor(); + const lines = paneCursorLines(p, t) catch return; + const cur = t.toModalCursor(); const r = (if (fp.o == fp.c) modal.enclosingQuote(lines, cur, fp.o) else modal.enclosingPair(lines, cur, fp.o, fp.c)) orelse return; - const eb = editText(p, pane, @as(i32, @intCast(r.a.row)), @as(i32, @intCast(r.b.row)), -1) orelse return; + const eb = editText(p, t, @as(i32, @intCast(r.a.row)), @as(i32, @intCast(r.b.row)), -1) orelse return; const drow = -eb.row0; // surface row -> buffer row const ar: usize = @intCast(@as(i32, @intCast(r.a.row)) + drow); const br: usize = @intCast(@as(i32, @intCast(r.b.row)) + drow); - pushUndo(p, pane); + pushUndo(p, t); const out = p.gpa.dupe(u8, eb.text) catch return; out[modal.lineStartOffset(out, ar) + r.a.col] = tp.o; out[modal.lineStartOffset(out, br) + r.b.col] = tp.c; - setEditText(p, pane, out); + setEditText(p, t, out); } -pub fn pushUndo(p: *Pardes, pane: *Pane) void { +pub fn pushUndo(p: *Pardes, t: *Text) void { + const pane = t.pane(); // one keystroke, one undo step — even when it edited at ten cursors. if (p.multi_on and !p.multi_first) return; if (pane.file != null) return panes.File.pushUndo(p, pane); panes.Terminal.pushUndo(p, pane); } -pub fn doUndo(p: *Pardes, pane: *Pane) void { +pub fn doUndo(p: *Pardes, t: *Text) void { + const pane = t.pane(); if (pane.file != null) return panes.File.undo(p, pane); panes.Terminal.undo(p, pane); } -pub fn doRedo(p: *Pardes, pane: *Pane) void { +pub fn doRedo(p: *Pardes, t: *Text) void { + const pane = t.pane(); if (pane.file != null) return panes.File.redo(p, pane); panes.Terminal.redo(p, pane); } @@ -1564,14 +1580,14 @@ test "insert newline adds one indent level after a closing call" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile(" callback({})\n"); - pane.mode = .insert; - pane.cur_col = 16; + pane.body.mode = .insert; + pane.body.cur_col = 16; p.update(.{ .key = .{ .cp = Key.enter } }); try std.testing.expectEqualSlices(u8, " callback({})\n \n", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); - try std.testing.expectEqual(@as(i32, 8), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 8), pane.body.cur_col); } test "insert and normal modes edit complete Unicode graphemes" { @@ -1580,41 +1596,41 @@ test "insert and normal modes edit complete Unicode graphemes" { defer p.deinit(); const pane = try p.setTestFile("a" ++ "e\u{301}" ++ "👩🏽\u{200d}🚀" ++ "界" ++ "z"); - pane.mode = .insert; - pane.cur_col = 22; // on z, after the CJK grapheme - insertKey(p, pane, .{ .cp = Key.left }); - try std.testing.expectEqual(@as(i32, 19), pane.cur_col); - insertKey(p, pane, .{ .cp = Key.left }); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); - insertKey(p, pane, .{ .cp = Key.right }); - try std.testing.expectEqual(@as(i32, 19), pane.cur_col); - insertKey(p, pane, .{ .cp = Key.backspace }); + pane.body.mode = .insert; + pane.body.cur_col = 22; // on z, after the CJK grapheme + insertKey(p, &pane.body, .{ .cp = Key.left }); + try std.testing.expectEqual(@as(i32, 19), pane.body.cur_col); + insertKey(p, &pane.body, .{ .cp = Key.left }); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); + insertKey(p, &pane.body, .{ .cp = Key.right }); + try std.testing.expectEqual(@as(i32, 19), pane.body.cur_col); + insertKey(p, &pane.body, .{ .cp = Key.backspace }); try std.testing.expectEqualStrings("ae\u{301}界z", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); - pane.mode = .normal; - pane.cur_col = 1; - normalDelete(p, pane, false); + pane.body.mode = .normal; + pane.body.cur_col = 1; + normalDelete(p, &pane.body, false); try std.testing.expectEqualStrings("a界z", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - normalReplaceChar(p, pane, 'λ'); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + normalReplaceChar(p, &pane.body, 'λ'); try std.testing.expectEqualStrings("aλz", pane.file.?.content); panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "界a")); - pane.cur_col = 3; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; - normalReplaceChar(p, pane, 'λ'); + pane.body.cur_col = 3; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + normalReplaceChar(p, &pane.body, 'λ'); try std.testing.expectEqualStrings("λλ", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 2), pane.cur_col); - try std.testing.expectEqual(@as(i32, 0), pane.vsel.col); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.vsel.col); panes.File.setContent(p, &pane.file.?, try gpa.dupe(u8, "界a")); - pane.cur_col = 0; - pane.vsel = .{ .active = true, .row = 0, .col = 3, .explicit = true }; - normalReplaceChar(p, pane, 'λ'); + pane.body.cur_col = 0; + pane.body.vsel = .{ .active = true, .row = 0, .col = 3, .explicit = true }; + normalReplaceChar(p, &pane.body, 'λ'); try std.testing.expectEqualStrings("λλ", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); - try std.testing.expectEqual(@as(i32, 2), pane.vsel.col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 2), pane.body.vsel.col); } test "flat text replacement preserves a terminal fragment origin and direction" { @@ -1622,23 +1638,23 @@ test "flat text replacement preserves a terminal fragment origin and direction" defer p.deinit(); p.update(.{ .output = .{ .pane = 0, .bytes = "first\r\nsecond\r\nthird\r\n界a\r\n" } }); const pane = p.panes[0].?; - pane.mode = .normal; - pane.cur_pinned = true; - pane.cur_row = 3; - pane.cur_col = 0; - pane.vsel = .{ .active = true, .row = 3, .col = 3, .explicit = true }; - normalReplaceChar(p, pane, 'λ'); + pane.body.mode = .normal; + pane.body.cur_pinned = true; + pane.body.cur_row = 3; + pane.body.cur_col = 0; + pane.body.vsel = .{ .active = true, .row = 3, .col = 3, .explicit = true }; + normalReplaceChar(p, &pane.body, 'λ'); try std.testing.expectEqual(@as(i32, 3), pane.ovl.?.row); try std.testing.expectEqualStrings("λλ", pane.ovl.?.text); - try std.testing.expectEqual(@as(i32, 3), pane.cur_row); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); - try std.testing.expectEqual(@as(i32, 3), pane.vsel.row); - try std.testing.expectEqual(@as(i32, 2), pane.vsel.col); - try std.testing.expect(pane.vsel.active and pane.vsel.explicit); - const lines = try paneCursorLines(p, pane); + try std.testing.expectEqual(@as(i32, 3), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 3), pane.body.vsel.row); + try std.testing.expectEqual(@as(i32, 2), pane.body.vsel.col); + try std.testing.expect(pane.body.vsel.active and pane.body.vsel.explicit); + const lines = try paneCursorLines(p, &pane.body); try std.testing.expectEqualStrings("first", lines[0]); try std.testing.expectEqualStrings("λλ", lines[3]); - const flat = try flatSurface(p, pane); + const flat = try flatSurface(p, &pane.body); try std.testing.expect(std.mem.startsWith(u8, flat, "first\nsecond\nthird\nλλ\n")); } -- cgit v1.3