mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-15 03:52:39 +00:00
terminal: Screen.promptClickMove for cl handling
This commit is contained in:
@@ -2942,6 +2942,179 @@ pub fn lineIterator(self: *const Screen, start: Pin) LineIterator {
|
||||
};
|
||||
}
|
||||
|
||||
pub const PromptClickMove = struct {
|
||||
left: usize,
|
||||
right: usize,
|
||||
|
||||
pub const zero = PromptClickMove{
|
||||
.left = 0,
|
||||
.right = 0,
|
||||
};
|
||||
};
|
||||
|
||||
/// Determine the inputs necessary to move the cursor to the given
|
||||
/// click location within a prompt input area.
|
||||
///
|
||||
/// If the cursor isn't currently at a prompt input location, this
|
||||
/// returns no movement.
|
||||
///
|
||||
/// This feature depends on well-behaved OSC133 shell integration. Specifically,
|
||||
/// this only moves over designated input areas (OSC 133 B). It is assumed
|
||||
/// that the shell will only move the cursor to input cells, so prompt cells
|
||||
/// and other blank cells are ignored as part of the movement calculation.
|
||||
pub fn promptClickMove(
|
||||
self: *Screen,
|
||||
click_pin: Pin,
|
||||
) PromptClickMove {
|
||||
// If we're not at an input cell with our cursor, no movement will
|
||||
// ever be possible.
|
||||
if (self.cursor.semantic_content != .input and
|
||||
self.cursor.page_cell.semantic_content != .input) return .zero;
|
||||
|
||||
return switch (self.semantic_prompt.click) {
|
||||
// None doesn't support movement and click_events must use a
|
||||
// different mechanism (SGR mouse events) that callers must handle.
|
||||
.none, .click_events => .zero,
|
||||
.cl => |cl| switch (cl) {
|
||||
// All of these currently use dumb line-based navigation.
|
||||
// But eventually we'll support more.
|
||||
.line,
|
||||
.multiple,
|
||||
.conservative_vertical,
|
||||
.smart_vertical,
|
||||
=> self.promptClickLine(click_pin),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Determine the inputs required to move from the cursor to the given
|
||||
/// click location. If the cursor isn't currently at a prompt input
|
||||
/// location, this will return zero.
|
||||
///
|
||||
/// This currently only supports moving a single line.
|
||||
fn promptClickLine(self: *Screen, click_pin: Pin) PromptClickMove {
|
||||
// If our click pin is our cursor pin, no movement is needed.
|
||||
// Do this early so we can assume later that they are different.
|
||||
const cursor_pin = self.cursor.page_pin.*;
|
||||
if (cursor_pin.eql(click_pin)) return .zero;
|
||||
|
||||
// If our cursor is before our click, we're only emitting right inputs.
|
||||
if (cursor_pin.before(click_pin)) {
|
||||
var count: usize = 0;
|
||||
|
||||
// We go row-by-row because soft-wrapped rows are still a single
|
||||
// line to a shell, so we can't just look at our page row.
|
||||
var row_it = cursor_pin.rowIterator(
|
||||
.right_down,
|
||||
click_pin,
|
||||
);
|
||||
row_it: while (row_it.next()) |row_pin| {
|
||||
const rac = row_pin.rowAndCell();
|
||||
const cells = row_pin.node.data.getCells(rac.row);
|
||||
|
||||
// Determine if this row is our cursor.
|
||||
const is_cursor_row = row_pin.node == cursor_pin.node and
|
||||
row_pin.y == cursor_pin.y;
|
||||
|
||||
// If this is not the cursor row, verify it's still part of the
|
||||
// continuation of our starting prompt.
|
||||
if (!is_cursor_row and
|
||||
rac.row.semantic_prompt != .prompt_continuation) break;
|
||||
|
||||
// Determine where our input starts.
|
||||
const start_x: usize = start_x: {
|
||||
// If this is our cursor row then we start after the cursor.
|
||||
if (is_cursor_row) break :start_x cursor_pin.x + 1;
|
||||
|
||||
// Otherwise, we start at the first input cell, because
|
||||
// we expect the shell to properly translate arrows across
|
||||
// lines to the start of the input. Some shells indent
|
||||
// where input starts on subsequent lines so we must do
|
||||
// this.
|
||||
for (cells, 0..) |cell, x| {
|
||||
if (cell.semantic_content == .input) break :start_x x;
|
||||
}
|
||||
|
||||
// We never found an input cell, so we need to move to the
|
||||
// next row.
|
||||
break :start_x cells.len;
|
||||
};
|
||||
|
||||
// Iterate over the input cells and assume arrow keys only
|
||||
// jump to input cells.
|
||||
for (cells[start_x..], start_x..) |cell, x| {
|
||||
// Ignore non-input cells, but allow breaks. We assume
|
||||
// the shell will translate arrow keys to only input
|
||||
// areas.
|
||||
if (cell.semantic_content != .input) continue;
|
||||
|
||||
// Increment our input count
|
||||
count += 1;
|
||||
|
||||
// If this is our target, we're done.
|
||||
if (row_pin.node == click_pin.node and
|
||||
row_pin.y == click_pin.y and
|
||||
x == click_pin.x)
|
||||
break :row_it;
|
||||
}
|
||||
|
||||
// If this row isn't soft-wrapped, we need to break out
|
||||
// because line based moving only handles single lines.
|
||||
// We're done!
|
||||
if (!rac.row.wrap) break;
|
||||
}
|
||||
|
||||
return .{ .left = 0, .right = count };
|
||||
}
|
||||
|
||||
// Otherwise, cursor is after click, so we're emitting left inputs.
|
||||
var count: usize = 0;
|
||||
|
||||
// We go row-by-row because soft-wrapped rows are still a single
|
||||
// line to a shell, so we can't just look at our page row.
|
||||
var row_it = cursor_pin.rowIterator(
|
||||
.left_up,
|
||||
click_pin,
|
||||
);
|
||||
row_it: while (row_it.next()) |row_pin| {
|
||||
const rac = row_pin.rowAndCell();
|
||||
const cells = row_pin.node.data.getCells(rac.row);
|
||||
|
||||
// Determine the length of the cells we look at in this row.
|
||||
const end_len: usize = end_len: {
|
||||
// If this is our cursor row then we end before the cursor.
|
||||
if (row_pin.node == cursor_pin.node and
|
||||
row_pin.y == cursor_pin.y) break :end_len cursor_pin.x;
|
||||
|
||||
// Otherwise, we end at the last cell in the row.
|
||||
break :end_len cells.len;
|
||||
};
|
||||
|
||||
// Iterate backwards over the input cells.
|
||||
for (0..end_len) |rev_x| {
|
||||
const x: usize = end_len - 1 - rev_x;
|
||||
const cell = cells[x];
|
||||
|
||||
// Ignore non-input cells.
|
||||
if (cell.semantic_content != .input) continue;
|
||||
|
||||
// Increment our input count
|
||||
count += 1;
|
||||
|
||||
// If this is our target, we're done.
|
||||
if (row_pin.node == click_pin.node and
|
||||
row_pin.y == click_pin.y and
|
||||
x == click_pin.x)
|
||||
break :row_it;
|
||||
}
|
||||
|
||||
// If this row is not a wrap continuation, then break out
|
||||
if (!rac.row.wrap_continuation) break;
|
||||
}
|
||||
|
||||
return .{ .left = count, .right = 0 };
|
||||
}
|
||||
|
||||
/// Returns the change in x/y that is needed to reach "to" from "from"
|
||||
/// within a prompt. If "to" is before or after the prompt bounds then
|
||||
/// the result will be bounded to the prompt.
|
||||
@@ -3080,8 +3253,8 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
if (self.cursor.semantic_content_clear_eol) {
|
||||
self.cursorSetSemanticContent(.output);
|
||||
} else switch (self.cursor.semantic_content) {
|
||||
.input, .output => {},
|
||||
.prompt => self.cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
.output => {},
|
||||
.prompt, .input => self.cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -3116,8 +3289,8 @@ pub fn testWriteString(self: *Screen, text: []const u8) !void {
|
||||
self.cursorHorizontalAbsolute(0);
|
||||
self.cursor.page_row.wrap_continuation = true;
|
||||
switch (self.cursor.semantic_content) {
|
||||
.input, .output => {},
|
||||
.prompt => self.cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
.output => {},
|
||||
.input, .prompt => self.cursor.page_row.semantic_prompt = .prompt_continuation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9871,3 +10044,386 @@ test "selectionString map allocation failure cleanup" {
|
||||
// If this test passes without memory leaks (when run with testing.allocator),
|
||||
// it means the errdefer properly cleaned up map.string when toOwnedSlice failed.
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right basic" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
|
||||
// Move cursor back to start of input (column 2, the 'h')
|
||||
s.cursorAbsolute(2, 0);
|
||||
|
||||
// Click on first 'l' (column 4), should require 2 right movements (h->e->l)
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), result.right);
|
||||
try testing.expectEqual(@as(usize, 0), result.left);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right cursor not on input" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
|
||||
// Move cursor back to prompt area (column 0, the '>')
|
||||
s.cursorAbsolute(0, 0);
|
||||
|
||||
// Cursor is on prompt, not input - should return zero
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(PromptClickMove.zero, result);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right click on same position" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
|
||||
// Move cursor to column 4
|
||||
s.cursorAbsolute(4, 0);
|
||||
|
||||
// Click on same position - no movement needed
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(PromptClickMove.zero, result);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right skips non-input cells" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write: "> h" then output "X" then input "llo"
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("h");
|
||||
s.cursorSetSemanticContent(.output);
|
||||
try s.testWriteString("X");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("llo");
|
||||
|
||||
// Move cursor to column 2 (the 'h')
|
||||
s.cursorAbsolute(2, 0);
|
||||
|
||||
// Click on 'l' at column 5 - should skip the 'X' output cell
|
||||
// Movement: h (start) -> l (col 4) -> l (col 5) = 2 right movements
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 5, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), result.right);
|
||||
try testing.expectEqual(@as(usize, 0), result.left);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right soft-wrapped line" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input that wraps
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
// Write 8 chars of input, first row has 2 for prompt + 8 input = 10 cols
|
||||
try s.testWriteString("abcdefgh");
|
||||
// Continue on next row (soft-wrapped)
|
||||
try s.testWriteString("ij");
|
||||
|
||||
// Verify soft wrap occurred
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("> abcdefgh\nij", contents);
|
||||
}
|
||||
|
||||
// Move cursor to column 2 (the 'a')
|
||||
s.cursorAbsolute(2, 0);
|
||||
|
||||
// Click on 'j' at column 1, row 1 - should count all input cells
|
||||
// Movement: a->b->c->d->e->f->g->h->i->j = 9 right movements
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 1, .y = 1 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 9), result.right);
|
||||
try testing.expectEqual(@as(usize, 0), result.left);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove disabled when click is none" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Click mode is .none by default (disabled)
|
||||
try testing.expectEqual(Screen.SemanticPrompt.SemanticClick.none, s.semantic_prompt.click);
|
||||
|
||||
// Write a prompt and input
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
|
||||
// Move cursor to start of input
|
||||
s.cursorAbsolute(2, 0);
|
||||
|
||||
// Click should return zero since click mode is disabled
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 4, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(PromptClickMove.zero, result);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right stops at hard wrap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write prompt and input on first line
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
// Hard wrap (newline)
|
||||
try s.testWriteString("\n");
|
||||
try s.testWriteString("world");
|
||||
|
||||
// Move cursor to column 2 (the 'h')
|
||||
s.cursorAbsolute(2, 0);
|
||||
|
||||
// Click on 'w' at column 0, row 1 - but line mode stops at hard wrap
|
||||
// Should only move to end of first line: h->e->l->l->o = 4 movements
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
// Should stop at end of first line, not cross hard wrap
|
||||
try testing.expectEqual(@as(usize, 4), result.right);
|
||||
try testing.expectEqual(@as(usize, 0), result.left);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line right stops at non-continuation row" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Row 0: PROMPT "> hello"
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello\n");
|
||||
|
||||
// Row 1: CONTINUATION "world"
|
||||
s.cursorSetSemanticContent(.{ .prompt = .continuation });
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("world\n");
|
||||
|
||||
// Row 2: NEW PROMPT "> again"
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("again");
|
||||
|
||||
// Verify content
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("> hello\nworld\n> again", contents);
|
||||
}
|
||||
|
||||
// Move cursor to 'w' at column 0, row 1
|
||||
s.cursorAbsolute(0, 1);
|
||||
|
||||
// Click on 'a' at column 2, row 2 - but row 2 is a new prompt
|
||||
// Should stop at end of "world": w->o->r->l->d = 4 movements
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 2 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
// Should stop at 'd' (end of world), not cross to new prompt
|
||||
try testing.expectEqual(@as(usize, 4), result.right);
|
||||
try testing.expectEqual(@as(usize, 0), result.left);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line left basic" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
|
||||
// Cursor is at column 7 (after 'o'), move it to column 6 (the 'o')
|
||||
s.cursorAbsolute(6, 0);
|
||||
|
||||
// Click on 'h' (column 2), should require 4 left movements (o->l->l->e->h)
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 4), result.left);
|
||||
try testing.expectEqual(@as(usize, 0), result.right);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line left skips non-input cells" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write: "> h" then output "X" then input "llo"
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("h");
|
||||
s.cursorSetSemanticContent(.output);
|
||||
try s.testWriteString("X");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("llo");
|
||||
|
||||
// Move cursor to column 6 (the 'o')
|
||||
s.cursorAbsolute(6, 0);
|
||||
|
||||
// Click on 'h' at column 2 - should skip the 'X' output cell
|
||||
// Movement: o->l->l->h = 3 left movements (skipping X)
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 3), result.left);
|
||||
try testing.expectEqual(@as(usize, 0), result.right);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line left soft-wrapped line" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 10, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write a prompt and input that wraps
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
// Write 8 chars of input, first row has 2 for prompt + 8 input = 10 cols
|
||||
try s.testWriteString("abcdefgh");
|
||||
// Continue on next row (soft-wrapped)
|
||||
try s.testWriteString("ij");
|
||||
|
||||
// Verify soft wrap occurred
|
||||
{
|
||||
const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} });
|
||||
defer alloc.free(contents);
|
||||
try testing.expectEqualStrings("> abcdefgh\nij", contents);
|
||||
}
|
||||
|
||||
// Cursor is at column 2, row 1 (after 'j'). Move to 'j' at column 1.
|
||||
s.cursorAbsolute(1, 1);
|
||||
|
||||
// Click on 'a' at column 2, row 0 - should count all input cells backwards
|
||||
// Movement: j->i->h->g->f->e->d->c->b->a = 9 left movements
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
try testing.expectEqual(@as(usize, 9), result.left);
|
||||
try testing.expectEqual(@as(usize, 0), result.right);
|
||||
}
|
||||
|
||||
test "Screen: promptClickMove line left stops at hard wrap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, .{ .cols = 20, .rows = 5, .max_scrollback = 0 });
|
||||
defer s.deinit();
|
||||
|
||||
// Enable line click mode
|
||||
s.semantic_prompt.click = .{ .cl = .line };
|
||||
|
||||
// Write prompt and input on first line, then hard wrap
|
||||
s.cursorSetSemanticContent(.{ .prompt = .initial });
|
||||
try s.testWriteString("> ");
|
||||
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
|
||||
try s.testWriteString("hello");
|
||||
// Hard wrap (newline)
|
||||
try s.testWriteString("\n");
|
||||
try s.testWriteString("world");
|
||||
|
||||
// Move cursor to 'd' at column 4, row 1 (an actual input cell)
|
||||
s.cursorAbsolute(4, 1);
|
||||
|
||||
// Click on 'h' at column 2, row 0 - but line mode stops at hard wrap
|
||||
// Should only move to start of second line, not cross to row 0
|
||||
const click_pin = s.pages.pin(.{ .active = .{ .x = 2, .y = 0 } }).?;
|
||||
const result = s.promptClickMove(click_pin);
|
||||
|
||||
// Should stop at start of second line: d->l->r->o->w = 4 movements
|
||||
try testing.expectEqual(@as(usize, 4), result.left);
|
||||
try testing.expectEqual(@as(usize, 0), result.right);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user