summaryrefslogtreecommitdiff
path: root/src/file_pane.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 10:41:33 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch)
tree91fce278abc85e4f7491de8b5f02184c8a2d5a56 /src/file_pane.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'src/file_pane.zig')
-rw-r--r--src/file_pane.zig65
1 files changed, 40 insertions, 25 deletions
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