diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-27 23:06:11 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 95a3a1bb822e583988bfe0f312d0ef219b27a454 (patch) | |
| tree | 95306e8d3f36f8af05b3ea919f6cdef1e94a028b | |
| parent | c8f0c25a854b8e169db08a84d1fdcd915a70aaa0 (diff) | |
| download | pardes-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]>
| -rw-r--r-- | src/File.zig | 78 | ||||
| -rw-r--r-- | src/Mini.zig | 2 | ||||
| -rw-r--r-- | src/Output.zig | 22 | ||||
| -rw-r--r-- | src/Pipe.zig | 52 | ||||
| -rw-r--r-- | src/Terminal.zig | 114 | ||||
| -rw-r--r-- | src/Text.zig | 606 | ||||
| -rw-r--r-- | src/body_layer.zig | 44 | ||||
| -rw-r--r-- | src/colors.zig | 6 | ||||
| -rw-r--r-- | src/config.zig | 6 | ||||
| -rw-r--r-- | src/detached/client.zig | 6 | ||||
| -rw-r--r-- | src/detached/server.zig | 4 | ||||
| -rw-r--r-- | src/dump.zig | 4 | ||||
| -rw-r--r-- | src/edit.zig | 1020 | ||||
| -rw-r--r-- | src/exec.zig | 40 | ||||
| -rw-r--r-- | src/gui/gui.zig | 14 | ||||
| -rw-r--r-- | src/host_io.zig | 2 | ||||
| -rw-r--r-- | src/layout.zig | 2 | ||||
| -rw-r--r-- | src/look.zig | 106 | ||||
| -rw-r--r-- | src/macos.zig | 8 | ||||
| -rw-r--r-- | src/mouse.zig | 130 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 18 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 26 | ||||
| -rw-r--r-- | src/normal.zig | 464 | ||||
| -rw-r--r-- | src/panes.zig | 623 | ||||
| -rw-r--r-- | src/pardes.zig | 239 | ||||
| -rw-r--r-- | src/pdf_view.zig | 48 | ||||
| -rw-r--r-- | src/tty/tty.zig | 6 | ||||
| -rw-r--r-- | test/fs_bench.zig | 4 | ||||
| -rw-r--r-- | test/hxdiff.zig | 34 | ||||
| -rw-r--r-- | test/mode.zig | 28 | ||||
| -rw-r--r-- | test/output.zig | 128 | ||||
| -rw-r--r-- | test/panes.zig | 398 | ||||
| -rw-r--r-- | test/pdf.zig | 42 | ||||
| -rw-r--r-- | test/perf.zig | 78 |
34 files changed, 2224 insertions, 2178 deletions
diff --git a/src/File.zig b/src/File.zig index eb1c6a7c..239aa4ba 100644 --- a/src/File.zig +++ b/src/File.zig @@ -17,16 +17,17 @@ const Pane = panes.Pane; const Output = panes.Output; const Mini = panes.Mini; const Terminal = panes.Terminal; +const Text = panes.Text; const SYNTAX_CONTEXT_AFTER_ROWS: usize = 2; /// Content and primary selection at one file edit boundary. Keeping only the -/// primary avoids putting Pane.max_selections ranges in every history entry. +/// primary avoids putting Text.max_selections ranges in every history entry. pub const Snapshot = struct { content: []u8, cur_row: i32, cur_col: i32, - vsel: Pane.CharSel, + vsel: Text.CharSel, }; pub const History = struct { @@ -652,27 +653,28 @@ pub fn cursorLines(arena: std.mem.Allocator, pane: *Pane, f: *State) ![]const [] /// Use the file's line index only when `text` is its complete live content. /// Edit-buffer fragments and other temporary text retain modal's scan path. -fn contentIndex(pane: *Pane, text: []const u8) ?[]const usize { +fn contentIndex(t: *Text, text: []const u8) ?[]const usize { + const pane = t.pane(); const f = if (pane.file) |*file| file else return null; if (text.ptr != f.content.ptr or text.len != f.content.len) return null; return lineIndex(pane.gpa, f) catch null; } -pub fn textOffset(pane: *Pane, text: []const u8, cursor: modal.Cursor) usize { - const index = contentIndex(pane, text) orelse return modal.offsetAt(text, cursor); +pub fn textOffset(t: *Text, text: []const u8, cursor: modal.Cursor) usize { + const index = contentIndex(t, text) orelse return modal.offsetAt(text, cursor); const row = @min(cursor.row, index.len - 1); const start = index[row]; const end = if (row + 1 < index.len) index[row + 1] - 1 else text.len; return start + modal.graphemeStart(text[start..end], @min(cursor.col, end - start)); } -pub fn textLineStart(pane: *Pane, text: []const u8, row: usize) usize { - const index = contentIndex(pane, text) orelse return modal.lineStartOffset(text, row); +pub fn textLineStart(t: *Text, text: []const u8, row: usize) usize { + const index = contentIndex(t, text) orelse return modal.lineStartOffset(text, row); return if (row >= index.len) text.len else index[row]; } -pub fn textLine(pane: *Pane, text: []const u8, row: usize) []const u8 { - const index = contentIndex(pane, text) orelse return modal.lineSlice(text, row); +pub fn textLine(t: *Text, text: []const u8, row: usize) []const u8 { + const index = contentIndex(t, text) orelse return modal.lineSlice(text, row); if (row >= index.len) return ""; const start = index[row]; const end = if (row + 1 < index.len) index[row + 1] - 1 else text.len; @@ -686,7 +688,7 @@ test "indexed text rows preserve fragment and allocation failure semantics" { const pane = try p.setTestFile("first\n\tλ界\n\nlast\n"); const text = pane.file.?.content; allocator.fail_index = allocator.alloc_index; - for (0..lineCount(text) + 2) |row| try std.testing.expectEqualStrings(modal.lineSlice(text, row), textLine(pane, text, row)); + for (0..lineCount(text) + 2) |row| try std.testing.expectEqualStrings(modal.lineSlice(text, row), textLine(&pane.body, text, row)); try std.testing.expectEqual(@as(usize, 0), pane.file.?.line_starts.len); allocator.fail_index = std.math.maxInt(usize); _ = try lineIndex(p.gpa, &pane.file.?); @@ -695,19 +697,19 @@ test "indexed text rows preserve fragment and allocation failure semantics" { for ([_][]const u8{ text, text[2..], text[0..8], "other\nrows\n\n", "" }) |fragment| { for (0..lineCount(fragment) + 2) |row| try std.testing.expectEqualStrings( modal.lineSlice(fragment, row), - textLine(pane, fragment, row), + textLine(&pane.body, fragment, row), ); } try std.testing.expectEqual(allocations, allocator.allocations); } -pub fn textLineCount(pane: *Pane, text: []const u8) usize { - const index = contentIndex(pane, text) orelse return modal.cursorLineCount(text); +pub fn textLineCount(t: *Text, text: []const u8) usize { + const index = contentIndex(t, text) orelse return modal.cursorLineCount(text); return index.len; } -pub fn textPosition(pane: *Pane, text: []const u8, offset: usize) modal.Cursor { - const index = contentIndex(pane, text) orelse return modal.positionAt(text, offset); +pub fn textPosition(t: *Text, text: []const u8, offset: usize) modal.Cursor { + const index = contentIndex(t, text) orelse return modal.positionAt(text, offset); const bounded = @min(offset, text.len); const row = std.sort.upperBound(usize, index, bounded, struct { fn cmp(key: usize, item: usize) std.math.Order { @@ -734,8 +736,8 @@ pub fn open(p: *Pardes, id: usize, path: []const u8, line: usize) !*Pane { const total = lineCount(content); const scroll: usize = if (line > 0 and line <= total) line - 1 else 0; pane.file = .{ .path = path_copy, .content = content, .scroll = scroll, .history = history }; - pane.cur_pinned = true; - pane.cur_row = @intCast(scroll); + pane.body.cur_pinned = true; + pane.body.cur_row = @intCast(scroll); p.installPane(id, pane); if (filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } }); return pane; @@ -794,8 +796,8 @@ pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane { .history = history, }; pane.file.?.tree_context = saved.tree_context and supportsContext(pane); - pane.cur_pinned = true; - pane.cur_row = @intCast(src.scroll); + pane.body.cur_pinned = true; + pane.body.cur_row = @intCast(src.scroll); pane.cols = @max(1, src.cols); pane.rows = @max(1, src.rows); if (output == null and filesystem.localPath(path) != null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true, .mode = .baseline_disk } }); @@ -858,12 +860,12 @@ pub fn restoreSnap(pane: *Pane, f: *State, snap: Snapshot) void { const n = nlines(pane.gpa, f); const row: usize = @min(@as(usize, @intCast(@max(0, snap.cur_row))), n - 1); const llen = modal.lineSlice(f.content, row).len; - pane.cur_row = @intCast(row); - pane.cur_col = @intCast(@min(@as(usize, @intCast(@max(0, snap.cur_col))), llen)); - pane.vsel = snap.vsel; - pane.msel.active = false; - pane.cur_pinned = true; - pane.sticky_col = -1; + pane.body.cur_row = @intCast(row); + pane.body.cur_col = @intCast(@min(@as(usize, @intCast(@max(0, snap.cur_col))), llen)); + pane.body.vsel = snap.vsel; + pane.body.msel.active = false; + pane.body.cur_pinned = true; + pane.body.sticky_col = -1; pane.ensureCursorVisible(); } @@ -883,9 +885,9 @@ pub fn pushUndo(p: *Pardes, pane: *Pane) void { if (history.undo_len > 0 and std.mem.eql(u8, history.undo[history.undo_len - 1].content, f.content)) return; const snap: Snapshot = .{ .content = p.gpa.dupe(u8, f.content) catch return, - .cur_row = pane.cur_row, - .cur_col = pane.cur_col, - .vsel = pane.vsel, + .cur_row = pane.body.cur_row, + .cur_col = pane.body.cur_col, + .vsel = pane.body.vsel, }; pushHistory(p.gpa, &history.undo, &history.undo_len, snap); for (history.redo[0..history.redo_len]) |item| p.gpa.free(item.content); @@ -898,9 +900,9 @@ pub fn undo(p: *Pardes, pane: *Pane) void { if (history.undo_len == 0) return; const current: Snapshot = .{ .content = p.gpa.dupe(u8, f.content) catch return, - .cur_row = pane.cur_row, - .cur_col = pane.cur_col, - .vsel = pane.vsel, + .cur_row = pane.body.cur_row, + .cur_col = pane.body.cur_col, + .vsel = pane.body.vsel, }; pushHistory(p.gpa, &history.redo, &history.redo_len, current); history.undo_len -= 1; @@ -915,9 +917,9 @@ pub fn redo(p: *Pardes, pane: *Pane) void { if (history.redo_len == 0) return; const current: Snapshot = .{ .content = p.gpa.dupe(u8, f.content) catch return, - .cur_row = pane.cur_row, - .cur_col = pane.cur_col, - .vsel = pane.vsel, + .cur_row = pane.body.cur_row, + .cur_col = pane.body.cur_col, + .vsel = pane.body.vsel, }; pushHistory(p.gpa, &history.undo, &history.undo_len, current); history.redo_len -= 1; @@ -939,9 +941,9 @@ pub fn changed(p: *Pardes, id: u8, bytes: []const u8) void { // restoreSnap only consumes cursor/selection from this synthetic snapshot. restoreSnap(pane, f, .{ .content = undefined, - .cur_row = pane.cur_row, - .cur_col = pane.cur_col, - .vsel = pane.vsel, + .cur_row = pane.body.cur_row, + .cur_col = pane.body.cur_col, + .vsel = pane.body.vsel, }); } @@ -1126,7 +1128,7 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod const prefix_width = gutterWidth(pane); const goff = pane.scroll(); const gcur = Terminal.gridCursor(pane); - const gcrow = if (pane.cur_pinned) pane.cur_row else @as(i32, gcur.y) + goff; + const gcrow = if (pane.body.cur_pinned) pane.body.cur_row else @as(i32, gcur.y) + goff; const cur_line: i32 = if (active and !pane.tag_edit) gcrow else std.math.minInt(i32); // the body's first row, the way renderPane derives it (Tagbottom) const body_y = p.bodyTop(r); diff --git a/src/Mini.zig b/src/Mini.zig index 52e26a37..e118e738 100644 --- a/src/Mini.zig +++ b/src/Mini.zig @@ -187,7 +187,7 @@ pub fn open(p: *Pardes, id: usize, argument: []const u8) !void { .history = history, .syntax_dirty = false, }; - pane.cur_pinned = true; + pane.body.cur_pinned = true; exec.placeDoc(p, id, free, pane); p.active = free; } diff --git a/src/Output.zig b/src/Output.zig index 6df2fd34..6e1765a7 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -402,7 +402,7 @@ pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u errdefer p.gpa.destroy(history); const pane = try p.newDocPane(id); pane.file = .{ .path = path, .content = content, .output = out, .history = history }; - pane.cur_pinned = true; + pane.body.cur_pinned = true; return pane; } @@ -737,20 +737,20 @@ pub fn openErrors(p: *Pardes, id: usize, content: []u8) !void { pub fn resetBody(p: *Pardes, pane: *Pane) void { pane.file.?.scroll = 0; - pane.cur_row = 0; - pane.cur_col = 0; - pane.cur_pinned = true; + pane.body.cur_row = 0; + pane.body.cur_col = 0; + pane.body.cur_pinned = true; pane.hscroll = 0; pane.wrap_n = 0; - pane.msel = .{}; - pane.vsel = .{}; - pane.nsel = 0; + pane.body.msel = .{}; + pane.body.vsel = .{}; + pane.body.nsel = 0; pane.nsel_snap = 0; - pane.select = false; + pane.body.select = false; pane.sel = @splat(.{}); - pane.sticky_col = -1; - pane.append_at = null; - pane.normal.clear(); + pane.body.sticky_col = -1; + pane.body.append_at = null; + pane.body.normal.clear(); pane.look_at = null; if (p.look_hover_wait) |wait| if (wait.serial == pane.serial) { p.look_hover_wait = null; diff --git a/src/Pipe.zig b/src/Pipe.zig index c01098ff..c2b49f31 100644 --- a/src/Pipe.zig +++ b/src/Pipe.zig @@ -45,7 +45,7 @@ pub const PendingPipe = struct { command: []u8, cwd: []u8, inputs: []selection_pipe.Input, - ranges: [Pane.max_selections]modal.Selection, + ranges: [panes.Text.max_selections]modal.Selection, primary: u8, explicit: bool, how: modal.Normal.PipeBehavior, @@ -84,8 +84,8 @@ pub fn submitPipe(p: *Pardes, id: usize) void { const command = armed[marker.len..]; if (command.len == 0) return; - var ranges: [Pane.max_selections]modal.Selection = undefined; - const got = pane.ranges(f.content, 0, &ranges); + var ranges: [panes.Text.max_selections]modal.Selection = undefined; + const got = pane.body.ranges(f.content, 0, &ranges); // `!`/`A-!` take no stdin and run ONCE — see `PendingPipe.nranges`. const ninputs = if (prompt.how.pipes()) got.n else 1; const inputs = p.gpa.alloc(selection_pipe.Input, ninputs) catch return; @@ -130,7 +130,7 @@ pub fn submitPipe(p: *Pardes, id: usize) void { .inputs = inputs, .ranges = ranges, .primary = @intCast(got.pri), - .explicit = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active, + .explicit = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active, .how = prompt.how, .nranges = @intCast(got.n), }; @@ -255,7 +255,7 @@ pub fn pipeResponse( const final_len = std.math.add(usize, kept, total_output) catch return; const replacement = p.gpa.alloc(u8, final_len) catch return; - var new_ranges: [Pane.max_selections]modal.Selection = undefined; + var new_ranges: [panes.Text.max_selections]modal.Selection = undefined; var read_at: usize = 0; var write_at: usize = 0; for (0..n) |i| { @@ -277,10 +277,10 @@ pub fn pipeResponse( @memcpy(replacement[write_at..], f.content[read_at..]); // One async request is one history transaction, even at 64 cursors. - edit.pushUndo(p, pane); + edit.pushUndo(p, &pane.body); panes.File.setContent(p, f, replacement); - pane.setRanges(f.content, new_ranges[0..n], &.{}, wait.primary, wait.explicit); - pane.select = false; + pane.body.setRanges(f.content, new_ranges[0..n], &.{}, wait.primary, wait.explicit); + pane.body.select = false; pane.ensureCursorVisible(); } @@ -290,9 +290,9 @@ test "selection pipe prompt submits exact request and Escape cancels" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("one\ntwo\n"); - pane.cur_row = 0; - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_row = 0; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; p.update(.{ .key = .{ .cp = '|' } }); try std.testing.expect(pane.tag_edit and pane.prompt == .pipe); @@ -327,7 +327,7 @@ test "selection pipe replaces all ranges atomically and undo restores them" { .{ .anchor = 0, .head = 2 }, .{ .anchor = 6, .head = 8 }, }; - pane.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); + pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); p.update(.{ .key = .{ .cp = '|' } }); p.update(.{ .key = .{ .cp = 'c', .text = "cat" } }); @@ -345,9 +345,9 @@ test "selection pipe replaces all ranges atomically and undo restores them" { p.update(.{ .pipe_resp = .{ .id = id, .success = true, .outputs = outputs } }); try std.testing.expectEqualSlices(u8, "AA bb \n", pane.file.?.content); try std.testing.expectEqual(@as(usize, 1), pane.file.?.history.undo_len); - try std.testing.expectEqual(@as(u8, 1), pane.nsel); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - try std.testing.expectEqual(@as(i32, 6), pane.sels[0].col); + try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 6), pane.body.sels[0].col); try std.testing.expect(!denied.has_induced_failure); p.update(.{ .key = .{ .cp = 'u' } }); @@ -378,7 +378,7 @@ test "the four shell behaviours put their output where helix puts it" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("aa bb cc\n"); - pane.setRanges(pane.file.?.content, &.{.{ .anchor = 3, .head = 5 }}, &.{}, 0, true); + pane.body.setRanges(pane.file.?.content, &.{.{ .anchor = 3, .head = 5 }}, &.{}, 0, true); p.update(.{ .key = case.key }); for ("cmd") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } }); @@ -399,7 +399,7 @@ test "a command with no stdin runs once and every cursor gets that one answer" { .{ .anchor = 0, .head = 2 }, .{ .anchor = 6, .head = 8 }, }; - pane.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); + pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); p.update(.{ .key = .{ .cp = '!' } }); // insert-output: no stdin for ("date") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } }); @@ -421,8 +421,8 @@ test "selection pipe failure and stale completion never mutate the file" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("abc\n"); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; p.update(.{ .key = .{ .cp = '|' } }); p.update(.{ .key = .{ .cp = 'c', .text = "cat" } }); @@ -448,8 +448,8 @@ test "a failed filter opens an errors buffer carrying the command's own words" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = try p.setTestFile("abc\n"); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; p.update(.{ .key = .{ .cp = '|' } }); for ("trr") |c| p.update(.{ .key = .{ .cp = c, .text = &.{c} } }); @@ -491,8 +491,8 @@ test "selection pipe rejects a reused pane slot and a superseded request" { defer p.deinit(); while (p.nextEffect()) |_| {} var pane = try p.setTestFile("old\n"); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; p.update(.{ .key = .{ .cp = '|' } }); p.update(.{ .key = .{ .cp = 'c', .text = "cat" } }); @@ -503,8 +503,8 @@ test "selection pipe rejects a reused pane slot and a superseded request" { try std.testing.expectEqualSlices(u8, "new\n", pane.file.?.content); try std.testing.expectEqual(@as(usize, 0), pane.file.?.history.undo_len); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; p.update(.{ .key = .{ .cp = '|' } }); p.update(.{ .key = .{ .cp = 'c', .text = "cat" } }); p.update(.{ .key = .{ .cp = Key.enter } }); @@ -533,7 +533,7 @@ test "selection pipe binding is file-normal-only" { p.update(.{ .key = .{ .cp = '|' } }); try std.testing.expect(pane.prompt != .pipe); pane.file.?.output = null; - pane.mode = .insert; + pane.body.mode = .insert; p.update(.{ .key = .{ .cp = '|', .text = "|" } }); try std.testing.expect(pane.prompt != .pipe); try std.testing.expectEqualSlices(u8, "|x", pane.file.?.content); diff --git a/src/Terminal.zig b/src/Terminal.zig index cb752613..fbe7bfc0 100644 --- a/src/Terminal.zig +++ b/src/Terminal.zig @@ -200,7 +200,7 @@ pub const Snapshot = struct { ovl: ?EditBuffer, cur_row: i32, cur_col: i32, - vsel: Pane.CharSel, + vsel: panes.Text.CharSel, }; pub const PendingCommand = struct { @@ -677,16 +677,16 @@ pub fn feedOutput(p: *Pardes, pane: *Pane, bytes: []const u8) void { // There are no pty reads at all without an emulator to parse them into. if (comptime !enabled) return; if (pane.terminal == null) return; - const has_positions = pane.ovl != null or pane.cur_pinned or pane.vsel.active or - pane.msel.active or pane.nsel > 0 or pane.append_at != null or pane.look_at != null or - pane.ed_undo_len > 0 or pane.ed_redo_len > 0 or pointerRowCount(pane) > 0; + const has_positions = pane.ovl != null or pane.body.cur_pinned or pane.body.vsel.active or + pane.body.msel.active or pane.body.nsel > 0 or pane.body.append_at != null or pane.look_at != null or + pane.body.ed_undo_len > 0 or pane.body.ed_redo_len > 0 or pointerRowCount(pane) > 0; // Sticky bottom, as every other terminal does it: output drags the // viewport down only when the viewport was already on the last row, so // a reader scrolled back into the scrollback stays where they scrolled. const at_bottom = atBottom(pane); if (has_positions) ingestWithPositions(pane, bytes) else ingest(pane, bytes); p.shell_rows.markStale(pane); - const body_mode = if (pane.tag_edit) pane.tag_mode else pane.mode; + const body_mode = if (pane.tag_edit) pane.tag_mode else pane.body.mode; if (body_mode == .tty and at_bottom) followOutput(pane); } @@ -752,15 +752,15 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void { const key = screens.active_key; const generation = screens.generation(key); const pages = &screens.active.pages; - const Group = struct { overlay: ?*EditBuffer, cursor: *i32, selection: *Pane.CharSel }; + const Group = struct { overlay: ?*EditBuffer, cursor: *i32, selection: *panes.Text.CharSel }; var groups: [history_max * 2 + 1]Group = undefined; groups[0] = .{ .overlay = if (pane.ovl) |*overlay| overlay else null, - .cursor = &pane.cur_row, - .selection = &pane.vsel, + .cursor = &pane.body.cur_row, + .selection = &pane.body.vsel, }; var ngroups: usize = 1; - for ([_][]Snapshot{ pane.ed_undo[0..pane.ed_undo_len], pane.ed_redo[0..pane.ed_redo_len] }) |history| { + for ([_][]Snapshot{ pane.body.ed_undo[0..pane.body.ed_undo_len], pane.body.ed_redo[0..pane.body.ed_redo_len] }) |history| { for (history) |*snapshot| { groups[ngroups] = .{ .overlay = if (snapshot.ovl) |*overlay| overlay else null, @@ -773,7 +773,7 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void { const Span = struct { overlay: *EditBuffer, start: RowPin, end: RowPin }; var spans: [groups.len]Span = undefined; var nspans: usize = 0; - var positions: [groups.len * 2 + Pane.max_selections * 2 + 4]PositionPin = undefined; + var positions: [groups.len * 2 + panes.Text.max_selections * 2 + 4]PositionPin = undefined; var npositions: usize = 0; var failed = false; for (groups[0..ngroups]) |group| { @@ -793,19 +793,19 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void { } } const overlay = groups[0].overlay; - var extra: [Pane.max_selections * 2 + 4]*i32 = undefined; + var extra: [panes.Text.max_selections * 2 + 4]*i32 = undefined; var nextra: usize = 0; - if (pane.msel.active) { - extra[nextra] = &pane.msel.r0; - extra[nextra + 1] = &pane.msel.r1; + if (pane.body.msel.active) { + extra[nextra] = &pane.body.msel.r0; + extra[nextra + 1] = &pane.body.msel.r1; nextra += 2; } - for (pane.sels[0..pane.nsel]) |*selection| { + for (pane.body.sels[0..pane.body.nsel]) |*selection| { extra[nextra] = &selection.row; extra[nextra + 1] = &selection.arow; nextra += 2; } - if (pane.append_at) |*at| { + if (pane.body.append_at) |*at| { extra[nextra] = &at.row; nextra += 1; } @@ -883,7 +883,7 @@ test "raw terminal output needs no position tracking allocations" { const p = try Pardes.init(failing.allocator(), .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; feedOutput(p, pane, "warm"); const allocations = failing.alloc_index; failing.fail_index = allocations; @@ -1045,13 +1045,13 @@ pub fn forwardKey(p: *Pardes, id: usize, key: Key) void { pub fn enterTty(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; const selected = pane.rawPointerText(pane.sel[@intFromEnum(config.select_button)]) != null; - if (comptime enabled) if (!selected and pane.terminal != null and pane.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: { + if (comptime enabled) if (!selected and pane.terminal != null and pane.body.cur_pinned and pane.terminal.?.vt.cursorIsAtPrompt()) handoff: { const screen = pane.terminal.?.vt.screens.active; const goff: i32 = @intCast(screen.pages.scrollbar().offset); - const vp_row = pane.gridRow(pane.cur_row) - goff; + const vp_row = pane.gridRow(pane.body.cur_row) - goff; if (vp_row < 0) break :handoff; - var grid_col: i32 = @max(0, pane.cur_col); + var grid_col: i32 = @max(0, pane.body.cur_col); if (screen.pages.pin(.{ .viewport = .{ .x = 0, .y = @intCast(vp_row) } })) |row_pin| { if (row_pin.rowAndCell().row.semantic_prompt != .none) switch (promptCut(row_pin)) { .cut => |cols| grid_col += @intCast(cols), @@ -1070,18 +1070,18 @@ pub fn enterTty(p: *Pardes, id: usize) void { for (0..moves.right) |_| p.emitWrite(id, "\x1b[C"); }; - pane.mode = .tty; + pane.body.mode = .tty; followOutput(pane); // entering raw mode is a request for the live shell - pane.msel.active = false; - pane.vsel.active = false; - pane.nsel = 0; + pane.body.msel.active = false; + pane.body.vsel.active = false; + pane.body.nsel = 0; // The raw renderer follows the live cursor. A captured mouse selection // keeps its modal endpoint for the next return to editor mode. - pane.cur_pinned = selected; - pane.select = false; - pane.append_at = null; - pane.sticky_col = -1; - pane.normal.clear(); + pane.body.cur_pinned = selected; + pane.body.select = false; + pane.body.append_at = null; + pane.body.sticky_col = -1; + pane.body.normal.clear(); } // Returned slices remain valid until the next cache rebuild or reset. @@ -1397,7 +1397,7 @@ pub fn dumpPane( var skip: i32 = 0; while (lines.next()) |raw| : (row += 1) { // Hidden overlay rows still consume prompt pins to keep them aligned. - const prompt = if (pane.mode != .tty) prompts.next() else null; + const prompt = if (pane.body.mode != .tty) prompts.next() else null; if (skip > 0) { skip -= 1; continue; @@ -1406,7 +1406,7 @@ pub fn dumpPane( stream[len] = '\n'; len += 1; } - if (pane.mode != .tty) if (pane.ovl) |overlay| if (row == overlay.row) { + if (pane.body.mode != .tty) if (pane.ovl) |overlay| if (row == overlay.row) { @memcpy(stream[len..][0..overlay.text.len], overlay.text); len += overlay.text.len; skip = overlay.rows - 1; @@ -1493,7 +1493,7 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane) ![]const u8 { var lines = std.mem.splitScalar(u8, raw, '\n'); var n: usize = 0; while (lines.next()) |ln| { - vp[n] = if (pane.mode != .tty) + vp[n] = if (pane.body.mode != .tty) if (prompts.next()) |pin| if (pin.rowAndCell().row.semantic_prompt != .none) promptRow(pin, ln) else ln else @@ -1535,7 +1535,7 @@ const BodyWalk = struct { return .{ .pane = pane, .goff = goff, - .g = if (pane.mode == .tty) goff else pane.gridRow(off), + .g = if (pane.body.mode == .tty) goff else pane.gridRow(off), .skip = if (pane.ovl) |o| @intCast(@max(0, off - pane.surfRow(o.row))) else 0, }; } @@ -1562,7 +1562,7 @@ const BodyWalk = struct { w.lines = null; continue; } - if (w.pane.mode != .tty) if (w.pane.ovl) |o| if (w.g == o.row) { + if (w.pane.body.mode != .tty) if (w.pane.ovl) |o| if (w.g == o.row) { w.lines = std.mem.splitScalar(u8, o.text, '\n'); w.covered = o.rows; w.line_idx = 0; @@ -1759,7 +1759,7 @@ pub fn recolorAnsi(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bo const vi = anchor.row; if (vi < 0 or vi >= vp_rows) continue; const row_pin = pages.pin(.{ .viewport = .{ .y = @intCast(vi) } }) orelse continue; - const cut: u16 = if (pane.mode == .tty) 0 else cut: { + const cut: u16 = if (pane.body.mode == .tty) 0 else cut: { if (row_pin.rowAndCell().row.semantic_prompt == .none) break :cut 0; break :cut switch (promptCut(row_pin)) { .keep => 0, @@ -2388,13 +2388,13 @@ test "tty ansi colors follow the prompt hug into normal mode" { const blue: pardes.Color = .{ .rgb = p.theme().palette.?[4] }; // tty mode projects the emulator's ansi colours cell for cell. - pane.mode = .tty; + pane.body.mode = .tty; p.shell_rows.stale = true; const tty = try p.render(frame.allocator()); try testing.expectEqual(red, tty.at(tx + 2, body_y).style.fg); try testing.expectEqual(blue, tty.at(tx + 3, body_y).style.fg); - pane.mode = .normal; + pane.body.mode = .normal; p.shell_rows.stale = true; _ = frame.reset(.retain_capacity); const norm = try p.render(frame.allocator()); @@ -2411,7 +2411,7 @@ test "a prompt row hidden end to end paints nothing at all" { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;A\x1b\\\x1b[32maaaaaaaaa\x1b]133;B\x1b\\\x1b[41;36m\u{754C}\x1b[0m\r\n" } }); var frame = std.heap.ArenaAllocator.init(testing.allocator); @@ -2467,7 +2467,7 @@ pub fn padOutputBelowEdits(p: *Pardes, id: usize) void { pub fn snap(p: *Pardes, pane: *Pane) ?Snapshot { var ovl: ?EditBuffer = null; if (pane.ovl) |o| ovl = .{ .row = o.row, .rows = o.rows, .text = p.gpa.dupe(u8, o.text) catch return null }; - return .{ .ovl = ovl, .cur_row = pane.cur_row, .cur_col = pane.cur_col, .vsel = pane.vsel }; + return .{ .ovl = ovl, .cur_row = pane.body.cur_row, .cur_col = pane.body.cur_col, .vsel = pane.body.vsel }; } /// undo/redo restores the selection recorded with the snapshot (helix @@ -2475,11 +2475,11 @@ pub fn snap(p: *Pardes, pane: *Pane) ?Snapshot { pub fn restoreSnap(p: *Pardes, pane: *Pane, s: Snapshot) void { if (pane.ovl) |o| p.gpa.free(o.text); pane.ovl = s.ovl; - pane.cur_row = s.cur_row; - pane.cur_col = s.cur_col; - pane.cur_pinned = true; - pane.vsel = s.vsel; - pane.msel.active = false; + pane.body.cur_row = s.cur_row; + pane.body.cur_col = s.cur_col; + pane.body.cur_pinned = true; + pane.body.vsel = s.vsel; + pane.body.msel.active = false; pane.ensureCursorVisible(); } @@ -2495,32 +2495,32 @@ fn pushHistory(gpa: std.mem.Allocator, slots: []Snapshot, len: *usize, value: Sn pub fn pushUndo(p: *Pardes, pane: *Pane) void { const current = pane.ovl orelse EditBuffer{ .rows = 0 }; - if (pane.ed_undo_len > 0) { - const top = pane.ed_undo[pane.ed_undo_len - 1]; + if (pane.body.ed_undo_len > 0) { + const top = pane.body.ed_undo[pane.body.ed_undo_len - 1]; const same = if (top.ovl) |overlay| pane.ovl != null and overlay.row == current.row and overlay.rows == current.rows and std.mem.eql(u8, overlay.text, current.text) else pane.ovl == null; if (same) return; } const value = snap(p, pane) orelse return; - pushHistory(p.gpa, &pane.ed_undo, &pane.ed_undo_len, value); - for (pane.ed_redo[0..pane.ed_redo_len]) |item| if (item.ovl) |overlay| p.gpa.free(overlay.text); - pane.ed_redo_len = 0; + pushHistory(p.gpa, &pane.body.ed_undo, &pane.body.ed_undo_len, value); + for (pane.body.ed_redo[0..pane.body.ed_redo_len]) |item| if (item.ovl) |overlay| p.gpa.free(overlay.text); + pane.body.ed_redo_len = 0; } pub fn undo(p: *Pardes, pane: *Pane) void { - if (pane.ed_undo_len == 0) return; + if (pane.body.ed_undo_len == 0) return; const current = snap(p, pane) orelse return; - pushHistory(p.gpa, &pane.ed_redo, &pane.ed_redo_len, current); - pane.ed_undo_len -= 1; - restoreSnap(p, pane, pane.ed_undo[pane.ed_undo_len]); + pushHistory(p.gpa, &pane.body.ed_redo, &pane.body.ed_redo_len, current); + pane.body.ed_undo_len -= 1; + restoreSnap(p, pane, pane.body.ed_undo[pane.body.ed_undo_len]); } pub fn redo(p: *Pardes, pane: *Pane) void { - if (pane.ed_redo_len == 0) return; + if (pane.body.ed_redo_len == 0) return; const current = snap(p, pane) orelse return; - pushHistory(p.gpa, &pane.ed_undo, &pane.ed_undo_len, current); - pane.ed_redo_len -= 1; - restoreSnap(p, pane, pane.ed_redo[pane.ed_redo_len]); + pushHistory(p.gpa, &pane.body.ed_undo, &pane.body.ed_undo_len, current); + pane.body.ed_redo_len -= 1; + restoreSnap(p, pane, pane.body.ed_redo[pane.body.ed_redo_len]); } pub fn ptyReport(handler: *ghostty_vt.TerminalStream.Handler, data: [:0]const u8) void { diff --git a/src/Text.zig b/src/Text.zig new file mode 100644 index 00000000..ad5aa186 --- /dev/null +++ b/src/Text.zig @@ -0,0 +1,606 @@ +//! One editable text, acme's Text (editors/acme/dat.h:171-190, text.c): the +//! cursor, the selections and the modal state that edit it, and the undo of +//! an edit buffer. A pane's body is one. Where its characters come from -- a +//! file's content, a terminal's edit overlay, a PDF's text -- is the window's +//! business (edit.zig's editText and flatSurface), the way acme's Text reads +//! its File; everything here works on the flat text it is handed. +const std = @import("std"); +const panes = @import("panes.zig"); +const modal = @import("modal.zig"); +const config = @import("config.zig"); +const Pane = panes.Pane; +const File = panes.File; +const Terminal = panes.Terminal; +const Text = @This(); + +pub const Mode = enum { normal, insert, tty }; + +/// A modal line selection (helix `x`): whole rows [r0, r1], absolute. +pub const LineSel = struct { + active: bool = false, + r0: i32 = 0, + r1: i32 = 0, +}; + +pub const CharSel = struct { + active: bool = false, + row: i32 = 0, + col: i32 = 0, + explicit: bool = false, +}; + +pub const max_selections = 64; + +pub const SelRange = struct { + row: i32, + col: i32, + arow: i32, + acol: i32, + /// this range's own j/k goal column (helix Range::old_visual_position); + /// the PRIMARY's copy is Text.sticky_col + sticky: i32 = -1, +}; + +mode: Mode = .normal, +cur_pinned: bool = false, +cur_row: i32 = 0, +cur_col: i32 = 0, +msel: LineSel = .{}, +vsel: CharSel = .{}, +sels: [max_selections - 1]SelRange = undefined, +nsel: u8 = 0, +select: bool = false, +/// sticky goal column for j/k runs (helix old_visual_position): any +/// non-vertical range write resets it to -1. +sticky_col: i32 = -1, +append_at: ?struct { row: i32, col: i32 } = null, +normal: modal.Normal.State = .{}, +/// last f/F/t/T motion, for Alt-. repeat +find_op: u8 = 0, +find_ch: u21 = 0, +ed_undo: [Terminal.history_max]Terminal.Snapshot = undefined, +ed_undo_len: usize = 0, +ed_redo: [Terminal.history_max]Terminal.Snapshot = undefined, +ed_redo_len: usize = 0, + +/// The window this text belongs to (acme's t->w). +pub fn pane(t: *Text) *Pane { + return @alignCast(@fieldParentPtr("body", t)); +} + +pub fn toModalCursor(t: *Text) modal.Cursor { + return .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }; +} + +pub fn fromModalCursor(t: *Text, c: modal.Cursor) void { + t.cur_row = @as(i32, @intCast(c.row)); + t.cur_col = @intCast(c.col); + t.cur_pinned = true; +} + +pub fn insertVerticalCursor(lines: []const []const u8, c: modal.Cursor, down: bool) modal.Cursor { + if (lines.len == 0) return c; + const row = if (down) @min(c.row + 1, lines.len - 1) else c.row -| 1; + const target = lines[row]; + if (target.len == 0) return .{ .row = row, .col = 0 }; + const source = if (c.row < lines.len) lines[c.row] else ""; + const goal = File.rawDisplayCol(source, c.col); + const mapped = File.rawAtDisplay(target, goal); + const last = modal.prevGrapheme(target, target.len); + return .{ .row = row, .col = modal.graphemeStart(target, @min(mapped, last)) }; +} + +pub fn primaryRange(t: *Text, text: []const u8, row0: i32) modal.Selection { + const c = File.textOffset(t, text, .{ .row = @intCast(@max(0, t.cur_row - row0)), .col = @intCast(@max(0, t.cur_col)) }); + if (t.msel.active) { + // legacy line selection (file-search results highlight): linewise + 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 s = modal.lineStartOffset(text, r0); + const e = if (r1 + 1 >= modal.cursorLineCount(text)) text.len else modal.lineStartOffset(text, r1 + 1); + return .{ .anchor = s, .head = @max(e, modal.nextGrapheme(text, c)) }; + } + if (t.vsel.active) return cellRange(text, t.vsel.row - row0, t.vsel.col, t.cur_row - row0, t.cur_col); + return .{ .anchor = c, .head = modal.nextGrapheme(text, c) }; +} + +pub fn cellRange(text: []const u8, arow: i32, acol: i32, hrow: i32, hcol: i32) modal.Selection { + const a = modal.offsetAt(text, .{ .row = @intCast(@max(0, arow)), .col = @intCast(@max(0, acol)) }); + const c = modal.offsetAt(text, .{ .row = @intCast(@max(0, hrow)), .col = @intCast(@max(0, hcol)) }); + return cellOffRange(text, a, c); +} + +/// the same, from the two cells' gap offsets +pub fn cellOffRange(text: []const u8, a: usize, c: usize) modal.Selection { + if (a <= c) return .{ .anchor = a, .head = modal.nextGrapheme(text, c) }; + return .{ .anchor = modal.nextGrapheme(text, a), .head = c }; +} + +pub fn rangeCells(text: []const u8, r: modal.Selection) struct { cur: usize, anc: usize } { + if (r.head > r.anchor) return .{ .cur = modal.prevGrapheme(text, r.head), .anc = r.anchor }; + if (r.head < r.anchor) return .{ .cur = r.head, .anc = modal.prevGrapheme(text, r.anchor) }; + return .{ .cur = r.head, .anc = r.head }; +} + +pub fn setRange(t: *Text, text: []const u8, row0: i32, r0: modal.Selection, explicit: bool) void { + var r = r0; + if (r.anchor == r.head) r.head = modal.nextGrapheme(text, r.head); // min_width_1 + const off = rangeCells(text, r); + const cc = File.textPosition(t, text, off.cur); + // a bare block cursor has both cells on the same offset — the common + // case by far — and this conversion is not free even indexed + const ac = if (off.anc == off.cur) cc else File.textPosition(t, text, off.anc); + t.cur_row = @as(i32, @intCast(cc.row)) + row0; + t.cur_col = @intCast(cc.col); + t.vsel = .{ + .active = off.anc != off.cur or t.select, + .row = @as(i32, @intCast(ac.row)) + row0, + .col = @intCast(ac.col), + .explicit = explicit or t.select, + }; + t.msel.active = false; + t.nsel = 0; // writing ONE range means the selection IS that range + t.cur_pinned = true; + t.sticky_col = -1; + t.normal.clear(); + t.pane().ensureCursorVisible(); +} + +pub fn ranges(t: *Text, text: []const u8, row0: i32, out: *[max_selections]modal.Selection) struct { n: usize, pri: usize } { + const pr = primaryRange(t, text, row0); + var n: usize = 0; + var pri: usize = 0; + var placed = false; + for (t.sels[0..t.nsel]) |s| { + const r = cellRange(text, s.arow - row0, s.acol, s.row - row0, s.col); + if (!placed and @min(pr.anchor, pr.head) <= @min(r.anchor, r.head)) { + pri = n; + out[n] = pr; + n += 1; + placed = true; + } + out[n] = r; + n += 1; + } + if (!placed) { + pri = n; + out[n] = pr; + n += 1; + } + return .{ .n = n, .pri = pri }; +} + +pub fn setRanges(t: *Text, text: []const u8, in: []const modal.Selection, sticky: []const i32, pri0: usize, explicit: bool) void { + if (in.len == 0) return; // helix asserts non-empty; here it just means "no change" + var r: [max_selections]modal.Selection = undefined; + var st: [max_selections]i32 = undefined; + var n: usize = @min(in.len, max_selections); + var pri: usize = @min(pri0, n - 1); + for (in[0..n], 0..) |x, i| { + r[i] = x; + if (r[i].anchor == r[i].head) r[i].head = modal.nextGrapheme(text, r[i].head); + st[i] = if (i < sticky.len) sticky[i] else -1; + } + // insertion sort by start — n is tiny and usually already ordered + var i: usize = 1; + while (i < n) : (i += 1) { + var j = i; + while (j > 0 and @min(r[j].anchor, r[j].head) < @min(r[j - 1].anchor, r[j - 1].head)) : (j -= 1) { + std.mem.swap(modal.Selection, &r[j], &r[j - 1]); + std.mem.swap(i32, &st[j], &st[j - 1]); + if (pri == j) pri = j - 1 else if (pri == j - 1) pri = j; + } + } + var k: usize = 0; + i = 1; + while (i < n) : (i += 1) { + const a = r[k]; + const b = r[i]; + const af = @min(a.anchor, a.head); + const at = @max(a.anchor, a.head); + const bf = @min(b.anchor, b.head); + const bt = @max(b.anchor, b.head); + if (af == bf or (at > bf and bt > af)) { + r[k] = if (a.anchor > a.head and b.anchor > b.head) + .{ .anchor = @max(a.anchor, b.anchor), .head = @min(a.head, b.head) } + else + .{ .anchor = @min(af, bf), .head = @max(at, bt) }; + if (pri == i) pri = k; + if (st[k] < 0) st[k] = st[i]; + continue; + } + k += 1; + r[k] = b; + st[k] = st[i]; + if (pri == i) pri = k; + } + n = k + 1; + setRange(t, text, 0, r[pri], explicit); + t.sticky_col = st[pri]; + var w: usize = 0; + for (r[0..n], 0..) |x, idx| { + if (idx == pri) continue; + const c = rangeCells(text, x); + const cc = modal.positionAt(text, c.cur); + const ac = modal.positionAt(text, c.anc); + t.sels[w] = .{ + .row = @as(i32, @intCast(cc.row)), + .col = @intCast(cc.col), + .arow = @as(i32, @intCast(ac.row)), + .acol = @intCast(ac.col), + .sticky = st[idx], + }; + w += 1; + } + t.nsel = @intCast(w); +} + +pub fn multiSelAction(t: *Text, text: []const u8, kind: modal.Normal.Multi, cnt: usize) void { + var rs: [max_selections]modal.Selection = undefined; + const got = ranges(t, text, 0, &rs); + const n = got.n; + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + if (kind == .remove_primary) { + if (n < 2) return; // helix: "no selections remaining" + var out: [max_selections]modal.Selection = undefined; + var m: usize = 0; + for (rs[0..n], 0..) |r, i| { + if (i == got.pri) continue; + out[m] = r; + m += 1; + } + // helix Selection::remove: the NEXT range takes over, or the + // previous one when the primary was last + return setRanges(t, text, out[0..m], &.{}, @min(got.pri, m - 1), expl); + } + if (kind == .rotate_forward or kind == .rotate_backward) { + const step = cnt % n; + const pri = if (kind == .rotate_forward) (got.pri + step) % n else (got.pri + (n - step)) % n; + return setRanges(t, text, rs[0..n], &.{}, pri, expl); + } + if (kind == .merge) { + // helix merge_selections: first.merge(last) — the ranges are + // sorted, so that is simply the whole span + const lo = @min(rs[0].anchor, rs[0].head); + const hi = @max(rs[n - 1].anchor, rs[n - 1].head); + const rev = rs[0].anchor > rs[0].head and rs[n - 1].anchor > rs[n - 1].head; + const one: modal.Selection = if (rev) .{ .anchor = hi, .head = lo } else .{ .anchor = lo, .head = hi }; + return setRanges(t, text, &.{one}, &.{}, 0, expl); + } + if (kind == .merge_consecutive) { + var out: [max_selections]modal.Selection = undefined; + var m: usize = 0; + var pri: usize = 0; + for (rs[0..n], 0..) |r, i| { + if (m > 0 and @min(r.anchor, r.head) == @max(out[m - 1].anchor, out[m - 1].head)) { + const lo = @min(@min(out[m - 1].anchor, out[m - 1].head), @min(r.anchor, r.head)); + const hi = @max(@max(out[m - 1].anchor, out[m - 1].head), @max(r.anchor, r.head)); + out[m - 1] = .{ .anchor = lo, .head = hi }; + if (i == got.pri) pri = m - 1; + continue; + } + if (i == got.pri) pri = m; + out[m] = r; + m += 1; + } + return setRanges(t, text, out[0..m], &.{}, pri, expl); + } + if (kind == .split_newline) { + // helix selection::split_on_newline — one range per line the + // selection covers, the newlines themselves left out + var out: [max_selections]modal.Selection = undefined; + var m: usize = 0; + for (rs[0..n]) |r| { + const from = @min(r.anchor, r.head); + const to = @max(r.anchor, r.head); + if (from == to) { + if (m < max_selections) { + out[m] = r; + m += 1; + } + continue; + } + var start = from; + while (start < to and m < max_selections) { + const eol = modal.lineEndOffset(text, modal.lineAtOffset(text, start)); + if (eol >= to) { + out[m] = .{ .anchor = start, .head = to }; + m += 1; + break; + } + out[m] = .{ .anchor = start, .head = eol }; + m += 1; + start = eol + 1; + } + } + if (m == 0) return; + return setRanges(t, text, out[0..m], &.{}, 0, true); // helix keeps primary 0 + } + if (kind == .trim) { + // helix trim_selections: whitespace off both ends; ranges that are + // empty or all whitespace are dropped entirely + var out: [max_selections]modal.Selection = undefined; + var m: usize = 0; + for (rs[0..n]) |r| { + var from = @min(r.anchor, r.head); + var to = @max(r.anchor, r.head); + while (from < to and std.ascii.isWhitespace(text[from])) from += 1; + while (to > from and std.ascii.isWhitespace(text[to - 1])) to -= 1; + if (from >= to) continue; + out[m] = if (r.anchor > r.head) .{ .anchor = to, .head = from } else .{ .anchor = from, .head = to }; + m += 1; + } + if (m == 0) { // helix: collapse_selection + keep_primary_selection + const c = modal.selectionCursor(text, rs[got.pri]); + return setRange(t, text, 0, .{ .anchor = c, .head = c }, false); + } + // helix: the first survivor that OVERLAPS the old primary, else the last + const pf = @min(rs[got.pri].anchor, rs[got.pri].head); + const pt = @max(rs[got.pri].anchor, rs[got.pri].head); + var pri = m - 1; + for (out[0..m], 0..) |r, i| { + const f = @min(r.anchor, r.head); + const to = @max(r.anchor, r.head); + if (f == pf or (to > pf and pt > f)) { + pri = i; + break; + } + } + return setRanges(t, text, out[0..m], &.{}, pri, expl); + } + const below = kind == .copy_below; + var out: [max_selections]modal.Selection = undefined; + var m: usize = 0; + var pri: usize = 0; + const nlines = modal.cursorLineCount(text); + for (rs[0..n], 0..) |r, ri| { + const is_pri = ri == got.pri; + // head-exclusive: back the leading end off onto its own cell + const hp = modal.positionAt(text, if (r.anchor < r.head) modal.prevGrapheme(text, r.head) else r.head); + const ap = modal.positionAt(text, if (r.anchor < r.head) r.anchor else modal.prevGrapheme(text, r.anchor)); + const height = @max(hp.row, ap.row) - @min(hp.row, ap.row) + 1; + if (m >= max_selections) break; + if (is_pri) pri = m; + out[m] = r; + m += 1; + var made: usize = 0; + var k: usize = 0; + while (made < cnt and m < max_selections) : (k += 1) { + const d = (k + 1) * height; + const arow = if (below) ap.row + d else ap.row -| d; + const hrow = if (below) hp.row + d else hp.row -| d; + if (arow >= nlines or hrow >= nlines) break; + const a2 = modal.offsetAt(text, .{ .row = arow, .col = ap.col }); + const h2 = modal.offsetAt(text, .{ .row = hrow, .col = hp.col }); + // a line too short to reach the column is skipped, not clamped + if (modal.positionAt(text, a2).col == ap.col and modal.positionAt(text, h2).col == hp.col) { + if (is_pri) pri = m; + out[m] = modal.moveSelectionCursor(text, .{ .anchor = a2, .head = a2 }, h2, true); + m += 1; + made += 1; + } + if (arow == 0 and hrow == 0) break; + } + } + setRanges(t, text, out[0..m], &.{}, pri, expl); +} + +/// a range's start CELL (document order key) — the smaller of its two ends +pub fn selStart(s: SelRange) struct { row: i32, col: i32 } { + if (s.arow < s.row or (s.arow == s.row and s.acol < s.col)) return .{ .row = s.arow, .col = s.acol }; + return .{ .row = s.row, .col = s.col }; +} + +pub fn maxLine(text: []const u8) usize { + const nl = modal.cursorLineCount(text); + return if (text.len == 0 or text[text.len - 1] == '\n') nl -| 2 else nl - 1; +} + +/// point-target motion: collapse there (extend in select mode) +pub fn pointMove(t: *Text, text: []const u8, range: modal.Selection, target: usize) void { + setRange(t, text, 0, modal.moveSelectionCursor(text, range, target, t.select), false); +} + +/// word motions select their traversed span (extend mode: head only) +pub fn wordMove(t: *Text, text: []const u8, range: modal.Selection, cnt: usize, target: modal.WordTarget) void { + const wr = modal.moveWord(text, range, cnt, target); + const res = if (t.select) modal.moveSelectionCursor(text, range, modal.selectionCursor(text, wr), true) else wr; + setRange(t, text, 0, res, false); +} + +/// f/t/F/T: anchor at the old cursor cell, head on the hit (not found: no move) +pub fn findMove(t: *Text, text: []const u8, range: modal.Selection, ch: u21, fwd: bool, till: bool, cnt: usize) void { + const cur = modal.selectionCursor(text, range); + const target = modal.findTarget(text, cur, ch, fwd, till, cnt) orelse return; + const res = if (t.select) + modal.moveSelectionCursor(text, range, target, true) + else + modal.moveSelectionCursor(text, .{ .anchor = cur, .head = cur }, target, true); + setRange(t, text, 0, res, false); +} + +/// j/k and friends: sticky goal column, clamped onto short lines' newline +pub fn verticalMove(t: *Text, text: []const u8, range: modal.Selection, down: bool, cnt: usize) void { + const cur = modal.selectionCursor(text, range); + const pos = File.textPosition(t, text, cur); + const goal: usize = if (t.sticky_col >= 0) + @intCast(t.sticky_col) + else + File.rawDisplayCol(File.textLine(t, text, pos.row), pos.col); + const last_row = File.textLineCount(t, text) - 1; + const nline = if (down) @min(pos.row + @max(1, cnt), last_row) else pos.row -| @max(1, cnt); + const target_col = File.rawAtDisplay(File.textLine(t, text, nline), goal); + const target = File.textOffset(t, text, .{ .row = nline, .col = target_col }); + // extend mode never walks onto the empty trailing line (helix) + if (t.select and target == text.len and text.len > 0 and text[text.len - 1] == '\n') return; + setRange(t, text, 0, modal.moveSelectionCursor(text, range, target, t.select), false); + t.sticky_col = @intCast(goal); +} + +pub fn visualMove( + t: *Text, + text: []const u8, + range: modal.Selection, + down: bool, + cnt: usize, + width: usize, +) void { + const cur = modal.selectionCursor(text, range); + const pos = File.textPosition(t, text, cur); + const last_row = File.textLineCount(t, text) - 1; + var row = pos.row; + var line = modal.lineSlice(text, row); + var vrow = File.visualRow(line, pos.col, width); + const goal: usize = if (t.sticky_col >= 0) + @intCast(t.sticky_col) + else + File.rawDisplayCol(line[vrow.start..vrow.end], pos.col -| vrow.start); + var steps = @max(1, cnt); + while (steps > 0) : (steps -= 1) { + if (down) { + if (vrow.end < line.len) { + vrow = File.visualRow(line, vrow.end, width); + continue; + } + if (row == last_row) break; + row += 1; + line = modal.lineSlice(text, row); + vrow = File.visualRow(line, 0, width); + } else { + if (vrow.start > 0) { + vrow = File.visualRow(line, vrow.start - 1, width); + continue; + } + if (row == 0) break; + row -= 1; + line = modal.lineSlice(text, row); + vrow = File.visualRow(line, line.len, width); + } + } + // The newline slot is a real cursor position, but the first byte of the + // NEXT visual row is not: landing there would read as two rows moved. + var target_col = vrow.start + File.rawAtDisplay(line[vrow.start..vrow.end], goal); + if (vrow.end < line.len and target_col >= vrow.end) + target_col = modal.graphemeStart(line, vrow.end - 1); + const target = File.textOffset(t, text, .{ .row = row, .col = target_col }); + // extend mode never walks onto the empty trailing line (helix) + if (t.select and target == text.len and text.len > 0 and text[text.len - 1] == '\n') return; + setRange(t, text, 0, modal.moveSelectionCursor(text, range, target, t.select), false); + t.sticky_col = @intCast(goal); +} + +/// Ctrl-d/u: scroll half a page AND move the cursor by the same rows +pub fn halfPageMove(t: *Text, text: []const u8, range: modal.Selection, down: bool) void { + const half: i32 = @max(1, @divTrunc(@as(i32, t.pane().rows), 2)); + t.pane().scrollBy(if (down) half else -half); + verticalMove(t, text, range, down, @intCast(half)); +} + +/// helix `scroll` without cursor sync (Ctrl-f/b, PgUp/PgDn, zj/zk): shift +/// the view, then snap a fallen-out cursor to the near scrolloff edge, col 0 +pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta: i32) void { + const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, t.pane().rows) - 1, 2)); + t.pane().scrollBy(delta); + const top = t.pane().scroll(); + const last_row: i32 = @intCast(File.textLineCount(t, text) - 1); + const cur = modal.selectionCursor(text, range); + if (delta > 0) { + const snap: i32 = @max(0, @min(top + margin, last_row)); + const head = File.textLineStart(t, text, @intCast(snap)); + if (head <= cur) return; + const anchor = if (t.select) range.anchor else head; + setRange(t, text, 0, .{ .anchor = anchor, .head = head }, false); + } else { + const snap: i32 = @max(0, @min(top + @as(i32, t.pane().rows) - margin - 1, last_row)); + const head = File.textLineStart(t, text, @intCast(snap)); + if (head >= cur) return; + const anchor = if (t.select) range.anchor else head; + setRange(t, text, 0, .{ .anchor = anchor, .head = head }, false); + } +} + +/// gt/gc/gb: view-relative rows, col 0, scrolloff clamped (helix goto_window) +pub fn gotoWindow(t: *Text, text: []const u8, range: modal.Selection, which: enum { top, center, bottom }, cnt: usize) void { + const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, t.pane().rows) - 1, 2)); + const top = t.pane().scroll(); + const last_row: i32 = @intCast(File.textLineCount(t, text) - 1); + const last_vis: i32 = @min(@as(i32, t.pane().rows) - 1, last_row - top); + const n: i32 = @intCast(cnt - 1); + var vline: i32 = switch (which) { + .top => top + margin + n, + .center => top + @divTrunc(last_vis, 2), + .bottom => top + last_vis - (margin + n), + }; + vline = @max(vline, top + margin); + vline = @min(vline, top + last_vis - margin); + const row: i32 = std.math.clamp(vline, 0, last_row); + pointMove(t, text, range, File.textLineStart(t, text, @intCast(row))); +} + +/// helix Range::line_range — the inclusive line span a range covers +pub fn rangeLineSpan(text: []const u8, r: modal.Selection) struct { start: usize, end: usize } { + const from = @min(r.anchor, r.head); + const to = @max(r.anchor, r.head); + const to_adj = if (from == to) to else @max(modal.prevGrapheme(text, to), from); + return .{ .start = modal.lineAtOffset(text, from), .end = modal.lineAtOffset(text, to_adj) }; +} + +fn lineStartOrEof(text: []const u8, line: usize) usize { + if (line >= modal.cursorLineCount(text)) return text.len; + return modal.lineStartOffset(text, line); +} + +/// helix `x` extend_line_below: full lines incl. the newline, cursor ON +/// the last one's '\n'; an already-line-bounded selection grows downward +pub fn lineSelect(t: *Text, text: []const u8, range: modal.Selection, cnt: usize) void { + const span = rangeLineSpan(text, range); + const start = modal.lineStartOffset(text, span.start); + const end = lineStartOrEof(text, span.end + 1); + const full = @min(range.anchor, range.head) == start and @max(range.anchor, range.head) == end; + const head = lineStartOrEof(text, span.end + cnt + @intFromBool(full)); + setRange(t, text, 0, .{ .anchor = start, .head = head }, true); +} + +/// helix `X` extend_to_line_bounds (direction kept) +pub fn lineBoundsSelect(t: *Text, text: []const u8, range: modal.Selection) void { + const span = rangeLineSpan(text, range); + const start = modal.lineStartOffset(text, span.start); + const end = lineStartOrEof(text, span.end + 1); + const r: modal.Selection = if (range.head < range.anchor) + .{ .anchor = end, .head = start } + else + .{ .anchor = start, .head = end }; + setRange(t, text, 0, r, true); +} + +/// helix `Alt-x` shrink_to_line_bounds (single-line selections untouched) +pub fn shrinkSelToLineBounds(t: *Text, text: []const u8, range: modal.Selection) void { + const span = rangeLineSpan(text, range); + if (span.start == span.end) return; + const from = @min(range.anchor, range.head); + const to = @max(range.anchor, range.head); + var start = modal.lineStartOffset(text, span.start); + var end = lineStartOrEof(text, span.end + 1); + if (start != from) start = lineStartOrEof(text, span.start + 1); + if (end != to) end = modal.lineStartOffset(text, span.end); + const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active; + const r: modal.Selection = if (range.head < range.anchor) + .{ .anchor = end, .head = start } + else + .{ .anchor = start, .head = end }; + setRange(t, text, 0, r, expl); +} + +/// pull the cursor back inside `text` after a rewrite; `row0` is the +/// absolute surface row of its first line (0 for a file) +pub fn clampCursor(t: *Text, text: []const u8, row0: i32) void { + const n = modal.lineCount(text); + const row: usize = @min(@as(usize, @intCast(@max(0, t.cur_row - row0))), if (n == 0) 0 else n - 1); + const llen = modal.lineSlice(text, row).len; + t.cur_row = @as(i32, @intCast(row)) + row0; + t.cur_col = @intCast(@min(@as(usize, @intCast(@max(0, t.cur_col))), llen)); + t.cur_pinned = true; + t.vsel.active = false; + t.msel.active = false; + t.pane().ensureCursorVisible(); +} diff --git a/src/body_layer.zig b/src/body_layer.zig index 6bac7018..71529326 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -132,7 +132,7 @@ test "compact context borders separate source gaps and preserve multiline header fn paintTerminalSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void { const body = bodyText(p, p.scratch.allocator(), pane) catch return; - const raw = pane.mode == .tty; + const raw = pane.body.mode == .tty; const tx = r.x + config.GUTTER; const width = r.w -| config.GUTTER; const body_y = p.bodyTop(r); @@ -366,14 +366,14 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sl, sel_btn[b], th.sel_fg); } // modal line selection (helix `x`): whole body rows, normal mode only - if ((pane.mode == .normal or modal_hover) and pane.msel.active) { - const hover_only = modal_hover and pane.mode != .normal; + if ((pane.body.mode == .normal or modal_hover) and pane.body.msel.active) { + const hover_only = modal_hover and pane.body.mode != .normal; const modal_bg = if (hover_only) colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)) else th.sel_bg; - const lo = @min(pane.msel.r0, pane.msel.r1); - const hi = @max(pane.msel.r0, pane.msel.r1); + const lo = @min(pane.body.msel.r0, pane.body.msel.r1); + const hi = @max(pane.body.msel.r0, pane.body.msel.r1); var row: u16 = BOX_H; // never paint the tag row while (row < body_bottom) : (row += 1) { const ar = pane.wrapAt(@as(i32, row) - @as(i32, BOX_H)).line; @@ -388,23 +388,23 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id } } const preview = normal.selRegexArmed(pane) != null; - const show_prim = (pane.mode == .normal and pane.vsel.active) or preview or - (modal_hover and pane.vsel.active); - const show_extra = pane.mode != .tty and pane.nsel > 0; + const show_prim = (pane.body.mode == .normal and pane.body.vsel.active) or preview or + (modal_hover and pane.body.vsel.active); + const show_extra = pane.body.mode != .tty and pane.body.nsel > 0; if (show_prim or show_extra) { const vpfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0; var si: usize = 0; - while (si <= pane.nsel) : (si += 1) { - const primary = si == pane.nsel; + while (si <= pane.body.nsel) : (si += 1) { + const primary = si == pane.body.nsel; if (if (primary) !show_prim else !show_extra) continue; - const sr = if (primary) Pane.SelRange{ - .row = pane.cur_row, - .col = pane.cur_col, - .arow = if (pane.vsel.active) pane.vsel.row else pane.cur_row, - .acol = if (pane.vsel.active) pane.vsel.col else pane.cur_col, - } else pane.sels[si]; + const sr = if (primary) panes.Text.SelRange{ + .row = pane.body.cur_row, + .col = pane.body.cur_col, + .arow = if (pane.body.vsel.active) pane.body.vsel.row else pane.body.cur_row, + .acol = if (pane.body.vsel.active) pane.body.vsel.col else pane.body.cur_col, + } else pane.body.sels[si]; const bnd = edit.cellBounds(sr); - const hover_only = primary and modal_hover and pane.mode != .normal and !preview; + const hover_only = primary and modal_hover and pane.body.mode != .normal and !preview; const bg = if (hover_only) colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)) else if (primary) @@ -462,18 +462,18 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id // cursor: tracks the shell cursor until pinned by a click or a key // (the tag cursor above wins while the tag is focused) if (active and !pane.tag_edit) { - if (pane.mode != .tty) { + if (pane.body.mode != .tty) { const cur = panes.Terminal.gridCursor(pane); const goff = panes.Terminal.gridOffset(pane); - const crow = if (pane.cur_pinned) pane.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff); - const ccol = if (pane.cur_pinned) pane.cur_col else @as(i32, @intCast(cur.x)); + const crow = if (pane.body.cur_pinned) pane.body.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff); + const ccol = if (pane.body.cur_pinned) pane.body.cur_col else @as(i32, @intCast(cur.x)); const cwp = pane.wrapRow(crow, ccol); const prow = cwp.row + @as(i32, BOX_H); // Files store source-byte columns; the Surface stores display // cells, so account for every expanded tab before the cursor. const cx = if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, crow, cwp.at, ccol) - else if (pane.cur_pinned) + else if (pane.body.cur_pinned) panes.File.lineDisplayOffset( modal.lineSlice(body, @intCast(@max(0, cwp.row))), @intCast(@max(0, cwp.at)), @@ -482,7 +482,7 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id else ccol; if (prow >= BOX_H and cx >= 0 and prow < body_bottom and cx < tw) - s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - BOX_H)), .bar = pane.mode == .insert }; + s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - BOX_H)), .bar = pane.body.mode == .insert }; } else if (panes.Terminal.visibleCursor(pane)) |cur| { if (cur.y + BOX_H < body_bottom and cur.x < tw) s.cursor = .{ .x = tx + cur.x, .y = body_y + cur.y }; diff --git a/src/colors.zig b/src/colors.zig index d6e1d535..741187c6 100644 --- a/src/colors.zig +++ b/src/colors.zig @@ -572,8 +572,8 @@ test "line numbers stay quiet without a current-line tag background" { const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 }); defer p.deinit(); const pane = try p.setTestFile("first\nsecond\n"); - pane.cur_pinned = true; - pane.cur_row = 0; + pane.body.cur_pinned = true; + pane.body.cur_row = 0; var custom = themes[0]; custom.name = try gpa.dupe(u8, "quiet-gutter"); custom.lineno = .{ 70, 70, 70 }; @@ -598,7 +598,7 @@ test "line numbers stay quiet without a current-line tag background" { try std.testing.expectEqual(Color{ .rgb = custom.bg.? }, fallback.at(x, y).style.bg); try std.testing.expect(fallback.at(x, y).style.bold); // Moving the caret clears the previous cue; editing the tag parks it. - pane.cur_row = 1; + pane.body.cur_row = 1; const moved = try p.render(p.scratch.allocator()); try std.testing.expect(!moved.at(x, y).style.bold); try std.testing.expect(moved.at(x, y + 1).style.bold); diff --git a/src/config.zig b/src/config.zig index 4b47b188..38b87011 100644 --- a/src/config.zig +++ b/src/config.zig @@ -173,16 +173,16 @@ test "Space-k is unbound while Kill and other k chords remain available" { const p = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const pane = try p.setTestFile("first\nsecond\n"); - pane.cur_row = 1; + pane.body.cur_row = 1; while (p.nextEffect()) |_| {} p.update(.{ .key = .{ .cp = ' ' } }); try std.testing.expect(p.leader_on); p.update(.{ .key = .{ .cp = 'k' } }); try std.testing.expect(!p.leader_on and !p.quit); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); while (p.nextEffect()) |effect| try std.testing.expect(effect != .quit); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); try std.testing.expect(p.executeBuiltinLine(p.active, "Kill")); try std.testing.expect(p.quit); } diff --git a/src/detached/client.zig b/src/detached/client.zig index 6476d521..4468c961 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -868,9 +868,9 @@ test "detached selection pipe runs off the loop and returns through the attached try h.init(60, 16); defer h.deinit(); const pane = try h.core.setTestFile("one\ntwo\n"); - pane.cur_row = 0; - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_row = 0; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; var client = try h.attach(60, 16); defer client.deinit(); _ = try h.pumpUntil(&client, .frame); diff --git a/src/detached/server.zig b/src/detached/server.zig index 2b9c9247..a3694c57 100644 --- a/src/detached/server.zig +++ b/src/detached/server.zig @@ -1251,8 +1251,8 @@ test "detached worker setup failure completes requests without changing document try std.testing.expect(s.core.lsp_wait == null); try std.testing.expect(s.lsp_task == null); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; s.core.update(.{ .key = .{ .cp = '|' } }); s.core.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } }); s.core.update(.{ .key = .{ .cp = pardes.Key.enter } }); diff --git a/src/dump.zig b/src/dump.zig index 46cf6001..2fb9ce80 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -740,7 +740,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo const restored = try pardes.panes.Terminal.restore(p, src, revive); p.installPane(i, restored); try restored.setOwnedCwd(t.cwd); - if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.mode = .tty; + if (std.mem.startsWith(u8, src.tag, "TTY ")) restored.body.mode = .tty; if (revive) p.emit(.{ .spawn = .{ .pane = @intCast(i), .cwd = .from(t.cwd) } }); break :terminal restored; }, @@ -773,7 +773,7 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo errdefer gpa.destroy(history); const restored = try p.newDocPane(i); restored.file = .{ .path = path, .content = content, .history = history }; - restored.cur_pinned = true; + restored.body.cur_pinned = true; restored.cols = @max(1, src.cols); restored.rows = @max(1, src.rows); p.emit(.{ .watch = .{ .pane = @intCast(i), .on = true, .mode = .baseline_disk } }); 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")); } diff --git a/src/exec.zig b/src/exec.zig index 75d07441..0a9d6f31 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -38,13 +38,13 @@ pub fn takesCommandLine(p: *const Pardes, id: usize) bool { } pub fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool { - if (pane.nsel == 0) return false; - const text = edit.flatSurface(p, pane) catch return false; - var ranges: [Pane.max_selections]modal.Selection = undefined; - const got = pane.ranges(text, 0, &ranges); + if (pane.body.nsel == 0) return false; + const text = edit.flatSurface(p, &pane.body) catch return false; + var ranges: [panes.Text.max_selections]modal.Selection = undefined; + const got = pane.body.ranges(text, 0, &ranges); if (got.n < 2) return false; - var texts: [Pane.max_selections][]u8 = undefined; + var texts: [panes.Text.max_selections][]u8 = undefined; var made: usize = 0; defer for (texts[0..made]) |t| p.gpa.free(t); for (ranges[0..got.n]) |range| { @@ -59,10 +59,10 @@ pub fn chordEachSel(p: *Pardes, pane: *Pane, cmd: Builtin) bool { const id = p.active; const serial = pane.serial; - pane.vsel.active = false; - pane.msel.active = false; - pane.select = false; - pane.nsel = 0; // the chord consumed them, exactly as it consumes one + pane.body.vsel.active = false; + pane.body.msel.active = false; + pane.body.select = false; + pane.body.nsel = 0; // the chord consumed them, exactly as it consumes one var primary_active: ?usize = null; for (texts[0..made], 0..) |txt, i| { @@ -96,7 +96,7 @@ pub const PointerOperand = struct { pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand { const visible = clicked.r0 - @as(i32, BOX_H); const wrapped = pane.wrapAt(visible); - const raw = pane.isTerminal() and pane.mode == .tty and clicked.r0 >= BOX_H; + const raw = pane.isTerminal() and pane.body.mode == .tty and clicked.r0 >= BOX_H; const row = if (raw) panes.Terminal.gridOffset(pane) + visible else wrapped.line; const raw_line = if (raw) modal.lineSlice(body_layer.bodyText(p, p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else ""; const col = if (raw) @@ -104,7 +104,7 @@ pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand else if (clicked.r0 >= BOX_H) edit.paneByteAtDisplay( p, - pane, + &pane.body, wrapped.line, wrapped.at, clicked.c0 - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0), @@ -143,14 +143,14 @@ pub fn pointerOperand(p: *Pardes, pane: *Pane, clicked: Pane.Sel) PointerOperand result.preview = kept; return result; } - if (pane.vsel.active and pane.vsel.explicit and - edit.spanHas(row, col, pane.vsel.row, pane.vsel.col, pane.cur_row, pane.cur_col)) + if (pane.body.vsel.active and pane.body.vsel.explicit and + edit.spanHas(row, col, pane.body.vsel.row, pane.body.vsel.col, pane.body.cur_row, pane.body.cur_col)) { result.text = edit.currentSelText(p, pane); return result; } - if (pane.msel.active and row >= @min(pane.msel.r0, pane.msel.r1) and - row <= @max(pane.msel.r0, pane.msel.r1)) + if (pane.body.msel.active and row >= @min(pane.body.msel.r0, pane.body.msel.r1) and + row <= @max(pane.body.msel.r0, pane.body.msel.r1)) { result.text = edit.currentSelText(p, pane); return result; @@ -202,7 +202,7 @@ pub fn heldSelection(p: *Pardes, first: usize) ?[]const u8 { }; if (t.sel[sel_slot].state == .done) { if (edit.selectionText(p, t, t.sel[sel_slot]) catch null) |tx| return tx; - } else if ((t.vsel.active and t.vsel.explicit) or t.msel.active) { + } else if ((t.body.vsel.active and t.body.vsel.explicit) or t.body.msel.active) { if (edit.currentSelText(p, t)) |tx| return tx; } } @@ -302,7 +302,7 @@ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void { const nt = p.newShell(free, Pardes.paneDir(src)) catch return; nt.greet = !v9fs; nt.v9fs_on_spawn = v9fs; - nt.mode = .tty; + nt.body.mode = .tty; const parent = layout.splitParent(p, from); const f = layout.findPane(p, parent).?; layout.insert(p, f.col, f.idx + 1, free); @@ -558,8 +558,8 @@ test "the acme chords act once per selection, not once on the primary" { .{ .anchor = 0, .head = 6 }, .{ .anchor = 7, .head = 13 }, }; - pane.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); - try std.testing.expectEqual(@as(u8, 1), pane.nsel); + pane.body.setRanges(pane.file.?.content, &ranges, &.{}, 0, true); + try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); const before = p.ncol; p.update(.{ .key = .{ .cp = Key.tab } }); // config.exec_key: Exec @@ -567,5 +567,5 @@ test "the acme chords act once per selection, not once on the primary" { // other cursor, so one keystroke over two cursors made one column. try std.testing.expectEqual(before + 2, p.ncol); // ...and the chord consumed the selection exactly as it does with one. - try std.testing.expectEqual(@as(u8, 0), p.panes[0].?.nsel); + try std.testing.expectEqual(@as(u8, 0), p.panes[0].?.body.nsel); } diff --git a/src/gui/gui.zig b/src/gui/gui.zig index a11b22c7..97bbc816 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -1399,7 +1399,7 @@ test "GUI completion failure survives backlog and leaves the document unchanged" const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); defer core.deinit(); const pane = try core.setTestFile("abc"); - pane.cur_col = 1; + pane.body.cur_col = 1; core.lspRequest(core.active, .completion, ""); const id = core.lsp_wait.?.id; var workers: LspWorkers = .{}; @@ -1426,7 +1426,7 @@ test "GUI completion failure survives backlog and leaves the document unchanged" shell.drainQueue(); try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expect(shell.saw_event); } @@ -1611,7 +1611,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo const core = try pardes.Pardes.init(gpa, .{ .tty_only = true }); defer core.deinit(); const pane = try core.setTestFile("abc"); - pane.cur_col = 1; + pane.body.cur_col = 1; var workers: LspWorkers = .{}; var gate: std.Io.Event = .unset; var threads: [LspWorkers.capacity]?std.Thread = @splat(null); @@ -1650,7 +1650,7 @@ test "GUI LSP worker limit rejects before allocation and recovers after owned wo }); try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "WorkersBusy") != null); try std.testing.expectEqual(@as(usize, 0), failing.alloc_index); try std.testing.expect(!failing.has_induced_failure); @@ -1762,8 +1762,8 @@ test "GUI worker setup failures finish matching LSP and pipe requests" { try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; core.update(.{ .key = .{ .cp = '|' } }); core.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } }); core.update(.{ .key = .{ .cp = pardes.Key.enter } }); @@ -3870,7 +3870,7 @@ const Shell = struct { s.closePty(e.pane); if (e.failure) |err| { if (s.core.panes[e.pane]) |pane| { - pane.mode = .normal; + pane.body.mode = .normal; s.core.reportError(e.pane, "terminal reader", err); } } else s.core.update(.{ .eof = .{ .pane = e.pane } }); diff --git a/src/host_io.zig b/src/host_io.zig index d0b9de71..959ce3bd 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -322,7 +322,7 @@ pub const Lsp = struct { const path = try gpa.dupe(u8, "/file.zig"); gpa.free(pane.file.?.path); pane.file.?.path = path; - pane.cur_col = 6; + pane.body.cur_col = 6; const revision = pane.file.?.revision; const undo_len = pane.file.?.history.undo_len; var failing = std.testing.FailingAllocator.init(gpa, .{}); diff --git a/src/layout.zig b/src/layout.zig index 3d8932a8..49da0207 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -704,7 +704,7 @@ pub fn focusDir(p: *Pardes, from: usize, dir: enum { left, right, up, down }) vo if (best) |b| { p.active = b; // a count typed before the hop was meant for the pane you left - p.panes[b].?.normal.clear(); + p.panes[b].?.body.normal.clear(); } } diff --git a/src/look.zig b/src/look.zig index 1534d332..efc3f312 100644 --- a/src/look.zig +++ b/src/look.zig @@ -482,15 +482,15 @@ pub fn expandedSel(p: *Pardes, pane: *Pane, at: Pane.Sel) ?Pane.Sel { /// the word under the modal cursor as a pane-local selection (paneText /// coords: row 0 is the tag; file panes carry the line-number prefix) pub fn cursorWordSel(p: *Pardes, pane: *Pane) Pane.Sel { - const w = pane.wrapRow(pane.cur_row, pane.cur_col); + const w = pane.wrapRow(pane.body.cur_row, pane.body.cur_col); const vrow = w.row + @as(i32, BOX_H); const vcol = if (pane.file != null) - panes.File.displayOffset(pane, pane.cur_row, w.at, pane.cur_col) + @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, pane.body.cur_row, w.at, pane.body.cur_col) + @as(i32, panes.File.gutterWidth(pane)) else blk: { - const lines = edit.paneCursorLines(p, pane) catch break :blk pane.cur_col; - const local = pane.cur_row; - if (local < 0 or @as(usize, @intCast(local)) >= lines.len) break :blk pane.cur_col; - break :blk panes.File.lineDisplayOffset(lines[@intCast(local)], @intCast(@max(0, w.at)), @intCast(@max(0, pane.cur_col))); + const lines = edit.paneCursorLines(p, &pane.body) catch break :blk pane.body.cur_col; + const local = pane.body.cur_row; + if (local < 0 or @as(usize, @intCast(local)) >= lines.len) break :blk pane.body.cur_col; + break :blk panes.File.lineDisplayOffset(lines[@intCast(local)], @intCast(@max(0, w.at)), @intCast(@max(0, pane.body.cur_col))); }; return .{ .state = .done, .c0 = vcol, .c1 = vcol, .r0 = vrow, .r1 = vrow }; } @@ -526,9 +526,9 @@ fn lookFirstHit(p: *Pardes, id: usize) void { if (p.active != rid) return; // walked out of the list; leave it there const txt = edit.currentSelText(p, rp) orelse return; // spent, exactly as the look chord spends it (handleKey) - rp.vsel.active = false; - rp.msel.active = false; - rp.select = false; + rp.body.vsel.active = false; + rp.body.msel.active = false; + rp.body.select = false; exec.runBuiltin(p, config.look_cmd, rid, "", txt); } @@ -587,7 +587,7 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start anchor = found.anchor; } } else { - const lines = try edit.paneCursorLines(p, pane); + const lines = try edit.paneCursorLines(p, &pane.body); // a real file names itself; a terminal or an output buffer has no path const has_path = if (pane.file) |f| f.output == null else false; var idbuf: [16]u8 = undefined; @@ -597,8 +597,8 @@ pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start std.fs.path.basename(pane.file.?.path) else std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{id}) catch return error.PathTooLong; - const cl: usize = @intCast(@max(0, pane.cur_row)); - const cc: usize = @intCast(@max(0, pane.cur_col)); + const cl: usize = @intCast(@max(0, pane.body.cur_row)); + const cc: usize = @intCast(@max(0, pane.body.cur_col)); for (lines, 0..) |ln, i| { const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue; const row = try std.fmt.allocPrint(arena, "{s}:{d}:{d}{c}{d} {s}\n", .{ @@ -656,11 +656,11 @@ pub fn jumpResult(p: *Pardes, id: usize, row: usize) bool { if (ln.len == 0) return false; const r: i32 = @intCast(row); pane.search_row = row; - rp.msel = .{ .active = true, .r0 = r, .r1 = r }; - rp.vsel.active = false; - rp.nsel = 0; - rp.cur_row = r; - rp.cur_pinned = true; + rp.body.msel = .{ .active = true, .r0 = r, .r1 = r }; + rp.body.vsel.active = false; + rp.body.nsel = 0; + rp.body.cur_row = r; + rp.body.cur_pinned = true; const off = rp.scroll(); const last = off + @as(i32, rp.rows) - 1; if (r < off) rp.scrollBy(r - off) else if (r > last) rp.scrollBy(r - last); @@ -676,14 +676,14 @@ pub fn jumpResult(p: *Pardes, id: usize, row: usize) bool { null; }; if (span) |selected| { - rp.cur_col = @intCast(selected.start); + rp.body.cur_col = @intCast(selected.start); rp.look_at = .{ .row = r, .col0 = @intCast(selected.start), .col1 = @intCast(selected.end - 1), }; } else { - rp.cur_col = 0; + rp.body.cur_col = 0; rp.look_at = null; } if (tr.commands) { @@ -692,7 +692,7 @@ pub fn jumpResult(p: *Pardes, id: usize, row: usize) bool { lookAt(p, rid, ln[selected.start..selected.end]); // This is the newly visited result, not a selection carried into // the destination. Look cleared the previous source selection. - rp.msel = .{ .active = true, .r0 = r, .r1 = r }; + rp.body.msel = .{ .active = true, .r0 = r, .r1 = r }; } armLookWalk(p, rid); return true; @@ -705,9 +705,9 @@ fn lookEdge(delta: i32) LookFrom { } fn lookStand(pane: *Pane) LookFrom { - if (pane.look_at) |s| if (s.row == pane.cur_row and s.col0 == pane.cur_col) + if (pane.look_at) |s| if (s.row == pane.body.cur_row and s.col0 == pane.body.cur_col) return .{ .row = s.row, .col = s.col0, .strict = true }; - return .{ .row = pane.cur_row, .col = pane.cur_col }; + return .{ .row = pane.body.cur_row, .col = pane.body.cur_col }; } fn lookWalkPanes(p: *Pardes, out: *[MAX_PANES]usize) []const usize { @@ -759,7 +759,7 @@ fn wholeRowSpan(ln: []const u8) ?Span { } fn lookSpanIn(p: *Pardes, pane: *Pane, from: LookFrom, delta: i32, budget: *usize) ?Pane.LookSpot { - const lines = edit.paneCursorLines(p, pane) catch return null; + const lines = edit.paneCursorLines(p, &pane.body) catch return null; const nrows: i32 = @intCast(lines.len); if (nrows == 0) return null; const grain: panes.Output.Grain = if (pane.file) |*f| panes.Output.grain(f.output) else .word; @@ -835,13 +835,13 @@ pub fn lookWalk(p: *Pardes, delta: i32) void { } fn landLookSpot(p: *Pardes, id: usize, pane: *Pane, spot: Pane.LookSpot) void { - edit.pinPaneCursor(p, pane); // fresh out of tty mode the cursor still tracks the shell - pane.vsel = .{ .active = true, .row = spot.row, .col = spot.col1, .explicit = true }; - pane.msel.active = false; - pane.nsel = 0; - pane.cur_row = spot.row; - pane.cur_col = spot.col0; - pane.cur_pinned = true; + edit.pinPaneCursor(p, &pane.body); // fresh out of tty mode the cursor still tracks the shell + pane.body.vsel = .{ .active = true, .row = spot.row, .col = spot.col1, .explicit = true }; + pane.body.msel.active = false; + pane.body.nsel = 0; + pane.body.cur_row = spot.row; + pane.body.cur_col = spot.col0; + pane.body.cur_pinned = true; pane.look_at = spot; p.look_walk_owner = pane.serial; p.active = id; @@ -944,7 +944,7 @@ pub fn focusPaneLine(p: *Pardes, id: usize, at: Spot, landing: enum { center, ke if (id >= MAX_PANES) return; const pane = p.panes[id] orelse return; p.active = id; - pane.normal.clear(); + pane.body.normal.clear(); if (pane.hasPdf()) { if (landing == .keep) return; if (comptime pdf_enabled) { @@ -964,21 +964,21 @@ pub fn focusPaneLine(p: *Pardes, id: usize, at: Spot, landing: enum { center, ke } } } - pane.cur_row = @intCast(at.line - 1); - pane.cur_col = if (at.col > 0) @intCast(at.col - 1) else 0; - pane.cur_pinned = true; + pane.body.cur_row = @intCast(at.line - 1); + pane.body.cur_col = if (at.col > 0) @intCast(at.col - 1) else 0; + pane.body.cur_pinned = true; if (at.end_line != 0) return selectSpan(p, pane, at); pane.ensureCursorVisible(); } fn selectSpan(p: *Pardes, pane: *Pane, at: Spot) void { - const text = edit.flatSurface(p, pane) catch return; + const text = edit.flatSurface(p, &pane.body) catch return; // saturating, not `- 1`: `f.zig:0-5` is a legal thing to type and a // 1-based zero is the same nothing an absent number is const acol: i32 = @intCast(at.col -| 1); const ecol: i32 = if (at.end_col > 0) @intCast(at.end_col - 1) else std.math.maxInt(i32); - const r = Pane.cellRange(text, @intCast(at.line -| 1), acol, @intCast(at.end_line -| 1), ecol); - pane.setRange(text, 0, r, true); + const r = panes.Text.cellRange(text, @intCast(at.line -| 1), acol, @intCast(at.end_line -| 1), ecol); + pane.body.setRange(text, 0, r, true); } fn openPaneTarget(p: *const Pardes, path: []const u8, at: Spot) ?Target { @@ -1006,13 +1006,13 @@ fn focusPaneByPath(p: *Pardes, path: []const u8, at: Spot) bool { /// Navigation starts a new selection at the destination. pub fn clearNavigationSelection(p: *Pardes, pane: *Pane) void { - pane.vsel = .{}; - pane.msel = .{}; - pane.nsel = 0; - pane.select = false; - pane.normal.clear(); - pane.append_at = null; - pane.sticky_col = -1; + pane.body.vsel = .{}; + pane.body.msel = .{}; + pane.body.nsel = 0; + pane.body.select = false; + pane.body.normal.clear(); + pane.body.append_at = null; + pane.body.sticky_col = -1; for (0..pane.sel.len) |slot| pane.clearPointerSelection(slot); pane.sel = @splat(.{}); if (comptime pdf_enabled) if (pane.pdf) |*state| { @@ -1150,7 +1150,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { const found = resolveLookTarget(p, id, txt, &realbuf); switch (found) { .none => { - const bmode = if (pane.tag_edit) pane.tag_mode else pane.mode; + const bmode = if (pane.tag_edit) pane.tag_mode else pane.body.mode; if (pane.image != null or bmode == .tty) return; runSearch(p, id, trimmed, .text, .cursor) catch |err| { p.reportError(id, "search", err); @@ -1203,7 +1203,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { const free = p.freeSlot() orelse return p.reportError(id, "look", error.NoPaneSlots); const nt = panes.File.open(p, free, target.path, target.at.line) catch |err| return p.reportError(id, "look", err); - if (target.at.col > 0) nt.cur_col = @intCast(target.at.col - 1); + if (target.at.col > 0) nt.body.cur_col = @intCast(target.at.col - 1); exec.placeDoc(p, id, free, nt); layout.compute(p); nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; @@ -1249,7 +1249,7 @@ test "Look hover waits without mutating the pane and input cancels it" { const selections_before = pane.sel; const active_before = p.active; - const cursor_before = .{ pane.cur_row, pane.cur_col }; + const cursor_before = .{ pane.body.cur_row, pane.body.cur_col }; const jumps_before = .{ p.njumps, p.jcur, p.n_look_src }; const effects_before = p.effects_len; @@ -1276,7 +1276,7 @@ test "Look hover waits without mutating the pane and input cancels it" { // Resolving and displaying the operand is observational only. try std.testing.expect(std.meta.eql(selections_before, pane.sel)); try std.testing.expectEqual(active_before, p.active); - try std.testing.expectEqual(cursor_before, .{ pane.cur_row, pane.cur_col }); + try std.testing.expectEqual(cursor_before, .{ pane.body.cur_row, pane.body.cur_col }); try std.testing.expectEqual(jumps_before, .{ p.njumps, p.jcur, p.n_look_src }); try std.testing.expectEqual(effects_before, p.effects_len); @@ -1436,8 +1436,8 @@ test "Look copies its operand before releasing pointer selection storage" { }; p.drag = .{ .select = .{ .id = 0, .button = config.select_button } }; lookAt(p, 0, operand); - try std.testing.expectEqual(@as(i32, 2), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expect(pane.pointer_selections[0] == null); try std.testing.expectEqual(.none, pane.sel[0].state); try std.testing.expectEqual(.none, p.drag); @@ -1448,10 +1448,10 @@ test "Look ignores missing and out of bounds pane addresses" { defer p.deinit(); const pane = try p.setTestFile("alpha\nbravo\n"); for ([_][]const u8{ config.pane_addr ++ "15:2", config.pane_addr ++ "999999:2" }) |operand| { - pane.setRange(pane.file.?.content, 0, .{ .anchor = 0, .head = 3 }, true); + pane.body.setRange(pane.file.?.content, 0, .{ .anchor = 0, .head = 3 }, true); lookAt(p, 0, operand); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expect(!pane.vsel.active); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expect(!pane.body.vsel.active); } } diff --git a/src/macos.zig b/src/macos.zig index ccbb5dd5..c9c83a2d 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -338,8 +338,8 @@ test "mac worker setup failures finish matching LSP and pipe requests" { try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; core.update(.{ .key = .{ .cp = '|' } }); core.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } }); core.update(.{ .key = .{ .cp = pardes.Key.enter } }); @@ -1465,8 +1465,8 @@ test "mac Restore keeps host state and rejects callbacks from the old core" { try std.testing.expect(st.core.lsp_wait == null); const pane = st.core.panes[0].?; - pane.cur_col = 4; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 4; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; st.core.update(.{ .key = .{ .cp = '|' } }); st.core.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } }); st.core.update(.{ .key = .{ .cp = pardes.Key.enter } }); diff --git a/src/mouse.zig b/src/mouse.zig index 2a00bded..a840c8fc 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -378,7 +378,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { // next cursor move or left wheel pulls it back } else if (pane.file != null and !p.settings.wrap) { // wrapped there is nothing off to the right to reach - const line = panes.File.sourceLine(pane, pane.cur_row); + const line = panes.File.sourceLine(pane, pane.body.cur_row); const visual = panes.File.rawDisplayCol(line, @intCast(@max(0, pane.hscroll))); const next: usize = if (m.button == .wheel_right) visual +| @as(usize, @intCast(config.wheel_cols)) @@ -682,7 +682,7 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand: const top = @min(sel.r0, sel.r1) - @as(i32, BOX_H); if (top < 0) break :lead null; const w = pane.wrapAt(top); - const col = edit.paneByteAtDisplay(p, pane, w.line, w.at, @min(sel.c0, sel.c1) - panes.File.gutterWidth(pane)); + const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, @min(sel.c0, sel.c1) - panes.File.gutterWidth(pane)); break :lead .{ .row = @intCast(@max(0, w.line)), .col = @intCast(@max(0, col)), @@ -720,22 +720,22 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void { const body = body_layer.bodyText(p, p.scratch.allocator(), pane) catch return; const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane)))); const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo; - pane.cur_row = pane.surfRow(endpoint.row); + pane.body.cur_row = pane.surfRow(endpoint.row); var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t"); if (pane.ovl) |overlay| { - const relative = pane.cur_row - overlay.row; + const relative = pane.body.cur_row - overlay.row; if (relative >= 0 and relative < modal.lineCount(overlay.text)) shown = modal.lineSlice(overlay.text, @intCast(relative)); } - pane.cur_col = @intCast(modal.graphemeStart(shown, @min(shown.len, byte -| endpoint.prompt_bytes))); - pane.cur_pinned = true; - pane.msel.active = false; - pane.vsel.active = false; - pane.nsel = 0; - pane.select = false; - pane.append_at = null; - pane.sticky_col = -1; - pane.normal.clear(); + pane.body.cur_col = @intCast(modal.graphemeStart(shown, @min(shown.len, byte -| endpoint.prompt_bytes))); + pane.body.cur_pinned = true; + pane.body.msel.active = false; + pane.body.vsel.active = false; + pane.body.nsel = 0; + pane.body.select = false; + pane.body.append_at = null; + pane.body.sticky_col = -1; + pane.body.normal.clear(); edit.setYank(p, text); } @@ -813,27 +813,27 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1; if (!dragged) pane.sel[sel_slot].state = .none; if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {}; - if (dragged and pane.mode == .tty) mirrorTtySelection(p, pane); + if (dragged and pane.body.mode == .tty) mirrorTtySelection(p, pane); const body_vis = sl.r1 - @as(i32, BOX_H); - if (body_vis >= 0 and pane.mode != .tty) { + if (body_vis >= 0 and pane.body.mode != .tty) { const w = pane.wrapAt(body_vis); - pane.cur_row = w.line; - pane.cur_col = edit.paneByteAtDisplay( + pane.body.cur_row = w.line; + pane.body.cur_col = edit.paneByteAtDisplay( p, - pane, + &pane.body, w.line, w.at, sl.c1 - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0), ); - pane.cur_pinned = true; - if (edit.clampFileCursor(pane)) pane.ensureCursorVisible(); - pane.msel.active = false; - pane.normal.clear(); - pane.vsel.active = false; - pane.nsel = 0; // a click says WHERE the one cursor is - pane.select = false; - pane.append_at = null; - pane.sticky_col = -1; + pane.body.cur_pinned = true; + if (edit.clampFileCursor(&pane.body)) pane.ensureCursorVisible(); + pane.body.msel.active = false; + pane.body.normal.clear(); + pane.body.vsel.active = false; + pane.body.nsel = 0; // a click says WHERE the one cursor is + pane.body.select = false; + pane.body.append_at = null; + pane.body.sticky_col = -1; if (s.ctrl) p.lspRequest(s.id, .definition, ""); } } else { @@ -842,7 +842,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { // this file -- a sticky context header's number included, // since the row map holds each header's own source line. if (s.button == config.look_button and clk.r0 >= BOX_H and clk.r0 == clk.r1 and - clk.c0 == clk.c1 and clk.c0 >= 0 and pane.mode != .tty) + clk.c0 == clk.c1 and clk.c0 >= 0 and pane.body.mode != .tty) if (pane.file) |f| if (f.output == null and clk.c0 < panes.File.gutterWidth(pane)) { const line = pane.wrapAt(clk.r0 - BOX_H).line; if (line >= 0) { @@ -858,10 +858,10 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { pane.sel[b] = expanded else if (operand.text == null) pane.sel[b].state = .none; - if (s.button == config.look_button and clk.r0 >= BOX_H and pane.mode != .tty) { - pane.cur_row = operand.row; - pane.cur_col = operand.col; - if (edit.clampFileCursor(pane)) pane.ensureCursorVisible(); + if (s.button == config.look_button and clk.r0 >= BOX_H and pane.body.mode != .tty) { + pane.body.cur_row = operand.row; + pane.body.cur_col = operand.col; + if (edit.clampFileCursor(&pane.body)) pane.ensureCursorVisible(); } const txt = operand.text; const cmd = if (s.button == config.look_button) config.look_cmd else config.exec_cmd; @@ -909,7 +909,7 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { if (cut and pv.selection_text.len > 0) edit.setYank(p, pv.selection_text); return; }; - if (pane.mode == .tty) { + if (pane.body.mode == .tty) { if (!s.chorded) { s.chorded = true; pane.sel[sel_slot].state = .none; // drop the sweep highlight @@ -944,44 +944,44 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { const w1 = pane.wrapAt(@max(0, sl.r1 - @as(i32, BOX_H))); const row0 = w0.line; const row1 = w1.line; - const col0 = edit.paneByteAtDisplay(p, pane, w0.line, w0.at, sl.c0 - pfx); - const col1 = edit.paneByteAtDisplay(p, pane, w1.line, w1.at, sl.c1 - pfx); - pane.cur_row = row1; - pane.cur_col = col1; - pane.cur_pinned = true; - pane.msel.active = false; - pane.normal.clear(); - if (!pane.isTerminal()) pane.mode = .normal; - pane.vsel = .{ .active = row0 != row1 or col0 != col1, .row = row0, .col = col0, .explicit = false }; - pane.nsel = 0; + const col0 = edit.paneByteAtDisplay(p, &pane.body, w0.line, w0.at, sl.c0 - pfx); + const col1 = edit.paneByteAtDisplay(p, &pane.body, w1.line, w1.at, sl.c1 - pfx); + pane.body.cur_row = row1; + pane.body.cur_col = col1; + pane.body.cur_pinned = true; + pane.body.msel.active = false; + pane.body.normal.clear(); + if (!pane.isTerminal()) pane.body.mode = .normal; + pane.body.vsel = .{ .active = row0 != row1 or col0 != col1, .row = row0, .col = col0, .explicit = false }; + pane.body.nsel = 0; } if (cut) { - if (pane.vsel.active) edit.normalDelete(p, pane, true); - } else if (pane.vsel.active) { - edit.normalReplaceYank(p, pane); + if (pane.body.vsel.active) edit.normalDelete(p, &pane.body, true); + } else if (pane.body.vsel.active) { + edit.normalReplaceYank(p, &pane.body); } else if (pane.file) |*f| { // bare cursor: splice the register in literally (no linewise // magic) so a paste right after a cut restores the text exactly const y = p.yank orelse return; if (y.len == 0) return; - const at = modal.Cursor{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }; - edit.pushUndo(p, pane); + const at = modal.Cursor{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) }; + edit.pushUndo(p, &pane.body); const new = modal.insertAt(p.gpa, f.content, at, y) catch return; panes.File.setContent(p, f, new); - pane.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @intCast(at.row), .col = @intCast(at.col), .explicit = false }; + pane.body.vsel = .{ .active = modal.nextGrapheme(y, 0) < y.len, .row = @intCast(at.row), .col = @intCast(at.col), .explicit = false }; const end = modal.advanceBy(at, y); if (end.col > 0) { - pane.cur_row = @intCast(end.row); - pane.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(f.content, end.row), end.col)); + pane.body.cur_row = @intCast(end.row); + pane.body.cur_col = @intCast(modal.prevGrapheme(modal.lineSlice(f.content, end.row), end.col)); } else { // the register ended in '\n': the cursor lands ON that newline - pane.cur_row = @intCast(end.row -| 1); - pane.cur_col = @intCast(modal.lineSlice(f.content, end.row -| 1).len); + pane.body.cur_row = @intCast(end.row -| 1); + pane.body.cur_col = @intCast(modal.lineSlice(f.content, end.row -| 1).len); } - pane.cur_pinned = true; - pane.sticky_col = -1; + pane.body.cur_pinned = true; + pane.body.sticky_col = -1; pane.ensureCursorVisible(); - } else edit.pasteText(p, pane, p.yank orelse return, true, 1); + } else edit.pasteText(p, &pane.body, p.yank orelse return, true, 1); } test "plain left click clears explicit modal selection" { @@ -989,10 +989,10 @@ test "plain left click clears explicit modal selection" { const p = try Pardes.init(gpa, .{ .cols = 40, .rows = 10 }); defer p.deinit(); const pane = try p.setTestFile("abcdef\n"); - pane.cur_row = 0; - pane.cur_col = 5; - pane.vsel = .{ .active = true, .row = 0, .col = 1, .explicit = true }; - pane.msel = .{ .active = true, .r0 = 0, .r1 = 0 }; + pane.body.cur_row = 0; + pane.body.cur_col = 5; + pane.body.vsel = .{ .active = true, .row = 0, .col = 1, .explicit = true }; + pane.body.msel = .{ .active = true, .r0 = 0, .r1 = 0 }; var frame = std.heap.ArenaAllocator.init(gpa); defer frame.deinit(); @@ -1003,9 +1003,9 @@ test "plain left click clears explicit modal selection" { p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = click_x, .row = body_y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = click_x, .row = body_y } }); - try std.testing.expectEqual(@as(i32, 3), pane.cur_col); - try std.testing.expect(!pane.vsel.active); - try std.testing.expect(!pane.msel.active); + try std.testing.expectEqual(@as(i32, 3), pane.body.cur_col); + try std.testing.expect(!pane.body.vsel.active); + try std.testing.expect(!pane.body.msel.active); try std.testing.expectEqual(.none, pane.sel[@intFromEnum(config.select_button)].state); } @@ -1412,7 +1412,7 @@ test "mouse thumb buttons navigate once and clear selection gestures" { const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12 }); defer p.deinit(); const pane = try p.setTestFile("alpha beta\ngamma delta\nepsilon zeta\n"); - pane.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); + pane.body.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); p.njumps = 3; p.jcur = 2; for (0..3) |i| p.jumps[i] = .{ .pane = 0, .serial = pane.serial, .line = @intCast(i + 1), .col = 1 }; @@ -1425,6 +1425,6 @@ test "mouse thumb buttons navigate once and clear selection gestures" { p.update(.{ .mouse = .{ .button = .forward, .kind = .press, .col = 0, .row = 0 } }); p.update(.{ .mouse = .{ .button = .forward, .kind = .release, .col = 0, .row = 0 } }); try std.testing.expectEqual(@as(usize, 2), p.jcur); - try std.testing.expect(!pane.vsel.active); + try std.testing.expect(!pane.body.vsel.active); try std.testing.expectEqual(.none, p.drag); } diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 645b1c33..484b08c9 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -297,9 +297,9 @@ test "the pane ctl takes get, lock and unlock, and nothing that a file of its ow defer p.deinit(); const ctl_node = Node.of(serialOf(p), .ctl); for ([_][]const u8{ - "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL", - "name x.txt", "put", "del", "delete", "Look x", "Exec Save", "clean", - "dirty", "cleartag", "dot=addr", "addr=dot", "show", "mark", "nomark", + "menu", "nomenu", "dump echo hi", "font Go Mono", "lock x", "bogus", "DEL", + "name x.txt", "put", "del", "delete", "Look x", "Exec Save", "clean", + "dirty", "cleartag", "dot=addr", "addr=dot", "show", "mark", "nomark", "scroll", "limit=addr", "get x", "look /tmp", "exec Del", }) |bad| try testing.expectEqual(E.INVAL, wr(p, ctl_node, bad).errno()); try testing.expect(p.paneBySerial(serialOf(p)) != null); @@ -447,27 +447,27 @@ test "look opens spaced paths and locations without editing the source pane" { const look_node = Node.of(source.serial, .look); const revision = source.file.?.revision; const undo_len = source.file.?.history.undo_len; - source.cur_col = 3; + source.body.cur_col = 3; var clicked: [4200]u8 = undefined; try testing.expectEqual(Status.ok, wr(p, look_node, try std.fmt.bufPrint(&clicked, "{s}:2:3\n", .{path})).reply.status); const opened = p.panes[p.active].?; try testing.expect(opened != source); try testing.expectEqualStrings(path, opened.file.?.path); try testing.expectEqualStrings("first\nsecond\nthird\n", opened.file.?.content); - try testing.expectEqual(@as(i32, 1), opened.cur_row); - try testing.expectEqual(@as(i32, 2), opened.cur_col); + try testing.expectEqual(@as(i32, 1), opened.body.cur_row); + try testing.expectEqual(@as(i32, 2), opened.body.cur_col); try testing.expectEqualStrings("source stays intact\n", source.file.?.content); try testing.expectEqual(revision, source.file.?.revision); try testing.expectEqual(undo_len, source.file.?.history.undo_len); - try testing.expectEqual(@as(i32, 3), source.cur_col); + try testing.expectEqual(@as(i32, 3), source.body.cur_col); var expected: [16]u8 = undefined; try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_look, 0, 4096).bytes); p.active = 0; try testing.expectEqual(Status.ok, wr(p, root_look, try std.fmt.bufPrint(&clicked, "{s}:3:2\n", .{path})).reply.status); try testing.expect(p.panes[p.active].? == opened); - try testing.expectEqual(@as(i32, 2), opened.cur_row); - try testing.expectEqual(@as(i32, 1), opened.cur_col); + try testing.expectEqual(@as(i32, 2), opened.body.cur_row); + try testing.expectEqual(@as(i32, 1), opened.body.cur_col); try testing.expectEqualStrings(try std.fmt.bufPrint(&expected, "{d}\n", .{opened.serial}), rd(p, root_look, 0, 4096).bytes); } diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index 76854dbc..7e9cf7e7 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -124,9 +124,9 @@ fn cellOf(row: i32, col: i32) modal.Cursor { pub fn dotOf(pane: *Pane) State.Range { const text = bodyOf(pane); - const head = modal.offsetAt(text, cellOf(pane.cur_row, pane.cur_col)); - if (!pane.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) }; - const anchor = modal.offsetAt(text, cellOf(pane.vsel.row, pane.vsel.col)); + const head = modal.offsetAt(text, cellOf(pane.body.cur_row, pane.body.cur_col)); + if (!pane.body.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) }; + const anchor = modal.offsetAt(text, cellOf(pane.body.vsel.row, pane.body.vsel.col)); var hi = @max(head, anchor); if (hi < text.len) hi = modal.nextGrapheme(text, hi); return .{ .q0 = clip(@min(head, anchor)), .q1 = clip(hi) }; @@ -137,23 +137,23 @@ pub fn setDot(pane: *Pane, r: State.Range) void { const q0 = @min(@as(usize, r.q0), text.len); const q1 = @max(q0, @min(@as(usize, r.q1), text.len)); const a = modal.positionAt(text, q0); - pane.vsel = .{ .active = q1 > q0, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; + pane.body.vsel = .{ .active = q1 > q0, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; const h = modal.positionAt(text, if (q1 > q0) modal.prevGrapheme(text, q1) else q0); - pane.cur_row = @intCast(h.row); - pane.cur_col = @intCast(h.col); - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.msel.active = false; + pane.body.cur_row = @intCast(h.row); + pane.body.cur_col = @intCast(h.col); + pane.body.cur_pinned = true; + pane.body.sticky_col = -1; + pane.body.msel.active = false; pane.ensureCursorVisible(); } pub fn showOffset(pane: *Pane, off: usize) void { const text = bodyOf(pane); const c = modal.positionAt(text, @min(off, text.len)); - pane.cur_row = @intCast(c.row); - pane.cur_col = @intCast(c.col); - pane.cur_pinned = true; - pane.sticky_col = -1; + pane.body.cur_row = @intCast(c.row); + pane.body.cur_col = @intCast(c.col); + pane.body.cur_pinned = true; + pane.body.sticky_col = -1; pane.ensureCursorVisible(); } diff --git a/src/normal.zig b/src/normal.zig index 6647dcca..192fd933 100644 --- a/src/normal.zig +++ b/src/normal.zig @@ -12,6 +12,7 @@ const edit = @import("edit.zig"); const config = @import("config.zig"); const pdf_enabled = panes.Pdf.enabled; const Pane = panes.Pane; +const Text = panes.Text; const Key = pardes.Key; const hit = pardes.hit; const sel_slot = pardes.sel_slot; @@ -66,11 +67,11 @@ pub fn multiOnce(p: *Pardes) bool { } fn startSelRegex(p: *Pardes, pane: *Pane, split: bool) void { - const text = edit.flatSurface(p, pane) catch return; - const got = pane.ranges(text, 0, &pane.sel_snap); + const text = edit.flatSurface(p, &pane.body) catch return; + const got = pane.body.ranges(text, 0, &pane.sel_snap); pane.nsel_snap = @intCast(got.n); pane.sel_snap_pri = @intCast(got.pri); - pane.sel_snap_expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; + pane.sel_snap_expl = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active; p.startPrompt(pane, .{ .search = if (split) config.split_marker else config.select_marker }); } @@ -89,9 +90,9 @@ pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } { } pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void { - const text = edit.flatSurface(p, pane) catch return; + const text = edit.flatSurface(p, &pane.body) catch return; const snap = pane.sel_snap[0..pane.nsel_snap]; - var out: [Pane.max_selections]modal.Selection = undefined; + var out: [Text.max_selections]modal.Selection = undefined; var m: usize = 0; if (pat.len > 0) if (mvzr.compile(pat)) |re| { var hay_all = text; @@ -109,7 +110,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void const hay = hay_all[from..to]; var at: usize = 0; var piece = from; // split: where the next piece begins - while (at < hay.len and m < Pane.max_selections) { + while (at < hay.len and m < Text.max_selections) { const hit_at = re.matchPos(at, hay) orelse break; if (split) { out[m] = .{ .anchor = piece, .head = from + hit_at.start }; @@ -122,7 +123,7 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void // an empty match would otherwise never advance at = if (hit_at.end > hit_at.start) hit_at.end else hit_at.end + 1; } - if (split and piece < to and m < Pane.max_selections) { + if (split and piece < to and m < Text.max_selections) { out[m] = .{ .anchor = piece, .head = to }; m += 1; } @@ -135,37 +136,37 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void r.anchor = @min(r.anchor, text.len); r.head = @min(r.head, text.len); } - return pane.setRanges(text, snap, &.{}, pane.sel_snap_pri, pane.sel_snap_expl); + return pane.body.setRanges(text, snap, &.{}, pane.sel_snap_pri, pane.sel_snap_expl); } - pane.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO) + pane.body.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO) } const KeyState = struct { - mode: Pane.Mode, + mode: Text.Mode, select: bool, normal: modal.Normal.State, find_op: u8, find_ch: u21, - append_at: @FieldType(Pane, "append_at"), + append_at: @FieldType(Text, "append_at"), - fn of(pane: *Pane) KeyState { + fn of(t: *Text) KeyState { return .{ - .mode = pane.mode, - .select = pane.select, - .normal = pane.normal, - .find_op = pane.find_op, - .find_ch = pane.find_ch, - .append_at = pane.append_at, + .mode = t.mode, + .select = t.select, + .normal = t.normal, + .find_op = t.find_op, + .find_ch = t.find_ch, + .append_at = t.append_at, }; } - fn into(s: KeyState, pane: *Pane) void { - pane.mode = s.mode; - pane.select = s.select; - pane.normal = s.normal; - pane.find_op = s.find_op; - pane.find_ch = s.find_ch; - pane.append_at = s.append_at; + fn into(s: KeyState, t: *Text) void { + t.mode = s.mode; + t.select = s.select; + t.normal = s.normal; + t.find_op = s.find_op; + t.find_ch = s.find_ch; + t.append_at = s.append_at; } }; @@ -175,25 +176,25 @@ const Replay = union(enum) { insert: Key, }; -pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { +pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void { const id = p.active; - const serial = pane.serial; - // the whole selection in pane coordinates, document order. pane.sels + const serial = t.pane().serial; + // the whole selection in text coordinates, document order. t.sels // is already ordered, so this only slots the primary into place. - var list: [Pane.max_selections]Pane.SelRange = undefined; + var list: [Text.max_selections]Text.SelRange = undefined; var n: usize = 0; var pri: usize = 0; - const prim: Pane.SelRange = .{ - .row = pane.cur_row, - .col = pane.cur_col, - .arow = if (pane.vsel.active) pane.vsel.row else pane.cur_row, - .acol = if (pane.vsel.active) pane.vsel.col else pane.cur_col, - .sticky = pane.sticky_col, + const prim: Text.SelRange = .{ + .row = t.cur_row, + .col = t.cur_col, + .arow = if (t.vsel.active) t.vsel.row else t.cur_row, + .acol = if (t.vsel.active) t.vsel.col else t.cur_col, + .sticky = t.sticky_col, }; - const pr = Pane.selStart(prim); + const pr = Text.selStart(prim); var placed = false; - for (pane.sels[0..pane.nsel]) |s| { - const sr = Pane.selStart(s); + for (t.sels[0..t.nsel]) |s| { + const sr = Text.selStart(s); if (!placed and (pr.row < sr.row or (pr.row == sr.row and pr.col <= sr.col))) { pri = n; list[n] = prim; @@ -209,13 +210,13 @@ pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { n += 1; } - const saved = KeyState.of(pane); - const explicit = pane.vsel.explicit; + const saved = KeyState.of(t); + const explicit = t.vsel.explicit; var after = saved; var after_expl = explicit; // each pass's result: cursor and anchor cells as distances from the // end of the surface text, plus the range's own j/k goal column - var res: [Pane.max_selections]struct { cur: usize, anc: usize, sticky: i32 } = undefined; + var res: [Text.max_selections]struct { cur: usize, anc: usize, sticky: i32 } = undefined; p.multi_on = true; p.multi_stop = false; var passes: usize = 0; @@ -224,98 +225,99 @@ pub fn replaySels(p: *Pardes, pane: *Pane, what: Replay) void { i -= 1; p.multi_first = passes == 0; passes += 1; - saved.into(pane); - pane.nsel = 0; // the handler underneath sees ONE selection - pane.cur_row = list[i].row; - pane.cur_col = list[i].col; - pane.vsel = .{ - .active = list[i].arow != list[i].row or list[i].acol != list[i].col or pane.select, + saved.into(t); + t.nsel = 0; // the handler underneath sees ONE selection + t.cur_row = list[i].row; + t.cur_col = list[i].col; + t.vsel = .{ + .active = list[i].arow != list[i].row or list[i].acol != list[i].col or t.select, .row = list[i].arow, .col = list[i].acol, .explicit = explicit, }; - pane.msel.active = false; - pane.sticky_col = list[i].sticky; + t.msel.active = false; + t.sticky_col = list[i].sticky; switch (what) { - .normal => |normal_action| executeNormalAction(p, pane, normal_action), - .insert => |insert_key| edit.insertKey(p, pane, insert_key), + .normal => |normal_action| executeNormalAction(p, t, normal_action), + .insert => |insert_key| edit.insertKey(p, t, insert_key), } if (p.multi_stop) break; if (i == pri) { - after = KeyState.of(pane); - after_expl = pane.vsel.explicit; + after = KeyState.of(t); + after_expl = t.vsel.explicit; } - const text = edit.flatSurface(p, pane) catch { + const text = edit.flatSurface(p, t) catch { p.multi_stop = true; break; }; - const co = modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }); - const ao = if (pane.vsel.active) - modal.offsetAt(text, .{ .row = @intCast(@max(0, pane.vsel.row)), .col = @intCast(@max(0, pane.vsel.col)) }) + const co = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) }); + const ao = if (t.vsel.active) + modal.offsetAt(text, .{ .row = @intCast(@max(0, t.vsel.row)), .col = @intCast(@max(0, t.vsel.col)) }) else co; - res[i] = .{ .cur = text.len - @min(co, text.len), .anc = text.len - @min(ao, text.len), .sticky = pane.sticky_col }; + res[i] = .{ .cur = text.len - @min(co, text.len), .anc = text.len - @min(ao, text.len), .sticky = t.sticky_col }; } p.multi_on = false; p.multi_first = false; if (p.multi_stop) { p.multi_stop = false; const pn = p.panes[id] orelse return; - if (pn.serial == serial) pn.nsel = 0; + if (pn.serial == serial) pn.body.nsel = 0; return; } - const text = edit.flatSurface(p, pane) catch return; - var rs: [Pane.max_selections]modal.Selection = undefined; - var st: [Pane.max_selections]i32 = undefined; + const text = edit.flatSurface(p, t) catch return; + var rs: [Text.max_selections]modal.Selection = undefined; + var st: [Text.max_selections]i32 = undefined; for (res[0..n], 0..) |r, k| { - rs[k] = Pane.cellOffRange(text, text.len - @min(r.anc, text.len), text.len - @min(r.cur, text.len)); + rs[k] = Text.cellOffRange(text, text.len - @min(r.anc, text.len), text.len - @min(r.cur, text.len)); st[k] = r.sticky; } - pane.setRanges(text, rs[0..n], st[0..n], pri, after_expl); - after.into(pane); - pane.ensureCursorVisible(); // the view follows the PRIMARY, not the last pass + t.setRanges(text, rs[0..n], st[0..n], pri, after_expl); + after.into(t); + t.pane().ensureCursorVisible(); // the view follows the PRIMARY, not the last pass } -pub fn handleNormal(p: *Pardes, pane: *Pane, key: Key) void { - const parsed = modal.Normal.parse(&pane.normal, normalInput(key)); +pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void { + const parsed = modal.Normal.parse(&t.normal, normalInput(key)); const semantic = switch (parsed) { .pending, .ignored, .unbound => return, .action => |value| value, }; - if (pane.nsel == 0 or semantic.scope() == .once) - return executeNormalAction(p, pane, semantic); - replaySels(p, pane, .{ .normal = semantic }); + if (t.nsel == 0 or semantic.scope() == .once) + return executeNormalAction(p, t, semantic); + replaySels(p, t, .{ .normal = semantic }); } -fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) void { +fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void { + const pane = t.pane(); const raw_selection = pane.rawPointerText(pane.sel[sel_slot]) != null; - const before = .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode }; - defer if (raw_selection and !std.meta.eql(before, .{ pane.cur_row, pane.cur_col, pane.vsel, pane.msel, pane.select, pane.nsel, pane.mode })) { + const before = .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.select, t.nsel, t.mode }; + defer if (raw_selection and !std.meta.eql(before, .{ t.cur_row, t.cur_col, t.vsel, t.msel, t.select, t.nsel, t.mode })) { pane.clearPointerSelection(sel_slot); pane.sel[sel_slot].state = .none; }; - if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return edit.normalYank(p, pane); - edit.pinPaneCursor(p, pane); - const text = edit.flatSurface(p, pane) catch return; - const range = pane.primaryRange(text, 0); + if (raw_selection and semantic == .edit and semantic.edit.kind == .yank) return edit.normalYank(p, t); + edit.pinPaneCursor(p, t); + const text = edit.flatSurface(p, t) catch return; + const range = t.primaryRange(text, 0); const cur = modal.selectionCursor(text, range); switch (semantic) { .escape => { - pane.select = false; + t.select = false; return exec.runBuiltin(p, .Last, p.active, "", null); }, .goto => |go| switch (go.target) { .file_start => { - const line = if (go.explicit_count) @min(@as(usize, go.count) - 1, Pane.maxLine(text)) else 0; - return pane.pointMove(text, range, modal.lineStartOffset(text, line)); + const line = if (go.explicit_count) @min(@as(usize, go.count) - 1, Text.maxLine(text)) else 0; + return t.pointMove(text, range, modal.lineStartOffset(text, line)); }, - .last_line => return pane.pointMove(text, range, modal.lineStartOffset(text, Pane.maxLine(text))), - .line_start => return pane.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))), + .last_line => return t.pointMove(text, range, modal.lineStartOffset(text, Text.maxLine(text))), + .line_start => return t.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))), .line_end => { const line = modal.lineAtOffset(text, cur); const ls = modal.lineStartOffset(text, line); - return pane.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); + return t.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); }, .first_nonws => { const line = modal.lineAtOffset(text, cur); @@ -323,35 +325,35 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v const slice = text[ls..modal.lineEndOffset(text, line)]; const nw = modal.firstNonWs(slice); if (nw == slice.len) return; - return pane.pointMove(text, range, ls + nw); + return t.pointMove(text, range, ls + nw); }, - .line_down => return pane.visualMove(text, range, true, go.count, edit.paneWrapWidth(p, pane)), - .line_up => return pane.visualMove(text, range, false, go.count, edit.paneWrapWidth(p, pane)), + .line_down => return t.visualMove(text, range, true, go.count, edit.paneWrapWidth(p, t)), + .line_up => return t.visualMove(text, range, false, go.count, edit.paneWrapWidth(p, t)), .column => { const line = modal.lineAtOffset(text, cur); const ls = modal.lineStartOffset(text, line); const slice = text[ls..modal.lineEndOffset(text, line)]; - return pane.pointMove(text, range, ls + modal.graphemeAtColumn(slice, @as(usize, go.count) - 1)); + return t.pointMove(text, range, ls + modal.graphemeAtColumn(slice, @as(usize, go.count) - 1)); }, - .view_top => return pane.gotoWindow(text, range, .top, go.count), - .view_center => return pane.gotoWindow(text, range, .center, go.count), - .view_bottom => return pane.gotoWindow(text, range, .bottom, go.count), + .view_top => return t.gotoWindow(text, range, .top, go.count), + .view_center => return t.gotoWindow(text, range, .center, go.count), + .view_bottom => return t.gotoWindow(text, range, .bottom, go.count), }, .view => |view| switch (view) { .top => { - pane.scrollBy(pane.cur_row - pane.scroll()); + pane.scrollBy(t.cur_row - pane.scroll()); pane.ensureCursorVisible(); }, .center => { - pane.scrollBy(pane.cur_row - (pane.scroll() + @divTrunc(@as(i32, pane.rows), 2) - 1)); + pane.scrollBy(t.cur_row - (pane.scroll() + @divTrunc(@as(i32, pane.rows), 2) - 1)); pane.ensureCursorVisible(); }, .bottom => { - pane.scrollBy(pane.cur_row - (pane.scroll() + @as(i32, pane.rows) - 1)); + pane.scrollBy(t.cur_row - (pane.scroll() + @as(i32, pane.rows) - 1)); pane.ensureCursorVisible(); }, - .scroll_down => return pane.scrollViewMove(text, range, 1), - .scroll_up => return pane.scrollViewMove(text, range, -1), + .scroll_down => return t.scrollViewMove(text, range, 1), + .scroll_up => return t.scrollViewMove(text, range, -1), }, .find => |find| { const op: u8 = switch (find.kind) { @@ -360,9 +362,9 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .till_forward => @intCast(config.till_char_fwd), .till_backward => @intCast(config.till_char_back), }; - pane.find_op = op; - pane.find_ch = find.char; - return pane.findMove( + t.find_op = op; + t.find_ch = find.char; + return t.findMove( text, range, find.char, @@ -371,21 +373,21 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v find.count, ); }, - .replace_char => |char| return edit.normalReplaceChar(p, pane, char), + .replace_char => |char| return edit.normalReplaceChar(p, t, char), .match_bracket => { - const lines = edit.paneCursorLines(p, pane) catch return; + const lines = edit.paneCursorLines(p, t) catch return; const mc = modal.matchBracket(lines, modal.positionAt(text, cur)) orelse return; - return pane.pointMove(text, range, modal.offsetAt(text, mc)); + return t.pointMove(text, range, modal.offsetAt(text, mc)); }, - .textobject => |object| return edit.textobjectSelect(p, pane, text, object.char, object.around), - .surround_add => |char| return edit.surroundAdd(p, pane, char), - .surround_delete => |char| return edit.surroundDelete(p, pane, char), - .surround_replace => |replace| return edit.surroundReplace(p, pane, replace.from, replace.to), + .textobject => |object| return edit.textobjectSelect(p, t, text, object.char, object.around), + .surround_add => |char| return edit.surroundAdd(p, t, char), + .surround_delete => |char| return edit.surroundDelete(p, t, char), + .surround_replace => |replace| return edit.surroundReplace(p, t, replace.from, replace.to), .paragraph => |paragraph| { - const r2 = modal.moveParagraph(text, range, paragraph.count, paragraph.direction == .forward, pane.select); - return pane.setRange(text, 0, r2, false); + const r2 = modal.moveParagraph(text, range, paragraph.count, paragraph.direction == .forward, t.select); + return t.setRange(text, 0, r2, false); }, - .add_newline => |newline| return edit.addNewline(p, pane, newline.direction == .forward, newline.count), + .add_newline => |newline| return edit.addNewline(p, t, newline.direction == .forward, newline.count), .diagnostic => |diagnostic| { const fwd = diagnostic.direction == .forward; if (!diagnostic.endpoint) { @@ -409,46 +411,46 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .left => { var target = cur; for (0..move.count) |_| target = modal.prevGrapheme(text, target); - return pane.pointMove(text, range, target); + return t.pointMove(text, range, target); }, .right => { var target = cur; for (0..move.count) |_| target = modal.nextGrapheme(text, target); - return pane.pointMove(text, range, target); + return t.pointMove(text, range, target); }, - .down => return pane.verticalMove(text, range, true, move.count), - .up => return pane.verticalMove(text, range, false, move.count), - .next_word_start => return pane.wordMove(text, range, move.count, .next_word_start), - .prev_word_start => return pane.wordMove(text, range, move.count, .prev_word_start), - .next_word_end => return pane.wordMove(text, range, move.count, .next_word_end), - .next_long_word_start => return pane.wordMove(text, range, move.count, .next_long_word_start), - .prev_long_word_start => return pane.wordMove(text, range, move.count, .prev_long_word_start), - .next_long_word_end => return pane.wordMove(text, range, move.count, .next_long_word_end), + .down => return t.verticalMove(text, range, true, move.count), + .up => return t.verticalMove(text, range, false, move.count), + .next_word_start => return t.wordMove(text, range, move.count, .next_word_start), + .prev_word_start => return t.wordMove(text, range, move.count, .prev_word_start), + .next_word_end => return t.wordMove(text, range, move.count, .next_word_end), + .next_long_word_start => return t.wordMove(text, range, move.count, .next_long_word_start), + .prev_long_word_start => return t.wordMove(text, range, move.count, .prev_long_word_start), + .next_long_word_end => return t.wordMove(text, range, move.count, .next_long_word_end), }, .repeat_find => |count| { - if (pane.find_op == 0) return; - const fwd = pane.find_op == config.find_char_fwd or pane.find_op == config.till_char_fwd; - const till = pane.find_op == config.till_char_fwd or pane.find_op == config.till_char_back; + if (t.find_op == 0) return; + const fwd = t.find_op == config.find_char_fwd or t.find_op == config.till_char_fwd; + const till = t.find_op == config.till_char_fwd or t.find_op == config.till_char_back; var repeated = range; var moved = false; for (0..count) |_| { const cc = modal.selectionCursor(text, repeated); - const target = modal.findTarget(text, cc, pane.find_ch, fwd, till, 1) orelse break; - repeated = if (pane.select) + const target = modal.findTarget(text, cc, t.find_ch, fwd, till, 1) orelse break; + repeated = if (t.select) modal.moveSelectionCursor(text, repeated, target, true) else modal.moveSelectionCursor(text, .{ .anchor = cc, .head = cc }, target, true); moved = true; } if (!moved) return; - return pane.setRange(text, 0, repeated, false); + return t.setRange(text, 0, repeated, false); }, .line => |line_kind| switch (line_kind) { - .start => return pane.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))), + .start => return t.pointMove(text, range, modal.lineStartOffset(text, modal.lineAtOffset(text, cur))), .end => { const line = modal.lineAtOffset(text, cur); const ls = modal.lineStartOffset(text, line); - return pane.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); + return t.pointMove(text, range, @max(ls, modal.prevGrapheme(text, modal.lineEndOffset(text, line)))); }, .first_nonws => { const line = modal.lineAtOffset(text, cur); @@ -456,21 +458,21 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v const slice = text[ls..modal.lineEndOffset(text, line)]; const nw = modal.firstNonWs(slice); if (nw == slice.len) return; - return pane.pointMove(text, range, ls + nw); + return t.pointMove(text, range, ls + nw); }, }, .goto_line => |go| { if (!go.explicit) return; - const line = @min(@as(usize, go.count) - 1, Pane.maxLine(text)); - return pane.pointMove(text, range, modal.lineStartOffset(text, line)); + const line = @min(@as(usize, go.count) - 1, Text.maxLine(text)); + return t.pointMove(text, range, modal.lineStartOffset(text, line)); }, .page => |page| switch (page.kind) { // Counts were parsed historically but these four text view // actions intentionally move exactly one viewport unit. - .half_down => return pane.halfPageMove(text, range, true), - .half_up => return pane.halfPageMove(text, range, false), - .down => return pane.scrollViewMove(text, range, @as(i32, pane.rows)), - .up => return pane.scrollViewMove(text, range, -@as(i32, pane.rows)), + .half_down => return t.halfPageMove(text, range, true), + .half_up => return t.halfPageMove(text, range, false), + .down => return t.scrollViewMove(text, range, @as(i32, pane.rows)), + .up => return t.scrollViewMove(text, range, -@as(i32, pane.rows)), }, .insert => |insert| { const where: edit.InsertAt = switch (insert.kind) { @@ -481,59 +483,59 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .open_below => .open_below, .open_above => .open_above, }; - return edit.enterInsert(p, pane, where, insert.count); + return edit.enterInsert(p, t, where, insert.count); }, .select => |select| switch (select.kind) { .mode => { - if (pane.select) { - pane.select = false; + if (t.select) { + t.select = false; } else { - pane.select = true; - if (pane.msel.active) { - pane.setRange(text, 0, range, true); - } else if (!pane.vsel.active) { - pane.vsel = .{ .active = true, .row = pane.cur_row, .col = pane.cur_col, .explicit = true }; + t.select = true; + if (t.msel.active) { + t.setRange(text, 0, range, true); + } else if (!t.vsel.active) { + t.vsel = .{ .active = true, .row = t.cur_row, .col = t.cur_col, .explicit = true }; } else { - pane.vsel.explicit = true; + t.vsel.explicit = true; } } - pane.cur_pinned = true; + t.cur_pinned = true; }, - .line => return pane.lineSelect(text, range, select.count), - .line_bounds => return pane.lineBoundsSelect(text, range), - .shrink_to_line_bounds => return pane.shrinkSelToLineBounds(text, range), - .collapse => return pane.setRange(text, 0, .{ .anchor = cur, .head = cur }, false), + .line => return t.lineSelect(text, range, select.count), + .line_bounds => return t.lineBoundsSelect(text, range), + .shrink_to_line_bounds => return t.shrinkSelToLineBounds(text, range), + .collapse => return t.setRange(text, 0, .{ .anchor = cur, .head = cur }, false), .flip => { - const explicit = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; - return pane.setRange(text, 0, .{ .anchor = range.head, .head = range.anchor }, explicit); + const explicit = (t.vsel.active and t.vsel.explicit) or t.msel.active; + return t.setRange(text, 0, .{ .anchor = range.head, .head = range.anchor }, explicit); }, - .all => return pane.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false), + .all => return t.setRange(text, 0, .{ .anchor = 0, .head = text.len }, false), }, .multi => |multi| { if (multi.kind == .keep_primary) { - pane.nsel = 0; + t.nsel = 0; return; } - return pane.multiSelAction(text, multi.kind, multi.count); + return t.multiSelAction(text, multi.kind, multi.count); }, .select_regex => |split| return startSelRegex(p, pane, split), .edit => |op| switch (op.kind) { - .delete => return edit.normalDelete(p, pane, true), - .delete_noyank => return edit.normalDelete(p, pane, false), - .change => return edit.normalChange(p, pane), - .yank => return edit.normalYank(p, pane), - .replace_with_yank => return edit.normalReplaceYank(p, pane), - .paste_after => return edit.pasteText(p, pane, p.yank orelse return, false, op.count), - .paste_before => return edit.pasteText(p, pane, p.yank orelse return, true, op.count), - .switch_case => return edit.normalCase(p, pane, .toggle), - .lowercase => return edit.normalCase(p, pane, .lower), - .uppercase => return edit.normalCase(p, pane, .upper), - .join_lines => return edit.normalJoin(p, pane), - .indent => return edit.normalIndent(p, pane, op.count, true), - .unindent => return edit.normalIndent(p, pane, op.count, false), - .comment_toggle => return edit.normalToggleComment(p, pane), - .undo => return edit.doUndo(p, pane), - .redo => return edit.doRedo(p, pane), + .delete => return edit.normalDelete(p, t, true), + .delete_noyank => return edit.normalDelete(p, t, false), + .change => return edit.normalChange(p, t), + .yank => return edit.normalYank(p, t), + .replace_with_yank => return edit.normalReplaceYank(p, t), + .paste_after => return edit.pasteText(p, t, p.yank orelse return, false, op.count), + .paste_before => return edit.pasteText(p, t, p.yank orelse return, true, op.count), + .switch_case => return edit.normalCase(p, t, .toggle), + .lowercase => return edit.normalCase(p, t, .lower), + .uppercase => return edit.normalCase(p, t, .upper), + .join_lines => return edit.normalJoin(p, t), + .indent => return edit.normalIndent(p, t, op.count, true), + .unindent => return edit.normalIndent(p, t, op.count, false), + .comment_toggle => return edit.normalToggleComment(p, t), + .undo => return edit.doUndo(p, t), + .redo => return edit.doRedo(p, t), }, .lsp => |request| return p.lspRequest(p.active, switch (request) { .definition => .definition, @@ -543,14 +545,14 @@ fn executeNormalAction(p: *Pardes, pane: *Pane, semantic: modal.Normal.Action) v .references => .references, .format => .format, }, ""), - .adjust_number => |delta| return edit.normalAdjustNumber(p, pane, delta), + .adjust_number => |delta| return edit.normalAdjustNumber(p, t, delta), .leader => { p.leader_on = true; p.leader_n = 0; }, .command_line => { p.enterTagEdit(pane, -1); - if (pane.tag_edit) pane.mode = .normal; + if (pane.tag_edit) t.mode = .normal; }, .pipe_selection => |how| return p.startPrompt(pane, .{ .pipe = how }), .search => return p.startPrompt(pane, .{ .search = config.search_marker }), @@ -565,15 +567,15 @@ pub fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void { if (comptime !pdf_enabled) return; if (pane.pdf == null) return; if (hit(key, config.leave_pane)) { - pane.normal.clear(); - pane.select = false; + pane.body.normal.clear(); + pane.body.select = false; return exec.runBuiltin(p, .Last, p.active, "", null); } - if (pane.normal.prefix == .none and isPrefix(key, 'f')) { - pane.normal.clear(); + if (pane.body.normal.prefix == .none and isPrefix(key, 'f')) { + pane.body.normal.clear(); return exec.runBuiltin(p, .PdfSections, p.active, "", null); } - const parsed = modal.Normal.parse(&pane.normal, normalInput(key)); + const parsed = modal.Normal.parse(&pane.body.normal, normalInput(key)); switch (parsed) { .pending, .ignored, .unbound => {}, .action => |semantic| { @@ -595,7 +597,7 @@ pub fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void { }, .command_line => { p.enterTagEdit(pane, -1); - if (pane.tag_edit) pane.mode = .normal; + if (pane.tag_edit) pane.body.mode = .normal; }, .search => p.startPrompt(pane, .{ .search = config.search_marker }), .search_forward => look.lookWalk(p, 1), @@ -618,57 +620,57 @@ test "gj/gk step the wrapped rows a body draws while j/k keep the file's lines" // gj holds the column INSIDE the row and lands on the next break; the line // is one line the whole way down, which is the whole point. - pane.cur_row = 0; - pane.cur_col = 3; - pane.sticky_col = -1; + pane.body.cur_row = 0; + pane.body.cur_col = 3; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.body.cur_col); p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, @intCast(width * 2 + 3)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(width * 2 + 3)), pane.body.cur_col); p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(width + 3)), pane.body.cur_col); // A count is counted in rows, not lines. p.update(.{ .key = .{ .cp = '2' } }); p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, @intCast(width * 3 + 3)), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(width * 3 + 3)), pane.body.cur_col); // Plain j/k are untouched: one FILE line each, whatever the body drew. - pane.cur_col = 3; - pane.sticky_col = -1; + pane.body.cur_col = 3; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); // The last row of a line steps into the next line's first row, and the // first row steps back onto the previous line's LAST row. - pane.cur_row = 0; - pane.cur_col = @intCast(long.len - 1); - pane.sticky_col = -1; + pane.body.cur_row = 0; + pane.body.cur_col = @intCast(long.len - 1); + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); - pane.cur_col = 0; - pane.sticky_col = -1; + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); + pane.body.cur_col = 0; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'k' } }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expect(pane.cur_col >= @as(i32, @intCast(long.len - long.len % width))); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expect(pane.body.cur_col >= @as(i32, @intCast(long.len - long.len % width))); // Wrap OFF: a line is one visual row, so gj/gk ARE j/k again. p.settings.wrap = false; - pane.cur_row = 0; - pane.cur_col = 3; - pane.sticky_col = -1; + pane.body.cur_row = 0; + pane.body.cur_col = 3; + pane.body.sticky_col = -1; p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = 'j' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); } test "flat text movement and selection replay need no scratch rows" { @@ -685,39 +687,39 @@ test "flat text movement and selection replay need no scratch rows" { p.scratch.deinit(); p.scratch = .init(denied.allocator()); - pane.cur_row = 4094; - handleNormal(p, pane, .{ .cp = 'l' }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - handleNormal(p, pane, .{ .cp = 'j' }); - try std.testing.expectEqual(@as(i32, 4095), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - handleNormal(p, pane, .{ .cp = 'h' }); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); - executeNormalAction(p, pane, .{ .textobject = .{ .char = 'w', .around = false } }); - try std.testing.expectEqual(@as(i32, 2), pane.cur_col); - try std.testing.expectEqual(@as(i32, 0), pane.vsel.col); + pane.body.cur_row = 4094; + handleNormal(p, &pane.body, .{ .cp = 'l' }); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + handleNormal(p, &pane.body, .{ .cp = 'j' }); + try std.testing.expectEqual(@as(i32, 4095), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + handleNormal(p, &pane.body, .{ .cp = 'h' }); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); + executeNormalAction(p, &pane.body, .{ .textobject = .{ .char = 'w', .around = false } }); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.vsel.col); - pane.vsel.active = false; - pane.cur_row = 1024; - pane.cur_col = 0; - pane.nsel = 1; - pane.sels[0] = .{ .row = 3072, .col = 0, .arow = 3072, .acol = 0 }; - handleNormal(p, pane, .{ .cp = 'l' }); - handleNormal(p, pane, .{ .cp = 'j' }); - try std.testing.expectEqual(@as(u8, 1), pane.nsel); - try std.testing.expectEqual(@as(i32, 1025), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - try std.testing.expectEqual(@as(i32, 3073), pane.sels[0].row); - try std.testing.expectEqual(@as(i32, 1), pane.sels[0].col); + pane.body.vsel.active = false; + pane.body.cur_row = 1024; + pane.body.cur_col = 0; + pane.body.nsel = 1; + pane.body.sels[0] = .{ .row = 3072, .col = 0, .arow = 3072, .acol = 0 }; + handleNormal(p, &pane.body, .{ .cp = 'l' }); + handleNormal(p, &pane.body, .{ .cp = 'j' }); + try std.testing.expectEqual(@as(u8, 1), pane.body.nsel); + try std.testing.expectEqual(@as(i32, 1025), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 3073), pane.body.sels[0].row); + try std.testing.expectEqual(@as(i32, 1), pane.body.sels[0].col); try std.testing.expect(!denied.has_induced_failure); try std.testing.expectEqual(@as(usize, 0), p.scratch.queryCapacity()); - pane.nsel = 0; - pane.cur_col = 4; - executeNormalAction(p, pane, .match_bracket); + pane.body.nsel = 0; + pane.body.cur_col = 4; + executeNormalAction(p, &pane.body, .match_bracket); try std.testing.expect(denied.has_induced_failure); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); denied.fail_index = std.math.maxInt(usize); - executeNormalAction(p, pane, .match_bracket); - try std.testing.expectEqual(@as(i32, 8), pane.cur_col); + executeNormalAction(p, &pane.body, .match_bracket); + try std.testing.expectEqual(@as(i32, 8), pane.body.cur_col); } diff --git a/src/panes.zig b/src/panes.zig index 9bf2d92f..e6634583 100644 --- a/src/panes.zig +++ b/src/panes.zig @@ -24,8 +24,6 @@ const Key = pardes.Key; pub const EditText = struct { text: []u8, row0: i32 }; pub const Pane = struct { - pub const Mode = enum { normal, insert, tty }; - /// One mouse selection (block-shaped), per button. c/r are text-area relative; /// r counts from the tag row (body starts at BOX_H). pub const Sel = struct { @@ -53,39 +51,12 @@ pub const Pane = struct { rows: []PointerRow, raw_text: ?[]u8 = null, }; - - /// A modal line selection (helix `x`): whole rows [r0, r1], absolute. - pub const LineSel = struct { - active: bool = false, - r0: i32 = 0, - r1: i32 = 0, - }; - - pub const CharSel = struct { - active: bool = false, - row: i32 = 0, - col: i32 = 0, - explicit: bool = false, - }; - pub const LookSpot = struct { row: i32, col0: i32, col1: i32, }; - pub const max_selections = 64; - - pub const SelRange = struct { - row: i32, - col: i32, - arow: i32, - acol: i32, - /// this range's own j/k goal column (helix Range::old_visual_position); - /// the PRIMARY's copy is Pane.sticky_col - sticky: i32 = -1, - }; - const PdfSlot = if (Pdf.enabled) ?Pdf.State else void; pub const Prompt = union(enum) { @@ -108,7 +79,9 @@ pub const Pane = struct { /// dies with it, as acme keeps the same on its Window /// (editors/acme/dat.h:226-266). fs: pardes.ctlfs.pane.State = .{}, - mode: Mode = .normal, + /// The body: what the pane shows and edits, its cursor, selections, + /// mode and edit-buffer undo. + body: Text = .{}, vweight: f32 = 1, /// A folded pane keeps its expansion weight, but occupies only its tag row. collapsed: bool = false, @@ -121,26 +94,13 @@ pub const Pane = struct { file: ?File.State = null, image: ?Image.State = null, pdf: PdfSlot = if (Pdf.enabled) null else {}, - msel: LineSel = .{}, - vsel: CharSel = .{}, - sels: [max_selections - 1]SelRange = undefined, - nsel: u8 = 0, - select: bool = false, - /// sticky goal column for j/k runs (helix old_visual_position): any - /// non-vertical range write resets it to -1. - sticky_col: i32 = -1, - append_at: ?struct { row: i32, col: i32 } = null, - normal: modal.Normal.State = .{}, - /// last f/F/t/T motion, for Alt-. repeat - find_op: u8 = 0, - find_ch: u21 = 0, /// Tag-tail input state. The tag text is presentation; this tag carries /// which operation owns it and the tail offset restored on submit/cancel. prompt: Prompt = .none, search_pane: ?usize = null, search_row: ?usize = null, look_at: ?LookSpot = null, - sel_snap: [max_selections]modal.Selection = undefined, + sel_snap: [Text.max_selections]modal.Selection = undefined, nsel_snap: u8 = 0, sel_snap_pri: u8 = 0, sel_snap_expl: bool = false, @@ -153,7 +113,7 @@ pub const Pane = struct { tag_sel: bool = false, /// the body mode a tag edit hijacked (tags are always insert); terminals /// restore it on exit so clicking the tag never changes the pane's mode - tag_mode: Mode = .normal, + tag_mode: Text.Mode = .normal, tag_col: u16 = 0, tag_saved_col: ?u16 = null, tag_anchor: u16 = 0, @@ -163,14 +123,7 @@ pub const Pane = struct { tag_name_len: ?u16 = null, /// Display columns hidden to reveal the tag caret; tag addresses stay whole. tag_scroll: u16 = 0, - ed_undo: [Terminal.history_max]Terminal.Snapshot = undefined, - ed_undo_len: usize = 0, - ed_redo: [Terminal.history_max]Terminal.Snapshot = undefined, - ed_redo_len: usize = 0, cwd: Cwd = .none, - cur_pinned: bool = false, - cur_row: i32 = 0, - cur_col: i32 = 0, hscroll: i32 = 0, // Visible rows map to source lines and raw/display column origins. wrap_line: [limits.wrap_rows]i32 = undefined, @@ -512,7 +465,7 @@ pub const Pane = struct { // already, so only treat an ancestor as pinned in the matching view. if (pane.context_rows < pane.wrap_n and pane.wrap_line[pane.context_rows] == off) { for (pane.wrap_line[0..pane.context_rows]) |row| { - if (row == pane.cur_row) { + if (row == pane.body.cur_row) { in_context = true; break; } @@ -528,10 +481,10 @@ pub const Pane = struct { margin = @min(margin, @divTrunc(@max(0, lines_shown), 2)); } if (!in_context) { - if (pane.cur_row < off + margin) { - pane.scrollBy(pane.cur_row - margin - off); - } else if (pane.cur_row > last - margin) { - var to = pane.cur_row + margin; + if (pane.body.cur_row < off + margin) { + pane.scrollBy(pane.body.cur_row - margin - off); + } else if (pane.body.cur_row > last - margin) { + var to = pane.body.cur_row + margin; if (pane.file) |*f| to = @min(to, @as(i32, @intCast(File.nlines(pane.gpa, f) -| 1))); if (comptime Pdf.enabled) { if (pane.pdf) |pv| @@ -544,8 +497,8 @@ pub const Pane = struct { if (pane.body_wrapped) return; const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, File.gutterWidth(pane))); const hmargin: i32 = @min(config.scroll_off, @divTrunc(w - 1, 2)); - const line = modal.lineSlice(f.content, @intCast(@max(0, pane.cur_row))); - const raw_cur: usize = @intCast(@max(0, pane.cur_col)); + const line = modal.lineSlice(f.content, @intCast(@max(0, pane.body.cur_row))); + const raw_cur: usize = @intCast(@max(0, pane.body.cur_col)); const raw_scroll: usize = @intCast(@max(0, pane.hscroll)); const cur = @as(i32, @intCast(File.rawDisplayCol(line, raw_cur))); const visual_scroll = @as(i32, @intCast(File.rawDisplayCol(line, raw_scroll))); @@ -559,16 +512,16 @@ pub const Pane = struct { } pub fn pinCursor(pane: *Pane) void { - if (pane.cur_pinned) return; + if (pane.body.cur_pinned) return; if (pane.file != null or hasPdf(pane)) { - pane.cur_row = pane.scroll(); - pane.cur_col = 0; + pane.body.cur_row = pane.scroll(); + pane.body.cur_col = 0; } else { const cur = Terminal.gridCursor(pane); - pane.cur_row = pane.surfRow(@as(i32, cur.y) + Terminal.gridOffset(pane)); - pane.cur_col = @intCast(cur.x); + pane.body.cur_row = pane.surfRow(@as(i32, cur.y) + Terminal.gridOffset(pane)); + pane.body.cur_col = @intCast(cur.x); } - pane.cur_pinned = true; + pane.body.cur_pinned = true; } pub fn hasPdf(pane: *const Pane) bool { @@ -581,545 +534,10 @@ pub const Pane = struct { else false; } - - pub fn toModalCursor(pane: *Pane) modal.Cursor { - return .{ .row = @intCast(@max(0, pane.cur_row)), .col = @intCast(@max(0, pane.cur_col)) }; - } - - pub fn fromModalCursor(pane: *Pane, c: modal.Cursor) void { - pane.cur_row = @as(i32, @intCast(c.row)); - pane.cur_col = @intCast(c.col); - pane.cur_pinned = true; - } - - pub fn insertVerticalCursor(lines: []const []const u8, c: modal.Cursor, down: bool) modal.Cursor { - if (lines.len == 0) return c; - const row = if (down) @min(c.row + 1, lines.len - 1) else c.row -| 1; - const target = lines[row]; - if (target.len == 0) return .{ .row = row, .col = 0 }; - const source = if (c.row < lines.len) lines[c.row] else ""; - const goal = File.rawDisplayCol(source, c.col); - const mapped = File.rawAtDisplay(target, goal); - const last = modal.prevGrapheme(target, target.len); - return .{ .row = row, .col = modal.graphemeStart(target, @min(mapped, last)) }; - } - - pub fn primaryRange(pane: *Pane, text: []const u8, row0: i32) modal.Selection { - const c = File.textOffset(pane, text, .{ .row = @intCast(@max(0, pane.cur_row - row0)), .col = @intCast(@max(0, pane.cur_col)) }); - if (pane.msel.active) { - // legacy line selection (file-search results highlight): linewise - 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)); - const s = modal.lineStartOffset(text, r0); - const e = if (r1 + 1 >= modal.cursorLineCount(text)) text.len else modal.lineStartOffset(text, r1 + 1); - return .{ .anchor = s, .head = @max(e, modal.nextGrapheme(text, c)) }; - } - if (pane.vsel.active) return cellRange(text, pane.vsel.row - row0, pane.vsel.col, pane.cur_row - row0, pane.cur_col); - return .{ .anchor = c, .head = modal.nextGrapheme(text, c) }; - } - - pub fn cellRange(text: []const u8, arow: i32, acol: i32, hrow: i32, hcol: i32) modal.Selection { - const a = modal.offsetAt(text, .{ .row = @intCast(@max(0, arow)), .col = @intCast(@max(0, acol)) }); - const c = modal.offsetAt(text, .{ .row = @intCast(@max(0, hrow)), .col = @intCast(@max(0, hcol)) }); - return cellOffRange(text, a, c); - } - - /// the same, from the two cells' gap offsets - pub fn cellOffRange(text: []const u8, a: usize, c: usize) modal.Selection { - if (a <= c) return .{ .anchor = a, .head = modal.nextGrapheme(text, c) }; - return .{ .anchor = modal.nextGrapheme(text, a), .head = c }; - } - - pub fn rangeCells(text: []const u8, r: modal.Selection) struct { cur: usize, anc: usize } { - if (r.head > r.anchor) return .{ .cur = modal.prevGrapheme(text, r.head), .anc = r.anchor }; - if (r.head < r.anchor) return .{ .cur = r.head, .anc = modal.prevGrapheme(text, r.anchor) }; - return .{ .cur = r.head, .anc = r.head }; - } - - pub fn setRange(pane: *Pane, text: []const u8, row0: i32, r0: modal.Selection, explicit: bool) void { - var r = r0; - if (r.anchor == r.head) r.head = modal.nextGrapheme(text, r.head); // min_width_1 - const off = rangeCells(text, r); - const cc = File.textPosition(pane, text, off.cur); - // a bare block cursor has both cells on the same offset — the common - // case by far — and this conversion is not free even indexed - const ac = if (off.anc == off.cur) cc else File.textPosition(pane, text, off.anc); - pane.cur_row = @as(i32, @intCast(cc.row)) + row0; - pane.cur_col = @intCast(cc.col); - pane.vsel = .{ - .active = off.anc != off.cur or pane.select, - .row = @as(i32, @intCast(ac.row)) + row0, - .col = @intCast(ac.col), - .explicit = explicit or pane.select, - }; - pane.msel.active = false; - pane.nsel = 0; // writing ONE range means the selection IS that range - pane.cur_pinned = true; - pane.sticky_col = -1; - pane.normal.clear(); - pane.ensureCursorVisible(); - } - - pub fn ranges(pane: *Pane, text: []const u8, row0: i32, out: *[max_selections]modal.Selection) struct { n: usize, pri: usize } { - const pr = primaryRange(pane, text, row0); - var n: usize = 0; - var pri: usize = 0; - var placed = false; - for (pane.sels[0..pane.nsel]) |s| { - const r = cellRange(text, s.arow - row0, s.acol, s.row - row0, s.col); - if (!placed and @min(pr.anchor, pr.head) <= @min(r.anchor, r.head)) { - pri = n; - out[n] = pr; - n += 1; - placed = true; - } - out[n] = r; - n += 1; - } - if (!placed) { - pri = n; - out[n] = pr; - n += 1; - } - return .{ .n = n, .pri = pri }; - } - - pub fn setRanges(pane: *Pane, text: []const u8, in: []const modal.Selection, sticky: []const i32, pri0: usize, explicit: bool) void { - if (in.len == 0) return; // helix asserts non-empty; here it just means "no change" - var r: [max_selections]modal.Selection = undefined; - var st: [max_selections]i32 = undefined; - var n: usize = @min(in.len, max_selections); - var pri: usize = @min(pri0, n - 1); - for (in[0..n], 0..) |x, i| { - r[i] = x; - if (r[i].anchor == r[i].head) r[i].head = modal.nextGrapheme(text, r[i].head); - st[i] = if (i < sticky.len) sticky[i] else -1; - } - // insertion sort by start — n is tiny and usually already ordered - var i: usize = 1; - while (i < n) : (i += 1) { - var j = i; - while (j > 0 and @min(r[j].anchor, r[j].head) < @min(r[j - 1].anchor, r[j - 1].head)) : (j -= 1) { - std.mem.swap(modal.Selection, &r[j], &r[j - 1]); - std.mem.swap(i32, &st[j], &st[j - 1]); - if (pri == j) pri = j - 1 else if (pri == j - 1) pri = j; - } - } - var k: usize = 0; - i = 1; - while (i < n) : (i += 1) { - const a = r[k]; - const b = r[i]; - const af = @min(a.anchor, a.head); - const at = @max(a.anchor, a.head); - const bf = @min(b.anchor, b.head); - const bt = @max(b.anchor, b.head); - if (af == bf or (at > bf and bt > af)) { - r[k] = if (a.anchor > a.head and b.anchor > b.head) - .{ .anchor = @max(a.anchor, b.anchor), .head = @min(a.head, b.head) } - else - .{ .anchor = @min(af, bf), .head = @max(at, bt) }; - if (pri == i) pri = k; - if (st[k] < 0) st[k] = st[i]; - continue; - } - k += 1; - r[k] = b; - st[k] = st[i]; - if (pri == i) pri = k; - } - n = k + 1; - setRange(pane, text, 0, r[pri], explicit); - pane.sticky_col = st[pri]; - var w: usize = 0; - for (r[0..n], 0..) |x, idx| { - if (idx == pri) continue; - const c = rangeCells(text, x); - const cc = modal.positionAt(text, c.cur); - const ac = modal.positionAt(text, c.anc); - pane.sels[w] = .{ - .row = @as(i32, @intCast(cc.row)), - .col = @intCast(cc.col), - .arow = @as(i32, @intCast(ac.row)), - .acol = @intCast(ac.col), - .sticky = st[idx], - }; - w += 1; - } - pane.nsel = @intCast(w); - } - - pub fn multiSelAction(pane: *Pane, text: []const u8, kind: modal.Normal.Multi, cnt: usize) void { - var rs: [max_selections]modal.Selection = undefined; - const got = ranges(pane, text, 0, &rs); - const n = got.n; - const expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; - if (kind == .remove_primary) { - if (n < 2) return; // helix: "no selections remaining" - var out: [max_selections]modal.Selection = undefined; - var m: usize = 0; - for (rs[0..n], 0..) |r, i| { - if (i == got.pri) continue; - out[m] = r; - m += 1; - } - // helix Selection::remove: the NEXT range takes over, or the - // previous one when the primary was last - return setRanges(pane, text, out[0..m], &.{}, @min(got.pri, m - 1), expl); - } - if (kind == .rotate_forward or kind == .rotate_backward) { - const step = cnt % n; - const pri = if (kind == .rotate_forward) (got.pri + step) % n else (got.pri + (n - step)) % n; - return setRanges(pane, text, rs[0..n], &.{}, pri, expl); - } - if (kind == .merge) { - // helix merge_selections: first.merge(last) — the ranges are - // sorted, so that is simply the whole span - const lo = @min(rs[0].anchor, rs[0].head); - const hi = @max(rs[n - 1].anchor, rs[n - 1].head); - const rev = rs[0].anchor > rs[0].head and rs[n - 1].anchor > rs[n - 1].head; - const one: modal.Selection = if (rev) .{ .anchor = hi, .head = lo } else .{ .anchor = lo, .head = hi }; - return setRanges(pane, text, &.{one}, &.{}, 0, expl); - } - if (kind == .merge_consecutive) { - var out: [max_selections]modal.Selection = undefined; - var m: usize = 0; - var pri: usize = 0; - for (rs[0..n], 0..) |r, i| { - if (m > 0 and @min(r.anchor, r.head) == @max(out[m - 1].anchor, out[m - 1].head)) { - const lo = @min(@min(out[m - 1].anchor, out[m - 1].head), @min(r.anchor, r.head)); - const hi = @max(@max(out[m - 1].anchor, out[m - 1].head), @max(r.anchor, r.head)); - out[m - 1] = .{ .anchor = lo, .head = hi }; - if (i == got.pri) pri = m - 1; - continue; - } - if (i == got.pri) pri = m; - out[m] = r; - m += 1; - } - return setRanges(pane, text, out[0..m], &.{}, pri, expl); - } - if (kind == .split_newline) { - // helix selection::split_on_newline — one range per line the - // selection covers, the newlines themselves left out - var out: [max_selections]modal.Selection = undefined; - var m: usize = 0; - for (rs[0..n]) |r| { - const from = @min(r.anchor, r.head); - const to = @max(r.anchor, r.head); - if (from == to) { - if (m < max_selections) { - out[m] = r; - m += 1; - } - continue; - } - var start = from; - while (start < to and m < max_selections) { - const eol = modal.lineEndOffset(text, modal.lineAtOffset(text, start)); - if (eol >= to) { - out[m] = .{ .anchor = start, .head = to }; - m += 1; - break; - } - out[m] = .{ .anchor = start, .head = eol }; - m += 1; - start = eol + 1; - } - } - if (m == 0) return; - return setRanges(pane, text, out[0..m], &.{}, 0, true); // helix keeps primary 0 - } - if (kind == .trim) { - // helix trim_selections: whitespace off both ends; ranges that are - // empty or all whitespace are dropped entirely - var out: [max_selections]modal.Selection = undefined; - var m: usize = 0; - for (rs[0..n]) |r| { - var from = @min(r.anchor, r.head); - var to = @max(r.anchor, r.head); - while (from < to and std.ascii.isWhitespace(text[from])) from += 1; - while (to > from and std.ascii.isWhitespace(text[to - 1])) to -= 1; - if (from >= to) continue; - out[m] = if (r.anchor > r.head) .{ .anchor = to, .head = from } else .{ .anchor = from, .head = to }; - m += 1; - } - if (m == 0) { // helix: collapse_selection + keep_primary_selection - const c = modal.selectionCursor(text, rs[got.pri]); - return setRange(pane, text, 0, .{ .anchor = c, .head = c }, false); - } - // helix: the first survivor that OVERLAPS the old primary, else the last - const pf = @min(rs[got.pri].anchor, rs[got.pri].head); - const pt = @max(rs[got.pri].anchor, rs[got.pri].head); - var pri = m - 1; - for (out[0..m], 0..) |r, i| { - const f = @min(r.anchor, r.head); - const t = @max(r.anchor, r.head); - if (f == pf or (t > pf and pt > f)) { - pri = i; - break; - } - } - return setRanges(pane, text, out[0..m], &.{}, pri, expl); - } - const below = kind == .copy_below; - var out: [max_selections]modal.Selection = undefined; - var m: usize = 0; - var pri: usize = 0; - const nlines = modal.cursorLineCount(text); - for (rs[0..n], 0..) |r, ri| { - const is_pri = ri == got.pri; - // head-exclusive: back the leading end off onto its own cell - const hp = modal.positionAt(text, if (r.anchor < r.head) modal.prevGrapheme(text, r.head) else r.head); - const ap = modal.positionAt(text, if (r.anchor < r.head) r.anchor else modal.prevGrapheme(text, r.anchor)); - const height = @max(hp.row, ap.row) - @min(hp.row, ap.row) + 1; - if (m >= max_selections) break; - if (is_pri) pri = m; - out[m] = r; - m += 1; - var made: usize = 0; - var k: usize = 0; - while (made < cnt and m < max_selections) : (k += 1) { - const d = (k + 1) * height; - const arow = if (below) ap.row + d else ap.row -| d; - const hrow = if (below) hp.row + d else hp.row -| d; - if (arow >= nlines or hrow >= nlines) break; - const a2 = modal.offsetAt(text, .{ .row = arow, .col = ap.col }); - const h2 = modal.offsetAt(text, .{ .row = hrow, .col = hp.col }); - // a line too short to reach the column is skipped, not clamped - if (modal.positionAt(text, a2).col == ap.col and modal.positionAt(text, h2).col == hp.col) { - if (is_pri) pri = m; - out[m] = modal.moveSelectionCursor(text, .{ .anchor = a2, .head = a2 }, h2, true); - m += 1; - made += 1; - } - if (arow == 0 and hrow == 0) break; - } - } - setRanges(pane, text, out[0..m], &.{}, pri, expl); - } - - /// a range's start CELL (document order key) — the smaller of its two ends - pub fn selStart(s: SelRange) struct { row: i32, col: i32 } { - if (s.arow < s.row or (s.arow == s.row and s.acol < s.col)) return .{ .row = s.arow, .col = s.acol }; - return .{ .row = s.row, .col = s.col }; - } - - pub fn maxLine(text: []const u8) usize { - const nl = modal.cursorLineCount(text); - return if (text.len == 0 or text[text.len - 1] == '\n') nl -| 2 else nl - 1; - } - - /// point-target motion: collapse there (extend in select mode) - pub fn pointMove(pane: *Pane, text: []const u8, range: modal.Selection, target: usize) void { - setRange(pane, text, 0, modal.moveSelectionCursor(text, range, target, pane.select), false); - } - - /// word motions select their traversed span (extend mode: head only) - pub fn wordMove(pane: *Pane, text: []const u8, range: modal.Selection, cnt: usize, target: modal.WordTarget) void { - const wr = modal.moveWord(text, range, cnt, target); - const res = if (pane.select) modal.moveSelectionCursor(text, range, modal.selectionCursor(text, wr), true) else wr; - setRange(pane, text, 0, res, false); - } - - /// f/t/F/T: anchor at the old cursor cell, head on the hit (not found: no move) - pub fn findMove(pane: *Pane, text: []const u8, range: modal.Selection, ch: u21, fwd: bool, till: bool, cnt: usize) void { - const cur = modal.selectionCursor(text, range); - const t = modal.findTarget(text, cur, ch, fwd, till, cnt) orelse return; - const res = if (pane.select) - modal.moveSelectionCursor(text, range, t, true) - else - modal.moveSelectionCursor(text, .{ .anchor = cur, .head = cur }, t, true); - setRange(pane, text, 0, res, false); - } - - /// j/k and friends: sticky goal column, clamped onto short lines' newline - pub fn verticalMove(pane: *Pane, text: []const u8, range: modal.Selection, down: bool, cnt: usize) void { - const cur = modal.selectionCursor(text, range); - const pos = File.textPosition(pane, text, cur); - const goal: usize = if (pane.sticky_col >= 0) - @intCast(pane.sticky_col) - else - File.rawDisplayCol(File.textLine(pane, text, pos.row), pos.col); - const last_row = File.textLineCount(pane, text) - 1; - const nline = if (down) @min(pos.row + @max(1, cnt), last_row) else pos.row -| @max(1, cnt); - const target_col = File.rawAtDisplay(File.textLine(pane, text, nline), goal); - const t = File.textOffset(pane, text, .{ .row = nline, .col = target_col }); - // extend mode never walks onto the empty trailing line (helix) - if (pane.select and t == text.len and text.len > 0 and text[text.len - 1] == '\n') return; - setRange(pane, text, 0, modal.moveSelectionCursor(text, range, t, pane.select), false); - pane.sticky_col = @intCast(goal); - } - - pub fn visualMove( - pane: *Pane, - text: []const u8, - range: modal.Selection, - down: bool, - cnt: usize, - width: usize, - ) void { - const cur = modal.selectionCursor(text, range); - const pos = File.textPosition(pane, text, cur); - const last_row = File.textLineCount(pane, text) - 1; - var row = pos.row; - var line = modal.lineSlice(text, row); - var vrow = File.visualRow(line, pos.col, width); - const goal: usize = if (pane.sticky_col >= 0) - @intCast(pane.sticky_col) - else - File.rawDisplayCol(line[vrow.start..vrow.end], pos.col -| vrow.start); - var steps = @max(1, cnt); - while (steps > 0) : (steps -= 1) { - if (down) { - if (vrow.end < line.len) { - vrow = File.visualRow(line, vrow.end, width); - continue; - } - if (row == last_row) break; - row += 1; - line = modal.lineSlice(text, row); - vrow = File.visualRow(line, 0, width); - } else { - if (vrow.start > 0) { - vrow = File.visualRow(line, vrow.start - 1, width); - continue; - } - if (row == 0) break; - row -= 1; - line = modal.lineSlice(text, row); - vrow = File.visualRow(line, line.len, width); - } - } - // The newline slot is a real cursor position, but the first byte of the - // NEXT visual row is not: landing there would read as two rows moved. - var target_col = vrow.start + File.rawAtDisplay(line[vrow.start..vrow.end], goal); - if (vrow.end < line.len and target_col >= vrow.end) - target_col = modal.graphemeStart(line, vrow.end - 1); - const t = File.textOffset(pane, text, .{ .row = row, .col = target_col }); - // extend mode never walks onto the empty trailing line (helix) - if (pane.select and t == text.len and text.len > 0 and text[text.len - 1] == '\n') return; - setRange(pane, text, 0, modal.moveSelectionCursor(text, range, t, pane.select), false); - pane.sticky_col = @intCast(goal); - } - - /// Ctrl-d/u: scroll half a page AND move the cursor by the same rows - pub fn halfPageMove(pane: *Pane, text: []const u8, range: modal.Selection, down: bool) void { - const half: i32 = @max(1, @divTrunc(@as(i32, pane.rows), 2)); - pane.scrollBy(if (down) half else -half); - verticalMove(pane, text, range, down, @intCast(half)); - } - - /// helix `scroll` without cursor sync (Ctrl-f/b, PgUp/PgDn, zj/zk): shift - /// the view, then snap a fallen-out cursor to the near scrolloff edge, col 0 - pub fn scrollViewMove(pane: *Pane, text: []const u8, range: modal.Selection, delta: i32) void { - const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2)); - pane.scrollBy(delta); - const top = pane.scroll(); - const last_row: i32 = @intCast(File.textLineCount(pane, text) - 1); - const cur = modal.selectionCursor(text, range); - if (delta > 0) { - const snap: i32 = @max(0, @min(top + margin, last_row)); - const head = File.textLineStart(pane, text, @intCast(snap)); - if (head <= cur) return; - const anchor = if (pane.select) range.anchor else head; - setRange(pane, text, 0, .{ .anchor = anchor, .head = head }, false); - } else { - const snap: i32 = @max(0, @min(top + @as(i32, pane.rows) - margin - 1, last_row)); - const head = File.textLineStart(pane, text, @intCast(snap)); - if (head >= cur) return; - const anchor = if (pane.select) range.anchor else head; - setRange(pane, text, 0, .{ .anchor = anchor, .head = head }, false); - } - } - - /// gt/gc/gb: view-relative rows, col 0, scrolloff clamped (helix goto_window) - pub fn gotoWindow(pane: *Pane, text: []const u8, range: modal.Selection, which: enum { top, center, bottom }, cnt: usize) void { - const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, pane.rows) - 1, 2)); - const top = pane.scroll(); - const last_row: i32 = @intCast(File.textLineCount(pane, text) - 1); - const last_vis: i32 = @min(@as(i32, pane.rows) - 1, last_row - top); - const n: i32 = @intCast(cnt - 1); - var vline: i32 = switch (which) { - .top => top + margin + n, - .center => top + @divTrunc(last_vis, 2), - .bottom => top + last_vis - (margin + n), - }; - vline = @max(vline, top + margin); - vline = @min(vline, top + last_vis - margin); - const row: i32 = std.math.clamp(vline, 0, last_row); - pointMove(pane, text, range, File.textLineStart(pane, text, @intCast(row))); - } - - /// helix Range::line_range — the inclusive line span a range covers - pub fn rangeLineSpan(text: []const u8, r: modal.Selection) struct { start: usize, end: usize } { - const from = @min(r.anchor, r.head); - const to = @max(r.anchor, r.head); - const to_adj = if (from == to) to else @max(modal.prevGrapheme(text, to), from); - return .{ .start = modal.lineAtOffset(text, from), .end = modal.lineAtOffset(text, to_adj) }; - } - - fn lineStartOrEof(text: []const u8, line: usize) usize { - if (line >= modal.cursorLineCount(text)) return text.len; - return modal.lineStartOffset(text, line); - } - - /// helix `x` extend_line_below: full lines incl. the newline, cursor ON - /// the last one's '\n'; an already-line-bounded selection grows downward - pub fn lineSelect(pane: *Pane, text: []const u8, range: modal.Selection, cnt: usize) void { - const span = rangeLineSpan(text, range); - const start = modal.lineStartOffset(text, span.start); - const end = lineStartOrEof(text, span.end + 1); - const full = @min(range.anchor, range.head) == start and @max(range.anchor, range.head) == end; - const head = lineStartOrEof(text, span.end + cnt + @intFromBool(full)); - setRange(pane, text, 0, .{ .anchor = start, .head = head }, true); - } - - /// helix `X` extend_to_line_bounds (direction kept) - pub fn lineBoundsSelect(pane: *Pane, text: []const u8, range: modal.Selection) void { - const span = rangeLineSpan(text, range); - const start = modal.lineStartOffset(text, span.start); - const end = lineStartOrEof(text, span.end + 1); - const r: modal.Selection = if (range.head < range.anchor) - .{ .anchor = end, .head = start } - else - .{ .anchor = start, .head = end }; - setRange(pane, text, 0, r, true); - } - - /// helix `Alt-x` shrink_to_line_bounds (single-line selections untouched) - pub fn shrinkSelToLineBounds(pane: *Pane, text: []const u8, range: modal.Selection) void { - const span = rangeLineSpan(text, range); - if (span.start == span.end) return; - const from = @min(range.anchor, range.head); - const to = @max(range.anchor, range.head); - var start = modal.lineStartOffset(text, span.start); - var end = lineStartOrEof(text, span.end + 1); - if (start != from) start = lineStartOrEof(text, span.start + 1); - if (end != to) end = modal.lineStartOffset(text, span.end); - const expl = (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; - const r: modal.Selection = if (range.head < range.anchor) - .{ .anchor = end, .head = start } - else - .{ .anchor = start, .head = end }; - setRange(pane, text, 0, r, expl); - } - - /// pull the cursor back inside `text` after a rewrite; `row0` is the - /// absolute surface row of its first line (0 for a file) - pub fn clampCursor(pane: *Pane, text: []const u8, row0: i32) void { - const n = modal.lineCount(text); - const row: usize = @min(@as(usize, @intCast(@max(0, pane.cur_row - row0))), if (n == 0) 0 else n - 1); - const llen = modal.lineSlice(text, row).len; - pane.cur_row = @as(i32, @intCast(row)) + row0; - pane.cur_col = @intCast(@min(@as(usize, @intCast(@max(0, pane.cur_col))), llen)); - pane.cur_pinned = true; - pane.vsel.active = false; - pane.msel.active = false; - pane.ensureCursorVisible(); - } }; +pub const Text = @import("Text.zig"); + pub const File = @import("File.zig"); pub const Output = @import("Output.zig"); @@ -1134,6 +552,7 @@ pub const Terminal = @import("Terminal.zig"); test { _ = Pane; + _ = Text; _ = File; _ = Output; _ = Mini; diff --git a/src/pardes.zig b/src/pardes.zig index 97860cfe..15b2c49b 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -240,6 +240,7 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) { }; pub const Pane = panes.Pane; +const Text = panes.Text; pub const MAX_PANES = 16; pub const BodyLayer = @import("body_layer.zig").Layer; @@ -875,19 +876,19 @@ test "Togglettymode builtin and keyboard chord share mode transitions including try std.testing.expect(std.mem.indexOf(u8, Pardes.curTail(pane), " Mode ") != null); p.seedTail(pane); try std.testing.expect(pane.appendTag(" Togglettymode")); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); - try std.testing.expectEqual(Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(Text.Mode.normal, pane.body.mode); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); p.enterTagEdit(pane, -1); - try std.testing.expectEqual(Pane.Mode.insert, pane.mode); + try std.testing.expectEqual(Text.Mode.insert, pane.body.mode); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); - try std.testing.expectEqual(Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(Text.Mode.normal, pane.body.mode); try std.testing.expect(!pane.tag_edit); p.enterTagEdit(pane, -1); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); try std.testing.expect(!pane.tag_edit); p.acknowledgePanelPresentation(&.{}); @@ -896,10 +897,10 @@ test "Togglettymode builtin and keyboard chord share mode transitions including const y = p.rects[0].y; p.update(.{ .mouse = .{ .button = .middle, .kind = .press, .col = x, .row = y } }); p.update(.{ .mouse = .{ .button = .middle, .kind = .release, .col = x, .row = y } }); - try std.testing.expectEqual(Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(Text.Mode.normal, pane.body.mode); const file = try p.setTestFile("unchanged\n"); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); - try std.testing.expectEqual(Pane.Mode.normal, file.mode); + try std.testing.expectEqual(Text.Mode.normal, file.body.mode); try std.testing.expectEqualStrings("unchanged\n", file.file.?.content); } @@ -1143,7 +1144,7 @@ test "Tty spawns a raw shell in the caller's directory" { try std.testing.expect(free != 0); const nt = p.panes[free] orelse return error.NoNewPane; try std.testing.expect(nt.isTerminal()); - try std.testing.expect(nt.mode == .tty); + try std.testing.expect(nt.body.mode == .tty); var found = false; while (p.nextEffect()) |effect| switch (effect) { @@ -1165,7 +1166,7 @@ test "Tty9p marks only the new Linux terminal for a mounted shell" { try std.testing.expect(p.active != 0); try std.testing.expect(!p.panes[0].?.v9fs_on_spawn); const pane = p.panes[p.active].?; - try std.testing.expect(pane.isTerminal() and pane.mode == .tty); + try std.testing.expect(pane.isTerminal() and pane.body.mode == .tty); try std.testing.expect(pane.v9fs_on_spawn); try std.testing.expect(!pane.greet); try std.testing.expect(!exec.takesCommandLine(p, p.active)); @@ -1753,7 +1754,7 @@ test "owned cwd EOF failure retains the pane and Del retries its close" { p.update(.{ .eof = .{ .pane = 0 } }); try std.testing.expectEqual(parent, p.panes[0].?); try std.testing.expectEqual(parent, child.cwd.inherited); - try std.testing.expectEqual(Pane.Mode.normal, parent.mode); + try std.testing.expectEqual(Text.Mode.normal, parent.body.mode); try std.testing.expect(std.mem.indexOf(u8, parent.msg[0..parent.msg_len], "Del retries close") != null); try std.testing.expectEqual(@as(usize, 0), p.effects_len); allocator.fail_index = std.math.maxInt(usize); @@ -2626,7 +2627,7 @@ test "Unicode display cells map back to body and tag byte cursors" { // Both halves map to its one byte boundary. p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = text_x + 2, .row = body_y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = text_x + 2, .row = body_y } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); // A click in the wide path glyph likewise becomes a byte cursor at the // grapheme start; arrow motion then advances by the full UTF-8 cluster. @@ -2660,8 +2661,8 @@ test "tabbed file aligns syntax cursor and mouse at the nearest insertion gap" { @memset(f.highlights, @intFromEnum(syntax.Syn.none)); @memset(f.highlights[1..7], @intFromEnum(syntax.Syn.keyword)); f.syntax_dirty = false; - pane.cur_row = 0; - pane.cur_col = 1; + pane.body.cur_row = 0; + pane.body.cur_col = 1; var frame = std.heap.ArenaAllocator.init(gpa); defer frame.deinit(); @@ -2678,14 +2679,14 @@ test "tabbed file aligns syntax cursor and mouse at the nearest insertion gap" { const click_x = text_x + config.tab_width + 2; p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = click_x, .row = body_y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = click_x, .row = body_y } }); - try std.testing.expectEqual(@as(i32, 3), pane.cur_col); + try std.testing.expectEqual(@as(i32, 3), pane.body.cur_col); const line = modal.lineSlice(f.content, 0); const virtual: u16 = 3; const blank_x = text_x + @as(u16, @intCast(panes.File.displayWidth(line))) + virtual; p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = blank_x, .row = body_y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = blank_x, .row = body_y } }); - try std.testing.expectEqual(@as(i32, @intCast(line.len)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(line.len)), pane.body.cur_col); _ = frame.reset(.retain_capacity); const virtual_surface = try p.render(frame.allocator()); @@ -3887,7 +3888,7 @@ pub const Pardes = struct { }, .tty, .tty_shell => { _ = try p.newShell(0, ""); - p.panes[0].?.mode = .tty; + p.panes[0].?.body.mode = .tty; p.oneColumn(); // A bare tty is still an editor: it opens with somewhere to put // text under the shell. The shell keeps the keyboard. @@ -4108,8 +4109,8 @@ pub const Pardes = struct { if (comptime pdf_enabled) if (pane.pdf) |*pv| panes.Pdf.deinitPane(p, pane, pv); if (pane.file) |*file| panes.File.deinit(p, pane, file); if (pane.ovl) |o| p.gpa.free(o.text); - for (pane.ed_undo[0..pane.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); - for (pane.ed_redo[0..pane.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); + for (pane.body.ed_undo[0..pane.body.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); + for (pane.body.ed_redo[0..pane.body.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text); panes.Terminal.deinitEmulator(pane, p.gpa); pane.clearCwd(); pane.fs.deinit(p.gpa); @@ -4159,7 +4160,7 @@ pub const Pardes = struct { errdefer p.gpa.free(content); const np = try panes.Output.open(p, free, paneDir(src), .{ .cmd = .New }, "", content); np.cwd = .{ .inherited = src }; - np.cur_pinned = true; + np.body.cur_pinned = true; return np; } @@ -4170,7 +4171,7 @@ pub const Pardes = struct { const content = try p.gpa.dupe(u8, ""); errdefer p.gpa.free(content); const np = try panes.Output.open(p, free, dir, .{ .cmd = .New }, "", content); - np.cur_pinned = true; + np.body.cur_pinned = true; return np; } @@ -4685,7 +4686,7 @@ pub const Pardes = struct { ctlfs.pty.noteMarks(p, o.pane, pane); }, .eof => |e| p.removePane(e.pane, null) catch |err| { - if (p.panes[e.pane]) |pane| pane.mode = .normal; + if (p.panes[e.pane]) |pane| pane.body.mode = .normal; p.reportError(e.pane, "terminal exited; Del retries close", err); }, .lsp_resp => |r| p.lspResponse(r.id, r.rows), @@ -4839,12 +4840,12 @@ pub const Pardes = struct { const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len); p.seedTail(pane); if (!pane.tag_init) return; - if (!pane.tag_edit) pane.tag_mode = pane.mode; + if (!pane.tag_edit) pane.tag_mode = pane.body.mode; pane.tag_edit = true; pane.tag_sel = false; // a one-line tag has no use for normal mode: always insert - pane.mode = .insert; - pane.normal.clear(); + pane.body.mode = .insert; + pane.body.normal.clear(); const end = edit0 + @as(i32, @intCast(pane.tag_tail_len)); const tail = pane.tagSlice(); const lead: i32 = @intCast(tail.len - std.mem.trimStart(u8, tail, " ").len); @@ -4870,8 +4871,8 @@ pub const Pardes = struct { pub fn exitTagEdit(pane: *Pane) void { if (pane.tag_edit and pane.prompt == .none) pane.tag_saved_col = pane.tag_col; if (pane.isTerminal()) { - if (pane.tag_edit) pane.mode = pane.tag_mode; - } else pane.mode = .normal; + if (pane.tag_edit) pane.body.mode = pane.tag_mode; + } else pane.body.mode = .normal; pane.tag_edit = false; pane.tag_sel = false; pane.tag_name_len = null; @@ -5054,7 +5055,7 @@ pub const Pardes = struct { const id = p.active; const body_mode = pane.tag_mode; exitTagEdit(pane); // leave the tag FIRST: Del frees the pane - pane.mode = body_mode; + pane.body.mode = body_mode; exec.runBuiltin(p, cmd, id, "", txt); return; } @@ -5075,7 +5076,7 @@ pub const Pardes = struct { else cur)); pane.tag_sel = false; - pane.mode = .insert; + pane.body.mode = .insert; return; } const dir: ?Builtin = for (config.window_keys) |wk| { @@ -5096,7 +5097,7 @@ pub const Pardes = struct { const dest = p.panes[p.active] orelse return; exitTagEdit(pane); // the pane we left is fully out of its tag... p.enterTagEdit(dest, -1); - if (dest.tag_edit) dest.mode = .normal; + if (dest.tag_edit) dest.body.mode = .normal; return; } if (lineMotion(text, cur, key)) |r| setTagRange(pane, text, r); @@ -5244,7 +5245,7 @@ pub const Pardes = struct { p.exitHeader(); p.active = d; p.enterTagEdit(pane, -1); - if (pane.tag_edit) pane.mode = .normal; + if (pane.tag_edit) pane.body.mode = .normal; return; } if (hit(key, config.topbar_up)) { @@ -5268,19 +5269,19 @@ pub const Pardes = struct { pub fn cycleMode(p: *Pardes, id: usize) void { const pane = p.panes[id] orelse return; if (pane.file == null and !pane.isTerminal()) return; - const previous = if (pane.tag_edit) pane.tag_mode else pane.mode; + const previous = if (pane.tag_edit) pane.tag_mode else pane.body.mode; p.active = id; p.exitHeader(); if (pane.tag_edit) exitTagEdit(pane); - pane.mode = previous; + pane.body.mode = previous; switch (previous) { .tty => { - pane.mode = .normal; - pane.normal.clear(); + pane.body.mode = .normal; + pane.body.normal.clear(); }, - .normal => edit.enterInsert(p, pane, .at, 1), + .normal => edit.enterInsert(p, &pane.body, .at, 1), .insert => { - edit.exitInsert(p, pane); + edit.exitInsert(p, &pane.body); if (pane.isTerminal()) panes.Terminal.enterTty(p, id); }, } @@ -5293,9 +5294,9 @@ pub const Pardes = struct { p.active = id; p.exitHeader(); if (pane.tag_edit) exitTagEdit(pane); - if (pane.mode == .tty) { - pane.mode = .normal; - pane.normal.clear(); + if (pane.body.mode == .tty) { + pane.body.mode = .normal; + pane.body.normal.clear(); } else panes.Terminal.enterTty(p, id); } @@ -5312,7 +5313,7 @@ pub const Pardes = struct { } if (p.topbar_col != null) return p.topbarKey(key); const pane = p.panes[p.active] orelse return; - if (pane.isTerminal() and pane.mode == .tty and !pane.tag_edit) { + if (pane.isTerminal() and pane.body.mode == .tty and !pane.tag_edit) { p.leader_on = false; p.ctrl_w_pending = false; if (key.ctrl and !key.alt and key.cp == p.opts.tty_toggle) @@ -5350,7 +5351,7 @@ pub const Pardes = struct { } // insert mode owns Ctrl-w (delete word backward, helix); the focus // prefix applies outside raw tty mode - if (hit(key, config.window_prefix) and pane.mode != .insert) { + if (hit(key, config.window_prefix) and pane.body.mode != .insert) { p.ctrl_w_pending = true; return; } @@ -5403,27 +5404,27 @@ pub const Pardes = struct { .none, .del_side => unreachable, } exitTagEdit(pane); - pane.mode = .normal; - pane.normal.clear(); + pane.body.mode = .normal; + pane.body.normal.clear(); return; } if (pane.tag_edit) { - if (pane.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key); + if (pane.body.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key); const pn = p.panes[p.active] orelse return; if (normal.selRegexArmed(pn)) |a| normal.applySelRegex(p, pn, a.pat, a.split); return; } - if (pane.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) { + if (pane.body.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) { const cmd = if (hit(key, config.look_key)) config.look_cmd else config.exec_cmd; - edit.pinPaneCursor(p, pane); + edit.pinPaneCursor(p, &pane.body); const explicit = (p.native_images and pane.hasPdfSelection()) or - (pane.vsel.active and pane.vsel.explicit) or pane.msel.active; + (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active; if (explicit) { if (exec.chordEachSel(p, pane, cmd)) return; if (edit.currentSelText(p, pane)) |txt| { - pane.vsel.active = false; - pane.msel.active = false; - pane.select = false; + pane.body.vsel.active = false; + pane.body.msel.active = false; + pane.body.select = false; exec.runBuiltin(p, cmd, p.active, "", txt); return; } @@ -5433,14 +5434,14 @@ pub const Pardes = struct { exec.runBuiltin(p, cmd, p.active, "", word); return; } - if (pane.mode == .normal and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key); - switch (pane.mode) { + if (pane.body.mode == .normal and pane.hasPdf()) return normal.handlePdfNormal(p, pane, key); + switch (pane.body.mode) { .normal => { - normal.handleNormal(p, pane, key); + normal.handleNormal(p, &pane.body, key); }, .insert => { - if (hit(key, config.escape)) return edit.exitInsert(p, pane); - edit.handleInsert(p, pane, key); + if (hit(key, config.escape)) return edit.exitInsert(p, &pane.body); + edit.handleInsert(p, &pane.body, key); }, .tty => panes.Terminal.forwardKey(p, p.active, key), } @@ -5529,8 +5530,8 @@ pub const Pardes = struct { }; pane.tag_edit = true; pane.tag_sel = false; - pane.mode = .insert; - pane.normal.clear(); + pane.body.mode = .insert; + pane.body.normal.clear(); pane.tag_col = @intCast(prefix_len + pane.tag_tail_len); } @@ -5551,8 +5552,8 @@ pub const Pardes = struct { if (!zig_buf and std.mem.indexOfAny(u8, arg, " \t\r\n\"\\") != null) return; } const off = if (pane.file) |f| modal.offsetAt(f.content, .{ - .row = @intCast(@max(0, pane.cur_row)), - .col = @intCast(@max(0, pane.cur_col)), + .row = @intCast(@max(0, pane.body.cur_row)), + .col = @intCast(@max(0, pane.body.cur_col)), }) else 0; p.lsp_seq +%= 1; p.lsp_wait = .{ @@ -5562,8 +5563,8 @@ pub const Pardes = struct { .serial = pane.serial, .revision = if (pane.file) |f| f.revision else 0, .arg = .from(arg), - .row = pane.cur_row, - .col = pane.cur_col, + .row = pane.body.cur_row, + .col = pane.body.cur_col, }; p.emit(.{ .lsp = .{ .id = p.lsp_seq, @@ -5666,8 +5667,8 @@ pub const Pardes = struct { const replacement = p.gpa.alloc(u8, final_len) catch return false; const old_cursor = modal.offsetAt(f.content, .{ - .row = @intCast(@max(0, pane.cur_row)), - .col = @intCast(@max(0, pane.cur_col)), + .row = @intCast(@max(0, pane.body.cur_row)), + .col = @intCast(@max(0, pane.body.cur_col)), }); const mapped_cursor = mapLspEditOffset(edits, old_cursor); var read_at: usize = 0; @@ -5681,15 +5682,15 @@ pub const Pardes = struct { } @memcpy(replacement[write_at..], f.content[read_at..]); - edit.pushUndo(p, pane); + edit.pushUndo(p, &pane.body); panes.File.setContent(p, f, replacement); const cursor = modal.positionAt(f.content, mapped_cursor); - pane.cur_row = @intCast(cursor.row); - pane.cur_col = @intCast(cursor.col); - pane.vsel.active = false; - pane.msel.active = false; - pane.select = false; - pane.sticky_col = -1; + pane.body.cur_row = @intCast(cursor.row); + pane.body.cur_col = @intCast(cursor.col); + pane.body.vsel.active = false; + pane.body.msel.active = false; + pane.body.select = false; + pane.body.sticky_col = -1; pane.ensureCursorVisible(); return true; } @@ -5718,8 +5719,8 @@ pub const Pardes = struct { } else return; } if (rows.len == 0) { - if (w.kind == .completion and pane.mode == .insert and pane.nsel == 0 and - pane.cur_row == w.row and pane.cur_col == w.col) edit.insertTab(p, pane); + if (w.kind == .completion and pane.body.mode == .insert and pane.body.nsel == 0 and + pane.body.cur_row == w.row and pane.body.cur_col == w.col) edit.insertTab(p, &pane.body); return; } @@ -5789,7 +5790,7 @@ pub const Pardes = struct { errdefer p.gpa.destroy(history); const pane = try p.newDocPane(id); pane.file = .{ .path = path, .content = content, .history = history }; - pane.cur_pinned = true; + pane.body.cur_pinned = true; return pane; } @@ -5806,7 +5807,7 @@ pub const Pardes = struct { p.panes[0] = null; pane.file = .{ .path = path, .content = copy, .history = history }; p.installPane(0, pane); - pane.cur_pinned = true; + pane.body.cur_pinned = true; p.active = 0; p.sync(); return pane; @@ -6015,8 +6016,8 @@ pub const Pardes = struct { const now: Loc = .{ .pane = @intCast(p.active), .serial = pane.serial, - .line = if (pane.pdfPage()) |page| @intCast(page + 1) else if (pane.cur_pinned) @intCast(@max(0, pane.cur_row) + 1) else 0, - .col = if (pane.pdfPage() != null) 0 else if (pane.cur_pinned) @intCast(@max(0, pane.cur_col) + 1) else 0, + .line = if (pane.pdfPage()) |page| @intCast(page + 1) else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_row) + 1) else 0, + .col = if (pane.pdfPage() != null) 0 else if (pane.body.cur_pinned) @intCast(@max(0, pane.body.cur_col) + 1) else 0, }; if (p.njumps > 0 and p.samePlace(p.jumps[p.jcur], now)) { p.jumps[p.jcur] = now; // same visit; the entry IS where you are @@ -6289,7 +6290,7 @@ pub const Pardes = struct { if (prompt_col < printed.dropped) continue; const caret = printed.left + (prompt_col - printed.dropped); if (caret < tx + tw) - s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.mode == .insert }; + s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.body.mode == .insert }; } } @@ -6717,7 +6718,7 @@ pub const Pardes = struct { const typed = panes.File.displayWidth(text[0..@min(pane.tag_col - prompt0, text.len)]); const dropped = shown - panes.File.displayWidth(kept); if (typed < dropped) continue; - p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.mode == .insert }; + p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.body.mode == .insert }; } } if (p.columnBarHeight() > 0) for (0..p.ncol) |column| { @@ -6820,7 +6821,7 @@ pub const Pardes = struct { .font_role = .tagline, }; s.fill(r.x, tag_y, config.GUTTER, BOX_H, box_style); - const box_mode = if (pane.tag_edit) pane.tag_mode else pane.mode; + const box_mode = if (pane.tag_edit) pane.tag_mode else pane.body.mode; s.set(r.x, tag_y, switch (box_mode) { .normal => config.tag_normal, .insert => config.tag_insert, @@ -6929,7 +6930,7 @@ pub const Pardes = struct { if (active and pane.tag_edit) { // bar while typing, block for `:` normal mode (same rule as a body) const col = panes.File.rawDisplayCol(tag, pane.tag_col) -| pane.tag_scroll; - if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = pane.mode == .insert }; + if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = pane.body.mode == .insert }; } for (pane.sel, 0..) |selection, button| { @@ -7039,7 +7040,7 @@ test "raw terminal cursor obeys visibility without hiding modal and tag cursors" defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; p.update(.{ .output = .{ .pane = 0, .bytes = "abc" } }); var frame = std.heap.ArenaAllocator.init(gpa); defer frame.deinit(); @@ -7051,10 +7052,10 @@ test "raw terminal cursor obeys visibility without hiding modal and tag cursors" p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?25l" } }); try std.testing.expect((try p.render(frame.allocator())).cursor == null); - pane.mode = .normal; - pane.cur_pinned = true; - pane.cur_row = 0; - pane.cur_col = 1; + pane.body.mode = .normal; + pane.body.cur_pinned = true; + pane.body.cur_row = 0; + pane.body.cur_col = 1; const modal_cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingCursor; try std.testing.expectEqual(rect.x + config.GUTTER + 1, modal_cursor.x); try std.testing.expectEqual(body_y, modal_cursor.y); @@ -7062,7 +7063,7 @@ test "raw terminal cursor obeys visibility without hiding modal and tag cursors" const tag_cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingCursor; try std.testing.expectEqual(if (p.settings.tag_bottom) rect.y + rect.h - BOX_H else rect.y, tag_cursor.y); Pardes.exitTagEdit(pane); - pane.mode = .tty; + pane.body.mode = .tty; try std.testing.expect((try p.render(frame.allocator())).cursor == null); p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?25h\x1b[?1049h\x1b[2;5H" } }); @@ -7086,7 +7087,7 @@ test "raw terminal cursor follows its active row only while that row is visible" while (p.nextEffect()) |_| {} p.settings.tag_bottom = tag_bottom; const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; for (0..60) |_| p.update(.{ .output = .{ .pane = 0, .bytes = "row\r\n" } }); p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[3;4H" } }); var frame = std.heap.ArenaAllocator.init(gpa); @@ -7129,7 +7130,7 @@ test "Esc back into a tty leaves its view at the prompt" { try std.testing.expectEqual(shell, p.active); p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); - try std.testing.expectEqual(Pane.Mode.tty, sp.mode); + try std.testing.expectEqual(Text.Mode.tty, sp.body.mode); // tty mode follows output to the bottom, so a screenful and a half of it // rides the view down and leaves that pin far up in the scrollback. @@ -7368,7 +7369,7 @@ test "the bare tty startup opens a text pane under the shell, unfocused" { const shell = p.panes[p.col_panes[0][0]].?; const scratch = p.panes[p.col_panes[0][1]].?; try std.testing.expect(shell.isTerminal()); - try std.testing.expectEqual(Pane.Mode.tty, shell.mode); + try std.testing.expectEqual(Text.Mode.tty, shell.body.mode); try std.testing.expect(!scratch.isTerminal()); try std.testing.expectEqualStrings("", scratch.file.?.content); @@ -7391,7 +7392,7 @@ test "tty output follows only from the bottom" { const shell = p.active; const sp = p.panes[shell].?; p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); - try std.testing.expectEqual(Pane.Mode.tty, sp.mode); + try std.testing.expectEqual(Text.Mode.tty, sp.body.mode); for (0..60) |i| { var buf: [32]u8 = undefined; @@ -7445,7 +7446,7 @@ test "Esc back into a file leaves its view where it was" { for (0..20) |_| p.update(.{ .key = .{ .cp = 'j' } }); p.sync(); const view = dp.file.?.scroll; - const row = dp.cur_row; + const row = dp.body.cur_row; try std.testing.expect(view > 0); p.update(.{ .key = .{ .cp = Key.escape } }); // out to the shell @@ -7458,10 +7459,10 @@ test "Esc back into a file leaves its view where it was" { // Nothing moved. The old landing recentred the line under the cursor, so // coming back repainted the whole screen to show what was already on it. try std.testing.expectEqual(view, dp.file.?.scroll); - try std.testing.expectEqual(row, dp.cur_row); + try std.testing.expectEqual(row, dp.body.cur_row); // ...and the cursor is still on screen, which is all `.nearest` promises. - try std.testing.expect(dp.cur_row >= @as(i32, @intCast(view))); - try std.testing.expect(dp.cur_row < @as(i32, @intCast(view)) + @as(i32, dp.rows)); + try std.testing.expect(dp.body.cur_row >= @as(i32, @intCast(view))); + try std.testing.expect(dp.body.cur_row < @as(i32, @intCast(view)) + @as(i32, dp.rows)); // The other arm still centres: a look target, a `:NN`, a search hit. look.focusPaneLine(p, doc, .{ .line = @intCast(row + 1), .col = 1 }, .center); @@ -7488,7 +7489,7 @@ test "raw tty Escape only leaves at a shell prompt" { p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqualStrings("", drainWrites(p, &buf)); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); p.update(.{ .key = .{ .cp = Key.escape } }); // back into the shell try std.testing.expectEqual(shell, p.active); @@ -7497,7 +7498,7 @@ test "raw tty Escape only leaves at a shell prompt" { p.update(.{ .key = .{ .cp = Key.escape } }); // bare Escape leaves at a prompt try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqualStrings("", drainWrites(p, &buf)); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(Text.Mode.tty, pane.body.mode); } test "raw tty Ctrl-B switches to editor mode without sending child input" { @@ -7511,7 +7512,7 @@ test "raw tty Ctrl-B switches to editor mode without sending child input" { _ = drainWrites(p, &buf); p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); try std.testing.expectEqual(shell, p.active); - try std.testing.expectEqual(Pane.Mode.normal, p.panes[shell].?.mode); + try std.testing.expectEqual(Text.Mode.normal, p.panes[shell].?.body.mode); try std.testing.expectEqualStrings("", drainWrites(p, &buf)); } @@ -7599,7 +7600,7 @@ test "raw tty forwards Ctrl letters and former global shortcuts to the child" { }; try std.testing.expectEqualStrings(encoded, drainWrites(p, &buf)); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(Pane.Mode.tty, p.panes[0].?.mode); + try std.testing.expectEqual(Text.Mode.tty, p.panes[0].?.body.mode); try std.testing.expect(!p.ctrl_w_pending and p.clip_pending == null); } // ...and the two paste chords are the exception: Ctrl-V types the register @@ -7745,13 +7746,13 @@ test "leaving tty hides the prompt and keeps the command typed at it" { // The BODY is what you look at, and it follows the mode: in tty the pane // is the program's own screen, so the prompt is there. - sp.mode = .tty; + sp.body.mode = .tty; try std.testing.expectEqualStrings( "user@box ~/src $ grep -rn TODO src/", bodyRowOf(p, sp, "grep") orelse return error.MissingPromptRow, ); - sp.mode = .normal; + sp.body.mode = .normal; try std.testing.expectEqualStrings( "grep -rn TODO src/", bodyRowOf(p, sp, "grep") orelse return error.MissingPromptRow, @@ -7788,7 +7789,7 @@ test "entering tty walks the shell cursor to the column clicked past the prompt" .bytes = "\x1b]133;A;cl=line\x1b\\prompt> \x1b]133;B\x1b\\0123456789", } }); p.sync(); - sp.mode = .normal; + sp.body.mode = .normal; p.shell_rows.stale = true; // The command shows LEFT-HUGGED, so its column 3 is the '3'... @@ -7802,9 +7803,9 @@ test "entering tty walks the shell cursor to the column clicked past the prompt" // ...but the shell's own cursor lives on the real grid, eight cells // further right, behind the prompt this pane is not showing. - sp.cur_row = row; - sp.cur_col = 3; - sp.cur_pinned = true; + sp.body.cur_row = row; + sp.body.cur_col = 3; + sp.body.cur_pinned = true; while (p.nextEffect()) |_| {} panes.Terminal.enterTty(p, shell); @@ -7897,8 +7898,8 @@ test "LSP failure clears only its matching wait without completion indentation" const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const pane = try p.setTestFile("x"); - pane.mode = .insert; - pane.cur_col = 1; + pane.body.mode = .insert; + pane.body.cur_col = 1; p.lspRequest(p.active, .completion, ""); const old_id = p.lsp_wait.?.id; @@ -7909,13 +7910,13 @@ test "LSP failure clears only its matching wait without completion indentation" p.update(.{ .lsp_resp = .{ .id = current_id, .rows = null } }); try std.testing.expect(p.lsp_wait == null); try std.testing.expectEqualStrings("x", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); p.lspRequest(p.active, .completion, ""); p.update(.{ .lsp_resp = .{ .id = p.lsp_wait.?.id, .rows = "" } }); try std.testing.expect(p.lsp_wait == null); try std.testing.expectEqualStrings("x ", pane.file.?.content); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); } test "board heap: every allocation failure during boot is a clean OutOfMemory" { @@ -7935,25 +7936,25 @@ test "jump history clears selections and lands at the recorded cursor" { p.jumps[0] = .{ .pane = 0, .serial = pane.serial, .line = 3, .col = 8 }; p.jumps[1] = .{ .pane = 0, .serial = pane.serial, .line = 1, .col = 1 }; for ([_]u21{ 'o', 'i' }, 0..) |key, index| { - pane.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); + pane.body.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); pane.sel[0] = .{ .state = .done, .c0 = 2, .c1 = 5, .r0 = 1, .r1 = 1 }; try edit.capturePointerSelection(p, pane, 0); - pane.select = true; + pane.body.select = true; p.update(.{ .key = .{ .cp = key, .ctrl = true } }); try std.testing.expectEqual(index, p.jcur); try std.testing.expectEqual(@as(usize, 2), p.njumps); try std.testing.expectEqual(@as(u32, 3), p.jumps[0].line); - try std.testing.expect(!pane.vsel.active and !pane.msel.active and !pane.select); - try std.testing.expectEqual(@as(u8, 0), pane.nsel); + try std.testing.expect(!pane.body.vsel.active and !pane.body.msel.active and !pane.body.select); + try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); try std.testing.expectEqual(.none, pane.sel[0].state); try std.testing.expect(pane.pointer_selections[0] == null); - try std.testing.expectEqual(@as(i32, if (index == 0) 2 else 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, if (index == 0) 7 else 0), pane.cur_col); + try std.testing.expectEqual(@as(i32, if (index == 0) 2 else 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, if (index == 0) 7 else 0), pane.body.cur_col); } - pane.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); - const selected = pane.primaryRange(pane.file.?.content, 0); + pane.body.setRange(pane.file.?.content, 0, .{ .anchor = 1, .head = 5 }, true); + const selected = pane.body.primaryRange(pane.file.?.content, 0); p.jumpBy(1); // No forward entry: selection stays untouched. - try std.testing.expectEqualDeep(selected, pane.primaryRange(pane.file.?.content, 0)); + try std.testing.expectEqualDeep(selected, pane.body.primaryRange(pane.file.?.content, 0)); for ([_]Loc{ .{ .pane = 15, .serial = pane.serial, .line = 1, .col = 1 }, .{ .pane = 0, .serial = pane.serial + 1, .line = 1, .col = 1 }, @@ -7961,7 +7962,7 @@ test "jump history clears selections and lands at the recorded cursor" { p.jumps[0] = missing; p.jumpBy(-1); try std.testing.expectEqual(@as(usize, 1), p.jcur); - try std.testing.expectEqualDeep(selected, pane.primaryRange(pane.file.?.content, 0)); + try std.testing.expectEqualDeep(selected, pane.body.primaryRange(pane.file.?.content, 0)); } } diff --git a/src/pdf_view.zig b/src/pdf_view.zig index 721dac5a..92a05618 100644 --- a/src/pdf_view.zig +++ b/src/pdf_view.zig @@ -1911,7 +1911,7 @@ pub fn openPane( errdefer state.deinit(core.pdf_gpa); const pane = try core.newDocPane(id); pane.pdf = state; - pane.cur_pinned = true; + pane.body.cur_pinned = true; if (filesystem.localPath(path) != null) core.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } }); return pane; } @@ -1967,15 +1967,15 @@ fn refreshCleanSections(core: *pardes.Pardes, state: *State) void { File.setContent(core, file, content); const rows = File.lineCount(file.content); file.scroll = @min(file.scroll, rows - 1); - const row = @min(@as(usize, @intCast(@max(0, result.cur_row))), rows - 1); - result.cur_row = @intCast(row); - result.cur_col = @intCast(@min( - @as(usize, @intCast(@max(0, result.cur_col))), + const row = @min(@as(usize, @intCast(@max(0, result.body.cur_row))), rows - 1); + result.body.cur_row = @intCast(row); + result.body.cur_col = @intCast(@min( + @as(usize, @intCast(@max(0, result.body.cur_col))), modal.lineSlice(file.content, row).len, )); - result.msel.active = false; - result.vsel.active = false; - result.nsel = 0; + result.body.msel.active = false; + result.body.vsel.active = false; + result.body.nsel = 0; state.sections_output.?.revision = file.revision; } @@ -2003,11 +2003,11 @@ fn rearmCleanSections( if (file.revision != remembered.revision) return false; file.scroll = 0; - result.cur_row = 0; - result.cur_col = 0; - result.msel.active = false; - result.vsel.active = false; - result.nsel = 0; + result.body.cur_row = 0; + result.body.cur_col = 0; + result.body.msel.active = false; + result.body.vsel.active = false; + result.body.nsel = 0; pane.search_pane = remembered.pane; pane.search_row = null; look.armLookWalk(core, remembered.pane); @@ -2110,11 +2110,11 @@ pub fn lookSection( } pub fn resetPageChrome(pane: *pardes.Pane) void { - pane.cur_row = 0; - pane.cur_col = 0; - pane.vsel.active = false; - pane.msel.active = false; - pane.nsel = 0; + pane.body.cur_row = 0; + pane.body.cur_col = 0; + pane.body.vsel.active = false; + pane.body.msel.active = false; + pane.body.nsel = 0; } pub fn activatePage( @@ -2454,12 +2454,12 @@ pub fn pointerRelease( const dragged = grid_selection.c0 != grid_selection.c1 or grid_selection.r0 != grid_selection.r1; if (!dragged) state.clearSelection(core.pdf_gpa); - pane.cur_pinned = true; - pane.mode = .normal; - pane.msel.active = false; - pane.vsel.active = false; - pane.normal.clear(); - pane.nsel = 0; + pane.body.cur_pinned = true; + pane.body.mode = .normal; + pane.body.msel.active = false; + pane.body.vsel.active = false; + pane.body.normal.clear(); + pane.body.nsel = 0; state.clearDrag(); return .{}; } diff --git a/src/tty/tty.zig b/src/tty/tty.zig index e3378f5b..903c5c0c 100644 --- a/src/tty/tty.zig +++ b/src/tty/tty.zig @@ -1418,8 +1418,8 @@ test "TTY worker setup failures finish matching LSP and pipe requests" { try std.testing.expect(core.lsp_wait == null); try std.testing.expectEqualStrings("abc", pane.file.?.content); - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; core.update(.{ .key = .{ .cp = '|' } }); core.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } }); core.update(.{ .key = .{ .cp = pardes.Key.enter } }); @@ -1607,7 +1607,7 @@ test "keypad Enter matches Return for editor input terminal writes and paste" { { const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer core.deinit(); - core.panes[0].?.mode = .tty; + core.panes[0].?.body.mode = .tty; while (core.nextEffect()) |_| {} core.update(event); var written: std.Io.Writer.Allocating = .init(std.testing.allocator); diff --git a/test/fs_bench.zig b/test/fs_bench.zig index 00601ca3..a816ebbd 100644 --- a/test/fs_bench.zig +++ b/test/fs_bench.zig @@ -281,7 +281,7 @@ pub fn main(init: std.process.Init) !void { var keys = try Session.init(&counting, 32 * 1024); defer keys.deinit(); const pane = keys.core.panes[keys.file_id].?; - pane.mode = .insert; + pane.body.mode = .insert; const initial_bytes = pane.file.?.content.len; key_body_bytes = initial_bytes; if (scripted) { @@ -295,7 +295,7 @@ pub fn main(init: std.process.Init) !void { while (keys.core.nextEffect()) |_| {} } const elapsed = nowNs() - start; - if (pane.file.?.content.len != initial_bytes + key_reps or pane.cur_col != key_reps or + if (pane.file.?.content.len != initial_bytes + key_reps or pane.body.cur_col != key_reps or !std.mem.allEqual(u8, pane.file.?.content[0..key_reps], 'x')) return error.MissingKeystroke; const events = &keys.core.panes[keys.file_id].?.fs.events; if (scripted) for (0..key_reps) |i| { diff --git a/test/hxdiff.zig b/test/hxdiff.zig index e79a6bb6..627bbac3 100644 --- a/test/hxdiff.zig +++ b/test/hxdiff.zig @@ -397,9 +397,9 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result core.update(.{ .key = .{ .cp = core.opts.tty_toggle, .ctrl = true } }); pump(core); if (core.panes[core.active]) |pane| { - pane.cur_row = 0; - pane.cur_col = 0; - pane.cur_pinned = true; + pane.body.cur_row = 0; + pane.body.cur_col = 0; + pane.body.cur_pinned = true; } } else fatal("case {s}: unknown pane {s}", .{ case.name, pane_kind }); @@ -488,7 +488,7 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result // the cursor's empty row below the output (see Pardes.shellRows), so // its line list is exactly a file content's — join it and you have the // same bytes, trailing newline included, with nothing to patch up. - const lines = try pardes.edit.paneCursorLines(core, pane); + const lines = try pardes.edit.paneCursorLines(core, &pane.body); var text_len: usize = if (lines.len == 0) 0 else lines.len - 1; for (lines) |ln| text_len = std.math.add(usize, text_len, ln.len) catch return error.OutOfMemory; const joined = try arena.alloc(u8, text_len); @@ -503,28 +503,28 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result } text = joined; } - var anchor: Pos = .{ .row = pane.cur_row, .col = pane.cur_col }; - if (pane.vsel.active) { - anchor = .{ .row = pane.vsel.row, .col = pane.vsel.col }; - } else if (pane.msel.active) { + var anchor: Pos = .{ .row = pane.body.cur_row, .col = pane.body.cur_col }; + if (pane.body.vsel.active) { + anchor = .{ .row = pane.body.vsel.row, .col = pane.body.vsel.col }; + } else if (pane.body.msel.active) { // line selection: the far end (the r0/r1 not holding the cursor), col 0 - anchor = .{ .row = if (pane.cur_row == pane.msel.r1) pane.msel.r0 else pane.msel.r1, .col = 0 }; + anchor = .{ .row = if (pane.body.cur_row == pane.body.msel.r1) pane.body.msel.r0 else pane.body.msel.r1, .col = 0 }; } // mode "select" ONLY while v-extend is on (helix keeps x/%/mi selections - // in normal mode); pane.mode itself never holds a select state - const mode = if (pane.mode == .normal and pane.select) "select" else @tagName(pane.mode); + // in normal mode); pane.body.mode itself never holds a select state + const mode = if (pane.body.mode == .normal and pane.body.select) "select" else @tagName(pane.body.mode); // multiple cursors: the primary (cur_row/cur_col + the anchor above) and - // pane.sels, slotted together in document order. pane.sels is already + // pane.body.sels, slotted together in document order. pane.body.sels is already // ordered, so the primary is the only one that needs placing. var sels: ?[]const Sel = null; var primary: ?usize = null; - if (pane.nsel > 0) { - const out = try arena.alloc(Sel, @as(usize, pane.nsel) + 1); + if (pane.body.nsel > 0) { + const out = try arena.alloc(Sel, @as(usize, pane.body.nsel) + 1); var out_len: usize = 0; - const prim: Sel = .{ .cursor = .{ .row = pane.cur_row, .col = pane.cur_col }, .anchor = anchor }; + const prim: Sel = .{ .cursor = .{ .row = pane.body.cur_row, .col = pane.body.cur_col }, .anchor = anchor }; const pr_row = @min(prim.cursor.row, prim.anchor.row); const pr_col = if (prim.cursor.row == prim.anchor.row) @min(prim.cursor.col, prim.anchor.col) else if (prim.cursor.row < prim.anchor.row) prim.cursor.col else prim.anchor.col; - for (pane.sels[0..pane.nsel]) |s| { + for (pane.body.sels[0..pane.body.nsel]) |s| { const s_row = @min(s.row, s.arow); const s_col = if (s.row == s.arow) @min(s.col, s.acol) else if (s.row < s.arow) s.col else s.acol; if (primary == null and (pr_row < s_row or (pr_row == s_row and pr_col <= s_col))) { @@ -545,7 +545,7 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result .name = case.name, .text = try arena.dupe(u8, text), .mode = mode, - .cursor = .{ .row = pane.cur_row, .col = pane.cur_col }, + .cursor = .{ .row = pane.body.cur_row, .col = pane.body.cur_col }, .anchor = anchor, .sels = sels, .primary = primary, diff --git a/test/mode.zig b/test/mode.zig index cc36ac04..19d59699 100644 --- a/test/mode.zig +++ b/test/mode.zig @@ -3,9 +3,9 @@ const pardes = @import("pardes"); const panes = pardes.panes; const Pardes = pardes.Pardes; -fn mode(p: *Pardes, id: usize, expected: pardes.Pane.Mode) !void { +fn mode(p: *Pardes, id: usize, expected: panes.Text.Mode) !void { try std.testing.expect(p.executeBuiltinLine(id, "Mode")); - try std.testing.expectEqual(expected, p.panes[id].?.mode); + try std.testing.expectEqual(expected, p.panes[id].?.body.mode); try std.testing.expect(!p.panes[id].?.tag_edit); } @@ -33,14 +33,14 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { p.presentation.enabled = false; while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expect(std.mem.indexOf(u8, try p.tagText(p.scratch.allocator(), pane), " Mode") != null); try mode(p, 0, .normal); try mode(p, 0, .insert); try clickModeTag(p); - try std.testing.expectEqual(pardes.Pane.Mode.insert, pane.tag_mode); + try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode); p.update(.{ .key = .{ .cp = pardes.Key.tab } }); - try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expect(!pane.tag_edit); while (p.nextEffect()) |effect| try std.testing.expect(effect != .write); @@ -48,11 +48,11 @@ test "Mode cycles terminal modes and keeps legacy toggle and custom tags" { try mode(p, 0, .normal); try mode(p, 0, .insert); try std.testing.expect(p.executeBuiltinLine(0, "Togglettymode")); - try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); - try std.testing.expectEqual(pardes.Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); p.update(.{ .key = .{ .cp = 'b', .ctrl = true } }); - try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(" Togglettymode Keep", pane.tag_tail[0..pane.tag_tail_len]); } @@ -71,19 +71,19 @@ test "Mode from actual tag Exec preserves parked file and output insert transact p.update(.{ .key = .{ .cp = 'x', .text = "x" } }); try std.testing.expectEqualStrings("界xab\n", pane.file.?.content); try clickModeTag(p); - try std.testing.expectEqual(pardes.Pane.Mode.insert, pane.tag_mode); - if (normal_tag) pane.mode = .normal; + try std.testing.expectEqual(panes.Text.Mode.insert, pane.tag_mode); + if (normal_tag) pane.body.mode = .normal; p.update(.{ .key = .{ .cp = pardes.Key.tab } }); - try std.testing.expectEqual(pardes.Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); try std.testing.expect(!pane.tag_edit); - try std.testing.expect(pane.append_at == null); + try std.testing.expect(pane.body.append_at == null); p.update(.{ .key = .{ .cp = 'u' } }); try std.testing.expectEqualStrings("ab\n", pane.file.?.content); p.update(.{ .key = .{ .cp = 'a' } }); p.update(.{ .key = .{ .cp = 'z', .text = "z" } }); - try std.testing.expect(pane.append_at != null); + try std.testing.expect(pane.body.append_at != null); try mode(p, 0, .normal); - try std.testing.expect(pane.append_at == null); + try std.testing.expect(pane.body.append_at == null); p.update(.{ .key = .{ .cp = 'u' } }); try std.testing.expectEqualStrings("ab\n", pane.file.?.content); } diff --git a/test/output.zig b/test/output.zig index 7c5f7f9b..c245e5f5 100644 --- a/test/output.zig +++ b/test/output.zig @@ -61,10 +61,10 @@ test "search tint survives wrapping and theme changes while selections remain au light_matches += 1; } try std.testing.expectEqual(@as(usize, 3), light_matches); - pane.mode = .normal; - pane.cur_row = 0; - pane.cur_col = @intCast(std.mem.indexOf(u8, text, "abc").? + 2); - pane.vsel = .{ .active = true, .row = 0, .col = pane.cur_col - 2, .explicit = true }; + pane.body.mode = .normal; + pane.body.cur_row = 0; + pane.body.cur_col = @intCast(std.mem.indexOf(u8, text, "abc").? + 2); + pane.body.vsel = .{ .active = true, .row = 0, .col = pane.body.cur_col - 2, .explicit = true }; const selected = try p.render(frame.allocator()); for (selected.cells) |cell| try std.testing.expect(!std.meta.eql(cell.style.bg, pardes.Color{ .rgb = p.theme().search_bg.? })); @@ -200,46 +200,46 @@ test "reference Look selects the next sorted row from the captured request and k const source = p.panes[0].?; const response = "m.zig:5:5-8 later\nz.zig:1:1 last-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\na.zig:1:1 first-file\nm.zig:4:5-8 next\n"; const sorted = "a.zig:1:1 first-file\nm.zig:1:7-10 earlier\nm.zig:3:5-8 current\nm.zig:4:5-8 next\nm.zig:5:5-8 later\nz.zig:1:1 last-file\n"; - source.cur_row = 2; - source.cur_col = 4; + source.body.cur_row = 2; + source.body.cur_col = 4; p.lspRequest(0, .references, ""); const request = p.lsp_wait.?.id; - source.cur_row = 4; + source.body.cur_row = 4; p.lspResponse(request, response); const rid = source.search_pane orelse return error.MissingResults; const results = p.panes[rid].?; try expectResultRows(sorted, &results.file.?); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(i32, 3), source.cur_row); - try std.testing.expect(source.vsel.active); - try std.testing.expectEqual(@as(i32, 4), @min(source.cur_col, source.vsel.col)); - try std.testing.expectEqual(@as(i32, 7), @max(source.cur_col, source.vsel.col)); - try std.testing.expectEqual(@as(i32, 3), results.cur_row); + try std.testing.expectEqual(@as(i32, 3), source.body.cur_row); + try std.testing.expect(source.body.vsel.active); + try std.testing.expectEqual(@as(i32, 4), @min(source.body.cur_col, source.body.vsel.col)); + try std.testing.expectEqual(@as(i32, 7), @max(source.body.cur_col, source.body.vsel.col)); + try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); try std.testing.expectEqual(@as(usize, 3), source.search_row.?); try std.testing.expectEqual(results.serial, p.look_walk_owner.?); try std.testing.expectEqual(@as(i32, 3), results.look_at.?.row); const available = p.freeSlot(); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); - try std.testing.expectEqual(@as(i32, 4), results.cur_row); + try std.testing.expectEqual(@as(i32, 4), results.body.cur_row); try std.testing.expectEqual(available, p.freeSlot()); p.update(.{ .key = .{ .cp = 'N' } }); - try std.testing.expectEqual(@as(i32, 3), results.cur_row); + try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); p.active = 0; - source.cur_row = 4; - source.cur_col = 4; + source.body.cur_row = 4; + source.body.cur_col = 4; p.lspRequest(0, .references, ""); p.lspResponse(p.lsp_wait.?.id, response); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/z.zig")); - try std.testing.expectEqual(@as(i32, 5), results.cur_row); + try std.testing.expectEqual(@as(i32, 5), results.body.cur_row); try std.testing.expectEqual(@as(usize, 5), source.search_row.?); const last = p.active; p.lspRequest(last, .references, ""); p.lspResponse(p.lsp_wait.?.id, response); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/a.zig")); const wrapped = p.panes[p.panes[last].?.search_pane.?].?; - try std.testing.expectEqual(@as(i32, 0), wrapped.cur_row); + try std.testing.expectEqual(@as(i32, 0), wrapped.body.cur_row); try std.testing.expectEqual(wrapped.serial, p.look_walk_owner.?); } @@ -280,7 +280,7 @@ test "a single language location opens the full spaced filename" { p.lspRequest(0, .references, ""); p.lspResponse(p.lsp_wait.?.id, "with space.zig:2:1 second\n"); try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/with space.zig")); - try std.testing.expectEqual(@as(i32, 1), p.panes[p.active].?.cur_row); + try std.testing.expectEqual(@as(i32, 1), p.panes[p.active].?.body.cur_row); for (p.panes) |slot| { const pane = slot orelse continue; if (pane.file) |file| try std.testing.expect(file.output == null); @@ -316,19 +316,19 @@ test "reordered identical results preserve the selected row and warm line index" const index = try panes.File.lineIndex(p.gpa, file); const content = file.content.ptr; const revision = file.revision; - result.cur_row = 1; - result.cur_col = 0; - result.vsel = .{ .active = true, .explicit = true, .row = 1, .col = 9 }; - result.select = true; - const selected = result.vsel; + result.body.cur_row = 1; + result.body.cur_col = 0; + result.body.vsel = .{ .active = true, .explicit = true, .row = 1, .col = 9 }; + result.body.select = true; + const selected = result.body.vsel; try panes.Output.fillResults(p, 0, "/tmp", .{ .query = .references }, "new request", try p.gpa.dupe(u8, "a.zig:10:1 second\na.zig:2:1 first\n"), 0); try expectResultRows(sorted, file); try std.testing.expectEqual(content, file.content.ptr); try std.testing.expectEqual(index.ptr, file.line_starts.ptr); try std.testing.expectEqual(revision, file.revision); - try std.testing.expectEqual(selected, result.vsel); - try std.testing.expect(result.select); - try std.testing.expectEqual(@as(i32, 1), result.cur_row); + try std.testing.expectEqual(selected, result.body.vsel); + try std.testing.expect(result.body.select); + try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); } @@ -390,7 +390,7 @@ test "result stepping reaches an unterminated last row" { try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); try std.testing.expectEqual(@as(usize, 1), source.search_row.?); const result = p.panes[source.search_pane.?].?; - try std.testing.expectEqual(@as(i32, 1), result.cur_row); + try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); } test "Mini uses OS source precedence and refreshes one independent snapshot" { @@ -708,10 +708,10 @@ fn projectPaths(cwd_buf: *[4096]u8, path_buf: *[4096]u8) !ProjectPaths { fn selectedOutputText(pane: *const pardes.Pane) ?[]const u8 { const file = pane.file orelse return null; - if (!pane.vsel.active or pane.vsel.row != pane.cur_row or pane.cur_row < 0) return null; - const line = modal.lineSlice(file.content, @intCast(pane.cur_row)); - const lo: usize = @intCast(@max(0, @min(pane.vsel.col, pane.cur_col))); - const hi: usize = @intCast(@max(0, @max(pane.vsel.col, pane.cur_col))); + if (!pane.body.vsel.active or pane.body.vsel.row != pane.body.cur_row or pane.body.cur_row < 0) return null; + const line = modal.lineSlice(file.content, @intCast(pane.body.cur_row)); + const lo: usize = @intCast(@max(0, @min(pane.body.vsel.col, pane.body.cur_col))); + const hi: usize = @intCast(@max(0, @max(pane.body.vsel.col, pane.body.cur_col))); if (lo >= line.len) return ""; return line[lo..@min(line.len, hi + 1)]; } @@ -735,7 +735,7 @@ test "n/N in ordinary text skips comment slashes and retains real paths" { }) |step| { p.update(.{ .key = .{ .cp = step.key } }); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(step.row, pane.cur_row); + try std.testing.expectEqual(step.row, pane.body.cur_row); try std.testing.expectEqualStrings(step.text, selectedOutputText(pane) orelse ""); try std.testing.expectEqual(available, p.freeSlot()); } @@ -788,7 +788,7 @@ test "n/N skips formatted context and selects comment match locations" { p.update(.{ .key = .{ .cp = step.key } }); try std.testing.expectEqual(id, p.active); const row = matches[step.match]; - try std.testing.expectEqual(@as(i32, @intCast(row)), result.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(row)), result.body.cur_row); const line = modal.lineSlice(file.content, row); try std.testing.expectEqualStrings(line[0..file.location_rows[row].location_end], selectedOutputText(result) orelse ""); try std.testing.expectEqual(available, p.freeSlot()); @@ -799,9 +799,9 @@ test "n/N skips formatted context and selects comment match locations" { pardes.look.armLookWalk(restored, id); const restored_result = restored.panes[id].?; restored.update(.{ .key = .{ .cp = 'n' } }); - try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(matches[0])), restored_result.body.cur_row); restored.update(.{ .key = .{ .cp = 'N' } }); - try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored_result.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored_result.body.cur_row); const restored_file = &restored_result.file.?; const last = modal.lineSlice(restored_file.content, matches[1]); try std.testing.expectEqualStrings(last[0..restored_file.location_rows[matches[1]].location_end], selectedOutputText(restored_result) orelse ""); @@ -825,18 +825,18 @@ test "n/N selects one output location per row and opens nothing" { p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); try std.testing.expectEqual(panes_before, p.freeSlot()); - try std.testing.expect(results.vsel.active and results.vsel.explicit); + try std.testing.expect(results.body.vsel.active and results.body.vsel.explicit); try std.testing.expectEqualStrings("build.zig:1:1", selectedOutputText(results) orelse ""); try std.testing.expectEqual(panes.Output.Grain.line, panes.Output.grain(results.file.?.output)); p.update(.{ .key = .{ .cp = 'n' } }); - try std.testing.expectEqual(@as(i32, 1), results.cur_row); + try std.testing.expectEqual(@as(i32, 1), results.body.cur_row); try std.testing.expectEqualStrings("mise.toml", selectedOutputText(results) orelse ""); p.update(.{ .key = .{ .cp = 'n' } }); - try std.testing.expectEqual(@as(i32, 3), results.cur_row); // row 2 is not look-able + try std.testing.expectEqual(@as(i32, 3), results.body.cur_row); // row 2 is not look-able try std.testing.expectEqualStrings("uucode_config.zig:7:1", selectedOutputText(results) orelse ""); p.update(.{ .key = .{ .cp = 'n' } }); - try std.testing.expectEqual(@as(i32, 0), results.cur_row); // ring seam + try std.testing.expectEqual(@as(i32, 0), results.body.cur_row); // ring seam p.update(.{ .key = .{ .cp = Key.enter } }); try std.testing.expect(p.freeSlot() != panes_before); @@ -861,16 +861,16 @@ test "n/N resumes the result output whose Look moved focus away" { try std.testing.expect(p.executeBuiltinLine(0, "Look target")); const rid = p.panes[0].?.search_pane orelse return error.MissingResults; const results = p.panes[rid].?; - const first = results.cur_row; + const first = results.body.cur_row; try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expect(results.look_at != null); try std.testing.expectEqual(results.serial, p.look_walk_owner orelse return error.MissingLookOwner); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); - try std.testing.expectEqual(first + 1, results.cur_row); + try std.testing.expectEqual(first + 1, results.body.cur_row); p.update(.{ .key = .{ .cp = 'N' } }); - try std.testing.expectEqual(first, results.cur_row); + try std.testing.expectEqual(first, results.body.cur_row); // A later no-match answer has no position to resume and therefore cannot // steal the provenance established by the Look above. @@ -888,7 +888,7 @@ test "n/N resumes the result output whose Look moved focus away" { try std.testing.expectEqual(owner, p.look_walk_owner.?); p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(rid, p.active); - try std.testing.expectEqual(first + 1, results.cur_row); + try std.testing.expectEqual(first + 1, results.body.cur_row); // Emptying the owner itself keeps its identity as the ring's starting // point. Deleting it then leaves a stale serial, never an id that can bind @@ -920,7 +920,7 @@ test "N exactly reverses n across output panes and the ring seam" { const here = struct { fn at(pp: *Pardes) Mark { const pane = pp.panes[pp.active].?; - return .{ .pane = pp.active, .row = pane.cur_row, .col = pane.cur_col }; + return .{ .pane = pp.active, .row = pane.body.cur_row, .col = pane.body.cur_col }; } }.at; p.update(.{ .key = .{ .cp = 'n' } }); @@ -957,7 +957,7 @@ test "ThemeSel groups native themes first and skips headings in both directions" const list = p.panes[tid].?; const content = list.file.?.content; try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(i32, 0), list.cur_row); + try std.testing.expectEqual(@as(i32, 0), list.body.cur_row); try std.testing.expectEqual(@as(usize, 0), list.file.?.scroll); try std.testing.expectEqualStrings("# Pardes themes", modal.lineSlice(content, 0)); try std.testing.expectEqualStrings("# Legacy and imported themes", modal.lineSlice(content, pardes.native_theme_count + 2)); @@ -1024,10 +1024,10 @@ test "identical result refresh keeps warm content caches without allocation" { const id = source.search_pane orelse return error.MissingResults; const result = p.panes[id].?; const file = &result.file.?; - result.cur_row = 1; - result.cur_col = 12; - result.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 2 }; - result.select = true; + result.body.cur_row = 1; + result.body.cur_col = 12; + result.body.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 2 }; + result.body.select = true; file.scroll = 1; result.hscroll = 3; panes.File.refreshHighlights(p); @@ -1040,7 +1040,7 @@ test "identical result refresh keeps warm content caches without allocation" { const body_ptr = file.content.ptr; const style_ptr = file.highlights.ptr; const highlight_start = file.highlight_start; - const selected = result.vsel; + const selected = result.body.vsel; const event_node = pardes.ctlfs.Node.of(result.serial, .event); const opened = pardes.ctlfs.handle(p, .{ .tag = 0, .op = .open, .node = event_node }); try std.testing.expectEqual(.ok, opened.status); @@ -1071,10 +1071,10 @@ test "identical result refresh keeps warm content caches without allocation" { try std.testing.expectEqual(freed + text.len, allocator.freed_bytes); try std.testing.expectEqual(deallocations + 1, allocator.deallocations); try std.testing.expect(!allocator.has_induced_failure); - try std.testing.expectEqual(selected, result.vsel); - try std.testing.expect(result.select); - try std.testing.expectEqual(@as(i32, 1), result.cur_row); - try std.testing.expectEqual(@as(i32, 12), result.cur_col); + try std.testing.expectEqual(selected, result.body.vsel); + try std.testing.expect(result.body.select); + try std.testing.expectEqual(@as(i32, 1), result.body.cur_row); + try std.testing.expectEqual(@as(i32, 12), result.body.cur_col); try std.testing.expectEqual(@as(usize, 1), file.scroll); try std.testing.expectEqual(@as(i32, 3), result.hscroll); try std.testing.expectEqualStrings("after", file.output.?.arg()); @@ -1093,9 +1093,9 @@ test "identical result refresh keeps warm content caches without allocation" { try std.testing.expectEqualSlices(usize, &.{ 0, 6 }, file.line_starts); try std.testing.expectEqual(@as(usize, 0), file.highlights.len); try std.testing.expect(file.syntax_dirty); - try std.testing.expect(!result.vsel.active and !result.select); - try std.testing.expectEqual(@as(i32, 0), result.cur_row); - try std.testing.expectEqual(@as(i32, 0), result.cur_col); + try std.testing.expect(!result.body.vsel.active and !result.body.select); + try std.testing.expectEqual(@as(i32, 0), result.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), result.body.cur_col); try std.testing.expectEqual(@as(usize, 0), file.scroll); try std.testing.expectEqual(@as(i32, 0), result.hscroll); try std.testing.expect(source.search_row == null); @@ -1211,15 +1211,15 @@ test "result alignment chunks keep seventeen matches independent and refresh sta const content = file.content.ptr; const revision = file.revision; const index = try panes.File.lineIndex(p.gpa, file); - result.cur_row = 8; - result.cur_col = @intCast(file.location_rows[8].code_start); + result.body.cur_row = 8; + result.body.cur_col = @intCast(file.location_rows[8].code_start); try panes.Output.fillResults(p, 0, "/", origin, "chunks", try p.gpa.dupe(u8, input.items), 8); try std.testing.expectEqual(result_id, source.search_pane.?); try std.testing.expectEqual(content, file.content.ptr); try std.testing.expectEqual(revision, file.revision); try std.testing.expectEqual(index.ptr, file.line_starts.ptr); try std.testing.expectEqual(@as(usize, 8), source.search_row.?); - try std.testing.expectEqual(@as(i32, 8), result.cur_row); + try std.testing.expectEqual(@as(i32, 8), result.body.cur_row); for (file.location_rows, starts) |row, start| try std.testing.expectEqual(start, row.code_start); // Simulate a restored view made by the previous global-padding @@ -1561,11 +1561,11 @@ test "stacked search and LSP results retain source tint navigation and dump owne const number = std.mem.indexOf(u8, file.content, "42").?; try std.testing.expectEqual(@intFromEnum(pardes.syntax.Syn.number), file.highlights[number]); p.active = id; - pane.mode = .normal; + pane.body.mode = .normal; pardes.look.armLookWalk(p, id); for ([_]struct { key: u21, match: usize }{ .{ .key = 'n', .match = 0 }, .{ .key = 'n', .match = 1 }, .{ .key = 'N', .match = 0 } }) |step| { p.update(.{ .key = .{ .cp = step.key } }); - try std.testing.expectEqual(@as(i32, @intCast(matches[step.match])), pane.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(matches[step.match])), pane.body.cur_row); const row = file.location_rows[matches[step.match]]; const line = modal.lineSlice(file.content, matches[step.match]); try std.testing.expectEqualStrings(line[0..row.location_end], selectedOutputText(pane) orelse ""); @@ -1587,7 +1587,7 @@ test "stacked search and LSP results retain source tint navigation and dump owne restored.active = id; pardes.look.armLookWalk(restored, id); restored.update(.{ .key = .{ .cp = 'N' } }); - try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(matches[1])), restored.panes[id].?.body.cur_row); try std.testing.expectEqualStrings(source_text, source.file.?.content); } } diff --git a/test/panes.zig b/test/panes.zig index 60dabb8a..4c969ab5 100644 --- a/test/panes.zig +++ b/test/panes.zig @@ -327,14 +327,14 @@ const TagNavigationTests = struct { key(p, 'i'); key(p, Key.end); const before = pane.tag_tail_len; - try std.testing.expect(pane.tag_edit and pane.mode == .insert); + try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); for ("hjklHJKL") |letter| { const text = [_]u8{letter}; p.update(.{ .key = .{ .cp = letter, .text = &text } }); - try std.testing.expect(pane.tag_edit and pane.mode == .insert); + try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); } try std.testing.expectEqualStrings("hjklHJKL", pane.tag_tail[before..pane.tag_tail_len]); - try std.testing.expect(pane.tag_edit and pane.mode == .insert and p.topbar_col == null); + try std.testing.expect(pane.tag_edit and pane.body.mode == .insert and p.topbar_col == null); try std.testing.expectEqual(@as(usize, 0), p.active); } } @@ -370,7 +370,7 @@ const BodyHitTests = struct { const expected = pane.wrapAt(row).line; p.update(.{ .mouse = mouse(layer, row, 2, .press) }); p.update(.{ .mouse = mouse(layer, row, 2, .release) }); - try std.testing.expectEqual(expected, pane.cur_row); + try std.testing.expectEqual(expected, pane.body.cur_row); try std.testing.expectEqual(top, pane.scroll()); } const last = pane.body_visible_rows - 1; @@ -383,11 +383,11 @@ const BodyHitTests = struct { try std.testing.expectEqualStrings("body\nbody", pardes.test_api.heldSelection(p, 0).?); p.update(.{ .mouse = mouse(layer, last, 10, .press) }); p.update(.{ .mouse = mouse(layer, last, 10, .release) }); - try std.testing.expectEqual(expected, pane.cur_row); + try std.testing.expectEqual(expected, pane.body.cur_row); var stale = mouse(layer, 0, 2, .press); stale.body_hit.?.serial +%= 1; p.update(.{ .mouse = stale }); - try std.testing.expectEqual(expected, pane.cur_row); + try std.testing.expectEqual(expected, pane.body.cur_row); p.update(.{ .key = .{ .cp = 'i' } }); p.update(.{ .key = .{ .cp = 'X', .text = "X" } }); try std.testing.expect(std.mem.indexOfScalar(u8, modal.lineSlice(pane.file.?.content, @intCast(expected)), 'X') != null); @@ -425,7 +425,7 @@ const LineNumberLookTests = struct { release.button = .right; p.update(.{ .mouse = press }); p.update(.{ .mouse = release }); - try std.testing.expectEqual(expected, pane.cur_row); + try std.testing.expectEqual(expected, pane.body.cur_row); } } }; @@ -496,9 +496,9 @@ const TreeContextTests = struct { try std.testing.expectEqual(@as(i32, 0), pane.scroll()); pardes.look.lookAt(p, 0, "/tree-context.zig:60"); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(i32, 59), pane.cur_row); + try std.testing.expectEqual(@as(i32, 59), pane.body.cur_row); const surface = try p.render(frame.allocator()); - const visible = pane.wrapRow(pane.cur_row, pane.cur_col); + const visible = pane.wrapRow(pane.body.cur_row, pane.body.cur_col); try std.testing.expect(visible.row >= pane.context_rows); try std.testing.expect(visible.row < pane.rows); try std.testing.expectEqual(@as(i32, 59), pane.wrapAt(visible.row).line); @@ -528,8 +528,8 @@ const TreeContextTests = struct { const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); const y = p.bodyTop(rect); ClickCursorTests.click(p, .left, x + 6, y); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, 6), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 6), pane.body.cur_col); try std.testing.expectEqual(@as(i32, 7), pane.scroll()); const cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingContextCursor; try std.testing.expectEqual(x + 6, cursor.x); @@ -552,7 +552,7 @@ const TreeContextTests = struct { try std.testing.expectEqual(pane.scroll(), pane.wrapAt(pane.context_rows).line); } try std.testing.expectEqual(@as(u16, 0), pane.context_rows); - try std.testing.expectEqual(@as(i32, 0), pane.wrapRow(0, pane.cur_col).row); + try std.testing.expectEqual(@as(i32, 0), pane.wrapRow(0, pane.body.cur_col).row); try std.testing.expect(p.executeBuiltinLine(0, "TreeContext")); pane.scrollBy(7); _ = try p.render(frame.allocator()); @@ -581,8 +581,8 @@ const TreeContextTests = struct { } const x = glyph_x orelse return error.MissingContextUnicode; ClickCursorTests.click(p, .left, x + 1, y); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, 9), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 9), pane.body.cur_col); try std.testing.expectEqual(@as(i32, 8), pane.scroll()); const cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingContextCursor; try std.testing.expectEqual(x, cursor.x); @@ -617,8 +617,8 @@ const TreeContextTests = struct { }; const glyph = target orelse return error.MissingWrappedContextBody; ClickCursorTests.click(p, .left, glyph.x + 1, glyph.y); - try std.testing.expectEqual(@as(i32, 2), pane.cur_row); - try std.testing.expectEqual(@as(i32, @intCast(std.mem.indexOf(u8, body, "界").?)), pane.cur_col); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(std.mem.indexOf(u8, body, "界").?)), pane.body.cur_col); const cursor = (try p.render(frame.allocator())).cursor orelse return error.MissingContextBodyCursor; try std.testing.expectEqual(glyph.x, cursor.x); try std.testing.expectEqual(glyph.y, cursor.y); @@ -652,7 +652,7 @@ const TtySelectionTests = struct { try std.testing.expectEqualStrings("lph", p.yank orelse return error.MissingAutomaticTtyYank); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); p.update(.{ .key = .{ .cp = 'l' } }); - try std.testing.expectEqual(@as(i32, 4), pane.cur_col); + try std.testing.expectEqual(@as(i32, 4), pane.body.cur_col); p.update(.{ .key = .{ .cp = 'y' } }); try std.testing.expectEqualStrings("a", p.yank.?); try noChildInput(p); @@ -664,7 +664,7 @@ const TtySelectionTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); defer p.deinit(); const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; pane.tty_filter = false; p.presentation.enabled = false; while (p.nextEffect()) |_| {} @@ -680,7 +680,7 @@ const TtySelectionTests = struct { const y = p.bodyTop(rect); JumpSelectionTests.drag(p, x + @as(u16, if (prompt) 0 else 1), y, x + 3, y + @as(u16, if (prompt) 0 else 1)); const expected = if (prompt) "$ ab" else "lph\nrav"; - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); try std.testing.expectEqualStrings(expected, p.yank orelse return error.MissingAutomaticTtyYank); try noChildInput(p); @@ -689,7 +689,7 @@ const TtySelectionTests = struct { try noChildInput(p); try std.testing.expectEqualStrings(expected, p.fallback.clipboard.items); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(Pane.Mode.normal, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); const normal = try p.render(frame.allocator()); if (!prompt) { const selection_bg: pardes.Color = .{ .rgb = p.theme().sel_bg }; @@ -701,13 +701,13 @@ const TtySelectionTests = struct { try std.testing.expectEqualStrings(expected, p.yank.?); try noChildInput(p); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try noChildInput(p); _ = try p.render(frame.allocator()); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b]133;C\x07" ++ "\r\nnew output" ** 30 } }); _ = try p.render(frame.allocator()); - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); try std.testing.expectEqualStrings(expected, pardes.test_api.heldSelection(p, 0).?); try std.testing.expectEqualStrings(expected, p.yank.?); try noChildInput(p); @@ -723,7 +723,7 @@ const TtySelectionTests = struct { p.presentation.enabled = false; pardes.exec.spawnTty(p, 0); for (p.panes[0..2]) |slot| { - slot.?.mode = .tty; + slot.?.body.mode = .tty; slot.?.tty_filter = false; slot.?.greet = false; slot.?.vweight = 1; @@ -765,8 +765,8 @@ const TtySelectionTests = struct { }); defer p.gpa.free(expected); try std.testing.expectEqualStrings(expected, written.written()); - try std.testing.expectEqual(Pane.Mode.tty, p.panes[0].?.mode); - try std.testing.expectEqual(Pane.Mode.tty, p.panes[1].?.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[0].?.body.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, p.panes[1].?.body.mode); try std.testing.expectEqualStrings("lph\nrav", pardes.test_api.heldSelection(p, 0).?); } } @@ -799,7 +799,7 @@ const TtySelectionTests = struct { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 40, .rows = 12 }); defer p.deinit(); const pane = p.panes[0].?; - try std.testing.expectEqual(Pane.Mode.tty, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); if (bracketed) p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?2004h" } }); try std.testing.expectEqual(bracketed, panes.Terminal.bracketedPaste(pane)); p.yank = try p.gpa.dupe(u8, "one\ntwo"); // what a `y` anywhere left behind @@ -867,13 +867,13 @@ const ClickCursorTests = struct { const x = rect.x + config.GUTTER + panes.File.gutterWidth(pane); const y = if (bottom_tag) rect.y else rect.y + pardes.BOX_H; click(p, .left, x + 15, y + case.click_row); - try std.testing.expectEqual(case.row, pane.cur_row); - try std.testing.expectEqual(case.col, pane.cur_col); + try std.testing.expectEqual(case.row, pane.body.cur_row); + try std.testing.expectEqual(case.col, pane.body.cur_col); const clicked = (try p.render(frame.allocator())).cursor orelse return error.MissingClickCursor; try std.testing.expectEqual(x + @as(u16, @intCast(case.col)), clicked.x); try std.testing.expectEqual(y + @as(u16, @intCast(case.row)), clicked.y); if (!already_insert) p.update(.{ .key = .{ .cp = 'i' } }); - try std.testing.expectEqual(Pane.Mode.insert, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode); const insert = (try p.render(frame.allocator())).cursor orelse return error.MissingInsertCursor; try std.testing.expectEqual(clicked.x, insert.x); try std.testing.expectEqual(clicked.y, insert.y); @@ -906,12 +906,12 @@ const ClickCursorTests = struct { if (wrap) try std.testing.expect(glyph.y > p.rects[0].y + pardes.BOX_H); click(p, .left, glyph.x + 1, glyph.y); const byte = std.mem.indexOf(u8, source, "界").?; - try std.testing.expectEqual(@as(i32, @intCast(byte)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(byte)), pane.body.cur_col); const clicked = (try p.render(frame.allocator())).cursor orelse return error.MissingClickCursor; try std.testing.expectEqual(glyph.x, clicked.x); try std.testing.expectEqual(glyph.y, clicked.y); if (!already_insert) p.update(.{ .key = .{ .cp = 'i' } }); - try std.testing.expectEqual(Pane.Mode.insert, pane.mode); + try std.testing.expectEqual(panes.Text.Mode.insert, pane.body.mode); const insert = (try p.render(frame.allocator())).cursor orelse return error.MissingInsertCursor; try std.testing.expectEqual(clicked.x, insert.x); try std.testing.expectEqual(clicked.y, insert.y); @@ -1016,10 +1016,10 @@ const JumpSelectionTests = struct { try std.testing.expectEqual(@as(usize, 0), p.jcur); try std.testing.expect(pane.file.?.scroll > 20); try LookResetTests.cleared(pane); - try std.testing.expect(!pane.vsel.active); + try std.testing.expect(!pane.body.vsel.active); try std.testing.expect(pardes.test_api.heldSelection(p, 0) == null); - try std.testing.expectEqual(@as(i32, 59), pane.cur_row); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); + try std.testing.expectEqual(@as(i32, 59), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); // A selection created after Back must also disappear on Forward. // Back announces itself, so the body sits under a notice band now. @@ -1032,10 +1032,10 @@ const JumpSelectionTests = struct { try std.testing.expectEqual(@as(usize, 1), p.jcur); try std.testing.expectEqual(@as(usize, 0), pane.file.?.scroll); try LookResetTests.cleared(pane); - try std.testing.expect(!pane.vsel.active); + try std.testing.expect(!pane.body.vsel.active); try std.testing.expect(pardes.test_api.heldSelection(p, 0) == null); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); } test "jump selection clears copied rectangles in both history directions" { @@ -1073,9 +1073,9 @@ const JumpSelectionTests = struct { try std.testing.expectEqual(@as(usize, 0), p.jcur); try LookResetTests.cleared(source); try LookResetTests.cleared(destination); - try std.testing.expect(!source.vsel.active and !destination.vsel.active); - try std.testing.expectEqual(@as(i32, 2), destination.cur_row); - try std.testing.expectEqual(@as(i32, 1), destination.cur_col); + try std.testing.expect(!source.body.vsel.active and !destination.body.vsel.active); + try std.testing.expectEqual(@as(i32, 2), destination.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), destination.body.cur_col); try LookResetTests.seed(source, "new source held text"); try LookResetTests.seed(destination, "new destination held text"); @@ -1084,9 +1084,9 @@ const JumpSelectionTests = struct { try std.testing.expectEqual(@as(usize, 1), p.jcur); try LookResetTests.cleared(source); try LookResetTests.cleared(destination); - try std.testing.expect(!source.vsel.active and !destination.vsel.active); - try std.testing.expectEqual(@as(i32, 1), source.cur_row); - try std.testing.expectEqual(@as(i32, 0), source.cur_col); + try std.testing.expect(!source.body.vsel.active and !destination.body.vsel.active); + try std.testing.expectEqual(@as(i32, 1), source.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), source.body.cur_col); } }; @@ -1395,7 +1395,7 @@ test "terminal overlay recoloring never materializes unrelated history" { const pane = p.panes[0].?; pane.tty_filter = false; for (0..1000) |_| p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[32mcaf\xc3\xa9 \xe7\x95\x8c\x1b[0m\r\n" } }); - pane.mode = .normal; + pane.body.mode = .normal; pane.ovl = .{ .row = 0, .rows = 1, .text = try p.gpa.dupe(u8, "edited history") }; var frame = std.heap.ArenaAllocator.init(testing.allocator); defer frame.deinit(); @@ -1422,7 +1422,7 @@ test "terminal output keeps following new rows after scrollback eviction" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; pane.tty_filter = false; const pages = &pane.terminal.?.vt.screens.active.pages; pages.explicit_max_size = 64 * 1024; @@ -1431,7 +1431,7 @@ test "terminal output keeps following new rows after scrollback eviction" { var evictions: usize = 0; for (0..3) |phase| { if (phase > 0) { - pane.mode = .normal; + pane.body.mode = .normal; panes.Terminal.scrollGrid(pane, -30); try std.testing.expect(pages.scrollbar().offset < pages.scrollbar().total - pane.rows); panes.Terminal.enterTty(p, 0); @@ -1483,7 +1483,7 @@ test "terminal edit stays on its surviving row when old scrollback is evicted" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; const pages = &pane.terminal.?.vt.screens.active.pages; pages.explicit_max_size = 64 * 1024; var next: usize = 0; @@ -1497,12 +1497,12 @@ test "terminal edit stays on its surviving row when old scrollback is evicted" { try std.testing.expect(next < 3000); var last_buf: [16]u8 = undefined; const last = try std.fmt.bufPrint(&last_buf, "row-{d:0>5}", .{next - 1}); - pane.mode = .normal; + pane.body.mode = .normal; const original_row: i32 = @intCast(pages.scrollbar().total - 2); pane.ovl = .{ .row = original_row, .rows = 1, .text = try gpa.dupe(u8, "my unsent edit") }; - pane.cur_row = original_row; - pane.cur_col = 3; - pane.cur_pinned = true; + pane.body.cur_row = original_row; + pane.body.cur_col = 3; + pane.body.cur_pinned = true; var removed: usize = 0; for (next..next + 3000) |n| { var buf: [32]u8 = undefined; @@ -1520,7 +1520,7 @@ test "terminal edit stays on its surviving row when old scrollback is evicted" { const row = original_row - @as(i32, @intCast(removed)); try std.testing.expectEqualStrings(last, modal.lineSlice(full, @intCast(row))); try std.testing.expectEqual(row, pane.ovl.?.row); - try std.testing.expectEqual(row, pane.cur_row); + try std.testing.expectEqual(row, pane.body.cur_row); try std.testing.expectEqualStrings("my unsent edit", pane.ovl.?.text); } @@ -1533,7 +1533,7 @@ test "terminal edits and undo survive partial history clearing and whole-screen defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; if (action == .reset_alternate) p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[?1049h" } }); p.update(.{ .output = .{ .pane = 0, .bytes = "old row\r\n" ** 40 } }); @@ -1541,15 +1541,15 @@ test "terminal edits and undo survive partial history clearing and whole-screen pages.explicit_max_size = 64 * 1024; const row: i32 = @as(i32, @intCast(pages.total_rows - pane.rows)) + @as(i32, if (action == .partial_clear_history) -1 else 2); - pane.mode = .normal; + pane.body.mode = .normal; pane.ovl = .{ .row = row, .rows = if (action == .partial_clear_history) 3 else 2, .text = try gpa.dupe(u8, "edit one\nedit two") }; - pane.cur_row = row + 1; - pane.cur_col = 4; - pane.cur_pinned = true; - pane.vsel = .{ .active = true, .row = row, .col = 1 }; + pane.body.cur_row = row + 1; + pane.body.cur_col = 4; + pane.body.cur_pinned = true; + pane.body.vsel = .{ .active = true, .row = row, .col = 1 }; panes.Terminal.pushUndo(p, pane); - pane.ed_redo[0] = panes.Terminal.snap(p, pane) orelse return error.SnapshotFailed; - pane.ed_redo_len = 1; + pane.body.ed_redo[0] = panes.Terminal.snap(p, pane) orelse return error.SnapshotFailed; + pane.body.ed_redo_len = 1; if (action == .hidden_undo) { gpa.free(pane.ovl.?.text); pane.ovl = null; @@ -1566,10 +1566,10 @@ test "terminal edits and undo survive partial history clearing and whole-screen try std.testing.expectEqual(expected, edit.row); try std.testing.expectEqual(@as(i32, if (action == .clear_history or action == .partial_clear_history) 2 else 1), edit.rows); try std.testing.expectEqualStrings("edit one\nedit two", edit.text); - try std.testing.expectEqual(expected + 1, pane.cur_row); - try std.testing.expectEqual(expected, pane.vsel.row); + try std.testing.expectEqual(expected + 1, pane.body.cur_row); + try std.testing.expectEqual(expected, pane.body.vsel.row); } - for ([_]panes.Terminal.Snapshot{ pane.ed_undo[0], pane.ed_redo[0] }) |snapshot| { + for ([_]panes.Terminal.Snapshot{ pane.body.ed_undo[0], pane.body.ed_redo[0] }) |snapshot| { try std.testing.expectEqual(expected, snapshot.ovl.?.row); try std.testing.expectEqual(expected + 1, snapshot.cur_row); try std.testing.expectEqualStrings("edit one\nedit two", snapshot.ovl.?.text); @@ -1588,11 +1588,11 @@ test "terminal lower scroll regions do not move edits above them" { defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "top\r\nsecond\r\nthird" } }); pane.ovl = .{ .row = 1, .rows = 1, .text = try gpa.dupe(u8, "unsent") }; - pane.cur_row = 1; - pane.cur_pinned = true; + pane.body.cur_row = 1; + pane.body.cur_pinned = true; panes.Terminal.pushUndo(p, pane); const total = panes.Terminal.scrollbar(pane).total; var setup: [64]u8 = undefined; @@ -1601,8 +1601,8 @@ test "terminal lower scroll regions do not move edits above them" { p.update(.{ .output = .{ .pane = 0, .bytes = "tail\r\n" ** 50 } }); try std.testing.expectEqual(total, panes.Terminal.scrollbar(pane).total); try std.testing.expectEqual(@as(i32, 1), pane.ovl.?.row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.ed_undo[0].ovl.?.row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.ed_undo[0].ovl.?.row); const full = try panes.Terminal.screenTextAlloc(pane, gpa); defer gpa.free(full); try std.testing.expect(std.mem.startsWith(u8, full, "top\nsecond\nthird\n")); @@ -1615,7 +1615,7 @@ test "terminal eviction and lower-region scrolling in one read preserve a surviv defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; - pane.mode = .tty; + pane.body.mode = .tty; const pages = &pane.terminal.?.vt.screens.active.pages; pages.explicit_max_size = 64 * 1024; var next: usize = 0; @@ -1630,10 +1630,10 @@ test "terminal eviction and lower-region scrolling in one read preserve a surviv try std.testing.expect(next < 3000); const removed = pages.pages.first.?.rows(); const old_row: i32 = @intCast(removed + 2); - pane.mode = .normal; + pane.body.mode = .normal; pane.ovl = .{ .row = old_row, .rows = 1, .text = try gpa.dupe(u8, "kept edit") }; - pane.cur_row = old_row; - pane.cur_pinned = true; + pane.body.cur_row = old_row; + pane.body.cur_pinned = true; panes.Terminal.pushUndo(p, pane); const old_total = pages.total_rows; var buf: [128]u8 = undefined; @@ -1641,8 +1641,8 @@ test "terminal eviction and lower-region scrolling in one read preserve a surviv p.update(.{ .output = .{ .pane = 0, .bytes = bytes } }); try std.testing.expectEqual(old_total + 1 - removed, pages.total_rows); try std.testing.expectEqual(@as(i32, 2), pane.ovl.?.row); - try std.testing.expectEqual(@as(i32, 2), pane.cur_row); - try std.testing.expectEqual(@as(i32, 2), pane.ed_undo[0].ovl.?.row); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 2), pane.body.ed_undo[0].ovl.?.row); try std.testing.expectEqualStrings("kept edit", pane.ovl.?.text); } @@ -1667,9 +1667,9 @@ test "tag prompt entry and cancellation preserve exact text and cursor state" { pane.tag_init = true; pane.tag_sel = true; pane.tag_col = 1; - pane.cur_row = 1; - pane.cur_col = 2; - pane.cur_pinned = true; + pane.body.cur_row = 1; + pane.body.cur_col = 2; + pane.body.cur_pinned = true; if (case.key) |key| { p.update(.{ .key = key }); } else if (case.save) { @@ -1679,18 +1679,18 @@ test "tag prompt entry and cancellation preserve exact text and cursor state" { } try std.testing.expect(pane.tag_edit); try std.testing.expect(!pane.tag_sel); - try std.testing.expectEqual(.insert, pane.mode); - try std.testing.expectEqualDeep(modal.Normal.State{}, pane.normal); + try std.testing.expectEqual(.insert, pane.body.mode); + try std.testing.expectEqualDeep(modal.Normal.State{}, pane.body.normal); try std.testing.expectEqualStrings(tail, pane.tag_tail[0..tail.len]); try std.testing.expectEqualStrings(case.marker, pane.tag_tail[tail.len..pane.tag_tail_len]); try std.testing.expectEqual(pane.file.?.path.len + pane.tag_tail_len, pane.tag_col); p.update(.{ .key = .{ .cp = Key.escape } }); try std.testing.expect(pane.prompt == .none); try std.testing.expect(!pane.tag_edit); - try std.testing.expectEqual(.normal, pane.mode); + try std.testing.expectEqual(.normal, pane.body.mode); try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); - try std.testing.expectEqual(@as(i32, 2), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_col); try std.testing.expectEqualStrings("one\ntwo\n", pane.file.?.content); } } @@ -1713,7 +1713,7 @@ test "tag prompt capacity refusal leaves the existing tail and input state untou try std.testing.expect(pane.prompt == .none); try std.testing.expect(!pane.tag_edit); try std.testing.expect(pane.tag_sel); - try std.testing.expectEqual(.normal, pane.mode); + try std.testing.expectEqual(.normal, pane.body.mode); try std.testing.expectEqual(@as(u16, 17), pane.tag_col); try std.testing.expectEqual(pane.tag_tail.len - 1, pane.tag_tail_len); for (pane.tag_tail) |byte| try std.testing.expectEqual(@as(u8, 'x'), byte); @@ -1739,9 +1739,9 @@ test "tag prompt owns the pipe behavior through cancellation and submission" { const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true }); defer p.deinit(); const pane = try p.setTestFile("one\ntwo\n"); - pane.cur_row = 0; - pane.cur_col = 2; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.cur_row = 0; + pane.body.cur_col = 2; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; for (std.enums.values(modal.Normal.PipeBehavior)) |how| { p.startPrompt(pane, .{ .pipe = .append }); p.update(.{ .key = .{ .cp = Key.escape } }); @@ -1767,21 +1767,21 @@ test "tag prompt cancellation restores the selection before regex previews" { .{ .key = 'S', .pattern = " ", .secondary = 2 }, }; for (cases) |case| { - pane.cur_row = 0; - pane.cur_col = 10; - pane.cur_pinned = true; - pane.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; - pane.nsel = 0; + pane.body.cur_row = 0; + pane.body.cur_col = 10; + pane.body.cur_pinned = true; + pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true }; + pane.body.nsel = 0; p.update(.{ .key = .{ .cp = case.key } }); try std.testing.expect(pane.prompt == .search); p.update(.{ .key = .{ .cp = case.pattern[0], .text = case.pattern } }); - try std.testing.expectEqual(case.secondary, pane.nsel); + try std.testing.expectEqual(case.secondary, pane.body.nsel); p.update(.{ .key = .{ .cp = Key.escape } }); - try std.testing.expectEqual(@as(u8, 0), pane.nsel); + try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); try std.testing.expectEqual(@as(u8, 0), pane.nsel_snap); - try std.testing.expectEqual(@as(i32, 10), pane.cur_col); - try std.testing.expectEqual(@as(i32, 0), pane.vsel.col); - try std.testing.expect(pane.vsel.active and pane.vsel.explicit); + try std.testing.expectEqual(@as(i32, 10), pane.body.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.vsel.col); + try std.testing.expect(pane.body.vsel.active and pane.body.vsel.explicit); try std.testing.expectEqualStrings("one two one", pane.file.?.content); } } @@ -1802,18 +1802,18 @@ test "tag prompt prefix allocation failure preserves an already seeded prompt" { pane.tag_sel = true; pane.tag_col = 14; pane.tag_anchor = 7; - pane.normal = .{ .count = 9, .prefix = .goto }; + pane.body.normal = .{ .count = 9, .prefix = .goto }; const previous_prompt = pane.prompt; - const previous_normal = pane.normal; + const previous_normal = pane.body.normal; _ = p.scratch.reset(.free_all); failing.fail_index = failing.alloc_index; p.startPrompt(pane, request); try std.testing.expect(failing.has_induced_failure); try std.testing.expectEqualDeep(previous_prompt, pane.prompt); - try std.testing.expectEqualDeep(previous_normal, pane.normal); + try std.testing.expectEqualDeep(previous_normal, pane.body.normal); try std.testing.expectEqualStrings(tail, pane.tag_tail[0..pane.tag_tail_len]); try std.testing.expect(pane.tag_edit and pane.tag_sel); - try std.testing.expectEqual(.normal, pane.mode); + try std.testing.expectEqual(.normal, pane.body.mode); try std.testing.expectEqual(@as(u16, 14), pane.tag_col); try std.testing.expectEqual(@as(u16, 7), pane.tag_anchor); } @@ -1867,7 +1867,7 @@ test "tag prompt successful seeded entry needs one scratch allocation" { const before = failing.alloc_index; p.startPrompt(pane, request); try std.testing.expectEqual(before + 1, failing.alloc_index); - try std.testing.expect(pane.tag_edit and pane.mode == .insert); + try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); } } @@ -1892,20 +1892,20 @@ test "tag prompt failure may initialize only the default tail" { pane.tag_tail_len = 0; pane.tag_col = 17; pane.tag_sel = true; - pane.normal = .{ .count = 9, .prefix = .goto }; - const previous_normal = pane.normal; + pane.body.normal = .{ .count = 9, .prefix = .goto }; + const previous_normal = pane.body.normal; _ = p.scratch.reset(.free_all); failing.fail_index = failing.alloc_index + failure; p.startPrompt(pane, request); if (!failing.has_induced_failure) { - try std.testing.expect(pane.tag_edit and pane.mode == .insert); + try std.testing.expect(pane.tag_edit and pane.body.mode == .insert); succeeded = true; break; } try std.testing.expect(pane.prompt == .none); try std.testing.expect(!pane.tag_edit and pane.tag_sel); - try std.testing.expectEqual(.normal, pane.mode); - try std.testing.expectEqualDeep(previous_normal, pane.normal); + try std.testing.expectEqual(.normal, pane.body.mode); + try std.testing.expectEqualDeep(previous_normal, pane.body.normal); try std.testing.expectEqual(@as(u16, 17), pane.tag_col); if (pane.tag_init) { saw_seeded_failure = true; @@ -2390,10 +2390,10 @@ const OutputTests = struct { try std.testing.expectEqual(matches[1], pane.search_row.?); try std.testing.expect(pardes.test_api.searchStep(p, 0, -1)); try std.testing.expectEqual(matches[0], pane.search_row.?); - try std.testing.expectEqual(@as(i32, 3), pane.cur_row); + try std.testing.expectEqual(@as(i32, 3), pane.body.cur_row); try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); try std.testing.expectEqual(matches[1], pane.search_row.?); - try std.testing.expectEqual(@as(i32, 5), pane.cur_row); + try std.testing.expectEqual(@as(i32, 5), pane.body.cur_row); try std.testing.expect(pardes.test_api.searchStep(p, 0, 1)); try std.testing.expectEqual(matches[1], pane.search_row.?); @@ -2571,9 +2571,9 @@ const OutputTests = struct { const id = p.active; const help = p.panes[id].?; for ("gevkk") |key| p.update(.{ .key = .{ .cp = key } }); - try std.testing.expect(help.vsel.active); - try std.testing.expect(help.vsel.row > 5); - try std.testing.expect(help.cur_row != help.vsel.row); + try std.testing.expect(help.body.vsel.active); + try std.testing.expect(help.body.vsel.row > 5); + try std.testing.expect(help.body.cur_row != help.body.vsel.row); for (" h?") |key| p.update(.{ .key = .{ .cp = key } }); try std.testing.expectEqual(id, p.active); const content = help.file.?.content; @@ -2607,35 +2607,35 @@ const OutputTests = struct { const result = p.panes[id].?; p.active = id; for ("Cv3") |key| p.update(.{ .key = .{ .cp = key } }); - try std.testing.expect(result.nsel > 0); - try std.testing.expect(result.select); - try std.testing.expect(result.vsel.active); - try std.testing.expect(result.normal.count > 0); + try std.testing.expect(result.body.nsel > 0); + try std.testing.expect(result.body.select); + try std.testing.expect(result.body.vsel.active); + try std.testing.expect(result.body.normal.count > 0); result.file.?.scroll = 1; result.hscroll = 2; - const selected = result.vsel; - const selections = result.nsel; - const cursor_row = result.cur_row; - const cursor_col = result.cur_col; + const selected = result.body.vsel; + const selections = result.body.nsel; + const cursor_row = result.body.cur_row; + const cursor_col = result.body.cur_col; try fillResults(p, 0, "/", .{ .query = .hover }, "", try p.gpa.dupe(u8, content), null); - try std.testing.expectEqual(selected, result.vsel); - try std.testing.expectEqual(selections, result.nsel); - try std.testing.expectEqual(cursor_row, result.cur_row); - try std.testing.expectEqual(cursor_col, result.cur_col); + try std.testing.expectEqual(selected, result.body.vsel); + try std.testing.expectEqual(selections, result.body.nsel); + try std.testing.expectEqual(cursor_row, result.body.cur_row); + try std.testing.expectEqual(cursor_col, result.body.cur_col); try std.testing.expectEqual(@as(usize, 1), result.file.?.scroll); try std.testing.expectEqual(@as(i32, 2), result.hscroll); - try std.testing.expect(result.normal.count > 0); + try std.testing.expect(result.body.normal.count > 0); try fillResults(p, 0, "/", .{ .query = .hover }, "", try p.gpa.dupe(u8, "short\n"), null); try std.testing.expectEqualStrings("short\n", result.file.?.content); - try std.testing.expect(!result.vsel.active and !result.msel.active and !result.select); - try std.testing.expectEqual(@as(u8, 0), result.nsel); - try std.testing.expectEqual(@as(i32, 0), result.cur_row); - try std.testing.expectEqual(@as(i32, 0), result.cur_col); + try std.testing.expect(!result.body.vsel.active and !result.body.msel.active and !result.body.select); + try std.testing.expectEqual(@as(u8, 0), result.body.nsel); + try std.testing.expectEqual(@as(i32, 0), result.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), result.body.cur_col); try std.testing.expectEqual(@as(usize, 0), result.file.?.scroll); try std.testing.expectEqual(@as(i32, 0), result.hscroll); - try std.testing.expectEqual(modal.Normal.State{}, result.normal); + try std.testing.expectEqual(modal.Normal.State{}, result.body.normal); } test "generated result replacement cancels an in-progress body selection" { @@ -2665,7 +2665,7 @@ const OutputTests = struct { try std.testing.expect(p.drag == .none); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = x + 2, .row = y + 1 } }); for (result.sel) |selection| try std.testing.expectEqual(.none, selection.state); - try std.testing.expect(!result.vsel.active and !result.msel.active); + try std.testing.expect(!result.body.vsel.active and !result.body.msel.active); } test "dump origins accept only builtins that actually own output panes" { @@ -2993,8 +2993,8 @@ const TerminalTests = struct { gpa.free(sn.style); }; - for ([_]pardes.Pane.Mode{ .tty, .normal }, 0..) |mode, i| { - pane.mode = mode; + for ([_]panes.Text.Mode{ .tty, .normal }, 0..) |mode, i| { + pane.body.mode = mode; p.shell_rows.stale = true; _ = frame.reset(.retain_capacity); const s = try p.render(frame.allocator()); @@ -3204,7 +3204,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31mAAA\x1b[0m\r\n\x1b[32mBBB\x1b[0m\r\n\x1b[34mCCC\x1b[0m" } }); @@ -3254,7 +3254,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; // A ONE-cell prompt carrying a combining mark — an NFD `e` — then `ABC` // typed at it. The cell walk that finds the prompt's end must step ONE @@ -3290,7 +3290,7 @@ const TerminalTests = struct { // Raw palette, so a leaked background reads back as `.index` — the theme // filter would repaint every blank cell and hide the evidence. pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; // Sixty rows, each a distinct background, so a leaked colour names the row // it leaked from. @@ -3346,7 +3346,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31mAAA\x1b[0m\r\n\x1b[32mBBB\x1b[0m\r\n\x1b[34mCCC\x1b[0m" } }); @@ -3383,7 +3383,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31mAAA\x1b[0m\r\n\x1b[32mBBB\x1b[0m\r\n\x1b[34mCCC\x1b[0m" } }); @@ -3459,7 +3459,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = filter; - pane.mode = .normal; + pane.body.mode = .normal; var buf: [256]u8 = undefined; const bytes = try std.fmt.bufPrint( @@ -3539,7 +3539,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .tty; + pane.body.mode = .tty; for (0..60) |i| { var buf: [40]u8 = undefined; @@ -3598,13 +3598,13 @@ const TerminalTests = struct { // ABSOLUTE assertions: both modes were wrong identically here, so a // tty/normal differential says nothing. for ([_]bool{ false, true }) |filter| { - for ([_]pardes.Pane.Mode{ .tty, .normal }) |mode| { + for ([_]panes.Text.Mode{ .tty, .normal }) |mode| { const p = try Pardes.init(testing.allocator, .{ .tty_only = true, .cols = 12, .rows = 8 }); defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = filter; - pane.mode = mode; + pane.body.mode = mode; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[32m\u{754C}\x1b[41m\x1b[K\x1b[7G\x1b[44m\x1b[K\r\n" } }); var frame = std.heap.ArenaAllocator.init(testing.allocator); @@ -3634,13 +3634,13 @@ const TerminalTests = struct { // ghostty leaves a `spacer_head` in that last column, carrying the row's // background, and wraps the glyph to the next row. A head OWNS its column, // so skipping it the way a tail is skipped left the row's final column bare. - for ([_]pardes.Pane.Mode{ .tty, .normal }) |mode| { + for ([_]panes.Text.Mode{ .tty, .normal }) |mode| { const p = try Pardes.init(testing.allocator, .{ .startup_config = "Theme helix\n", .tty_only = true, .cols = 16, .rows = 8 }); defer p.deinit(); while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = mode; + pane.body.mode = mode; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[41mzzzzzzzzzzzzz\u{754C}\x1b[0m\r\n" } }); var frame = std.heap.ArenaAllocator.init(testing.allocator); @@ -3667,7 +3667,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .tty; + pane.body.mode = .tty; for (0..40) |i| { var buf: [64]u8 = undefined; const bytes = std.fmt.bufPrint(&buf, "\x1b[38;5;{d}mline-{d:0>2}\x1b[0m\r\n", .{ 20 + i, i }) catch unreachable; @@ -3717,7 +3717,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = filter; - pane.mode = .normal; + pane.body.mode = .normal; var frame = std.heap.ArenaAllocator.init(testing.allocator); defer frame.deinit(); @@ -3749,7 +3749,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; for (0..6) |i| { var buf: [40]u8 = undefined; @@ -3791,7 +3791,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31m000\x1b[0m\r\n\x1b[32m111\x1b[0m\r\n\r\n\x1b[34m333\x1b[0m\r\n\x1b[35m444\x1b[0m" } }); @@ -3825,7 +3825,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .tty; + pane.body.mode = .tty; p.update(.{ .output = .{ .pane = 0, .bytes = "\x1b[31mAAA\x1b[0m\r\n\x1b[32mBBB\x1b[0m\r\n\x1b[34mCCC\x1b[0m" } }); @@ -3862,7 +3862,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; for (0..6) |i| { var buf: [64]u8 = undefined; const bytes = std.fmt.bufPrint(&buf, "\x1b[38;5;{d}mrow-{d}-abcdefgh\x1b[0m\r\n", .{ 30 + i, i }) catch unreachable; @@ -3912,7 +3912,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; for (0..26) |i| { var buf: [64]u8 = undefined; const bytes = std.fmt.bufPrint(&buf, "\x1b[38;5;{d}mrow-{d:0>2}-xyzzy\x1b[0m\r\n", .{ 20 + i, i }) catch unreachable; @@ -3971,7 +3971,7 @@ const TerminalTests = struct { // The raw palette, so a row's text names its exact colour instead of one // this test would have to re-derive from the theme. pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; for (0..26) |i| { var buf: [64]u8 = undefined; const bytes = std.fmt.bufPrint(&buf, "\x1b[38;5;{d}mrow-{d:0>2}-xyzzy\x1b[0m\r\n", .{ 20 + i, i }) catch unreachable; @@ -4059,7 +4059,7 @@ const TerminalTests = struct { while (p.nextEffect()) |_| {} const pane = p.panes[0].?; pane.tty_filter = false; - pane.mode = .normal; + pane.body.mode = .normal; for (0..26) |i| { var buf: [64]u8 = undefined; const bytes = std.fmt.bufPrint(&buf, "\x1b[38;5;{d}mrow-{d:0>2}-xyzzy\x1b[0m\r\n", .{ 20 + i, i }) catch unreachable; @@ -4130,9 +4130,9 @@ test "an empty counted paste leaves no count for the next motion" { const pane = try p.setTestFile("abcdefgh\n"); if (empty_register) p.yank = try p.gpa.dupe(u8, ""); for ("3p") |cp| p.update(.{ .key = .{ .cp = cp } }); - try std.testing.expectEqual(modal.Normal.State{}, pane.normal); + try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); p.update(.{ .key = .{ .cp = 'l' } }); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); try std.testing.expectEqualStrings("abcdefgh\n", pane.file.?.content); } } @@ -4156,7 +4156,7 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu defer frame.deinit(); _ = try p.render(frame.allocator()); for (keys) |cp| p.update(.{ .key = .{ .cp = cp } }); - try std.testing.expect(!std.meta.eql(modal.Normal.State{}, pane.normal)); + try std.testing.expect(!std.meta.eql(modal.Normal.State{}, pane.body.normal)); switch (gesture) { .body, .tag => { const rect = p.rects[0]; @@ -4185,10 +4185,10 @@ test "cancelled normal gestures leave no count or subprefix after mouse and focu try std.testing.expectEqual(@as(usize, 0), p.active); }, } - try std.testing.expectEqual(modal.Normal.State{}, pane.normal); - const col = pane.cur_col; + try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); + const col = pane.body.cur_col; p.update(.{ .key = .{ .cp = 'l' } }); - try std.testing.expectEqual(col + 1, pane.cur_col); + try std.testing.expectEqual(col + 1, pane.body.cur_col); try std.testing.expectEqualStrings("abcdefghij\n", pane.file.?.content); } } @@ -4199,16 +4199,16 @@ test "cancelled normal gestures leave no count or subprefix through raw tty mode const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 12 }); defer p.deinit(); const pane = p.panes[0].?; - pane.mode = .normal; + pane.body.mode = .normal; p.update(.{ .output = .{ .pane = 0, .bytes = "abcdefghij\r\n" } }); for (keys) |cp| p.update(.{ .key = .{ .cp = cp } }); - try std.testing.expect(!std.meta.eql(modal.Normal.State{}, pane.normal)); + try std.testing.expect(!std.meta.eql(modal.Normal.State{}, pane.body.normal)); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(pardes.Pane.Mode.tty, pane.mode); - try std.testing.expectEqual(modal.Normal.State{}, pane.normal); + try std.testing.expectEqual(panes.Text.Mode.tty, pane.body.mode); + try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } }); - try std.testing.expectEqual(pardes.Pane.Mode.normal, pane.mode); - try std.testing.expectEqual(modal.Normal.State{}, pane.normal); + try std.testing.expectEqual(panes.Text.Mode.normal, pane.body.mode); + try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); } } @@ -4333,8 +4333,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { gpa.free(file.path); file.path = try gpa.dupe(u8, "/highlight.zig"); file.scroll = line - 1; - pane.cur_row = @intCast(line - 1); - pane.cur_col = @intCast(std.mem.indexOf(u8, sample, "fn").?); + pane.body.cur_row = @intCast(line - 1); + pane.body.cur_col = @intCast(std.mem.indexOf(u8, sample, "fn").?); pane.hscroll = view.horizontal; p.settings.colors = true; p.settings.wrap = view.wrap; @@ -4373,8 +4373,8 @@ test "Zig keywords keep every rendered byte colored across line-number widths" { const clicked = if_position.?; p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = clicked.x, .row = clicked.y } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = clicked.x, .row = clicked.y } }); - try std.testing.expectEqual(@as(i32, @intCast(line - 1)), pane.cur_row); - try std.testing.expectEqual(@as(i32, @intCast(std.mem.indexOf(u8, sample, "if").?)), pane.cur_col); + try std.testing.expectEqual(@as(i32, @intCast(line - 1)), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, @intCast(std.mem.indexOf(u8, sample, "if").?)), pane.body.cur_col); } } } @@ -4389,7 +4389,7 @@ test "terminal overlays preserve trailing blank styles without coloring inserted const pane = p.panes[0].?; pane.tty_filter = false; p.update(.{ .output = .{ .pane = 0, .bytes = "A\r\n\x1b[41m\x1b[2K\r\n\x1b[42m\x1b[2K\r\n\x1b[44m\x1b[2K\x1b[0m" } }); - pane.mode = .normal; + pane.body.mode = .normal; pane.ovl = .{ .row = 0, .rows = 3, .text = try gpa.dupe(u8, "A\n\n") }; var frame = std.heap.ArenaAllocator.init(gpa); defer frame.deinit(); @@ -4421,16 +4421,16 @@ test "terminal overlays preserve trailing blank styles without coloring inserted const LookResetTests = struct { fn seed(pane: *Pane, operand: []const u8) !void { - pane.cur_row = 0; - pane.cur_col = 3; - pane.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 0 }; - pane.msel = .{ .active = true, .r0 = 0, .r1 = 1 }; - pane.sels[0] = .{ .row = 1, .col = 2, .arow = 0, .acol = 1 }; - pane.nsel = 1; - pane.select = true; - pane.normal.count = 5; - pane.append_at = .{ .row = 0, .col = 3 }; - pane.sticky_col = 7; + pane.body.cur_row = 0; + pane.body.cur_col = 3; + pane.body.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 0 }; + pane.body.msel = .{ .active = true, .r0 = 0, .r1 = 1 }; + pane.body.sels[0] = .{ .row = 1, .col = 2, .arow = 0, .acol = 1 }; + pane.body.nsel = 1; + pane.body.select = true; + pane.body.normal.count = 5; + pane.body.append_at = .{ .row = 0, .col = 3 }; + pane.body.sticky_col = 7; for (0..pane.sel.len) |slot| { pane.clearPointerSelection(slot); pane.sel[slot] = .{ .state = .done, .source_id = slot + 1, .r0 = 1, .r1 = 1, .c0 = 0, .c1 = 3 }; @@ -4445,21 +4445,21 @@ const LookResetTests = struct { } fn cleared(pane: *const Pane) !void { - try std.testing.expect(!pane.msel.active); - try std.testing.expectEqual(@as(u8, 0), pane.nsel); - try std.testing.expect(!pane.select); - try std.testing.expectEqual(modal.Normal.State{}, pane.normal); - try std.testing.expect(pane.append_at == null); - try std.testing.expectEqual(@as(i32, -1), pane.sticky_col); + try std.testing.expect(!pane.body.msel.active); + try std.testing.expectEqual(@as(u8, 0), pane.body.nsel); + try std.testing.expect(!pane.body.select); + try std.testing.expectEqual(modal.Normal.State{}, pane.body.normal); + try std.testing.expect(pane.body.append_at == null); + try std.testing.expectEqual(@as(i32, -1), pane.body.sticky_col); for (pane.sel) |selection| try std.testing.expectEqual(.none, selection.state); for (pane.pointer_selections) |selection| try std.testing.expect(selection == null); } fn selected(pane: *const Pane) []const u8 { - if (!pane.vsel.active or pane.cur_row != pane.vsel.row) return ""; - const line = modal.lineSlice(pane.file.?.content, @intCast(pane.cur_row)); - const lo: usize = @intCast(@min(pane.cur_col, pane.vsel.col)); - const hi: usize = @intCast(@max(pane.cur_col, pane.vsel.col)); + if (!pane.body.vsel.active or pane.body.cur_row != pane.body.vsel.row) return ""; + const line = modal.lineSlice(pane.file.?.content, @intCast(pane.body.cur_row)); + const lo: usize = @intCast(@min(pane.body.cur_col, pane.body.vsel.col)); + const hi: usize = @intCast(@max(pane.body.cur_col, pane.body.vsel.col)); return line[lo..modal.nextGrapheme(line, hi)]; } @@ -4472,14 +4472,14 @@ const LookResetTests = struct { pardes.look.lookAt(p, 0, pane.pointer_selections[0].?.raw_text.?); try cleared(pane); try std.testing.expectEqual(@as(usize, 0), p.active); - try std.testing.expectEqual(@as(i32, 2), pane.cur_row); - try std.testing.expectEqual(@as(i32, 1), pane.cur_col); - try std.testing.expect(!pane.vsel.active); + try std.testing.expectEqual(@as(i32, 2), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_col); + try std.testing.expect(!pane.body.vsel.active); try seed(pane, "/test.txt:2:2-4"); pardes.look.lookAt(p, 0, pane.pointer_selections[0].?.raw_text.?); try cleared(pane); - try std.testing.expectEqual(@as(i32, 1), pane.cur_row); + try std.testing.expectEqual(@as(i32, 1), pane.body.cur_row); try std.testing.expectEqualStrings("rav", selected(pane)); p.gpa.free(pane.file.?.path); @@ -4487,7 +4487,7 @@ const LookResetTests = struct { try seed(pane, "needle"); pardes.look.lookAt(p, 0, pane.pointer_selections[0].?.raw_text.?); try cleared(pane); - try std.testing.expectEqual(@as(i32, 3), pane.cur_row); + try std.testing.expectEqual(@as(i32, 3), pane.body.cur_row); try std.testing.expectEqualStrings("needle", selected(pane)); } @@ -4512,14 +4512,14 @@ const LookResetTests = struct { try std.testing.expectEqual(id, p.active); try cleared(source); try cleared(destination); - try std.testing.expect(!source.vsel.active); + try std.testing.expect(!source.body.vsel.active); if (range) { - try std.testing.expectEqual(@as(i32, 1), destination.cur_row); + try std.testing.expectEqual(@as(i32, 1), destination.body.cur_row); try std.testing.expectEqualStrings("rav", selected(destination)); } else { - try std.testing.expectEqual(@as(i32, 2), destination.cur_row); - try std.testing.expectEqual(@as(i32, 1), destination.cur_col); - try std.testing.expect(!destination.vsel.active); + try std.testing.expectEqual(@as(i32, 2), destination.body.cur_row); + try std.testing.expectEqual(@as(i32, 1), destination.body.cur_col); + try std.testing.expect(!destination.body.vsel.active); } } } diff --git a/test/pdf.zig b/test/pdf.zig index 8e87dfb1..52fb62aa 100644 --- a/test/pdf.zig +++ b/test/pdf.zig @@ -13,7 +13,7 @@ const Pardes = pardes.Pardes; const Builtin = builtins.registry.Builtin(); const Key = pardes.Key; const Event = pardes.Event; -const Mode = pardes.Pane.Mode; +const Mode = panes.Text.Mode; const Surface = pardes.Surface; const ImagePlace = pardes.ImagePlace; const PdfFitMode = panes.Pdf.FitMode; @@ -457,8 +457,8 @@ test "watched PDF reload replaces MuPDF state and preserves the reading view" { anchor_fraction * @as(f64, @floatFromInt(state.page_heights[2])); state.scroll_to_page_pending = false; state.layout_anchor_pending = false; - pane.cur_row = 9; - pane.cur_col = 8; + pane.body.cur_row = 9; + pane.body.cur_col = 8; try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "live.pdf", .data = replacement }); try std.testing.expect(p.reloadWatchedFile(0, &.{})); @@ -482,8 +482,8 @@ test "watched PDF reload replaces MuPDF state and preserves the reading view" { try std.testing.expect(state.layout_anchor_pending); try std.testing.expectEqual(@as(usize, 2), state.layout_anchor_page); try std.testing.expectApproxEqAbs(anchor_fraction, state.layout_anchor_fraction, 0.0001); - try std.testing.expectEqual(@as(i32, 0), pane.cur_row); - try std.testing.expectEqual(@as(i32, 0), pane.cur_col); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), pane.body.cur_col); _ = state.ensureText(p.pdf_gpa); const text_lines = std.mem.count(u8, state.text, "\n") + 1; try std.testing.expect(state.text_scroll < text_lines); @@ -690,8 +690,8 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { p.update(.{ .key = .{ .cp = '4' } }); p.update(.{ .key = .{ .cp = 'g' } }); p.update(.{ .key = .{ .cp = '?' } }); - try std.testing.expectEqual(.none, pane.normal.prefix); - try std.testing.expectEqual(@as(u32, 0), pane.normal.count); + try std.testing.expectEqual(.none, pane.body.normal.prefix); + try std.testing.expectEqual(@as(u32, 0), pane.body.normal.count); p.update(.{ .key = .{ .cp = 'j' } }); try std.testing.expectEqual(row_scroll, pane.pdf.?.document_scroll_y); @@ -893,9 +893,9 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" { p.update(.{ .key = .{ .cp = 'n' } }); try std.testing.expectEqual(results_id, p.active); const results_pane = p.panes[results_id].?; - try std.testing.expect(results_pane.vsel.active and results_pane.vsel.explicit); - try std.testing.expectEqual(@as(i32, 0), results_pane.cur_row); - try std.testing.expectEqual(@as(i32, 0), results_pane.cur_col); + try std.testing.expect(results_pane.body.vsel.active and results_pane.body.vsel.explicit); + try std.testing.expectEqual(@as(i32, 0), results_pane.body.cur_row); + try std.testing.expectEqual(@as(i32, 0), results_pane.body.cur_col); p.update(.{ .key = .{ .cp = Key.enter } }); try std.testing.expectEqual(@as(usize, 0), p.active); try std.testing.expectEqualStrings("Pardes", pane.pdf.?.search_query); @@ -1097,15 +1097,15 @@ test "PDF normal adapter consumes unsupported actions and navigates page fallbac p.update(.{ .key = .{ .cp = '|' } }); try std.testing.expectEqual(page_before_noop, pv.page); try std.testing.expectEqual(revision_before_noop, pv.next_raster_revision); - try std.testing.expectEqual(@as(u32, 0), pane.normal.count); - try std.testing.expectEqual(.none, pane.normal.prefix); - try std.testing.expect(!pane.vsel.active and !pane.msel.active and pane.nsel == 0); + try std.testing.expectEqual(@as(u32, 0), pane.body.normal.count); + try std.testing.expectEqual(.none, pane.body.normal.prefix); + try std.testing.expect(!pane.body.vsel.active and !pane.body.msel.active and pane.body.nsel == 0); try std.testing.expect(!pane.tag_edit); // Cross-pane BODY-NORMAL actions keep their established shared paths. p.update(.{ .key = .{ .cp = ':' } }); try std.testing.expect(pane.tag_edit); - try std.testing.expectEqual(Mode.normal, pane.mode); + try std.testing.expectEqual(Mode.normal, pane.body.mode); p.update(.{ .key = .{ .cp = Key.escape } }); try std.testing.expect(!pane.tag_edit); p.update(.{ .key = .{ .cp = ' ' } }); @@ -1167,8 +1167,8 @@ test "Escape cancels PDF chrome in place and Shift-Escape leaves the pane" { try std.testing.expect(!pv.highlights_dirty and !pv.search_reveal_pending); try std.testing.expectEqual(page_before, pv.page); try std.testing.expectEqual(scroll_before, pv.document_scroll_y); - try std.testing.expectEqual(@as(u32, 0), pane.normal.count); - try std.testing.expectEqual(.none, pane.normal.prefix); + try std.testing.expectEqual(@as(u32, 0), pane.body.normal.count); + try std.testing.expectEqual(.none, pane.body.normal.prefix); // A second Escape on a bare document is inert rather than an exit. p.update(.{ .key = .{ .cp = Key.escape } }); try std.testing.expectEqual(@as(usize, 0), p.active); @@ -1781,8 +1781,8 @@ test "PDF native mouse selection, Look, and highlights share page geometry" { const page_before = pv.page; const scroll_before = pv.document_scroll_y; const sels_before = pane.sel; - const cursor_before = .{ pane.cur_row, pane.cur_col }; - const modal_before = .{ pane.msel, pane.vsel, pane.nsel }; + const cursor_before = .{ pane.body.cur_row, pane.body.cur_col }; + const modal_before = .{ pane.body.msel, pane.body.vsel, pane.body.nsel }; const selection_before = pv.selection orelse return error.MissingPdfSelection; const selection_quads_before = try gpa.dupe(pdf_impl.Quad, selection_before.quads); defer gpa.free(selection_quads_before); @@ -1836,8 +1836,8 @@ test "PDF native mouse selection, Look, and highlights share page geometry" { try std.testing.expectEqual(page_before, pv.page); try std.testing.expectEqual(scroll_before, pv.document_scroll_y); try std.testing.expect(std.meta.eql(sels_before, pane.sel)); - try std.testing.expectEqual(cursor_before, .{ pane.cur_row, pane.cur_col }); - try std.testing.expect(std.meta.eql(modal_before, .{ pane.msel, pane.vsel, pane.nsel })); + try std.testing.expectEqual(cursor_before, .{ pane.body.cur_row, pane.body.cur_col }); + try std.testing.expect(std.meta.eql(modal_before, .{ pane.body.msel, pane.body.vsel, pane.body.nsel })); const selection_after = pv.selection orelse return error.HoverDroppedPdfSelection; try std.testing.expectEqualSlices(pdf_impl.Quad, selection_quads_before, selection_after.quads); try std.testing.expectEqualStrings(selection_text_before, pv.selection_text); @@ -1938,7 +1938,7 @@ test "PDF native mouse selection, Look, and highlights share page geometry" { try std.testing.expect(panes.Pdf.beginSelection(p, pane, word_col, selected_row)); p.update(.{ .key = .{ .cp = 'v' } }); try std.testing.expect(pv.selection != null); - try std.testing.expect(!pane.vsel.active); + try std.testing.expect(!pane.body.vsel.active); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = word_col, .row = selected_row } }); p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = word_col, .row = selected_row } }); try std.testing.expect(pv.selection == null); diff --git a/test/perf.zig b/test/perf.zig index 6ce8f014..8eec1ed0 100644 --- a/test/perf.zig +++ b/test/perf.zig @@ -229,7 +229,7 @@ fn measureStartup(io: std.Io, reps: usize, json: bool) !void { try std.json.Stringify.valueAlloc(gpa, result, .{}) else try std.fmt.allocPrint(gpa, "{s}: boot {d} ns (cold {d}), first frame {d} ns, {d} allocations / {d} B, peak {d} B", .{ - result.case, result.init_median_ns, result.cold_ns, result.frame_median_ns, + result.case, result.init_median_ns, result.cold_ns, result.frame_median_ns, result.allocations, result.allocated_bytes, result.peak_bytes, }); defer gpa.free(text); @@ -473,7 +473,7 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u for (0..warmup + reps) |n| { seek(pane, fx, n); _ = try frame(core); // settle the view at the new spot, untimed - const before = .{ .row = pane.cur_row, .col = pane.cur_col, .scroll = pane.scroll(), .len = pane.file.?.content.len }; + const before = .{ .row = pane.body.cur_row, .col = pane.body.cur_col, .scroll = pane.scroll(), .len = pane.file.?.content.len }; const rect = core.rects[id]; const wheel_col = rect.x + pardes.config.GUTTER; const wheel_row = if (core.settings.tag_bottom) rect.y else rect.y + pardes.BOX_H; @@ -492,16 +492,16 @@ fn measure(op: Op, fx: Fixture, path: []const u8, expected_bytes: usize, reps: u const dt = nowNs() -| t0; switch (op) { .open => unreachable, - .render => if (pane.cur_row != before.row or pane.cur_col != before.col or pane.file.?.content.len != before.len) + .render => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col or pane.file.?.content.len != before.len) return error.RenderChangedDocument, - .key_down => if (pane.cur_row != before.row + 1) return error.DownDidNotMove, - .key_right => if (pane.cur_row != before.row or pane.cur_col != before.col + 1) return error.RightDidNotMove, + .key_down => if (pane.body.cur_row != before.row + 1) return error.DownDidNotMove, + .key_right => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col + 1) return error.RightDidNotMove, .page_down => if (pane.scroll() <= before.scroll and before.scroll < @as(i32, @intCast(fx.lines)) - pane.rows) return error.PageDidNotScroll, .wheel => if (pane.scroll() <= before.scroll and before.scroll < @as(i32, @intCast(fx.lines)) - pane.rows) return error.WheelDidNotScroll, .edit_char => { - if (pane.file.?.content.len != before.len + 1 or pane.cur_row != before.row or pane.cur_col != before.col + 1) + if (pane.file.?.content.len != before.len + 1 or pane.body.cur_row != before.row or pane.body.cur_col != before.col + 1) return error.CharacterWasNotInserted; const line = pardes.panes.File.sourceLine(pane, before.row); if (line[@intCast(before.col)] != 'x') return error.CharacterWasNotInserted; @@ -521,13 +521,13 @@ fn measureTerm(op: TermOp, fx: TermFixture, reps: usize) !Cell { defer core.deinit(); const pane = core.panes[0] orelse return error.MissingTerminal; const state = pane.terminal orelse return error.MissingTerminal; - if (op == .key_down or op == .edit_char) pane.mode = .normal; + if (op == .key_down or op == .edit_char) pane.body.mode = .normal; if (op == .edit_char) { core.update(.{ .key = .{ .cp = 'i', .text = "i" } }); pump(core); } _ = try frame(core); - const motion_rows = if (op == .key_down) (try pardes.edit.paneCursorLines(core, pane)).len else 0; + const motion_rows = if (op == .key_down) (try pardes.edit.paneCursorLines(core, &pane.body)).len else 0; if (op == .key_down and motion_rows < 3) return error.EmptyTerminalFixture; var cols = screen_cols; @@ -535,16 +535,16 @@ fn measureTerm(op: TermOp, fx: TermFixture, reps: usize) !Cell { for (0..warmup + reps) |n| { if (op == .key_down) { const span = @max(1, motion_rows * 8 / 10); - pane.cur_row = @intCast(@min(motion_rows - 2, motion_rows / 10 + (n *% 7919) % span)); - pane.cur_col = 0; - pane.cur_pinned = true; - pane.sticky_col = -1; + pane.body.cur_row = @intCast(@min(motion_rows - 2, motion_rows / 10 + (n *% 7919) % span)); + pane.body.cur_col = 0; + pane.body.cur_pinned = true; + pane.body.sticky_col = -1; pane.ensureCursorVisible(); _ = try frame(core); } const before = .{ - .row = pane.cur_row, - .col = pane.cur_col, + .row = pane.body.cur_row, + .col = pane.body.cur_col, .edit_len = if (pane.ovl) |overlay| overlay.text.len else 0, .replay_head = state.replay.head, .replay_len = state.replay.len, @@ -570,7 +570,7 @@ fn measureTerm(op: TermOp, fx: TermFixture, reps: usize) !Cell { _ = try frame(core); const dt = nowNs() -| t0; switch (op) { - .render => if (pane.cur_row != before.row or pane.cur_col != before.col or + .render => if (pane.body.cur_row != before.row or pane.body.cur_col != before.col or state.replay.head != before.replay_head or state.replay.len != before.replay_len) return error.RenderChangedTerminal, .output => { @@ -590,12 +590,12 @@ fn measureTerm(op: TermOp, fx: TermFixture, reps: usize) !Cell { state.vt.screens.active.pages.rows != pane.rows) return error.TerminalDidNotResize, .resize_cols => if (pane.cols == before.cols or pane.rows != before.rows or state.vt.screens.active.pages.cols != pane.cols) return error.TerminalDidNotResize, - .key_down => if (pane.cur_row != before.row + 1) return error.DownDidNotMove, + .key_down => if (pane.body.cur_row != before.row + 1) return error.DownDidNotMove, .edit_char => { const overlay = pane.ovl orelse return error.CharacterWasNotInserted; - if (overlay.text.len != before.edit_len + 1 or pane.cur_row != before.row or pane.cur_col != before.col + 1) + if (overlay.text.len != before.edit_len + 1 or pane.body.cur_row != before.row or pane.body.cur_col != before.col + 1) return error.CharacterWasNotInserted; - const line = pardes.modal.lineSlice(overlay.text, @intCast(pane.cur_row - overlay.row)); + const line = pardes.modal.lineSlice(overlay.text, @intCast(pane.body.cur_row - overlay.row)); if (before.col < 0 or before.col >= line.len or line[@intCast(before.col)] != 'x') return error.CharacterWasNotInserted; }, @@ -711,7 +711,7 @@ const UndoOutput = struct { fn measureUndoOutput(core: *pardes.Pardes, counter: *Allocations, arena: *std.heap.ArenaAllocator, samples: []u64) ![3]UndoOutput { const pane = core.panes[0].?; const pages = &pane.terminal.?.vt.screens.active.pages; - if (pane.ed_undo_len != 0 or pane.ed_redo_len != 0) return error.UnexpectedTerminalHistory; + if (pane.body.ed_undo_len != 0 or pane.body.ed_redo_len != 0) return error.UnexpectedTerminalHistory; if (pane.ovl) |overlay| core.gpa.free(overlay.text); pane.ovl = null; var result: [3]UndoOutput = undefined; @@ -722,17 +722,17 @@ fn measureUndoOutput(core: *pardes.Pardes, counter: *Allocations, arena: *std.he const first: i32 = @intCast(@min(128, pages.total_rows - 2)); for (0..depth) |index| { pane.ovl = .{ .row = first, .rows = 2, .text = try std.fmt.allocPrint(core.gpa, "saved edit {d}\nsecond row", .{index}) }; - pane.cur_row = first + 1; - pane.cur_col = 1; - pane.vsel = .{ .active = true, .row = first, .col = 0 }; + pane.body.cur_row = first + 1; + pane.body.cur_col = 1; + pane.body.vsel = .{ .active = true, .row = first, .col = 0 }; pardes.panes.Terminal.pushUndo(core, pane); core.gpa.free(pane.ovl.?.text); pane.ovl = null; - if (pane.ed_undo_len != index + 1) return error.TerminalUndoSnapshotFailed; + if (pane.body.ed_undo_len != index + 1) return error.TerminalUndoSnapshotFailed; } defer { - for (pane.ed_undo[0..pane.ed_undo_len]) |snapshot| if (snapshot.ovl) |overlay| core.gpa.free(overlay.text); - pane.ed_undo_len = 0; + for (pane.body.ed_undo[0..pane.body.ed_undo_len]) |snapshot| if (snapshot.ovl) |overlay| core.gpa.free(overlay.text); + pane.body.ed_undo_len = 0; } pardes.panes.Terminal.enterTty(core, 0); pump(core); @@ -767,7 +767,7 @@ fn measureUndoOutput(core: *pardes.Pardes, counter: *Allocations, arena: *std.he if (pages.countTrackedPins() != tracked_before + 2) return error.TerminalPinsLeaked; const expected: i32 = if (start.garbage) 0 else @intCast((pages.pointFromPin(.screen, start.*) orelse return error.TerminalAnchorLost).screen.y); const last: i32 = if (end.garbage) 0 else @intCast((pages.pointFromPin(.screen, end.*) orelse return error.TerminalAnchorLost).screen.y); - for (pane.ed_undo[0..pane.ed_undo_len], 0..) |snapshot, index| { + for (pane.body.ed_undo[0..pane.body.ed_undo_len], 0..) |snapshot, index| { const overlay = snapshot.ovl orelse return error.TerminalUndoTextLost; var expected_buffer: [64]u8 = undefined; const text = try std.fmt.bufPrint(&expected_buffer, "saved edit {d}\nsecond row", .{index}); @@ -812,7 +812,7 @@ fn measureTerminalSession(io: std.Io, mib: usize, reps: usize, json: bool) !void pump(core); const pane = core.panes[0] orelse return error.MissingTerminal; const state = pane.terminal orelse return error.MissingTerminal; - pane.mode = .tty; + pane.body.mode = .tty; pane.tty_filter = false; var arena: std.heap.ArenaAllocator = .init(counter.allocator()); defer arena.deinit(); @@ -855,17 +855,17 @@ fn measureTerminalSession(io: std.Io, mib: usize, reps: usize, json: bool) !void pump(core); _ = try core.render(arena.allocator()); const first_modal_ns = nowNs() - modal_begin; - if (pane.mode != .normal or !pane.cur_pinned or pane.cur_row != first_row - 1) return error.FirstModalKeyDidNotMove; + if (pane.body.mode != .normal or !pane.body.cur_pinned or pane.body.cur_row != first_row - 1) return error.FirstModalKeyDidNotMove; for (modal_samples, 0..) |*sample, index| { const down = index % 2 == 0; - const before = pane.cur_row; + const before = pane.body.cur_row; _ = arena.reset(.retain_capacity); const begin = nowNs(); core.update(.{ .key = .{ .cp = if (down) 'j' else 'k' } }); pump(core); _ = try core.render(arena.allocator()); sample.* = nowNs() - begin; - if (pane.cur_row != before + @as(i32, if (down) 1 else -1)) return error.ModalKeyDidNotMove; + if (pane.body.cur_row != before + @as(i32, if (down) 1 else -1)) return error.ModalKeyDidNotMove; } core.update(.{ .key = .{ .cp = core.opts.tty_toggle, .ctrl = true } }); pump(core); @@ -883,8 +883,8 @@ fn measureTerminalSession(io: std.Io, mib: usize, reps: usize, json: bool) !void const live_after_modal = counter.live; var overlay_median_ns: [2]u64 = undefined; var overlay_p95_ns: [2]u64 = undefined; - pane.mode = .normal; - pane.cur_pinned = true; + pane.body.mode = .normal; + pane.body.cur_pinned = true; pane.ovl = .{ .row = 0, .rows = 1, .text = try core.gpa.dupe(u8, "edited output") }; for (0..2) |phase| { for (modal_samples, 0..) |*sample, index| { @@ -897,8 +897,8 @@ fn measureTerminalSession(io: std.Io, mib: usize, reps: usize, json: bool) !void pump(core); const cursor_row = pardes.panes.Terminal.gridOffset(pane) + @as(i32, pardes.panes.Terminal.gridCursor(pane).y); pane.ovl.?.row = if (phase == 0) 0 else cursor_row - 1; - pane.cur_row = pane.ovl.?.row; - pane.cur_col = 0; + pane.body.cur_row = pane.ovl.?.row; + pane.body.cur_col = 0; const surface = try core.render(arena.allocator()); sample.* = nowNs() - begin; try checkTerminalTail(core, surface, tail_id); @@ -1146,11 +1146,11 @@ fn seek(pane: *pardes.Pane, fx: Fixture, n: usize) void { const row = fx.lines / 10 + (n *% 7919) % span; f.scroll = @min(row, fx.lines -| 1); f.syntax_dirty = true; - pane.cur_row = @intCast(f.scroll); - pane.cur_col = if (fx.cols > 200) @intCast(fx.cols * 3 / 4) else 0; - pane.cur_pinned = true; - pane.hscroll = @max(0, pane.cur_col - 40); - pane.sticky_col = -1; + pane.body.cur_row = @intCast(f.scroll); + pane.body.cur_col = if (fx.cols > 200) @intCast(fx.cols * 3 / 4) else 0; + pane.body.cur_pinned = true; + pane.hscroll = @max(0, pane.body.cur_col - 40); + pane.body.sticky_col = -1; } fn boot() !*pardes.Pardes { |
