Add core editing commands
This commit is contained in:
+211
@@ -26,9 +26,22 @@ pub const Pair = struct {
|
||||
pub const Command = union(enum) {
|
||||
move_left,
|
||||
move_right,
|
||||
move_up,
|
||||
move_down,
|
||||
move_word_forward,
|
||||
move_word_back,
|
||||
move_word_end,
|
||||
insert: []const u8,
|
||||
insert_pair: Pair,
|
||||
delete_backward,
|
||||
delete_forward,
|
||||
delete_line,
|
||||
change_line,
|
||||
open_line_below,
|
||||
open_line_above,
|
||||
replace_char: []const u8,
|
||||
move_line_start,
|
||||
move_line_end,
|
||||
};
|
||||
|
||||
pub const Snapshot = struct {
|
||||
@@ -74,9 +87,22 @@ pub const Buffer = struct {
|
||||
switch (command) {
|
||||
.move_left => self.moveLeft(),
|
||||
.move_right => self.moveRight(),
|
||||
.move_up => self.moveUp(),
|
||||
.move_down => self.moveDown(),
|
||||
.move_word_forward => self.moveWordForward(),
|
||||
.move_word_back => self.moveWordBack(),
|
||||
.move_word_end => self.moveWordEnd(),
|
||||
.insert => |text| try self.insert(text),
|
||||
.insert_pair => |pair| try self.insertPair(pair),
|
||||
.delete_backward => self.deleteBackward(),
|
||||
.delete_forward => self.deleteForward(),
|
||||
.delete_line => self.deleteLine(),
|
||||
.change_line => try self.changeLine(),
|
||||
.open_line_below => try self.openLineBelow(),
|
||||
.open_line_above => try self.openLineAbove(),
|
||||
.replace_char => |text| try self.replaceChar(text),
|
||||
.move_line_start => self.moveLineStart(),
|
||||
.move_line_end => self.moveLineEnd(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +116,55 @@ pub const Buffer = struct {
|
||||
self.refreshCell();
|
||||
}
|
||||
|
||||
pub fn moveUp(self: *Buffer) void {
|
||||
const current = self.currentLineRange(false);
|
||||
if (current.start == 0) 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();
|
||||
}
|
||||
|
||||
pub fn moveDown(self: *Buffer) void {
|
||||
const current = self.currentLineRange(false);
|
||||
if (current.end >= self.bytes.items.len) return;
|
||||
const next_start = current.end + 1;
|
||||
if (next_start > self.bytes.items.len) 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();
|
||||
}
|
||||
|
||||
pub fn moveWordForward(self: *Buffer) void {
|
||||
var at = self.cursor.byte;
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn moveWordBack(self: *Buffer) void {
|
||||
var at = self.cursor.byte;
|
||||
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();
|
||||
}
|
||||
|
||||
pub fn moveWordEnd(self: *Buffer) void {
|
||||
var at = self.cursor.byte;
|
||||
while (at < self.bytes.items.len and isWordSeparator(self.bytes.items[at])) at = nextBoundary(self.bytes.items, at);
|
||||
while (at < self.bytes.items.len) {
|
||||
const next = nextBoundary(self.bytes.items, at);
|
||||
if (next >= self.bytes.items.len or isWordSeparator(self.bytes.items[next])) break;
|
||||
at = next;
|
||||
}
|
||||
self.cursor.byte = at;
|
||||
self.refreshCell();
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -117,6 +192,76 @@ pub const Buffer = struct {
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
pub fn deleteForward(self: *Buffer) void {
|
||||
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.selection = null;
|
||||
}
|
||||
|
||||
pub fn replaceChar(self: *Buffer, text: []const u8) !void {
|
||||
if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8Insertion;
|
||||
if (self.cursor.byte < self.bytes.items.len) self.deleteForward();
|
||||
try self.insert(text);
|
||||
self.moveLeft();
|
||||
}
|
||||
|
||||
pub fn deleteLine(self: *Buffer) void {
|
||||
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.selection = null;
|
||||
}
|
||||
|
||||
pub fn changeLine(self: *Buffer) !void {
|
||||
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.selection = null;
|
||||
}
|
||||
|
||||
pub fn openLineBelow(self: *Buffer) !void {
|
||||
const range = self.currentLineRange(false);
|
||||
const has_line_break = range.end < self.bytes.items.len and self.bytes.items[range.end] == '\n';
|
||||
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.selection = null;
|
||||
}
|
||||
|
||||
pub fn openLineAbove(self: *Buffer) !void {
|
||||
const range = self.currentLineRange(false);
|
||||
try self.bytes.insertSlice(self.allocator, range.start, "\n");
|
||||
self.cursor.byte = range.start;
|
||||
self.refreshCell();
|
||||
self.selection = null;
|
||||
}
|
||||
|
||||
pub fn moveLineStart(self: *Buffer) void {
|
||||
self.cursor.byte = self.currentLineRange(false).start;
|
||||
self.refreshCell();
|
||||
}
|
||||
|
||||
pub fn moveLineEnd(self: *Buffer) void {
|
||||
self.cursor.byte = self.currentLineRange(false).end;
|
||||
self.refreshCell();
|
||||
}
|
||||
|
||||
const LineRange = struct { start: usize, end: usize };
|
||||
|
||||
fn currentLineRange(self: *const Buffer, include_newline: bool) LineRange {
|
||||
var start = self.cursor.byte;
|
||||
while (start > 0 and self.bytes.items[start - 1] != '\n') start -= 1;
|
||||
var end = self.cursor.byte;
|
||||
while (end < self.bytes.items.len and self.bytes.items[end] != '\n') end += 1;
|
||||
if (include_newline and end < self.bytes.items.len and self.bytes.items[end] == '\n') end += 1;
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn refreshCell(self: *Buffer) void {
|
||||
self.cursor.cell = cellWidth(self.bytes.items[0..self.cursor.byte]);
|
||||
}
|
||||
@@ -227,6 +372,23 @@ pub const Session = struct {
|
||||
}
|
||||
};
|
||||
|
||||
fn isWordSeparator(byte: u8) bool {
|
||||
return std.ascii.isWhitespace(byte) or std.mem.indexOfScalar(u8, "(){}[]<>.,;:+-*/=\"'`", byte) != null;
|
||||
}
|
||||
|
||||
fn byteForCell(bytes: []const u8, start: usize, end: usize, target_cell: usize) usize {
|
||||
var at = start;
|
||||
var best = start;
|
||||
while (at < end) {
|
||||
const rel = cellWidth(bytes[start..at]);
|
||||
if (rel > target_cell) break;
|
||||
best = at;
|
||||
at = nextBoundary(bytes, at);
|
||||
}
|
||||
if (cellWidth(bytes[start..@min(at, end)]) <= target_cell) return @min(at, end);
|
||||
return best;
|
||||
}
|
||||
|
||||
pub fn boundaryAtOrBefore(bytes: []const u8, cursor: usize) usize {
|
||||
var i = @min(cursor, bytes.len);
|
||||
if (i == bytes.len) return i;
|
||||
@@ -441,3 +603,52 @@ test "adversarial: session panel operations fail clearly on empty or invalid sta
|
||||
const snap = try session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 0), snap.panel_depth);
|
||||
}
|
||||
|
||||
test "regular: buffer moves by line word and line edges" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "one\nab cd\nxy");
|
||||
defer buffer.deinit();
|
||||
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
|
||||
buffer.moveRight();
|
||||
buffer.moveRight();
|
||||
buffer.moveDown();
|
||||
try std.testing.expectEqual(@as(usize, 12), buffer.cursor.byte);
|
||||
buffer.moveUp();
|
||||
try std.testing.expectEqual(@as(usize, 9), buffer.cursor.byte);
|
||||
buffer.moveLineStart();
|
||||
try std.testing.expectEqual(@as(usize, 4), buffer.cursor.byte);
|
||||
buffer.moveWordForward();
|
||||
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
|
||||
buffer.moveWordEnd();
|
||||
try std.testing.expectEqual(@as(usize, 8), buffer.cursor.byte);
|
||||
buffer.moveWordBack();
|
||||
try std.testing.expectEqual(@as(usize, 7), buffer.cursor.byte);
|
||||
buffer.moveLineEnd();
|
||||
try std.testing.expectEqual(@as(usize, 9), buffer.cursor.byte);
|
||||
}
|
||||
|
||||
test "regular: buffer line operations replace delete and open around utf8" {
|
||||
var buffer = try Buffer.openFromBytes(std.testing.allocator, "éx\nsecond");
|
||||
defer buffer.deinit();
|
||||
|
||||
try buffer.replaceChar("A");
|
||||
var snap = buffer.snapshot();
|
||||
try std.testing.expectEqualStrings("Ax\nsecond", snap.bytes);
|
||||
try std.testing.expectEqual(@as(usize, 0), snap.cursor_byte);
|
||||
|
||||
try buffer.openLineBelow();
|
||||
try buffer.insert("below");
|
||||
snap = buffer.snapshot();
|
||||
try std.testing.expectEqualStrings("Ax\nbelow\nsecond", snap.bytes);
|
||||
|
||||
buffer.moveLineStart();
|
||||
try buffer.changeLine();
|
||||
try buffer.insert("changed");
|
||||
snap = buffer.snapshot();
|
||||
try std.testing.expectEqualStrings("Ax\nchanged\nsecond", snap.bytes);
|
||||
|
||||
buffer.deleteLine();
|
||||
snap = buffer.snapshot();
|
||||
try std.testing.expectEqualStrings("Ax\nsecond", snap.bytes);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user