diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index f1ffd521e..fbf4b11c1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -1408,6 +1408,11 @@ pub const Scroll = union(enum) { /// Scroll up (negative) or down (positive) by the given number of /// rows. This is clamped to the "top" and "active" top left. delta_row: isize, + + /// 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 in that direction. + delta_prompt: isize, }; /// Scroll the viewport. This will never create new scrollback, allocate @@ -1417,6 +1422,7 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { switch (behavior) { .active => self.viewport = .{ .active = {} }, .top => self.viewport = .{ .top = {} }, + .delta_prompt => |n| self.scrollPrompt(n), .delta_row => |n| { if (n == 0) return; @@ -1448,6 +1454,45 @@ pub fn scroll(self: *PageList, behavior: Scroll) void { } } +/// Jump the viewport forwards (positive) or backwards (negative) a set number of +/// prompts (delta). +fn scrollPrompt(self: *PageList, delta: isize) void { + // If we aren't jumping any prompts then we don't need to do anything. + if (delta == 0) return; + const delta_start: usize = @intCast(if (delta > 0) delta else -delta); + var delta_rem: usize = delta_start; + + // Iterate and count the number of prompts we see. + const viewport_pin = self.getTopLeft(.viewport); + var it = viewport_pin.rowIterator(if (delta > 0) .right_down else .left_up, null); + _ = it.next(); // skip our own row + var prompt_pin: ?Pin = null; + while (it.next()) |next| { + const row = next.rowAndCell().row; + switch (row.semantic_prompt) { + .command, .unknown => {}, + .prompt, .prompt_continuation, .input => { + delta_rem -= 1; + prompt_pin = next; + }, + } + + if (delta_rem == 0) break; + } + + // If we found a prompt, we move to it. If the prompt is in the active + // area we keep our viewport as active because we can't scroll DOWN + // into the active area. Otherwise, we scroll up to the pin. + if (prompt_pin) |p| { + if (self.pinIsActive(p)) { + self.viewport = .{ .active = {} }; + } else { + self.viewport_pin.* = p; + self.viewport = .{ .pin = {} }; + } + } +} + /// Clear the screen by scrolling written contents up into the scrollback. /// This will not update the viewport. pub fn scrollClear(self: *PageList) !void { @@ -3062,6 +3107,75 @@ test "PageList scroll clear" { } } +test "PageList: jump zero" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 3, null); + defer s.deinit(); + try s.growRows(3); + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + { + const rac = page.getRowAndCell(0, 1); + rac.row.semantic_prompt = .prompt; + } + { + const rac = page.getRowAndCell(0, 5); + rac.row.semantic_prompt = .prompt; + } + + s.scroll(.{ .delta_prompt = 0 }); + try testing.expect(s.viewport == .active); +} + +test "Screen: jump to prompt" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 5, 3, null); + defer s.deinit(); + try s.growRows(3); + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + { + const rac = page.getRowAndCell(0, 1); + rac.row.semantic_prompt = .prompt; + } + { + const rac = page.getRowAndCell(0, 5); + rac.row.semantic_prompt = .prompt; + } + + // Jump back + { + s.scroll(.{ .delta_prompt = -1 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 1, + } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + } + { + s.scroll(.{ .delta_prompt = -1 }); + try testing.expect(s.viewport == .pin); + try testing.expectEqual(point.Point{ .screen = .{ + .x = 0, + .y = 1, + } }, s.pointFromPin(.screen, s.pin(.{ .viewport = .{} }).?).?); + } + + // Jump forward + { + s.scroll(.{ .delta_prompt = 1 }); + try testing.expect(s.viewport == .active); + } + { + s.scroll(.{ .delta_prompt = 1 }); + try testing.expect(s.viewport == .active); + } +} + test "PageList grow fit in capacity" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 3883a4270..414051fcf 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -532,6 +532,7 @@ pub const Scroll = union(enum) { active, top, delta_row: isize, + delta_prompt: isize, }; /// Scroll the viewport of the terminal grid. @@ -545,6 +546,7 @@ pub fn scroll(self: *Screen, behavior: Scroll) void { .active => self.pages.scroll(.{ .active = {} }), .top => self.pages.scroll(.{ .top = {} }), .delta_row => |v| self.pages.scroll(.{ .delta_row = v }), + .delta_prompt => |v| self.pages.scroll(.{ .delta_prompt = v }), } } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 0acfe59a0..1dab11741 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -508,20 +508,13 @@ pub fn scrollViewport(self: *Exec, scroll: terminal.Terminal.ScrollViewport) !vo /// Jump the viewport to the prompt. pub fn jumpToPrompt(self: *Exec, delta: isize) !void { - _ = self; - _ = delta; - // TODO(paged-terminal) - // 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(); - // } + { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + self.terminal.screen.scroll(.{ .delta_prompt = delta }); + } + + try self.renderer_wakeup.notify(); } /// Called when the child process exited abnormally but before