Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 108 additions & 2 deletions src/terminal/Screen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3693,6 +3693,24 @@ fn promptClickLine(self: *Screen, click_pin: Pin) PromptClickMove {
const cursor_pin = self.cursor.page_pin.*;
if (cursor_pin.eql(click_pin)) return .zero;

const click_row = click_pin.rowAndCell().row;

// If click pin is in the prompt area, traverse down the prompt rows.
// Allow cursor movement only if one of the rows contains input.
if (click_row.semantic_prompt != .none) {
var row_pin = click_pin;
prompt: while (true) {
const rac = row_pin.rowAndCell();
const cells = row_pin.node.page().getCells(rac.row);
for (cells) |cell| {
if (cell.semantic_content == .input) break :prompt;
}

if (!rac.row.wrap) return .zero;
row_pin = row_pin.down(1) orelse 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;
Expand Down Expand Up @@ -9285,7 +9303,7 @@ test "Screen: resize more cols with cursor at prompt" {
defer s.deinit();

// zig fmt: off
try s.testWriteString("ABCDE\n");
try s.testWriteString("ABCDE\n");
s.cursorSetSemanticContent(.{ .prompt = .initial });
try s.testWriteString("> ");
s.cursorSetSemanticContent(.{ .input = .clear_eol });
Expand Down Expand Up @@ -9326,7 +9344,7 @@ test "Screen: resize more cols with cursor not at prompt" {
defer s.deinit();

// zig fmt: off
try s.testWriteString("ABCDE\n");
try s.testWriteString("ABCDE\n");
s.cursorSetSemanticContent(.{ .prompt = .initial });
try s.testWriteString("> ");
s.cursorSetSemanticContent(.{ .input = .clear_eol });
Expand Down Expand Up @@ -12374,6 +12392,94 @@ test "Screen: promptClickMove line left basic" {
try testing.expectEqual(@as(usize, 0), result.right);
}

test "Screen: promptClickMove ignores click on prompt-only line" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;

var s = try init(io, alloc, .{ .cols = 20, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();

s.semantic_prompt.click = .{ .cl = .line };

s.cursorSetSemanticContent(.{ .prompt = .initial });
try s.testWriteString("> \n");
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
try s.testWriteString("hello");

const click_on_top_left = s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?;
const result = s.promptClickMove(click_on_top_left);

try testing.expectEqual(PromptClickMove.zero, result);

const click_on_blank_cell = s.pages.pin(.{ .active = .{ .x = 10, .y = 0 } }).?;
const blank_result = s.promptClickMove(click_on_blank_cell);

try testing.expectEqual(PromptClickMove.zero, blank_result);
}

test "Screen: promptClickMove starship-style two-line prompt" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;

var s = try init(io, alloc, .{ .cols = 30, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();

s.semantic_prompt.click = .{ .cl = .line };

// user ~/workspace/ghostty
// > ls -lah
s.cursorSetSemanticContent(.{ .prompt = .initial });
try s.testWriteString("user ~/workspace/ghostty\n");
s.cursorSetSemanticContent(.{ .prompt = .continuation });
try s.testWriteString("> ");
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
try s.testWriteString("ls -lah");

// Clicking anywhere on the prompt-only line must not move the cursor.
const click_prompt_text = s.pages.pin(.{ .active = .{ .x = 5, .y = 0 } }).?;
try testing.expectEqual(PromptClickMove.zero, s.promptClickMove(click_prompt_text));

const click_prompt_blank = s.pages.pin(.{ .active = .{ .x = 28, .y = 0 } }).?;
try testing.expectEqual(PromptClickMove.zero, s.promptClickMove(click_prompt_blank));

// Clicking the prompt character on the line with input still moves to the
// start of the input.
const click_prompt_char = s.pages.pin(.{ .active = .{ .x = 0, .y = 1 } }).?;
const result = s.promptClickMove(click_prompt_char);
try testing.expectEqual(@as(usize, 7), result.left);
try testing.expectEqual(@as(usize, 0), result.right);
}

test "Screen: promptClickMove click on wrapped multi-line prompt moves to input start" {
const testing = std.testing;
const alloc = testing.allocator;
const io = testing.io;

var s = try init(io, alloc, .{ .cols = 10, .rows = 5, .max_scrollback_bytes = 0 });
defer s.deinit();

s.semantic_prompt.click = .{ .cl = .line };

s.cursorSetSemanticContent(.{ .prompt = .initial });
try s.testWriteString(">>>>>>>>>>>>");
s.cursorSetSemanticContent(.{ .input = .clear_explicit });
try s.testWriteString("hi");

s.cursorAbsolute(4, 1);

const click_first_prompt_row = s.pages.pin(.{ .active = .{ .x = 0, .y = 0 } }).?;
const result_first_prompt_row = s.promptClickMove(click_first_prompt_row);
try testing.expectEqual(@as(usize, 2), result_first_prompt_row.left);
try testing.expectEqual(@as(usize, 0), result_first_prompt_row.right);

const click_second_prompt_row = s.pages.pin(.{ .active = .{ .x = 1, .y = 1 } }).?;
const result_second_prompt_row = s.promptClickMove(click_second_prompt_row);
try testing.expectEqual(@as(usize, 2), result_second_prompt_row.left);
try testing.expectEqual(@as(usize, 0), result_second_prompt_row.right);
}

test "Screen: promptClickMove line left skips non-input cells" {
const testing = std.testing;
const alloc = testing.allocator;
Expand Down
Loading