From fae356be5a518edbb7f7c1d1d6106fa6049cad66 Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Sun, 29 Oct 2023 22:08:57 +0530 Subject: [PATCH 1/4] implement selecting output a `ScreenPoint` is in This works by finding prompt markers provided by shell integration Does not yet close #752 as this is not exposed --- src/terminal/Screen.zig | 49 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0a3c524ee..8fb8a9299 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1612,6 +1612,55 @@ pub fn selectWord(self: *Screen, pt: point.ScreenPoint) ?Selection { }; } +/// Select the command output under the given point. The limits of the output +/// are determined by semantic prompt information provided by shell integration. +/// A selection can span multiple physical lines if they are soft-wrapped. +/// +/// This will return null if a selection is impossible. The only scenario +/// this happens is if the point pt is outside of the written screen space. +pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { + // Impossible to select anything outside of the area we've written. + const y_max = self.rowsWritten() - 1; + if (pt.y > y_max) return null; + + // Go forwards to find our end boundary + // We are looking for input start / prompt markers + const end: point.ScreenPoint = boundary: { + var y: usize = pt.y; + while (y <= y_max) : (y += 1) { + const row = self.getRow(.{ .screen = y }); + switch (row.getSemanticPrompt()) { + .input, .prompt_continuation, .prompt => { + const prev_row = self.getRow(.{ .screen = y - 1 }); + break :boundary .{ .x = prev_row.lenCells(), .y = y - 1 }; + }, + else => {}, + } + } + + break :boundary .{ .x = self.cols - 1, .y = y_max }; + }; + + // Go backwards to find our start boundary + // We are looking for output start markers + const start: point.ScreenPoint = boundary: { + var y: usize = pt.y; + while (y > 0) : (y -= 1) { + const row = self.getRow(.{ .screen = y }); + switch (row.getSemanticPrompt()) { + .command => break :boundary .{ .x = 0, .y = y }, + else => {}, + } + } + break :boundary .{ .x = 0, .y = 0 }; + }; + + return Selection{ + .start = start, + .end = end, + }; +} + /// Scroll behaviors for the scroll function. pub const Scroll = union(enum) { /// Scroll to the top of the scroll buffer. The first line of the From 3ff20c7418abaca00636491051c94b1bc4a635ad Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Mon, 30 Oct 2023 09:09:38 +0530 Subject: [PATCH 2/4] add tests --- src/terminal/Screen.zig | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 8fb8a9299..0bf3bffac 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -4091,6 +4091,62 @@ test "Screen: selectWord with single quote boundary" { } } +test "Screen: selectOutput" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 15, 10, 0); + defer s.deinit(); + + // 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 + + var row = s.getRow(.{.screen = 2}); + row.setSemanticPrompt(.prompt); + row = s.getRow(.{.screen = 3}); + row.setSemanticPrompt(.input); + row = s.getRow(.{.screen = 4}); + row.setSemanticPrompt(.command); + row = s.getRow(.{.screen = 6}); + row.setSemanticPrompt(.input); + row = s.getRow(.{.screen = 7}); + row.setSemanticPrompt(.command); + + // No start marker, should select from the beginning + { + const sel = s.selectOutput(.{.x = 1, .y = 1}).?; + try testing.expectEqual(@as(usize, 0), sel.start.x); + try testing.expectEqual(@as(usize, 0), sel.start.y); + try testing.expectEqual(@as(usize, 10), sel.end.x); + try testing.expectEqual(@as(usize, 1), sel.end.y); + } + // Both start and end markers, should select between them + { + const sel = s.selectOutput(.{ .x = 3, .y = 5 }).?; + try testing.expectEqual(@as(usize, 0), sel.start.x); + try testing.expectEqual(@as(usize, 4), sel.start.y); + try testing.expectEqual(@as(usize, 10), sel.end.x); + try testing.expectEqual(@as(usize, 5), sel.end.y); + } + // No end marker, should select till the end + { + const sel = s.selectOutput(.{ .x = 2, .y = 7 }).?; + try testing.expectEqual(@as(usize, 0), sel.start.x); + try testing.expectEqual(@as(usize, 7), sel.start.y); + try testing.expectEqual(@as(usize, 9), sel.end.x); + try testing.expectEqual(@as(usize, 10), sel.end.y); + } +} + test "Screen: scrollRegionUp single" { const testing = std.testing; const alloc = testing.allocator; From 0920ab08cd875db639f1fd3b6623ec908488d7a6 Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Mon, 30 Oct 2023 11:52:35 +0530 Subject: [PATCH 3/4] handle cursor on a prompt line --- src/terminal/Screen.zig | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0bf3bffac..bc6764060 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1616,8 +1616,10 @@ pub fn selectWord(self: *Screen, pt: point.ScreenPoint) ?Selection { /// are determined by semantic prompt information provided by shell integration. /// A selection can span multiple physical lines if they are soft-wrapped. /// -/// This will return null if a selection is impossible. The only scenario -/// this happens is if the point pt is outside of the written screen space. +/// This will return null if a selection is impossible. The only scenarios +/// this happens is if: +/// - the point pt is outside of the written screen space. +/// - the point pt is on a prompt / input line. pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { // Impossible to select anything outside of the area we've written. const y_max = self.rowsWritten() - 1; @@ -1626,11 +1628,14 @@ pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { // Go forwards to find our end boundary // We are looking for input start / prompt markers const end: point.ScreenPoint = boundary: { - var y: usize = pt.y; - while (y <= y_max) : (y += 1) { + for (pt.y..y_max + 1) |y| { const row = self.getRow(.{ .screen = y }); switch (row.getSemanticPrompt()) { .input, .prompt_continuation, .prompt => { + if (y == pt.y) { + // Cursor on a prompt line, selection impossible + return null; + } const prev_row = self.getRow(.{ .screen = y - 1 }); break :boundary .{ .x = prev_row.lenCells(), .y = y - 1 }; }, @@ -4110,20 +4115,20 @@ test "Screen: selectOutput" { try s.testWriteString("output3\n"); // 8 try s.testWriteString("output3"); // 9 - var row = s.getRow(.{.screen = 2}); + var row = s.getRow(.{ .screen = 2 }); row.setSemanticPrompt(.prompt); - row = s.getRow(.{.screen = 3}); + row = s.getRow(.{ .screen = 3 }); row.setSemanticPrompt(.input); - row = s.getRow(.{.screen = 4}); + row = s.getRow(.{ .screen = 4 }); row.setSemanticPrompt(.command); - row = s.getRow(.{.screen = 6}); + row = s.getRow(.{ .screen = 6 }); row.setSemanticPrompt(.input); - row = s.getRow(.{.screen = 7}); + row = s.getRow(.{ .screen = 7 }); row.setSemanticPrompt(.command); // No start marker, should select from the beginning { - const sel = s.selectOutput(.{.x = 1, .y = 1}).?; + const sel = s.selectOutput(.{ .x = 1, .y = 1 }).?; try testing.expectEqual(@as(usize, 0), sel.start.x); try testing.expectEqual(@as(usize, 0), sel.start.y); try testing.expectEqual(@as(usize, 10), sel.end.x); @@ -4145,6 +4150,19 @@ test "Screen: selectOutput" { try testing.expectEqual(@as(usize, 9), sel.end.x); try testing.expectEqual(@as(usize, 10), sel.end.y); } + // input / prompt at y = 0, pt.y = 0 + { + s.deinit(); + s = try init(alloc, 5, 10, 0); + try s.testWriteString("prompt1$ input1\n"); + try s.testWriteString("output1\n"); + try s.testWriteString("prompt2\n"); + row = s.getRow(.{ .screen = 0 }); + row.setSemanticPrompt(.input); + row = s.getRow(.{ .screen = 1 }); + row.setSemanticPrompt(.command); + try testing.expect(s.selectOutput(.{ .x = 2, .y = 0 }) == null); + } } test "Screen: scrollRegionUp single" { From bccf1216bc4e41d4c5eb09d2d38a75d4f9a7a22f Mon Sep 17 00:00:00 2001 From: Chinmay Dalal Date: Mon, 30 Oct 2023 12:42:58 +0530 Subject: [PATCH 4/4] exit early when cursor is on a prompt line --- src/terminal/Screen.zig | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index bc6764060..6df52340f 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1624,6 +1624,14 @@ pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { // Impossible to select anything outside of the area we've written. const y_max = self.rowsWritten() - 1; if (pt.y > y_max) return null; + const point_row = self.getRow(.{ .screen = pt.y }); + switch (point_row.getSemanticPrompt()) { + .input, .prompt_continuation, .prompt => { + // Cursor on a prompt line, selection impossible + return null; + }, + else => {}, + } // Go forwards to find our end boundary // We are looking for input start / prompt markers @@ -1632,10 +1640,6 @@ pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection { const row = self.getRow(.{ .screen = y }); switch (row.getSemanticPrompt()) { .input, .prompt_continuation, .prompt => { - if (y == pt.y) { - // Cursor on a prompt line, selection impossible - return null; - } const prev_row = self.getRow(.{ .screen = y - 1 }); break :boundary .{ .x = prev_row.lenCells(), .y = y - 1 }; },