From 23f430573efe4f6ef55b0180aa0d742ff4eca1d9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 00:01:57 -0300 Subject: Make a pane's tag a Text edited with the body's own keys The tag was a fixed buffer with its own one-line editor, a cursor measured across the path, and a hijack of the body's mode while it was typed into. It is now the pane's second Text: its own characters after the computed prefix (path, dirty marker, PDF page), which is never stored in it, so a rename or a dirty marker never moves its cursor. Normal and insert mode are the body's, undo included; `:` moves the keyboard between body and tag, the tag remembers its cursor and starts on Save, and executing a word from it hands the keyboard back. The mouse still sees the prefix: a sweep selects across it and a click on the path drafts a new name. Co-Authored-By: Claude Opus 5.5 --- src/Text.zig | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 11 deletions(-) (limited to 'src/Text.zig') 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; +} -- cgit v1.3