summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 12:54:29 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commitfc2738eea560769c8d150a7cc955fe8d54121298 (patch)
tree4545ef73972d98f52529795f4b49c2bf9e37f8d7 /src
parentc787d59a987ec601cd7b65399543afe6d37a615d (diff)
downloadpardes-fc2738eea560769c8d150a7cc955fe8d54121298.tar.gz
pardes-fc2738eea560769c8d150a7cc955fe8d54121298.zip
Align file clicks and insertion cursors
Diffstat (limited to 'src')
-rw-r--r--src/pardes.zig71
1 files changed, 55 insertions, 16 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index efd82474..1b2e7ff5 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -3879,7 +3879,7 @@ test "Unicode display cells map back to body and tag byte cursors" {
try std.testing.expectEqualStrings(before, pane.tagSlice());
}
-test "tabbed file aligns syntax cursor and mouse while preserving virtual columns" {
+test "tabbed file aligns syntax cursor and mouse at the nearest insertion gap" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 12 });
defer p.deinit();
@@ -3916,11 +3916,11 @@ test "tabbed file aligns syntax cursor and mouse while preserving virtual column
const blank_x = text_x + @as(u16, @intCast(panes.File.displayWidth(line))) + virtual;
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = blank_x, .row = body_y } });
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = blank_x, .row = body_y } });
- try std.testing.expectEqual(@as(i32, @intCast(line.len + @as(usize, virtual))), pane.cur_col);
+ try std.testing.expectEqual(@as(i32, @intCast(line.len)), pane.cur_col);
_ = frame.reset(.retain_capacity);
const virtual_surface = try p.render(frame.allocator());
- try std.testing.expectEqual(blank_x, virtual_surface.cursor.?.x);
+ try std.testing.expectEqual(blank_x - virtual, virtual_surface.cursor.?.x);
const local_blank: i32 = @intCast(blank_x - rect.x - config.GUTTER);
try std.testing.expect(p.expandedSel(pane, .{ .state = .dragging, .c0 = local_blank, .c1 = local_blank, .r0 = BOX_H, .r1 = BOX_H }) == null);
@@ -7697,9 +7697,17 @@ pub const Pardes = struct {
var result: PointerOperand = .{ .row = row, .col = col };
const kept = pane.sel[sel_slot];
- if (kept.state == .done and
- spanHas(clicked.r0, clicked.c0, kept.r0, kept.c0, kept.r1, kept.c1))
- {
+ const inside_kept = if (pane.pointerSelection(kept)) |rows| inside: {
+ if (clicked.r0 < BOX_H) break :inside false;
+ if (pane.file != null and clicked.c0 < panes.File.gutterWidth(pane)) break :inside false;
+ const source_col: usize = @intCast(@max(0, col));
+ for (rows) |span| {
+ if (span.row == row and source_col >= span.lo and source_col < span.hi)
+ break :inside true;
+ }
+ break :inside false;
+ } else spanHas(clicked.r0, clicked.c0, kept.r0, kept.c0, kept.r1, kept.c1);
+ if (kept.state == .done and inside_kept) {
result.text = p.selectionText(pane, kept) catch null;
result.preview = kept;
return result;
@@ -9420,9 +9428,23 @@ pub const Pardes = struct {
const InsertAt = enum { at, append, line_start, line_end, open_below, open_above };
+ /// Files insert only inside existing rows and at grapheme boundaries.
+ /// Terminal overlays can materialize virtual rows/columns, so keep those.
+ fn clampFileCursor(pane: *Pane) bool {
+ const file = if (pane.file) |*file| file else return false;
+ const old_row = pane.cur_row;
+ const old_col = pane.cur_col;
+ const last = panes.File.nlines(pane.gpa, file) -| 1;
+ pane.cur_row = @intCast(@min(last, @as(usize, @intCast(@max(0, pane.cur_row)))));
+ const line = panes.File.sourceLine(pane, pane.cur_row);
+ pane.cur_col = @intCast(modal.graphemeStart(line, @min(line.len, @as(usize, @intCast(@max(0, pane.cur_col))))));
+ return old_row != pane.cur_row or old_col != pane.cur_col;
+ }
+
fn enterInsert(p: *Pardes, pane: *Pane, where: InsertAt, cnt: usize) void {
if (pane.hasPdf()) return;
p.pinPaneCursor(pane);
+ _ = clampFileCursor(pane);
// snapshot once per insert session (WITH the pre-insert selection) so
// `u` undoes the whole session and restores what was selected
p.pushUndo(pane);
@@ -9532,6 +9554,7 @@ pub const Pardes = struct {
if (hit(key, config.insert_delete_alias)) return p.insertKey(pane, .{ .cp = Key.delete });
pane.vsel.active = false;
if (!pane.cur_pinned) p.pinPaneCursor(pane);
+ _ = clampFileCursor(pane);
switch (key.cp) {
Key.left, Key.right, Key.up, Key.down => {
const lines = p.paneCursorLines(pane) catch return;
@@ -10942,7 +10965,7 @@ pub const Pardes = struct {
p.drag = .{ .tag = .{ .id = id } };
} else {
p.active = id;
- exitTagEdit(pane); // clicking the body leaves tag editing
+ if (pane.tag_edit) exitTagEdit(pane);
const sc: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
const v: i32 = @as(i32, mrow) - @as(i32, body_y) + @as(i32, BOX_H);
pane.clearPointerSelection(sel_slot);
@@ -11127,6 +11150,19 @@ pub const Pardes = struct {
return .{ .q0 = q0, .q1 = @intCast(@min(q0 + text.len, tag.len)) };
}
const f = if (pane.file) |*file| file else return .{};
+ if (operand.preview) |selection| if (pane.pointerSelection(selection)) |rows| {
+ if (rows.len == 0) return .{};
+ const first = rows[0];
+ const last = rows[rows.len - 1];
+ const q0 = panes.File.lineStart(p.gpa, f, @intCast(@max(0, first.row))) +
+ @min(first.lo, panes.File.sourceLine(pane, first.row).len);
+ const q1 = panes.File.lineStart(p.gpa, f, @intCast(@max(0, last.row))) +
+ @min(last.hi, panes.File.sourceLine(pane, last.row).len);
+ // A rectangle may contain disjoint pieces. Such an operand uses
+ // the event's expansion text instead of inventing a source range.
+ if (q1 < q0 or !std.mem.eql(u8, f.content[q0..q1], text)) return .{};
+ return .{ .q0 = @intCast(q0), .q1 = @intCast(q1) };
+ };
const start: ?modal.Cursor = if (operand.file_word) |w|
.{ .row = @intCast(@max(0, w.row)), .col = @intCast(@max(0, w.lo)) }
else if (operand.expanded orelse operand.preview) |sel| lead: {
@@ -11248,11 +11284,14 @@ pub const Pardes = struct {
sl.c1 - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0),
);
pane.cur_pinned = true;
- if (!pane.isTerminal()) pane.mode = .normal;
+ if (clampFileCursor(pane)) pane.ensureCursorVisible();
pane.msel.active = false;
pane.normal.clear();
pane.vsel.active = false;
pane.nsel = 0; // a click says WHERE the one cursor is
+ pane.select = false;
+ pane.append_at = null;
+ pane.sticky_col = -1;
if (s.ctrl) p.lspRequest(s.id, .definition, "");
}
} else {
@@ -11265,6 +11304,7 @@ pub const Pardes = struct {
if (s.button == config.look_button and clk.r0 >= BOX_H and pane.mode != .tty) {
pane.cur_row = operand.row;
pane.cur_col = operand.col;
+ if (clampFileCursor(pane)) pane.ensureCursorVisible();
}
const txt = operand.text;
const cmd = if (s.button == config.look_button) config.look_cmd else config.exec_cmd;
@@ -12770,8 +12810,8 @@ pub const Pardes = struct {
}
fn paintPointerSelection(
- s: *Surface,
- pane: *const Pane,
+ p: *Pardes,
+ pane: *Pane,
r: Rect,
tx: u16,
tw: u16,
@@ -12781,6 +12821,8 @@ pub const Pardes = struct {
bg: [3]u8,
fg: ?[3]u8,
) void {
+ if (pane.pointerSelection(sl)) |rows| return p.paintSourceSelection(pane, r, rows, bg, fg);
+ const s = &p.surface;
const r0 = @min(sl.r0, sl.r1);
const r1 = @max(sl.r0, sl.r1);
const c0 = @max(0, @min(sl.c0, sl.c1));
@@ -12878,7 +12920,7 @@ pub const Pardes = struct {
if (p.look_hover_preview) |preview| {
if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < BOX_H) {
const preview_bg = mix(page_rgb, mix(page_rgb, th.sel_bg));
- paintPointerSelection(s, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
+ p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
};
}
if (pane.tag_edit and pane.tag_sel) {
@@ -12968,7 +13010,7 @@ pub const Pardes = struct {
panes.File.paintWordSelection(p, pane, r, word.row, word.lo, word.hi, preview_bg)
else if (preview.sel) |sel|
if (@max(sel.r0, sel.r1) >= BOX_H)
- paintPointerSelection(s, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
+ p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
}
}
const modal_hover = if (p.look_hover_preview) |preview|
@@ -12978,10 +13020,7 @@ pub const Pardes = struct {
for (pane.sel, 0..) |sl, b| {
if (sl.state == .none) continue;
if (sl.state == .done and b != 0) continue;
- if (pane.pointerSelection(sl)) |rows|
- p.paintSourceSelection(pane, r, rows, sel_btn[b], th.sel_fg)
- else
- paintPointerSelection(s, pane, r, tx, tw, tag_y, body_y, sl, sel_btn[b], th.sel_fg);
+ p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sl, sel_btn[b], th.sel_fg);
}
// modal line selection (helix `x`): whole body rows, normal mode only
if ((pane.mode == .normal or modal_hover) and pane.msel.active) {