summaryrefslogtreecommitdiff
path: root/src/ninep
diff options
context:
space:
mode:
Diffstat (limited to 'src/ninep')
-rw-r--r--src/ninep/ctl.zig1
-rw-r--r--src/ninep/pane.zig80
2 files changed, 75 insertions, 6 deletions
diff --git a/src/ninep/ctl.zig b/src/ninep/ctl.zig
index 5ed03c58..05f93ac5 100644
--- a/src/ninep/ctl.zig
+++ b/src/ninep/ctl.zig
@@ -983,6 +983,7 @@ fn get(p: *Pardes, pane: *Pane, failed: *anyerror) u16 {
panes.File.setContent(p, f, bytes);
f.saved_revision = f.revision;
f.saved_hash = std.hash.Wyhash.hash(0, f.content);
+ f.saved_len = f.content.len;
f.disk_newer = null;
f.disk_gone = false;
if (discarded) {
diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig
index 3fad1fae..af8bb52c 100644
--- a/src/ninep/pane.zig
+++ b/src/ninep/pane.zig
@@ -149,12 +149,62 @@ pub fn clip(n: usize) u32 {
return std.math.cast(u32, n) orelse std.math.maxInt(u32);
}
+/// The file's line index when it describes `text` (built by a render or a
+/// look), else null: with it a row is a binary search, not a count of every
+/// newline from the top, which a bulk write paid on each flush, twice over.
+fn lineIndex(pane: *const Pane, text: []const u8) ?[]const usize {
+ const f = if (pane.file) |*file| file else return null;
+ if (f.line_starts.len == 0 or f.content.ptr != text.ptr or f.content.len != text.len) return null;
+ return f.line_starts;
+}
+
+/// The line `off` is on: the last start at or before it.
+fn rowAt(starts: []const usize, off: usize) usize {
+ const after = std.sort.upperBound(usize, starts, off, struct {
+ fn order(key: usize, item: usize) std.math.Order {
+ return std.math.order(key, item);
+ }
+ }.order);
+ return after -| 1;
+}
+
+fn lineEnd(starts: []const usize, text: []const u8, row: usize) usize {
+ return if (row + 1 < starts.len) starts[row + 1] - 1 else text.len;
+}
+
+/// `modal.runeOffsetAt`, through the line index when there is one.
+fn runeOffsetAt(pane: *const Pane, text: []const u8, c: modal.Cursor) usize {
+ const starts = lineIndex(pane, text) orelse return modal.runeOffsetAt(text, c);
+ const row = @min(c.row, starts.len - 1);
+ const s = starts[row];
+ const e = lineEnd(starts, text, row);
+ return s + modal.runeStart(text[s..e], @min(c.col, e - s));
+}
+
+/// `modal.runePositionAt`, through the line index when there is one.
+fn runePositionAt(pane: *const Pane, text: []const u8, off: usize) modal.Cursor {
+ const starts = lineIndex(pane, text) orelse return modal.runePositionAt(text, off);
+ const bounded = modal.runeStart(text, off);
+ const row = rowAt(starts, bounded);
+ return .{ .row = row, .col = bounded - starts[row] };
+}
+
+/// `modal.positionAt`, through the line index when there is one.
+fn positionAt(pane: *const Pane, text: []const u8, off: usize) modal.Cursor {
+ const starts = lineIndex(pane, text) orelse return modal.positionAt(text, off);
+ const bounded = @min(off, text.len);
+ const row = rowAt(starts, bounded);
+ const s = starts[row];
+ const e = lineEnd(starts, text, row);
+ return .{ .row = row, .col = modal.graphemeStart(text[s..e], @min(bounded - s, e - s)) };
+}
+
pub fn dotOf(pane: *Pane) State.Range {
const text = bodyOf(pane);
// In runes, as every address is (modal.runeStart).
- const head = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) });
+ const head = runeOffsetAt(pane, text, .{ .row = @intCast(@max(0, pane.body.cur_row)), .col = @intCast(@max(0, pane.body.cur_col)) });
if (!pane.body.vsel.active) return .{ .q0 = clip(head), .q1 = clip(head) };
- const anchor = modal.runeOffsetAt(text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) });
+ const anchor = runeOffsetAt(pane, text, .{ .row = @intCast(@max(0, pane.body.vsel.row)), .col = @intCast(@max(0, pane.body.vsel.col)) });
var hi = @max(head, anchor);
if (hi < text.len) hi = modal.nextRune(text, hi);
return .{ .q0 = clip(@min(head, anchor)), .q1 = clip(hi) };
@@ -164,9 +214,9 @@ pub fn setDot(pane: *Pane, r: State.Range) void {
const text = bodyOf(pane);
const q0 = @min(@as(usize, r.q0), text.len);
const q1 = @max(q0, @min(@as(usize, r.q1), text.len));
- const a = modal.runePositionAt(text, q0);
+ const a = runePositionAt(pane, text, q0);
pane.body.vsel = .{ .active = q1 > q0, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
- const h = modal.runePositionAt(text, if (q1 > q0) modal.prevRune(text, q1) else q0);
+ const h = runePositionAt(pane, text, if (q1 > q0) modal.prevRune(text, q1) else q0);
pane.body.cur_row = @intCast(h.row);
pane.body.cur_col = @intCast(h.col);
pane.body.cur_pinned = true;
@@ -177,7 +227,7 @@ pub fn setDot(pane: *Pane, r: State.Range) void {
pub fn showOffset(pane: *Pane, off: usize) void {
const text = bodyOf(pane);
- const c = modal.positionAt(text, @min(off, text.len));
+ const c = positionAt(pane, text, @min(off, text.len));
pane.body.cur_row = @intCast(c.row);
pane.body.cur_col = @intCast(c.col);
pane.body.cur_pinned = true;
@@ -229,10 +279,28 @@ pub fn spliceBody(p: *Pardes, pane: *Pane, q0: usize, q1: usize, bytes: []const
const join = pane.fs.joined == f.revision;
pane.fs.joined = null;
if (!pane.fs.nomark and !join) panes.File.pushUndo(p, pane);
- panes.File.setContent(p, f, new);
+ panes.File.setContentSpan(p, f, new, .{ .head = lo, .tail = f.content.len - hi });
return take;
}
+test "rows through the line index are the ones counting newlines gives, at every offset" {
+ const p = try withFile(testing.allocator, "héllo\nwo\u{301}rld\n\n last line é\ntail");
+ defer p.deinit();
+ const pane = p.panes[0].?;
+ const f = &pane.file.?;
+ _ = try panes.File.lineIndex(p.gpa, f);
+ const text = bodyOf(pane);
+ try testing.expect(lineIndex(pane, text) != null);
+ for (0..text.len + 2) |off| {
+ try testing.expectEqual(modal.positionAt(text, off), positionAt(pane, text, off));
+ try testing.expectEqual(modal.runePositionAt(text, off), runePositionAt(pane, text, off));
+ }
+ for (0..8) |row| for (0..16) |col| {
+ const c: modal.Cursor = .{ .row = row, .col = col };
+ try testing.expectEqual(modal.runeOffsetAt(text, c), runeOffsetAt(pane, text, c));
+ };
+}
+
test "a terminal's body stats as long as it reads" {
const gpa = testing.allocator;
const p = try pardes.Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });