summaryrefslogtreecommitdiff
path: root/tutor_gen.zig
diff options
context:
space:
mode:
Diffstat (limited to 'tutor_gen.zig')
-rw-r--r--tutor_gen.zig386
1 files changed, 0 insertions, 386 deletions
diff --git a/tutor_gen.zig b/tutor_gen.zig
deleted file mode 100644
index 09875e64..00000000
--- a/tutor_gen.zig
+++ /dev/null
@@ -1,386 +0,0 @@
-const std = @import("std");
-const modal = @import("modal.zig");
-
-// tutor_gen: a separate binary (NOT comptime) that parses tutor.txt and writes
-// tutor_cases.zig — a file of standard Zig `test` blocks, one per practice block.
-// `zig build tutor-gen` runs it (writing the file); `zig build unit-test` then
-// compiles+runs those tests against modal.stepFile.
-//
-// Format of tutor.txt practice blocks (all directive lines start with "# "):
-// # keys: l l l i X esc <- space-separated keystroke tokens
-// # at: 0,4 <- optional starting cursor "row,col" (default 0,0)
-// # before <- the lines below are the starting buffer
-// <clean line>
-// # after <- the lines below are the expected buffer
-// <clean line>
-// Everything not under a # before/# after is prose (ignored). A `# keys:` starts
-// a new case; cases end at the next `# keys:` or a non-practice line.
-//
-// Keystroke tokens: each char of a token is typed in turn, EXCEPT the named
-// specials `esc`, `enter`, `bs` (backspace), `left/right/up/down`, `home/end`,
-// `pgup/pgdn`. `Ctrl-d`/`Ctrl-u`/`Ctrl-f`/`Ctrl-b` map to half/full page.
-
-const Case = struct {
- keys: []const u8,
- at: ?[]const u8 = null,
- before: []const u8,
- after: []const u8,
-};
-
-fn parseCases(alloc: std.mem.Allocator, src: []const u8) ![]Case {
- var cases: std.ArrayList(Case) = .empty;
- var it = std.mem.splitScalar(u8, src, '\n');
- // accumulate the current case
- var cur_keys: ?[]const u8 = null;
- var cur_at: ?[]const u8 = null;
- var before: ?std.ArrayList(u8) = null;
- var after: ?std.ArrayList(u8) = null;
- // which section is actively collecting content (so # after stops before from
- // absorbing the after lines). `none` = neither.
- var section: enum { none, before, after } = .none;
- defer if (before) |*b| b.deinit(alloc);
- defer if (after) |*a| a.deinit(alloc);
- // NOTE: cur_keys / cur_at ownership is handled explicitly at each flush/drop
- // (moved into a case, or freed + nulled). We do NOT defer-free them here:
- // a defer would double-free a value that was moved into a case (the move
- // leaves the local pointing at the now-case-owned memory until it's nulled,
- // and a function-exit defer can't tell).
-
- while (it.next()) |raw_line| {
- // Practice-block lines in tutor.txt are indented by one leading space
- // (the tutor's prose style). Strip exactly that one space so directives
- // match AND content lines don't carry the indent into the buffer.
- var line = raw_line;
- if (line.len > 0 and line[0] == ' ') line = line[1..];
- line = std.mem.trimEnd(u8, line, "\r");
- if (std.mem.startsWith(u8, line, "# keys:")) {
- // flush previous case
- if (cur_keys != null and before != null and after != null) {
- try cases.append(alloc, .{
- .keys = cur_keys.?,
- .at = cur_at,
- .before = try before.?.toOwnedSlice(alloc),
- .after = try after.?.toOwnedSlice(alloc),
- });
- before = null;
- after = null;
- } else if (cur_keys != null) {
- // a # keys: with no before/after — drop it (incomplete): free the
- // owned keys/at so they don't leak.
- if (cur_keys) |k| alloc.free(k);
- if (cur_at) |at| alloc.free(at);
- before = null;
- after = null;
- }
- cur_keys = try alloc.dupe(u8, std.mem.trim(u8, line["# keys:".len..], " "));
- // cur_at from a prior case was either moved (flushed) or freed (drop);
- // start fresh.
- cur_at = null;
- before = null;
- after = null;
- section = .none;
- continue;
- }
- if (std.mem.startsWith(u8, line, "# at:")) {
- if (cur_at) |old| alloc.free(old);
- cur_at = try alloc.dupe(u8, std.mem.trim(u8, line["# at:".len..], " "));
- continue;
- }
- if (std.mem.startsWith(u8, line, "# before")) {
- before = .empty;
- after = null;
- section = .before;
- continue;
- }
- if (std.mem.startsWith(u8, line, "# after")) {
- after = .empty;
- section = .after;
- continue;
- }
- // any other "# ..." directive that isn't keys/at/before/after ends a case
- if (std.mem.startsWith(u8, line, "#")) {
- if (cur_keys != null and before != null and after != null and after.?.items.len > 0) {
- try cases.append(alloc, .{
- .keys = cur_keys.?,
- .at = cur_at,
- .before = try before.?.toOwnedSlice(alloc),
- .after = try after.?.toOwnedSlice(alloc),
- });
- } else {
- // incomplete: free the owned keys/at + any partial content
- if (cur_keys) |k| alloc.free(k);
- if (cur_at) |at| alloc.free(at);
- if (before) |*b| b.deinit(alloc);
- if (after) |*a| a.deinit(alloc);
- }
- cur_keys = null;
- cur_at = null;
- before = null;
- after = null;
- section = .none;
- continue;
- }
- // a blank line: if we have a complete case (keys + before + after with
- // content), flush it now — the practice block is delimited by blank lines
- // in the tutor, so prose after the blank line must NOT be absorbed.
- if (line.len == 0) {
- if (cur_keys != null and before != null and after != null and after.?.items.len > 0) {
- try cases.append(alloc, .{
- .keys = cur_keys.?,
- .at = cur_at,
- .before = try before.?.toOwnedSlice(alloc),
- .after = try after.?.toOwnedSlice(alloc),
- });
- cur_keys = null;
- before = null;
- after = null;
- section = .none;
- } else {
- section = .none; // a blank line closes the active section
- }
- continue;
- }
- // a content line: belongs to the ACTIVE section only
- if (section == .before) {
- if (before) |*b| {
- if (b.items.len > 0) try b.append(alloc, '\n');
- try b.appendSlice(alloc, line);
- }
- continue;
- }
- if (section == .after) {
- if (after) |*a| {
- if (a.items.len > 0) try a.append(alloc, '\n');
- try a.appendSlice(alloc, line);
- }
- continue;
- }
- // prose line, ignore
- }
- // flush trailing case
- if (cur_keys != null and before != null and after != null and after.?.items.len > 0) {
- try cases.append(alloc, .{
- .keys = cur_keys.?,
- .at = cur_at,
- .before = try before.?.toOwnedSlice(alloc),
- .after = try after.?.toOwnedSlice(alloc),
- });
- // ownership transferred; clear so the defers above don't double-free.
- cur_keys = null;
- cur_at = null;
- before = null;
- after = null;
- }
- return cases.toOwnedSlice(alloc);
-}
-
-// Emit a Zig string literal for `s`, escaping per Zig string syntax.
-fn emitStringLiteral(out: *std.ArrayList(u8), alloc: std.mem.Allocator, s: []const u8) !void {
- try out.append(alloc, '"');
- for (s) |c| {
- switch (c) {
- '"' => try out.appendSlice(alloc, "\\\""),
- '\\' => try out.appendSlice(alloc, "\\\\"),
- '\n' => try out.appendSlice(alloc, "\\n"),
- '\r' => try out.appendSlice(alloc, "\\r"),
- '\t' => try out.appendSlice(alloc, "\\t"),
- else => {
- if (c >= 0x20 and c < 0x7f) {
- try out.append(alloc, c);
- } else {
- try out.print(alloc, "\\x{x:0>2}", .{c});
- }
- },
- }
- }
- try out.append(alloc, '"');
-}
-
-fn writeCases(io: std.Io, alloc: std.mem.Allocator, cases: []const Case, out_path: []const u8) !void {
- // Build the generated source in a buffer, then write it in one shot.
- var out: std.ArrayList(u8) = .empty;
- defer out.deinit(alloc);
-
- try out.appendSlice(alloc, "// AUTO-GENERATED by tutor_gen from tutor.txt. DO NOT EDIT.\n");
- try out.appendSlice(alloc, "// One `test` block per practice block in the tutor. Re-run: zig build tutor-gen\n");
- try out.appendSlice(alloc, "const std = @import(\"std\");\n");
- try out.appendSlice(alloc, "const modal = @import(\"modal\");\n\n");
-
- for (cases, 0..) |c, i| {
- // Derive a short human label from the keys for the test name. Sanitize
- // it to a valid Zig identifier fragment (spaces/specials -> '_').
- var name_buf: [128]u8 = undefined;
- const printed = std.fmt.bufPrint(&name_buf, "tutor_case_{d:0>2}_{s}", .{ i, c.keys }) catch "tutor_case";
- var san_buf: [128]u8 = undefined;
- const san_len = @min(printed.len, san_buf.len);
- for (printed[0..san_len], 0..) |ch, j| {
- san_buf[j] = switch (ch) {
- 'a'...'z', 'A'...'Z', '0'...'9', '_' => ch,
- else => '_',
- };
- }
- const name_san = san_buf[0..san_len];
- try out.print(alloc, "test \"{s}\" {{\n", .{name_san});
- try out.appendSlice(alloc, " const a = std.testing.allocator;\n");
- try out.appendSlice(alloc, " var yank: ?[]u8 = null;\n");
- try out.appendSlice(alloc, " defer if (yank) |y| a.free(y);\n");
- try out.appendSlice(alloc, " var scroll: usize = 0;\n");
- try out.appendSlice(alloc, " var st = modal.FileState{};\n");
- try out.appendSlice(alloc, " const view = modal.View{ .rows = 200, .scroll = &scroll };\n");
-
- // starting content
- try out.appendSlice(alloc, " var content: []u8 = try a.dupe(u8, ");
- try emitStringLiteral(&out, alloc, c.before);
- try out.appendSlice(alloc, ");\n");
- try out.appendSlice(alloc, " defer a.free(content);\n");
-
- // starting cursor
- if (c.at) |at| {
- try out.print(alloc, " st.cur = .{{ .row = {s}, .col = {s} }};\n", .{
- splitField(at, 0),
- splitField(at, 1),
- });
- }
-
- // keystrokes
- var tit = std.mem.tokenizeAny(u8, c.keys, " ");
- while (tit.next()) |tok| {
- try emitKey(&out, alloc, tok);
- }
-
- // assertion
- try out.appendSlice(alloc, " try std.testing.expectEqualStrings(");
- try emitStringLiteral(&out, alloc, c.after);
- try out.appendSlice(alloc, ", content);\n");
- try out.appendSlice(alloc, "}\n\n");
- }
- try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = out_path, .data = out.items });
-}
-
-// "row,col" -> the row part (idx 0) or col part (idx 1).
-fn splitField(at: []const u8, idx: usize) []const u8 {
- var it = std.mem.splitScalar(u8, at, ',');
- var i: usize = 0;
- while (it.next()) |part| : (i += 1) {
- if (i == idx) return std.mem.trim(u8, part, " ");
- }
- return "0";
-}
-
-// Emit the stepFile calls for one keystroke token.
-// One stepFile call, as a generated line. `key_expr` is the Zig Key payload
-// expression (e.g. `.esc`, `.ch = 'x'`).
-fn emitStep(out: *std.ArrayList(u8), alloc: std.mem.Allocator, key_expr: []const u8) !void {
- try out.print(alloc, " {{ const n = try modal.stepFile(a, content, &st, view, {s}, &yank); if (n) |nn| {{ a.free(content); content = nn; }} }}\n", .{key_expr});
-}
-
-fn emitKey(out: *std.ArrayList(u8), alloc: std.mem.Allocator, tok: []const u8) !void {
- // named specials -> a single stepFile with that key
- if (std.mem.eql(u8, tok, "esc")) return emitStep(out, alloc, ".esc");
- if (std.mem.eql(u8, tok, "enter")) return emitStep(out, alloc, ".enter");
- if (std.mem.eql(u8, tok, "bs")) return emitStep(out, alloc, ".backspace");
- if (std.mem.eql(u8, tok, "left")) return emitStep(out, alloc, ".left");
- if (std.mem.eql(u8, tok, "right")) return emitStep(out, alloc, ".right");
- if (std.mem.eql(u8, tok, "up")) return emitStep(out, alloc, ".up");
- if (std.mem.eql(u8, tok, "down")) return emitStep(out, alloc, ".down");
- if (std.mem.eql(u8, tok, "home")) return emitStep(out, alloc, ".home");
- if (std.mem.eql(u8, tok, "end")) return emitStep(out, alloc, ".end");
- if (std.mem.eql(u8, tok, "pgdn") or std.mem.eql(u8, tok, "pagedown")) return emitStep(out, alloc, ".page_down");
- if (std.mem.eql(u8, tok, "pgup") or std.mem.eql(u8, tok, "pageup")) return emitStep(out, alloc, ".page_up");
- if (std.mem.eql(u8, tok, "Ctrl-d")) return emitStep(out, alloc, ".half_down");
- if (std.mem.eql(u8, tok, "Ctrl-u")) return emitStep(out, alloc, ".half_up");
- if (std.mem.eql(u8, tok, "Ctrl-f")) return emitStep(out, alloc, ".page_down");
- if (std.mem.eql(u8, tok, "Ctrl-b")) return emitStep(out, alloc, ".page_up");
- // otherwise: each byte of the token is a typed char (in order)
- for (tok) |c| {
- var expr_buf: [32]u8 = undefined;
- const expr = std.fmt.bufPrint(&expr_buf, ".{{ .ch = {d} }}", .{c}) catch return;
- try emitStep(out, alloc, expr);
- }
-}
-
-pub fn main(init: std.process.Init) !void {
- const io = init.io;
- const gpa = init.gpa;
- const args = try init.minimal.args.toSlice(init.arena.allocator());
- const in_path = if (args.len > 1) args[1] else "tutor.txt";
- const out_path = if (args.len > 2) args[2] else "tutor_cases.zig";
-
- const src = try std.Io.Dir.cwd().readFileAlloc(io, in_path, gpa, .limited(1 << 20));
- defer gpa.free(src);
-
- const cases = try parseCases(gpa, src);
- defer {
- for (cases) |c| {
- gpa.free(c.keys);
- if (c.at) |at| gpa.free(at);
- gpa.free(c.before);
- gpa.free(c.after);
- }
- gpa.free(cases);
- }
-
- try writeCases(io, gpa, cases, out_path);
- std.debug.print("tutor_gen: wrote {d} test cases to {s}\n", .{ cases.len, out_path });
-}
-
-// ---- self-test: the parser on tiny fixtures ----
-test "parseCases extracts a before/after block" {
- const a = std.testing.allocator;
- const src =
- \\# keys: l l l
- \\# before
- \\abcdef
- \\# after
- \\abcdef
- \\
- ;
- const cases = try parseCases(a, src);
- defer {
- for (cases) |c| {
- a.free(c.keys);
- if (c.at) |at| a.free(at);
- a.free(c.before);
- a.free(c.after);
- }
- a.free(cases);
- }
- try std.testing.expectEqual(@as(usize, 1), cases.len);
- try std.testing.expectEqualStrings("l l l", cases[0].keys);
- try std.testing.expectEqualStrings("abcdef", cases[0].before);
- try std.testing.expectEqualStrings("abcdef", cases[0].after);
-}
-
-test "parseCases handles # at: and multiple cases" {
- const a = std.testing.allocator;
- const src =
- \\# keys: b i Z esc
- \\# at: 0,4
- \\# before
- \\foo bar
- \\# after
- \\Zfoo bar
- \\
- \\# keys: x d
- \\# before
- \\keep
- \\gone
- \\# after
- \\keep
- \\
- ;
- const cases = try parseCases(a, src);
- defer {
- for (cases) |c| {
- a.free(c.keys);
- if (c.at) |at| a.free(at);
- a.free(c.before);
- a.free(c.after);
- }
- a.free(cases);
- }
- try std.testing.expectEqual(@as(usize, 2), cases.len);
- try std.testing.expectEqualStrings("0,4", cases[0].at.?);
- try std.testing.expectEqualStrings("keep\ngone", cases[1].before);
- try std.testing.expectEqualStrings("keep", cases[1].after);
-}