summaryrefslogtreecommitdiff
path: root/src/Text.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text.zig')
-rw-r--r--src/Text.zig68
1 files changed, 61 insertions, 7 deletions
diff --git a/src/Text.zig b/src/Text.zig
index 43dd8a95..ab23bf8e 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -11,6 +11,7 @@ const std = @import("std");
const panes = @import("panes.zig");
const modal = @import("modal.zig");
const config = @import("config.zig");
+const memory = @import("memory.zig");
const Pane = panes.Pane;
const File = panes.File;
const Terminal = panes.Terminal;
@@ -36,7 +37,7 @@ pub const CharSel = struct {
explicit: bool = false,
};
-pub const max_selections = 64;
+pub const max_selections = memory.limits.selections;
pub const SelRange = struct {
row: i32,
@@ -59,13 +60,26 @@ cur_row: i32 = 0,
cur_col: i32 = 0,
msel: LineSel = .{},
vsel: CharSel = .{},
-sels: [max_selections - 1]SelRange = undefined,
-nsel: u8 = 0,
+/// The ranges beside the primary, `nsel` of them: room for max_selections - 1
+/// made when a second range appears and given back when the selection is one
+/// range again, so a single cursor holds nothing here.
+sels: []SelRange = &.{},
+nsel: u16 = 0,
+/// What a column's or the workspace's tag allocates its ranges from; a text
+/// with a pane uses the pane's.
+gpa: ?std.mem.Allocator = null,
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,
+/// Where the last edit ended, a deletion's start or an insertion's end, in
+/// surface rows: for `g.` (helix last_edit_pos).
+last_edit: ?struct { row: i32, col: i32 } = null,
+/// insert `Ctrl-r` was pressed: the next key names the register to insert
+reg_wait: bool = false,
normal: modal.Normal.State = .{},
/// last f/F/t/T motion, for Alt-. repeat
find_op: u8 = 0,
@@ -93,10 +107,39 @@ pub fn pane(t: *Text) ?*Pane {
/// Free what a text owns: its characters and the buffers its undo keeps.
pub fn deinit(t: *Text, gpa: std.mem.Allocator) void {
+ if (t.sels.len > 0) gpa.free(t.sels);
if (t.own) |own| gpa.free(own);
for (t.ed_undo[0..t.ed_undo_len]) |item| if (item.ovl) |overlay| gpa.free(overlay.text);
for (t.ed_redo[0..t.ed_redo_len]) |item| if (item.ovl) |overlay| gpa.free(overlay.text);
- t.* = .{ .what = t.what };
+ t.* = .{ .what = t.what, .gpa = t.gpa };
+}
+
+fn allocator(t: *Text) ?std.mem.Allocator {
+ return if (t.pane()) |w| w.gpa else t.gpa;
+}
+
+/// Room for the ranges beside the primary; false when there is none to be had.
+pub fn reserveSels(t: *Text) bool {
+ if (t.sels.len > 0) return true;
+ const gpa = t.allocator() orelse return false;
+ t.sels = gpa.alloc(SelRange, max_selections - 1) catch return false;
+ return true;
+}
+
+/// The ranges beside the primary as they were (a refused edit's undoing).
+pub fn restoreSels(t: *Text, kept: []const SelRange) void {
+ if (kept.len == 0) return t.dropSels();
+ if (!t.reserveSels()) return;
+ @memcpy(t.sels[0..kept.len], kept);
+ t.nsel = @intCast(kept.len);
+}
+
+/// Back to one range: the room for the others goes.
+pub fn dropSels(t: *Text) void {
+ t.nsel = 0;
+ if (t.sels.len == 0) return;
+ if (t.allocator()) |gpa| gpa.free(t.sels);
+ t.sels = &.{};
}
/// Bring the cursor into view (acme's textshow). Only a body scrolls to it;
@@ -118,7 +161,8 @@ pub fn fromModalCursor(t: *Text, c: modal.Cursor) void {
pub fn insertVerticalCursor(lines: []const []const u8, c: modal.Cursor, down: bool) modal.Cursor {
if (lines.len == 0) return c;
- const row = if (down) @min(c.row + 1, lines.len - 1) else c.row -| 1;
+ // A cursor past the lines (its text changed under it) moves from the last.
+ const row = if (down) @min(c.row + 1, lines.len - 1) else @min(c.row, lines.len) -| 1;
const target = lines[row];
if (target.len == 0) return .{ .row = row, .col = 0 };
const source = if (c.row < lines.len) lines[c.row] else "";
@@ -177,7 +221,7 @@ pub fn setRange(t: *Text, text: []const u8, row0: i32, r0: modal.Selection, expl
.explicit = explicit or t.select,
};
t.msel.active = false;
- t.nsel = 0; // writing ONE range means the selection IS that range
+ t.dropSels(); // writing ONE range means the selection IS that range
t.cur_pinned = true;
t.sticky_col = -1;
t.normal.clear();
@@ -254,6 +298,7 @@ pub fn setRanges(t: *Text, text: []const u8, in: []const modal.Selection, sticky
}
n = k + 1;
setRange(t, text, 0, r[pri], explicit);
+ if (n > 1 and !t.reserveSels()) return; // no room: the primary alone
t.sticky_col = st[pri];
var w: usize = 0;
for (r[0..n], 0..) |x, idx| {
@@ -323,6 +368,15 @@ pub fn multiSelAction(t: *Text, text: []const u8, kind: modal.Normal.Multi, cnt:
}
return setRanges(t, text, out[0..m], &.{}, pri, expl);
}
+ if (kind == .ensure_forward) {
+ // helix ensure_selections_forward: every range points forward
+ for (rs[0..n]) |*r| {
+ const from = @min(r.anchor, r.head);
+ r.head = @max(r.anchor, r.head);
+ r.anchor = from;
+ }
+ return setRanges(t, text, rs[0..n], &.{}, got.pri, expl);
+ }
if (kind == .split_newline) {
// helix selection::split_on_newline — one range per line the
// selection covers, the newlines themselves left out