summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig133
1 files changed, 116 insertions, 17 deletions
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