87544d89
Fix visible selection text extraction
a73x 2026-04-09 18:04
Commit message
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); |