summaryrefslogtreecommitdiff
path: root/src/modal.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/modal.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig252
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;