From 8b60c2c29c016c7db0fa6ee63769cea1defd9b3c Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 30 Jun 2026 20:29:05 -0300 Subject: minor fixes --- modal.zig | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) (limited to 'modal.zig') 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; -- cgit v1.3