From dedfdea43f0d6c7151c541284c81027969d89032 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 10 Aug 2026 20:38:37 -0300 Subject: ghostty: bump pin to 82e53e3f and fix tab/virtual-column vt handling - build.zig.zon: bump ghostty ba38b493 -> 82e53e3f (translate-c backport, fixes 404 on cold cache), update hash - src/pardes.zig: adapt Terminal.init/resize to new std.Io signatures; fix display-column conversion for tabs and clicks past EOL (fileRawDisplayCol/fileRawAtDisplay) - test/e2e_harness.zig: adapt to new ghostty_vt signatures - snap suite 86/86 green --- src/pardes.zig | 84 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 23 deletions(-) (limited to 'src') 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; -- cgit v1.3