Add core editing commands
This commit is contained in:
+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