summaryrefslogtreecommitdiff
path: root/src/edit.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 23:42:53 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit9584b0682b45f17aaa42157540947904d867777a (patch)
treefaac80b84c02e9611e9df9ff0e210c1d1c895a84 /src/edit.zig
parent95a3a1bb822e583988bfe0f312d0ef219b27a454 (diff)
downloadpardes-9584b0682b45f17aaa42157540947904d867777a.tar.gz
pardes-9584b0682b45f17aaa42157540947904d867777a.zip
Give prompts their own line to type into instead of the tag's tail
A search, pipe or Save prompt wrote its marker and answer onto the end of the tag, so the tag's cursor, its selection, its 9P writes and a prompt abandoned by a click all shared one buffer: the abandoned text stayed in the tag and a 9P write landed inside the prompt. The answer is now its own Text on the pane, typed in insert mode and shown on the notice band where it was already drawn, and the tag never holds any of it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/edit.zig')
-rw-r--r--src/edit.zig116
1 files changed, 75 insertions, 41 deletions
diff --git a/src/edit.zig b/src/edit.zig
index f1b381d6..5249a64c 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -84,12 +84,23 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
const pane = p.panes[id] orelse return;
if (req) |r| if (pane.serial != r.serial) return;
p.active = id;
+ if (pane.prompt != .none and pane.prompt != .del_side) {
+ // an answer is one line: a newline in it would only be submitting it
+ const line = p.scratch.allocator().alloc(u8, bytes.len) catch return;
+ var n: usize = 0;
+ for (bytes) |byte| if (byte != '\n' and byte != '\r') {
+ line[n] = byte;
+ n += 1;
+ };
+ if (n > 0) insertKey(p, &pane.input, .{ .cp = line[0], .text = line[0..n] });
+ 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);
- if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split);
return;
}
if (pane.isTerminal() and pane.body.mode == .tty) return typeToTty(p, id, pane, bytes);
@@ -274,7 +285,7 @@ pub fn exitInsert(p: *Pardes, t: *Text) void {
t.cur_col = @intCast(bc.col);
t.vsel = .{ .active = a_off != back, .row = aa.row, .col = aa.col, .explicit = false };
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
for (t.sels[0..t.nsel]) |*s| {
const sgap = modal.offsetAt(text, .{ .row = @intCast(@max(0, s.row)), .col = @intCast(@max(0, s.col)) });
const b2 = if (back == gap) sgap else modal.prevGrapheme(text, sgap);
@@ -288,8 +299,15 @@ pub fn exitInsert(p: *Pardes, t: *Text) void {
}
pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 {
- const pane = t.pane();
const arena = p.scratch.allocator();
+ if (t.what != .body) {
+ const text = t.own orelse "";
+ 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().?;
+ return lines;
+ }
+ const pane = t.pane();
if (pane.file) |*f| return panes.File.cursorLines(arena, pane, f);
if (pane.hasPdf()) {
if (comptime pdf_enabled) return panes.Pdf.textLines(&pane.pdf.?, p.pdf_gpa, arena);
@@ -299,9 +317,8 @@ pub fn paneCursorLines(p: *Pardes, t: *Text) ![]const []const u8 {
}
pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_col: i32) i32 {
- const pane = t.pane();
- if (pane.file != null)
- return panes.File.byteAtRowDisplay(pane, row, from_raw, display_col);
+ if (t.what == .body and t.pane().file != null)
+ return panes.File.byteAtRowDisplay(t.pane(), row, from_raw, display_col);
const lines = paneCursorLines(p, t) catch return @max(0, from_raw + display_col);
const local = row;
if (local < 0 or @as(usize, @intCast(local)) >= lines.len)
@@ -316,14 +333,20 @@ pub fn paneByteAtDisplay(p: *Pardes, t: *Text, row: i32, from_raw: i32, display_
/// Freeze the live terminal cursor into the modal coordinate space. The
/// emulator reports screen cells; editing state stores UTF-8 byte offsets.
pub fn pinPaneCursor(p: *Pardes, t: *Text) void {
- const pane = t.pane();
if (t.cur_pinned) return;
+ // Only a body has a live cursor of someone else's to freeze.
+ if (t.what != .body) {
+ t.cur_pinned = true;
+ return;
+ }
+ const pane = t.pane();
pane.pinCursor();
if (pane.file == null and !pane.hasPdf())
t.cur_col = paneByteAtDisplay(p, t, t.cur_row, 0, t.cur_col);
}
pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 {
+ if (t.what != .body) return t.own orelse "";
const pane = t.pane();
if (pane.file) |f| return f.content;
if (pane.hasPdf()) {
@@ -335,13 +358,16 @@ pub fn flatSurface(p: *Pardes, t: *Text) ![]const u8 {
}
pub fn paneWrapWidth(p: *const Pardes, t: *Text) usize {
+ if (t.what != .body) return 0;
const pane = t.pane();
if (pane.file == null) return 0;
return panes.File.wrapWidth(pane, p.settings.wrap);
}
-/// Route shared edit operations to a file's content or a terminal overlay.
+/// 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 {
+ 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 };
if (pane.image != null or pane.hasPdf()) return null;
@@ -358,6 +384,11 @@ fn editTextEol(p: *Pardes, t: *Text, b: Bounds) ?panes.EditText {
/// install a rewritten editable text (frees the old one)
fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
+ if (t.what != .body) {
+ if (t.own) |old| p.gpa.free(old);
+ t.own = new;
+ return;
+ }
const pane = t.pane();
if (pane.file) |*f| return panes.File.setContent(p, f, new);
panes.Terminal.setEditText(p, pane, new);
@@ -368,6 +399,7 @@ pub const InsertAt = enum { at, append, line_start, line_end, open_below, open_a
/// Files insert only inside existing rows and at grapheme boundaries.
/// Terminal overlays can materialize virtual rows/columns, so keep those.
pub fn clampFileCursor(t: *Text) bool {
+ if (t.what != .body) return false;
const pane = t.pane();
const file = if (pane.file) |*file| file else return false;
const old_row = t.cur_row;
@@ -380,8 +412,7 @@ pub fn clampFileCursor(t: *Text) bool {
}
pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
- const pane = t.pane();
- if (pane.hasPdf()) return;
+ if (t.what == .body and t.pane().hasPdf()) return;
pinPaneCursor(p, t);
_ = clampFileCursor(t);
// snapshot once per insert session (WITH the pre-insert selection) so
@@ -478,7 +509,7 @@ pub fn enterInsert(p: *Pardes, t: *Text, where: InsertAt, cnt: usize) void {
t.mode = .insert;
t.msel.active = false;
t.normal.clear();
- pane.ensureCursorVisible();
+ t.show();
}
pub fn handleInsert(p: *Pardes, t: *Text, key: Key) void {
@@ -487,7 +518,6 @@ pub fn handleInsert(p: *Pardes, t: *Text, key: Key) void {
}
pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
- const pane = t.pane();
// helix aliases: normalize to the base key and re-dispatch
if (hit(key, config.insert_backspace_alias)) return insertKey(p, t, .{ .cp = Key.backspace });
if (hit(key, config.insert_enter_alias)) return insertKey(p, t, .{ .cp = Key.enter });
@@ -507,7 +537,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
else => cur0,
};
t.fromModalCursor(nc);
- pane.ensureCursorVisible();
+ t.show();
return;
},
Key.page_up, Key.page_down => {
@@ -515,14 +545,15 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
// scroll + cursor snap as normal mode
const flat = flatSurface(p, t) catch return;
const range = t.primaryRange(flat, 0);
- const step: i32 = @intCast(@max(1, pane.rows));
+ if (t.what != .body) return;
+ const step: i32 = @intCast(@max(1, t.pane().rows));
t.scrollViewMove(flat, range, if (key.cp == Key.page_down) step else -step);
return;
},
Key.home => {
t.cur_col = 0;
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
},
Key.end => {
@@ -531,7 +562,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
const cur0 = t.toModalCursor();
t.cur_col = @intCast(panes.File.textLine(t, text, cur0.row).len);
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
},
else => {},
@@ -549,7 +580,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
} else t.cur_col += 1;
}
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
}
// helix insert-mode kills (word/line; deleteSpan is exclusive)
@@ -569,7 +600,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
t.cur_row = @as(i32, @intCast(fc.row)) + e2.row0;
t.cur_col = @intCast(fc.col);
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
}
if (hit(key, config.delete_word_forward)) {
@@ -592,7 +623,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
setEditText(p, t, new);
t.cur_col = 0;
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
}
if (hit(key, config.kill_to_line_end)) {
@@ -615,7 +646,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
t.cur_row += 1;
t.cur_col = @intCast(indent);
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
},
Key.backspace => {
if (t.cur_col > 0) {
@@ -625,7 +656,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
setEditText(p, t, new);
t.cur_col = @intCast(prev);
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
return;
}
// at col 0: eat the newline above (join up) — a terminal grows
@@ -640,7 +671,7 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
t.cur_row -= 1;
t.cur_col = @intCast(prevlen);
t.cur_pinned = true;
- pane.ensureCursorVisible();
+ t.show();
},
Key.delete => {
// delete forward; at line end eat the newline (join), which on
@@ -661,7 +692,8 @@ pub fn insertKey(p: *Pardes, t: *Text, key: Key) void {
Key.tab => {
const ln = modal.lineSlice(text, c.row);
if (!p.multi_on and c.col > 0 and c.col <= ln.len and ln[c.col - 1] == '.') dot: {
- const f = pane.file orelse break :dot;
+ if (t.what != .body) break :dot;
+ const f = t.pane().file orelse break :dot;
if (f.output != null or !lsp.speaks(f.path)) break :dot;
const seq = p.lsp_seq;
p.lspRequest(p.active, .completion, "");
@@ -684,7 +716,7 @@ pub fn insertTab(p: *Pardes, t: *Text) void {
setEditText(p, t, new);
t.cur_col += @intCast(pad);
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
}
const Bounds = struct { lo_row: i32, lo_col: i32, hi_row: i32, hi_col: i32 };
@@ -779,7 +811,7 @@ pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void {
t.cur_col = 0;
t.msel.active = false;
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
} else {
pushUndo(p, t);
const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return;
@@ -799,12 +831,11 @@ pub fn normalDelete(p: *Pardes, t: *Text, yank: bool) void {
}
pub fn normalYank(p: *Pardes, t: *Text) void {
- const pane = t.pane();
t.select = false;
- if (pane.rawPointerText(pane.sel[sel_slot])) |text| {
+ if (t.what == .body) if (t.pane().rawPointerText(t.pane().sel[sel_slot])) |text| {
setYank(p, text);
return;
- }
+ };
if (t.vsel.active) {
setYank(p, vselText(p, t));
return;
@@ -882,11 +913,10 @@ pub fn pasteText(p: *Pardes, t: *Text, y0: []const u8, before: bool, count: usiz
}
t.msel.active = false;
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
}
pub fn normalChange(p: *Pardes, t: *Text) void {
- const pane = t.pane();
t.select = false;
t.sticky_col = -1;
if (t.vsel.active) {
@@ -913,7 +943,7 @@ pub fn normalChange(p: *Pardes, t: *Text) void {
t.cur_pinned = true;
t.mode = .insert;
t.normal.clear();
- pane.ensureCursorVisible();
+ t.show();
return;
}
setYank(p, vselText(p, t));
@@ -974,7 +1004,7 @@ pub fn normalChange(p: *Pardes, t: *Text) void {
t.cur_pinned = true;
t.mode = .insert;
t.normal.clear();
- pane.ensureCursorVisible();
+ t.show();
} else {
pushUndo(p, t);
const eb = editTextEol(p, t, .{ .lo_row = t.cur_row, .lo_col = t.cur_col, .hi_row = t.cur_row, .hi_col = t.cur_col }) orelse return;
@@ -1075,7 +1105,7 @@ fn replaceWithText(p: *Pardes, t: *Text, y: []const u8) void {
}
t.cur_pinned = true;
t.sticky_col = -1;
- t.pane().ensureCursorVisible();
+ t.show();
}
/// `~` / `` ` `` / ``Alt-` ``: case-map the selection (or the cursor char),
@@ -1174,7 +1204,7 @@ pub fn normalJoin(p: *Pardes, t: *Text) void {
t.msel.active = false;
t.cur_pinned = true;
t.sticky_col = -1;
- t.pane().ensureCursorVisible();
+ t.show();
}
pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void {
@@ -1242,7 +1272,7 @@ pub fn normalIndent(p: *Pardes, t: *Text, cnt: usize, add: bool) void {
t.msel.active = false;
t.cur_pinned = true;
t.sticky_col = -1;
- t.pane().ensureCursorVisible();
+ t.show();
}
pub fn normalToggleComment(p: *Pardes, t: *Text) void {
@@ -1277,7 +1307,7 @@ pub fn normalToggleComment(p: *Pardes, t: *Text) void {
}
next = @max(next, end);
}
- const ext = if (t.pane().file) |f| std.fs.path.extension(f.path) else "";
+ const ext = if (t.what != .body) "" else if (t.pane().file) |f| std.fs.path.extension(f.path) else "";
var token: []const u8 = config.comment_token_default;
lang: for (config.comment_tokens) |row| {
for (row.exts) |e| if (std.ascii.eqlIgnoreCase(ext, e)) {
@@ -1359,7 +1389,7 @@ pub fn normalAdjustNumber(p: *Pardes, t: *Text, delta: i64) void {
t.cur_col = @intCast(cc.col);
t.cur_pinned = true;
t.sticky_col = -1;
- t.pane().ensureCursorVisible();
+ t.show();
}
/// `]Space` / `[Space`: add blank lines below/above, the cursor staying
@@ -1381,7 +1411,7 @@ pub fn addNewline(p: *Pardes, t: *Text, below: bool, cnt: usize) void {
t.vsel.row += @intCast(cnt);
}
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
}
/// pair mapping for m-mode: either bracket names the pair; anything else
@@ -1450,7 +1480,7 @@ pub fn surroundAdd(p: *Pardes, t: *Text, ch: u21) void {
t.cur_col = @intCast(close_col + @as(usize, if (r.a.row == r.b.row) 1 else 0));
t.cur_pinned = true;
t.sticky_col = -1;
- t.pane().ensureCursorVisible();
+ t.show();
}
/// `md<ch>`: delete the enclosing pair's chars; the cursor maps through
@@ -1487,7 +1517,7 @@ pub fn surroundDelete(p: *Pardes, t: *Text, ch: u21) void {
t.vsel.active = false;
t.msel.active = false;
t.cur_pinned = true;
- t.pane().ensureCursorVisible();
+ t.show();
}
/// `mr<from><to>`: swap the enclosing <from> pair's chars for <to>'s
@@ -1512,20 +1542,24 @@ pub fn surroundReplace(p: *Pardes, t: *Text, from: u21, to: u21) void {
}
pub fn pushUndo(p: *Pardes, t: *Text) void {
- const pane = t.pane();
// 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;
+ 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 != .body) return;
const pane = t.pane();
if (pane.file != null) return panes.File.undo(p, pane);
panes.Terminal.undo(p, pane);
}
pub fn doRedo(p: *Pardes, t: *Text) void {
+ if (t.what != .body) return;
const pane = t.pane();
if (pane.file != null) return panes.File.redo(p, pane);
panes.Terminal.redo(p, pane);