summaryrefslogtreecommitdiff
path: root/src/modal.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-16 15:49:12 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:44:42 -0300
commit1551e409c31992437cb2fa864f576d45c8433801 (patch)
treee2fae8451f87b735a1360c7c2e383fdc40165789 /src/modal.zig
parentbe2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff)
downloadpardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz
pardes-1551e409c31992437cb2fa864f576d45c8433801.zip
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/modal.zig')
-rw-r--r--src/modal.zig412
1 files changed, 0 insertions, 412 deletions
diff --git a/src/modal.zig b/src/modal.zig
index 33c63c52..6ab7ee35 100644
--- a/src/modal.zig
+++ b/src/modal.zig
@@ -204,15 +204,6 @@ pub fn gotoLast(lines: []const []const u8) Cursor {
return .{ .row = r, .col = 0 };
}
-// half/full page: target row, caller clamps + scrolls.
-pub fn pageDown(c: Cursor, page: usize) Cursor {
- return .{ .row = c.row + page, .col = c.col };
-}
-
-pub fn pageUp(c: Cursor, page: usize) Cursor {
- return .{ .row = if (c.row > page) c.row - page else 0, .col = c.col };
-}
-
// the character the cursor sits on; line terminators / EOF read as '\n'.
fn charAt(lines: []const []const u8, c: Cursor) u8 {
if (c.row >= lines.len) return '\n';
@@ -1329,411 +1320,8 @@ test "hx increment" {
try std.testing.expectEqual(@as(?[]u8, null), try hxIncrement(a, "", 1));
}
-// ---- retired standalone file-pane modal prototype ----
-// This old pure-std model has no production or harness callers. It remains
-// only as context for the primitive algorithms around it; normal_input.zig is
-// the sole BODY-NORMAL recognizer and Pardes owns Action execution.
-
-pub const Key = union(enum) {
- ch: u21, // a printable codepoint (insert inserts its UTF-8 bytes)
- esc,
- enter,
- backspace,
- left,
- right,
- up,
- down,
- home,
- end,
- page_down,
- page_up,
- half_down, // Ctrl-d
- half_up, // Ctrl-u
-};
-
-pub const FileMode = enum { normal, insert };
-
-pub const FileState = struct {
- cur: Cursor = .{},
- mode: FileMode = .normal,
- pending: u21 = 0, // prefix codepoint in flight: 'g' or 'z'
- msel_active: bool = false,
- msel_r0: i32 = 0,
- msel_r1: i32 = 0,
-};
-
-// The view the cursor must stay inside. `scroll` is read/written so viewport
-// alignment (zt/zz/zb) and page motions can adjust it; the app syncs it back to
-// the file pane's scroll. The unit-test Sim passes a large rows + a dummy scroll
-// so visibility is a no-op (content is what's asserted).
-pub const View = struct {
- rows: usize,
- scroll: *usize,
-};
-
-fn chEq(key: Key, cp: u21) bool {
- return switch (key) {
- .ch => |c| c == cp,
- else => false,
- };
-}
-
-fn tagOf(key: Key) std.meta.Tag(Key) {
- return std.meta.activeTag(key);
-}
-
-fn splitLines(alloc: std.mem.Allocator, content: []const u8) ![][]const u8 {
- 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');
- 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 {
- const s = view.scroll.*;
- if (row < s) {
- view.scroll.* = row;
- return;
- }
- const last = s +| (view.rows -| 1);
- if (row > last) view.scroll.* = row -| (view.rows -| 1);
-}
-
-// helix `x`: start a line selection at the cursor, or extend it down by one.
-fn lineSelectFile(st: *FileState, lines: []const []const u8) void {
- if (!st.msel_active) {
- st.msel_active = true;
- st.msel_r0 = @intCast(st.cur.row);
- st.msel_r1 = @intCast(st.cur.row);
- } else {
- const last: i32 = @intCast(if (lines.len == 0) 0 else lines.len - 1);
- st.msel_r1 = @min(st.msel_r1 + 1, last);
- st.cur.row = @intCast(st.msel_r1);
- }
- st.cur.col = 0;
- st.pending = 0;
-}
-
-// `d`: delete the selected lines (yanking them), or the char at the cursor.
-fn deleteFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, lines: []const []const u8, yank: *?[]u8) !?[]u8 {
- if (st.msel_active) {
- 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)));
- 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;
- return d.content;
- }
- // no selection: delete the char at the cursor (no-op if past end)
- const llen = lineLenOf(lines, st.cur.row);
- if (st.cur.col >= llen) {
- st.pending = 0;
- return null;
- }
- const new = try deleteChar(alloc, content, st.cur);
- st.pending = 0;
- return new;
-}
-
-// `y`: yank the selected lines, or the current line. Cursor moves to the
-// selection start; the selection is collapsed.
-fn yankFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, yank: *?[]u8) !void {
- 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)));
- const new_yank = try joinedLineRangeAlloc(alloc, content, r0, r1);
- if (yank.*) |old| alloc.free(old);
- yank.* = new_yank;
- } else {
- const new_yank = try alloc.dupe(u8, lineSlice(content, st.cur.row));
- if (yank.*) |old| alloc.free(old);
- yank.* = new_yank;
- }
- st.pending = 0;
-}
-
-// `p`: paste the yanked text as a new line below the cursor.
-fn pasteFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, yank: *?[]u8) !?[]u8 {
- const y = yank.* orelse {
- st.pending = 0;
- return null;
- };
- const new = try pasteLineBelow(alloc, content, st.cur.row, y);
- st.cur = .{ .row = st.cur.row + 1, .col = 0 };
- st.pending = 0;
- return new;
-}
-
-// `c`: change the selection (delete + enter INSERT, keeping one empty line), or
-// change the char at the cursor. Yanks the removed text.
-fn changeFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, lines: []const []const u8, yank: *?[]u8) !?[]u8 {
- 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)));
- // yank the original lines first (from the un-mutated content)
- 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;
- st.pending = 0;
- return cl;
- }
- // no selection: delete the char at the cursor (if any), then enter INSERT
- const llen = lineLenOf(lines, st.cur.row);
- if (st.cur.col >= llen) {
- st.mode = .insert;
- st.msel_active = false;
- st.pending = 0;
- return null;
- }
- const new = try deleteChar(alloc, content, st.cur);
- st.mode = .insert;
- st.msel_active = false;
- st.pending = 0;
- return new;
-}
-
-// INSERT-mode key on a file pane. Returns new content or null (no change).
-fn stepInsertFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, lines: []const []const u8, key: Key) !?[]u8 {
- switch (key) {
- .esc => {
- st.mode = .normal;
- st.msel_active = false;
- st.pending = 0;
- return null;
- },
- .ch => |cp| {
- if (cp == '\n') return try stepInsertFile(alloc, content, st, lines, .enter);
- var buf: [4]u8 = undefined;
- const n = std.unicode.utf8Encode(cp, &buf) catch return null;
- const new = try insertAt(alloc, content, st.cur, buf[0..n]);
- st.cur.col += n; // col is a byte offset; advance by the char's byte length
- st.pending = 0;
- return new;
- },
- .enter => {
- const indent = hxNewlineIndentWidth(lineSlice(content, st.cur.row), st.cur.col);
- const block = try alloc.alloc(u8, 1 + indent);
- defer alloc.free(block);
- block[0] = '\n';
- @memset(block[1..], ' ');
- const new = try insertAt(alloc, content, st.cur, block);
- st.cur = .{ .row = st.cur.row + 1, .col = indent };
- st.pending = 0;
- return new;
- },
- .backspace => {
- if (st.cur.col > 0) {
- const new = try deleteChar(alloc, content, .{ .row = st.cur.row, .col = st.cur.col - 1 });
- st.cur.col -= 1;
- return new;
- } else if (st.cur.row > 0) {
- const prevlen = lineSlice(content, st.cur.row - 1).len;
- const new = try deleteChar(alloc, content, .{ .row = st.cur.row - 1, .col = prevlen });
- st.cur = .{ .row = st.cur.row - 1, .col = prevlen };
- return new;
- }
- return null;
- },
- .left => {
- st.cur = charLeft(st.cur);
- return null;
- },
- .right => {
- st.cur = charRight(lines, st.cur);
- return null;
- },
- .up => {
- st.cur = lineUp(lines, st.cur);
- return null;
- },
- .down => {
- st.cur = lineDown(lines, st.cur);
- return null;
- },
- .home => {
- st.cur = lineStart(st.cur);
- return null;
- },
- .end => {
- st.cur = lineEnd(lines, st.cur);
- return null;
- },
- else => return null,
- }
-}
-
-// RETIRED LEGACY REFERENCE INTERPRETER. Nothing in production or the test
-// harness calls this mini-parser; BODY-NORMAL recognition lives exclusively
-// in normal_input.zig and Pardes executes its semantic Actions for text/PDF.
-// Keep this frozen only as old modal-algorithm context; do not add bindings or
-// infer current behavior from it.
-pub fn stepFile(alloc: std.mem.Allocator, content: []const u8, st: *FileState, view: View, key: Key, yank: *?[]u8) !?[]u8 {
- const lines = try splitLines(alloc, content);
- defer alloc.free(lines);
- if (st.mode == .insert) return try stepInsertFile(alloc, content, st, lines, key);
-
- var cur = clampToChar(lines, st.cur);
-
- // prefix continuations
- if (st.pending != 0) {
- const p = st.pending;
- st.pending = 0;
- if (p == 'g') {
- if (chEq(key, 'g')) cur = gotoFirst() else if (chEq(key, 'e')) cur = gotoLast(lines) else if (chEq(key, 'h')) cur = lineStart(cur) else if (chEq(key, 'l')) cur = lineEnd(lines, cur) else return null; // unknown continuation: consume, no-op
- st.cur = cur;
- st.msel_active = false;
- return null;
- }
- if (p == 'z') {
- if (chEq(key, 't')) {
- view.scroll.* = cur.row;
- } else if (chEq(key, 'z')) {
- view.scroll.* = @intCast(@max(0, @as(i32, @intCast(cur.row)) - @as(i32, @intCast(view.rows / 2))));
- } else if (chEq(key, 'b')) {
- view.scroll.* = @intCast(@max(0, @as(i32, @intCast(cur.row)) - @as(i32, @intCast(view.rows)) + 1));
- } else return null;
- st.msel_active = false;
- return null;
- }
- }
- if (chEq(key, 'g')) {
- st.pending = 'g';
- return null;
- }
- if (chEq(key, 'z')) {
- st.pending = 'z';
- return null;
- }
-
- // motions -> move cursor, collapse selection, keep visible
- var moved: ?Cursor = null;
- if (chEq(key, 'h') or tagOf(key) == .left) moved = charLeft(cur) else if (chEq(key, 'l') or tagOf(key) == .right) moved = charRight(lines, cur) else if (chEq(key, 'j') or tagOf(key) == .down) moved = lineDown(lines, cur) else if (chEq(key, 'k') or tagOf(key) == .up) moved = lineUp(lines, cur) else if (chEq(key, 'w')) moved = nextWordStart(lines, cur, false) else if (chEq(key, 'W')) moved = nextWordStart(lines, cur, true) else if (chEq(key, 'b')) moved = prevWordStart(lines, cur, false) else if (chEq(key, 'B')) moved = prevWordStart(lines, cur, true) else if (chEq(key, 'e')) moved = nextWordEnd(lines, cur, false) else if (chEq(key, 'E')) moved = nextWordEnd(lines, cur, true) else if (chEq(key, '0') or tagOf(key) == .home) moved = lineStart(cur) else if (chEq(key, '$') or tagOf(key) == .end) moved = lineEnd(lines, cur) else if (chEq(key, '^')) moved = firstNonWsOf(lines, cur) else if (chEq(key, 'G')) moved = gotoLast(lines) else if (tagOf(key) == .half_down) moved = clampToChar(lines, pageDown(cur, @max(1, view.rows / 2))) else if (tagOf(key) == .half_up) moved = clampToChar(lines, pageUp(cur, @max(1, view.rows / 2))) else if (tagOf(key) == .page_down) moved = clampToChar(lines, pageDown(cur, view.rows)) else if (tagOf(key) == .page_up) moved = clampToChar(lines, pageUp(cur, view.rows));
- if (moved) |m| {
- st.cur = m;
- st.msel_active = false;
- st.pending = 0;
- ensureVisible(view, m.row);
- return null;
- }
-
- // insert entry
- if (chEq(key, 'i')) {
- st.mode = .insert;
- st.cur = cur;
- st.msel_active = false;
- st.pending = 0;
- return null;
- }
- if (chEq(key, 'a')) {
- st.mode = .insert;
- st.cur = charRight(lines, cur);
- st.msel_active = false;
- st.pending = 0;
- return null;
- }
- if (chEq(key, 'I')) {
- st.mode = .insert;
- st.cur = firstNonWsOf(lines, cur);
- st.msel_active = false;
- st.pending = 0;
- return null;
- }
- if (chEq(key, 'A')) {
- st.mode = .insert;
- st.cur = .{ .row = cur.row, .col = lineLenOf(lines, cur.row) };
- st.msel_active = false;
- st.pending = 0;
- return null;
- }
- if (chEq(key, 'o')) {
- const new = try insertAt(alloc, content, .{ .row = cur.row, .col = lineLenOf(lines, cur.row) }, "\n");
- st.cur = .{ .row = cur.row + 1, .col = 0 };
- st.mode = .insert;
- st.msel_active = false;
- st.pending = 0;
- ensureVisible(view, st.cur.row);
- return new;
- }
- if (chEq(key, 'O')) {
- const new = try insertAt(alloc, content, .{ .row = cur.row, .col = 0 }, "\n");
- st.cur = .{ .row = cur.row, .col = 0 };
- st.mode = .insert;
- st.msel_active = false;
- st.pending = 0;
- ensureVisible(view, st.cur.row);
- return new;
- }
-
- // select / edit
- if (chEq(key, 'x')) {
- lineSelectFile(st, lines);
- return null;
- }
- if (chEq(key, 'd')) return try deleteFile(alloc, content, st, lines, yank);
- if (chEq(key, 'c')) return try changeFile(alloc, content, st, lines, yank);
- if (chEq(key, 'y')) {
- try yankFile(alloc, content, st, yank);
- return null;
- }
- if (chEq(key, 'p')) return try pasteFile(alloc, content, st, yank);
-
- // Enter / other keys: no-op on a file (no tty mode)
- return null;
-}
-
// ---- tests ----
-const testEq = struct {
- fn cur(lines: []const []const u8, s: []const u8) Cursor {
- var row: usize = 0;
- for (lines) |ln| {
- if (std.mem.eql(u8, ln, s)) return .{ .row = row, .col = 0 };
- row += 1;
- }
- unreachable;
- }
-};
-
test "kindOf" {
try std.testing.expectEqual(Kind.word, kindOf('a'));
try std.testing.expectEqual(Kind.word, kindOf('_'));