diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 4 | ||||
| -rw-r--r-- | src/pardes.zig | 133 |
2 files changed, 118 insertions, 19 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 85a6470d..6a8518ea 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -404,7 +404,7 @@ pub const Help = struct { pub const Find = struct { pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); - if (pat.len > 0) return c.p.runSearch(c.id, pat, .find); + if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top); c.p.startSearch(c.pane, config.find_marker); } }; @@ -414,7 +414,7 @@ pub const Find = struct { pub const Grep = struct { pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); - if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep); + if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top); c.p.startSearch(c.pane, config.grep_marker); } }; diff --git a/src/pardes.zig b/src/pardes.zig index 6245d1f4..9b21ec79 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -1898,6 +1898,17 @@ pub const Pardes = struct { return out.items; } + /// Is (r,c) inside the span (ar,ac)..(br,bc), in reading order? Either end + /// may be given first — a selection swept upwards has its anchor after its + /// head — and the coordinate SYSTEM is the caller's: screen cells for a + /// mouse selection, absolute rows for a modal one. + fn spanHas(r: i32, c: i32, ar: i32, ac: i32, br: i32, bc: i32) bool { + const fwd = ar < br or (ar == br and ac <= bc); + const sr, const sc = if (fwd) .{ ar, ac } else .{ br, bc }; + const er, const ec = if (fwd) .{ br, bc } else .{ ar, ac }; + return (r > sr or (r == sr and c >= sc)) and (r < er or (r == er and c <= ec)); + } + /// acme: a no-drag middle/right click expands to the word under it — /// file-ish, or a whole `` @`...` `` run (config.wordBounds is the spelling) fn expandSel(p: *Pardes, pane: *Pane, sl: *Sel) void { @@ -3736,7 +3747,7 @@ pub const Pardes = struct { .grep else .text; - p.runSearch(id, armed[slash + 1 ..], kind); + p.runSearch(id, armed[slash + 1 ..], kind, .top); } /// Fill this pane's results buffer with everything matching `pat_raw` @@ -3757,7 +3768,13 @@ pub const Pardes = struct { /// 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 { + /// + /// `start` is where the WALK begins, which belongs to the gesture and not + /// to the search: a click POINTS at one of the hits, so its list is armed + /// there and the first step goes to the next one (acme's button-3 walking a + /// word). `/`, Find and Grep point at nothing, so their list is walked from + /// the top, which is also the only place a list of OTHER files could start. + pub fn runSearch(p: *Pardes, id: usize, pat_raw: []const u8, kind: Search, start: enum { top, cursor }) void { const pane = p.panes[id] orelse return; const pat = std.mem.trim(u8, pat_raw, " \t\r\n"); if (pat.len == 0) return; @@ -3766,6 +3783,8 @@ pub const Pardes = struct { // root, and the directory the results buffer is named in. const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); var out: std.ArrayList(u8) = .empty; + var nrows: usize = 0; + var anchor: ?usize = null; if (kind == .grep) { // One walk per PLACE the session is open on: every pane's // directory, minus the ones another pane's already contains, so a @@ -3807,8 +3826,15 @@ pub const Pardes = struct { std.fs.path.basename(pane.file.?.path) else std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{id}) catch return; + // the hit at or before the cursor is the one you are ON, so arming + // there makes the first step land on the NEXT one: a click on the + // second `foo` goes to the third, not back to the first. + const cl: usize = @intCast(@max(0, pane.cur_row)); + const cc: usize = @intCast(@max(0, pane.cur_col)); for (pl.lines, 0..) |ln, i| { const at = std.ascii.indexOfIgnoreCase(ln, pat) orelse continue; + if (start == .cursor and (i < cl or (i == cl and at <= cc))) anchor = nrows; + nrows += 1; // 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", .{ @@ -3818,15 +3844,6 @@ pub const Pardes = struct { } } const content = p.gpa.dupe(u8, out.items) catch return; - // every search opens its OWN buffer, even from the same pane: the - // earlier lists stay open at their sizes and the new one stacks - // directly below this pane, taking its rows from here (placeDoc). - // search_pane is the newest, so n/N step the search just run; focus - // stays here. - const free = p.freeSlot() orelse { - p.gpa.free(content); - return; - }; // the buffer records WHICH search filled it, pattern and all: Find and // Grep are builtins (words you can execute), the bare `/` is a key const from: output_pane.Origin = switch (kind) { @@ -3834,6 +3851,26 @@ pub const Pardes = struct { .find => .{ .cmd = .Find }, .grep => .{ .cmd = .Grep }, }; + // The SAME search asked again REFILLS the list it already opened — + // right-clicking a word in four places is one +Search walked four + // times, not four +Searches over identical rows. A different pattern + // still gets its own buffer, and that IS the old rule: two searches are + // two lists, both stay open at their sizes, and the new one stacks + // directly below this pane (placeDoc). Focus stays here either way. + if (output_pane.resultsFrom(p, pane, from)) { + const rp = p.panes[pane.search_pane.?].?; + const rf = &rp.file.?; + if (std.mem.eql(u8, rf.output.?.arg(), pat)) { + file_pane.setContent(p, rf, content); + rf.scroll = 0; + pane.search_row = anchor; + return; + } + } + const free = p.freeSlot() orelse { + p.gpa.free(content); + return; + }; const np = output_pane.open(p, free, dir, from, pat, content) catch { p.gpa.free(content); return; @@ -3841,7 +3878,7 @@ pub const Pardes = struct { p.placeDoc(id, free, np); p.active = id; pane.search_pane = free; - pane.search_row = null; + pane.search_row = anchor; } /// n/N: step to the next/previous row of this pane's results buffer and @@ -5744,8 +5781,52 @@ pub const Pardes = struct { // acme execute (middle) / look (right): a no-drag click // expands to the word under it first; a captured chord // argument rides along and is consumed here. - p.expandSel(pane, &pane.sel[b]); - const txt = p.selectionText(pane, pane.sel[b]) catch null; + // WHERE THE CLICK LANDED, before any expansion — as a body + // position, the way the left button converts its drag end. + const clk = pane.sel[b]; + const cvis = clk.r0 - @as(i32, BOX_H); + const crow = cvis + pane.scroll(); + const ccol = if (pane.file != null) + @max(0, clk.c0 - @as(i32, config.PREFIX_W)) + pane.hscroll + else + clk.c0; + // A click INSIDE a live selection takes the WHOLE selection + // and expands nothing: you already said what you meant, and + // the file-ish word heuristic can only cut it down — a path + // with a space in it, `git log -p`, half a URL. This is the + // rule the keyboard chords follow (an explicit modal + // selection wins over the word under the cursor), asked of + // where the click landed rather than where the cursor is, + // so a selection left lying in a pane never traps the + // buttons: click OUTSIDE it and the ordinary word expansion + // is back. A kept left sweep is screen-coordinate; the + // modal one (v/x, n/N) is in absolute rows, hence the two + // spellings of the same question. + const ls = pane.sel[sel_slot]; + const held: ?[]const u8 = if (ls.state == .done and + spanHas(clk.r0, clk.c0, ls.r0, ls.c0, ls.r1, ls.c1)) + (p.selectionText(pane, ls) catch null) + else if (pane.vsel.active and pane.vsel.explicit) + (if (spanHas(crow, ccol, pane.vsel.row, pane.vsel.col, pane.cur_row, pane.cur_col)) + p.currentSelText(pane) + else + null) + else if (pane.msel.active and crow >= @min(pane.msel.r0, pane.msel.r1) and + crow <= @max(pane.msel.r0, pane.msel.r1)) + p.currentSelText(pane) + else + null; + if (held == null) p.expandSel(pane, &pane.sel[b]); + // A look that names no file SEARCHES from where you are, so + // the click has to say where that is and the search walks + // forward from there. Not PINNED — a shell's cursor still + // belongs to the shell, and the search reads this in the + // same event. + if (s.button == config.look_button and cvis >= 0 and pane.mode != .tty) { + pane.cur_row = crow; + pane.cur_col = ccol; + } + const txt = held orelse (p.selectionText(pane, pane.sel[b]) catch null); const arg = p.chord_arg; p.chord_arg = null; defer if (arg) |a| p.gpa.free(a); @@ -6312,15 +6393,33 @@ pub const Pardes = struct { } switch (found) { // acme button-3: a word that names no file/dir is a search of - // the pane it was clicked in — exactly what `/` runs (n/N then - // walk the results). Paths (src/a/b.rs:100) still resolve above + // the pane it was clicked in — exactly what `/` runs, and then a + // step onto a hit, so a click GOES somewhere and clicking again + // goes to the next one. Paths (src/a/b.rs:100) still resolve above // and open; only the non-file case falls through here. A shell // searches its scrollback like anything else, EXCEPT in tty // mode, where the click belongs to the program on the other // end; an image pane has no text to search either way. .none => { const bmode = if (pane.tag_edit) pane.tag_mode else pane.mode; - if (pane.image == null and bmode != .tty) p.runSearch(id, txt, .text); + if (pane.image != null or bmode == .tty) return; + // ...and then STEP it, which is the other half of button-3: a + // click does not merely LIST the hits, it goes to one — the + // one AFTER the word clicked, since runSearch armed the walk + // where the click put the cursor. Asking again is free: the + // same pattern refills its own list rather than opening a + // second, so clicking a word repeatedly walks its hits. + p.runSearch(id, trimmed, .text, .cursor); + const at = pane.search_row; + _ = p.searchStep(id, 1); + // past the last hit, back to the first: acme's search is a + // RING, and a step that could not move left the row where it + // was (searchStep clamps rather than wrapping, because n/N are + // also how `]d`/`[d` walk to the end of a list and stop). + if (at != null and pane.search_row == at) { + pane.search_row = null; + _ = p.searchStep(id, 1); + } }, // `@p7:10:5`: pane 7, line 10, column 5 — how a search result // points at a terminal or an output buffer, neither of which |
