diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-09 10:41:33 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch) | |
| tree | 91fce278abc85e4f7491de8b5f02184c8a2d5a56 /src/modal.zig | |
| parent | 2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff) | |
| download | pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip | |
replace ArrayLists with bounded storage
Diffstat (limited to 'src/modal.zig')
| -rw-r--r-- | src/modal.zig | 252 |
1 files changed, 148 insertions, 104 deletions
diff --git a/src/modal.zig b/src/modal.zig index 8e36d1ae..424421f6 100644 --- a/src/modal.zig +++ b/src/modal.zig @@ -453,6 +453,13 @@ pub fn lineSlice(content: []const u8, row: usize) []const u8 { } // ---- file content mutations. caller frees the returned slice + the old one. ---- +fn spliceAlloc(alloc: std.mem.Allocator, content: []const u8, start: usize, end: usize, replacement: []const u8) ![]u8 { + const out = try alloc.alloc(u8, content.len - (end - start) + replacement.len); + @memcpy(out[0..start], content[0..start]); + @memcpy(out[start..][0..replacement.len], replacement); + @memcpy(out[start + replacement.len ..], content[end..]); + return out; +} // insert `text` at (row, col). col is clamped to the line length. pub fn insertAt(alloc: std.mem.Allocator, content: []const u8, c: Cursor, text: []const u8) ![]u8 { @@ -460,11 +467,7 @@ pub fn insertAt(alloc: std.mem.Allocator, content: []const u8, c: Cursor, text: const line = lineSlice(content, row); const col = @min(c.col, line.len); const off = lineStartOffset(content, row) + col; - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..off]); - try out.appendSlice(alloc, text); - try out.appendSlice(alloc, content[off..]); - return out.toOwnedSlice(alloc); + return spliceAlloc(alloc, content, off, off, text); } // delete the character at (row, col). no-op if col is past the line end. @@ -472,10 +475,7 @@ pub fn deleteChar(alloc: std.mem.Allocator, content: []const u8, c: Cursor) ![]u const line = lineSlice(content, c.row); if (c.col >= line.len) return alloc.dupe(u8, content); const off = lineStartOffset(content, c.row) + c.col; - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..off]); - try out.appendSlice(alloc, content[off + 1 ..]); - return out.toOwnedSlice(alloc); + return spliceAlloc(alloc, content, off, off + 1, ""); } // delete whole lines [r0, r1] inclusive (the line content + their terminators). @@ -486,7 +486,11 @@ pub fn deleteLines(alloc: std.mem.Allocator, content: []const u8, r0: usize, r1: const n = lineCount(content); const lo = @min(r0, r1); const hi = @min(@max(r0, r1), if (n == 0) 0 else n - 1); - if (n == 0 or hi < lo) return .{ .content = try alloc.dupe(u8, content), .deleted = try alloc.dupe(u8, "") }; + if (n == 0 or hi < lo) { + const unchanged = try alloc.dupe(u8, content); + errdefer alloc.free(unchanged); + return .{ .content = unchanged, .deleted = try alloc.dupe(u8, "") }; + } const start = lineStartOffset(content, lo); // end = start of line (hi+1), or content.len if hi is the last line. @@ -497,18 +501,23 @@ pub fn deleteLines(alloc: std.mem.Allocator, content: []const u8, r0: usize, r1: const cut_hi = end; if (hi + 1 == n and start > 0) cut_lo -= 1; // remove the '\n' before the last line - var deleted: std.ArrayList(u8) = .empty; - { - var r = lo; - while (r <= hi) : (r += 1) { - if (r > lo) try deleted.append(alloc, '\n'); - try deleted.appendSlice(alloc, lineSlice(content, r)); + var deleted_len: usize = hi - lo; + var r = lo; + while (r <= hi) : (r += 1) deleted_len += lineSlice(content, r).len; + const deleted = try alloc.alloc(u8, deleted_len); + errdefer alloc.free(deleted); + var write: usize = 0; + r = lo; + while (r <= hi) : (r += 1) { + if (r > lo) { + deleted[write] = '\n'; + write += 1; } + const line = lineSlice(content, r); + @memcpy(deleted[write..][0..line.len], line); + write += line.len; } - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..cut_lo]); - try out.appendSlice(alloc, content[cut_hi..]); - return .{ .content = try out.toOwnedSlice(alloc), .deleted = try deleted.toOwnedSlice(alloc) }; + return .{ .content = try spliceAlloc(alloc, content, cut_lo, cut_hi, ""), .deleted = deleted }; } // normalize two cursors into document order (lo <= hi), then byte offsets of the @@ -537,40 +546,39 @@ pub fn deleteRange(alloc: std.mem.Allocator, content: []const u8, a: Cursor, b: const r = rangeBytes(content, a, b); const deleted = try alloc.dupe(u8, content[r.s..r.e]); errdefer alloc.free(deleted); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..r.s]); - try out.appendSlice(alloc, content[r.e..]); - return .{ .content = try out.toOwnedSlice(alloc), .deleted = deleted }; + return .{ .content = try spliceAlloc(alloc, content, r.s, r.e, ""), .deleted = deleted }; } // replace line `row`'s text with "" (keep the line, empty it). For `c`hange line. pub fn clearLine(alloc: std.mem.Allocator, content: []const u8, row: usize) ![]u8 { const line = lineSlice(content, row); const start = lineStartOffset(content, row); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..start]); - try out.appendSlice(alloc, content[start + line.len ..]); - return out.toOwnedSlice(alloc); + return spliceAlloc(alloc, content, start, start + line.len, ""); } // paste `text` as a new line BELOW `row`. Multiline `text` becomes several lines. pub fn pasteLineBelow(alloc: std.mem.Allocator, content: []const u8, row: usize, text: []const u8) ![]u8 { const n = lineCount(content); const off = if (row + 1 < n) lineStartOffset(content, row + 1) else content.len; - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..off]); - if (off < content.len or (content.len > 0 and content[content.len - 1] == '\n')) { - // mid-buffer, or below the last line of newline-terminated content: - // the pasted block carries its own terminator - try out.appendSlice(alloc, text); - try out.append(alloc, '\n'); - } else { - // below a terminator-less last line: open it with a newline instead - if (off > 0) try out.append(alloc, '\n'); - try out.appendSlice(alloc, text); + const suffix_newline = off < content.len or (content.len > 0 and content[content.len - 1] == '\n'); + const prefix_newline = !suffix_newline and off > 0; + const extra: usize = @intFromBool(suffix_newline or prefix_newline); + const out = try alloc.alloc(u8, content.len + text.len + extra); + var write: usize = 0; + @memcpy(out[write..][0..off], content[0..off]); + write += off; + if (prefix_newline) { + out[write] = '\n'; + write += 1; + } + @memcpy(out[write..][0..text.len], text); + write += text.len; + if (suffix_newline) { + out[write] = '\n'; + write += 1; } - try out.appendSlice(alloc, content[off..]); - return out.toOwnedSlice(alloc); + @memcpy(out[write..], content[off..]); + return out; } // the cursor position AFTER `text` inserted at `c` (one past its last char). @@ -589,11 +597,7 @@ pub fn advanceBy(c: Cursor, text: []const u8) Cursor { // replace the inclusive char range [a, b] with `text` (R replace-with-yank). pub fn replaceRange(alloc: std.mem.Allocator, content: []const u8, a: Cursor, b: Cursor, text: []const u8) ![]u8 { const r = rangeBytes(content, a, b); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..r.s]); - try out.appendSlice(alloc, text); - try out.appendSlice(alloc, content[r.e..]); - return out.toOwnedSlice(alloc); + return spliceAlloc(alloc, content, r.s, r.e, text); } // `r<ch>`: overwrite every char in the inclusive range [a, b] with `ch` — @@ -631,11 +635,12 @@ pub fn joinLine(alloc: std.mem.Allocator, content: []const u8, row: usize) !?str const b = std.mem.trimStart(u8, next, " \t"); const start = lineStartOffset(content, row); const rest = lineStartOffset(content, row + 1) + (next.len - b.len); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0 .. start + a.len]); - try out.append(alloc, ' '); - try out.appendSlice(alloc, content[rest..]); - return .{ .content = try out.toOwnedSlice(alloc), .col = a.len }; + const prefix_end = start + a.len; + const out = try alloc.alloc(u8, prefix_end + 1 + content.len - rest); + @memcpy(out[0..prefix_end], content[0..prefix_end]); + out[prefix_end] = ' '; + @memcpy(out[prefix_end + 1 ..], content[rest..]); + return .{ .content = out, .col = a.len }; } // `>` / `<`: indent/unindent lines [r0, r1]. Fixed width — pardes has no @@ -643,25 +648,52 @@ pub fn joinLine(alloc: std.mem.Allocator, content: []const u8, row: usize) !?str pub const INDENT_W = 4; pub fn indentLines(alloc: std.mem.Allocator, content: []const u8, r0: usize, r1: usize, add: bool) ![]u8 { - var out: std.ArrayList(u8) = .empty; + const lo = @min(r0, r1); + const hi = @max(r0, r1); + var out_len = content.len; var it = std.mem.splitScalar(u8, content, '\n'); var row: usize = 0; while (it.next()) |line| : (row += 1) { - if (row > 0) try out.append(alloc, '\n'); - if (row >= @min(r0, r1) and row <= @max(r0, r1)) { + if (row < lo or row > hi) continue; + if (add) { + if (line.len != 0) out_len += INDENT_W; + } else { + var cut: usize = 0; + if (line.len > 0 and line[0] == '\t') { + cut = 1; + } else while (cut < line.len and cut < INDENT_W and line[cut] == ' ') cut += 1; + out_len -= cut; + } + } + + const out = try alloc.alloc(u8, out_len); + it = std.mem.splitScalar(u8, content, '\n'); + row = 0; + var write: usize = 0; + while (it.next()) |line| : (row += 1) { + if (row > 0) { + out[write] = '\n'; + write += 1; + } + var selected = line; + if (row >= lo and row <= hi) { if (add) { - if (line.len != 0) try out.appendSlice(alloc, " " ** INDENT_W); // empty lines stay empty (helix) - try out.appendSlice(alloc, line); + if (line.len != 0) { + @memset(out[write..][0..INDENT_W], ' '); + write += INDENT_W; + } + } else if (line.len > 0 and line[0] == '\t') { + selected = line[1..]; } else { var cut: usize = 0; - if (line.len > 0 and line[0] == '\t') { - cut = 1; - } else while (cut < line.len and cut < INDENT_W and line[cut] == ' ') cut += 1; - try out.appendSlice(alloc, line[cut..]); + while (cut < line.len and cut < INDENT_W and line[cut] == ' ') cut += 1; + selected = line[cut..]; } - } else try out.appendSlice(alloc, line); + } + @memcpy(out[write..][0..selected.len], selected); + write += selected.len; } - return out.toOwnedSlice(alloc); + return out; } // `Ctrl-a`/`Ctrl-x`: add `delta` to the decimal integer under the cursor @@ -689,11 +721,8 @@ pub fn adjustNumber(alloc: std.mem.Allocator, content: []const u8, c: Cursor, de else std.fmt.bufPrint(&buf, "{d}", .{@abs(nv)}) catch return null; const off = lineStartOffset(content, c.row); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0 .. off + s]); - try out.appendSlice(alloc, numstr); - try out.appendSlice(alloc, content[off + e ..]); - return .{ .content = try out.toOwnedSlice(alloc), .cur = .{ .row = c.row, .col = s + numstr.len - 1 } }; + const start = off + s; + return .{ .content = try spliceAlloc(alloc, content, start, off + e, numstr), .cur = .{ .row = c.row, .col = s + numstr.len - 1 } }; } // delete the EXCLUSIVE span [a, b) — insert-mode kills. col may equal the @@ -702,10 +731,7 @@ pub fn deleteSpan(alloc: std.mem.Allocator, content: []const u8, a: Cursor, b: C const s = lineStartOffset(content, a.row) + @min(a.col, lineSlice(content, a.row).len); const e = lineStartOffset(content, b.row) + @min(b.col, lineSlice(content, b.row).len); if (e <= s) return alloc.dupe(u8, content); - var out: std.ArrayList(u8) = .empty; - try out.appendSlice(alloc, content[0..s]); - try out.appendSlice(alloc, content[e..]); - return out.toOwnedSlice(alloc); + return spliceAlloc(alloc, content, s, e, ""); } // ---- helix range engine (phase 5) ---- @@ -1162,19 +1188,22 @@ pub fn hxIncrement(alloc: std.mem.Allocator, frag: []const u8, amount: i64) !?[] var flen: usize = frag.len; if (neg and !neg_after) flen -= 1; if (!neg and neg_after) flen += 1; - var out: std.ArrayList(u8) = .empty; - errdefer out.deinit(alloc); var buf: [48]u8 = undefined; // "{d}" prints '+' for positive signed ints — format the magnitude unsigned const mag = std.fmt.bufPrint(&buf, "{d}", .{@abs(nv)}) catch return null; - if (neg_after) try out.append(alloc, '-'); - if (pad) { - const want = flen - @as(usize, if (neg_after) 1 else 0); - var i = mag.len; - while (i < want) : (i += 1) try out.append(alloc, '0'); + const sign_len: usize = @intFromBool(neg_after); + const want = if (pad) flen - sign_len else mag.len; + const digits_len = @max(want, mag.len); + const out = try alloc.alloc(u8, sign_len + digits_len); + var write: usize = 0; + if (neg_after) { + out[0] = '-'; + write = 1; } - try out.appendSlice(alloc, mag); - return try out.toOwnedSlice(alloc); + @memset(out[write..][0 .. digits_len - mag.len], '0'); + write += digits_len - mag.len; + @memcpy(out[write..], mag); + return out; } test "hx word moves match helix" { @@ -1333,10 +1362,35 @@ fn tagOf(key: Key) std.meta.Tag(Key) { } fn splitLines(alloc: std.mem.Allocator, content: []const u8) ![][]const u8 { - var ls: std.ArrayList([]const u8) = .empty; + const count = std.mem.count(u8, content, "\n") + 1; + const lines = try alloc.alloc([]const u8, count); var it = std.mem.splitScalar(u8, content, '\n'); - while (it.next()) |ln| try ls.append(alloc, ln); - return ls.toOwnedSlice(alloc); + var i: usize = 0; + while (it.next()) |line| : (i += 1) lines[i] = line; + return lines; +} + +fn joinedLineRangeAlloc(alloc: std.mem.Allocator, content: []const u8, r0: usize, r1: usize) ![]u8 { + var len = r1 - r0; + var r = r0; + while (true) : (r += 1) { + len += lineSlice(content, r).len; + if (r == r1) break; + } + const out = try alloc.alloc(u8, len); + var write: usize = 0; + r = r0; + while (true) : (r += 1) { + if (r > r0) { + out[write] = '\n'; + write += 1; + } + const line = lineSlice(content, r); + @memcpy(out[write..][0..line.len], line); + write += line.len; + if (r == r1) break; + } + return out; } fn ensureVisible(view: View, row: usize) void { @@ -1370,10 +1424,9 @@ fn deleteFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, lin const r0: i32 = @min(st.msel_r0, st.msel_r1); const r1: i32 = @max(st.msel_r0, st.msel_r1); const d = try deleteLines(alloc, content, @intCast(@max(0, r0)), @intCast(@max(0, r1))); - if (yank.*) |old| alloc.free(old); - yank.* = try alloc.dupe(u8, d.deleted); - alloc.free(d.deleted); const n = lineCount(d.content); + if (yank.*) |old| alloc.free(old); + yank.* = d.deleted; st.cur = .{ .row = @intCast(@min(@as(usize, @intCast(@max(0, r0))), if (n == 0) 0 else n - 1)), .col = 0 }; st.msel_active = false; st.pending = 0; @@ -1396,19 +1449,13 @@ fn yankFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, yank: if (st.msel_active) { const r0: usize = @intCast(@max(0, @min(st.msel_r0, st.msel_r1))); const r1: usize = @intCast(@max(0, @max(st.msel_r0, st.msel_r1))); - var buf: std.ArrayList(u8) = .empty; - var r: usize = r0; - while (r <= r1) : (r += 1) { - if (r > r0) try buf.append(alloc, '\n'); - try buf.appendSlice(alloc, lineSlice(content, r)); - } + const new_yank = try joinedLineRangeAlloc(alloc, content, r0, r1); if (yank.*) |old| alloc.free(old); - yank.* = try buf.toOwnedSlice(alloc); - st.cur = .{ .row = r0, .col = 0 }; - st.msel_active = false; + yank.* = new_yank; } else { + const new_yank = try alloc.dupe(u8, lineSlice(content, st.cur.row)); if (yank.*) |old| alloc.free(old); - yank.* = try alloc.dupe(u8, lineSlice(content, st.cur.row)); + yank.* = new_yank; } st.pending = 0; } @@ -1432,20 +1479,17 @@ fn changeFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, lin const r0: usize = @intCast(@max(0, @min(st.msel_r0, st.msel_r1))); const r1: usize = @intCast(@max(0, @max(st.msel_r0, st.msel_r1))); // yank the original lines first (from the un-mutated content) - var buf: std.ArrayList(u8) = .empty; - var r: usize = r0; - while (r <= r1) : (r += 1) { - if (r > r0) try buf.append(alloc, '\n'); - try buf.appendSlice(alloc, lineSlice(content, r)); - } - if (yank.*) |old| alloc.free(old); - yank.* = try buf.toOwnedSlice(alloc); + const new_yank = try joinedLineRangeAlloc(alloc, content, r0, r1); + errdefer alloc.free(new_yank); // drop the extra lines (keep one), then empty the kept line const d = if (r1 > r0) try deleteLines(alloc, content, r0 + 1, r1) else null; defer if (d) |dd| alloc.free(dd.deleted); + errdefer if (d) |dd| alloc.free(dd.content); const after_del: []const u8 = if (d) |dd| dd.content else content; const cl = try clearLine(alloc, after_del, r0); if (d) |dd| alloc.free(dd.content); // free the intermediate (after_del copy) + if (yank.*) |old| alloc.free(old); + yank.* = new_yank; st.cur = .{ .row = r0, .col = 0 }; st.mode = .insert; st.msel_active = false; |
