summaryrefslogtreecommitdiff
path: root/src/sam_edit.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/sam_edit.zig')
-rw-r--r--src/sam_edit.zig37
1 files changed, 36 insertions, 1 deletions
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index a88728aa..0f317a83 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -111,11 +111,26 @@ pub fn waitsFor(arena: std.mem.Allocator, command: []const u8) Wait {
var ps: Parser = .{ .arena = arena, .src = src, .why = &why };
while (true) {
const c = ps.parse(0) catch
- return if (ps.open_text) .text else if (std.mem.eql(u8, why.text(), "unmatched `{'")) .group else .none;
+ return if (ps.open_text) .text else if (std.mem.eql(u8, why.text(), "unmatched `{'") or groupOpen(command)) .group else .none;
if (c == null) return if (ps.open_text) .text else .none;
}
}
+/// A `{` block not closed yet by its `}` line: a command in it that fails
+/// to parse still leaves the block to end at its `}`, so the error is that
+/// command's, not the `}`'s taken as a line of its own.
+fn groupOpen(command: []const u8) bool {
+ var depth: usize = 0;
+ var lines = std.mem.splitScalar(u8, command, '\n');
+ while (lines.next()) |raw| {
+ const line = std.mem.trim(u8, raw, " \t\r");
+ if (std.mem.eql(u8, line, "}")) {
+ depth -|= 1;
+ } else if (std.mem.endsWith(u8, line, "{")) depth += 1;
+ }
+ return depth > 0;
+}
+
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;
@@ -445,6 +460,9 @@ const Exec = struct {
fn compile(ex: *Exec, pat: []const u8, c: u8) Failure!regexp.Regex {
return regexp.Regex.compile(pat) catch |err| switch (err) {
error.Anchor => fail(ex.why, "{s}", .{regexp.Regex.e_anchor}),
+ error.TooLong => fail(ex.why, "{s}", .{regexp.Regex.e_long}),
+ error.WideRange => fail(ex.why, "{s}", .{regexp.Regex.e_wide}),
+ error.NegatedRunes => fail(ex.why, "{s}", .{regexp.Regex.e_negated}),
error.Bad => fail(ex.why, "bad regexp in {c} command", .{c}),
};
}
@@ -455,6 +473,9 @@ const Exec = struct {
/// reaches back into what the last one took.
fn find(ex: *Exec, rx: *regexp.Regex, from: usize, hi: usize) Failure!?regexp.Regex.Match {
if (from > hi) return null;
+ // The budget is each search's own: an x over 100k lines makes 100k
+ // searches, none of which is the slow one.
+ rx.steps = regexp.Regex.budget;
const m = (rx.find(ex.text, from, hi, hi) catch return fail(ex.why, "{s}", .{addr_lang.e_slow})) orelse return null;
var start = modal.runeStart(ex.text, m.start);
if (start < from) start = modal.runeEnd(ex.text, m.start);
@@ -693,6 +714,20 @@ fn expectEdit(text: []const u8, command: []const u8, want: []const u8) !void {
try std.testing.expectEqualStrings(want, got);
}
+test "an x over 100k lines is 100k searches, each with its own step budget" {
+ const th = @import("ninep/testing.zig");
+ const line = "x" ** 80 ++ "\n";
+ const text = try std.testing.allocator.alloc(u8, line.len * 100_000);
+ defer std.testing.allocator.free(text);
+ for (0..100_000) |k| @memcpy(text[line.len * k ..][0..line.len], line);
+ const p = try th.withFile(std.testing.allocator, text);
+ defer p.deinit();
+ const tree = @import("ninep/tree.zig");
+ const r = th.wr(p, tree.Node.of(th.serialOf(p), .ctl), "Edit ,x/x+/c/z/\n");
+ try std.testing.expectEqual(tree.Status.ok, r.reply.status);
+ try std.testing.expect(std.mem.startsWith(u8, p.panes[0].?.file.?.content, "z\nz\n"));
+}
+
test "sam's classic commands" {
try expectEdit("foo x foo y foo\n", ",x/foo/c/foobar/", "foobar x foobar y foobar\n");
try expectEdit("a b\nc d\n", ",x/ /c/_/", "a_b\nc_d\n");