summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Output.zig2
-rw-r--r--src/Terminal.zig8
-rw-r--r--src/Text.zig4
-rw-r--r--src/edit.zig127
-rw-r--r--src/look.zig2
-rw-r--r--src/mouse.zig4
-rw-r--r--src/normal.zig6
-rw-r--r--src/tagline.zig4
8 files changed, 116 insertions, 41 deletions
diff --git a/src/Output.zig b/src/Output.zig
index 6e1765a7..201b5491 100644
--- a/src/Output.zig
+++ b/src/Output.zig
@@ -749,7 +749,7 @@ pub fn resetBody(p: *Pardes, pane: *Pane) void {
pane.body.select = false;
pane.sel = @splat(.{});
pane.body.sticky_col = -1;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.normal.clear();
pane.look_at = null;
if (p.look_hover_wait) |wait| if (wait.serial == pane.serial) {
diff --git a/src/Terminal.zig b/src/Terminal.zig
index e204b44c..e3378007 100644
--- a/src/Terminal.zig
+++ b/src/Terminal.zig
@@ -717,7 +717,7 @@ pub fn feedOutput(p: *Pardes, pane: *Pane, bytes: []const u8) void {
if (comptime !enabled) return;
if (pane.terminal == null) return;
const has_positions = pane.ovl != null or pane.body.cur_pinned or pane.body.vsel.active or
- pane.body.msel.active or pane.body.nsel > 0 or pane.body.append_at != null or pane.look_at != null or
+ pane.body.msel.active or pane.body.nsel > 0 or pane.look_at != null or
pane.body.ed_undo_len > 0 or pane.body.ed_redo_len > 0 or pointerRowCount(pane) > 0;
// Sticky bottom, as every other terminal does it: output drags the
// viewport down only when the viewport was already on the last row, so
@@ -843,10 +843,6 @@ fn ingestWithPositions(pane: *Pane, bytes: []const u8) void {
extra[nextra + 1] = &selection.arow;
nextra += 2;
}
- if (pane.body.append_at) |*at| {
- extra[nextra] = &at.row;
- nextra += 1;
- }
if (pane.look_at) |*at| {
extra[nextra] = &at.row;
nextra += 1;
@@ -1124,7 +1120,7 @@ pub fn enterTty(p: *Pardes, id: usize) void {
// keeps its modal endpoint for the next return to editor mode.
pane.body.cur_pinned = selected;
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
pane.body.normal.clear();
}
diff --git a/src/Text.zig b/src/Text.zig
index 7ad243a1..764b8a6a 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -65,7 +65,9 @@ select: bool = false,
/// sticky goal column for j/k runs (helix old_visual_position): any
/// non-vertical range write resets it to -1.
sticky_col: i32 = -1,
-append_at: ?struct { row: i32, col: i32 } = null,
+/// The insert session began with `a`: on Esc every range gives back the
+/// one character it was stretched by (helix restore_cursor).
+restore_cursor: bool = false,
/// insert `Ctrl-r` was pressed: the next key names the register to insert
reg_wait: bool = false,
normal: modal.Normal.State = .{},
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);
diff --git a/src/look.zig b/src/look.zig
index 5e2d6068..30fb0dab 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -1016,7 +1016,7 @@ pub fn clearNavigationSelection(p: *Pardes, pane: *Pane) void {
pane.body.nsel = 0;
pane.body.select = false;
pane.body.normal.clear();
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
for (0..pane.sel.len) |slot| pane.clearPointerSelection(slot);
pane.sel = @splat(.{});
diff --git a/src/mouse.zig b/src/mouse.zig
index 91ec41a6..d1c32b40 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -747,7 +747,7 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
pane.body.vsel.active = false;
pane.body.nsel = 0;
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
pane.body.normal.clear();
edit.setYank(p, text);
@@ -859,7 +859,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
pane.body.vsel.active = false;
pane.body.nsel = 0; // a click says WHERE the one cursor is
pane.body.select = false;
- pane.body.append_at = null;
+ pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
if (s.ctrl) p.lspRequest(s.id, .definition, "");
}
diff --git a/src/normal.zig b/src/normal.zig
index f5e868a4..53e416fd 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -149,7 +149,7 @@ const KeyState = struct {
normal: modal.Normal.State,
find_op: u8,
find_ch: u21,
- append_at: @FieldType(Text, "append_at"),
+ restore_cursor: bool,
reg_wait: bool,
fn of(t: *Text) KeyState {
@@ -159,7 +159,7 @@ const KeyState = struct {
.normal = t.normal,
.find_op = t.find_op,
.find_ch = t.find_ch,
- .append_at = t.append_at,
+ .restore_cursor = t.restore_cursor,
.reg_wait = t.reg_wait,
};
}
@@ -170,7 +170,7 @@ const KeyState = struct {
t.normal = s.normal;
t.find_op = s.find_op;
t.find_ch = s.find_ch;
- t.append_at = s.append_at;
+ t.restore_cursor = s.restore_cursor;
t.reg_wait = s.reg_wait;
}
};
diff --git a/src/tagline.zig b/src/tagline.zig
index 14bf5677..1c060a66 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -219,7 +219,7 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void {
t.msel.active = false;
t.nsel = 0;
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.sticky_col = -1;
t.mode = .normal;
t.normal.clear();
@@ -325,7 +325,7 @@ pub fn enterHeader(p: *Pardes, column: ?usize) void {
t.msel.active = false;
t.nsel = 0;
t.select = false;
- t.append_at = null;
+ t.restore_cursor = false;
t.mode = .normal;
t.normal.clear();
}