diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 19:03:48 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 4cea4751960b359561997cb040f05179923cb80e (patch) | |
| tree | 19834dde787a2fc4315211f55c20421f183b79d7 /src/ninep/ctl.zig | |
| parent | 8ce3e3c62a22d0e70df4f241ff28de6f6cae78d3 (diff) | |
| download | pardes-4cea4751960b359561997cb040f05179923cb80e.tar.gz pardes-4cea4751960b359561997cb040f05179923cb80e.zip | |
A write of an Edit block reaches Edit whole, on a pane's ctl, the root's and exec
Every write was split into lines, so a { group or an a/c/i text block written to ctl or exec became lines of their own: i/.../ and } ran as shell command panes that exited 127. An Edit line now takes the lines after it while its group is open or its text block waits for its . line (sam_edit.needsMore, asking sam's parser), on all three; the root ctl takes Edit at the active pane. s/re/text/N counts as sN/re/text/ does. The docs and the skill show a block written by heredoc or env printf, since bash's printf writes line by line, and take round 5's doc list: get refusing once, the (xN) on a repeated err, limit bounding only a forward search's end, line:col's clamping, and $NINE_MOUNT under 9ns --unix.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/ninep/ctl.zig')
| -rw-r--r-- | src/ninep/ctl.zig | 69 |
1 files changed, 58 insertions, 11 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 18068ab1..b11692e0 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -87,6 +87,37 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { noteResult(p, if (exec) context else (p.panes[p.active] orelse return).serial); } +/// A write's messages, a line each, except that an `Edit` line takes the +/// lines after it while its `{` group or `a`/`c`/`i` text block is open +/// (sam_edit.needsMore): a block written whole reaches Edit whole, never +/// as lines of their own. +const Messages = struct { + p: *Pardes, + data: []const u8, + at: usize = 0, + + fn init(p: *Pardes, data: []const u8) Messages { + return .{ .p = p, .data = data }; + } + + fn next(m: *Messages) ?[]const u8 { + if (m.at >= m.data.len) return null; + const start = m.at; + var end = std.mem.indexOfScalarPos(u8, m.data, start, '\n') orelse m.data.len; + m.at = end + 1; + const first = std.mem.trim(u8, m.data[start..end], " \t\r"); + const edit = "Edit"; + if (!std.mem.startsWith(u8, first, edit) or (first.len > edit.len and first[edit.len] != ' ' and first[edit.len] != '\t')) + return first; + const sam = @import("../sam_edit.zig"); + while (m.at < m.data.len and sam.needsMore(m.p.scratch.allocator(), std.mem.trim(u8, m.data[start..end], " \t\r")[edit.len..])) { + end = std.mem.indexOfScalarPos(u8, m.data, m.at, '\n') orelse m.data.len; + m.at = end + 1; + } + return std.mem.trim(u8, m.data[start..end], " \t\r"); + } +}; + // ---- look and exec ---- /// One command per line, checked whole before any line runs: a line is the @@ -96,11 +127,11 @@ fn run(p: *Pardes, id: usize, exec: bool, text: []const u8) void { pub fn command(p: *Pardes, req: Req, serial: ?u32, exec: bool) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const text = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |text| { if (text.len == 0) continue; - for (text) |c| if (c < ' ' and c != '\t') return Reply.fail(req.tag, E.INVAL); + // Only an Edit block holds newlines (Messages). + for (text) |c| if (c < ' ' and c != '\t' and c != '\n') return Reply.fail(req.tag, E.INVAL); if (!apply) continue; const id = if (serial) |s| p.paneBySerial(s) orelse break else p.active; if (p.panes[id] == null) return Reply.fail(req.tag, E.NOENT); @@ -226,7 +257,8 @@ fn checkBuiltin(p: *Pardes, req: Req, line: []const u8, scope: builtins.Scope) ? const word = line[0 .. sp orelse line.len]; const arg = if (sp) |s| std.mem.trim(u8, line[s + 1 ..], " \t") else ""; const b = std.meta.stringToEnum(Builtin, word) orelse return refuse(p, req, "unknown control message", line); - if (builtins.registry.scope(b) != scope) + // Edit is the one pane word the root takes too: at the active pane. + if (builtins.registry.scope(b) != scope and b != .Edit) return refuseTo(p, req, if (scope == .pane) "not a window control message" else "not a session control message", line, if (scope == .pane) "/ctl" else "pane/<n>/ctl"); if (arg.len > 0 and !builtins.registry.takesArg(b)) return refuse(p, req, "wrong #args in control message", line); if (arg.len == 0 and builtins.registry.requiresArg(b)) return refuse(p, req, "wrong #args in control message", line); @@ -342,9 +374,8 @@ pub fn readRoot(p: *Pardes, req: Req) Reply { pub fn writeRoot(p: *Pardes, req: Req) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.len == 0) continue; if (!apply) { if (checkBuiltin(p, req, line, .session)) |refusal| return refusal; @@ -453,9 +484,8 @@ pub fn writePane(p: *Pardes, req: Req, pane: *Pane) Reply { for ([2]bool{ false, true }) |apply| { if (apply) p.fs.results_len = 0; var held = !other and pane.fs.lock != null; - var it = std.mem.splitScalar(u8, req.data, '\n'); - while (it.next()) |raw| { - const line = std.mem.trim(u8, raw, " \t\r"); + var it = Messages.init(p, req.data); + while (it.next()) |line| { if (line.len == 0) continue; // A line before this one may have closed the pane (`Del`). if (apply and p.paneBySerial(serial) == null) return Reply.fail(req.tag, E.NOENT); @@ -787,6 +817,23 @@ test "Edit through a pane's ctl renames every foo in one undo step, and a failed try testing.expectEqualStrings("foo x foo y foo\n", rd(p, body, 0, 64).bytes); } +test "a write of an Edit block carries it whole, to a pane's ctl, the root's ctl and exec" { + const p = try withFile(testing.allocator, "foo x\nfoo y\n"); + defer p.deinit(); + const serial = serialOf(p); + const body = Node.of(serial, .body); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\ni/[/\na/]/\n}\n").reply.status); + try testing.expectEqualStrings("[foo] x\n[foo] y\n", rd(p, body, 0, 64).bytes); + p.active = p.paneBySerial(serial).?; + try testing.expectEqual(Status.ok, wr(p, @intFromEnum(tree.TopFile.ctl), "Edit ,x/\\[/{\nc/</\n}\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\n", rd(p, body, 0, 64).bytes); + try testing.expectEqual(Status.ok, wr(p, Node.of(serial, .exec), "Edit $a\nend\n.\n").reply.status); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); + // An unclosed group is refused, and nothing after it runs as a line. + try testing.expectEqual(E.IO, wr(p, Node.of(serial, .ctl), "Edit ,x/foo/{\nd\n").errno()); + try testing.expectEqualStrings("<foo] x\n<foo] y\nend\n", rd(p, body, 0, 64).bytes); +} + test "dirty goes back to 0 when the text is the saved text again" { const p = try withFile(testing.allocator, "one\n"); defer p.deinit(); |
