Add composable object selections
This commit is contained in:
@@ -370,6 +370,44 @@ pub const Session = struct {
|
||||
.active_panel_title = panel_state.active_title,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn moveToByte(self: *Session, byte: usize) !void {
|
||||
if (self.buffer) |*buffer| {
|
||||
if (byte > buffer.bytes.items.len) return error.SelectionOutOfBounds;
|
||||
buffer.cursor.byte = byte;
|
||||
buffer.refreshCell();
|
||||
return;
|
||||
}
|
||||
return error.NoActiveBuffer;
|
||||
}
|
||||
|
||||
pub fn selectRange(self: *Session, start: usize, end: usize) !void {
|
||||
if (self.buffer) |*buffer| {
|
||||
if (start > end or end > buffer.bytes.items.len) return error.SelectionOutOfBounds;
|
||||
buffer.selection = .{ .anchor = start, .cursor = end };
|
||||
buffer.cursor.byte = end;
|
||||
buffer.refreshCell();
|
||||
return;
|
||||
}
|
||||
return error.NoActiveBuffer;
|
||||
}
|
||||
|
||||
pub fn clearSelection(self: *Session) void {
|
||||
if (self.buffer) |*buffer| buffer.selection = null;
|
||||
}
|
||||
|
||||
pub fn replaceRange(self: *Session, start: usize, end: usize, bytes: []const u8) !void {
|
||||
if (!std.unicode.utf8ValidateSlice(bytes)) return error.InvalidUtf8Insertion;
|
||||
if (self.buffer) |*buffer| {
|
||||
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.selection = null;
|
||||
return;
|
||||
}
|
||||
return error.NoActiveBuffer;
|
||||
}
|
||||
};
|
||||
|
||||
fn isWordSeparator(byte: u8) bool {
|
||||
|
||||
+423
-2
@@ -45,6 +45,16 @@ const EditSnapshot = struct {
|
||||
cursor_byte: usize,
|
||||
};
|
||||
|
||||
const ObjectRange = struct {
|
||||
start: usize,
|
||||
end: usize,
|
||||
};
|
||||
|
||||
const PairRange = struct {
|
||||
open: usize,
|
||||
close: usize,
|
||||
};
|
||||
|
||||
pub const Error = error{
|
||||
ViewportTooSmall,
|
||||
InvalidResize,
|
||||
@@ -845,6 +855,13 @@ pub const Client = struct {
|
||||
}
|
||||
if (std.mem.eql(u8, text, "m")) return self.openPrefix(.match);
|
||||
if (std.mem.eql(u8, text, "g")) return self.openPrefix(.go);
|
||||
if (text.len == 1) {
|
||||
if (self.objectRangeForKey(text[0])) |range| {
|
||||
try self.session.selectRange(range.start, range.end);
|
||||
self.message = "selected object";
|
||||
return;
|
||||
} else |_| {}
|
||||
}
|
||||
self.unknownPrefixOrInput("select");
|
||||
},
|
||||
.key => |key| switch (key) {
|
||||
@@ -887,7 +904,9 @@ pub const Client = struct {
|
||||
switch (active) {
|
||||
.none => {},
|
||||
.insert_space => try self.applyInsertSpaceRail(event),
|
||||
.match => self.applyKnownRailOrMessage(event, "match rail ready"),
|
||||
.match => self.applyMatchRail(event) catch |err| {
|
||||
self.message = @errorName(err);
|
||||
},
|
||||
.go => self.applyKnownRailOrMessage(event, "go rail ready"),
|
||||
.repeat => self.applyKnownRailOrMessage(event, "repeat rail ready"),
|
||||
.delete => try self.applyDeleteRail(event),
|
||||
@@ -954,6 +973,33 @@ pub const Client = struct {
|
||||
self.message = null;
|
||||
}
|
||||
|
||||
fn applyMatchRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "m")) return self.jumpToMatch(null);
|
||||
if (std.mem.eql(u8, text, "s")) {
|
||||
const range = try self.matchRange(null, false);
|
||||
try self.session.selectRange(range.start, range.end);
|
||||
self.message = "selected inside pair";
|
||||
return;
|
||||
}
|
||||
if (std.mem.eql(u8, text, "a")) {
|
||||
const range = try self.matchRange(null, true);
|
||||
try self.session.selectRange(range.start, range.end);
|
||||
self.message = "selected around pair";
|
||||
return;
|
||||
}
|
||||
if (text.len == 1 and isPairSelector(text[0])) {
|
||||
if (self.effectiveMode() == .select) {
|
||||
const range = try self.matchRange(text[0], true);
|
||||
try self.session.selectRange(range.start, range.end);
|
||||
} else {
|
||||
try self.jumpToMatch(text[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
fn applyDeleteRail(self: *Client, event: input.Event) !void {
|
||||
const text = eventText(event) orelse return self.unknownPrefixOrInput("normal");
|
||||
if (std.mem.eql(u8, text, "d")) {
|
||||
@@ -963,6 +1009,9 @@ pub const Client = struct {
|
||||
}
|
||||
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");
|
||||
if (text.len == 1) {
|
||||
if (self.objectRangeForKey(text[0])) |range| return self.deleteRange(range) else |_| {}
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
@@ -987,6 +1036,12 @@ pub const Client = struct {
|
||||
self.message = "insert";
|
||||
return;
|
||||
}
|
||||
if (text.len == 1) {
|
||||
if (self.objectRangeForKey(text[0])) |range| {
|
||||
try self.changeRange(range);
|
||||
return;
|
||||
} else |_| {}
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
@@ -997,6 +1052,13 @@ pub const Client = struct {
|
||||
self.message = "yanked line";
|
||||
return;
|
||||
}
|
||||
if (text.len == 1) {
|
||||
if (self.objectRangeForKey(text[0])) |range| {
|
||||
try self.yankRange(range);
|
||||
self.message = "yanked object";
|
||||
return;
|
||||
} else |_| {}
|
||||
}
|
||||
self.unknownPrefixOrInput("normal");
|
||||
}
|
||||
|
||||
@@ -1018,6 +1080,36 @@ pub const Client = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn deleteRange(self: *Client, range: ObjectRange) !void {
|
||||
try self.yankRange(range);
|
||||
try self.recordUndo();
|
||||
self.clearRedo();
|
||||
self.session.replaceRange(range.start, range.end, "") catch |err| {
|
||||
self.dropLastUndoSnapshot();
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn changeRange(self: *Client, range: ObjectRange) !void {
|
||||
try self.yankRange(range);
|
||||
try self.recordUndo();
|
||||
self.clearRedo();
|
||||
self.session.replaceRange(range.start, range.end, "") catch |err| {
|
||||
self.dropLastUndoSnapshot();
|
||||
return err;
|
||||
};
|
||||
self.mode = .insert;
|
||||
self.message = "insert";
|
||||
}
|
||||
|
||||
fn yankRange(self: *Client, range: ObjectRange) !void {
|
||||
const snap = try self.session.snapshot();
|
||||
if (range.start > range.end or range.end > snap.bytes.len) return error.SelectionOutOfBounds;
|
||||
const copy = try self.allocator.dupe(u8, snap.bytes[range.start..range.end]);
|
||||
if (self.yank_bytes) |old| self.allocator.free(old);
|
||||
self.yank_bytes = copy;
|
||||
}
|
||||
|
||||
fn pasteRegister(self: *Client) !void {
|
||||
const bytes = self.yank_bytes orelse {
|
||||
self.message = "nothing yanked";
|
||||
@@ -1072,6 +1164,102 @@ pub const Client = struct {
|
||||
return .{ .bytes = try self.allocator.dupe(u8, snap.bytes), .cursor_byte = snap.cursor_byte };
|
||||
}
|
||||
|
||||
fn objectRangeForKey(self: *Client, key: u8) !ObjectRange {
|
||||
return switch (key) {
|
||||
'w' => try self.wordRange(),
|
||||
'l' => try self.lineRange(true),
|
||||
'i' => try self.indentRange(),
|
||||
'p' => try self.parameterRange(),
|
||||
'f' => try self.enclosingFormRange(),
|
||||
'd' => try self.lineRange(false),
|
||||
else => error.UnsupportedObject,
|
||||
};
|
||||
}
|
||||
|
||||
fn wordRange(self: *Client) !ObjectRange {
|
||||
const snap = try self.session.snapshot();
|
||||
var start = snap.cursor_byte;
|
||||
while (start > 0 and !isObjectSeparator(snap.bytes[previousBoundaryLocal(snap.bytes, start)])) start = previousBoundaryLocal(snap.bytes, start);
|
||||
var end = snap.cursor_byte;
|
||||
while (end < snap.bytes.len and !isObjectSeparator(snap.bytes[end])) end = nextBoundaryLocal(snap.bytes, end);
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn lineRange(self: *Client, include_newline: bool) !ObjectRange {
|
||||
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) end += 1;
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn indentRange(self: *Client) !ObjectRange {
|
||||
const snap = try self.session.snapshot();
|
||||
const current = try self.lineRange(false);
|
||||
const indent = lineIndent(snap.bytes[current.start..current.end]);
|
||||
var start = current.start;
|
||||
while (start > 0) {
|
||||
const prev_end = start - 1;
|
||||
var prev_start = prev_end;
|
||||
while (prev_start > 0 and snap.bytes[prev_start - 1] != '\n') prev_start -= 1;
|
||||
if (lineContentLen(snap.bytes[prev_start..prev_end]) != 0 and lineIndent(snap.bytes[prev_start..prev_end]) < indent) break;
|
||||
start = prev_start;
|
||||
}
|
||||
var end = current.end;
|
||||
while (end < snap.bytes.len) {
|
||||
const next_start = if (end < snap.bytes.len and snap.bytes[end] == '\n') end + 1 else end;
|
||||
if (next_start >= snap.bytes.len) break;
|
||||
var next_end = next_start;
|
||||
while (next_end < snap.bytes.len and snap.bytes[next_end] != '\n') next_end += 1;
|
||||
if (lineContentLen(snap.bytes[next_start..next_end]) != 0 and lineIndent(snap.bytes[next_start..next_end]) < indent) break;
|
||||
end = next_end;
|
||||
}
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn parameterRange(self: *Client) !ObjectRange {
|
||||
const around = try self.matchRange('(', false);
|
||||
const snap = try self.session.snapshot();
|
||||
var start = around.start;
|
||||
var end = around.end;
|
||||
var at = around.start;
|
||||
while (at < around.end) : (at += 1) {
|
||||
if (snap.bytes[at] == ',' and at < snap.cursor_byte) start = at + 1;
|
||||
if (snap.bytes[at] == ',' and at >= snap.cursor_byte) {
|
||||
end = at;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return trimRange(snap.bytes, .{ .start = start, .end = end });
|
||||
}
|
||||
|
||||
fn enclosingFormRange(self: *Client) !ObjectRange {
|
||||
return self.matchRange('{', true) catch self.matchRange('(', true);
|
||||
}
|
||||
|
||||
fn jumpToMatch(self: *Client, selector: ?u8) !void {
|
||||
const pair = try self.matchPair(selector);
|
||||
const snap = try self.session.snapshot();
|
||||
const target = if (snap.cursor_byte <= pair.open) pair.close else pair.open;
|
||||
try self.session.moveToByte(target);
|
||||
self.message = "matched pair";
|
||||
}
|
||||
|
||||
fn matchRange(self: *Client, selector: ?u8, around: bool) !ObjectRange {
|
||||
const pair = try self.matchPair(selector);
|
||||
return if (around) .{ .start = pair.open, .end = pair.close + 1 } else .{ .start = pair.open + 1, .end = pair.close };
|
||||
}
|
||||
|
||||
fn matchPair(self: *Client, selector: ?u8) !PairRange {
|
||||
const snap = try self.session.snapshot();
|
||||
if (snap.bytes.len == 0) return error.NoMatch;
|
||||
if (selector) |sel| return findPairForSelector(snap.bytes, snap.cursor_byte, sel) orelse error.NoMatch;
|
||||
if (findPairAtOrNear(snap.bytes, snap.cursor_byte)) |pair| return pair;
|
||||
return error.NoMatch;
|
||||
}
|
||||
|
||||
fn insertText(self: *Client, text: []const u8) !void {
|
||||
const line = try std.fmt.allocPrint(self.allocator, "insert {s}", .{text});
|
||||
defer self.allocator.free(line);
|
||||
@@ -2604,7 +2792,7 @@ test "regular: modal rails expose match go repeat and unknown recovery" {
|
||||
try std.testing.expectEqualStrings("none", client.pendingRailName());
|
||||
const unknown_frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(unknown_frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, unknown_frame, "unknown prefix key") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, unknown_frame, "unknown normal key") != null);
|
||||
|
||||
try client.handleInput("g");
|
||||
try std.testing.expectEqualStrings("go", client.pendingRailName());
|
||||
@@ -2749,3 +2937,236 @@ test "adversarial: replace rejects invalid utf8 and undo preserves content" {
|
||||
defer std.testing.allocator.free(after_undo);
|
||||
try std.testing.expectEqualStrings("safe", after_undo);
|
||||
}
|
||||
|
||||
fn isObjectSeparator(byte: u8) bool {
|
||||
return std.ascii.isWhitespace(byte) or std.mem.indexOfScalar(u8, "(){}[]<>.,;:+-*/=\"'`", byte) != null;
|
||||
}
|
||||
|
||||
fn previousBoundaryLocal(bytes: []const u8, cursor: usize) usize {
|
||||
return session_mod.boundaryAtOrBefore(bytes, if (cursor == 0) 0 else cursor - 1);
|
||||
}
|
||||
|
||||
fn nextBoundaryLocal(bytes: []const u8, cursor: usize) usize {
|
||||
var at = cursor + 1;
|
||||
while (at < bytes.len and (bytes[at] & 0b1100_0000) == 0b1000_0000) at += 1;
|
||||
return @min(at, bytes.len);
|
||||
}
|
||||
|
||||
fn lineIndent(bytes: []const u8) usize {
|
||||
var count: usize = 0;
|
||||
while (count < bytes.len and (bytes[count] == ' ' or bytes[count] == '\t')) count += 1;
|
||||
return count;
|
||||
}
|
||||
|
||||
fn lineContentLen(bytes: []const u8) usize {
|
||||
for (bytes) |byte| if (!std.ascii.isWhitespace(byte)) return bytes.len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn trimRange(bytes: []const u8, range: ObjectRange) ObjectRange {
|
||||
var start = range.start;
|
||||
var end = range.end;
|
||||
while (start < end and std.ascii.isWhitespace(bytes[start])) start += 1;
|
||||
while (end > start and std.ascii.isWhitespace(bytes[end - 1])) end -= 1;
|
||||
return .{ .start = start, .end = end };
|
||||
}
|
||||
|
||||
fn isPairSelector(byte: u8) bool {
|
||||
return std.mem.indexOfScalar(u8, "({[\"'`", byte) != null;
|
||||
}
|
||||
|
||||
fn pairChars(selector: u8) struct { open: u8, close: u8, quote: bool } {
|
||||
return switch (selector) {
|
||||
'(' => .{ .open = '(', .close = ')', .quote = false },
|
||||
'{' => .{ .open = '{', .close = '}', .quote = false },
|
||||
'[' => .{ .open = '[', .close = ']', .quote = false },
|
||||
'\"' => .{ .open = '\"', .close = '\"', .quote = true },
|
||||
'\'' => .{ .open = '\'', .close = '\'', .quote = true },
|
||||
'`' => .{ .open = '`', .close = '`', .quote = true },
|
||||
else => .{ .open = '(', .close = ')', .quote = false },
|
||||
};
|
||||
}
|
||||
|
||||
fn findPairForSelector(bytes: []const u8, cursor: usize, selector: u8) ?PairRange {
|
||||
const chars = pairChars(selector);
|
||||
if (chars.quote) return findQuotePair(bytes, cursor, chars.open);
|
||||
var depth: usize = 0;
|
||||
var open: ?usize = null;
|
||||
var i: usize = @min(cursor, bytes.len);
|
||||
while (i > 0) {
|
||||
i -= 1;
|
||||
if (bytes[i] == chars.close) depth += 1 else if (bytes[i] == chars.open) {
|
||||
if (depth == 0) {
|
||||
open = i;
|
||||
break;
|
||||
}
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
const found_open = open orelse return null;
|
||||
depth = 0;
|
||||
i = found_open;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
if (bytes[i] == chars.open) depth += 1 else if (bytes[i] == chars.close) {
|
||||
depth -= 1;
|
||||
if (depth == 0) return .{ .open = found_open, .close = i };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findQuotePair(bytes: []const u8, cursor: usize, quote: u8) ?PairRange {
|
||||
var open: ?usize = null;
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) : (i += 1) {
|
||||
if (bytes[i] != quote or (i > 0 and bytes[i - 1] == '\\')) continue;
|
||||
if (open == null) open = i else {
|
||||
const start = open.?;
|
||||
if (cursor >= start and cursor <= i) return .{ .open = start, .close = i };
|
||||
open = null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findPairAtOrNear(bytes: []const u8, cursor: usize) ?PairRange {
|
||||
const selectors = "({[\"'`";
|
||||
if (cursor < bytes.len and isPairSelector(bytes[cursor])) {
|
||||
if (findPairForSelector(bytes, cursor + 1, bytes[cursor])) |pair| return pair;
|
||||
}
|
||||
if (cursor > 0 and isPairSelector(bytes[cursor - 1])) {
|
||||
if (findPairForSelector(bytes, cursor, bytes[cursor - 1])) |pair| return pair;
|
||||
}
|
||||
for (selectors) |selector| {
|
||||
if (findPairForSelector(bytes, cursor, selector)) |pair| return pair;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
test "regular: match rail jumps and selects delimiter pairs" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 60, .height = 10 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open fn main() { call(one, two); }");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("(");
|
||||
var snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.cursor_byte > 7);
|
||||
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("s");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expect(snap.selection != null);
|
||||
try std.testing.expectEqualStrings("one, two", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("a");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("(one, two)", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
}
|
||||
|
||||
test "regular: select mode object grammar selects word line indent parameter and form" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 12 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open fn main() {\n alpha(beta, gamma);\n next();\n}\n");
|
||||
try client.handleInput("j");
|
||||
try client.handleInput("w");
|
||||
try client.handleInput("s");
|
||||
try client.handleInput("w");
|
||||
var snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("alpha", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
|
||||
try client.handleInput("n");
|
||||
try client.handleInput("s");
|
||||
try client.handleInput("l");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings(" alpha(beta, gamma);\n", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
|
||||
try client.handleInput("n");
|
||||
try client.handleInput("s");
|
||||
try client.handleInput("i");
|
||||
snap = try client.session.snapshot();
|
||||
try std.testing.expectEqualStrings(" alpha(beta, gamma);\n next();", snap.bytes[snap.selection.?.anchor..snap.selection.?.cursor]);
|
||||
|
||||
var param_client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 12 });
|
||||
defer param_client.deinit();
|
||||
try param_client.handleTraceLine("open call(beta, gamma)");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleTraceLine("right");
|
||||
try param_client.handleInput("s");
|
||||
try param_client.handleInput("p");
|
||||
var param_snap = try param_client.session.snapshot();
|
||||
try std.testing.expectEqualStrings("beta", param_snap.bytes[param_snap.selection.?.anchor..param_snap.selection.?.cursor]);
|
||||
|
||||
var form_client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 12 });
|
||||
defer form_client.deinit();
|
||||
try form_client.handleTraceLine("open { alpha(); }");
|
||||
try form_client.handleTraceLine("right");
|
||||
try form_client.handleTraceLine("right");
|
||||
try form_client.handleInput("s");
|
||||
try form_client.handleInput("f");
|
||||
var form_snap = try form_client.session.snapshot();
|
||||
try std.testing.expect(std.mem.startsWith(u8, form_snap.bytes[form_snap.selection.?.anchor..form_snap.selection.?.cursor], "{"));
|
||||
}
|
||||
|
||||
test "regular: delete change yank compose with shared object ranges" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 10 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open alpha beta\n one\n two\nend");
|
||||
|
||||
try client.handleInput("d");
|
||||
try client.handleInput("w");
|
||||
const after_delete_word = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(after_delete_word);
|
||||
try std.testing.expectEqualStrings(" beta\n one\n two\nend", after_delete_word);
|
||||
|
||||
try client.handleInput("p");
|
||||
const pasted = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(pasted);
|
||||
try std.testing.expectEqualStrings("alpha beta\n one\n two\nend", pasted);
|
||||
|
||||
try client.handleInput("j");
|
||||
try client.handleInput("c");
|
||||
try client.handleInput("i");
|
||||
try client.handleInput("X");
|
||||
try client.handleInput(" ");
|
||||
try client.handleInput("n");
|
||||
const changed = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(changed);
|
||||
try std.testing.expectEqualStrings("alpha beta\nX\nend", changed);
|
||||
}
|
||||
|
||||
test "adversarial: unmatched delimiter reports without corrupting buffer" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 60, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open call(one");
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("(");
|
||||
const bytes = try client.snapshotBytesAlloc(std.testing.allocator);
|
||||
defer std.testing.allocator.free(bytes);
|
||||
try std.testing.expectEqualStrings("call(one", bytes);
|
||||
const frame = try client.render(std.testing.allocator);
|
||||
defer std.testing.allocator.free(frame);
|
||||
try std.testing.expect(std.mem.indexOf(u8, frame, "unknown normal key") != null or std.mem.indexOf(u8, frame, "NoMatch") != null);
|
||||
}
|
||||
|
||||
test "regular: quote matching ignores escaped quote" {
|
||||
var client = try Client.init(std.testing.allocator, .{ .width = 80, .height = 8 });
|
||||
defer client.deinit();
|
||||
try client.handleTraceLine("open say(\"a\\\"b\")");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleTraceLine("right");
|
||||
try client.handleInput("m");
|
||||
try client.handleInput("\"");
|
||||
const snap = try client.session.snapshot();
|
||||
try std.testing.expectEqual(@as(usize, 4), snap.cursor_byte);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user