summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 18:59:33 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commitb7914a0bd59334f618b62e0b98271211bbec46b9 (patch)
tree53af62ce0ae022e580e35aadbd952e1656dcfa30
parent3f4f26e0e40d06427e72f72e760d01adcdc0326c (diff)
downloadpardes-b7914a0bd59334f618b62e0b98271211bbec46b9.tar.gz
pardes-b7914a0bd59334f618b62e0b98271211bbec46b9.zip
Clear previous selections before resolving Look destinations
-rw-r--r--src/panes.zig5
-rw-r--r--src/pardes.zig62
-rw-r--r--test/panes.zig110
-rw-r--r--test/pdf.zig26
4 files changed, 189 insertions, 14 deletions
diff --git a/src/panes.zig b/src/panes.zig
index 59993148..c3fa732e 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -5371,7 +5371,10 @@ pub const Pdf = struct {
const destination = owner.state.sectionDestination(core.pdf_gpa, at.col - 1) orelse return true;
switch (destination) {
- .internal => |internal| revealOutlineDestination(core, owner.pane, internal),
+ .internal => |internal| {
+ core.clearLookSelection(owner.pane);
+ revealOutlineDestination(core, owner.pane, internal);
+ },
.external => |uri| if (uri.len <= 256) core.emit(.{ .open_link = .from(uri) }),
.none => unreachable,
}
diff --git a/src/pardes.zig b/src/pardes.zig
index c78d4702..8bccfd89 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -9446,6 +9446,9 @@ pub const Pardes = struct {
p.runBuiltin(config.exec_cmd, rid, "", std.mem.trim(u8, ln, " \t\r"));
} else if (span) |selected| {
p.lookAt(rid, ln[selected.start..selected.end]);
+ // This is the newly visited result, not a selection carried into
+ // the destination. Look cleared the previous source selection.
+ rp.msel = .{ .active = true, .r0 = r, .r1 = r };
}
p.armLookWalk(rid);
return true;
@@ -12217,10 +12220,32 @@ pub const Pardes = struct {
fn focusPaneByPath(p: *Pardes, path: []const u8, at: look.Spot) bool {
const target = p.openPaneTarget(path, at) orelse return false;
+ p.clearLookSelection(p.panes[target.pane.id].?);
p.focusPaneLine(target.pane.id, target.pane.at, .center);
return true;
}
+ /// Look starts a new selection; history navigation keeps the old one.
+ pub fn clearLookSelection(p: *Pardes, pane: *Pane) void {
+ pane.vsel = .{};
+ pane.msel = .{};
+ pane.nsel = 0;
+ pane.select = false;
+ pane.normal.clear();
+ pane.append_at = null;
+ pane.sticky_col = -1;
+ for (0..pane.sel.len) |slot| pane.clearPointerSelection(slot);
+ pane.sel = @splat(.{});
+ if (comptime pdf_enabled) if (pane.pdf) |*state| {
+ state.clearDrag();
+ state.clearSelection(p.pdf_gpa);
+ };
+ if (p.drag == .select and p.panes[p.drag.select.id] == pane) p.drag = .none;
+ if (p.jump_selection_cursor) |saved| if (saved.serial == pane.serial) {
+ p.jump_selection_cursor = null;
+ };
+ }
+
/// Resolve without changing focus or falling back to search. PDF links use
/// the same location rules as an ordinary Look before offering a choice.
fn resolveLookTarget(p: *Pardes, id: usize, txt: []const u8, realbuf: *[4096]u8) look.Target {
@@ -12308,6 +12333,7 @@ pub const Pardes = struct {
const pane = p.panes[id] orelse return;
const link = probe.link orelse return;
const target = p.pdfLinkLocation(pane, link) orelse return;
+ p.clearLookSelection(pane);
if (p.canonicalLookLocation(id, probe.text)) |visible| {
if (!std.mem.eql(u8, visible, target)) {
const content = std.fmt.allocPrint(p.gpa, "{s}\n{s}\n", .{ visible, target }) catch |err|
@@ -12329,15 +12355,19 @@ pub const Pardes = struct {
}
}
- pub fn lookAt(p: *Pardes, id: usize, txt: []const u8) void {
+ pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void {
const pane = p.panes[id] orelse return;
p.noteHaptic(.look);
p.noteLookSource(id);
- const trimmed = std.mem.trim(u8, txt, " \t\r\n");
- if (config.commandWord(trimmed)) |cmd| {
+ const command = std.mem.trim(u8, operand, " \t\r\n");
+ if (config.commandWord(command)) |cmd| {
if (p.execute(id, cmd)) |dst| p.active = dst;
return;
}
+ // The operand may borrow selected terminal/PDF text released below.
+ const txt = p.scratch.allocator().dupe(u8, operand) catch return;
+ const trimmed = std.mem.trim(u8, txt, " \t\r\n");
+ p.clearLookSelection(pane);
const pl = look.parsePathLine(trimmed);
if (comptime pdf_enabled) if (panes.Pdf.lookSection(p, id, pl.path, pl.at)) return;
var realbuf: [4096]u8 = undefined;
@@ -12357,11 +12387,15 @@ pub const Pardes = struct {
_ = p.searchStep(id, 1);
}
},
- .pane => |t| p.focusPaneLine(t.id, t.at, .center),
+ .pane => |t| {
+ p.clearLookSelection(p.panes[t.id].?);
+ p.focusPaneLine(t.id, t.at, .center);
+ },
.url => |u| if (u.len <= 256) p.emit(.{ .open_link = .from(u) }),
.dir => |dir| {
for (p.panes, 0..) |slot, i| {
if (slot) |tt| if (std.mem.eql(u8, tt.cwdSlice(), dir) and p.takesCommandLine(i)) {
+ p.clearLookSelection(tt);
p.active = i;
p.emitWrite(i, "ls\r");
return;
@@ -14931,6 +14965,26 @@ test "board heap: every allocation failure during boot is a clean OutOfMemory" {
);
}
+test "Look copies its operand before releasing pointer selection storage" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12 });
+ defer p.deinit();
+ const pane = try p.setTestFile("alpha beta\ngamma delta\nepsilon zeta\n");
+ const operand = try p.gpa.dupe(u8, "/test.txt:3:2");
+ pane.sel[0] = .{ .state = .done, .source_id = 1 };
+ pane.pointer_selections[0] = .{
+ .gesture = pane.sel[0],
+ .rows = try p.gpa.alloc(Pane.PointerRow, 0),
+ .raw_text = operand,
+ };
+ p.drag = .{ .select = .{ .id = 0, .button = config.select_button } };
+ p.lookAt(0, operand);
+ try std.testing.expectEqual(@as(i32, 2), pane.cur_row);
+ try std.testing.expectEqual(@as(i32, 1), pane.cur_col);
+ try std.testing.expect(pane.pointer_selections[0] == null);
+ try std.testing.expectEqual(.none, pane.sel[0].state);
+ try std.testing.expectEqual(.none, p.drag);
+}
+
test "jump history preserves modal and mouse selections" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 60, .rows = 12 });
defer p.deinit();
diff --git a/test/panes.zig b/test/panes.zig
index 28af8141..cb2e8169 100644
--- a/test/panes.zig
+++ b/test/panes.zig
@@ -4256,3 +4256,113 @@ test "terminal overlays preserve trailing blank styles without coloring inserted
try std.testing.expectEqual(colors[2], extended.at(x, y + 4).style.bg);
try std.testing.expect(p.shell_rows.pane == null);
}
+
+const LookResetTests = struct {
+ fn seed(pane: *Pane, operand: []const u8) !void {
+ pane.cur_row = 0;
+ pane.cur_col = 3;
+ pane.vsel = .{ .active = true, .explicit = true, .row = 0, .col = 0 };
+ pane.msel = .{ .active = true, .r0 = 0, .r1 = 1 };
+ pane.sels[0] = .{ .row = 1, .col = 2, .arow = 0, .acol = 1 };
+ pane.nsel = 1;
+ pane.select = true;
+ pane.normal.count = 5;
+ pane.append_at = .{ .row = 0, .col = 3 };
+ pane.sticky_col = 7;
+ for (0..pane.sel.len) |slot| {
+ pane.clearPointerSelection(slot);
+ pane.sel[slot] = .{ .state = .done, .source_id = slot + 1, .r0 = 1, .r1 = 1, .c0 = 0, .c1 = 3 };
+ const rows = try pane.gpa.alloc(Pane.PointerRow, 1);
+ rows[0] = .{ .row = 0, .lo = 0, .hi = 4 };
+ pane.pointer_selections[slot] = .{
+ .gesture = pane.sel[slot],
+ .rows = rows,
+ .raw_text = try pane.gpa.dupe(u8, operand),
+ };
+ }
+ }
+
+ fn cleared(pane: *const Pane) !void {
+ try std.testing.expect(!pane.msel.active);
+ try std.testing.expectEqual(@as(u8, 0), pane.nsel);
+ try std.testing.expect(!pane.select);
+ try std.testing.expectEqual(modal.Normal.State{}, pane.normal);
+ try std.testing.expect(pane.append_at == null);
+ try std.testing.expectEqual(@as(i32, -1), pane.sticky_col);
+ for (pane.sel) |selection| try std.testing.expectEqual(.none, selection.state);
+ for (pane.pointer_selections) |selection| try std.testing.expect(selection == null);
+ }
+
+ fn selected(pane: *const Pane) []const u8 {
+ if (!pane.vsel.active or pane.cur_row != pane.vsel.row) return "";
+ const line = modal.lineSlice(pane.file.?.content, @intCast(pane.cur_row));
+ const lo: usize = @intCast(@min(pane.cur_col, pane.vsel.col));
+ const hi: usize = @intCast(@max(pane.cur_col, pane.vsel.col));
+ return line[lo..modal.nextGrapheme(line, hi)];
+ }
+
+ test "Look replaces old selection for same-pane addresses ranges and text fallback" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ const pane = try p.setTestFile("alpha\nbravo\ncharlie\nneedle\n");
+ try seed(pane, "/test.txt:3:2");
+ // This operand is released by Look's selection cleanup.
+ p.lookAt(0, pane.pointer_selections[0].?.raw_text.?);
+ try cleared(pane);
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+ try std.testing.expectEqual(@as(i32, 2), pane.cur_row);
+ try std.testing.expectEqual(@as(i32, 1), pane.cur_col);
+ try std.testing.expect(!pane.vsel.active);
+
+ try seed(pane, "/test.txt:2:2-4");
+ p.lookAt(0, pane.pointer_selections[0].?.raw_text.?);
+ try cleared(pane);
+ try std.testing.expectEqual(@as(i32, 1), pane.cur_row);
+ try std.testing.expectEqualStrings("rav", selected(pane));
+
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = try p.gpa.dupe(u8, "/tmp/look-selection-source.txt");
+ try seed(pane, "needle");
+ p.lookAt(0, pane.pointer_selections[0].?.raw_text.?);
+ try cleared(pane);
+ try std.testing.expectEqual(@as(i32, 3), pane.cur_row);
+ try std.testing.expectEqualStrings("needle", selected(pane));
+ }
+
+ test "Look clears both source and existing destination selections before focus" {
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ const source = try p.setTestFile("source\nlines\n");
+ const id = p.freeSlot().?;
+ const destination = try p.newDocPane(id);
+ destination.file = .{
+ .path = try p.gpa.dupe(u8, "/look-destination.txt"),
+ .content = try p.gpa.dupe(u8, "alpha\nbravo\ncharlie\n"),
+ .history = try panes.File.History.create(p.gpa),
+ };
+ layout.insert(p, 0, 1, id);
+ layout.compute(p);
+ for ([_]bool{ false, true }) |range| {
+ p.active = 0;
+ try seed(source, if (range) "/look-destination.txt:2:2-4" else "/look-destination.txt:3:2");
+ try seed(destination, "previous destination");
+ p.lookAt(0, source.pointer_selections[0].?.raw_text.?);
+ try std.testing.expectEqual(id, p.active);
+ try cleared(source);
+ try cleared(destination);
+ try std.testing.expect(!source.vsel.active);
+ if (range) {
+ try std.testing.expectEqual(@as(i32, 1), destination.cur_row);
+ try std.testing.expectEqualStrings("rav", selected(destination));
+ } else {
+ try std.testing.expectEqual(@as(i32, 2), destination.cur_row);
+ try std.testing.expectEqual(@as(i32, 1), destination.cur_col);
+ try std.testing.expect(!destination.vsel.active);
+ }
+ }
+ }
+};
+
+test {
+ _ = LookResetTests;
+}
diff --git a/test/pdf.zig b/test/pdf.zig
index 113ed8b9..7a05447e 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -120,8 +120,12 @@ const PdfLinkTests = struct {
defer frame.deinit();
_ = try p.render(frame.allocator());
while (p.nextEffect()) |_| {}
- click(p, try textCell(p, "target.txt"));
+ const target_cell = try textCell(p, "target.txt");
+ try std.testing.expect(panes.Pdf.beginSelection(p, p.panes[0].?, target_cell.col, target_cell.row));
+ try std.testing.expect(p.panes[0].?.pdf.?.selection != null);
+ click(p, target_cell);
try expectOpened(p, null);
+ try std.testing.expect(p.panes[0].?.pdf.?.selection == null);
const choices = for (p.panes, 0..) |slot, id| {
if (id == 0) continue;
if (slot) |pane| if (pane.file) |file| if (file.output != null) break pane;
@@ -146,7 +150,7 @@ const PdfLinkTests = struct {
for (0..(config.look_preview_delay_frames orelse 0)) |_| p.update(.tick);
}
- test "PDF links hover clears on leave page and reload while right drag selects text" {
+ test "PDF links hover clears on leave page and reload while right drag chooses Look text" {
if (!pdf_enabled or platform == .web) return;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
@@ -181,10 +185,13 @@ const PdfLinkTests = struct {
const label = try textCell(p, "label");
p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = label.col, .row = label.row } });
p.update(.{ .mouse = .{ .button = config.look_button, .kind = .drag, .col = label.col + 3, .row = label.row } });
- p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = label.col + 3, .row = label.row } });
- try expectOpened(p, null);
try std.testing.expect(pane.pdf.?.selection != null);
try std.testing.expect(pane.pdf.?.selection_text.len > 0);
+ p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = label.col + 3, .row = label.row } });
+ try expectOpened(p, null);
+ try std.testing.expect(pane.pdf.?.selection == null);
+ try std.testing.expect(pane.pdf.?.selection_text.len == 0);
+ try std.testing.expect(pane.pdf.?.search_query.len > 0);
}
};
@@ -1844,8 +1851,8 @@ test "PDF native mouse selection, Look, and highlights share page geometry" {
}
// A native right-click resolves the same MuPDF-snapped word and feeds it
- // to Look. Search highlights precede selection highlights so the live
- // selection remains visually authoritative where they overlap.
+ // to Look. Resolving it clears the previous selection, leaving the new
+ // search highlights without carrying an old selection into the result.
p.update(.{ .mouse = .{ .button = config.look_button, .kind = .press, .col = word_col, .row = selected_row } });
p.update(.{ .mouse = .{ .button = config.look_button, .kind = .release, .col = word_col, .row = selected_row } });
try std.testing.expect(std.ascii.indexOfIgnoreCase(pv.search_query, "Pardes") != null);
@@ -1855,9 +1862,9 @@ test "PDF native mouse selection, Look, and highlights share page geometry" {
p.scratch.allocator(),
panes.Pdf.highlightInput(p, 0, pane),
)).forPage(pv.page);
- try std.testing.expect(highlights.len > 1);
- try std.testing.expectEqual(pdf_impl.HighlightKind.search, highlights[0].kind);
- try std.testing.expectEqual(pdf_impl.HighlightKind.selection, highlights[highlights.len - 1].kind);
+ try std.testing.expect(highlights.len > 0);
+ for (highlights) |highlight| try std.testing.expectEqual(pdf_impl.HighlightKind.search, highlight.kind);
+ try std.testing.expect(pv.selection == null and pv.selection_text.len == 0);
const revision_before_reveal = raster.revision;
const max_document_scroll = @as(f64, @floatFromInt(
pv.document_height -| panes.Pdf.paneViewport(p, pane).?.pixel_h,
@@ -1928,6 +1935,7 @@ test "PDF native mouse selection, Look, and highlights share page geometry" {
// Unsupported text-selection actions are consumed by the PDF adapter:
// they neither invent a range over placeholder cells nor disturb the
// native MuPDF selection. The select-button's no-drag click still clears.
+ try std.testing.expect(panes.Pdf.beginSelection(p, pane, word_col, selected_row));
p.update(.{ .key = .{ .cp = 'v' } });
try std.testing.expect(pv.selection != null);
try std.testing.expect(!pane.vsel.active);