terminal: selectLine can disable whitespace/sem prompt splitting

This commit is contained in:
Mitchell Hashimoto
2024-03-15 12:12:06 -07:00
parent dae4c3e52d
commit f4fa54984c
2 changed files with 119 additions and 52 deletions

View File

@ -2361,7 +2361,7 @@ pub fn mouseButtonCallback(
const sel_ = if (mods.ctrl) const sel_ = if (mods.ctrl)
self.io.terminal.screen.selectOutput(pin.*) self.io.terminal.screen.selectOutput(pin.*)
else else
self.io.terminal.screen.selectLine(pin.*); self.io.terminal.screen.selectLine(.{ .pin = pin.* });
if (sel_) |sel| { if (sel_) |sel| {
try self.setSelection(sel); try self.setSelection(sel);
try self.queueRender(); try self.queueRender();
@ -2728,12 +2728,12 @@ fn dragLeftClickTriple(
const click_pin = self.mouse.left_click_pin.?.*; const click_pin = self.mouse.left_click_pin.?.*;
// Get the word under our current point. If there isn't a word, do nothing. // 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. // 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 // 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. // 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); try self.setSelection(word);
return; return;
}; };

View File

@ -1229,30 +1229,42 @@ pub fn selectionString(
return string; 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 /// 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 /// lines and will omit the leading and trailing whitespace. If the point is
/// over whitespace but the line has non-whitespace characters elsewhere, the /// over whitespace but the line has non-whitespace characters elsewhere, the
/// line will be selected. /// line will be selected.
pub fn selectLine(self: *Screen, pin: Pin) ?Selection { pub fn selectLine(self: *Screen, opts: SelectLine) ?Selection {
_ = self; _ = self;
// Whitespace characters for selection purposes
const whitespace = &[_]u32{ 0, ' ', '\t' };
// Get the current point semantic prompt state since that determines // Get the current point semantic prompt state since that determines
// boundary conditions too. This makes it so that line selection can // boundary conditions too. This makes it so that line selection can
// only happen within the same prompt state. For example, if you triple // 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 // 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. // then the selection will stop prior to the prompt. See issue #1329.
const semantic_prompt_state = state: { const semantic_prompt_state: ?bool = state: {
const rac = pin.rowAndCell(); if (!opts.semantic_prompt_boundary) break :state null;
const rac = opts.pin.rowAndCell();
break :state rac.row.semantic_prompt.promptOrInput(); break :state rac.row.semantic_prompt.promptOrInput();
}; };
// The real start of the row is the first row in the soft-wrap. // The real start of the row is the first row in the soft-wrap.
const start_pin: Pin = start_pin: { const start_pin: Pin = start_pin: {
var it = pin.rowIterator(.left_up, null); var it = opts.pin.rowIterator(.left_up, null);
var it_prev: Pin = pin; var it_prev: Pin = opts.pin;
while (it.next()) |p| { while (it.next()) |p| {
const row = p.rowAndCell().row; const row = p.rowAndCell().row;
@ -1262,12 +1274,14 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
break :start_pin copy; break :start_pin copy;
} }
// See semantic_prompt_state comment for why if (semantic_prompt_state) |v| {
const current_prompt = row.semantic_prompt.promptOrInput(); // See semantic_prompt_state comment for why
if (current_prompt != semantic_prompt_state) { const current_prompt = row.semantic_prompt.promptOrInput();
var copy = it_prev; if (current_prompt != v) {
copy.x = 0; var copy = it_prev;
break :start_pin copy; copy.x = 0;
break :start_pin copy;
}
} }
it_prev = p; 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. // The real end of the row is the final row in the soft-wrap.
const end_pin: Pin = end_pin: { 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| { while (it.next()) |p| {
const row = p.rowAndCell().row; const row = p.rowAndCell().row;
// See semantic_prompt_state comment for why if (semantic_prompt_state) |v| {
const current_prompt = row.semantic_prompt.promptOrInput(); // See semantic_prompt_state comment for why
if (current_prompt != semantic_prompt_state) { const current_prompt = row.semantic_prompt.promptOrInput();
var prev = p.up(1).?; if (current_prompt != v) {
prev.x = p.page.data.size.cols - 1; var prev = p.up(1).?;
break :end_pin prev; prev.x = p.page.data.size.cols - 1;
break :end_pin prev;
}
} }
if (!row.wrap) { 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. // Go forward from the start to find the first non-whitespace character.
const start: Pin = start: { const start: Pin = start: {
const whitespace = opts.whitespace orelse break :start start_pin;
var it = start_pin.cellIterator(.right_down, end_pin); var it = start_pin.cellIterator(.right_down, end_pin);
while (it.next()) |p| { while (it.next()) |p| {
const cell = p.rowAndCell().cell; const cell = p.rowAndCell().cell;
@ -1311,9 +1328,9 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
// Non-empty means we found it. // Non-empty means we found it.
const this_whitespace = std.mem.indexOfAny( const this_whitespace = std.mem.indexOfAny(
u32, u21,
whitespace, whitespace,
&[_]u32{cell.content.codepoint}, &[_]u21{cell.content.codepoint},
) != null; ) != null;
if (this_whitespace) continue; 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. // Go backward from the end to find the first non-whitespace character.
const end: Pin = end: { const end: Pin = end: {
const whitespace = opts.whitespace orelse break :end end_pin;
var it = end_pin.cellIterator(.left_up, start_pin); var it = end_pin.cellIterator(.left_up, start_pin);
while (it.next()) |p| { while (it.next()) |p| {
const cell = p.rowAndCell().cell; const cell = p.rowAndCell().cell;
@ -1332,9 +1350,9 @@ pub fn selectLine(self: *Screen, pin: Pin) ?Selection {
// Non-empty means we found it. // Non-empty means we found it.
const this_whitespace = std.mem.indexOfAny( const this_whitespace = std.mem.indexOfAny(
u32, u21,
whitespace, whitespace,
&[_]u32{cell.content.codepoint}, &[_]u21{cell.content.codepoint},
) != null; ) != null;
if (this_whitespace) continue; if (this_whitespace) continue;
@ -4883,10 +4901,10 @@ test "Screen: selectLine" {
// Going forward // Going forward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 0, .x = 0,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 0, .x = 0,
@ -4900,10 +4918,10 @@ test "Screen: selectLine" {
// Going backward // Going backward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 7, .x = 7,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 0, .x = 0,
@ -4917,10 +4935,10 @@ test "Screen: selectLine" {
// Going forward and backward // Going forward and backward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 3, .x = 3,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 0, .x = 0,
@ -4934,10 +4952,10 @@ test "Screen: selectLine" {
// Outside active area // Outside active area
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 9, .x = 9,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 0, .x = 0,
@ -4960,10 +4978,10 @@ test "Screen: selectLine across soft-wrap" {
// Going forward // Going forward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 1, .x = 1,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 1, .x = 1,
@ -4986,10 +5004,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
// Going forward // Going forward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 1, .x = 1,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 1, .x = 1,
@ -5003,10 +5021,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
// Going backward // Going backward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 1, .x = 1,
.y = 1, .y = 1,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 1, .x = 1,
@ -5020,10 +5038,10 @@ test "Screen: selectLine across soft-wrap ignores blank lines" {
// Going forward and backward // Going forward and backward
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 3, .x = 3,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .screen = .{ try testing.expectEqual(point.Point{ .screen = .{
.x = 1, .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" { test "Screen: selectLine with scrollback" {
const testing = std.testing; const testing = std.testing;
const alloc = testing.allocator; const alloc = testing.allocator;
@ -5046,10 +5113,10 @@ test "Screen: selectLine with scrollback" {
// Selecting first line // Selecting first line
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 0, .x = 0,
.y = 0, .y = 0,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{ try testing.expectEqual(point.Point{ .active = .{
.x = 0, .x = 0,
@ -5063,10 +5130,10 @@ test "Screen: selectLine with scrollback" {
// Selecting last line // Selecting last line
{ {
var sel = s.selectLine(s.pages.pin(.{ .active = .{ var sel = s.selectLine(.{ .pin = s.pages.pin(.{ .active = .{
.x = 0, .x = 0,
.y = 2, .y = 2,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{ try testing.expectEqual(point.Point{ .active = .{
.x = 0, .x = 0,
@ -5102,10 +5169,10 @@ test "Screen: selectLine semantic prompt boundary" {
// Selecting output stops at the prompt even if soft-wrapped // 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, .x = 1,
.y = 1, .y = 1,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{ try testing.expectEqual(point.Point{ .active = .{
.x = 0, .x = 0,
@ -5117,10 +5184,10 @@ test "Screen: selectLine semantic prompt boundary" {
} }, s.pages.pointFromPin(.active, sel.end()).?); } }, 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, .x = 1,
.y = 2, .y = 2,
} }).?).?; } }).? }).?;
defer sel.deinit(&s); defer sel.deinit(&s);
try testing.expectEqual(point.Point{ .active = .{ try testing.expectEqual(point.Point{ .active = .{
.x = 0, .x = 0,