diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 04:45:20 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | bda5b5f621d0edd8e26f9700043ab21cae18f329 (patch) | |
| tree | b183df1b137f405a9dcecfe32769e374d2da5c50 | |
| parent | 85d5467023a9db801d8ea2538e9611b71cd10d9d (diff) | |
| download | pardes-bda5b5f621d0edd8e26f9700043ab21cae18f329.tar.gz pardes-bda5b5f621d0edd8e26f9700043ab21cae18f329.zip | |
A looked word selects its next place in its own pane, as acme's look3
A plain word looked at (a look write, a right click, an ML record written
back) opened another identically named +Search pane each time, until the
panes ran out, and never wrapped. As acme's look3 searches the text for
it (look.c:210), it now selects the word's next place in that pane after
the dot, wrapping at the end, and opens nothing. LookWord list, a new
root setting, keeps the +Search listing (search is the default); the
rsearch snapshot and an output test that walk the listing say LookWord
list. (The leader-path check over every builtin gets a comptime
branch quota, as one more builtin took the GUI build past the default.)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
| -rw-r--r-- | docs/fs.md | 5 | ||||
| -rw-r--r-- | src/config.zig | 12 | ||||
| -rw-r--r-- | src/look.zig | 19 | ||||
| -rw-r--r-- | src/ninep/ctl.zig | 23 | ||||
| -rw-r--r-- | test/output.zig | 1 | ||||
| -rw-r--r-- | test/snapshots/rsearch.snap | 3 |
6 files changed, 61 insertions, 2 deletions
@@ -310,7 +310,10 @@ same tree without leaving the process. - a line written to `look` is a right click on it: a path opens a file, `file:12` jumps to a line, a directory opens a shell there, a URL opens in - the browser. + the browser, and a plain word, as acme's look3 does, selects its next + place in that pane after the dot, wrapping at the end, opening nothing + (`LookWord list` on the root ctl lists every place in a `+Search` pane + instead, as pardes did before; `LookWord search` is the default). - a line written to `exec` is a middle click: a command word from `src/builtins.zig` (`Save`, `Del`, `New`, `Newcol`, `Mount NAME DIAL`, `Unmount NAME`, `Dump`, `Restore`, `Msg TEXT`, `Find`, `Grep`, `Tty`, ...; diff --git a/src/config.zig b/src/config.zig index e0fcab48..b7662f1a 100644 --- a/src/config.zig +++ b/src/config.zig @@ -26,6 +26,7 @@ const undecided: []const u8 = "<undecided>"; // null means word/chord-only. Every enabled builtin must explicitly choose a path. pub const leader_path = paths: { + @setEvalBranchQuota(10_000); // a check per builtin, and they grow var table = std.EnumArray(Builtin, ?[]const u8).initDefault(@as(?[]const u8, undecided), .{ .Help = &[_]u8{leader_help}, .Hover = "lk", @@ -75,6 +76,7 @@ pub const leader_path = paths: { .Joincol = "cj", .Placement = "cp", .BootShell = null, + .LookWord = null, .Debug = "td", .Msg = null, .Colors = "tc", @@ -707,12 +709,17 @@ pub const Runtime = struct { /// has typed into stands: `keep` leaves the shell, `replace` closes it /// (exec.evictLonePristineTty). boot_shell: BootShell = .keep, + /// A plain word looked at in a pane: `search`, acme's look3, selects its + /// next place in that pane, wrapping; `list`, what pardes did before, + /// lists every place in a +Search pane (look.zig, lookWord). + look_word: LookWord = .search, pub const ShaderAnimation = enum { off, on, always }; pub const Placement = enum { acme, pardes }; pub const BootShell = enum { keep, replace }; + pub const LookWord = enum { search, list }; /// A setting that is one of a few words, each an enum field of Runtime. - pub const Choice = enum { placement, boot_shell }; + pub const Choice = enum { placement, boot_shell, look_word }; /// shadow: a soft drop shadow. rim: a light hairline along the top of /// the focused pane's tag rows. auto: a shadow on a light page; on a @@ -963,6 +970,7 @@ pub const Runtime = struct { .{ .word = "ShaderAnimation", .action = .shader_animation, .availability = .scene_shaders }, .{ .word = "Placement", .action = .{ .choice = .placement } }, .{ .word = "BootShell", .action = .{ .choice = .boot_shell } }, + .{ .word = "LookWord", .action = .{ .choice = .look_word } }, }; pub fn find(name: []const u8) ?Setting { @@ -1636,6 +1644,7 @@ pub const Runtime = struct { .choice => |which| switch (which) { .placement => "pardes", .boot_shell => "replace", + .look_word => "list", }, .lift => "rim", .motion => "playful", @@ -1694,6 +1703,7 @@ pub const Runtime = struct { .choice => |which| switch (which) { .placement => "Placement pardes\n", .boot_shell => "BootShell replace\n", + .look_word => "LookWord list\n", }, .lift => "Lift rim\n", .motion => "Motion playful\n", diff --git a/src/look.zig b/src/look.zig index 5d60e8b3..86238413 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1229,6 +1229,24 @@ fn settleLine(p: *Pardes, id: usize, path: []const u8, at: Spot, before: usize, } } +/// A plain word looked at in a pane, as acme's look3 does with one that +/// names no file (look.c:210, search): its next place in that pane after +/// the dot, wrapping to the top, is selected there. No +Search pane +/// (`LookWord list` lists every place in one instead). +fn lookWord(p: *Pardes, id: usize, pane: *Pane, word: []const u8) void { + const ninep_pane = @import("ninep/pane.zig"); + const f = pane.file orelse return; + const text = f.content; + const from: usize = @min(ninep_pane.dotOf(pane).q1, text.len); + const at = std.mem.indexOfPos(u8, text, from, word) orelse (std.mem.indexOf(u8, text[0..@min(text.len, from + word.len -| 1)], word) orelse { + var cut: [miss_path_cap + "…".len]u8 = undefined; + return missed(p, id, "no match for \"{s}\"", .{missText(word, &cut)}); + }); + clearNavigationSelection(p, pane); + ninep_pane.setDot(pane, .{ .q0 = @intCast(at), .q1 = @intCast(at + word.len) }); + p.active = id; +} + /// `file:addr` or `:addr`, acme's look.c:450-543: a colon followed by an /// address character (addr.c:28, isaddrc) that is not a line spot this /// parser already reads (`file:12`, `file:12:5`). Not a URL. @@ -1315,6 +1333,7 @@ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { switch (found) { .none => { if (pane.image != null or pane.body.mode == .tty) return; + if (p.settings.look_word == .search and pane.file != null) return lookWord(p, id, pane, trimmed); const rows = runSearch(p, id, trimmed, .text, .cursor, false) catch |err| { p.reportError(id, "search", err); return; diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig index fac1e5eb..f40825e3 100644 --- a/src/ninep/ctl.zig +++ b/src/ninep/ctl.zig @@ -1588,3 +1588,26 @@ test "a file changed on disk under unsaved edits is not reloaded: it says so, ge try testing.expect(std.mem.indexOf(u8, first.reply.ename, "modified on disk since read") != null); try testing.expect(wr(p, Node.of(serial, .ctl), "Save\n").saved); } + +test "a looked word is found in its own pane, next place after the dot, wrapping, and opens no pane" { + const p = try withFile(testing.allocator, "foo x foo y\n"); + defer p.deinit(); + const serial = serialOf(p); + const look_node = Node.of(serial, .look); + const dot = Node.of(serial, .dot); + var count: usize = 0; + for (p.panes) |slot| count += @intFromBool(slot != null); + for ([_][]const u8{ " 0 3 ", " 6 9 ", " 0 3 " }) |want| { + try testing.expectEqual(Status.ok, wr(p, look_node, "foo\n").reply.status); + try testing.expectEqualStrings(want, rd(p, dot, 0, 64).bytes); + } + var after: usize = 0; + for (p.panes) |slot| after += @intFromBool(slot != null); + try testing.expectEqual(count, after); + // LookWord list: the +Search listing, as before. + try testing.expect(p.executeBuiltinLine(0, "LookWord list")); + try testing.expectEqual(Status.ok, wr(p, look_node, "foo\n").reply.status); + after = 0; + for (p.panes) |slot| after += @intFromBool(slot != null); + try testing.expect(after > count); +} diff --git a/test/output.zig b/test/output.zig index f3e61e93..478f1b4a 100644 --- a/test/output.zig +++ b/test/output.zig @@ -862,6 +862,7 @@ test "n/N resumes the result output whose Look moved focus away" { const p = try Pardes.init(std.testing.allocator, .{ .file = path, .cols = 80, .rows = 24 }); defer p.deinit(); p.locations_config.layout = .@"inline"; + p.settings.look_word = .list; // the +Search listing is what is walked try std.testing.expect(p.executeBuiltinLine(0, "Look target")); const rid = p.panes[0].?.search_pane orelse return error.MissingResults; diff --git a/test/snapshots/rsearch.snap b/test/snapshots/rsearch.snap index 8c81db1d..75d0212c 100644 --- a/test/snapshots/rsearch.snap +++ b/test/snapshots/rsearch.snap @@ -11,6 +11,9 @@ file code.txt run count_x now\nopen sub/dep.txt:3 here\ncount_x again\npick two # the top of the body -- which moves every row this script clicks. What the # verbs do is what is under test, not the announcement. config Verbose off +# the +Search listing is what this walks: LookWord list (search, acme's look3, +# is the default; ctl.zig tests it) +config LookWord list start 31 210 -n 1 wait 8000 code.txt stable 700 20000 |
