summaryrefslogtreecommitdiff
path: root/src/modal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-31 07:32:15 -0300
committerGabriel Schneider <[email protected]>2026-08-01 15:02:08 -0300
commit560ae0a63f7b5d354bad3ba13dd8ab26b5f0a870 (patch)
treedb2762cc2bc7bdeaae187e15cfc68a33ef2d1c48 /src/modal.zig
parent5bf8d6dd077517270377e5d8551108ecf252374f (diff)
downloadpardes-560ae0a63f7b5d354bad3ba13dd8ab26b5f0a870.tar.gz
pardes-560ae0a63f7b5d354bad3ba13dd8ab26b5f0a870.zip
motion, scrolling and redraw are flat in file size now
zig build perf drives the core directly — event, effects, one frame, no pty — over four generated fixtures: 1k lines, 50k, 300k, and 400 lines of 8000 columns, because a file that is long and a file that is wide fail differently. Every sample seeks somewhere else in the file first, since measuring at line 3 of a 300k-line file hides exactly the bug. perf record said half the run was scanning for newlines from byte 0. So File carries a line index, built on demand and invalidated in exactly ONE place — setContent, the funnel every content swap already goes through. That killed the scrollbar's per-frame line count (12.6% of the whole run by itself), scrollBy, ensureCursorVisible, lastNavRow, the syntax window bounds and two O(scroll) walks. normalKey computed max_line as a const at the top: two full passes over the buffer on every keystroke of every kind, for three g/G branches. It is lazy now. The modal primitives each walked the text twice for the same line. And the visible window was re-parsed on every scrolled row — a third of a megabyte per keypress on the wide fixture. The highlighted range is remembered, a scroll inside it is free, and only a re-parse that FOLLOWS a scroll takes slack: doing it unconditionally made typing 2.1x slower, since every character paid for a band it could never amortise. One j on a 19 MB file: 37.8ms -> 266us. Render: 4.1ms -> 77us. Open costs 1.25x more for the one extra pass, which buys 54x on every frame after, and 8 bytes per line of memory. Left standing, measured and named: edit-char is 14ms on 19MB because content is immutable and every keystroke copies the buffer. A third of that is the index rebuild, which could be a shift if setContent knew the edit offset; the rest wants a rope. bodyText's double copy and Surface.print's per-cell decode never rose above 2% of the profile afterwards, so they were left alone. No golden moved.
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig21
1 files changed, 16 insertions, 5 deletions
diff --git a/src/modal.zig b/src/modal.zig
index 742dbc6f..cdc49142 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -438,7 +438,7 @@ pub fn lineStartOffset(content: []const u8, row: usize) usize {
var off: usize = 0;
var r: usize = 0;
while (r < row) : (r += 1) {
- const nl = std.mem.indexOfPos(u8, content, off, "\n") orelse return content.len;
+ const nl = std.mem.indexOfScalarPos(u8, content, off, '\n') orelse return content.len;
off = nl + 1;
}
return off;
@@ -775,15 +775,24 @@ pub fn hxLineEndIdx(text: []const u8, line: usize) usize {
/// gap offset -> (row, col) cell
pub fn hxPos(text: []const u8, off: usize) Cursor {
- const row = hxLineOf(text, off);
- return .{ .row = row, .col = @min(off, text.len) - lineStartOffset(text, row) };
+ const o = @min(off, text.len);
+ // The line start is the byte after the last '\n' BEFORE off, which is the
+ // same number lineStartOffset(text, row) walks the whole prefix to reach —
+ // one backward scan of a single line instead of a second pass over
+ // everything above the cursor. On a multi-MB buffer that second pass was
+ // most of what a keystroke cost.
+ const s = if (std.mem.lastIndexOfScalar(u8, text[0..o], '\n')) |nl| nl + 1 else 0;
+ return .{ .row = hxLineOf(text, off), .col = o - s };
}
/// (row, col) -> clamped gap offset; col == line length lands ON the '\n'
pub fn hxOff(text: []const u8, c: Cursor) usize {
const row = @min(c.row, hxLineCount(text) - 1);
const s = lineStartOffset(text, row);
- return @min(s + c.col, hxLineEndIdx(text, row));
+ // hxLineEndIdx(text, row) inlined: it starts by walking to `row` again,
+ // and we are already standing there
+ const e = std.mem.indexOfScalarPos(u8, text, s, '\n') orelse text.len;
+ return @min(s + c.col, e);
}
pub const WordTarget = enum {
@@ -979,7 +988,9 @@ pub fn hxVertTarget(text: []const u8, pos: usize, down: bool, count: usize, goal
const line = hxLineOf(text, pos);
const nline = if (down) @min(line + @max(1, count), nlines - 1) else line -| @max(1, count);
const s = lineStartOffset(text, nline);
- return @min(s + goal_col, hxLineEndIdx(text, nline));
+ // hxLineEndIdx(text, nline) without its second walk to nline (see hxOff)
+ const e = std.mem.indexOfScalarPos(u8, text, s, '\n') orelse text.len;
+ return @min(s + goal_col, e);
}
/// f/F/t/T target cell. helix find_char: the exclusive (till) search starts