From b542f7e3c498150bc00f94e9968ba1ed024b015f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 6 Jul 2023 10:15:23 -0700 Subject: [PATCH 1/3] terminal: jump to prompt core methods --- src/terminal/Screen.zig | 143 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 710dd75fb..6255fd333 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1476,6 +1476,11 @@ pub const Scroll = union(enum) { /// Scrolling down at the bottom will do nothing (similar to how /// delta at the top does nothing). delta_no_grow: isize, + + /// Scroll so the given row is in view. If the row is in the viewport, + /// this will change nothing. If the row is outside the viewport, the + /// viewport will change so that this row is at the top of the viewport. + row: RowIndex, }; /// Scroll the screen by the given behavior. Note that this will always @@ -1495,13 +1500,34 @@ pub fn scroll(self: *Screen, behavior: Scroll) !void { // TODO: deltas greater than the entire scrollback .delta => |delta| try self.scrollDelta(delta, true), .delta_no_grow => |delta| try self.scrollDelta(delta, false), + + // Scroll to a specific row + .row => |idx| self.scrollRow(idx), } } +fn scrollRow(self: *Screen, idx: RowIndex) void { + // Convert the given row to a screen point. + const screen_idx = idx.toScreen(self); + const screen_pt: point.ScreenPoint = .{ .y = screen_idx.screen }; + + // If the point is already in our viewport, we do nothing. + if (screen_pt.inViewport(self)) return; + + // Move the viewport so that the screen point is in view. We do the + // @min here so that we don't scroll down below where our "bottom" + // viewport is. + self.viewport = @min(self.history, screen_pt.y); + assert(screen_pt.inViewport(self)); +} + fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void { const tracy = trace(@src()); defer tracy.end(); + // Just in case, to avoid a bunch of stuff below. + if (delta == 0) return; + // If we're scrolling up, then we just subtract and we're done. // We just clamp at 0 which blocks us from scrolling off the top. if (delta < 0) { @@ -1611,6 +1637,62 @@ fn scrollDelta(self: *Screen, delta: isize, grow: bool) !void { ); } +/// The options for where you can jump to on the screen. +pub const JumpTarget = union(enum) { + /// Jump forwards (positive) or backwards (negative) a set number of + /// prompts. If the absolute value is greater than the number of prompts + /// in either direction, jump to the furthest prompt. + prompt_delta: isize, +}; + +/// Jump the viewport to specific location. +pub fn jump(self: *Screen, target: JumpTarget) bool { + return switch (target) { + .prompt_delta => |delta| self.jumpPrompt(delta), + }; +} + +/// Jump the viewport forwards (positive) or backwards (negative) a set number of +/// prompts (delta). Returns true if the viewport changed and false if no jump +/// occurred. +fn jumpPrompt(self: *Screen, delta: isize) bool { + // If we aren't jumping any prompts then we don't need to do anything. + if (delta == 0) return false; + + // The screen y value we start at + const start_y: isize = start_y: { + const idx: RowIndex = .{ .viewport = 0 }; + const screen = idx.toScreen(self); + break :start_y @intCast(screen.screen); + }; + + // The maximum y in the positive direction. Negative is always 0. + const max_y: isize = @intCast(self.rowsWritten() - 1); + + // Go line-by-line counting the number of prompts we see. + var step: isize = if (delta > 0) 1 else -1; + var y: isize = start_y + step; + const delta_start: usize = @intCast(if (delta > 0) delta else -delta); + var delta_rem: usize = delta_start; + while (y >= 0 and y <= max_y and delta_rem > 0) : (y += step) { + const row = self.getRow(.{ .screen = @intCast(y) }); + switch (row.getSemanticPrompt()) { + .prompt, .input => delta_rem -= 1, + .command, .unknown => {}, + } + } + + // If we didn't find any, do nothing. + if (delta_rem == delta_start) return false; + + // Done! We count the number of lines we changed and scroll. + const y_delta = (y - step) - start_y; + const new_y: usize = @intCast(start_y + y_delta); + const old_viewport = self.viewport; + self.scroll(.{ .row = .{ .screen = new_y } }) catch unreachable; + return self.viewport != old_viewport; +} + /// Returns the raw text associated with a selection. This will unwrap /// soft-wrapped edges. The returned slice is owned by the caller and allocated /// using alloc, not the allocator associated with the screen (unless they match). @@ -5451,3 +5533,64 @@ test "Screen: resize more rows then shrink again" { try testing.expectEqualStrings(str, contents); } } + +test "Screen: jump zero" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 10); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); + try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); + try testing.expect(s.viewportIsBottom()); + + // Set semantic prompts + { + const row = s.getRow(.{ .screen = 1 }); + row.setSemanticPrompt(.prompt); + } + { + const row = s.getRow(.{ .screen = 5 }); + row.setSemanticPrompt(.prompt); + } + + try testing.expect(!s.jump(.{ .prompt_delta = 0 })); + try testing.expectEqual(@as(usize, 3), s.viewport); +} + +test "Screen: jump to prompt" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 3, 5, 10); + defer s.deinit(); + try s.testWriteString("1ABCD\n2EFGH\n3IJKL\n"); + try s.testWriteString("4ABCD\n5EFGH\n6IJKL"); + try testing.expect(s.viewportIsBottom()); + + // Set semantic prompts + { + const row = s.getRow(.{ .screen = 1 }); + row.setSemanticPrompt(.prompt); + } + { + const row = s.getRow(.{ .screen = 5 }); + row.setSemanticPrompt(.prompt); + } + + // Jump back + try testing.expect(s.jump(.{ .prompt_delta = -1 })); + try testing.expectEqual(@as(usize, 1), s.viewport); + + // Jump back + try testing.expect(!s.jump(.{ .prompt_delta = -1 })); + try testing.expectEqual(@as(usize, 1), s.viewport); + + // Jump forward + try testing.expect(s.jump(.{ .prompt_delta = 1 })); + try testing.expectEqual(@as(usize, 3), s.viewport); + + // Jump forward + try testing.expect(!s.jump(.{ .prompt_delta = 1 })); + try testing.expectEqual(@as(usize, 3), s.viewport); +} From 9f86c48fd88eef9634942f95cf2eb63fbc3f4a48 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 6 Jul 2023 10:30:29 -0700 Subject: [PATCH 2/3] keybinding jump_to_prompt for semantic prompts --- src/Surface.zig | 7 +++++++ src/config.zig | 12 ++++++++++++ src/input/Binding.zig | 5 +++++ src/terminal/Screen.zig | 3 +++ src/terminal/Terminal.zig | 2 +- src/termio/Exec.zig | 15 +++++++++++++++ src/termio/Thread.zig | 1 + src/termio/message.zig | 3 +++ 8 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3393d8d31..3f0e0b25b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1106,6 +1106,13 @@ pub fn keyCallback( try self.io_thread.wakeup.notify(); }, + .jump_to_prompt => |delta| { + _ = self.io_thread.mailbox.push(.{ + .jump_to_prompt = @intCast(delta), + }, .{ .forever = {} }); + try self.io_thread.wakeup.notify(); + }, + .toggle_dev_mode => if (DevMode.enabled) { DevMode.instance.visible = !DevMode.instance.visible; try self.queueRender(); diff --git a/src/config.zig b/src/config.zig index 456ba97b0..a54c5e062 100644 --- a/src/config.zig +++ b/src/config.zig @@ -491,6 +491,18 @@ pub const Config = struct { .{ .clear_screen = {} }, ); + // Semantic prompts + try result.keybind.set.put( + alloc, + .{ .key = .up, .mods = .{ .super = true, .shift = true } }, + .{ .jump_to_prompt = -1 }, + ); + try result.keybind.set.put( + alloc, + .{ .key = .down, .mods = .{ .super = true, .shift = true } }, + .{ .jump_to_prompt = 1 }, + ); + // Mac windowing try result.keybind.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2cb9c346e..010992960 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -169,6 +169,11 @@ pub const Action = union(enum) { /// Clear the screen. This also clears all scrollback. clear_screen: void, + /// Jump the viewport forward or back by prompt. Positive + /// number is the number of prompts to jump forward, negative + /// is backwards. + jump_to_prompt: i16, + /// Dev mode toggle_dev_mode: void, diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 6255fd333..a45650395 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1682,6 +1682,8 @@ fn jumpPrompt(self: *Screen, delta: isize) bool { } } + //log.warn("delta={} delta_rem={} start_y={} y={}", .{ delta, delta_rem, start_y, y }); + // If we didn't find any, do nothing. if (delta_rem == delta_start) return false; @@ -1690,6 +1692,7 @@ fn jumpPrompt(self: *Screen, delta: isize) bool { const new_y: usize = @intCast(start_y + y_delta); const old_viewport = self.viewport; self.scroll(.{ .row = .{ .screen = new_y } }) catch unreachable; + //log.warn("delta={} y_delta={} start_y={} new_y={}", .{ delta, y_delta, start_y, new_y }); return self.viewport != old_viewport; } diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 35c2f3834..b1fba7f90 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -1504,7 +1504,7 @@ pub fn setScrollingRegion(self: *Terminal, top: usize, bottom: usize) void { /// (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: {}", .{p}); + //log.warn("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, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 9c735fb2d..d6a9dafa5 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -288,6 +288,21 @@ pub fn clearScreen(self: *Exec, history: bool) !void { try self.queueWrite(&[_]u8{0x0C}); } +/// Jump the viewport to the prompt. +pub fn jumpToPrompt(self: *Exec, delta: isize) !void { + const wakeup: bool = wakeup: { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + break :wakeup self.terminal.screen.jump(.{ + .prompt_delta = delta, + }); + }; + + if (wakeup) { + try self.renderer_wakeup.notify(); + } +} + pub inline fn queueWrite(self: *Exec, data: []const u8) !void { const ev = self.data.?; diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d034ede3d..d64200792 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -156,6 +156,7 @@ fn drainMailbox(self: *Thread) !void { }, .resize => |v| self.handleResize(v), .clear_screen => |v| try self.impl.clearScreen(v.history), + .jump_to_prompt => |v| try self.impl.jumpToPrompt(v), .write_small => |v| try self.impl.queueWrite(v.data[0..v.len]), .write_stable => |v| try self.impl.queueWrite(v), .write_alloc => |v| { diff --git a/src/termio/message.zig b/src/termio/message.zig index e19af749e..20579c4e0 100644 --- a/src/termio/message.zig +++ b/src/termio/message.zig @@ -45,6 +45,9 @@ pub const Message = union(enum) { history: bool, }, + /// Jump forward/backward n prompts. + jump_to_prompt: isize, + /// Write where the data fits in the union. write_small: WriteReq.Small, From b3b19997ea2c1e5c3d1d429d3170f806abfca47c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 6 Jul 2023 10:31:47 -0700 Subject: [PATCH 3/3] terminal: scroll to row always tries to get it to the top --- src/terminal/Screen.zig | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index a45650395..3c6c80ffe 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -1511,9 +1511,6 @@ fn scrollRow(self: *Screen, idx: RowIndex) void { const screen_idx = idx.toScreen(self); const screen_pt: point.ScreenPoint = .{ .y = screen_idx.screen }; - // If the point is already in our viewport, we do nothing. - if (screen_pt.inViewport(self)) return; - // Move the viewport so that the screen point is in view. We do the // @min here so that we don't scroll down below where our "bottom" // viewport is.