diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ninep/ctl.zig | 69 | ||||
| -rw-r--r-- | src/sam_edit.zig | 36 |
2 files changed, 93 insertions, 12 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(); diff --git a/src/sam_edit.zig b/src/sam_edit.zig index e8e90b67..679823da 100644 --- a/src/sam_edit.zig +++ b/src/sam_edit.zig @@ -82,6 +82,19 @@ pub fn run(arena: std.mem.Allocator, text: []const u8, dot: Range, name: []const return .{ .ops = ex.ops.items, .dot = d, .printed = ex.out.items }; } +/// Whether `command` stops inside a `{` group or an `a`, `c` or `i` text +/// block still waiting for its `.` line: a writer of lines (a ctl, exec) +/// hands an Edit the lines after it until it does not. +pub fn needsMore(arena: std.mem.Allocator, command: []const u8) bool { + var why: Why = .{}; + const src = std.fmt.allocPrint(arena, "{s}\n", .{command}) catch return false; + var ps: Parser = .{ .arena = arena, .src = src, .why = &why }; + while (true) { + const c = ps.parse(0) catch return std.mem.eql(u8, why.text(), "unmatched `{'") or ps.open_text; + if (c == null) return ps.open_text; + } +} + fn fail(why: *Why, comptime fmt: []const u8, args: anytype) Failure { why.len = if (std.fmt.bufPrint(&why.buf, fmt, args)) |s| s.len else |_| 0; return error.Edit; @@ -97,6 +110,8 @@ const Parser = struct { lastpat: []const u8 = "", /// The last `parse` that answered null met a `}`, not the end. braced: bool = false, + /// A text block of `a`, `c` or `i` ran out before its `.` line. + open_text: bool = false, why: *Why, fn getch(ps: *Parser) ?u8 { @@ -250,6 +265,7 @@ const Parser = struct { try s.appendSlice(ps.arena, line); try s.append(ps.arena, '\n'); } + ps.open_text = true; return s.items; } const delim = ps.getch() orelse return ""; @@ -292,7 +308,11 @@ const Parser = struct { if (ps.nextc() == 'g') { ps.i += 1; cmd.global = true; - } + } else if (ps.nextc()) |d| if (d >= '1' and d <= '9') { + // `s/a/b/2`, the count after, as some write it for + // sam's `s2/a/b/`. + cmd.num = ps.getnum(1); + }; } try ps.atnl(); }, @@ -636,6 +656,7 @@ test "sam's classic commands" { try expectEdit("aaa\n", ",s/a/b/g", "bbb\n"); try expectEdit("aaa\n", ",s/a/b/", "baa\n"); try expectEdit("aaa\n", ",s2/a/b/", "aba\n"); + try expectEdit("aaa\n", ",s/a/b/2", "aba\n"); try expectEdit("cat dog\n", ",s/[a-z]+/<&>/g", "<cat> <dog>\n"); // x within x, g within x. try expectEdit("int a;\nfloat b;\nint c;\n", ",x/.*\\n/g/^int/x/[a-z];/c/X;/", "int X;\nfloat b;\nint X;\n"); @@ -676,6 +697,19 @@ test "an Edit that fails halfway changes nothing, and says why in acme's words" } } +test "an Edit wants the lines after it until its braces and text blocks close" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const a = arena_state.allocator(); + try std.testing.expect(needsMore(a, ",x/foo/{")); + try std.testing.expect(needsMore(a, ",x/foo/{\ni/[/")); + try std.testing.expect(!needsMore(a, ",x/foo/{\ni/[/\n}")); + try std.testing.expect(needsMore(a, "$a\nline one")); + try std.testing.expect(!needsMore(a, "$a\nline one\n.")); + try std.testing.expect(!needsMore(a, ",s/{/x/")); + try std.testing.expect(!needsMore(a, ",x/foo/c/bar/")); +} + test "the dot an Edit leaves selects what a change put where it stood" { const ops = [_]Op{.{ .q0 = 2, .q1 = 4, .text = "XYZ" }}; try std.testing.expectEqual(Range{ .q0 = 2, .q1 = 5 }, moveDot(.{ .q0 = 2, .q1 = 2 }, &ops)); |
