Add core editing commands
This commit is contained in:
@@ -91,7 +91,20 @@ fn parseToolLog(log_line: []const u8) ?struct { name: []const u8, command: []con
|
||||
fn commandResponse(allocator: std.mem.Allocator, session: *session_mod.Session, command: []const u8) ![]u8 {
|
||||
if (std.mem.eql(u8, command, "move_left")) return dispatchAndRespond(allocator, session, .move_left);
|
||||
if (std.mem.eql(u8, command, "move_right")) return dispatchAndRespond(allocator, session, .move_right);
|
||||
if (std.mem.eql(u8, command, "move_up")) return dispatchAndRespond(allocator, session, .move_up);
|
||||
if (std.mem.eql(u8, command, "move_down")) return dispatchAndRespond(allocator, session, .move_down);
|
||||
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_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, "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);
|
||||
if (std.mem.eql(u8, command, "change_line")) return dispatchAndRespond(allocator, session, .change_line);
|
||||
if (std.mem.eql(u8, command, "open_line_below")) return dispatchAndRespond(allocator, session, .open_line_below);
|
||||
if (std.mem.eql(u8, command, "open_line_above")) return dispatchAndRespond(allocator, session, .open_line_above);
|
||||
if (std.mem.startsWith(u8, command, "replace_char ")) return dispatchAndRespond(allocator, session, .{ .replace_char = command[13..] });
|
||||
if (std.mem.startsWith(u8, command, "insert ")) return dispatchAndRespond(allocator, session, .{ .insert = command[7..] });
|
||||
if (std.mem.startsWith(u8, command, "panel_open ")) return panelRespond(allocator, session, .{ .open = command[11..] });
|
||||
if (std.mem.startsWith(u8, command, "list_open ")) return listOpenRespond(allocator, session, command[10..]);
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+319
-18
@@ -34,6 +34,15 @@ const PrefixRail = enum {
|
||||
match,
|
||||
go,
|
||||
repeat,
|
||||
delete,
|
||||
change,
|
||||
yank,
|
||||
replace,
|
||||
};
|
||||
|
||||
const EditSnapshot = struct {
|
||||
bytes: []u8,
|
||||
cursor_byte: usize,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
@@ -116,6 +125,9 @@ pub const Client = struct {
|
||||
prefix: PrefixRail = .none,
|
||||
pending_count: usize = 0,
|
||||
last_rail: PrefixRail = .none,
|
||||
undo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||
redo_stack: std.ArrayList(EditSnapshot) = .empty,
|
||||
yank_bytes: ?[]u8 = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, viewport: Viewport) !Client {
|
||||
return initWithIo(allocator, viewport, null);
|
||||
@@ -135,6 +147,9 @@ pub const Client = struct {
|
||||
|
||||
pub fn deinit(self: *Client) void {
|
||||
if (self.saved_bytes) |bytes| self.allocator.free(bytes);
|
||||
if (self.yank_bytes) |bytes| self.allocator.free(bytes);
|
||||
self.freeSnapshotStack(&self.undo_stack);
|
||||
self.freeSnapshotStack(&self.redo_stack);
|
||||
self.repo.deinit();
|
||||
self.leader.deinit();
|
||||
self.session.deinit();
|
||||
@@ -206,7 +221,7 @@ pub const Client = struct {
|
||||
if (std.mem.startsWith(u8, line, "insert ")) return self.applyProtocolCommand(line);
|
||||
if (std.mem.eql(u8, line, "left")) return self.applyProtocol("command move_left");
|
||||
if (std.mem.eql(u8, line, "right")) return self.applyProtocol("command move_right");
|
||||
if (std.mem.eql(u8, line, "backspace")) return self.applyProtocol("command delete_backward");
|
||||
if (std.mem.eql(u8, line, "backspace")) return self.applyMutatingProtocol("command delete_backward");
|
||||
if (std.mem.eql(u8, line, "save")) return self.save();
|
||||
if (std.mem.startsWith(u8, line, "resize ")) return self.resize(line[7..]);
|
||||
if (std.mem.eql(u8, line, "render")) return;
|
||||
@@ -400,6 +415,10 @@ pub const Client = struct {
|
||||
.match => "match: m jump s inside a around ( { [ quotes",
|
||||
.go => "go: d definition r references e diagnostic a parameter",
|
||||
.repeat => "repeat: digits count . repeat-last",
|
||||
.delete => "delete: d line h previous-char l char",
|
||||
.change => "change: c line h previous-char l char",
|
||||
.yank => "yank: y line",
|
||||
.replace => "replace: next key replaces char",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -735,26 +754,51 @@ pub const Client = struct {
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "a")) {
|
||||
try self.applyProtocol("command move_right");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "o")) {
|
||||
try self.applyMutatingProtocol("command open_line_below");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "O")) {
|
||||
try self.applyMutatingProtocol("command open_line_above");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "s")) {
|
||||
self.mode = .select;
|
||||
self.message = "select";
|
||||
self.pending_count = 0;
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "d")) return self.openPrefix(.delete);
|
||||
if (std.mem.eql(u8, text, "c")) return self.openPrefix(.change);
|
||||
if (std.mem.eql(u8, text, "y")) return self.openPrefix(.yank);
|
||||
if (std.mem.eql(u8, text, "r")) return self.openPrefix(.replace);
|
||||
if (std.mem.eql(u8, text, "p")) return self.pasteRegister();
|
||||
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, "0")) return self.applyProtocol("command move_line_start");
|
||||
if (std.mem.eql(u8, text, "$")) return self.applyProtocol("command move_line_end");
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
if (std.mem.eql(u8, text, "j")) {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "k")) {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
return;
|
||||
}
|
||||
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());
|
||||
if (std.mem.eql(u8, text, "h")) return self.repeatProtocol("command move_left", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "l")) return self.repeatProtocol("command move_right", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "w")) return self.repeatProtocol("command move_word_forward", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "b")) return self.repeatProtocol("command move_word_back", self.takeRepeat());
|
||||
if (std.mem.eql(u8, text, "e")) return self.repeatProtocol("command move_word_end", self.takeRepeat());
|
||||
self.unknownPrefixOrInput("normal");
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
@@ -762,13 +806,11 @@ pub const Client = struct {
|
||||
self.pending_count = 0;
|
||||
self.message = "normal";
|
||||
},
|
||||
.backspace => try self.applyProtocol("command delete_backward"),
|
||||
.backspace => try self.applyMutatingProtocol("command delete_backward"),
|
||||
.arrow_left => try self.repeatProtocol("command move_left", self.takeRepeat()),
|
||||
.arrow_right => try self.repeatProtocol("command move_right", self.takeRepeat()),
|
||||
.arrow_up, .arrow_down => {
|
||||
_ = self.takeRepeat();
|
||||
self.message = "vertical movement is not built in this profile yet";
|
||||
},
|
||||
.arrow_up => try self.repeatProtocol("command move_up", self.takeRepeat()),
|
||||
.arrow_down => try self.repeatProtocol("command move_down", self.takeRepeat()),
|
||||
else => self.unknownPrefixOrInput("normal"),
|
||||
},
|
||||
.unknown => self.unknownPrefixOrInput("normal"),
|
||||
@@ -784,7 +826,7 @@ pub const Client = struct {
|
||||
self.mode = .normal;
|
||||
self.message = "normal";
|
||||
},
|
||||
.backspace => try self.applyProtocol("command delete_backward"),
|
||||
.backspace => try self.applyMutatingProtocol("command delete_backward"),
|
||||
.arrow_left => try self.applyProtocol("command move_left"),
|
||||
.arrow_right => try self.applyProtocol("command move_right"),
|
||||
else => {},
|
||||
@@ -848,6 +890,10 @@ pub const Client = struct {
|
||||
.match => self.applyKnownRailOrMessage(event, "match rail ready"),
|
||||
.go => self.applyKnownRailOrMessage(event, "go rail ready"),
|
||||
.repeat => self.applyKnownRailOrMessage(event, "repeat rail ready"),
|
||||
.delete => try self.applyDeleteRail(event),
|
||||
.change => try self.applyChangeRail(event),
|
||||
.yank => try self.applyYankRail(event),
|
||||
.replace => try self.applyReplaceRail(event),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -908,10 +954,146 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn applyDeleteRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "d")) {
|
||||
try self.yankCurrentLine(true);
|
||||
try self.applyMutatingProtocol("command delete_line");
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "h")) return self.applyMutatingProtocol("command delete_backward");
|
||||
if (std.mem.eql(u8, text, "l")) return self.applyMutatingProtocol("command delete_forward");
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
fn applyChangeRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "c")) {
|
||||
try self.yankCurrentLine(false);
|
||||
try self.applyMutatingProtocol("command change_line");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "h")) {
|
||||
try self.applyMutatingProtocol("command delete_backward");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "l")) {
|
||||
try self.applyMutatingProtocol("command delete_forward");
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
return;
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
fn applyYankRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "y")) {
|
||||
try self.yankCurrentLine(true);
|
||||
self.message = "yanked line";
|
||||
return;
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
fn applyReplaceRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
const command = try std.fmt.allocPrint(self.allocator, "command replace_char {s}", .{text});
|
||||
defer self.allocator.free(command);
|
||||
try self.applyMutatingProtocol(command);
|
||||
}
|
||||
|
||||
fn eventText(event: input.Event) ?[]const u8 {
|
||||
return switch (event) {
|
||||
.text => |text| text,
|
||||
.key => |key| switch (key) {
|
||||
.space => " ",
|
||||
else => null,
|
||||
},
|
||||
.unknown => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn pasteRegister(self: *Client) !void {
|
||||
const bytes = self.yank_bytes orelse {
|
||||
self.message = "nothing yanked";
|
||||
return;
|
||||
};
|
||||
try self.recordUndo();
|
||||
self.clearRedo();
|
||||
self.session.dispatch(.{ .insert = bytes }) catch |err| {
|
||||
self.dropLastUndoSnapshot();
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn yankCurrentLine(self: *Client, include_newline: bool) !void {
|
||||
const snap = try self.session.snapshot();
|
||||
var start = snap.cursor_byte;
|
||||
while (start > 0 and snap.bytes[start - 1] != '\n') start -= 1;
|
||||
var end = snap.cursor_byte;
|
||||
while (end < snap.bytes.len and snap.bytes[end] != '\n') end += 1;
|
||||
if (include_newline and end < snap.bytes.len and snap.bytes[end] == '\n') end += 1;
|
||||
const copy = try self.allocator.dupe(u8, snap.bytes[start..end]);
|
||||
if (self.yank_bytes) |old| self.allocator.free(old);
|
||||
self.yank_bytes = copy;
|
||||
}
|
||||
|
||||
fn undoEdit(self: *Client) !void {
|
||||
const previous = self.undo_stack.pop() orelse {
|
||||
self.message = "nothing to undo";
|
||||
return;
|
||||
};
|
||||
const current = try self.takeCurrentSnapshot();
|
||||
try self.redo_stack.append(self.allocator, current);
|
||||
try self.restoreSnapshot(previous);
|
||||
self.allocator.free(previous.bytes);
|
||||
self.message = "undo";
|
||||
}
|
||||
|
||||
fn redoEdit(self: *Client) !void {
|
||||
const next = self.redo_stack.pop() orelse {
|
||||
self.message = "nothing to redo";
|
||||
return;
|
||||
};
|
||||
const current = try self.takeCurrentSnapshot();
|
||||
try self.undo_stack.append(self.allocator, current);
|
||||
try self.restoreSnapshot(next);
|
||||
self.allocator.free(next.bytes);
|
||||
self.message = "redo";
|
||||
}
|
||||
|
||||
fn takeCurrentSnapshot(self: *Client) !EditSnapshot {
|
||||
const snap = try self.session.snapshot();
|
||||
return .{ .bytes = try self.allocator.dupe(u8, snap.bytes), .cursor_byte = snap.cursor_byte };
|
||||
}
|
||||
|
||||
fn insertText(self: *Client, text: []const u8) !void {
|
||||
const line = try std.fmt.allocPrint(self.allocator, "insert {s}", .{text});
|
||||
defer self.allocator.free(line);
|
||||
try self.applyProtocolCommand(line);
|
||||
try self.applyMutatingProtocolCommand(line);
|
||||
}
|
||||
|
||||
fn applyMutatingProtocolCommand(self: *Client, line: []const u8) !void {
|
||||
try self.recordUndo();
|
||||
self.clearRedo();
|
||||
self.applyProtocolCommand(line) catch |err| {
|
||||
self.dropLastUndoSnapshot();
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn applyMutatingProtocol(self: *Client, line: []const u8) !void {
|
||||
try self.recordUndo();
|
||||
self.clearRedo();
|
||||
self.applyProtocol(line) catch |err| {
|
||||
self.dropLastUndoSnapshot();
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn repeatProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||
@@ -919,6 +1101,39 @@ pub const Client = struct {
|
||||
while (i < repeat) : (i += 1) try self.applyProtocol(line);
|
||||
}
|
||||
|
||||
fn repeatMutatingProtocol(self: *Client, line: []const u8, repeat: usize) !void {
|
||||
var i: usize = 0;
|
||||
while (i < repeat) : (i += 1) try self.applyMutatingProtocol(line);
|
||||
}
|
||||
|
||||
fn recordUndo(self: *Client) !void {
|
||||
const snap = try self.session.snapshot();
|
||||
const bytes = try self.allocator.dupe(u8, snap.bytes);
|
||||
errdefer self.allocator.free(bytes);
|
||||
try self.undo_stack.append(self.allocator, .{
|
||||
.bytes = bytes,
|
||||
.cursor_byte = snap.cursor_byte,
|
||||
});
|
||||
}
|
||||
|
||||
fn dropLastUndoSnapshot(self: *Client) void {
|
||||
if (self.undo_stack.pop()) |snap| self.allocator.free(snap.bytes);
|
||||
}
|
||||
|
||||
fn restoreSnapshot(self: *Client, snap: EditSnapshot) !void {
|
||||
try self.session.openFixtureAt(snap.bytes, snap.cursor_byte);
|
||||
}
|
||||
|
||||
fn freeSnapshotStack(self: *Client, stack: *std.ArrayList(EditSnapshot)) void {
|
||||
for (stack.items) |snap| self.allocator.free(snap.bytes);
|
||||
stack.deinit(self.allocator);
|
||||
}
|
||||
|
||||
fn clearRedo(self: *Client) void {
|
||||
self.freeSnapshotStack(&self.redo_stack);
|
||||
self.redo_stack = .empty;
|
||||
}
|
||||
|
||||
fn unknownPrefixOrInput(self: *Client, mode: []const u8) void {
|
||||
self.pending_count = 0;
|
||||
self.message = if (std.mem.eql(u8, mode, "normal"))
|
||||
@@ -2448,3 +2663,89 @@ test "regular: insert pending space commits literal space or returns normal" {
|
||||
try client.handleInput("n");
|
||||
try std.testing.expectEqualStrings("normal", client.modeName());
|
||||
}
|
||||
|
||||
test "regular: normal core editing delete yank paste undo redo" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open abc\ndef");
|
||||
|
||||
try client.handleInput("d");
|
||||
try client.handleInput("d");
|
||||
const after_delete = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_delete);
|
||||
try std.testing.expectEqualStrings("def", after_delete);
|
||||
|
||||
try client.handleInput("p");
|
||||
const after_paste = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_paste);
|
||||
try std.testing.expectEqualStrings("abc\ndef", after_paste);
|
||||
|
||||
try client.handleInput("u");
|
||||
const after_undo = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_undo);
|
||||
try std.testing.expectEqualStrings("def", after_undo);
|
||||
|
||||
try client.handleInput("U");
|
||||
const after_redo = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_redo);
|
||||
try std.testing.expectEqualStrings("abc\ndef", after_redo);
|
||||
}
|
||||
|
||||
test "regular: normal change replace open lines and movement enter insert" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open abc\ndef");
|
||||
|
||||
try client.handleInput("r");
|
||||
try client.handleInput("Z");
|
||||
const after_replace = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_replace);
|
||||
try std.testing.expectEqualStrings("Zbc\ndef", after_replace);
|
||||
|
||||
try client.handleInput("c");
|
||||
try client.handleInput("c");
|
||||
try std.testing.expectEqualStrings("insert", client.modeName());
|
||||
try client.handleInput("X");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
const after_change = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_change);
|
||||
try std.testing.expectEqualStrings("X\ndef", after_change);
|
||||
|
||||
try client.handleInput("o");
|
||||
try client.handleInput("Y");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
const after_open = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_open);
|
||||
try std.testing.expectEqualStrings("X\nY\ndef", after_open);
|
||||
|
||||
try client.handleInput("0");
|
||||
try client.handleInput("l");
|
||||
try client.handleInput("k");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("e");
|
||||
try client.handleInput("b");
|
||||
}
|
||||
|
||||
test "adversarial: replace rejects invalid utf8 and undo preserves content" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 48, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open safe");
|
||||
|
||||
const bad = [_]u8{ 0xc3, 0x28 };
|
||||
try client.handleInput("r");
|
||||
try client.handleInput(&bad);
|
||||
const after_bad_replace = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_bad_replace);
|
||||
try std.testing.expectEqualStrings("safe", after_bad_replace);
|
||||
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("!");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
try client.handleInput("u");
|
||||
const after_undo = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_undo);
|
||||
try std.testing.expectEqualStrings("safe", after_undo);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user