terminal: select line considers semantic prompt change a boundary

Fixes #1329

Some shells and scripts use spaces and soft-wrapping as a way to move to
the next line instead of using newline (`\n`). Line selection
(triple-click by default) considers a soft-wrapped line as a single
line, so it was selecting the prompt.

This commit makes it so line selection considers semantic prompt state
(prompt vs command output) an additional boundary condition. This
requires shell integration but will make selection behave more
expectedly.
This commit is contained in:
Mitchell Hashimoto
2024-01-19 15:48:53 -08:00
parent e7169afffa
commit 48d6c93e09

View File

@ -207,6 +207,11 @@ pub const RowHeader = struct {
/// This line is the start of command output.
command = 4,
/// True if this is a prompt or input line.
pub fn promptOrInput(self: SemanticPrompt) bool {
return self == .prompt or self == .prompt_continuation or self == .input;
}
};
};
@ -1592,6 +1597,15 @@ pub fn selectLine(self: *Screen, pt: point.ScreenPoint) ?Selection {
const y_max = self.rowsWritten() - 1;
if (pt.y > y_max or pt.x >= self.cols) return null;
// Get the current point semantic prompt state since that determines
// boundary conditions too. This makes it so that line selection can
// only happen within the same prompt state. For example, if you triple
// click output, but the shell uses spaces to soft-wrap to the prompt
// then the selection will stop prior to the prompt. See issue #1329.
const semantic_prompt_state = self.getRow(.{ .screen = pt.y })
.getSemanticPrompt()
.promptOrInput();
// The real start of the row is the first row in the soft-wrap.
const start_row: usize = start_row: {
if (pt.y == 0) break :start_row 0;
@ -1600,6 +1614,11 @@ pub fn selectLine(self: *Screen, pt: point.ScreenPoint) ?Selection {
while (true) {
const current = self.getRow(.{ .screen = y });
if (!current.header().flags.wrap) break :start_row y + 1;
// See semantic_prompt_state comment for why
const current_prompt = current.getSemanticPrompt().promptOrInput();
if (current_prompt != semantic_prompt_state) break :start_row y + 1;
if (y == 0) break :start_row y;
y -= 1;
}
@ -1611,6 +1630,12 @@ pub fn selectLine(self: *Screen, pt: point.ScreenPoint) ?Selection {
var y: usize = pt.y;
while (y <= y_max) : (y += 1) {
const current = self.getRow(.{ .screen = y });
// See semantic_prompt_state comment for why
const current_prompt = current.getSemanticPrompt().promptOrInput();
if (current_prompt != semantic_prompt_state) break :end_row y - 1;
// End of the screen or not wrapped, we're done.
if (y == y_max or !current.header().flags.wrap) break :end_row y;
}
unreachable;
@ -4503,6 +4528,41 @@ test "Screen: selectLine across soft-wrap" {
}
}
// https://github.com/mitchellh/ghostty/issues/1329
test "Screen: selectLine semantic prompt boundary" {
const testing = std.testing;
const alloc = testing.allocator;
var s = try init(alloc, 10, 5, 0);
defer s.deinit();
try s.testWriteString("ABCDE\nA > ");
{
const contents = try s.testString(alloc, .screen);
defer alloc.free(contents);
try testing.expectEqualStrings("ABCDE\nA \n> ", contents);
}
var row = s.getRow(.{ .screen = 2 });
row.setSemanticPrompt(.prompt);
// Selecting output stops at the prompt even if soft-wrapped
{
const sel = s.selectLine(.{ .x = 1, .y = 1 }).?;
try testing.expectEqual(@as(usize, 0), sel.start.x);
try testing.expectEqual(@as(usize, 1), sel.start.y);
try testing.expectEqual(@as(usize, 0), sel.end.x);
try testing.expectEqual(@as(usize, 1), sel.end.y);
}
{
const sel = s.selectLine(.{ .x = 1, .y = 2 }).?;
try testing.expectEqual(@as(usize, 0), sel.start.x);
try testing.expectEqual(@as(usize, 2), sel.start.y);
try testing.expectEqual(@as(usize, 0), sel.end.x);
try testing.expectEqual(@as(usize, 2), sel.end.y);
}
}
test "Screen: selectLine across soft-wrap ignores blank lines" {
const testing = std.testing;
const alloc = testing.allocator;