fix: cmd+triple-click select all command output when first line wraps (#5373)

I found this bug was easily reproduced with any command that wrapped to
multiple rows on the first line of its output. The cause is that we stop
searching for rows once we reach the first one where
`row.semantic_prompt = .command`, which means that we reach the bottom
line of wrapped output and stop there.

This PR makes it so that we continue iterating until we reach a row
where `semantic_prompt != .command` and then return the previous one (or
the last one if we run out of rows).

I also updated the test cases to include this.

I considered that this bug would also be avoided if we didn't propagate
the `command` semantic prompt to additional rows on wrapped lines, but I
don't know enough about the shell integration to make a call on that.

Closes #4693
This commit is contained in:
Mitchell Hashimoto
2025-01-29 07:28:17 -08:00
committed by GitHub

View File

@ -2591,13 +2591,39 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection {
const start: Pin = boundary: {
var it = pin.rowIterator(.left_up, null);
var it_prev = pin;
// First, iterate until we find the first line of command output
while (it.next()) |p| {
it_prev = p;
const row = p.rowAndCell().row;
switch (row.semantic_prompt) {
.command => break,
.unknown,
.prompt,
.prompt_continuation,
.input,
=> {},
}
if (row.semantic_prompt == .command) {
break;
}
}
// Because the first line of command output may span multiple visual rows we must now
// iterate until we find the first row of anything other than command output and then
// yield the previous row.
while (it.next()) |p| {
const row = p.rowAndCell().row;
switch (row.semantic_prompt) {
.command => break :boundary p,
else => {},
}
.command => {},
.unknown,
.prompt,
.prompt_continuation,
.input,
=> break :boundary it_prev,
}
it_prev = p;
}
@ -7641,17 +7667,17 @@ test "Screen: selectOutput" {
// zig fmt: off
{
// line number:
try s.testWriteString("output1\n"); // 0
try s.testWriteString("output1\n"); // 1
try s.testWriteString("prompt2\n"); // 2
try s.testWriteString("input2\n"); // 3
try s.testWriteString("output2\n"); // 4
try s.testWriteString("output2\n"); // 5
try s.testWriteString("prompt3$ input3\n"); // 6
try s.testWriteString("output3\n"); // 7
try s.testWriteString("output3\n"); // 8
try s.testWriteString("output3"); // 9
// line number:
try s.testWriteString("output1\n"); // 0
try s.testWriteString("output1\n"); // 1
try s.testWriteString("prompt2\n"); // 2
try s.testWriteString("input2\n"); // 3
try s.testWriteString("output2output2output2output2\n"); // 4, 5, 6 due to overflow
try s.testWriteString("output2\n"); // 7
try s.testWriteString("prompt3$ input3\n"); // 8
try s.testWriteString("output3\n"); // 9
try s.testWriteString("output3\n"); // 10
try s.testWriteString("output3"); // 11
}
// zig fmt: on
@ -7670,13 +7696,23 @@ test "Screen: selectOutput" {
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 5 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 6 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 8 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .input;
}
{
const pin = s.pages.pin(.{ .screen = .{ .y = 7 } }).?;
const pin = s.pages.pin(.{ .screen = .{ .y = 9 } }).?;
const row = pin.rowAndCell().row;
row.semantic_prompt = .command;
}
@ -7701,7 +7737,7 @@ test "Screen: selectOutput" {
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 3,
.y = 5,
.y = 7,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
@ -7710,23 +7746,23 @@ test "Screen: selectOutput" {
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
.y = 5,
.y = 7,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// No end marker, should select till the end
{
var sel = s.selectOutput(s.pages.pin(.{ .active = .{
.x = 2,
.y = 7,
.y = 10,
} }).?).?;
defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{
.x = 0,
.y = 7,
.y = 9,
} }, s.pages.pointFromPin(.active, sel.start()).?);
try testing.expectEqual(point.Point{ .active = .{
.x = 9,
.y = 10,
.y = 12,
} }, s.pages.pointFromPin(.active, sel.end()).?);
}
// input / prompt at y = 0, pt.y = 0