diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 18:59:33 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | b7914a0bd59334f618b62e0b98271211bbec46b9 (patch) | |
| tree | 53af62ce0ae022e580e35aadbd952e1656dcfa30 | |
| parent | 3f4f26e0e40d06427e72f72e760d01adcdc0326c (diff) | |
| download | pardes-b7914a0bd59334f618b62e0b98271211bbec46b9.tar.gz pardes-b7914a0bd59334f618b62e0b98271211bbec46b9.zip | |
Clear previous selections before resolving Look destinations
| -rw-r--r-- | src/panes.zig | 5 | ||||
| -rw-r--r-- | src/pardes.zig | 62 | ||||
| -rw-r--r-- | test/panes.zig | 110 | ||||
| -rw-r--r-- | test/pdf.zig | 26 |
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); |
