a73x

87544d89

Fix visible selection text extraction

a73x   2026-04-09 18:04

Commit message
Fix visible selection text extraction

src/main.zig
Old New
@@ -554,11 +554,19 @@ fn extractSelectedText(
554 span: SelectionSpan, 554 span: SelectionSpan,
555 ) ![]u8 { 555 ) ![]u8 {
556 const normalized = span.normalized(); 556 const normalized = span.normalized();
557 if (row_data.len == 0) return try alloc.alloc(u8, 0);
558
559 const max_row = row_data.len - 1;
560 if (normalized.start.row > max_row) return try alloc.alloc(u8, 0);
561
562 const visible_end_row = @min(normalized.end.row, max_row);
563 if (normalized.start.row > visible_end_row) return try alloc.alloc(u8, 0);
564
557 var out: std.ArrayListUnmanaged(u8) = .empty; 565 var out: std.ArrayListUnmanaged(u8) = .empty;
558 errdefer out.deinit(alloc); 566 errdefer out.deinit(alloc);
559 567
560 const start_row: usize = @intCast(normalized.start.row); 568 const start_row: usize = @intCast(normalized.start.row);
561 const end_row: usize = @intCast(normalized.end.row); 569 const end_row: usize = @intCast(visible_end_row);
562 var row_idx = start_row; 570 var row_idx = start_row;
563 while (row_idx <= end_row) : (row_idx += 1) { 571 while (row_idx <= end_row) : (row_idx += 1) {
564 if (row_idx > start_row) try out.append(alloc, '\n'); 572 if (row_idx > start_row) try out.append(alloc, '\n');
@@ -592,7 +600,7 @@ fn appendSelectedRowText(
592 600
593 while (end_col >= start_col) { 601 while (end_col >= start_col) {
594 const cell = cells[end_col]; 602 const cell = cells[end_col];
595 if (cellContributesVisibleText(cell)) break; 603 if (!isTrailingBlankCell(cell)) break;
596 if (end_col == 0) return; 604 if (end_col == 0) return;
597 end_col -= 1; 605 end_col -= 1;
598 } 606 }
@@ -600,20 +608,26 @@ fn appendSelectedRowText(
600 var col = start_col; 608 var col = start_col;
601 while (col <= end_col) : (col += 1) { 609 while (col <= end_col) : (col += 1) {
602 const cell = cells[col]; 610 const cell = cells[col];
603 if (!cellContributesVisibleText(cell)) continue; 611 try appendSelectedCellText(alloc, out, cell);
604 try appendCellText(alloc, out, cell);
605 } 612 }
606 } 613 }
607 614
608 fn cellContributesVisibleText(cell: anytype) bool { 615 fn isTrailingBlankCell(cell: anytype) bool {
609 return cell.hasText() and cell.wide != .spacer_tail and cell.wide != .spacer_head; 616 return !cell.hasText() and cell.wide != .spacer_tail and cell.wide != .spacer_head;
610 } 617 }
611 618
612 fn appendCellText( 619 fn appendSelectedCellText(
613 alloc: std.mem.Allocator, 620 alloc: std.mem.Allocator,
614 out: *std.ArrayListUnmanaged(u8), 621 out: *std.ArrayListUnmanaged(u8),
615 cell: anytype, 622 cell: anytype,
616 ) !void { 623 ) !void {
624 if (cell.wide == .spacer_tail or cell.wide == .spacer_head) return;
625
626 if (!cell.hasText()) {
627 try out.append(alloc, ' ');
628 return;
629 }
630
617 const cp = cell.codepoint(); 631 const cp = cell.codepoint();
618 if (cp == 0) return; 632 if (cp == 0) return;
619 633
@@ -2410,6 +2424,26 @@ test "extractSelectedText trims trailing blanks on each visible row" {
2410 try std.testing.expectEqualStrings("ab\nc", text); 2424 try std.testing.expectEqualStrings("ab\nc", text);
2411 } 2425 }
2412 2426
2427 test "extractSelectedText preserves interior spaces while trimming trailing blanks" {
2428 var term = try vt.Terminal.init(std.testing.allocator, .{
2429 .cols = 10,
2430 .rows = 1,
2431 });
2432 defer term.deinit();
2433
2434 term.write("a b c ");
2435 try term.snapshot();
2436
2437 const span = SelectionSpan{
2438 .start = .{ .col = 0, .row = 0 },
2439 .end = .{ .col = 9, .row = 0 },
2440 };
2441 const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span);
2442 defer std.testing.allocator.free(text);
2443
2444 try std.testing.expectEqualStrings("a b c ", text);
2445 }
2446
2413 test "extractSelectedText respects partial first and last rows" { 2447 test "extractSelectedText respects partial first and last rows" {
2414 var term = try vt.Terminal.init(std.testing.allocator, .{ 2448 var term = try vt.Terminal.init(std.testing.allocator, .{
2415 .cols = 8, 2449 .cols = 8,
@@ -2430,6 +2464,26 @@ test "extractSelectedText respects partial first and last rows" {
2430 try std.testing.expectEqualStrings("bc\nde", text); 2464 try std.testing.expectEqualStrings("bc\nde", text);
2431 } 2465 }
2432 2466
2467 test "extractSelectedText clamps an offscreen end row to visible rows" {
2468 var term = try vt.Terminal.init(std.testing.allocator, .{
2469 .cols = 8,
2470 .rows = 2,
2471 });
2472 defer term.deinit();
2473
2474 term.write("row0\r\nrow1");
2475 try term.snapshot();
2476
2477 const span = SelectionSpan{
2478 .start = .{ .col = 0, .row = 0 },
2479 .end = .{ .col = 7, .row = 9 },
2480 };
2481 const text = try extractSelectedText(std.testing.allocator, term.render_state.row_data.items(.cells), span);
2482 defer std.testing.allocator.free(text);
2483
2484 try std.testing.expectEqualStrings("row0\nrow1", text);
2485 }
2486
2433 test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" { 2487 test "drainSelectionPipeThenRoundtrip drains large paste before roundtrip" {
2434 const payload_len: usize = 8192; 2488 const payload_len: usize = 8192;
2435 const payload = try std.testing.allocator.alloc(u8, payload_len); 2489 const payload = try std.testing.allocator.alloc(u8, payload_len);