summaryrefslogtreecommitdiff
path: root/src/edit.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 23:06:11 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit95a3a1bb822e583988bfe0f312d0ef219b27a454 (patch)
tree95306e8d3f36f8af05b3ea919f6cdef1e94a028b /src/edit.zig
parentc8f0c25a854b8e169db08a84d1fdcd915a70aaa0 (diff)
downloadpardes-95a3a1bb822e583988bfe0f312d0ef219b27a454.tar.gz
pardes-95a3a1bb822e583988bfe0f312d0ef219b27a454.zip
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 <[email protected]>
Diffstat (limited to 'src/edit.zig')
-rw-r--r--src/edit.zig1020
1 files changed, 518 insertions, 502 deletions
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<ch>`: 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<ch>`: 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<ch>`: 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<from><to>`: swap the enclosing <from> pair's chars for <to>'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"));
}