diff options
Diffstat (limited to 'src/Text.zig')
| -rw-r--r-- | src/Text.zig | 68 |
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 |
