summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig168
1 files changed, 90 insertions, 78 deletions
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 });