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/edit.zig | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-) (limited to 'src/edit.zig') diff --git a/src/edit.zig b/src/edit.zig index 5249a64c..02e138e8 100644 --- a/src/edit.zig +++ b/src/edit.zig @@ -96,18 +96,12 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void { if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split); return; } - if (pane.tag_edit) { - const text = p.tagText(p.scratch.allocator(), pane) catch return; - const range: @TypeOf(Pardes.tagSelBounds(pane)) = if (pane.tag_sel) Pardes.tagSelBounds(pane) else .{ .lo = pane.tag_col, .hi = pane.tag_col }; - const hi = if (pane.tag_sel) modal.nextGrapheme(text, range.hi) else range.hi; - _ = p.replaceTagText(pane, range.lo, hi, bytes); - return; - } - if (pane.isTerminal() and pane.body.mode == .tty) return typeToTty(p, id, pane, bytes); + const t = if (pane.focus == .tag) &pane.tag else &pane.body; + if (t.what == .body and pane.isTerminal() and pane.body.mode == .tty) return typeToTty(p, id, pane, bytes); switch (if (req) |r| r.mode else .after) { - .after => pasteText(p, &pane.body, bytes, false, 1), - .before => pasteText(p, &pane.body, bytes, true, 1), - .replace => replaceWithText(p, &pane.body, bytes), + .after => pasteText(p, t, bytes, false, 1), + .before => pasteText(p, t, bytes, true, 1), + .replace => replaceWithText(p, t, bytes), } } @@ -301,7 +295,7 @@ pub fn exitInsert(p: *Pardes, t: *Text) void { pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 { const arena = p.scratch.allocator(); if (t.what != .body) { - const text = t.own orelse ""; + const text = try flatSurface(p, t); const lines = try arena.alloc([]const u8, std.mem.count(u8, text, "\n") + 1); var it = std.mem.splitScalar(u8, text, '\n'); for (lines) |*line| line.* = it.next().?; @@ -346,6 +340,7 @@ pub fn pinPaneCursor(p: *Pardes, t: *Text) void { } pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 { + if (t.what == .tag) return Pardes.curTail(t.pane()); if (t.what != .body) return t.own orelse ""; const pane = t.pane(); if (pane.file) |f| return f.content; @@ -367,6 +362,10 @@ pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize { /// Route shared edit operations to the characters `t` edits: its own, or /// for a body its file's content or its terminal's overlay. fn editText(p: *Pardes, t: *Text, lo: i32, hi: i32, col: i32) ?panes.EditText { + // A tag nobody has changed shows its default; the first edit makes that + // default its own text. + if (t.what == .tag and t.own == null) + t.own = p.gpa.dupe(u8, Pardes.defaultTail(t.pane())) catch return null; if (t.what != .body) return .{ .text = t.own orelse &.{}, .row0 = 0 }; const pane = t.pane(); if (pane.file) |f| return .{ .text = f.content, .row0 = 0 }; @@ -1544,14 +1543,19 @@ pub fn surroundReplace(p: *Pardes, t: *Text, from: u21, to: u21) void { pub fn pushUndo(p: *Pardes, t: *Text) void { // one keystroke, one undo step — even when it edited at ten cursors. if (p.multi_on and !p.multi_first) return; - // a prompt's answer is typed once and gone; it keeps no history - if (t.what != .body) return; + switch (t.what) { + .body => {}, + .tag => return t.remember(p.gpa, if (t.own) |own| .{ .text = own } else null), + // a prompt's answer is typed once and gone; it keeps no history + .input => return, + } const pane = t.pane(); if (pane.file != null) return panes.File.pushUndo(p, pane); panes.Terminal.pushUndo(p, pane); } pub fn doUndo(p: *Pardes, t: *Text) void { + if (t.what == .tag) return stepOwn(p, t, true); if (t.what != .body) return; const pane = t.pane(); if (pane.file != null) return panes.File.undo(p, pane); @@ -1559,12 +1563,21 @@ pub fn doUndo(p: *Pardes, t: *Text) void { } pub fn doRedo(p: *Pardes, t: *Text) void { + if (t.what == .tag) return stepOwn(p, t, false); if (t.what != .body) return; const pane = t.pane(); if (pane.file != null) return panes.File.redo(p, pane); panes.Terminal.redo(p, pane); } +/// Undo or redo an edit of a text's own characters; stepping back past its +/// first edit gives a tag its default again. +fn stepOwn(p: *Pardes, t: *Text, back: bool) void { + const to = t.step(p.gpa, if (t.own) |own| .{ .text = own } else null, back) orelse return; + if (t.own) |own| p.gpa.free(own); + t.own = if (to.ovl) |buffer| buffer.text else null; +} + const PointerTextSelection = struct { sel: Pane.Sel, on_tag: bool, -- cgit v1.3