diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-31 11:42:18 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:08 -0300 |
| commit | 911215846f1d5a9b2e37a00e74fb9266d4a0884a (patch) | |
| tree | 31413ca3336cbbd1933d9dc3e24f09a3b8ef46b4 /src/pardes.zig | |
| parent | 0b7a480bef38b209741c520e2478d178767a9e51 (diff) | |
| download | pardes-911215846f1d5a9b2e37a00e74fb9266d4a0884a.tar.gz pardes-911215846f1d5a9b2e37a00e74fb9266d4a0884a.zip | |
a Look path can name a range, and search selects what it found
file:LINE:COL-ENDLINE:ENDCOL, with the two short forms people actually type
reading naturally: file:412:9-21 on one line, file:412-418 whole ones. Ends are
inclusive. A path feature, not a search feature — a ranged path typed in a tag
or middle-clicked out of a shell's output selects just the same; search is only
its first consumer.
The dash is the fussy part. `-` was already a file char, so a ranged word
survives click expansion whole, but a range needs a number on BOTH sides or
my-file:10, build-2 and 2026-07-30 would stop being paths. Table-driven test in
look.zig for exactly that.
Selecting goes through the cellRange/setPaneRange pair the multi-cursor work
left, and hxOff clamps both ends, so a stale range selects what still exists
rather than crashing or reaching past EOF — pinned with an 8:6-400:9 range in a
nine-line file.
Producers: / search, Grep, and five LSP sites through a new spanRow — goto,
references, rename tokens and both symbol lists were throwing away real
protocol ranges at path:line:col. Left alone deliberately: Find rows are bare
paths with nothing to span, a jump is a spot not a span, and the diagnostic and
format paths only ever have a point, where half a range would be worse than
none.
One knock-on worth knowing: n now leaves an EXPLICIT selection, so a topbar
execute chords it. grep.snap's no-match step was silently becoming
`Grep TARGET`; it runs from the leader path now, which never chords, and the
dedicated chord steps stayed where they were.
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 93 |
1 files changed, 69 insertions, 24 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index fbf48f05..eb2c8402 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -3665,8 +3665,12 @@ pub const Pardes = struct { /// path when it has a real file, else `@pN`. /// find — the pane's DIRECTORY, walked like fd. Rows are bare paths. /// grep — the CONTENTS of every file under every pane's directory, - /// walked like `grep -R`. Rows are `path:LINE:COL text`, the path - /// relative to THIS pane's directory (absolute for a hit outside it). + /// walked like `grep -R`. Rows are `path:LINE:COL-ENDCOL text`, the + /// path relative to THIS pane's directory (absolute for a hit outside + /// it). + /// Both searches that match TEXT name the match's whole span, so looking a + /// row — which is all n/N do — selects what matched rather than parking on + /// its first cell. Find's rows are bare paths and have nothing to span. /// No matches = an empty buffer. pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search) void { const pane = p.panes[id] orelse return; @@ -3720,8 +3724,10 @@ pub const Pardes = struct { std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{id}) catch return; for (pl.lines, 0..) |ln, i| { const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue; - const row = std.fmt.allocPrint(arena, "{s}:{d}:{d} {s}\n", .{ - loc, i + 1, at + 1, std.mem.trimEnd(u8, ln, " \t"), + // the row names the MATCH's span, not just its first cell, so + // n/N land ON the hit with it selected (config.range_sep) + const row = std.fmt.allocPrint(arena, "{s}:{d}:{d}{c}{d} {s}\n", .{ + loc, i + 1, at + 1, config.range_sep, at + pat.len, std.mem.trimEnd(u8, ln, " \t"), }) catch return; out.appendSlice(arena, row) catch return; } @@ -6058,18 +6064,19 @@ pub const Pardes = struct { // ---- the ONE dispatcher: look (right/Enter) and execute (middle/Tab) ---- - /// Focus pane `id` and, for a nonzero 1-based `line`, put its modal cursor - /// there (`col` likewise, 0 = line start): files recenter the view on it, - /// terminals ride their scrollback to it. Both look targets that name a - /// live pane land here — a path a pane already holds, and `@pN:LINE:COL`. - pub fn focusPaneLine(p: *Pardes, id: usize, line: usize, col: usize) void { + /// Focus pane `id` and, for a nonzero 1-based `at.line`, put its modal + /// cursor there (`at.col` likewise, 0 = line start): files recenter the + /// view on it, terminals ride their scrollback to it. Both look targets + /// that name a live pane land here — a path a pane already holds, and + /// `@pN:LINE:COL`. A RANGED spot selects (selectSpan below). + pub fn focusPaneLine(p: *Pardes, id: usize, at: look.Spot) void { if (id >= MAX_PANES) return; const pane = p.panes[id] orelse return; p.active = id; - if (line == 0) return; + if (at.line == 0) return; if (pane.file) |*f| { - if (line > file_pane.nlines(p.gpa, f)) return; - const next = (line - 1) -| pane.rows / 2; // center, clamp at top + if (at.line > file_pane.nlines(p.gpa, f)) return; + const next = (at.line - 1) -| pane.rows / 2; // center, clamp at top if (next != f.scroll) { f.scroll = next; f.syntax_dirty = true; @@ -6078,16 +6085,44 @@ pub const Pardes = struct { // land the modal cursor on the target line (and keep // ensureCursorVisible agreeing with the recenter — a stale cursor // would yank the view right back) - pane.cur_row = @intCast(line - 1); - pane.cur_col = if (col > 0) @intCast(col - 1) else 0; + pane.cur_row = @intCast(at.line - 1); + pane.cur_col = if (at.col > 0) @intCast(at.col - 1) else 0; pane.cur_pinned = true; + if (at.end_line != 0) return p.selectSpan(pane, at); pane.ensureCursorVisible(); } + /// Select the span a RANGED look word names (config.range_sep): the two + /// ends are block-cursor CELLS, so this is the same cellRange/setPaneRange + /// pair every motion writes back through — the cursor lands on the span's + /// last cell with the anchor on its first, which is where helix leaves you + /// after a search too. + /// + /// EXPLICIT, so the acme chords act on it like a v/x selection: the whole + /// point of `n` selecting a hit is being able to chord the match straight + /// into the next command. + /// + /// Everything clamps, because a range is a claim about a file that may + /// have changed underneath it: hxOff pins a row past the end to the last + /// line and a column past the end to that line's terminator, so a stale + /// row selects what is still there instead of crashing or highlighting + /// garbage. A whole-lines range (no end column) runs to the terminator by + /// asking for a column no line can have, which is helix's own `x`. + fn selectSpan(p: *Pardes, pane: *Pane, at: look.Spot) void { + const pl = p.paneCursorLines(pane) catch return; + const text = p.flatSurface(pane, pl) catch return; + // saturating, not `- 1`: `f.zig:0-5` is a legal thing to type and a + // 1-based zero is the same nothing an absent number is + const acol: i32 = @intCast(at.col -| 1); + const ecol: i32 = if (at.end_col > 0) @intCast(at.end_col - 1) else std.math.maxInt(i32); + const r = cellRange(text, @intCast(at.line -| 1), acol, @intCast(at.end_line -| 1), ecol); + setPaneRange(pane, pl, text, r, true); + } + /// focus the pane already loaded on `path` (exact match), if any: file /// panes recenter on a :NN line like the look dedup always has, image /// panes just focus. Returns false when no pane holds that path. - fn focusPaneByPath(p: *Pardes, path: []const u8, line: usize, col: usize) bool { + fn focusPaneByPath(p: *Pardes, path: []const u8, at: look.Spot) bool { for (p.panes, 0..) |slot, i| { const tt = slot orelse continue; if (tt.image) |iv| if (std.mem.eql(u8, iv.path, path)) { @@ -6096,7 +6131,7 @@ pub const Pardes = struct { }; const ff = if (tt.file) |*f| f else continue; if (!std.mem.eql(u8, ff.path, path)) continue; - p.focusPaneLine(i, line, col); + p.focusPaneLine(i, at); return true; } return false; @@ -6126,7 +6161,7 @@ pub const Pardes = struct { // canonical (realpath'd or dump-given), so match the word as-is // here and joined onto each directory below. const pl = look.parsePathLine(trimmed); - if (pl.path.len > 0 and p.focusPaneByPath(pl.path, pl.line, pl.col)) return; + if (pl.path.len > 0 and p.focusPaneByPath(pl.path, pl.at)) return; // The word is resolved against the pane DIRECTORIES in access // order: the pane the click came from FIRST — its answer is the // one taken, so nothing that resolves today moves — then every @@ -6155,7 +6190,7 @@ pub const Pardes = struct { if (pl.path.len > 0 and pl.path[0] != '/') { var joinbuf: [2048]u8 = undefined; if (std.fmt.bufPrint(&joinbuf, "{s}/{s}", .{ dir, pl.path }) catch null) |j| - if (p.focusPaneByPath(j, pl.line, pl.col)) return; + if (p.focusPaneByPath(j, pl.at)) return; } found = look.resolve(txt, dir, &realbuf); if (found != .none or pl.path.len == 0 or pl.path[0] == '/') break; @@ -6175,7 +6210,7 @@ pub const Pardes = struct { // `@p7:10:5`: pane 7, line 10, column 5 — how a search result // points at a terminal or an output buffer, neither of which // has a path. - .pane => |t| p.focusPaneLine(t.id, t.line, t.col), + .pane => |t| p.focusPaneLine(t.id, t.at), .url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }), .dir => |dir| { // focus an existing terminal on this dir, else fork one below @@ -6197,19 +6232,29 @@ pub const Pardes = struct { }, .file => |target| { // focus an existing pane on this path (rescrolled), else open - if (p.focusPaneByPath(target.path, target.line, target.col)) return; + if (p.focusPaneByPath(target.path, target.at)) return; const free = p.freeSlot() orelse return; - const nt = file_pane.open(p, free, target.path, target.line) catch return; - if (target.col > 0) nt.cur_col = @intCast(target.col - 1); + const nt = file_pane.open(p, free, target.path, target.at.line) catch return; + if (target.at.col > 0) nt.cur_col = @intCast(target.at.col - 1); p.placeDoc(id, free, nt); // center the target line: the pane's real body height only // exists after placement, so lay out now and pull the // scroll up by half a body (line 0 opens stay at the top) p.computeGeom(); nt.file.?.scroll -|= @max(1, p.rects[free].h -| BOX_H) / 2; + // ...and only THEN select a range, restoring that scroll: + // setPaneRange keeps its cursor visible, and a pane this fresh + // has no true geometry yet for it to judge against (pane.rows + // is only refreshed in sync), so the centering just computed is + // the answer and ensureCursorVisible's is not. + if (target.at.end_line != 0) { + const centered = nt.file.?.scroll; + p.selectSpan(nt, target.at); + nt.file.?.scroll = centered; + } }, .image => |target| { - if (p.focusPaneByPath(target.path, 0, 0)) return; + if (p.focusPaneByPath(target.path, .{})) return; const free = p.freeSlot() orelse return; // petscii by default when the host has no kitty graphics const nt = p.openImageView(free, target.path, !p.kitty_ok) catch return; @@ -6716,7 +6761,7 @@ pub const Pardes = struct { if (p.njumps == 0 or next < 0 or next >= p.njumps) return; p.jcur = @intCast(next); const j = p.jumps[p.jcur]; - p.focusPaneLine(j.pane, j.line, j.col); + p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }); } /// Recompute geometry, push grid-size changes to each emulator + pty, fire |
