summaryrefslogtreecommitdiff
path: root/src/edit.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/edit.zig')
-rw-r--r--src/edit.zig127
1 files changed, 102 insertions, 25 deletions
diff --git a/src/edit.zig b/src/edit.zig
index 14d8b7ed..8f6f09c6 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -344,28 +344,20 @@ pub fn exitInsert(p: *Pardes, t: *Text) void {
t.reg_wait = false;
t.msel.active = false;
t.normal.clear();
- if (t.append_at) |aa| {
- t.append_at = null;
- const text = flatSurface(p, t) catch return;
- const gap = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
- const a_off = modal.offsetAt(text, .{ .row = @intCast(@max(0, aa.row)), .col = @intCast(@max(0, aa.col)) });
- const back = @max(a_off, modal.prevGrapheme(text, gap));
- const bc = modal.positionAt(text, back);
- t.cur_row = @intCast(bc.row);
- t.cur_col = @intCast(bc.col);
- t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false };
- t.cur_pinned = true;
- t.show();
- for (t.sels[0..t.nsel]) |*s| {
- const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) });
- const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap);
- const bp = modal.positionAt(text, b2);
- s.row = @intCast(bp.row);
- s.col = @intCast(bp.col);
- s.arow = s.row;
- s.acol = s.col;
- }
+ if (!t.restore_cursor) return;
+ t.restore_cursor = false;
+ // helix enter_normal_mode: every range, from its start to the char
+ // before its end when it points forward, else to its end
+ const expl = (t.vsel.active and t.vsel.explicit) or t.msel.active;
+ const text = flatSurface(p, t) catch return;
+ var rs: [Text.max_selections]modal.Selection = undefined;
+ const got = t.ranges(text, 0, &rs);
+ for (rs[0..got.n]) |*r| {
+ const from = @min(r.anchor, r.head);
+ const to = @max(r.anchor, r.head);
+ r.* = .{ .anchor = from, .head = if (r.head > r.anchor) @max(from, modal.prevGrapheme(text, to)) else to };
}
+ t.setRanges(text, rs[0..got.n], &.{}, got.pri, expl);
}
pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 {
@@ -518,7 +510,7 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
// `u` undoes the whole session and restores what was selected
pushUndo(p, t);
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.sticky_col = -1;
const text = flatSurface(p, t) catch {
t.mode = .insert;
@@ -544,12 +536,14 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
const lo_col = if (b) |bb| bb.lo_col else t.cur_col;
const hi_row = if (b) |bb| bb.hi_row else t.cur_row;
const hi_col = if (b) |bb| bb.hi_col else t.cur_col;
- t.append_at = .{ .row = lo_row, .col = lo_col };
+ t.restore_cursor = true;
const gap = modal.nextGrapheme(text, modal.offsetAt(text, .{ .row = @intCast(@max(0, hi_row)), .col = @intCast(@max(0, hi_col)) }));
const gc = modal.positionAt(text, gap);
t.cur_row = @intCast(gc.row);
t.cur_col = @intCast(gc.col);
- t.vsel = .{ .active = b != null, .row = lo_row, .col = lo_col, .explicit = false };
+ // helix append_mode: the range reaches one past its end, which
+ // is where typing goes; a bare cursor becomes a range too
+ t.vsel = .{ .active = true, .row = lo_row, .col = lo_col, .explicit = false };
t.cur_pinned = true;
},
.line_start => {
@@ -634,6 +628,72 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
t.reg_wait = true;
return;
}
+ // helix maps every range through each insert-mode edit (Range::map):
+ // the edits below work on the bare cursor, and the range the cursor
+ // heads is carried through what they did to the text afterwards
+ if (!t.vsel.active) return insertEdit(p, t, key);
+ const old = flatSurface(p, t) catch return insertEdit(p, t, key);
+ const r = t.primaryRange(old, 0);
+ const c = modal.selectionCursor(old, r);
+ const old_len = old.len;
+ const explicit = t.vsel.explicit;
+ t.vsel.active = false;
+ insertEdit(p, t, key);
+ if (t.mode != .insert) return;
+ const new = flatSurface(p, t) catch return;
+ const c2 = modal.offsetAt(new, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ // every insert-mode edit happens at the cursor: an insertion there, or
+ // a deletion that ends there (backward) or starts there (forward)
+ if (key.cp == Key.enter) {
+ // helix insert_newline: an appending range stretches by what the
+ // cursor moved, any other slides by it
+ const d = @as(isize, @intCast(c2)) - @as(isize, @intCast(c));
+ const head: usize = @intCast(@as(isize, @intCast(r.head)) + d);
+ const anchor: usize = if (c > r.anchor) r.anchor else @intCast(@as(isize, @intCast(r.anchor)) + d);
+ return setInsertRange(t, new, .{ .anchor = anchor, .head = head }, explicit);
+ }
+ var at: usize = c;
+ var del: usize = 0;
+ var ins: usize = 0;
+ if (new.len > old_len and c2 == c + (new.len - old_len)) {
+ ins = new.len - old_len;
+ } else if (new.len < old_len and c2 + (old_len - new.len) == c) {
+ at = c2;
+ del = old_len - new.len;
+ } else if (new.len < old_len and c2 == c) {
+ del = old_len - new.len;
+ } else if (new.len != old_len or c2 != c) {
+ return; // a motion: the range collapses onto the cursor (helix)
+ }
+ // helix Assoc: the head sticks after an insertion at it when the range
+ // points backward or is empty, the anchor when it points forward
+ const fwd = r.anchor < r.head;
+ setInsertRange(t, new, .{
+ .anchor = mapThroughEdit(r.anchor, at, del, ins, !fwd and r.anchor != r.head),
+ .head = mapThroughEdit(r.head, at, del, ins, fwd),
+ }, explicit);
+}
+
+fn setInsertRange(t: *Text, new: []const u8, mapped: modal.Selection, explicit: bool) void {
+ const cells = Text.rangeCells(new, mapped);
+ const cc = modal.positionAt(new, cells.cur);
+ const ac = modal.positionAt(new, cells.anc);
+ t.cur_row = @intCast(cc.row);
+ t.cur_col = @intCast(cc.col);
+ t.vsel = .{ .active = cells.anc != cells.cur, .row = @intCast(ac.row), .col = @intCast(ac.col), .explicit = explicit };
+}
+
+/// A position through one edit at `at` that deleted `del` bytes and put
+/// `ins` in (one of the two is 0). A position exactly at an insertion stays
+/// before it when `before` is set, else moves past it.
+fn mapThroughEdit(pos: usize, at: usize, del: usize, ins: usize, before: bool) usize {
+ if (pos < at) return pos;
+ if (del > 0) return if (pos < at + del) at else pos - del;
+ if (pos == at and before) return pos;
+ return pos + ins;
+}
+
+fn insertEdit(p: *Pardes, t: *Text, key: Key) void {
t.vsel.active = false;
if (!t.cur_pinned) pinPaneCursor(p, t);
_ = clampFileCursor(t);
@@ -748,12 +808,29 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
switch (key.cp) {
Key.enter => {
const line = modal.lineSlice(text, c.row);
+ const before = line[0..@min(c.col, line.len)];
+ // helix insert_newline: after nothing but blanks, the line
+ // itself moves down, blanks and all; else the blanks before the
+ // cursor go and the new line takes the indent
+ if (std.mem.indexOfNone(u8, before, " \t") == null) {
+ const new = modal.insertAt(p.gpa, text, .{ .row = c.row, .col = 0 }, "\n") catch return;
+ setEditText(p, t, new);
+ t.cur_row += 1;
+ t.cur_pinned = true;
+ t.show();
+ return;
+ }
const indent = modal.newlineIndentWidth(line, c.col);
const arena = p.scratch.allocator();
const block = arena.alloc(u8, 1 + indent) catch return;
block[0] = '\n';
@memset(block[1..], ' ');
- const new = modal.insertAt(p.gpa, text, c, block) catch return;
+ // a tag keeps its blanks: they part its words, and the last
+ // may be its read-only prefix's
+ const kept = if (t.what == .body) std.mem.trimEnd(u8, before, " \t").len else c.col;
+ const cut = modal.deleteSpan(p.gpa, text, .{ .row = c.row, .col = kept }, c) catch return;
+ defer p.gpa.free(cut);
+ const new = modal.insertAt(p.gpa, cut, .{ .row = c.row, .col = kept }, block) catch return;
setEditText(p, t, new);
t.cur_row += 1;
t.cur_col = @intCast(indent);