diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 21:44:04 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 163531dce202e71977f4524cab65702606caca62 (patch) | |
| tree | d59a7f507c7ca81e6307e790e7f2584d9446ab67 /src | |
| parent | 988b288b870dd0165f09b991839c2c37fcd3fd62 (diff) | |
| parent | db2a259a18b9247edc23b1128dcfda703858f120 (diff) | |
| download | pardes-163531dce202e71977f4524cab65702606caca62.tar.gz pardes-163531dce202e71977f4524cab65702606caca62.zip | |
Release Pardes 0.13
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/CHANGELOG.md | 3 | ||||
| -rw-r--r-- | src/config.zig | 121 |
2 files changed, 123 insertions, 1 deletions
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index a1caffb4..8a5949b8 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog -## Unreleased +## 0.0.13 +- A one-page A4 cheatsheet, built from Typst sources in `docs/typ/` (`zig build cheatsheet`); a test fails when it names a word, key or tag that no longer exists. - A look on a diff line is the look of the `path:line` it names, exactly as if that text were selected by hand: the same resolution, placement and errors. A patch cached outside any checkout opens the file the way a diff --git a/src/config.zig b/src/config.zig index 6bb4d6e4..d3da47c1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -2027,3 +2027,124 @@ test { _ = Runtime; _ = User; } + +/// The docs in docs/typ name words, keys and tags; each must still be here. +const doc_typ = struct { + /// The string argument of every `name("...")` call in a source. + fn calls(gpa: std.mem.Allocator, source: []const u8, comptime name: []const u8, out: *std.ArrayList([]const u8)) !void { + var at: usize = 0; + while (std.mem.indexOfPos(u8, source, at, name ++ "(\"")) |i| { + at = i + name.len + 2; + if (i > 0 and (std.ascii.isAlphanumeric(source[i - 1]) or source[i - 1] == '-')) continue; + const end = std.mem.indexOfScalarPos(u8, source, at, '"') orelse return error.Unterminated; + try out.append(gpa, source[at..end]); + } + } + + fn word(w: []const u8) bool { + const name = w[0 .. std.mem.indexOfScalar(u8, w, '+') orelse w.len]; + return std.meta.stringToEnum(Builtin, name) != null; + } + + fn chord(tok: []const u8) ?Chord { + var c: Chord = .{ .cp = 0 }; + var rest = tok; + while (rest.len > 2) { + if (std.mem.startsWith(u8, rest, "Ctrl-")) c.ctrl = true else if (std.mem.startsWith(u8, rest, "Alt-")) c.alt = true else if (std.mem.startsWith(u8, rest, "Shift-")) c.shift = true else break; + rest = rest[std.mem.indexOfScalar(u8, rest, '-').? + 1 ..]; + } + const named = [_]struct { []const u8, u21 }{ .{ "Esc", Key.escape }, .{ "Enter", Key.enter }, .{ "Tab", Key.tab }, .{ "SPC", ' ' } }; + for (named) |n| if (std.mem.eql(u8, rest, n[0])) { + c.cp = n[1]; + return c; + }; + if (rest.len != 1) return null; + c.cp = rest[0]; + return c; + } + + fn same(a: Chord, b: Chord) bool { + return a.cp == b.cp and a.ctrl == b.ctrl and a.alt == b.alt and a.shift == b.shift; + } + + fn hasChordField(T: type) bool { + if (@typeInfo(T) != .array or @typeInfo(@typeInfo(T).array.child) != .@"struct") return false; + for (@typeInfo(@typeInfo(T).array.child).@"struct".fields) |f| if (f.type == Chord) return true; + return false; + } + + /// Bound somewhere in this file: a binding, a prefix or a key list. + fn bound(c: Chord) bool { + const config = @import("config.zig"); + inline for (@typeInfo(config).@"struct".decls) |d| { + const v = @field(config, d.name); + const T = @TypeOf(v); + if (T == []const Chord) { + for (v) |b| if (same(b, c)) return true; + } else if (T == comptime_int or T == u21 or T == u8) { + // a prefix or a bare key; only the tty toggle carries Ctrl + const plain = !c.alt and !c.shift and (!c.ctrl or comptime std.mem.indexOf(u8, d.name, "toggle") != null); + if (plain and v >= 0x20 and v < 0x7f and v == c.cp) return true; + } else if (comptime hasChordField(T)) { + for (v) |e| inline for (@typeInfo(@TypeOf(e)).@"struct".fields) |f| + if (f.type == Chord and same(@field(e, f.name), c)) return true; + } + } + return false; + } + + fn key(k: []const u8) bool { + var toks = std.mem.tokenizeScalar(u8, k, ' '); + const first = toks.next() orelse return false; + if (std.mem.eql(u8, first, "SPC")) { + var path: [8]u8 = undefined; + var n: usize = 0; + while (toks.next()) |t| : (n += 1) { + if (t.len != 1 or n == path.len) return false; + path[n] = t[0]; + } + if (n == 0) return leader.len == 1 and leader[0].cp == ' '; + for (std.enums.values(Builtin)) |b| if (leader_path.get(b)) |p| + if (std.mem.eql(u8, p, path[0..n])) return true; + return false; + } + toks.reset(); + while (toks.next()) |t| if (!bound(chord(t) orelse return false)) return false; + return true; + } + + fn tag(t: []const u8) bool { + var norm: [256]u8 = undefined; + var w: std.Io.Writer = .fixed(&norm); + var words = std.mem.tokenizeScalar(u8, t, ' '); + while (words.next()) |x| { + if (w.end > 0) w.writeByte(' ') catch return false; + w.writeAll(x[0 .. std.mem.indexOfScalar(u8, x, '+') orelse x.len]) catch return false; + } + const tags = [_][]const u8{ topbar_str, @import("tagline.zig").column_tag_default, pane_builtins_str, pdf_pane_builtins_str, file_pane_builtins_str, terminal_pane_builtins_str, command_pane_builtins_str }; + for (tags) |d| if (std.mem.eql(u8, d, w.buffered())) return true; + return false; + } +}; + +test "docs/typ names only words, keys and default tags that exist" { + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const gpa = arena.allocator(); + const io = std.testing.io; + var dir = try std.Io.Dir.cwd().openDir(io, @import("doc_typ").dir, .{ .iterate = true }); + defer dir.close(io); + var found: [3]std.ArrayList([]const u8) = @splat(.empty); + var it = dir.iterate(); + while (try it.next(io)) |entry| if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".typ")) { + const source = try dir.readFileAlloc(io, entry.name, gpa, .limited(1 << 20)); + inline for (.{ "word", "key", "tag" }, &found) |name, *list| try doc_typ.calls(gpa, source, name, list); + }; + inline for (.{ "word", "key", "tag" }, found) |name, list| { + try std.testing.expect(list.items.len >= 5); + for (list.items) |arg| if (!@field(doc_typ, name)(arg)) { + std.debug.print("docs/typ: {s}(\"{s}\") is not in pardes\n", .{ name, arg }); + return error.DocDrift; + }; + } +} |
