summaryrefslogtreecommitdiff
path: root/src/Text.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text.zig')
-rw-r--r--src/Text.zig79
1 files changed, 68 insertions, 11 deletions
diff --git a/src/Text.zig b/src/Text.zig
index aaa6dc88..3e48a42e 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -1,11 +1,11 @@
//! One editable text, acme's Text (editors/acme/dat.h:171-190, text.c): the
//! cursor, the selections and the modal state that edit it, and the undo of
-//! an edit buffer. A pane's body is one, and so is the answer typed into a
-//! pane's prompt. Where a body's characters come from -- a file's content, a
-//! terminal's edit overlay, a PDF's text -- is the window's business
-//! (edit.zig's editText and flatSurface), the way acme's Text reads its File;
-//! every other text owns its characters. Everything here works on the flat
-//! text it is handed.
+//! an edit buffer. A pane's body is one, its tag is another, and so is the
+//! answer typed into a pane's prompt. Where a body's characters come from --
+//! a file's content, a terminal's edit overlay, a PDF's text -- is the
+//! window's business (edit.zig's editText and flatSurface), the way acme's
+//! Text reads its File; every other text owns its characters. Everything
+//! here works on the flat text it is handed.
const std = @import("std");
const panes = @import("panes.zig");
const modal = @import("modal.zig");
@@ -19,7 +19,7 @@ pub const Mode = enum { normal, insert, tty };
/// Which text of its window this is, acme's `what`. It says where the
/// characters live and whether moving the cursor scrolls a body.
-pub const What = enum { body, input };
+pub const What = enum { body, tag, input };
/// A modal line selection (helix `x`): whole rows [r0, r1], absolute.
pub const LineSel = struct {
@@ -49,7 +49,8 @@ pub const SelRange = struct {
what: What = .body,
/// The characters of a text that holds its own (gpa-owned): everything but
-/// a body, which reads its pane's.
+/// a body, which reads its pane's. A tag nobody has changed has none yet and
+/// shows the default its pane's kind starts with.
own: ?[]u8 = null,
mode: Mode = .normal,
cur_pinned: bool = false,
@@ -77,12 +78,14 @@ ed_redo_len: usize = 0,
pub fn pane(t: *Text) *Pane {
return switch (t.what) {
.body => @alignCast(@fieldParentPtr("body", t)),
+ .tag => @alignCast(@fieldParentPtr("tag", t)),
.input => @alignCast(@fieldParentPtr("input", t)),
};
}
/// Bring the cursor into view (acme's textshow). Only a body scrolls to it;
-/// a prompt's answer is drawn whole.
+/// the tagline follows its caret where it is drawn, and a prompt's answer is
+/// drawn whole.
pub fn show(t: *Text) void {
if (t.what == .body) t.pane().ensureCursorVisible();
}
@@ -273,8 +276,8 @@ pub fn multiSelAction(t: *Text, text: []const u8, kind: modal.Normal.Multi, cnt:
return setRanges(t, text, out[0..m], &.{}, @min(got.pri, m - 1), expl);
}
if (kind == .rotate_forward or kind == .rotate_backward) {
- const step = cnt % n;
- const pri = if (kind == .rotate_forward) (got.pri + step) % n else (got.pri + (n - step)) % n;
+ const turn = cnt % n;
+ const pri = if (kind == .rotate_forward) (got.pri + turn) % n else (got.pri + (n - turn)) % n;
return setRanges(t, text, rs[0..n], &.{}, pri, expl);
}
if (kind == .merge) {
@@ -510,6 +513,8 @@ pub fn visualMove(
/// Ctrl-d/u: scroll half a page AND move the cursor by the same rows
pub fn halfPageMove(t: *Text, text: []const u8, range: modal.Selection, down: bool) void {
+ // a view to move through is a body's; a tag or an answer is all shown
+ if (t.what != .body) return;
const half: i32 = @max(1, @divTrunc(@as(i32, t.pane().rows), 2));
t.pane().scrollBy(if (down) half else -half);
verticalMove(t, text, range, down, @intCast(half));
@@ -518,6 +523,8 @@ pub fn halfPageMove(t: *Text, text: []const u8, range: modal.Selection, down: bo
/// helix `scroll` without cursor sync (Ctrl-f/b, PgUp/PgDn, zj/zk): shift
/// the view, then snap a fallen-out cursor to the near scrolloff edge, col 0
pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta: i32) void {
+ // a view to move through is a body's; a tag or an answer is all shown
+ if (t.what != .body) return;
const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, t.pane().rows) - 1, 2));
t.pane().scrollBy(delta);
const top = t.pane().scroll();
@@ -540,6 +547,8 @@ pub fn scrollViewMove(t: *Text, text: []const u8, range: modal.Selection, delta:
/// gt/gc/gb: view-relative rows, col 0, scrolloff clamped (helix goto_window)
pub fn gotoWindow(t: *Text, text: []const u8, range: modal.Selection, which: enum { top, center, bottom }, cnt: usize) void {
+ // a view to move through is a body's; a tag or an answer is all shown
+ if (t.what != .body) return;
const margin: i32 = @min(config.scroll_off, @divTrunc(@as(i32, t.pane().rows) - 1, 2));
const top = t.pane().scroll();
const last_row: i32 = @intCast(File.textLineCount(t, text) - 1);
@@ -623,3 +632,51 @@ pub fn clampCursor(t: *Text, text: []const u8, row0: i32) void {
t.msel.active = false;
t.show();
}
+
+fn pushHistory(gpa: std.mem.Allocator, slots: []Terminal.Snapshot, len: *usize, value: Terminal.Snapshot) void {
+ if (len.* == slots.len) {
+ if (slots[0].ovl) |overlay| gpa.free(overlay.text);
+ std.mem.copyForwards(Terminal.Snapshot, slots[0 .. slots.len - 1], slots[1..]);
+ len.* -= 1;
+ }
+ slots[len.*] = value;
+ len.* += 1;
+}
+
+/// Record an edit buffer (a terminal's overlay, a tag's own text) as it
+/// stands before an edit, once per change of it, and forget what could have
+/// been redone. Null is a buffer not made yet.
+pub fn remember(t: *Text, gpa: std.mem.Allocator, current: ?Terminal.EditBuffer) void {
+ if (t.ed_undo_len > 0) {
+ const top = t.ed_undo[t.ed_undo_len - 1];
+ const same = if (top.ovl) |overlay| if (current) |now| overlay.row == now.row and
+ overlay.rows == now.rows and std.mem.eql(u8, overlay.text, now.text) else false else current == null;
+ if (same) return;
+ }
+ const copy: ?Terminal.EditBuffer = if (current) |now| .{ .row = now.row, .rows = now.rows, .text = gpa.dupe(u8, now.text) catch return } else null;
+ pushHistory(gpa, &t.ed_undo, &t.ed_undo_len, .{ .ovl = copy, .cur_row = t.cur_row, .cur_col = t.cur_col, .vsel = t.vsel });
+ for (t.ed_redo[0..t.ed_redo_len]) |item| if (item.ovl) |overlay| gpa.free(overlay.text);
+ t.ed_redo_len = 0;
+}
+
+/// Undo (back) or redo one edit of an edit buffer: `current` goes on the
+/// other history and the state to return to comes back, its cursor and
+/// selection already restored here. The caller installs its buffer, which
+/// it then owns. Null when there is nothing to step to.
+pub fn step(t: *Text, gpa: std.mem.Allocator, current: ?Terminal.EditBuffer, back: bool) ?Terminal.Snapshot {
+ const from, const from_len, const to, const to_len = if (back)
+ .{ &t.ed_undo, &t.ed_undo_len, &t.ed_redo, &t.ed_redo_len }
+ else
+ .{ &t.ed_redo, &t.ed_redo_len, &t.ed_undo, &t.ed_undo_len };
+ if (from_len.* == 0) return null;
+ const copy: ?Terminal.EditBuffer = if (current) |now| .{ .row = now.row, .rows = now.rows, .text = gpa.dupe(u8, now.text) catch return null } else null;
+ pushHistory(gpa, to, to_len, .{ .ovl = copy, .cur_row = t.cur_row, .cur_col = t.cur_col, .vsel = t.vsel });
+ from_len.* -= 1;
+ const back_to = from[from_len.*];
+ t.cur_row = back_to.cur_row;
+ t.cur_col = back_to.cur_col;
+ t.cur_pinned = true;
+ t.vsel = back_to.vsel;
+ t.msel.active = false;
+ return back_to;
+}