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.zig138
1 files changed, 112 insertions, 26 deletions
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index 3f23aeff..d0c51bdb 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -12,8 +12,8 @@
//! What touches more than the text is the caller's (src/EditCmd.zig): `<`,
//! `|` and `>` are collected as jobs, a command to run per address, whose
//! output fills the change they hold the place of; `w` as the bytes to
-//! write; `D`, and the names `e` and `f` set, as marks on the file; `B` and
-//! the reads of `e` and `r` through `World`, at once, as acme does them.
+//! write; `D`, `e`'s load and the name `f` sets, as marks on the file; `B`
+//! and the reads of `e` and `r` through `World`, at once, as acme does them.
//!
//! Left out: the `'` address, `B <cmd` and `D <cmd` (a file list from a
//! command), and `\1`..`\9` in `s` and sam's longest of alternatives (mvzr
@@ -22,8 +22,9 @@
//! sequence (elog.c:130) and applies them anyway; here the Edit is refused.
//! A command that fails (`|`, `<`, `>`) refuses the whole Edit, where sam
//! warns and puts in what it wrote; `D` of a file the Edit changed is
-//! refused rather than drop the changes; `e` of an edited file is refused
-//! every time, not once.
+//! refused rather than drop the changes; `e` loads its file once the Edit
+//! is done, by Get's way (ninep/ctl.zig getFile), so the file takes no
+//! other command in the same Edit.
const std = @import("std");
const regexp = @import("regexp.zig");
const addr_lang = @import("ninep/addr.zig");
@@ -58,8 +59,10 @@ pub const File = struct {
renamed: bool = false,
/// `D` closes it once the Edit is done.
close: bool = false,
- /// `e` read its own file whole: it is clean again once applied.
- clean: bool = false,
+ /// `e` loads this file into it once the Edit is done (the editor's Get
+ /// path, which renames the pane and leaves it clean); the file takes no
+ /// more commands in the Edit.
+ get: ?[]const u8 = null,
/// A command left a dot in it.
moved: bool = false,
};
@@ -75,11 +78,17 @@ pub const Write = struct { file: u32, path: []const u8, bytes: []const u8, whole
/// What the editor does for `B`, `e` and `r` while an Edit runs.
pub const World = struct {
ctx: *anyopaque,
+ /// Whether `B` could open the absolute `path`: false, and `why` set,
+ /// when it could not, before any is opened.
+ check: *const fn (ctx: *anyopaque, path: []const u8, why: *Why) bool,
/// The text pane holding the absolute `path`, opened if it is not open:
/// `B`. Null, and `why` set, when it cannot be.
open: *const fn (ctx: *anyopaque, path: []const u8, why: *Why) ?File,
/// The bytes of the absolute `path`, in `arena`: `e` and `r`.
read: *const fn (ctx: *anyopaque, arena: std.mem.Allocator, path: []const u8, why: *Why) ?[]const u8,
+ /// `e` over unsaved edits in the pane `id`: asked once, as Get asks;
+ /// true, and `why` set, when it is refused this time.
+ refuseGet: *const fn (ctx: *anyopaque, id: usize, why: *Why) bool,
};
/// What an Edit came to: every file (those `B` opened after the ones it
@@ -221,6 +230,28 @@ pub fn needsMore(arena: std.mem.Allocator, command: []const u8) bool {
}
}
+/// Whether `command` holds a `<`, `|` or `>`: its write is answered only
+/// when they have run (ninep/ctl.zig refuses other lines beside it).
+pub fn runsCommands(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 };
+ const Walk = struct {
+ fn any(c: ?*Cmd) bool {
+ var it = c;
+ while (it) |each| : (it = each.next) {
+ if (std.mem.indexOfScalar(u8, "<|>", each.c) != null) return true;
+ if (any(each.sub)) return true;
+ }
+ return false;
+ }
+ };
+ while (true) {
+ const c = (ps.parse(0) catch return false) orelse return false;
+ if (Walk.any(c)) return true;
+ }
+}
+
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;
@@ -642,7 +673,10 @@ const Exec = struct {
/// `name` as a path: itself when absolute, else in `fi`'s directory
/// (acme's dirname).
- fn pathIn(ex: *Exec, fi: usize, name: []const u8) Failure![]const u8 {
+ fn pathIn(ex: *Exec, fi: usize, name_in: []const u8) Failure![]const u8 {
+ // `~` and `~user` as Get, Incl and Save read them.
+ var home: [4096]u8 = undefined;
+ const name = try ex.arena.dupe(u8, @import("fs.zig").expandHome(name_in, &home));
if (name.len > 0 and name[0] == '/') return name;
const dir = std.fs.path.dirnamePosix(ex.files.items[fi].name) orelse "/";
return std.fs.path.resolvePosix(ex.arena, &.{ dir, name });
@@ -683,6 +717,7 @@ const Exec = struct {
var fi = fi_in;
var dot = dot_in;
if (ex.files.items[fi].close) return fail(ex.why, "no current window", .{});
+ if (ex.files.items[fi].get) |path| return fail(ex.why, "{s} is read in by e at the Edit's end: no more commands in it", .{path});
// A `"re"` address: the command runs in that file, from its dot.
if (c.file_re) |pat| {
fi = try ex.matchFile(pat);
@@ -797,6 +832,9 @@ const Exec = struct {
if (list.len > 0 and list[0] == '<') return fail(ex.why, "B <command is not supported: give the names", .{});
if (list.len == 0) return fail(ex.why, e_noname, .{});
const w = ex.world orelse return fail(ex.why, "B: no panes to open here", .{});
+ // Every name first: one that cannot open opens none.
+ var checks = std.mem.tokenizeAny(u8, list, " \t");
+ while (checks.next()) |name| if (!w.check(w.ctx, try ex.pathIn(fi, name), ex.why)) return error.Edit;
var names = std.mem.tokenizeAny(u8, list, " \t");
while (names.next()) |name| {
const path = try ex.pathIn(fi, name);
@@ -822,17 +860,28 @@ const Exec = struct {
}
return r;
},
- 'e', 'r' => {
- // ecmd.c e_cmd: e reads a file over all of this one, r over
- // dot; e names this file after it.
+ 'r' => {
+ // ecmd.c e_cmd: r reads a file over dot.
+ const path = (try ex.cmdName(fi, c.text, false)) orelse return fail(ex.why, e_noname, .{});
+ try ex.log(fi, r.q0, r.q1, try ex.readFile(path), null);
+ return r;
+ },
+ 'e' => {
+ // ecmd.c e_cmd: a file read over all of this one, which takes
+ // its name: Get's way (ctl.getFile), once the Edit is done,
+ // asking first, once, over unsaved edits.
const f = &ex.files.items[fi];
- if (c.c == 'e' and f.dirty and !f.scratch) return fail(ex.why, "{s} modified", .{f.name});
- const own = f.name;
- const path = (try ex.cmdName(fi, c.text, c.c == 'e')) orelse return fail(ex.why, e_noname, .{});
- const bytes = try ex.readFile(path);
- const q: Range = if (c.c == 'e') .{ .q0 = 0, .q1 = clip(txt.len) } else r;
- try ex.log(fi, q.q0, q.q1, bytes, null);
- if (c.c == 'e' and std.mem.eql(u8, path, own)) ex.files.items[fi].clean = true;
+ if (f.ops.items.len > 0) return fail(ex.why, "e: {s} has changes pending in this Edit", .{f.name});
+ const given = std.mem.trim(u8, c.text, " \t");
+ const path = if (given.len == 0)
+ (if (f.scratch) return fail(ex.why, e_noname, .{}) else f.name)
+ else
+ try ex.pathIn(fi, given);
+ const w = ex.world orelse return fail(ex.why, "can't open {s}: no files here", .{path});
+ if (w.refuseGet(w.ctx, f.id, ex.why)) return error.Edit;
+ _ = try ex.readFile(path);
+ f.get = path;
+ f.name = path;
return r;
},
'w' => {
@@ -893,7 +942,7 @@ const Exec = struct {
/// in this Edit is refused rather than have them dropped.
fn closeFile(ex: *Exec, fi: usize) Failure!void {
const f = &ex.files.items[fi];
- if (f.ops.items.len > 0) return fail(ex.why, "D: {s} has changes pending in this Edit", .{f.name});
+ if (f.ops.items.len > 0 or f.get != null) return fail(ex.why, "D: {s} has changes pending in this Edit", .{f.name});
f.close = true;
}
@@ -1166,7 +1215,23 @@ const Fixture = struct {
.{ .name = "/src/+Errors", .text = "foo\n", .id = 3, .scratch = true },
};
+ var opened: usize = 0;
+
+ fn check(_: *anyopaque, path: []const u8, why: *Why) bool {
+ if (std.mem.eql(u8, path, "/src/new.c")) return true;
+ why.len = if (std.fmt.bufPrint(&why.buf, "B: cannot open {s}", .{path})) |said| said.len else |_| 0;
+ return false;
+ }
+
+ /// The edited file, b.h, is asked about: refused.
+ fn refuseGet(_: *anyopaque, id: usize, why: *Why) bool {
+ if (id != 1) return false;
+ why.len = (std.fmt.bufPrint(&why.buf, "/src/b.h: Modified (e again to discard)", .{}) catch unreachable).len;
+ return true;
+ }
+
fn open(_: *anyopaque, path: []const u8, why: *Why) ?File {
+ opened += 1;
if (std.mem.eql(u8, path, "/src/new.c")) return .{ .name = "/src/new.c", .text = "new foo\n", .id = 9 };
why.len = if (std.fmt.bufPrint(&why.buf, "B: cannot open {s}", .{path})) |said| said.len else |_| 0;
return null;
@@ -1188,7 +1253,7 @@ const Fixture = struct {
fn edit(fx: *Fixture, from: usize, command: []const u8) !Result {
var ctx: u8 = 0;
- const world: World = .{ .ctx = &ctx, .open = open, .read = read };
+ const world: World = .{ .ctx = &ctx, .check = check, .open = open, .read = read, .refuseGet = refuseGet };
return run(fx.arena_state.allocator(), &files, from, command, world, &fx.why);
}
@@ -1247,17 +1312,23 @@ test "b moves the current file, \"re\" addresses another, and f names one" {
test "e and r read a file into the text, w writes it, D and B close and open panes" {
var fx: Fixture = .init();
defer fx.deinit();
- // r replaces dot; e the whole text, and names the file after it.
+ // r replaces dot; e loads a file over all of it at the end, by Get's
+ // way, which names the pane after it.
const r = try fx.edit(0, "2 r data");
try std.testing.expectEqualStrings("int foo;\nDATA\n", try fx.after(r, 0));
try std.testing.expect(!r.files[0].renamed);
- const e = try fx.edit(0, "e /src/data");
- try std.testing.expectEqualStrings("DATA\n", try fx.after(e, 0));
- try std.testing.expectEqualStrings("/src/data", e.files[0].name);
- try std.testing.expect(!e.files[0].clean);
- // e of an edited file is refused, as acme's winclean refuses it.
+ const e = try fx.edit(0, "e data");
+ try std.testing.expectEqualStrings("/src/data", e.files[0].get.?);
+ try std.testing.expectEqual(@as(usize, 0), e.files[0].ops.items.len);
+ // ...and takes no more commands, nor follows another change.
+ try std.testing.expectError(error.Edit, fx.edit(0, "e data\n,d"));
+ try std.testing.expectError(error.Edit, fx.edit(0, "1d\ne data"));
+ // e of an edited file is asked about, as Get asks.
try std.testing.expectError(error.Edit, fx.edit(1, "e"));
- try std.testing.expectEqualStrings("/src/b.h modified", fx.why.text());
+ try std.testing.expectEqualStrings("/src/b.h: Modified (e again to discard)", fx.why.text());
+ // `~` is the home directory, as Get and Save read it.
+ const home = try fx.edit(0, "w ~/t.txt");
+ try std.testing.expect(home.writes[0].path[0] == '/' and std.mem.indexOfScalar(u8, home.writes[0].path, '~') == null);
try std.testing.expectError(error.Edit, fx.edit(0, "r nothing"));
try std.testing.expectEqualStrings("can't open /src/nothing: file does not exist", fx.why.text());
// w: all of it by default, to its own name; an address and a name.
@@ -1282,6 +1353,10 @@ test "e and r read a file into the text, w writes it, D and B close and open pan
try std.testing.expectEqualStrings("old foo\n", try fx.after(b, 4));
try std.testing.expectError(error.Edit, fx.edit(0, "B gone.c"));
try std.testing.expectEqualStrings("B: cannot open /src/gone.c", fx.why.text());
+ // Every name is checked first: one that cannot open opens none.
+ Fixture.opened = 0;
+ try std.testing.expectError(error.Edit, fx.edit(0, "B new.c gone.c"));
+ try std.testing.expectEqual(@as(usize, 0), Fixture.opened);
}
test "<, | and > are jobs: a command per address, its output the change" {
@@ -1309,3 +1384,14 @@ test "<, | and > are jobs: a command per address, its output the change" {
try std.testing.expectEqual(@as(usize, 2), x.jobs.len);
try std.testing.expectEqual(@as(u32, 2), x.jobs[1].file);
}
+
+test "an Edit that runs commands is told from one that does not" {
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ const a = arena_state.allocator();
+ try std.testing.expect(runsCommands(a, ", | sort"));
+ try std.testing.expect(runsCommands(a, ",x/a/{\n< date\n}"));
+ try std.testing.expect(runsCommands(a, "X/c/ , > wc"));
+ try std.testing.expect(!runsCommands(a, ",s/|/>/g"));
+ try std.testing.expect(!runsCommands(a, ",x/</d"));
+}