summaryrefslogtreecommitdiff
path: root/modal.zig
diff options
context:
space:
mode:
Diffstat (limited to 'modal.zig')
-rw-r--r--modal.zig55
1 files changed, 55 insertions, 0 deletions
diff --git a/modal.zig b/modal.zig
index 3038c430..2a195432 100644
--- a/modal.zig
+++ b/modal.zig
@@ -306,6 +306,38 @@ pub fn deleteLines(alloc: std.mem.Allocator, content: []const u8, r0: usize, r1:
return .{ .content = try out.toOwnedSlice(alloc), .deleted = try deleted.toOwnedSlice(alloc) };
}
+// normalize two cursors into document order (lo <= hi), then byte offsets of the
+// INCLUSIVE range [lo .. hi] (the char under hi is included). e == s means empty.
+fn rangeBytes(content: []const u8, a: Cursor, b: Cursor) struct { s: usize, e: usize } {
+ var lo = a;
+ var hi = b;
+ if (hi.row < lo.row or (hi.row == lo.row and hi.col < lo.col)) {
+ lo = b;
+ hi = a;
+ }
+ const s = lineStartOffset(content, lo.row) + @min(lo.col, lineSlice(content, lo.row).len);
+ var e = lineStartOffset(content, hi.row) + @min(hi.col, lineSlice(content, hi.row).len);
+ if (e < content.len) e += 1; // include the char under the head
+ return .{ .s = s, .e = @max(s, e) };
+}
+
+// the text of the inclusive char range [a, b] (cursors in either order). Caller frees.
+pub fn rangeText(alloc: std.mem.Allocator, content: []const u8, a: Cursor, b: Cursor) ![]u8 {
+ const r = rangeBytes(content, a, b);
+ return alloc.dupe(u8, content[r.s..r.e]);
+}
+
+// delete the inclusive char range [a, b]. returns new content + the removed text.
+pub fn deleteRange(alloc: std.mem.Allocator, content: []const u8, a: Cursor, b: Cursor) !Deleted {
+ const r = rangeBytes(content, a, b);
+ const deleted = try alloc.dupe(u8, content[r.s..r.e]);
+ errdefer alloc.free(deleted);
+ var out: std.ArrayList(u8) = .empty;
+ try out.appendSlice(alloc, content[0..r.s]);
+ try out.appendSlice(alloc, content[r.e..]);
+ return .{ .content = try out.toOwnedSlice(alloc), .deleted = deleted };
+}
+
// replace line `row`'s text with "" (keep the line, empty it). For `c`hange line.
pub fn clearLine(alloc: std.mem.Allocator, content: []const u8, row: usize) ![]u8 {
const line = lineSlice(content, row);
@@ -884,6 +916,29 @@ test "deleteLines only line" {
try std.testing.expectEqualStrings("only", d.deleted);
}
+test "rangeText + deleteRange (char-wise select)" {
+ const a = std.testing.allocator;
+ const content = "hello\nworld\nfoo";
+ // same-line inclusive range: "hello"[1..3] -> "ell"
+ const t1 = try rangeText(a, content, .{ .row = 0, .col = 1 }, .{ .row = 0, .col = 3 });
+ defer a.free(t1);
+ try std.testing.expectEqualStrings("ell", t1);
+ // reversed cursors give the same range
+ const t2 = try rangeText(a, content, .{ .row = 0, .col = 3 }, .{ .row = 0, .col = 1 });
+ defer a.free(t2);
+ try std.testing.expectEqualStrings("ell", t2);
+ // cross-line range includes the newline: row0 col3 .. row1 col1 -> "lo\nwo"
+ const t3 = try rangeText(a, content, .{ .row = 0, .col = 3 }, .{ .row = 1, .col = 1 });
+ defer a.free(t3);
+ try std.testing.expectEqualStrings("lo\nwo", t3);
+ // delete the same cross-line range
+ const d = try deleteRange(a, content, .{ .row = 0, .col = 3 }, .{ .row = 1, .col = 1 });
+ defer a.free(d.content);
+ defer a.free(d.deleted);
+ try std.testing.expectEqualStrings("helrld\nfoo", d.content);
+ try std.testing.expectEqualStrings("lo\nwo", d.deleted);
+}
+
test "clearLine" {
const content = "keep\nzap me\nkeep2";
const a = std.testing.allocator;