summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/pardes.zig84
1 files changed, 61 insertions, 23 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 4ce4d101..e2ad426f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2824,11 +2824,11 @@ test "surface print expands configured tabs and normalizes other controls" {
try std.testing.expectEqualStrings("A", cells[cell_count - 1].grapheme());
}
-test "tabbed file aligns syntax cursor and mouse with display cells" {
+test "tabbed file aligns syntax cursor and mouse while preserving virtual columns" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .cols = 60, .rows = 12 });
defer p.deinit();
- const pane = try p.hxOpenFileContent("\treturn x;\n");
+ const pane = try p.hxOpenFileContent("\treturn x\n");
const f = &pane.file.?;
f.highlights = try p.tree_sitter_gpa.alloc(u8, f.content.len);
@memset(f.highlights, @intFromEnum(syntax.Syn.none));
@@ -2853,6 +2853,27 @@ test "tabbed file aligns syntax cursor and mouse with display cells" {
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .press, .col = click_x, .row = body_y } });
p.update(.{ .mouse = .{ .button = config.select_button, .kind = .release, .col = click_x, .row = body_y } });
try std.testing.expectEqual(@as(i32, 3), pane.cur_col);
+
+ // Display-column conversion must not turn a click in the blank space
+ // after EOL into a click on the final byte. Besides moving the modal
+ // cursor, that would make a no-drag Look expand the last word instead of
+ // remaining inert over blank space.
+ const line = modal.lineSlice(f.content, 0);
+ const virtual: u16 = 3;
+ const blank_x = text_x + @as(u16, @intCast(file_pane.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);
+
+ _ = frame.reset(.retain_capacity);
+ const virtual_surface = try p.render(frame.allocator());
+ try std.testing.expectEqual(blank_x, virtual_surface.cursor.?.x);
+
+ const local_blank: i32 = @intCast(blank_x - rect.x - config.GUTTER);
+ var look_sel = Sel{ .state = .dragging, .c0 = local_blank, .c1 = local_blank, .r0 = BOX_H, .r1 = BOX_H };
+ p.expandSel(pane, &look_sel);
+ try std.testing.expectEqual(local_blank, look_sel.c0);
+ try std.testing.expectEqual(local_blank, look_sel.c1);
}
test "plain left click clears explicit modal selection" {
@@ -3559,6 +3580,20 @@ const Prompt = union(enum) {
pipe: u16,
};
+/// File cursor columns may live past EOL. Tabs expand before that boundary;
+/// every virtual column after it is still one screen cell, as it was before
+/// display-cell-aware file coordinates were introduced.
+fn fileRawDisplayCol(line: []const u8, raw_col: usize) usize {
+ const bounded = @min(raw_col, line.len);
+ return file_pane.displayWidth(line[0..bounded]) +| (raw_col - bounded);
+}
+
+fn fileRawAtDisplay(line: []const u8, display_col: usize) usize {
+ const width = file_pane.displayWidth(line);
+ if (display_col > width) return line.len +| (display_col - width);
+ return file_pane.byteAtDisplay(line, display_col);
+}
+
pub const Pane = struct {
vt: ghostty_vt.Terminal,
stream: ghostty_vt.TerminalStream,
@@ -3969,16 +4004,16 @@ pub const Pane = struct {
const w: i32 = @max(1, @as(i32, pane.cols) - @as(i32, config.PREFIX_W));
const hmargin: i32 = @min(config.scroll_off, @divTrunc(w - 1, 2));
const line = modal.lineSlice(f.content, @intCast(@max(0, pane.cur_row)));
- const raw_cur = @min(@as(usize, @intCast(@max(0, pane.cur_col))), line.len);
- const raw_scroll = @min(@as(usize, @intCast(@max(0, pane.hscroll))), line.len);
- const cur = @as(i32, @intCast(file_pane.displayWidth(line[0..raw_cur])));
- const visual_scroll = @as(i32, @intCast(file_pane.displayWidth(line[0..raw_scroll])));
+ const raw_cur: usize = @intCast(@max(0, pane.cur_col));
+ const raw_scroll: usize = @intCast(@max(0, pane.hscroll));
+ const cur = @as(i32, @intCast(fileRawDisplayCol(line, raw_cur)));
+ const visual_scroll = @as(i32, @intCast(fileRawDisplayCol(line, raw_scroll)));
var target = visual_scroll;
if (cur < visual_scroll + hmargin)
target = @max(0, cur - hmargin)
else if (cur > visual_scroll + w - 1 - hmargin)
target = cur - (w - 1 - hmargin);
- if (target != visual_scroll) pane.hscroll = @intCast(file_pane.byteAtDisplay(line, @intCast(target)));
+ if (target != visual_scroll) pane.hscroll = @intCast(fileRawAtDisplay(line, @intCast(target)));
}
}
@@ -4538,7 +4573,7 @@ pub const Pardes = struct {
const pane = try p.gpa.create(Pane);
errdefer p.gpa.destroy(pane);
pane.* = .{
- .vt = try ghostty_vt.Terminal.init(p.gpa, .{
+ .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), p.gpa, .{
.cols = p.screen_w,
.rows = p.screen_h,
.max_scrollback = 16 * 1024 * 1024,
@@ -4567,7 +4602,7 @@ pub const Pardes = struct {
const pane = try p.gpa.create(Pane);
errdefer p.gpa.destroy(pane);
pane.* = .{
- .vt = try ghostty_vt.Terminal.init(p.gpa, .{ .cols = 1, .rows = 1 }),
+ .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), p.gpa, .{ .cols = 1, .rows = 1 }),
.stream = undefined,
.gpa = p.gpa,
.cols = p.screen_w,
@@ -5277,10 +5312,12 @@ pub const Pardes = struct {
fn fileDisplayOffset(pane: *const Pane, row: i32, from_raw: i32, to_raw: i32) i32 {
const line = fileLine(pane, row);
- const from = @min(@as(usize, @intCast(@max(0, from_raw))), line.len);
- const to = @min(@as(usize, @intCast(@max(0, to_raw))), line.len);
- if (to >= from) return @intCast(file_pane.displayWidth(line[from..to]));
- return -@as(i32, @intCast(file_pane.displayWidth(line[to..from])));
+ const from: usize = @intCast(@max(0, from_raw));
+ const to: usize = @intCast(@max(0, to_raw));
+ const from_display = fileRawDisplayCol(line, from);
+ const to_display = fileRawDisplayCol(line, to);
+ if (to_display >= from_display) return @intCast(to_display - from_display);
+ return -@as(i32, @intCast(from_display - to_display));
}
fn fileDisplayEndOffset(pane: *const Pane, row: i32, from_raw: i32, at_raw: i32) i32 {
@@ -5293,23 +5330,24 @@ pub const Pardes = struct {
fn fileByteAtDisplay(pane: *const Pane, row: i32, from_raw: i32, display_col: i32) i32 {
const line = fileLine(pane, row);
- const from = @min(@as(usize, @intCast(@max(0, from_raw))), line.len);
+ const from: usize = @intCast(@max(0, from_raw));
const display: usize = @intCast(@max(0, display_col));
- return @intCast(from + file_pane.byteAtDisplay(line[from..], display));
+ if (from >= line.len) return @intCast(from +| display);
+ return @intCast(from +| fileRawAtDisplay(line[from..], display));
}
fn renderedLineByteCol(pane: *const Pane, row: i32, line: []const u8, display_col: usize) usize {
- if (pane.file == null or row < BOX_H) return @min(display_col, line.len);
+ if (pane.file == null or row < BOX_H) return display_col;
const prefix = @min(@as(usize, config.PREFIX_W), line.len);
if (display_col <= prefix) return display_col;
- return prefix + file_pane.byteAtDisplay(line[prefix..], display_col - prefix);
+ return prefix +| fileRawAtDisplay(line[prefix..], display_col - prefix);
}
fn renderedLineDisplayCol(pane: *const Pane, row: i32, line: []const u8, byte_col: usize) usize {
- if (pane.file == null or row < BOX_H) return @min(byte_col, line.len);
+ if (pane.file == null or row < BOX_H) return byte_col;
const prefix = @min(@as(usize, config.PREFIX_W), line.len);
if (byte_col <= prefix) return byte_col;
- return prefix + file_pane.displayWidth(line[prefix..@min(byte_col, line.len)]);
+ return prefix +| fileRawDisplayCol(line[prefix..], byte_col - prefix);
}
/// tag + '\n' + body: the full selectable pane text (row 0 = the tag)
@@ -5333,7 +5371,7 @@ pub const Pardes = struct {
var count_row: i32 = 0;
while (count_it.next()) |line| : (count_row += 1) {
if (count_row < r0 or count_row > r1) continue;
- const b0 = renderedLineByteCol(pane, count_row, line, c0);
+ const b0 = @min(renderedLineByteCol(pane, count_row, line, c0), line.len);
const b1 = @min(renderedLineByteCol(pane, count_row, line, c1) + 1, line.len);
total += b1 - b0 + @intFromBool(selected > 0);
selected += 1;
@@ -5350,7 +5388,7 @@ pub const Pardes = struct {
at += 1;
}
first = false;
- const b0 = renderedLineByteCol(pane, v, line, c0);
+ const b0 = @min(renderedLineByteCol(pane, v, line, c0), line.len);
const b1 = @min(renderedLineByteCol(pane, v, line, c1) + 1, line.len);
@memcpy(out[at..][0 .. b1 - b0], line[b0..b1]);
at += b1 - b0;
@@ -12132,7 +12170,7 @@ pub const Pardes = struct {
const t = src.terminal.?;
const pane = try gpa.create(Pane);
pane.* = .{
- .vt = try ghostty_vt.Terminal.init(gpa, .{
+ .vt = try ghostty_vt.Terminal.init(std.Io.Threaded.global_single_threaded.io(), gpa, .{
.cols = @max(1, src.cols),
.rows = @max(1, src.rows),
.max_scrollback = 16 * 1024 * 1024,
@@ -12435,7 +12473,7 @@ pub const Pardes = struct {
// doc panes have no pty/emulator grid to reflow; just record
// the size so bodyText renders the right number of rows
if (pane.isTerminal()) {
- pane.vt.resize(p.gpa, cols, rows) catch {};
+ pane.vt.resize(p.gpa, .{ .cols = cols, .rows = rows }) catch {};
p.emit(.{ .resize_pty = .{ .pane = @intCast(id), .cols = cols, .rows = rows } });
}
pane.cols = cols;