Preserve preferred column on vertical motion
This commit is contained in:
+92
-29
@@ -59,6 +59,7 @@ pub const Buffer = struct {
|
||||
bytes: std.ArrayList(u8),
|
||||
cursor: Cursor = .{},
|
||||
selection: ?Selection = null,
|
||||
preferred_vertical_cell: ?usize = null,
|
||||
|
||||
pub fn openFromBytes(allocator: std.mem.Allocator, fixture: []const u8) !Buffer {
|
||||
var bytes = std.ArrayList(u8).empty;
|
||||
@@ -108,33 +109,44 @@ pub const Buffer = struct {
|
||||
|
||||
pub fn moveLeft(self: *Buffer) void {
|
||||
self.cursor.byte = previousBoundary(self.bytes.items, self.cursor.byte);
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveRight(self: *Buffer) void {
|
||||
self.cursor.byte = nextBoundary(self.bytes.items, self.cursor.byte);
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveUp(self: *Buffer) void {
|
||||
const current = self.currentLineRange(false);
|
||||
if (current.start == 0) return;
|
||||
const target_cell = self.preferred_vertical_cell orelse self.cursor.cell;
|
||||
if (current.start == 0) {
|
||||
self.preferred_vertical_cell = target_cell;
|
||||
return;
|
||||
}
|
||||
const previous_end = current.start - 1;
|
||||
var previous_start = previous_end;
|
||||
while (previous_start > 0 and self.bytes.items[previous_start - 1] != '\n') previous_start -= 1;
|
||||
self.cursor.byte = byteForCell(self.bytes.items, previous_start, previous_end, self.cursor.cell);
|
||||
self.refreshCell();
|
||||
self.cursor.byte = byteForCell(self.bytes.items, previous_start, previous_end, target_cell);
|
||||
self.refreshCellPreservePreferred(target_cell);
|
||||
}
|
||||
|
||||
pub fn moveDown(self: *Buffer) void {
|
||||
const current = self.currentLineRange(false);
|
||||
if (current.end >= self.bytes.items.len) return;
|
||||
const target_cell = self.preferred_vertical_cell orelse self.cursor.cell;
|
||||
if (current.end >= self.bytes.items.len) {
|
||||
self.preferred_vertical_cell = target_cell;
|
||||
return;
|
||||
}
|
||||
const next_start = current.end + 1;
|
||||
if (next_start > self.bytes.items.len) return;
|
||||
if (next_start > self.bytes.items.len) {
|
||||
self.preferred_vertical_cell = target_cell;
|
||||
return;
|
||||
}
|
||||
var next_end = next_start;
|
||||
while (next_end < self.bytes.items.len and self.bytes.items[next_end] != '\n') next_end += 1;
|
||||
self.cursor.byte = byteForCell(self.bytes.items, next_start, next_end, self.cursor.cell);
|
||||
self.refreshCell();
|
||||
self.cursor.byte = byteForCell(self.bytes.items, next_start, next_end, target_cell);
|
||||
self.refreshCellPreservePreferred(target_cell);
|
||||
}
|
||||
|
||||
pub fn moveWordForward(self: *Buffer) void {
|
||||
@@ -142,7 +154,7 @@ pub const Buffer = struct {
|
||||
while (at < self.bytes.items.len and !isWordSeparator(self.bytes.items[at])) at = nextBoundary(self.bytes.items, at);
|
||||
while (at < self.bytes.items.len and isWordSeparator(self.bytes.items[at])) at = nextBoundary(self.bytes.items, at);
|
||||
self.cursor.byte = at;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveWordBack(self: *Buffer) void {
|
||||
@@ -150,7 +162,7 @@ pub const Buffer = struct {
|
||||
while (at > 0 and isWordSeparator(self.bytes.items[previousBoundary(self.bytes.items, at)])) at = previousBoundary(self.bytes.items, at);
|
||||
while (at > 0 and !isWordSeparator(self.bytes.items[previousBoundary(self.bytes.items, at)])) at = previousBoundary(self.bytes.items, at);
|
||||
self.cursor.byte = at;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveWordEnd(self: *Buffer) void {
|
||||
@@ -162,14 +174,14 @@ pub const Buffer = struct {
|
||||
at = next;
|
||||
}
|
||||
self.cursor.byte = at;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn insert(self: *Buffer, text: []const u8) !void {
|
||||
if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8Insertion;
|
||||
try self.bytes.insertSlice(self.allocator, self.cursor.byte, text);
|
||||
self.cursor.byte += text.len;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -179,7 +191,7 @@ pub const Buffer = struct {
|
||||
defer self.allocator.free(combined);
|
||||
try self.bytes.insertSlice(self.allocator, self.cursor.byte, combined);
|
||||
self.cursor.byte += pair.open.len;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -188,7 +200,7 @@ pub const Buffer = struct {
|
||||
const start = previousBoundary(self.bytes.items, self.cursor.byte);
|
||||
self.bytes.replaceRangeAssumeCapacity(start, self.cursor.byte - start, "");
|
||||
self.cursor.byte = start;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -196,7 +208,7 @@ pub const Buffer = struct {
|
||||
if (self.cursor.byte >= self.bytes.items.len) return;
|
||||
const end = nextBoundary(self.bytes.items, self.cursor.byte);
|
||||
self.bytes.replaceRangeAssumeCapacity(self.cursor.byte, end - self.cursor.byte, "");
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -211,7 +223,7 @@ pub const Buffer = struct {
|
||||
const range = self.currentLineRange(true);
|
||||
self.bytes.replaceRangeAssumeCapacity(range.start, range.end - range.start, "");
|
||||
self.cursor.byte = @min(range.start, self.bytes.items.len);
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -219,7 +231,7 @@ pub const Buffer = struct {
|
||||
const range = self.currentLineRange(false);
|
||||
self.bytes.replaceRangeAssumeCapacity(range.start, range.end - range.start, "");
|
||||
self.cursor.byte = @min(range.start, self.bytes.items.len);
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -229,7 +241,7 @@ pub const Buffer = struct {
|
||||
const at = if (has_line_break) range.end + 1 else self.bytes.items.len;
|
||||
try self.bytes.insertSlice(self.allocator, at, "\n");
|
||||
self.cursor.byte = if (has_line_break) at else at + 1;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
@@ -237,18 +249,18 @@ pub const Buffer = struct {
|
||||
const range = self.currentLineRange(false);
|
||||
try self.bytes.insertSlice(self.allocator, range.start, "\n");
|
||||
self.cursor.byte = range.start;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
pub fn moveLineStart(self: *Buffer) void {
|
||||
self.cursor.byte = self.currentLineRange(false).start;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
pub fn moveLineEnd(self: *Buffer) void {
|
||||
self.cursor.byte = self.currentLineRange(false).end;
|
||||
self.refreshCell();
|
||||
self.refreshCellResetPreferred();
|
||||
}
|
||||
|
||||
const LineRange = struct { start: usize, end: usize };
|
||||
@@ -262,8 +274,19 @@ pub const Buffer = struct {
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn refreshCell(self: *Buffer) void {
|
||||
self.cursor.cell = cellWidth(self.bytes.items[0..self.cursor.byte]);
|
||||
fn refreshCellResetPreferred(self: *Buffer) void {
|
||||
self.refreshCellLineLocal();
|
||||
self.preferred_vertical_cell = null;
|
||||
}
|
||||
|
||||
fn refreshCellPreservePreferred(self: *Buffer, target_cell: usize) void {
|
||||
self.refreshCellLineLocal();
|
||||
self.preferred_vertical_cell = target_cell;
|
||||
}
|
||||
|
||||
fn refreshCellLineLocal(self: *Buffer) void {
|
||||
const line = self.currentLineRange(false);
|
||||
self.cursor.cell = cellWidth(self.bytes.items[line.start..self.cursor.byte]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -290,7 +313,7 @@ pub const Session = struct {
|
||||
if (self.buffer) |*buffer| buffer.deinit();
|
||||
var buffer = try Buffer.openFromBytes(self.allocator, fixture);
|
||||
buffer.cursor.byte = boundaryAtOrBefore(buffer.bytes.items, @min(cursor_byte, buffer.bytes.items.len));
|
||||
buffer.refreshCell();
|
||||
buffer.refreshCellResetPreferred();
|
||||
self.buffer = buffer;
|
||||
}
|
||||
|
||||
@@ -379,7 +402,7 @@ pub const Session = struct {
|
||||
if (self.buffer) |*buffer| {
|
||||
if (byte > buffer.bytes.items.len) return error.SelectionOutOfBounds;
|
||||
buffer.cursor.byte = byte;
|
||||
buffer.refreshCell();
|
||||
buffer.refreshCellResetPreferred();
|
||||
return;
|
||||
}
|
||||
return error.NoActiveBuffer;
|
||||
@@ -390,7 +413,7 @@ pub const Session = struct {
|
||||
if (start > end or end > buffer.bytes.items.len) return error.SelectionOutOfBounds;
|
||||
buffer.selection = .{ .anchor = start, .cursor = end };
|
||||
buffer.cursor.byte = end;
|
||||
buffer.refreshCell();
|
||||
buffer.refreshCellResetPreferred();
|
||||
return;
|
||||
}
|
||||
return error.NoActiveBuffer;
|
||||
@@ -406,7 +429,7 @@ pub const Session = struct {
|
||||
if (start > end or end > buffer.bytes.items.len) return error.SelectionOutOfBounds;
|
||||
try buffer.bytes.replaceRange(buffer.allocator, start, end - start, bytes);
|
||||
buffer.cursor.byte = start + bytes.len;
|
||||
buffer.refreshCell();
|
||||
buffer.refreshCellResetPreferred();
|
||||
buffer.selection = null;
|
||||
return;
|
||||
}
|
||||
@@ -657,7 +680,7 @@ test "regular: buffer moves by line word and line edges" {
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 12), buffer.cursor.byte);
|
||||
buffer.moveUp();
|
||||
try std.testing.expectEqual(@as(usize, 9), buffer.cursor.byte);
|
||||
try std.testing.expectEqual(@as(usize, 6), buffer.cursor.byte);
|
||||
buffer.moveLineStart();
|
||||
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
|
||||
buffer.moveWordForward();
|
||||
@@ -694,3 +717,43 @@ test "regular: buffer line operations replace delete and open around utf8" {
|
||||
snap = buffer.snapshot();
|
||||
try std.testing.expectEqualStrings("Ax\nsecond", snap.bytes);
|
||||
}
|
||||
|
||||
test "regular: vertical motion preserves preferred column across short lines" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "abcdef\nxy\n123456\n");
|
||||
defer buffer.deinit();
|
||||
|
||||
buffer.moveLineEnd();
|
||||
try std.testing.expectEqual(@as(usize, 6), buffer.cursor.cell);
|
||||
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 9), buffer.cursor.byte);
|
||||
try std.testing.expectEqual(@as(usize, 2), buffer.cursor.cell);
|
||||
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 16), buffer.cursor.byte);
|
||||
try std.testing.expectEqual(@as(usize, 6), buffer.cursor.cell);
|
||||
}
|
||||
|
||||
test "regular: horizontal motion resets vertical preferred column" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "abcdef\nxy\n123456\n");
|
||||
defer buffer.deinit();
|
||||
|
||||
buffer.moveLineEnd();
|
||||
buffer.moveDown();
|
||||
buffer.moveLeft();
|
||||
try std.testing.expectEqual(@as(usize, 1), buffer.cursor.cell);
|
||||
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 11), buffer.cursor.byte);
|
||||
try std.testing.expectEqual(@as(usize, 1), buffer.cursor.cell);
|
||||
}
|
||||
|
||||
test "regular: line object selection includes the newline byte" {
|
||||
var session = Session.init(std.testing.allocator);
|
||||
defer session.deinit();
|
||||
|
||||
try session.openFixture("abc\ndef\n");
|
||||
try session.selectRange(0, 4);
|
||||
const snap = try session.snapshot();
|
||||
try std.testing.expectEqualStrings("abc\n", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user