Add document and half-page motions

This commit is contained in:
slhx agent
2026-06-21 21:03:10 +02:00
parent 2277ed281d
commit c98c530c44
8 changed files with 99 additions and 2 deletions
+4
View File
@@ -22,6 +22,8 @@ pub const Key = enum {
end,
page_up,
page_down,
ctrl_u,
ctrl_d,
};
pub const Event = union(enum) {
@@ -90,6 +92,8 @@ pub fn normalize(raw: []const u8) Event {
if (std.mem.eql(u8, raw, "\x1b[F") or std.mem.eql(u8, raw, "\x1b[4~") or std.mem.eql(u8, raw, "\x1b[8~")) return .{ .key = .end };
if (std.mem.eql(u8, raw, "\x1b[5~")) return .{ .key = .page_up };
if (std.mem.eql(u8, raw, "\x1b[6~")) return .{ .key = .page_down };
if (std.mem.eql(u8, raw, "\x15")) return .{ .key = .ctrl_u };
if (std.mem.eql(u8, raw, "\x04")) return .{ .key = .ctrl_d };
if (std.unicode.utf8ValidateSlice(raw) and isPrintableText(raw)) return .{ .text = raw };
return .{ .unknown = raw };
+1 -1
View File
@@ -162,7 +162,7 @@ pub fn classifyEvent(profile: *const Profile, event: input.Event) Classified {
const kind: Kind = switch (event) {
.key => |key| switch (key) {
.space, .tab => .whitespace,
.enter, .backspace, .escape, .arrow_left, .arrow_right, .arrow_up, .arrow_down, .home, .end, .page_up, .page_down => .control,
.enter, .backspace, .escape, .arrow_left, .arrow_right, .arrow_up, .arrow_down, .home, .end, .page_up, .page_down, .ctrl_u, .ctrl_d => .control,
},
.text => |text| lookupFact(profile, text) orelse .unknown,
.unknown => .unknown,
+2
View File
@@ -98,6 +98,8 @@ fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session,
if (std.mem.eql(u8, command, "move_word_end")) return dispatchAndRespond(allocator, session, .move_word_end);
if (std.mem.eql(u8, command, "move_line_start")) return dispatchAndRespond(allocator, session, .move_line_start);
if (std.mem.eql(u8, command, "move_line_end")) return dispatchAndRespond(allocator, session, .move_line_end);
if (std.mem.eql(u8, command, "move_document_start")) return dispatchAndRespond(allocator, session, .move_document_start);
if (std.mem.eql(u8, command, "move_document_end")) return dispatchAndRespond(allocator, session, .move_document_end);
if (std.mem.eql(u8, command, "delete_backward")) return dispatchAndRespond(allocator, session, .delete_backward);
if (std.mem.eql(u8, command, "delete_forward")) return dispatchAndRespond(allocator, session, .delete_forward);
if (std.mem.eql(u8, command, "delete_line")) return dispatchAndRespond(allocator, session, .delete_line);
+38
View File
@@ -42,6 +42,8 @@ pub const Command = union(enum) {
replace_char: []const u8,
move_line_start,
move_line_end,
move_document_start,
move_document_end,
};
pub const Snapshot = struct {
@@ -104,6 +106,8 @@ pub const Buffer = struct {
.replace_char => |text| try self.replaceChar(text),
.move_line_start => self.moveLineStart(),
.move_line_end => self.moveLineEnd(),
.move_document_start => self.moveDocumentStart(),
.move_document_end => self.moveDocumentEnd(),
}
}
@@ -263,6 +267,24 @@ pub const Buffer = struct {
self.refreshCellResetPreferred();
}
pub fn moveDocumentStart(self: *Buffer) void {
self.cursor.byte = 0;
self.refreshCellResetPreferred();
}
pub fn moveDocumentEnd(self: *Buffer) void {
if (self.bytes.items.len == 0) {
self.cursor.byte = 0;
self.refreshCellResetPreferred();
return;
}
var at = self.bytes.items.len;
if (at > 0 and self.bytes.items[at - 1] == '\n') at -= 1;
while (at > 0 and self.bytes.items[at - 1] != '\n') at -= 1;
self.cursor.byte = at;
self.refreshCellResetPreferred();
}
const LineRange = struct { start: usize, end: usize };
fn currentLineRange(self: *const Buffer, include_newline: bool) LineRange {
@@ -757,3 +779,19 @@ test "regular: line object selection includes the newline byte" {
const snap = try session.snapshot();
try std.testing.expectEqualStrings("abc\n", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
}
test "regular: document motions jump to top and bottom line" {
var session = Session.init(std.testing.allocator);
defer session.deinit();
try session.openFixtureAt("one\ntwo\nthree\n", 4);
try session.dispatch(.move_document_end);
var snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 8), snap.cursor_byte);
try std.testing.expectEqual(@as(usize, 0), snap.cursor_cell);
try session.dispatch(.move_document_start);
snap = try session.snapshot();
try std.testing.expectEqual(@as(usize, 0), snap.cursor_byte);
try std.testing.expectEqual(@as(usize, 0), snap.cursor_cell);
}
+42
View File
@@ -324,11 +324,16 @@ pub const Client = struct {
const gutter_digits = @max(@as(usize, 2), decimalDigits(total_lines));
const gutter_width = @min(self.viewport.width, gutter_digits + 2);
const content_width = if (self.viewport.width > gutter_width) self.viewport.width - gutter_width else 0;
const first_visible_line = firstVisibleLineForCursor(cursor_line, max_body_lines);
var visible_line_index: usize = 0;
var body_lines_used: usize = 0;
var line_start_byte: usize = 0;
var line_iter = std.mem.splitScalar(u8, snap.bytes, '\n');
while (line_iter.next()) |line| : (visible_line_index += 1) {
if (visible_line_index < first_visible_line) {
line_start_byte += line.len + 1;
continue;
}
if (body_lines_used >= max_body_lines) break;
try appendEditorLine(
allocator,
@@ -1076,6 +1081,7 @@ pub const Client = struct {
if (std.mem.eql(u8, text, "L") or std.mem.eql(u8, text, "$")) return self.applyProtocol("command move_line_end");
if (std.mem.eql(u8, text, "%")) return self.jumpToMatch(null);
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
if (std.mem.eql(u8, text, "G")) return self.applyProtocol("command move_document_end");
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
if (std.mem.eql(u8, text, "j")) return self.repeatProtocol("command move_down", self.takeRepeat());
if (std.mem.eql(u8, text, "k")) return self.repeatProtocol("command move_up", self.takeRepeat());
@@ -1100,6 +1106,8 @@ pub const Client = struct {
.end => try self.applyProtocol("command move_line_end"),
.page_up => try self.repeatProtocol("command move_up", @max(@as(usize, 1), self.viewport.height - 2)),
.page_down => try self.repeatProtocol("command move_down", @max(@as(usize, 1), self.viewport.height - 2)),
.ctrl_u => try self.halfPage(.up),
.ctrl_d => try self.halfPage(.down),
else => self.unknownPrefixOrInput("normal"),
},
.unknown => self.unknownPrefixOrInput("normal"),
@@ -1810,6 +1818,10 @@ pub const Client = struct {
fn applyGoRail(self: *Client, event: input.Event) !void {
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
if (std.mem.eql(u8, text, "g")) return self.applyProtocol("command move_document_start");
if (std.mem.eql(u8, text, "G")) return self.applyProtocol("command move_document_end");
if (std.mem.eql(u8, text, "u")) return self.halfPage(.up);
if (std.mem.eql(u8, text, "d")) return self.halfPage(.down);
if (std.mem.eql(u8, text, "a")) return self.moveParameter(.next);
if (std.mem.eql(u8, text, "A")) return self.moveParameter(.previous);
if (std.mem.eql(u8, text, "e")) return self.gotoDiagnostic(.next);
@@ -2201,6 +2213,17 @@ pub const Client = struct {
self.noteDocumentChanged();
}
const PageDirection = enum { up, down };
fn halfPage(self: *Client, direction: PageDirection) !void {
const rows = @max(@as(usize, 1), (self.viewport.height - 1) / 2);
const command = switch (direction) {
.up => "command move_up",
.down => "command move_down",
};
try self.repeatProtocol(command, rows);
}
fn repeatProtocol(self: *Client, line: []const u8, repeat: usize) !void {
var i: usize = 0;
while (i < repeat) : (i += 1) try self.applyProtocol(line);
@@ -2571,6 +2594,12 @@ fn countDocumentLines(bytes: []const u8) usize {
return lines;
}
fn firstVisibleLineForCursor(cursor_line: usize, max_body_lines: usize) usize {
if (max_body_lines == 0) return cursor_line;
if (cursor_line < max_body_lines) return 0;
return cursor_line - max_body_lines + 1;
}
fn decimalDigits(value: usize) usize {
var digits: usize = 1;
var n = value;
@@ -5143,3 +5172,16 @@ test "regular: insert Enter copies current line indent and Tab follows tab-inden
snap = try tabs.session.snapshot();
try std.testing.expectEqualStrings("\titem x", snap.bytes);
}
test "regular: vertical viewport follows cursor line" {
var client = try Client.init(std.testing.allocator, .{ .width = 32, .height = 6 });
defer client.deinit();
try client.handleTraceLine("open l1\nl2\nl3\nl4\nl5\nl6\nl7\nl8\nl9\nl10\nl11\nl12");
try client.applyProtocol("command move_document_end");
const frame = try client.render(std.testing.allocator);
defer std.testing.allocator.free(frame);
try std.testing.expect(std.mem.indexOf(u8, frame, "12│") != null);
try std.testing.expect(std.mem.indexOf(u8, frame, " 1│") == null);
}