summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/File.zig3
-rw-r--r--src/Pipe.zig19
-rw-r--r--src/Text.zig33
-rw-r--r--src/body_layer.zig4
-rw-r--r--src/edit.zig116
-rw-r--r--src/exec.zig8
-rw-r--r--src/mouse.zig4
-rw-r--r--src/normal.zig11
-rw-r--r--src/panes.zig36
-rw-r--r--src/pardes.zig168
10 files changed, 234 insertions, 168 deletions
diff --git a/src/File.zig b/src/File.zig
index 239aa4ba..bf4bf08c 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -1129,7 +1129,8 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod
const goff = pane.scroll();
const gcur = Terminal.gridCursor(pane);
const gcrow = if (pane.body.cur_pinned) pane.body.cur_row else @as(i32, gcur.y) + goff;
- const cur_line: i32 = if (active and !pane.tag_edit) gcrow else std.math.minInt(i32);
+ const typing_body = !pane.tag_edit and (pane.prompt == .none or pane.prompt == .del_side);
+ const cur_line: i32 = if (active and typing_body) gcrow else std.math.minInt(i32);
// the body's first row, the way renderPane derives it (Tagbottom)
const body_y = p.bodyTop(r);
const context_bg = p.theme().tag_bg;
diff --git a/src/Pipe.zig b/src/Pipe.zig
index c2b49f31..3e6752a8 100644
--- a/src/Pipe.zig
+++ b/src/Pipe.zig
@@ -73,13 +73,12 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
const f = pane.file orelse return;
if (!panes.Output.fileTraits(f.output).saves) return;
- const prompt = switch (pane.prompt) {
+ const how = switch (pane.prompt) {
.pipe => |pipe| pipe,
else => return,
};
- const tail = pane.tagSlice();
- const armed = tail[@min(prompt.at, tail.len)..];
- const marker = pipeMarker(prompt.how);
+ const armed = pane.promptText() orelse return;
+ const marker = pipeMarker(how);
if (!std.mem.startsWith(u8, armed, marker)) return;
const command = armed[marker.len..];
if (command.len == 0) return;
@@ -87,11 +86,11 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
var ranges: [panes.Text.max_selections]modal.Selection = undefined;
const got = pane.body.ranges(f.content, 0, &ranges);
// `!`/`A-!` take no stdin and run ONCE — see `PendingPipe.nranges`.
- const ninputs = if (prompt.how.pipes()) got.n else 1;
+ const ninputs = if (how.pipes()) got.n else 1;
const inputs = p.gpa.alloc(selection_pipe.Input, ninputs) catch return;
var made: usize = 0;
for (ranges[0..ninputs], 0..) |range, i| {
- const bytes = if (prompt.how.pipes()) bytes: {
+ const bytes = if (how.pipes()) bytes: {
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
if (hi > f.content.len) break;
@@ -131,7 +130,7 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
.ranges = ranges,
.primary = @intCast(got.pri),
.explicit = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active,
- .how = prompt.how,
+ .how = how,
.nranges = @intCast(got.n),
};
p.emit(.{ .pipe = .{ .id = p.pipe.seq } });
@@ -295,12 +294,12 @@ test "selection pipe prompt submits exact request and Escape cancels" {
pane.body.vsel = .{ .active = true, .row = 0, .col = 0, .explicit = true };
p.update(.{ .key = .{ .cp = '|' } });
- try std.testing.expect(pane.tag_edit and pane.prompt == .pipe);
- try std.testing.expect(std.mem.endsWith(u8, pane.tagSlice(), config.pipe_marker));
+ try std.testing.expect(!pane.tag_edit and pane.prompt == .pipe);
+ try std.testing.expectEqualStrings(config.pipe_marker, pane.promptText().?);
try std.testing.expect(nextPipeEffect(p) == null);
p.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } });
p.update(.{ .key = .{ .cp = Key.enter } });
- try std.testing.expect(!pane.tag_edit and pane.prompt != .pipe);
+ try std.testing.expect(!pane.tag_edit and pane.prompt != .pipe and pane.input.own == null);
const id = nextPipeEffect(p) orelse return error.MissingPipeEffect;
const request = p.pipe.pipeRequest(id) orelse return error.MissingPipeRequest;
try std.testing.expectEqualStrings("tr a-z A-Z", request.command);
diff --git a/src/Text.zig b/src/Text.zig
index ad5aa186..aaa6dc88 100644
--- a/src/Text.zig
+++ b/src/Text.zig
@@ -1,9 +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. Where its 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; everything here works on the flat text it is handed.
+//! 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.
const std = @import("std");
const panes = @import("panes.zig");
const modal = @import("modal.zig");
@@ -15,6 +17,10 @@ const Text = @This();
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 };
+
/// A modal line selection (helix `x`): whole rows [r0, r1], absolute.
pub const LineSel = struct {
active: bool = false,
@@ -41,6 +47,10 @@ pub const SelRange = struct {
sticky: i32 = -1,
};
+what: What = .body,
+/// The characters of a text that holds its own (gpa-owned): everything but
+/// a body, which reads its pane's.
+own: ?[]u8 = null,
mode: Mode = .normal,
cur_pinned: bool = false,
cur_row: i32 = 0,
@@ -65,7 +75,16 @@ ed_redo_len: usize = 0,
/// The window this text belongs to (acme's t->w).
pub fn pane(t: *Text) *Pane {
- return @alignCast(@fieldParentPtr("body", t));
+ return switch (t.what) {
+ .body => @alignCast(@fieldParentPtr("body", 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.
+pub fn show(t: *Text) void {
+ if (t.what == .body) t.pane().ensureCursorVisible();
}
pub fn toModalCursor(t: *Text) modal.Cursor {
@@ -143,7 +162,7 @@ pub fn setRange(t: *Text, text: []const u8, row0: i32, r0: modal.Selection, expl
t.cur_pinned = true;
t.sticky_col = -1;
t.normal.clear();
- t.pane().ensureCursorVisible();
+ t.show();
}
pub fn ranges(t: *Text, text: []const u8, row0: i32, out: *[max_selections]modal.Selection) struct { n: usize, pri: usize } {
@@ -602,5 +621,5 @@ pub fn clampCursor(t: *Text, text: []const u8, row0: i32) void {
t.cur_pinned = true;
t.vsel.active = false;
t.msel.active = false;
- t.pane().ensureCursorVisible();
+ t.show();
}
diff --git a/src/body_layer.zig b/src/body_layer.zig
index 71529326..1f210220 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -460,8 +460,8 @@ pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id
}
// cursor: tracks the shell cursor until pinned by a click or a key
- // (the tag cursor above wins while the tag is focused)
- if (active and !pane.tag_edit) {
+ // (the tag's or a prompt's cursor wins while that is being typed into)
+ if (active and !pane.tag_edit and (pane.prompt == .none or pane.prompt == .del_side)) {
if (pane.body.mode != .tty) {
const cur = panes.Terminal.gridCursor(pane);
const goff = panes.Terminal.gridOffset(pane);
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);
diff --git a/src/exec.zig b/src/exec.zig
index 0a9d6f31..a2dbac7c 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -219,12 +219,8 @@ pub fn withArg(p: *Pardes, txt: []const u8, arg: ?[]const u8) []const u8 {
/// Enter on a save input: the path is everything past the marker.
pub fn submitSave(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
- const at = switch (pane.prompt) {
- .save => |a| a,
- else => return,
- };
- const armed = pane.tag_tail[at..pane.tag_tail_len];
- pane.tag_tail_len = at; // drop the prompt back to the seeded tail
+ if (pane.prompt != .save) return;
+ const armed = pane.promptText() orelse return;
if (!std.mem.startsWith(u8, armed, config.save_marker)) return;
const path = std.mem.trim(u8, armed[config.save_marker.len..], " \t\r\n");
if (path.len == 0) return;
diff --git a/src/mouse.zig b/src/mouse.zig
index a840c8fc..5f77634b 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -304,6 +304,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
{
if (m.button == config.select_button) if (p.panes[p.active]) |pane| {
if (pane.tag_edit) Pardes.exitTagEdit(pane);
+ pane.closePrompt();
};
var column: ?usize = null;
var x: u16 = 0;
@@ -439,6 +440,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const pane = p.panes[id] orelse return;
if (id != p.active) if (p.panes[p.active]) |previous| {
if (previous.tag_edit) Pardes.exitTagEdit(previous);
+ previous.closePrompt();
};
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
const body_y = p.bodyTop(r);
@@ -449,12 +451,14 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
} else if (mrow >= tag_y and mrow < tag_y + BOX_H) {
if (mcol < r.x + TAG_TEXT_INSET) return;
p.active = id;
+ pane.closePrompt();
p.enterTagEdit(pane, if (tagColumn(p, .pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + TAG_TEXT_INSET));
pane.tag_anchor = pane.tag_col;
p.drag = .{ .tag = .{ .id = id } };
} else {
p.active = id;
if (pane.tag_edit) Pardes.exitTagEdit(pane);
+ pane.closePrompt();
const pointed = edit.pointerTextSelection(p, id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return;
pane.clearPointerSelection(sel_slot);
pane.sel[sel_slot] = pointed.sel;
diff --git a/src/normal.zig b/src/normal.zig
index 192fd933..a77f27a7 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -76,13 +76,8 @@ fn startSelRegex(p: *Pardes, pane: *Pane, split: bool) void {
}
pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
- const prompt_at = switch (pane.prompt) {
- .search => |at| at,
- else => return null,
- };
- if (pane.nsel_snap == 0) return null;
- const tail = pane.tagSlice();
- const armed = tail[@min(prompt_at, tail.len)..];
+ if (pane.prompt != .search or pane.nsel_snap == 0) return null;
+ const armed = pane.input.own orelse "";
const split = std.mem.startsWith(u8, armed, config.split_marker);
if (!split and !std.mem.startsWith(u8, armed, config.select_marker)) return null;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return null;
@@ -274,7 +269,7 @@ pub fn replaySels(p: *Pardes, t: *Text, what: Replay) void {
}
t.setRanges(text, rs[0..n], st[0..n], pri, after_expl);
after.into(t);
- t.pane().ensureCursorVisible(); // the view follows the PRIMARY, not the last pass
+ t.show(); // the view follows the PRIMARY, not the last pass
}
pub fn handleNormal(p: *Pardes, t: *Text, key: Key) void {
diff --git a/src/panes.zig b/src/panes.zig
index e6634583..c26065ca 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -59,12 +59,13 @@ pub const Pane = struct {
const PdfSlot = if (Pdf.enabled) ?Pdf.State else void;
+ /// What an open prompt is asking. The answer is typed into `input`.
pub const Prompt = union(enum) {
none,
- search: u16,
- pipe: struct { at: u16, how: modal.Normal.PipeBehavior },
- /// Save on a scratch buffer or a terminal: the tail is a path to write to.
- save: u16,
+ search,
+ pipe: modal.Normal.PipeBehavior,
+ /// Save on a scratch buffer or a terminal: the answer is a path to write to.
+ save,
/// Del from the keyboard with open panes both above and below: which
/// one takes the rows. Nothing is typed; one key answers.
del_side,
@@ -94,9 +95,11 @@ pub const Pane = struct {
file: ?File.State = null,
image: ?Image.State = null,
pdf: PdfSlot = if (Pdf.enabled) null else {},
- /// Tag-tail input state. The tag text is presentation; this tag carries
- /// which operation owns it and the tail offset restored on submit/cancel.
prompt: Prompt = .none,
+ /// The answer to an open prompt: its marker and what is typed after it,
+ /// edited in insert mode and shown on the notice band. It is its own text
+ /// so a prompt never touches the tag.
+ input: Text = .{ .what = .input },
search_pane: ?usize = null,
search_row: ?usize = null,
look_at: ?LookSpot = null,
@@ -297,20 +300,23 @@ pub const Pane = struct {
pub fn tagSlice(p: *const Pane) []const u8 {
return p.tag_tail[0..p.tag_tail_len];
}
- pub fn promptAt(p: *const Pane) ?u16 {
- return switch (p.prompt) {
- .none, .del_side => null,
- .search, .save => |at| at,
- .pipe => |pipe| pipe.at,
- };
+ /// Close an open prompt, answered or not. Its answer goes with it, and an
+ /// s/S preview keeps whatever it last previewed.
+ pub fn closePrompt(pane: *Pane) void {
+ if (pane.input.own) |text| pane.gpa.free(text);
+ pane.input = .{ .what = .input };
+ pane.prompt = .none;
+ pane.nsel_snap = 0;
}
/// What an open prompt shows on its notice band: the one answer every
/// painter of the band reads.
pub fn promptText(p: *const Pane) ?[]const u8 {
- if (p.prompt == .del_side) return config.del_question;
- const at = p.promptAt() orelse return null;
- return p.tagSlice()[@min(at, p.tag_tail_len)..];
+ return switch (p.prompt) {
+ .none => null,
+ .del_side => config.del_question,
+ else => p.input.own orelse "",
+ };
}
pub fn appendTag(p: *Pane, text: []const u8) bool {
diff --git a/src/pardes.zig b/src/pardes.zig
index 15b2c49b..a29973e7 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1824,10 +1824,10 @@ test "Save on a scratch asks for a path in its inherited dir and makes it a file
// Save on a scratch arms a PATH input, prefilled with the inherited dir
try std.testing.expect(p.executeBuiltinLine(id, "Save"));
try std.testing.expect(np.prompt == .save);
- try std.testing.expect(std.mem.endsWith(u8, np.tagSlice(), " Save /tmp/pardes-save-dir/"));
+ try std.testing.expectEqualStrings(" Save /tmp/pardes-save-dir/", np.promptText().?);
// typing the filename and submitting converts it into an ordinary file
- try std.testing.expect(np.appendTag("note.txt"));
+ edit.insertKey(p, &np.input, .{ .cp = 'n', .text = "note.txt" });
exec.submitSave(p, id);
try std.testing.expect(np.file.?.output == null);
try std.testing.expectEqualStrings("/tmp/pardes-save-dir/note.txt", np.file.?.path);
@@ -1852,7 +1852,7 @@ test "Save on a terminal writes its plaintext scrollback and stays a terminal" {
try std.testing.expect(p.executeBuiltinLine(0, "Save"));
try std.testing.expect(pane.prompt == .save);
- try std.testing.expect(pane.appendTag("log.txt"));
+ edit.insertKey(p, &pane.input, .{ .cp = 'l', .text = "log.txt" });
exec.submitSave(p, 0);
// the pane is untouched: still a terminal, no file behind it
@@ -1901,7 +1901,7 @@ test "Save on an output buffer writes its rows out and leaves the buffer alone"
try std.testing.expectEqualStrings(file_pane_tail, Pardes.curTail(out));
try std.testing.expect(p.executeBuiltinLine(id, "Save"));
try std.testing.expect(out.prompt == .save);
- try std.testing.expect(out.appendTag("help.txt"));
+ edit.insertKey(p, &out.input, .{ .cp = 'h', .text = "help.txt" });
exec.submitSave(p, id);
// the buffer itself is untouched: same origin, same name, still refillable
@@ -4109,6 +4109,7 @@ pub const Pardes = struct {
if (comptime pdf_enabled) if (pane.pdf) |*pv| panes.Pdf.deinitPane(p, pane, pv);
if (pane.file) |*file| panes.File.deinit(p, pane, file);
if (pane.ovl) |o| p.gpa.free(o.text);
+ if (pane.input.own) |text| p.gpa.free(text);
for (pane.body.ed_undo[0..pane.body.ed_undo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
for (pane.body.ed_redo[0..pane.body.ed_redo_len]) |sn| if (sn.ovl) |o| p.gpa.free(o.text);
panes.Terminal.deinitEmulator(pane, p.gpa);
@@ -4836,7 +4837,7 @@ pub const Pardes = struct {
}
pub fn enterTagEdit(p: *Pardes, pane: *Pane, col: i32) void {
- if (pane.tag_edit and pane.prompt == .none) pane.tag_saved_col = pane.tag_col;
+ if (pane.tag_edit) pane.tag_saved_col = pane.tag_col;
const edit0: i32 = @intCast((p.tagPrefix(pane) catch return).len);
p.seedTail(pane);
if (!pane.tag_init) return;
@@ -4869,7 +4870,7 @@ pub const Pardes = struct {
}
pub fn exitTagEdit(pane: *Pane) void {
- if (pane.tag_edit and pane.prompt == .none) pane.tag_saved_col = pane.tag_col;
+ if (pane.tag_edit) pane.tag_saved_col = pane.tag_col;
if (pane.isTerminal()) {
if (pane.tag_edit) pane.body.mode = pane.tag_mode;
} else pane.body.mode = .normal;
@@ -4877,8 +4878,6 @@ pub const Pardes = struct {
pane.tag_sel = false;
pane.tag_name_len = null;
pane.tag_scroll = 0;
- pane.prompt = .none; // an abandoned prompt stays as tag text
- pane.nsel_snap = 0; // ...and its s/S preview keeps whatever it previewed
}
pub fn tagSelBounds(pane: *Pane) struct { lo: usize, hi: usize } {
@@ -5273,6 +5272,7 @@ pub const Pardes = struct {
p.active = id;
p.exitHeader();
if (pane.tag_edit) exitTagEdit(pane);
+ pane.closePrompt();
pane.body.mode = previous;
switch (previous) {
.tty => {
@@ -5294,6 +5294,7 @@ pub const Pardes = struct {
p.active = id;
p.exitHeader();
if (pane.tag_edit) exitTagEdit(pane);
+ pane.closePrompt();
if (pane.body.mode == .tty) {
pane.body.mode = .normal;
pane.body.normal.clear();
@@ -5351,7 +5352,7 @@ pub const Pardes = struct {
}
// insert mode owns Ctrl-w (delete word backward, helix); the focus
// prefix applies outside raw tty mode
- if (hit(key, config.window_prefix) and pane.body.mode != .insert) {
+ if (hit(key, config.window_prefix) and pane.body.mode != .insert and pane.prompt == .none) {
p.ctrl_w_pending = true;
return;
}
@@ -5384,34 +5385,29 @@ pub const Pardes = struct {
if (pane.isTerminal() and tty_toggle) {
return p.toggleTty(p.active);
}
- if (pane.prompt != .none and (hit(key, config.search_submit) or hit(key, config.escape))) {
- const submit = hit(key, config.search_submit);
- switch (pane.prompt) {
- .pipe => |pipe| {
- if (submit) Pipe.submitPipe(p, p.active);
- pane.tag_tail_len = @min(pipe.at, pane.tag_tail_len);
- },
- .save => |at| {
- if (submit) exec.submitSave(p, p.active) else pane.tag_tail_len = @min(at, pane.tag_tail_len);
- },
- .search => |at| {
- if (submit)
+ if (pane.prompt != .none) {
+ if (hit(key, config.search_submit) or hit(key, config.escape)) {
+ const submit = hit(key, config.search_submit);
+ switch (pane.prompt) {
+ .pipe => if (submit) Pipe.submitPipe(p, p.active),
+ .save => if (submit) exec.submitSave(p, p.active),
+ .search => if (submit)
look.submitSearch(p, p.active)
else if (normal.selRegexArmed(pane)) |_|
- normal.applySelRegex(p, pane, "", false);
- pane.tag_tail_len = @min(at, pane.tag_tail_len);
- },
- .none, .del_side => unreachable,
+ normal.applySelRegex(p, pane, "", false),
+ .none, .del_side => unreachable,
+ }
+ pane.closePrompt();
+ pane.body.mode = .normal;
+ pane.body.normal.clear();
+ return;
}
- exitTagEdit(pane);
- pane.body.mode = .normal;
- pane.body.normal.clear();
+ edit.handleInsert(p, &pane.input, key);
+ if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split);
return;
}
if (pane.tag_edit) {
if (pane.body.mode == .normal) p.tagNormalKey(pane, key) else p.tagInsertKey(pane, key);
- const pn = p.panes[p.active] orelse return;
- if (normal.selRegexArmed(pn)) |a| normal.applySelRegex(p, pn, a.pat, a.split);
return;
}
if (pane.body.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
@@ -5506,33 +5502,24 @@ pub const Pardes = struct {
break :pipe Pipe.pipeMarker(how);
},
};
- p.seedTail(pane);
- if (!pane.tag_init) return;
+ // Save asks for a path, so its answer starts out as the pane's own
+ // directory for the name to be typed onto.
const dir = if (request == .save) paneDir(pane) else "";
- const slash = request == .save and (dir.len == 0 or dir[dir.len - 1] != '/');
- var room = pane.tag_tail.len - pane.tag_tail_len;
- if (marker.len > room) return;
- room -= marker.len;
- if (dir.len > room) return;
- room -= dir.len;
- if (slash and room == 0) return;
- const prefix_len = (p.tagPrefix(pane) catch return).len;
- const prompt_at: u16 = @intCast(pane.tag_tail_len);
- _ = pane.appendTag(marker);
- if (request == .save) {
- _ = pane.appendTag(dir);
- if (slash) _ = pane.appendTag("/");
- }
+ const slash: []const u8 = if (request == .save and (dir.len == 0 or dir[dir.len - 1] != '/')) "/" else "";
+ const answer = std.mem.concat(p.gpa, u8, &.{ marker, dir, slash }) catch return;
+ if (pane.tag_edit) exitTagEdit(pane);
+ // Not closePrompt: an s/S snapshot was taken for this very prompt.
+ if (pane.input.own) |old| p.gpa.free(old);
pane.prompt = switch (request) {
- .search => .{ .search = prompt_at },
- .pipe => |how| .{ .pipe = .{ .at = prompt_at, .how = how } },
- .save => .{ .save = prompt_at },
+ .search => .search,
+ .pipe => |how| .{ .pipe = how },
+ .save => .save,
};
- pane.tag_edit = true;
- pane.tag_sel = false;
- pane.body.mode = .insert;
+ pane.input = .{ .what = .input, .own = answer, .mode = .insert, .cur_pinned = true, .cur_col = @intCast(answer.len) };
+ // Whatever mode the body was in, it is in normal mode once the
+ // prompt is answered or dropped.
+ pane.body.mode = .normal;
pane.body.normal.clear();
- pane.tag_col = @intCast(prefix_len + pane.tag_tail_len);
}
pub const SavePath = Effect.Buf(effect_path_cap);
@@ -6066,7 +6053,7 @@ pub const Pardes = struct {
p.trackJump();
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
- if (pane.tag_edit and pane.prompt == .none and (id != p.active or p.topbar_col != null))
+ if (pane.tag_edit and (id != p.active or p.topbar_col != null))
exitTagEdit(pane);
if (comptime terminal_panes) if (pane.terminal) |state| {
var off: u16 = 0;
@@ -6279,18 +6266,14 @@ pub const Pardes = struct {
// the detached wire will encode -- which drops every frame for
// as long as the prompt is up.
const printed = Messages.printRight(s, cx, row, chip -| 1, text, style);
- if (kind != .prompt or id != p.active) continue;
- const at = pane.promptAt() orelse continue;
- const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
- const col = @as(usize, pane.tag_col);
- if (col < prompt0) continue;
+ if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue;
// The cursor follows the text to wherever it landed; a caret
// in the part a narrow band dropped has nowhere to be.
- const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
+ const prompt_col = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
if (prompt_col < printed.dropped) continue;
const caret = printed.left + (prompt_col - printed.dropped);
if (caret < tx + tw)
- s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.body.mode == .insert };
+ s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.input.mode == .insert };
}
}
@@ -6652,7 +6635,7 @@ pub const Pardes = struct {
layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - TAG_TEXT_INSET, .h = BOX_H };
layer.cols = columns;
layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
- layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null;
+ layer.cursor = if (p.topbar_col == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null;
}
var text_buf: [1200]u8 = undefined;
// With the workspace tag row handed to native chrome there is no
@@ -6711,14 +6694,11 @@ pub const Pardes = struct {
p.surface.tag_layers[layer_index].fade = fade;
// The caret rides the band, where the prompt's glyphs are,
// at the same place in the text the grid pass puts it.
- if (kind != .prompt or id != p.active) continue;
- const at = pane.promptAt() orelse continue;
- const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
- if (pane.tag_col < prompt0) continue;
- const typed = panes.File.displayWidth(text[0..@min(pane.tag_col - prompt0, text.len)]);
+ if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue;
+ const typed = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
const dropped = shown - panes.File.displayWidth(kept);
if (typed < dropped) continue;
- p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.body.mode == .insert };
+ p.surface.tag_layers[layer_index].cursor = .{ .col = @intCast(pad + typed - dropped), .bar = pane.input.mode == .insert };
}
}
if (p.columnBarHeight() > 0) for (0..p.ncol) |column| {
@@ -6877,8 +6857,7 @@ pub const Pardes = struct {
.font_role = .tagline,
});
const tag = try p.tagText(arena, pane);
- const prompt_at = pane.promptAt();
- if (active and pane.tag_edit and prompt_at == null) {
+ if (active and pane.tag_edit) {
const caret = panes.File.rawDisplayCol(tag, pane.tag_col);
var scroll: usize = pane.tag_scroll;
if (caret < scroll) scroll = caret;
@@ -6890,16 +6869,12 @@ pub const Pardes = struct {
scroll = panes.File.rawDisplayCol(tag, raw);
}
pane.tag_scroll = @intCast(scroll);
- } else if (!pane.tag_edit or prompt_at != null) pane.tag_scroll = 0;
- const tag_cut = if (prompt_at) |at|
- @min(tag.len, tag.len - @min(tag.len, pane.tag_tail_len) + at)
- else
- tag.len;
- const tag_start = @min(tag_cut, panes.File.rawAtDisplay(tag, pane.tag_scroll));
- _ = s.print(tx, tag_y, tw, tag[tag_start..tag_cut], tag_style);
+ } else pane.tag_scroll = 0;
+ const tag_start = panes.File.rawAtDisplay(tag, pane.tag_scroll);
+ _ = s.print(tx, tag_y, tw, tag[tag_start..], tag_style);
if (tagTintRange(pane, tag, tag.len - curTail(pane).len)) |name| {
const lo = @max(name.lo, tag_start);
- const hi = @min(name.hi, tag_cut);
+ const hi = @min(name.hi, tag.len);
if (lo < hi) {
var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll;
const end = panes.File.rawDisplayCol(tag, hi) -| pane.tag_scroll;
@@ -7204,6 +7179,43 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
for (pane.sel) |selection| try std.testing.expect(selection.state == .none);
}
+test "a prompt is typed on its own line in the notice band and leaves the tag row alone" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ defer p.deinit();
+ const pane = try p.setTestFile("one two\n");
+ var frame = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+ const tag_before = try std.testing.allocator.dupe(Cell, p.surface.tag_layers[0].cells);
+ defer std.testing.allocator.free(tag_before);
+
+ p.update(.{ .key = .{ .cp = '/' } });
+ p.update(.{ .key = .{ .cp = 't', .text = "tw" } });
+ p.update(.{ .key = .{ .cp = Key.left } });
+ try std.testing.expect(pane.prompt == .search and !pane.tag_edit);
+ try std.testing.expectEqualStrings(config.search_marker ++ "tw", pane.promptText().?);
+ _ = frame.reset(.retain_capacity);
+ _ = try p.render(frame.allocator());
+ // The tagline is exactly what it was: nothing of the prompt is in it.
+ try std.testing.expectEqual(tag_before.len, p.surface.tag_layers[0].cells.len);
+ for (tag_before, p.surface.tag_layers[0].cells) |was, is| try std.testing.expect(was.visuallyEqual(&is));
+ try std.testing.expect(p.surface.tag_layers[0].cursor == null);
+ // The band carries the answer and the caret, one cell back from its end.
+ const band = for (p.surface.tagLayers()) |*layer| {
+ if (layer.cols != 0 and layer.kind == .notice) break layer;
+ } else return error.NoPromptBand;
+ const caret = band.cursor orelse return error.MissingBandCaret;
+ try std.testing.expectEqualStrings("t", band.cells[caret.col - 1].grapheme());
+ try std.testing.expectEqualStrings("w", band.cells[caret.col].grapheme());
+ const grid = p.surface.cursor orelse return error.MissingPromptCursor;
+ try std.testing.expectEqual(p.bodyTop(p.rects[0]), grid.y);
+
+ // Submitting searches for what was typed and gives the prompt its line back.
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(pane.prompt == .none and pane.input.own == null);
+}
+
test "a stillborn pane gives its column to the first pane that joins it" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 160, .rows = 40 });