diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 71 |
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) { |
