core: click to move cursor

This commit is contained in:
Mitchell Hashimoto
2023-12-20 21:33:13 -08:00
parent 6b1fd2b9eb
commit 4a3e1e15e5
3 changed files with 94 additions and 14 deletions

View File

@ -1981,6 +1981,16 @@ pub fn mouseButtonCallback(
}
}
// For left button click release we check if we are moving our cursor.
if (button == .left and action == .release and mods.alt) {
// Moving always resets the click count so that we don't highlight.
self.mouse.left_click_count = 0;
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
try self.clickMoveCursor(self.mouse.left_click_point);
}
// For left button clicks we always record some information for
// selection/highlighting purposes.
if (button == .left and action == .press) {
@ -1988,6 +1998,8 @@ pub fn mouseButtonCallback(
defer self.renderer_state.mutex.unlock();
const pos = try self.rt_surface.getCursorPos();
const pt_viewport = self.posToViewport(pos.x, pos.y);
const pt_screen = pt_viewport.toScreen(&self.io.terminal.screen);
// If we move our cursor too much between clicks then we reset
// the multi-click state.
@ -2002,8 +2014,7 @@ pub fn mouseButtonCallback(
}
// Store it
const point = self.posToViewport(pos.x, pos.y);
self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen);
self.mouse.left_click_point = pt_screen;
self.mouse.left_click_xpos = pos.x;
self.mouse.left_click_ypos = pos.y;
@ -2029,10 +2040,13 @@ pub fn mouseButtonCallback(
}
switch (self.mouse.left_click_count) {
// First mouse click, clear selection
1 => if (self.io.terminal.screen.selection != null) {
self.setSelection(null);
try self.queueRender();
// Single click
1 => {
// If we have a selection, clear it. This always happens.
if (self.io.terminal.screen.selection != null) {
self.setSelection(null);
try self.queueRender();
}
},
// Double click, select the word under our mouse
@ -2075,6 +2089,51 @@ pub fn mouseButtonCallback(
}
}
fn clickMoveCursor(self: *Surface, to: terminal.point.ScreenPoint) !void {
// Get our path
const from = (terminal.point.Viewport{
.x = self.io.terminal.screen.cursor.x,
.y = self.io.terminal.screen.cursor.y,
}).toScreen(&self.io.terminal.screen);
const path = self.io.terminal.screen.promptPath(from, to);
log.debug("click-to-move-cursor from={} to={} path={}", .{ from, to, path });
// If we aren't moving at all, fast path out of here.
if (path.x == 0 and path.y == 0) return;
// Convert our path to arrow key inputs. Yes, that is how this works.
// Yes, that is pretty sad. Yes, this could backfire in various ways.
// But its the best we can do.
// We do Y first because it prevents any weird wrap behavior.
if (path.y != 0) {
const arrow = if (path.y < 0) arrow: {
break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOA" else "\x1b[A";
} else arrow: {
break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOB" else "\x1b[B";
};
for (0..@abs(path.y)) |_| {
_ = self.io_thread.mailbox.push(.{
.write_stable = arrow,
}, .{ .instant = {} });
}
}
if (path.x != 0) {
const arrow = if (path.x < 0) arrow: {
break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOD" else "\x1b[D";
} else arrow: {
break :arrow if (self.io.terminal.modes.get(.cursor_keys)) "\x1bOC" else "\x1b[C";
};
for (0..@abs(path.x)) |_| {
_ = self.io_thread.mailbox.push(.{
.write_stable = arrow,
}, .{ .instant = {} });
}
}
try self.io_thread.wakeup.notify();
}
/// Returns the link at the given cursor position, if any.
fn linkAtPos(
self: *Surface,

View File

@ -1886,24 +1886,43 @@ pub fn selectOutput(self: *Screen, pt: point.ScreenPoint) ?Selection {
pub fn selectPrompt(self: *Screen, pt: point.ScreenPoint) ?Selection {
// Ensure that the line the point is on is a prompt.
const pt_row = self.getRow(.{ .screen = pt.y });
switch (pt_row.getSemanticPrompt()) {
.prompt, .prompt_continuation, .input => {},
.command, .unknown => return null,
}
const is_known = switch (pt_row.getSemanticPrompt()) {
.prompt, .prompt_continuation, .input => true,
.command => return null,
// We allow unknown to continue because not all shells output any
// semantic prompt information for continuation lines. This has the
// possibility of making this function VERY slow (we look at all
// scrollback) so we should try to avoid this in the future by
// setting a flag or something if we have EVER seen a semantic
// prompt sequence.
.unknown => false,
};
// Find the start of the prompt.
var saw_semantic_prompt = is_known;
const start: usize = start: for (0..pt.y) |offset| {
const y = pt.y - offset;
const row = self.getRow(.{ .screen = y - 1 });
switch (row.getSemanticPrompt()) {
// A prompt, we continue searching.
.prompt, .prompt_continuation, .input => {},
.prompt, .prompt_continuation, .input => saw_semantic_prompt = true,
// See comment about "unknown" a few lines above. If we have
// previously seen a semantic prompt then if we see an unknown
// we treat it as a boundary.
.unknown => if (saw_semantic_prompt) break :start y,
// Command output or unknown, definitely not a prompt.
.command, .unknown => break :start y,
.command => break :start y,
}
} else 0;
// If we never saw a semantic prompt flag, then we can't trust our
// start value and we return null. This scenario usually means that
// semantic prompts aren't enabled via the shell.
if (!saw_semantic_prompt) return null;
// Find the end of the prompt.
const end: usize = end: for (pt.y..self.rowsWritten()) |y| {
const row = self.getRow(.{ .screen = y });
@ -4891,10 +4910,12 @@ test "Screen: selectPrompt prompt at start" {
row.setSemanticPrompt(.prompt);
row = s.getRow(.{ .screen = 1 });
row.setSemanticPrompt(.input);
row = s.getRow(.{ .screen = 2 });
row.setSemanticPrompt(.command);
// Not at a prompt
{
const sel = s.selectPrompt(.{ .x = 0, .y = 2 });
const sel = s.selectPrompt(.{ .x = 0, .y = 3 });
try testing.expect(sel == null);
}

View File

@ -2115,7 +2115,7 @@ pub fn setLeftAndRightMargin(self: *Terminal, left_req: usize, right_req: usize)
/// (OSC 133) only allow setting this for wherever the current active cursor
/// is located.
pub fn markSemanticPrompt(self: *Terminal, p: SemanticPrompt) void {
//log.warn("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p });
//log.debug("semantic_prompt y={} p={}", .{ self.screen.cursor.y, p });
const row = self.screen.getRow(.{ .active = self.screen.cursor.y });
row.setSemanticPrompt(switch (p) {
.prompt => .prompt,