From 9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sun, 9 Aug 2026 10:41:33 -0300 Subject: replace ArrayLists with bounded storage --- src/file_pane.zig | 65 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 40 insertions(+), 25 deletions(-) (limited to 'src/file_pane.zig') diff --git a/src/file_pane.zig b/src/file_pane.zig index b06599ae..c0a2350d 100644 --- a/src/file_pane.zig +++ b/src/file_pane.zig @@ -43,9 +43,8 @@ pub fn lineCount(content: []const u8) usize { /// is what they already did when their own arena ran out. pub fn lineIndex(gpa: std.mem.Allocator, f: *File) ![]const usize { if (f.line_starts.len > 0) return f.line_starts; - // exact alloc, not an ArrayList: deinitPane frees `line_starts` itself, and - // an ArrayList's items slice is shorter than its allocation whenever the - // growth strategy rounded up — freeing that is a wrong-size free. + // Exact allocation: deinitPane frees `line_starts` itself, so the stored + // slice must span the complete allocation rather than spare capacity. const starts = try gpa.alloc(usize, lineCount(f.content)); starts[0] = 0; var i: usize = 1; @@ -81,6 +80,7 @@ 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 }; + pane.kind = .file; pane.cur_pinned = true; pane.cur_row = @intCast(scroll); // watches follow pane lifetime: this is the only place a real file is read @@ -100,7 +100,7 @@ pub fn setContent(p: *Pardes, f: *File, new: []u8) void { // the highlights go too, and not just because they are stale: their byte // range is what refreshHighlights tests a scroll against, and a range // measured on the OLD content would let it skip a re-parse it needs - if (f.highlights.len > 0) p.gpa.free(f.highlights); + if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights); f.highlights = &.{}; f.highlight_start = 0; f.syntax_dirty = true; @@ -133,7 +133,7 @@ pub fn refreshHighlights(p: *Pardes) void { const f = &pane.file.?; if (!f.syntax_dirty) continue; if (!p.colors_on) { - if (f.highlights.len > 0) p.gpa.free(f.highlights); + if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights); f.highlights = &.{}; f.highlight_start = 0; f.syntax_dirty = false; @@ -168,11 +168,11 @@ pub fn refreshHighlights(p: *Pardes) void { const slack: usize = if (f.highlights.len == 0) 0 else pane.rows; const start = lineStart(p.gpa, f, f.scroll -| slack); const end = @max(start, lineStart(p.gpa, f, f.scroll + pane.rows + SYNTAX_CONTEXT_AFTER_ROWS + slack)); - const new_highlights = syntax.highlightFileRange(p.gpa, f.path, f.content, start, end) catch { + const new_highlights = syntax.highlightFileRange(p.tree_sitter_gpa, f.path, f.content, start, end) catch { f.syntax_dirty = false; continue; }; - if (f.highlights.len > 0) p.gpa.free(f.highlights); + if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights); f.highlights = new_highlights; f.highlight_start = if (f.highlights.len > 0) start else 0; f.syntax_dirty = false; @@ -190,7 +190,6 @@ pub fn refreshHighlights(p: *Pardes) void { /// built. `wrap_n` stays 0 for an unwrapped body, and that is the value the /// readers treat as "rows are lines", so the off path never consults an array. pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![]const u8 { - var out: std.ArrayList(u8) = .empty; // the pane's own content width, less the ONE column the break marker sits // in — a wrapped row that filled its last cell would have nowhere to say it // wrapped. 0 means don't wrap. A pane taller than the map refuses too, @@ -200,6 +199,21 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] else @max(1, @as(usize, pane.cols -| config.PREFIX_W) -| 1); pane.wrap_n = 0; + + // Count the exact rendered bytes first. Unwrapped source lines are not + // bounded by the pane width, so a rows*cols buffer would either truncate + // them or quietly restore a growable builder under another name. + const len = fillBody(null, pane, f, width, false); + const out = try arena.alloc(u8, len); + const filled = fillBody(out, pane, f, width, true); + std.debug.assert(filled == out.len); + return out; +} + +/// Run the file-body row walk. With no destination it is the exact sizing +/// pass; with one it fills that allocation and records the wrapping map. +fn fillBody(dst: ?[]u8, pane: *Pane, f: *File, width: usize, record_wrap: bool) usize { + if (record_wrap) pane.wrap_n = 0; // start ON the first visible line instead of walking the file to it: this // walk was O(f.scroll) and recolorSyntax below ran the identical one again var flines = std.mem.splitScalar(u8, f.content[lineStart(pane.gpa, f, f.scroll)..], '\n'); @@ -212,15 +226,17 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] var abs: i32 = @intCast(f.scroll); var at: usize = 0; var cur = flines.next(); - var i: usize = 0; - while (i < pane.rows) : (i += 1) { - if (i > 0) try out.append(arena, '\n'); - if (width > 0) { + var written: usize = 0; + for (0..pane.rows) |i| { + if (i > 0) { + if (dst) |out| out[written] = '\n'; + written += 1; + } + if (width > 0 and record_wrap) { pane.wrap_line[i] = abs; pane.wrap_col[i] = @intCast(at); pane.wrap_n = @intCast(i + 1); } - const row_start = out.items.len; if (cur) |text| { var lbuf: [16]u8 = undefined; // unsigned: {d} prints a leading '+' for signed ints @@ -232,7 +248,9 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] " " else std.fmt.bufPrint(&lbuf, "{d: >4} ", .{lineno}) catch " "; - try out.appendSlice(arena, prefix); + if (dst) |out| @memcpy(out[written..][0..prefix.len], prefix); + written += prefix.len; + // A byte cut, like the hscroll one below, and it can land inside a // multi-byte glyph for the same reason: columns here are BYTES. // Surface.print decodes by hand and emits U+FFFD per undecodable @@ -241,7 +259,13 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] // the pane either: a UTF-8 sequence is never fewer bytes than the // cells it draws in. const take = if (width == 0) text.len - at else @min(width, text.len - at); - try out.appendSlice(arena, text[at .. at + take]); + const cut = if (pane.hscroll > 0 and width == 0) + @min(@as(usize, @intCast(pane.hscroll)), take) + else + 0; + const shown = text[at + cut .. at + take]; + if (dst) |out| @memcpy(out[written..][0..shown.len], shown); + written += shown.len; if (width > 0 and at + take < text.len) { at += take; } else { @@ -250,17 +274,8 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] cur = flines.next(); } } else abs += 1; - // horizontal scroll: cut hscroll columns after the gutter. Wrapped - // there is nothing to scroll to, so the cut is not made at all. - if (pane.hscroll > 0 and width == 0) { - const cut_at = row_start + config.PREFIX_W; - if (out.items.len > cut_at) { - const cut = @min(@as(usize, @intCast(pane.hscroll)), out.items.len - cut_at); - out.replaceRangeAssumeCapacity(cut_at, cut, &.{}); - } - } } - return out.items; + return written; } /// line-number gutter: mute the first PREFIX_W columns. Cheap chrome, not -- cgit v1.3