From f058514afb2f0e62a46158dc8c361c99ecd5149c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Wed, 30 Sep 2026 23:04:26 -0300 Subject: The tutor follows the guide's order, points at its sections, and no longer teaches $ as line end or the terminal tag's old words; a test checks the words, paths, chords and tags it names Co-Authored-By: Claude Opus 5.5 --- src/config.zig | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) (limited to 'src/config.zig') diff --git a/src/config.zig b/src/config.zig index d3da47c1..53def6a1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -661,7 +661,7 @@ pub const Runtime = struct { inactive_dim: u8 = 0, /// The grip's button and the scrollbar under it, one width, as a percent /// of the theme's `rail_px` (acme's 12px Scrollwid at a 17px tagline): - /// a pixel shell's (docs/themes.md). + /// a pixel shell's (docs/typ/themes.typ). grip_width: u16 = 150, /// How the fx track's animations move (animation.Motion): off for /// reduced motion. @@ -2148,3 +2148,125 @@ test "docs/typ names only words, keys and default tags that exist" { }; } } + +/// The tutor's own spellings, which the docs/typ test cannot read: a +/// builtin in backticks (`Save`, `Kill make`), a tag on a line of its own +/// after "| ", a leader path after SPC (`SPC f s`), a chord as Ctrl-, Alt- +/// or Shift- and one key. +const doc_tutor = struct { + /// Read before the bindings in this file: a raw terminal's paste + /// (Pardes.handleKey). + const elsewhere = [_]Chord{ .{ .cp = 'V', .ctrl = true }, .{ .cp = 'V', .ctrl = true, .shift = true } }; + + /// The first word in each pair of backticks that looks like a builtin: + /// a capital, then letters and digits with a lower-case one among them. + fn words(gpa: std.mem.Allocator, text: []const u8, out: *std.ArrayList([]const u8)) !void { + var it = std.mem.splitScalar(u8, text, '`'); + _ = it.next(); // before the first backtick + while (it.next()) |inside| { + _ = it.next() orelse break; // what follows the closing one + const first = inside[0 .. std.mem.indexOfAny(u8, inside, " +") orelse inside.len]; + if (first.len < 2 or !std.ascii.isUpper(first[0])) continue; + var lower = false; + for (first) |c| { + if (!std.ascii.isAlphanumeric(c)) break; + lower = lower or std.ascii.isLower(c); + } else if (lower) try out.append(gpa, first); + } + } + + /// The keys pressed after each SPC, up to the first that is no single + /// key: a path, or the start of one. + fn paths(gpa: std.mem.Allocator, text: []const u8, out: *std.ArrayList([]const u8)) !void { + var toks = std.mem.tokenizeAny(u8, text, " \n"); + while (toks.next()) |t| { + if (!std.mem.eql(u8, t, "SPC")) continue; + var keys: std.ArrayList(u8) = .empty; + while (toks.peek()) |k| { + // `o,` and `j)` end a path written in a sentence + const ends = k.len == 2 and std.mem.indexOfScalar(u8, ",.;:)", k[1]) != null; + if ((k.len != 1 and !ends) or k[0] == '/') break; + try keys.append(gpa, k[0]); + _ = toks.next(); + if (ends) break; + } + if (keys.items.len > 0) try out.append(gpa, keys.items); + } + } + + fn path(keys: []const u8) bool { + for (std.enums.values(Builtin)) |b| if (leader_path.get(b)) |p| + if (std.mem.startsWith(u8, p, keys)) return true; + return false; + } + + /// Every Ctrl-, Alt- or Shift- chord: its modifiers, then one key. + fn chords(gpa: std.mem.Allocator, text: []const u8, out: *std.ArrayList(Chord)) !void { + const named = [_]struct { []const u8, u21 }{ .{ "Esc", Key.escape }, .{ "Enter", Key.enter }, .{ "Tab", Key.tab } }; + var i: usize = 0; + while (i < text.len) : (i += 1) { + if (i > 0 and std.ascii.isAlphanumeric(text[i - 1])) continue; + var c: Chord = .{ .cp = 0 }; + var at = i; + while (true) { + const rest = text[at..]; + 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; + at += std.mem.indexOfScalar(u8, rest, '-').? + 1; + } + if (at == i or at >= text.len) continue; + c.cp = text[at]; + for (named) |n| if (std.mem.startsWith(u8, text[at..], n[0])) { + c.cp = n[1]; + }; + try out.append(gpa, c); + i = at; + } + } +}; + +test "the tutor names only words, keys, paths and default tags that exist" { + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const gpa = arena.allocator(); + const tutor = @embedFile("tutor.txt"); + + var found: std.ArrayList([]const u8) = .empty; + try doc_tutor.words(gpa, tutor, &found); + try std.testing.expect(found.items.len >= 20); + for (found.items) |w| if (!doc_typ.word(w)) { + std.debug.print("tutor.txt: `{s}` is no builtin\n", .{w}); + return error.DocDrift; + }; + + var tags: usize = 0; + var lines = std.mem.splitScalar(u8, tutor, '\n'); + while (lines.next()) |line| if (std.mem.startsWith(u8, line, "| ")) { + tags += 1; + if (!doc_typ.tag(line[2..])) { + std.debug.print("tutor.txt: \"{s}\" is no default tag\n", .{line}); + return error.DocDrift; + } + }; + try std.testing.expect(tags >= 5); + + found.clearRetainingCapacity(); + try doc_tutor.paths(gpa, tutor, &found); + try std.testing.expect(found.items.len >= 10); + for (found.items) |p| if (!doc_tutor.path(p)) { + std.debug.print("tutor.txt: SPC {s} leads nowhere\n", .{p}); + return error.DocDrift; + }; + + var chords: std.ArrayList(Chord) = .empty; + try doc_tutor.chords(gpa, tutor, &chords); + try std.testing.expect(chords.items.len >= 10); + for (chords.items) |c| { + const elsewhere = for (doc_tutor.elsewhere) |e| { + if (doc_typ.same(e, c)) break true; + } else false; + if (!elsewhere and !doc_typ.bound(c)) { + std.debug.print("tutor.txt: a chord on {u} (ctrl {} alt {} shift {}) is bound nowhere\n", .{ c.cp, c.ctrl, c.alt, c.shift }); + return error.DocDrift; + } + } +} -- cgit v1.3