Separate first nonblank line motion

This commit is contained in:
slhx agent
2026-06-21 22:34:51 +02:00
parent d5c31a92c5
commit d7bf5edc1f
6 changed files with 65 additions and 5 deletions
+17
View File
@@ -96,6 +96,7 @@ fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session,
if (std.mem.eql(u8, command, "move_word_forward")) return dispatchAndRespond(allocator, session, .move_word_forward);
if (std.mem.eql(u8, command, "move_word_back")) return dispatchAndRespond(allocator, session, .move_word_back);
if (std.mem.eql(u8, command, "move_word_end")) return dispatchAndRespond(allocator, session, .move_word_end);
if (std.mem.eql(u8, command, "move_line_first_nonblank")) return dispatchAndRespond(allocator, session, .move_line_first_nonblank);
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);
@@ -489,3 +490,19 @@ test "adversarial: protocol list errors are explicit" {
try std.testing.expectEqualStrings("err no panel open\n", response);
}
}
test "regular: protocol exposes first-nonblank line motion" {
var session = session_mod.Session.init(std.testing.allocator);
defer session.deinit();
var response = try handleLine(std.testing.allocator, &session, "open top\n alpha\n");
std.testing.allocator.free(response);
response = try handleLine(std.testing.allocator, &session, "command move_down\n");
std.testing.allocator.free(response);
response = try handleLine(std.testing.allocator, &session, "command move_line_end\n");
std.testing.allocator.free(response);
response = try handleLine(std.testing.allocator, &session, "command move_line_first_nonblank\n");
defer std.testing.allocator.free(response);
try std.testing.expectEqualStrings("ok state cursor_byte=6 cursor_cell=2 bytes_len=11 panel_depth=0 active_panel=- panel_path=- panel_summary=-\n", response);
}
+40
View File
@@ -31,6 +31,7 @@ pub const Command = union(enum) {
move_word_forward,
move_word_back,
move_word_end,
move_line_first_nonblank,
insert: []const u8,
insert_pair: Pair,
delete_backward,
@@ -95,6 +96,7 @@ pub const Buffer = struct {
.move_word_forward => self.moveWordForward(),
.move_word_back => self.moveWordBack(),
.move_word_end => self.moveWordEnd(),
.move_line_first_nonblank => self.moveLineFirstNonblank(),
.insert => |text| try self.insert(text),
.insert_pair => |pair| try self.insertPair(pair),
.delete_backward => self.deleteBackward(),
@@ -267,6 +269,14 @@ pub const Buffer = struct {
self.refreshCellResetPreferred();
}
pub fn moveLineFirstNonblank(self: *Buffer) void {
const range = self.currentLineRange(false);
var at = range.start;
while (at < range.end and (self.bytes.items[at] == ' ' or self.bytes.items[at] == '\t')) : (at += 1) {}
self.cursor.byte = at;
self.refreshCellResetPreferred();
}
pub fn moveDocumentStart(self: *Buffer) void {
self.cursor.byte = 0;
self.refreshCellResetPreferred();
@@ -795,3 +805,33 @@ test "regular: document motions jump to top and bottom line" {
try std.testing.expectEqual(@as(usize, 0), snap.cursor_byte);
try std.testing.expectEqual(@as(usize, 0), snap.cursor_cell);
}
test "regular: line first-nonblank is distinct from absolute line start" {
var buffer = try Buffer.openFromBytes(std.testing.allocator, "top\n \talpha\nend");
defer buffer.deinit();
buffer.moveDown();
buffer.moveLineEnd();
try std.testing.expectEqual(@as(usize, 12), buffer.cursor.byte);
buffer.moveLineStart();
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
buffer.moveLineEnd();
buffer.moveLineFirstNonblank();
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
try std.testing.expectEqual(@as(usize, 2), buffer.cursor.cell);
}
test "adversarial: line first-nonblank on blank or whitespace-only line goes to line end" {
var buffer = try Buffer.openFromBytes(std.testing.allocator, "a\n \n\t\tb");
defer buffer.deinit();
buffer.moveDown();
buffer.moveLineFirstNonblank();
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
buffer.moveDown();
buffer.moveLineFirstNonblank();
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
}
+4 -2
View File
@@ -1115,7 +1115,8 @@ pub const Client = struct {
if (std.mem.eql(u8, text, ".")) return self.repeatLastEdit();
if (std.mem.eql(u8, text, "u")) return self.undoEdit();
if (std.mem.eql(u8, text, "U")) return self.redoEdit();
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0") or std.mem.eql(u8, text, "^")) return self.applyProtocol("command move_line_start");
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0")) return self.applyProtocol("command move_line_start");
if (std.mem.eql(u8, text, "^")) return self.applyProtocol("command move_line_first_nonblank");
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);
@@ -1186,7 +1187,8 @@ pub const Client = struct {
return;
}
if (std.mem.eql(u8, text, "%")) return self.extendSelectionWithJump(null);
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0") or std.mem.eql(u8, text, "^")) return self.extendSelectionWithProtocol("command move_line_start");
if (std.mem.eql(u8, text, "H") or std.mem.eql(u8, text, "0")) return self.extendSelectionWithProtocol("command move_line_start");
if (std.mem.eql(u8, text, "^")) return self.extendSelectionWithProtocol("command move_line_first_nonblank");
if (std.mem.eql(u8, text, "L") or std.mem.eql(u8, text, "$")) return self.extendSelectionWithProtocol("command move_line_end");
if (std.mem.eql(u8, text, "j")) return self.repeatSelectionProtocol("command move_down", self.takeRepeat());
if (std.mem.eql(u8, text, "k")) return self.repeatSelectionProtocol("command move_up", self.takeRepeat());