From f48ab559a4d257137a3aae987a4962f9d74ede23 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 01:09:05 -0300 Subject: Have Text.pane return nothing for a header instead of trapping A column's or the workspace's tag has no pane, and asking one for it hit unreachable; every caller guarded against that by checking `what` first. An optional makes each caller say what it does without a pane, so a forgotten guard no longer compiles into a crash. Co-Authored-By: Claude Opus 5.5 --- src/File.zig | 2 +- src/Text.zig | 30 +++++++++++++++++------------- src/edit.zig | 37 +++++++++++++++++++------------------ src/normal.zig | 4 ++-- 4 files changed, 39 insertions(+), 34 deletions(-) (limited to 'src') diff --git a/src/File.zig b/src/File.zig index 262f3ebb..af538385 100644 --- a/src/File.zig +++ b/src/File.zig @@ -655,7 +655,7 @@ pub fn cursorLines(arena: std.mem.Allocator, pane: *Pane, f: *State) ![]const [] /// Edit-buffer fragments and other temporary text retain modal's scan path. fn contentIndex(t: *Text, text: []const u8) ?[]const usize { if (t.what != .body) return null; - const pane = t.pane(); + const pane = t.pane() orelse return null; 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; diff --git a/src/Text.zig b/src/Text.zig index d4b6d048..61f238ae 100644 --- a/src/Text.zig +++ b/src/Text.zig @@ -76,13 +76,14 @@ ed_redo: [Terminal.history_max]Terminal.Snapshot = undefined, ed_redo_len: usize = 0, /// The window this text belongs to (acme's t->w). A column's or the -/// workspace's tag belongs to no pane; nothing asks it for one. -pub fn pane(t: *Text) *Pane { +/// workspace's tag belongs to none, and the optional makes every caller +/// say what it does then. +pub fn pane(t: *Text) ?*Pane { return switch (t.what) { .body => @alignCast(@fieldParentPtr("body", t)), .tag => @alignCast(@fieldParentPtr("tag", t)), .input => @alignCast(@fieldParentPtr("input", t)), - .column, .workspace => unreachable, + .column, .workspace => null, }; } @@ -98,7 +99,7 @@ pub fn deinit(t: *Text, gpa: std.mem.Allocator) void { /// the tagline follows its caret where it is drawn, and a prompt's answer is /// drawn whole. pub fn show(t: *Text) void { - if (t.what == .body) t.pane().ensureCursorVisible(); + if (t.what == .body) if (t.pane()) |w| w.ensureCursorVisible(); } pub fn toModalCursor(t: *Text) modal.Cursor { @@ -526,8 +527,9 @@ pub fn visualMove( pub fn halfPageMove(t: *Text, text: []const u8, range: modal.Selection, down: bool) void { // a view to move through is a body's; a tag or an answer is all shown if (t.what != .body) return; - const half: i32 = @max(1, @divTrunc(@as(i32, t.pane().rows), 2)); - t.pane().scrollBy(if (down) half else -half); + const w = t.pane() orelse return; + const half: i32 = @max(1, @divTrunc(@as(i32, w.rows), 2)); + w.scrollBy(if (down) half else -half); verticalMove(t, text, range, down, @intCast(half)); } @@ -536,9 +538,10 @@ pub fn halfPageMove(t: *Text, text: []const u8, range: modal.Selection, down: bo pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta: i32) void { // a view to move through is a body's; a tag or an answer is all shown if (t.what != .body) return; - 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 w = t.pane() orelse return; + const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, w.rows) - 1, 2)); + w.scrollBy(delta); + const top = w.scroll(); const last_row: i32 = @intCast(File.textLineCount(t, text) - 1); const cur = modal.selectionCursor(text, range); if (delta > 0) { @@ -548,7 +551,7 @@ pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta: 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 snap: i32 = @max(0, @min(top + @as(i32, w.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; @@ -560,10 +563,11 @@ pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta: pub fn gotoWindow(t: *Text, text: []const u8, range: modal.Selection, which: enum { top, center, bottom }, cnt: usize) void { // a view to move through is a body's; a tag or an answer is all shown if (t.what != .body) return; - const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, t.pane().rows) - 1, 2)); - const top = t.pane().scroll(); + const w = t.pane() orelse return; + const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, w.rows) - 1, 2)); + const top = w.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 last_vis: i32 = @min(@as(i32, w.rows) - 1, last_row - top); const n: i32 = @intCast(cnt - 1); var vline: i32 = switch (which) { .top => top + margin + n, diff --git a/src/edit.zig b/src/edit.zig index 42032168..837a22c6 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -315,7 +315,7 @@ pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { for (lines) |*line| line.* = it.next().?; return lines; } - const pane = t.pane(); + const pane = t.pane() orelse return &.{}; if (pane.file) |*f| return panes.File.cursorLines(arena, pane, f); if (pane.hasPdf()) { if (comptime pdf_enabled) return panes.Pdf.textLines(&pane.pdf.?, p.pdf_gpa, arena); @@ -325,8 +325,8 @@ pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { } pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_col: i32) i32 { - if (t.what == .body and t.pane().file != null) - return panes.File.byteAtRowDisplay(t.pane(), row, from_raw, display_col); + if (t.what == .body) if (t.pane()) |pane| if (pane.file != null) + return panes.File.byteAtRowDisplay(pane, row, 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) @@ -347,7 +347,7 @@ pub fn pinPaneCursor(p: *Pardes, t: *Text) void { t.cur_pinned = true; return; } - const pane = t.pane(); + const pane = t.pane() orelse return; pane.pinCursor(); if (pane.file == null and !pane.hasPdf()) t.cur_col = paneByteAtDisplay(p, t, t.cur_row, 0, t.cur_col); @@ -356,11 +356,11 @@ pub fn pinPaneCursor(p: *Pardes, t: *Text) void { pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { switch (t.what) { .body => {}, - .tag => return tagline.curTail(t.pane()), + .tag => return if (t.pane()) |pane| tagline.curTail(pane) else "", .input => return t.own orelse "", .column, .workspace => return t.own orelse tagline.headerDefault(p, t.what), } - const pane = t.pane(); + const pane = t.pane() orelse return ""; if (pane.file) |f| return f.content; if (pane.hasPdf()) { if (comptime pdf_enabled) return pane.pdf.?.ensureText(p.pdf_gpa); @@ -372,7 +372,7 @@ pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize { if (t.what != .body) return 0; - const pane = t.pane(); + const pane = t.pane() orelse return 0; if (pane.file == null) return 0; return panes.File.wrapWidth(pane, p.settings.wrap); } @@ -385,7 +385,7 @@ fn editText(p: *Pardes, t: *Text, lo: i32, hi: i32, col: i32) ?panes.EditText { if (t.what != .body and t.what != .input and t.own == null) t.own = p.gpa.dupe(u8, flatSurface(p, t) catch return null) catch return null; if (t.what != .body) return .{ .text = t.own orelse &.{}, .row0 = 0 }; - const pane = t.pane(); + const pane = t.pane() orelse return null; 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); @@ -406,7 +406,7 @@ fn setEditText(p: *Pardes, t: *Text, new: []u8) void { t.own = new; return; } - const pane = t.pane(); + const pane = t.pane() orelse return p.gpa.free(new); if (pane.file) |*f| return panes.File.setContent(p, f, new); panes.Terminal.setEditText(p, pane, new); } @@ -417,7 +417,7 @@ pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_a /// Terminal overlays can materialize virtual rows/columns, so keep those. pub fn clampFileCursor(t: *Text) bool { if (t.what != .body) return false; - const pane = t.pane(); + const pane = t.pane() orelse return false; const file = if (pane.file) |*file| file else return false; const old_row = t.cur_row; const old_col = t.cur_col; @@ -429,7 +429,7 @@ pub fn clampFileCursor(t: *Text) bool { } pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void { - if (t.what == .body and t.pane().hasPdf()) return; + if (t.what == .body) if (t.pane()) |pane| if (pane.hasPdf()) return; pinPaneCursor(p, t); _ = clampFileCursor(t); // snapshot once per insert session (WITH the pre-insert selection) so @@ -563,7 +563,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { const flat = flatSurface(p, t) catch return; const range = t.primaryRange(flat, 0); if (t.what != .body) return; - const step: i32 = @intCast(@max(1, t.pane().rows)); + const step: i32 = @intCast(@max(1, (t.pane() orelse return).rows)); t.scrollViewMove(flat, range, if (key.cp == Key.page_down) step else -step); return; }, @@ -710,7 +710,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void { const ln = modal.lineSlice(text, c.row); if (!p.multi_on and c.col > 0 and c.col <= ln.len and ln[c.col - 1] == '.') dot: { if (t.what != .body) break :dot; - const f = t.pane().file orelse break :dot; + const f = (t.pane() orelse break :dot).file orelse break :dot; if (f.output != null or !lsp.speaks(f.path)) break :dot; const seq = p.lsp_seq; p.lspRequest(p.active, .completion, ""); @@ -849,7 +849,7 @@ pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void { pub fn normalYank(p: *Pardes, t: *Text) void { t.select = false; - if (t.what == .body) if (t.pane().rawPointerText(t.pane().sel[sel_slot])) |text| { + if (t.what == .body) if (t.pane()) |pane| if (pane.rawPointerText(pane.sel[sel_slot])) |text| { setYank(p, text); return; }; @@ -1324,7 +1324,8 @@ pub fn normalToggleComment(p: *Pardes, t: *Text) void { } next = @max(next, end); } - const ext = if (t.what != .body) "" else if (t.pane().file) |f| std.fs.path.extension(f.path) else ""; + const file = if (t.what != .body) null else if (t.pane()) |pane| pane.file else null; + const ext = if (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)) { @@ -1567,7 +1568,7 @@ pub fn pushUndo(p: *Pardes, t: *Text) void { // a prompt's answer is typed once and gone; it keeps no history .input => return, } - const pane = t.pane(); + const pane = t.pane() orelse return; if (pane.file != null) return panes.File.pushUndo(p, pane); panes.Terminal.pushUndo(p, pane); } @@ -1575,7 +1576,7 @@ pub fn pushUndo(p: *Pardes, t: *Text) void { pub fn doUndo(p: *Pardes, t: *Text) void { if (t.what == .tag or t.what == .column or t.what == .workspace) return stepOwn(p, t, true); if (t.what != .body) return; - const pane = t.pane(); + const pane = t.pane() orelse return; if (pane.file != null) return panes.File.undo(p, pane); panes.Terminal.undo(p, pane); } @@ -1583,7 +1584,7 @@ pub fn doUndo(p: *Pardes, t: *Text) void { pub fn doRedo(p: *Pardes, t: *Text) void { if (t.what == .tag or t.what == .column or t.what == .workspace) return stepOwn(p, t, false); if (t.what != .body) return; - const pane = t.pane(); + const pane = t.pane() orelse return; if (pane.file != null) return panes.File.redo(p, pane); panes.Terminal.redo(p, pane); } diff --git a/src/normal.zig b/src/normal.zig index 29d6d620..c7e75d70 100644 --- a/src/normal.zig +++ b/src/normal.zig @@ -175,7 +175,7 @@ const Replay = union(enum) { pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void { const id = p.active; // a header belongs to no pane and cannot go away under the replay - const owner: ?*Pane = if (t.what == .column or t.what == .workspace) null else t.pane(); + const owner = t.pane(); const serial = if (owner) |pane| pane.serial else 0; // the whole selection in text coordinates, document order. t.sels // is already ordered, so this only slots the primary into place. @@ -292,7 +292,7 @@ pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void { fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void { // What reaches past the text -- a builtin, a search, a language query -- // acts for the pane the text belongs to; a header's is the active one. - const pane = if (t.what == .column or t.what == .workspace) p.panes[p.active] orelse return else t.pane(); + const pane = t.pane() orelse p.panes[p.active] orelse return; const raw_selection = t.what == .body and pane.rawPointerText(pane.sel[sel_slot]) != null; 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 })) { -- cgit v1.3