diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /src/modal.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-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.zig | 412 |
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('_')); |
