mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
Merge pull request #54 from mitchellh/multiclick
Double/Triple-click selects word and lines respectively
This commit is contained in:
160
src/Window.zig
160
src/Window.zig
@ -75,6 +75,7 @@ renderer_thr: std.Thread,
|
||||
|
||||
/// Mouse state.
|
||||
mouse: Mouse,
|
||||
mouse_interval: u64,
|
||||
|
||||
/// The terminal IO handler.
|
||||
io: termio.Impl,
|
||||
@ -117,6 +118,12 @@ const Mouse = struct {
|
||||
left_click_xpos: f64 = 0,
|
||||
left_click_ypos: f64 = 0,
|
||||
|
||||
/// The count of clicks to count double and triple clicks and so on.
|
||||
/// The left click time was the last time the left click was done. This
|
||||
/// is always set on the first left click.
|
||||
left_click_count: u8 = 0,
|
||||
left_click_time: std.time.Instant = undefined,
|
||||
|
||||
/// The last x/y sent for mouse reports.
|
||||
event_point: terminal.point.Viewport = .{},
|
||||
};
|
||||
@ -388,6 +395,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
|
||||
},
|
||||
.renderer_thr = undefined,
|
||||
.mouse = .{},
|
||||
.mouse_interval = 500 * 1_000_000, // 500ms
|
||||
.io = io,
|
||||
.io_thread = io_thread,
|
||||
.io_thr = undefined,
|
||||
@ -1525,11 +1533,57 @@ fn mouseButtonCallback(
|
||||
win.mouse.left_click_xpos = pos.xpos;
|
||||
win.mouse.left_click_ypos = pos.ypos;
|
||||
|
||||
// Selection is always cleared
|
||||
if (win.io.terminal.selection != null) {
|
||||
win.io.terminal.selection = null;
|
||||
win.queueRender() catch |err|
|
||||
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
||||
// Setup our click counter and timer
|
||||
if (std.time.Instant.now()) |now| {
|
||||
// If we have mouse clicks, then we check if the time elapsed
|
||||
// is less than and our interval and if so, increase the count.
|
||||
if (win.mouse.left_click_count > 0) {
|
||||
const since = now.since(win.mouse.left_click_time);
|
||||
if (since > win.mouse_interval) {
|
||||
win.mouse.left_click_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
win.mouse.left_click_time = now;
|
||||
win.mouse.left_click_count += 1;
|
||||
|
||||
// We only support up to triple-clicks.
|
||||
if (win.mouse.left_click_count > 3) win.mouse.left_click_count = 1;
|
||||
} else |err| {
|
||||
win.mouse.left_click_count = 1;
|
||||
log.err("error reading time, mouse multi-click won't work err={}", .{err});
|
||||
}
|
||||
|
||||
switch (win.mouse.left_click_count) {
|
||||
// First mouse click, clear selection
|
||||
1 => if (win.io.terminal.selection != null) {
|
||||
win.io.terminal.selection = null;
|
||||
win.queueRender() catch |err|
|
||||
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
||||
},
|
||||
|
||||
// Double click, select the word under our mouse
|
||||
2 => {
|
||||
const sel_ = win.io.terminal.screen.selectWord(win.mouse.left_click_point);
|
||||
if (sel_) |sel| {
|
||||
win.io.terminal.selection = sel;
|
||||
win.queueRender() catch |err|
|
||||
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
||||
}
|
||||
},
|
||||
|
||||
// Triple click, select the line under our mouse
|
||||
3 => {
|
||||
const sel_ = win.io.terminal.screen.selectLine(win.mouse.left_click_point);
|
||||
if (sel_) |sel| {
|
||||
win.io.terminal.selection = sel;
|
||||
win.queueRender() catch |err|
|
||||
log.err("error scheduling render in mouseButtinCallback err={}", .{err});
|
||||
}
|
||||
},
|
||||
|
||||
// We should be bounded by 1 to 3
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1598,6 +1652,70 @@ fn cursorPosCallback(
|
||||
const viewport_point = win.posToViewport(xpos, ypos);
|
||||
const screen_point = viewport_point.toScreen(&win.io.terminal.screen);
|
||||
|
||||
// Handle dragging depending on click count
|
||||
switch (win.mouse.left_click_count) {
|
||||
1 => win.dragLeftClickSingle(screen_point, xpos),
|
||||
2 => win.dragLeftClickDouble(screen_point),
|
||||
3 => win.dragLeftClickTriple(screen_point),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
/// Double-click dragging moves the selection one "word" at a time.
|
||||
fn dragLeftClickDouble(
|
||||
self: *Window,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
) void {
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = self.io.terminal.screen.selectWord(screen_point) 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 = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse {
|
||||
self.io.terminal.selection = word;
|
||||
return;
|
||||
};
|
||||
|
||||
// Grow our selection
|
||||
if (screen_point.before(self.mouse.left_click_point)) {
|
||||
sel.start = word.start;
|
||||
} else {
|
||||
sel.end = word.end;
|
||||
}
|
||||
self.io.terminal.selection = sel;
|
||||
}
|
||||
|
||||
/// Triple-click dragging moves the selection one "line" at a time.
|
||||
fn dragLeftClickTriple(
|
||||
self: *Window,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
) void {
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = self.io.terminal.screen.selectLine(screen_point) 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 = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse {
|
||||
self.io.terminal.selection = word;
|
||||
return;
|
||||
};
|
||||
|
||||
// Grow our selection
|
||||
if (screen_point.before(self.mouse.left_click_point)) {
|
||||
sel.start = word.start;
|
||||
} else {
|
||||
sel.end = word.end;
|
||||
}
|
||||
self.io.terminal.selection = sel;
|
||||
}
|
||||
|
||||
fn dragLeftClickSingle(
|
||||
self: *Window,
|
||||
screen_point: terminal.point.ScreenPoint,
|
||||
xpos: f64,
|
||||
) void {
|
||||
// NOTE(mitchellh): This logic super sucks. There has to be an easier way
|
||||
// to calculate this, but this is good for a v1. Selection isn't THAT
|
||||
// common so its not like this performance heavy code is running that
|
||||
@ -1607,13 +1725,13 @@ fn cursorPosCallback(
|
||||
// If we were selecting, and we switched directions, then we restart
|
||||
// calculations because it forces us to reconsider if the first cell is
|
||||
// selected.
|
||||
if (win.io.terminal.selection) |sel| {
|
||||
if (self.io.terminal.selection) |sel| {
|
||||
const reset: bool = if (sel.end.before(sel.start))
|
||||
sel.start.before(screen_point)
|
||||
else
|
||||
screen_point.before(sel.start);
|
||||
|
||||
if (reset) win.io.terminal.selection = null;
|
||||
if (reset) self.io.terminal.selection = null;
|
||||
}
|
||||
|
||||
// Our logic for determing if the starting cell is selected:
|
||||
@ -1629,23 +1747,23 @@ fn cursorPosCallback(
|
||||
//
|
||||
|
||||
// the boundary point at which we consider selection or non-selection
|
||||
const cell_xboundary = win.cell_size.width * 0.6;
|
||||
const cell_xboundary = self.cell_size.width * 0.6;
|
||||
|
||||
// first xpos of the clicked cell
|
||||
const cell_xstart = @intToFloat(f32, win.mouse.left_click_point.x) * win.cell_size.width;
|
||||
const cell_start_xpos = win.mouse.left_click_xpos - cell_xstart;
|
||||
const cell_xstart = @intToFloat(f32, self.mouse.left_click_point.x) * self.cell_size.width;
|
||||
const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart;
|
||||
|
||||
// If this is the same cell, then we only start the selection if weve
|
||||
// moved past the boundary point the opposite direction from where we
|
||||
// started.
|
||||
if (std.meta.eql(screen_point, win.mouse.left_click_point)) {
|
||||
if (std.meta.eql(screen_point, self.mouse.left_click_point)) {
|
||||
const cell_xpos = xpos - cell_xstart;
|
||||
const selected: bool = if (cell_start_xpos < cell_xboundary)
|
||||
cell_xpos >= cell_xboundary
|
||||
else
|
||||
cell_xpos < cell_xboundary;
|
||||
|
||||
win.io.terminal.selection = if (selected) .{
|
||||
self.io.terminal.selection = if (selected) .{
|
||||
.start = screen_point,
|
||||
.end = screen_point,
|
||||
} else null;
|
||||
@ -1655,29 +1773,29 @@ fn cursorPosCallback(
|
||||
|
||||
// If this is a different cell and we haven't started selection,
|
||||
// we determine the starting cell first.
|
||||
if (win.io.terminal.selection == null) {
|
||||
if (self.io.terminal.selection == null) {
|
||||
// - If we're moving to a point before the start, then we select
|
||||
// the starting cell if we started after the boundary, else
|
||||
// we start selection of the prior cell.
|
||||
// - Inverse logic for a point after the start.
|
||||
const click_point = win.mouse.left_click_point;
|
||||
const click_point = self.mouse.left_click_point;
|
||||
const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: {
|
||||
if (win.mouse.left_click_xpos > cell_xboundary) {
|
||||
if (self.mouse.left_click_xpos > cell_xboundary) {
|
||||
break :start click_point;
|
||||
} else {
|
||||
break :start if (click_point.x > 0) terminal.point.ScreenPoint{
|
||||
.y = click_point.y,
|
||||
.x = click_point.x - 1,
|
||||
} else terminal.point.ScreenPoint{
|
||||
.x = win.io.terminal.screen.cols - 1,
|
||||
.x = self.io.terminal.screen.cols - 1,
|
||||
.y = click_point.y -| 1,
|
||||
};
|
||||
}
|
||||
} else start: {
|
||||
if (win.mouse.left_click_xpos < cell_xboundary) {
|
||||
if (self.mouse.left_click_xpos < cell_xboundary) {
|
||||
break :start click_point;
|
||||
} else {
|
||||
break :start if (click_point.x < win.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{
|
||||
break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{
|
||||
.y = click_point.y,
|
||||
.x = click_point.x + 1,
|
||||
} else terminal.point.ScreenPoint{
|
||||
@ -1687,7 +1805,7 @@ fn cursorPosCallback(
|
||||
}
|
||||
};
|
||||
|
||||
win.io.terminal.selection = .{ .start = start, .end = screen_point };
|
||||
self.io.terminal.selection = .{ .start = start, .end = screen_point };
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1696,8 +1814,8 @@ fn cursorPosCallback(
|
||||
|
||||
// We moved! Set the selection end point. The start point should be
|
||||
// set earlier.
|
||||
assert(win.io.terminal.selection != null);
|
||||
win.io.terminal.selection.?.end = screen_point;
|
||||
assert(self.io.terminal.selection != null);
|
||||
self.io.terminal.selection.?.end = screen_point;
|
||||
}
|
||||
|
||||
fn posToViewport(self: Window, xpos: f64, ypos: f64) terminal.point.Viewport {
|
||||
|
@ -64,6 +64,9 @@ const fastmem = @import("../fastmem.zig");
|
||||
|
||||
const log = std.log.scoped(.screen);
|
||||
|
||||
/// Whitespace characters for selection purposes
|
||||
const whitespace = &[_]u32{ 0, ' ', '\t' };
|
||||
|
||||
/// Cursor represents the cursor state.
|
||||
pub const Cursor = struct {
|
||||
// x, y where the cursor currently exists (0-indexed). This x/y is
|
||||
@ -1096,6 +1099,220 @@ pub fn clearHistory(self: *Screen) void {
|
||||
self.viewport = 0;
|
||||
}
|
||||
|
||||
/// 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, pt: point.ScreenPoint) ?Selection {
|
||||
// Impossible to select anything outside of the area we've written.
|
||||
const y_max = self.rowsWritten() - 1;
|
||||
if (pt.y > y_max or pt.x >= self.cols) return null;
|
||||
|
||||
// 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;
|
||||
|
||||
var y: usize = pt.y - 1;
|
||||
while (true) {
|
||||
const current = self.getRow(.{ .screen = y });
|
||||
if (!current.header().flags.wrap) break :start_row y + 1;
|
||||
if (y == 0) break :start_row y;
|
||||
y -= 1;
|
||||
}
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// The real end of the row is the final row in the soft-wrap.
|
||||
const end_row: usize = end_row: {
|
||||
var y: usize = pt.y;
|
||||
while (y < y_max) : (y += 1) {
|
||||
const current = self.getRow(.{ .screen = y });
|
||||
if (y == y_max or !current.header().flags.wrap) break :end_row y;
|
||||
}
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Go forward from the start to find the first non-whitespace character.
|
||||
const start: point.ScreenPoint = start: {
|
||||
var y: usize = start_row;
|
||||
while (y <= y_max) : (y += 1) {
|
||||
const current_row = self.getRow(.{ .screen = y });
|
||||
var x: usize = 0;
|
||||
while (x < self.cols) : (x += 1) {
|
||||
const cell = current_row.getCell(x);
|
||||
|
||||
// Empty is whitespace
|
||||
if (cell.empty()) continue;
|
||||
|
||||
// Non-empty means we found it.
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
whitespace,
|
||||
&[_]u32{cell.char},
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
break :start .{ .x = x, .y = y };
|
||||
}
|
||||
}
|
||||
|
||||
// There is no start point and therefore no line that can be selected.
|
||||
return null;
|
||||
};
|
||||
|
||||
// Go backward from the end to find the first non-whitespace character.
|
||||
const end: point.ScreenPoint = end: {
|
||||
var y: usize = end_row;
|
||||
while (true) {
|
||||
const current_row = self.getRow(.{ .screen = y });
|
||||
|
||||
var x: usize = 0;
|
||||
while (x < self.cols) : (x += 1) {
|
||||
const real_x = self.cols - x - 1;
|
||||
const cell = current_row.getCell(real_x);
|
||||
|
||||
// Empty or whitespace, ignore.
|
||||
if (cell.empty()) continue;
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
whitespace,
|
||||
&[_]u32{cell.char},
|
||||
) != null;
|
||||
if (this_whitespace) continue;
|
||||
|
||||
// Got it
|
||||
break :end .{ .x = real_x, .y = y };
|
||||
}
|
||||
|
||||
if (y == 0) break;
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
// There is no start point and therefore no line that can be selected.
|
||||
return null;
|
||||
};
|
||||
|
||||
return Selection{
|
||||
.start = start,
|
||||
.end = end,
|
||||
};
|
||||
}
|
||||
|
||||
/// Select the word under the given point. A word is any consecutive series
|
||||
/// of characters that are exclusively whitespace or exclusively non-whitespace.
|
||||
/// A selection can span multiple physical lines if they are soft-wrapped.
|
||||
///
|
||||
/// This will return null if a selection is impossible. The only scenario
|
||||
/// this happens is if the point pt is outside of the written screen space.
|
||||
pub fn selectWord(self: *Screen, pt: point.ScreenPoint) ?Selection {
|
||||
// Impossible to select anything outside of the area we've written.
|
||||
const y_max = self.rowsWritten() - 1;
|
||||
if (pt.y > y_max) return null;
|
||||
|
||||
// Get our row
|
||||
const row = self.getRow(.{ .screen = pt.y });
|
||||
const start_cell = row.getCell(pt.x);
|
||||
|
||||
// If our cell is empty we can't select a word, because we can't select
|
||||
// areas where the screen is not yet written.
|
||||
if (start_cell.empty()) return null;
|
||||
|
||||
// Determine if we are whitespace or not to determine what our boundary is.
|
||||
const expect_whitespace = std.mem.indexOfAny(u32, whitespace, &[_]u32{start_cell.char}) != null;
|
||||
|
||||
// Go forwards to find our end boundary
|
||||
const end: point.ScreenPoint = boundary: {
|
||||
var prev: point.ScreenPoint = pt;
|
||||
var y: usize = pt.y;
|
||||
var x: usize = pt.x;
|
||||
while (y <= y_max) : (y += 1) {
|
||||
const current_row = self.getRow(.{ .screen = y });
|
||||
|
||||
// Go through all the remainining cells on this row until
|
||||
// we reach a boundary condition.
|
||||
while (x < self.cols) : (x += 1) {
|
||||
const cell = current_row.getCell(x);
|
||||
|
||||
// If we reached an empty cell its always a boundary
|
||||
if (cell.empty()) break :boundary prev;
|
||||
|
||||
// If we do not match our expected set, we hit a boundary
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
whitespace,
|
||||
&[_]u32{cell.char},
|
||||
) != null;
|
||||
if (this_whitespace != expect_whitespace) break :boundary prev;
|
||||
|
||||
// Increase our prev
|
||||
prev.x = x;
|
||||
prev.y = y;
|
||||
}
|
||||
|
||||
// If we aren't wrapping, then we're done this is a boundary.
|
||||
if (!current_row.header().flags.wrap) break :boundary prev;
|
||||
|
||||
// If we are wrapping, reset some values and search the next line.
|
||||
x = 0;
|
||||
}
|
||||
|
||||
break :boundary .{ .x = self.cols - 1, .y = y_max };
|
||||
};
|
||||
|
||||
// Go backwards to find our start boundary
|
||||
const start: point.ScreenPoint = boundary: {
|
||||
var current_row = row;
|
||||
var prev: point.ScreenPoint = pt;
|
||||
|
||||
var y: usize = pt.y;
|
||||
var x: usize = pt.x;
|
||||
while (true) {
|
||||
// Go through all the remainining cells on this row until
|
||||
// we reach a boundary condition.
|
||||
while (x > 0) : (x -= 1) {
|
||||
const cell = current_row.getCell(x - 1);
|
||||
const this_whitespace = std.mem.indexOfAny(
|
||||
u32,
|
||||
whitespace,
|
||||
&[_]u32{cell.char},
|
||||
) != null;
|
||||
if (this_whitespace != expect_whitespace) break :boundary prev;
|
||||
|
||||
// Update our prev
|
||||
prev.x = x - 1;
|
||||
prev.y = y;
|
||||
}
|
||||
|
||||
// If we're at the start, we need to check if the previous line wrapped.
|
||||
// If we are wrapped, we continue searching. If we are not wrapped,
|
||||
// then we've hit a boundary.
|
||||
assert(prev.x == 0);
|
||||
|
||||
// If we're at the end, we're done!
|
||||
if (y == 0) break;
|
||||
|
||||
// If the previous row did not wrap, then we're done. Otherwise
|
||||
// we keep searching.
|
||||
y -= 1;
|
||||
current_row = self.getRow(.{ .screen = y });
|
||||
if (!current_row.header().flags.wrap) break :boundary prev;
|
||||
|
||||
// Set x to start at the first non-empty cell
|
||||
x = self.cols;
|
||||
while (x > 0) : (x -= 1) {
|
||||
if (!current_row.getCell(x - 1).empty()) break;
|
||||
}
|
||||
}
|
||||
|
||||
break :boundary .{ .x = 0, .y = 0 };
|
||||
};
|
||||
|
||||
return Selection{
|
||||
.start = start,
|
||||
.end = end,
|
||||
};
|
||||
}
|
||||
|
||||
/// Scroll behaviors for the scroll function.
|
||||
pub const Scroll = union(enum) {
|
||||
/// Scroll to the top of the scroll buffer. The first line of the
|
||||
@ -2438,6 +2655,248 @@ test "Screen: clone one line viewport" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectLine" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("ABC DEF\n 123\n456");
|
||||
|
||||
// Outside of active area
|
||||
try testing.expect(s.selectLine(.{ .x = 13, .y = 0 }) == null);
|
||||
try testing.expect(s.selectLine(.{ .x = 0, .y = 5 }) == null);
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 0, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 7), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Going backward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 7, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 7), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 3, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 7), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Outside active area
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 9, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 7), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectLine across soft-wrap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 5, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString(" 12 34012 \n 123");
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 1, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 3), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectLine across soft-wrap ignores blank lines" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 5, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString(" 12 34012 \n 123");
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 1, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 3), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going backward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 1, .y = 1 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 3), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
const sel = s.selectLine(.{ .x = 3, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 3), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectWord" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 10, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("ABC DEF\n 123\n456");
|
||||
|
||||
// Outside of active area
|
||||
try testing.expect(s.selectWord(.{ .x = 9, .y = 0 }) == null);
|
||||
try testing.expect(s.selectWord(.{ .x = 0, .y = 5 }) == null);
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 0, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Going backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 2, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 1, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Whitespace
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 3, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 3), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 4), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.end.y);
|
||||
}
|
||||
|
||||
// Whitespace single char
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 0, .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);
|
||||
}
|
||||
|
||||
// End of screen
|
||||
{
|
||||
const sel = s.selectWord(.{ .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, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectWord across soft-wrap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 5, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString(" 1234012\n 123");
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 1, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 1, .y = 1 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 3, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: selectWord whitespace across soft-wrap" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 10, 5, 0);
|
||||
defer s.deinit();
|
||||
try s.testWriteString("1 1\n 123");
|
||||
|
||||
// Going forward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 1, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 1, .y = 1 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
|
||||
// Going forward and backward
|
||||
{
|
||||
const sel = s.selectWord(.{ .x = 3, .y = 0 }).?;
|
||||
try testing.expectEqual(@as(usize, 1), sel.start.x);
|
||||
try testing.expectEqual(@as(usize, 0), sel.start.y);
|
||||
try testing.expectEqual(@as(usize, 2), sel.end.x);
|
||||
try testing.expectEqual(@as(usize, 1), sel.end.y);
|
||||
}
|
||||
}
|
||||
|
||||
test "Screen: scrollRegionUp single" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
Reference in New Issue
Block a user