mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 19:56:08 +03:00
terminal: selectLine can disable whitespace/sem prompt splitting
This commit is contained in:
@ -2361,7 +2361,7 @@ pub fn mouseButtonCallback(
|
||||
const sel_ = if (mods.ctrl)
|
||||
self.io.terminal.screen.selectOutput(pin.*)
|
||||
else
|
||||
self.io.terminal.screen.selectLine(pin.*);
|
||||
self.io.terminal.screen.selectLine(.{ .pin = pin.* });
|
||||
if (sel_) |sel| {
|
||||
try self.setSelection(sel);
|
||||
try self.queueRender();
|
||||
@ -2728,12 +2728,12 @@ fn dragLeftClickTriple(
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = screen.selectLine(drag_pin) orelse return;
|
||||
const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
|
||||
// Get our selection to grow it. If we don't have a selection, start it now.
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = screen.selectLine(click_pin) orelse {
|
||||
var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
|
||||
try self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
|
@ -1229,30 +1229,42 @@ pub fn selectionString(
|
||||
return string;
|
||||
}
|
||||
|
||||
pub const SelectLine = struct {
|
||||
/// The pin of some part of the line to select.
|
||||
pin: Pin,
|
||||
|
||||
/// These are the codepoints to consider whitespace to trim
|
||||
/// from the ends of the selection.
|
||||
whitespace: ?[]const u21 = &.{ 0, ' ', '\t' },
|
||||
|
||||
/// If true, line selection will consider semantic prompt
|
||||
/// state changing a boundary. State changing is ANY state
|
||||
/// change.
|
||||
semantic_prompt_boundary: bool = true,
|
||||
};
|
||||
|
||||
/// Select the line under the given point. This will select across soft-wrapped
|
||||
/// lines and will omit the leading and trailing whitespace. If the point is
|
||||
/// over whitespace but the line has non-whitespace characters elsewhere, the
|
||||
/// line will be selected.
|
||||
pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
pub fn selectLine(self: *Screen, opts: SelectLine) ?Selection {
|
||||
_ = self;
|
||||
|
||||
// Whitespace characters for selection purposes
|
||||
const whitespace = &[_]u32{ 0, ' ', '\t' };
|
||||
|
||||
// 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 = state: {
|
||||
const rac = pin.rowAndCell();
|
||||
const semantic_prompt_state: ?bool = state: {
|
||||
if (!opts.semantic_prompt_boundary) break :state null;
|
||||
const rac = opts.pin.rowAndCell();
|
||||
break :state rac.row.semantic_prompt.promptOrInput();
|
||||
};
|
||||
|
||||
// The real start of the row is the first row in the soft-wrap.
|
||||
const start_pin: Pin = start_pin: {
|
||||
var it = pin.rowIterator(.left_up, null);
|
||||
var it_prev: Pin = pin;
|
||||
var it = opts.pin.rowIterator(.left_up, null);
|
||||
var it_prev: Pin = opts.pin;
|
||||
while (it.next()) |p| {
|
||||
const row = p.rowAndCell().row;
|
||||
|
||||
@ -1262,12 +1274,14 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
break :start_pin copy;
|
||||
}
|
||||
|
||||
// See semantic_prompt_state comment for why
|
||||
const current_prompt = row.semantic_prompt.promptOrInput();
|
||||
if (current_prompt != semantic_prompt_state) {
|
||||
var copy = it_prev;
|
||||
copy.x = 0;
|
||||
break :start_pin copy;
|
||||
if (semantic_prompt_state) |v| {
|
||||
// See semantic_prompt_state comment for why
|
||||
const current_prompt = row.semantic_prompt.promptOrInput();
|
||||
if (current_prompt != v) {
|
||||
var copy = it_prev;
|
||||
copy.x = 0;
|
||||
break :start_pin copy;
|
||||
}
|
||||
}
|
||||
|
||||
it_prev = p;
|
||||
@ -1280,16 +1294,18 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
|
||||
// The real end of the row is the final row in the soft-wrap.
|
||||
const end_pin: Pin = end_pin: {
|
||||
var it = pin.rowIterator(.right_down, null);
|
||||
var it = opts.pin.rowIterator(.right_down, null);
|
||||
while (it.next()) |p| {
|
||||
const row = p.rowAndCell().row;
|
||||
|
||||
// See semantic_prompt_state comment for why
|
||||
const current_prompt = row.semantic_prompt.promptOrInput();
|
||||
if (current_prompt != semantic_prompt_state) {
|
||||
var prev = p.up(1).?;
|
||||
prev.x = p.page.data.size.cols - 1;
|
||||
break :end_pin prev;
|
||||
if (semantic_prompt_state) |v| {
|
||||
// See semantic_prompt_state comment for why
|
||||
const current_prompt = row.semantic_prompt.promptOrInput();
|
||||
if (current_prompt != v) {
|
||||
var prev = p.up(1).?;
|
||||
prev.x = p.page.data.size.cols - 1;
|
||||
break :end_pin prev;
|
||||
}
|
||||
}
|
||||
|
||||
if (!row.wrap) {
|
||||
@ -1304,6 +1320,7 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
|
||||
// Go forward from the start to find the first non-whitespace character.
|
||||
const start: Pin = start: {
|
||||
const whitespace = opts.whitespace orelse break :start start_pin;
|
||||
var it = start_pin.cellIterator(.right_down, end_pin);
|
||||
while (it.next()) |p| {
|
||||
const cell = p.rowAndCell().cell;
|
||||
@ -1311,9 +1328,9 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
|
||||
// Non-empty means we found it.
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
u21,
|
||||
whitespace,
|
||||
&[_]u32{cell.content.codepoint},
|
||||
&[_]u21{cell.content.codepoint},
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
@ -1325,6 +1342,7 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
|
||||
// Go backward from the end to find the first non-whitespace character.
|
||||
const end: Pin = end: {
|
||||
const whitespace = opts.whitespace orelse break :end end_pin;
|
||||
var it = end_pin.cellIterator(.left_up, start_pin);
|
||||
while (it.next()) |p| {
|
||||
const cell = p.rowAndCell().cell;
|
||||
@ -1332,9 +1350,9 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
|
||||
|
||||
// Non-empty means we found it.
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
u21,
|
||||
whitespace,
|
||||
&[_]u32{cell.content.codepoint},
|
||||
&[_]u21{cell.content.codepoint},
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
@ -4883,10 +4901,10 @@ test "Screen: selectLine" {
|
||||
|
||||
// Going forward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
@ -4900,10 +4918,10 @@ test "Screen: selectLine" {
|
||||
|
||||
// Going backward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 7,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
@ -4917,10 +4935,10 @@ test "Screen: selectLine" {
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
@ -4934,10 +4952,10 @@ test "Screen: selectLine" {
|
||||
|
||||
// Outside active area
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 9,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
@ -4960,10 +4978,10 @@ test "Screen: selectLine across soft-wrap" {
|
||||
|
||||
// Going forward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 1,
|
||||
@ -4986,10 +5004,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||
|
||||
// Going forward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 1,
|
||||
@ -5003,10 +5021,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||
|
||||
// Going backward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 1,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 1,
|
||||
@ -5020,10 +5038,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 3,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 1,
|
||||
@ -5036,6 +5054,55 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectLine disabled whitespace trimming" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 5, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString(" 12 34012 \n 123");
|
||||
|
||||
// Going forward
|
||||
{
|
||||
var sel = s.selectLine(.{
|
||||
.pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 0,
|
||||
} }).?,
|
||||
.whitespace = null,
|
||||
}).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }, s.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 4,
|
||||
.y = 2,
|
||||
} }, s.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
|
||||
// Non-wrapped
|
||||
{
|
||||
var sel = s.selectLine(.{
|
||||
.pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 3,
|
||||
} }).?,
|
||||
.whitespace = null,
|
||||
}).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 0,
|
||||
.y = 3,
|
||||
} }, s.pages.pointFromPin(.screen, sel.start()).?);
|
||||
try testing.expectEqual(point.Point{ .screen = .{
|
||||
.x = 4,
|
||||
.y = 3,
|
||||
} }, s.pages.pointFromPin(.screen, sel.end()).?);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectLine with scrollback" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
@ -5046,10 +5113,10 @@ test "Screen: selectLine with scrollback" {
|
||||
|
||||
// Selecting first line
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
@ -5063,10 +5130,10 @@ test "Screen: selectLine with scrollback" {
|
||||
|
||||
// Selecting last line
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 0,
|
||||
.y = 2,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
@ -5102,10 +5169,10 @@ test "Screen: selectLine semantic prompt boundary" {
|
||||
|
||||
// Selecting output stops at the prompt even if soft-wrapped
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 1,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
@ -5117,10 +5184,10 @@ test "Screen: selectLine semantic prompt boundary" {
|
||||
} }, s.pages.pointFromPin(.active, sel.end()).?);
|
||||
}
|
||||
{
|
||||
var sel = s.selectLine(s.pages.pin(.{ .active = .{
|
||||
var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
|
||||
.x = 1,
|
||||
.y = 2,
|
||||
} }).?).?;
|
||||
} }).? }).?;
|
||||
defer sel.deinit(&s);
|
||||
try testing.expectEqual(point.Point{ .active = .{
|
||||
.x = 0,
|
||||
|
Reference in New Issue
Block a user