summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/File.zig78
-rw-r--r--src/Mini.zig2
-rw-r--r--src/Output.zig22
-rw-r--r--src/Pipe.zig52
-rw-r--r--src/Terminal.zig114
-rw-r--r--src/Text.zig606
-rw-r--r--src/body_layer.zig44
-rw-r--r--src/colors.zig6
-rw-r--r--src/config.zig6
-rw-r--r--src/detached/client.zig6
-rw-r--r--src/detached/server.zig4
-rw-r--r--src/dump.zig4
-rw-r--r--src/edit.zig1020
-rw-r--r--src/exec.zig40
-rw-r--r--src/gui/gui.zig14
-rw-r--r--src/host_io.zig2
-rw-r--r--src/layout.zig2
-rw-r--r--src/look.zig106
-rw-r--r--src/macos.zig8
-rw-r--r--src/mouse.zig130
-rw-r--r--src/ninep/ctl.zig18
-rw-r--r--src/ninep/pane.zig26
-rw-r--r--src/normal.zig464
-rw-r--r--src/panes.zig623
-rw-r--r--src/pardes.zig239
-rw-r--r--src/pdf_view.zig48
-rw-r--r--src/tty/tty.zig6
27 files changed, 1868 insertions, 1822 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);