diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 19:11:53 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 19:57:27 -0300 |
| commit | 5f41a03fdcf0eaa3c1917e0f94243878c481cf8b (patch) | |
| tree | c3025e2ca80a295962fa941bb4423df9a1525509 | |
| parent | 6ee4c3caa6faf42b9825dca39e9130ac83f425a8 (diff) | |
| download | pardes-5f41a03fdcf0eaa3c1917e0f94243878c481cf8b.tar.gz pardes-5f41a03fdcf0eaa3c1917e0f94243878c481cf8b.zip | |
Incl dir ... adds directories a look at a name found nowhere else is tried in, as acme's includename, <stdio.h> too; bare Incl lists them and Incl - empties them; the session's, seeded with /usr/include and /usr/local/include, kept in a dump; Indent's refusal says Enter keeps the indent always
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/typ/builtins-gui.json | 1 | ||||
| -rw-r--r-- | docs/typ/builtins-tty.json | 1 | ||||
| -rw-r--r-- | src/builtins.zig | 35 | ||||
| -rw-r--r-- | src/config.zig | 4 | ||||
| -rw-r--r-- | src/dump.zig | 11 | ||||
| -rw-r--r-- | src/exec.zig | 3 | ||||
| -rw-r--r-- | src/look.zig | 50 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 2 | ||||
| -rw-r--r-- | src/pardes.zig | 13 | ||||
| -rw-r--r-- | test/snapshots/dump.golden | 1 |
10 files changed, 117 insertions, 4 deletions
diff --git a/docs/typ/builtins-gui.json b/docs/typ/builtins-gui.json index d6ddaf77..7674ca34 100644 --- a/docs/typ/builtins-gui.json +++ b/docs/typ/builtins-gui.json @@ -50,6 +50,7 @@ {"name": "Hover", "leader": "SPC l k", "also": [], "arg": false, "scope": "pane", "setting": false, "choices": null, "doc": "Ask the language server about the symbol under the cursor."}, {"name": "HoverGlow", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "A soft underline of light under the word a look-hover would open, on or off."}, {"name": "InactiveDim", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off."}, + {"name": "Incl", "leader": null, "also": [], "arg": true, "scope": "root", "setting": false, "choices": null, "doc": "acme's Incl, for the session: `Incl dir ...` adds directories a look at a name found nowhere else is tried in (`stdio.h`, `<foo.h>`), bare lists them, `Incl -` empties the list."}, {"name": "Joincol", "leader": "SPC c j", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Fold the active pane's column into the one on its right, keeping its panes."}, {"name": "JumpScope", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "file, all", "doc": "What Back and Forward keep to: file, the focused pane's file until it runs out that way, or all, every place in order."}, {"name": "JumpTrail", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "In a terminal, a jump of the cursor leaves a short trail that fades, on or off."}, diff --git a/docs/typ/builtins-tty.json b/docs/typ/builtins-tty.json index f4f06830..837b2853 100644 --- a/docs/typ/builtins-tty.json +++ b/docs/typ/builtins-tty.json @@ -42,6 +42,7 @@ {"name": "Help", "leader": "SPC ?", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Open the help: every key and every builtin."}, {"name": "Hover", "leader": "SPC l k", "also": [], "arg": false, "scope": "pane", "setting": false, "choices": null, "doc": "Ask the language server about the symbol under the cursor."}, {"name": "InactiveDim", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": null, "doc": "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off."}, + {"name": "Incl", "leader": null, "also": [], "arg": true, "scope": "root", "setting": false, "choices": null, "doc": "acme's Incl, for the session: `Incl dir ...` adds directories a look at a name found nowhere else is tried in (`stdio.h`, `<foo.h>`), bare lists them, `Incl -` empties the list."}, {"name": "Joincol", "leader": "SPC c j", "also": ["topbar"], "arg": false, "scope": "root", "setting": false, "choices": null, "doc": "Fold the active pane's column into the one on its right, keeping its panes."}, {"name": "JumpScope", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "file, all", "doc": "What Back and Forward keep to: file, the focused pane's file until it runs out that way, or all, every place in order."}, {"name": "JumpTrail", "leader": null, "also": [], "arg": true, "scope": "root", "setting": true, "choices": "on, off", "doc": "In a terminal, a jump of the cursor leaves a short trail that fades, on or off."}, diff --git a/src/builtins.zig b/src/builtins.zig index 8fcc3c01..780d0bb7 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1159,6 +1159,41 @@ pub const Putall = struct { } }; +/// acme's Incl, for the session: `Incl dir ...` adds directories a look at a +/// name found nowhere else is tried in (`stdio.h`, `<foo.h>`), bare lists +/// them, `Incl -` empties the list. +pub const Incl = struct { + pub const scope: Scope = .session; + pub const takes_arg = true; + pub fn run(c: Ctx) void { + const p = c.p; + const typed = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); + if (std.mem.eql(u8, typed, "-")) { + for (p.incl.items) |dir| p.gpa.free(dir); + p.incl.clearRetainingCapacity(); + return p.setMessage(c.id, "Incl: none"); + } + var words = std.mem.tokenizeAny(u8, typed, " \t"); + while (words.next()) |dir_word| { + var home_buf: [4096]u8 = undefined; + const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ Pardes.paneDir(c.pane), @import("fs.zig").expandHome(dir_word, &home_buf) }) catch return; + if (!std.fs.path.isAbsolute(full)) return p.reportFailure(c.id, "Incl: invalid directory"); + for (p.incl.items) |dir| { + if (std.mem.eql(u8, dir, full)) break; + } else { + const copy = p.gpa.dupe(u8, full) catch return; + p.incl.append(p.gpa, copy) catch return p.gpa.free(copy); + } + } + if (typed.len > 0) return; + var list: std.Io.Writer.Allocating = .init(p.scratch.allocator()); + list.writer.writeAll("Incl:") catch return; + for (p.incl.items) |dir| list.writer.print(" {s}", .{dir}) catch return; + if (p.incl.items.len == 0) list.writer.writeAll(" none") catch return; + p.setMessage(c.id, list.written()); + } +}; + /// Write the pane's text to its file, or to the file its argument names. pub const Save = struct { pub const takes_arg = true; diff --git a/src/config.zig b/src/config.zig index 640f3cd0..375b4c69 100644 --- a/src/config.zig +++ b/src/config.zig @@ -60,6 +60,7 @@ pub const leader_path = paths: { .Exit = "q", .Get = null, .Putall = null, + .Incl = null, .ClipYank = "y", .ClipYankMain = "Y", .ClipPaste = "p", @@ -400,6 +401,9 @@ pub fn defaultShell() []const u8 { // Minimum file gutter width, including the space after the line number. pub const PREFIX_W: u16 = 5; +/// acme's Incl's C defaults: where a look at `stdio.h` finds it. +pub const default_incl = [_][]const u8{ "/usr/include", "/usr/local/include" }; + pub const tab_width: u16 = 4; comptime { if (tab_width == 0) @compileError("config.tab_width must be greater than zero"); diff --git a/src/dump.zig b/src/dump.zig index 005770e1..b64441d3 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -302,6 +302,8 @@ pub const State = struct { /// config are kept on their own, and the font is the frontend's. Absent /// in older dumps. settings: []const []const u8 = &.{}, + /// Incl's directories, in order; null in older dumps (the defaults). + incl: ?[]const []const u8 = null, }; /// A header is a text like any other: it may hold newlines, but not a @@ -798,6 +800,7 @@ pub fn dumpState(p: *Pardes) !void { .panes = dump_panes[0..panes_len], .mounts = mounts[0..p.fs.mounts.items.len], .settings = try changedSettings(p, arena), + .incl = p.incl.items, }; try validate(state); var out: std.Io.Writer.Allocating = .init(p.gpa); @@ -902,6 +905,14 @@ fn initDump(gpa: std.mem.Allocator, opts: Options, zon_bytes: []const u8, previo p.locations_config = st.locations_config; p.settings.tree_context_tag_style = st.tree_context_tag_style; applySettings(p, st.settings); + if (st.incl) |dirs| { + for (p.incl.items) |dir| p.gpa.free(dir); + p.incl.clearRetainingCapacity(); + for (dirs) |dir| if (dir.len > 0 and dir[0] == '/' and std.mem.indexOfScalar(u8, dir, 0) == null) { + const copy = p.gpa.dupe(u8, dir) catch continue; + p.incl.append(p.gpa, copy) catch p.gpa.free(copy); + }; + } // A dump naming a ThemeFile asks the host to load it again: until it // has, and for good if it fails, the theme this session showed goes on // (a failed ThemeFile changes nothing, a Restore's included), unless diff --git a/src/exec.zig b/src/exec.zig index c465bda8..4bdb2584 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -954,9 +954,8 @@ pub const acme_words = [_]struct { acme: []const u8, pardes: ?[]const u8, hint: .{ .acme = "ID", .pardes = null }, .{ .acme = "Send", .pardes = null }, .{ .acme = "Tab", .pardes = null }, - .{ .acme = "Indent", .pardes = null }, + .{ .acme = "Indent", .pardes = null, .hint = ": Enter, o and O keep the indent always" }, .{ .acme = "Local", .pardes = null }, - .{ .acme = "Incl", .pardes = null }, .{ .acme = "Abort", .pardes = null, .hint = ": a debugging word that crashes acme" }, }; diff --git a/src/look.zig b/src/look.zig index a2aa5be0..3d02802a 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1141,6 +1141,13 @@ fn resolveLookTarget(p: *Pardes, id: usize, txt: []const u8, realbuf: *[4096]u8) // `.`, `..`, `./x` are this pane's directory's, not another's. if (found != .none or pl.path.len == 0 or pl.path[0] == '/' or filesystem.dotRelative(pl.path)) break; } + // Found nowhere else, a relative name is tried in Incl's directories, + // as acme's includename tries its (look.c:500). + if (found == .none and pl.path.len > 0 and pl.path[0] != '/' and !filesystem.dotRelative(pl.path)) + for (p.incl.items) |dir| { + found = resolve(p, txt, dir, realbuf); + if (found != .none) break; + }; return found; } @@ -1734,7 +1741,9 @@ pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void { const txt = p.scratch.allocator().dupe(u8, looked) catch return; // A trailing `:` (`a.txt:2:3:`, as a compiler's message ends) is no // part of the place, as a click's expansion leaves it off. - const trimmed = std.mem.trimEnd(u8, std.mem.trim(u8, txt, " \t\r\n"), ":"); + const bare = std.mem.trimEnd(u8, std.mem.trim(u8, txt, " \t\r\n"), ":"); + // `<stdio.h>` written whole: the name inside, as a click on it takes. + const trimmed = if (bare.len > 2 and bare[0] == '<' and bare[bare.len - 1] == '>') bare[1 .. bare.len - 1] else bare; // acme's `file:addr` and `:addr` (look.c:450-543): an address after // the colon, evaluated in that file from its dot (openfile, look.c:675). // `file:3:0`, and `file:0:0` too: columns count from 1, as addr's do, @@ -2586,6 +2595,45 @@ test "a tag word looked at by a click and by Enter is one record to an event rea try std.testing.expect(p.panes[0] != null); } +test "Incl's directories find a name found nowhere else, <name> too; bare Incl lists them, Incl - empties them, and a dump keeps them" { + if (!platform_has_fs) return; + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + try tmp.dir.createDirPath(std.testing.io, "inc"); + try tmp.dir.createDirPath(std.testing.io, "src"); + try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "inc/pardes_incl_probe.h", .data = "#define X 1\n" }); + var dir_buf: [4096]u8 = undefined; + const root = dir_buf[0..try tmp.dir.realPath(std.testing.io, &dir_buf)]; + const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); + defer p.deinit(); + // C's two by default. + try std.testing.expectEqualStrings("/usr/include", p.incl.items[0]); + const pane = try p.setTestFile("#include <pardes_incl_probe.h>\n"); + p.gpa.free(pane.file.?.path); + pane.file.?.path = try std.fs.path.join(p.gpa, &.{ root, "src", "main.c" }); + var line: [4200]u8 = undefined; + try std.testing.expect(p.executeBuiltinLine(0, try std.fmt.bufPrint(&line, "Incl {s}/inc", .{root}))); + for ([_][]const u8{ "pardes_incl_probe.h", "<pardes_incl_probe.h>" }) |word| { + p.active = 0; + p.look_missed = false; + lookAt(p, 0, word); + try std.testing.expect(!p.look_missed); + try std.testing.expect(std.mem.endsWith(u8, p.panes[p.active].?.file.?.path, "/inc/pardes_incl_probe.h")); + } + // Bare, it lists them. + try std.testing.expect(p.executeBuiltinLine(0, "Incl")); + try std.testing.expect(std.mem.indexOf(u8, pane.msg[0..pane.msg_len], "/inc") != null); + // A dump keeps them. + try pardes.dump.dumpState(p); + const restored = try pardes.dump.restore(p, p.dump_out.?, "/tmp/pardes.dump.zon"); + defer restored.deinit(); + try std.testing.expectEqual(p.incl.items.len, restored.incl.items.len); + try std.testing.expect(std.mem.endsWith(u8, restored.incl.items[restored.incl.items.len - 1], "/inc")); + // Emptied, the name is found nowhere. + try std.testing.expect(p.executeBuiltinLine(0, "Incl -")); + try std.testing.expectEqual(@as(usize, 0), p.incl.items.len); +} + test "Look outside a diff pane expands and looks as ever" { const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 30 }); defer p.deinit(); diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index 028ddf55..d0b1defb 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -3210,7 +3210,7 @@ test "acme's words run as pardes's (Put is Save, Delete a Del that does not ask) const p = try withFile(testing.allocator, "x\n"); defer p.deinit(); const serial = serialOf(p); - for ([_][]const u8{ "Snarf\n", "Zerox\n", "Tab\n", "Indent\n", "Local\n", "Incl\n", "Abort\n" }) |line| { + for ([_][]const u8{ "Snarf\n", "Zerox\n", "Tab\n", "Indent\n", "Local\n", "Abort\n" }) |line| { for ([_]u64{ Node.of(serial, .ctl), Node.of(serial, .exec) }) |node| { const r = wr(p, node, line); try testing.expectEqual(E.INVAL, r.errno()); diff --git a/src/pardes.zig b/src/pardes.zig index 425a0993..36a37293 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4324,6 +4324,10 @@ pub const Pardes = struct { /// The files opened lately, closed ones too (recent.zig): `Recent`, /// /recent, and the jumplist's way back to a closed pane. recent: recent_files.List = .{}, + /// acme's Incl: the directories a look at a name that is nowhere else + /// (`stdio.h`, `<foo.h>`) is tried in, in order. The session's, seeded + /// with C's (/usr/include, /usr/local/include), kept in a dump. + incl: std.ArrayList([]u8) = .empty, /// The kept list was read (or there was none): only then is it written, /// so a session never writes over one it did not read. recent_loaded: bool = false, @@ -4709,6 +4713,13 @@ pub const Pardes = struct { } p.sync(); p.loadRecent(); + for (config.default_incl) |dir| { + const copy = try gpa.dupe(u8, dir); + p.incl.append(gpa, copy) catch |err| { + gpa.free(copy); + return err; + }; + } exec.applyStartupConfig(p); colors.finishThemeInitialization(p); p.sync(); @@ -4816,6 +4827,8 @@ pub const Pardes = struct { for (p.panes) |slot| if (slot) |pane| p.noteClosed(pane); p.saveRecent(); p.recent.deinit(p.recentGpa()); + for (p.incl.items) |dir| p.gpa.free(dir); + p.incl.deinit(p.gpa); p.fs.batch.bytes.deinit(p.gpa); if (p.stand_in) |pane| p.teardownPane(pane); look.cancelLookHover(p); diff --git a/test/snapshots/dump.golden b/test/snapshots/dump.golden index ea6e5912..571bacbc 100644 --- a/test/snapshots/dump.golden +++ b/test/snapshots/dump.golden @@ -108,5 +108,6 @@ "MessageLinger 0", "Shell bash", }, + .incl = .{ "/usr/include", "/usr/local/include" }, } == end file |
