summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Pipe.zig96
-rw-r--r--src/body_layer.zig9
-rw-r--r--src/builtins.zig10
-rw-r--r--src/edit.zig10
-rw-r--r--src/look.zig2
-rw-r--r--src/normal.zig30
-rw-r--r--src/panes.zig5
-rw-r--r--src/pardes.zig77
8 files changed, 176 insertions, 63 deletions
diff --git a/src/Pipe.zig b/src/Pipe.zig
index 4cabfd88..1ad7b826 100644
--- a/src/Pipe.zig
+++ b/src/Pipe.zig
@@ -9,6 +9,7 @@ const config = @import("config.zig");
const selection_pipe = @import("selection_pipe.zig");
const edit = @import("edit.zig");
const Pane = panes.Pane;
+const Text = panes.Text;
const Key = pardes.Key;
const Pardes = pardes.Pardes;
const Pipe = @This();
@@ -41,7 +42,14 @@ pub const PendingPipe = struct {
id: u32,
pane: usize,
serial: u32,
+ /// The text piped: the pane's body or tag, or a header (`column`
+ /// names which).
+ what: Text.What,
+ column: usize,
+ /// What the answer is checked against before it is applied: a body's
+ /// file revision, any other text's contents.
revision: u32,
+ sum: u64,
command: []u8,
cwd: []u8,
inputs: []selection_pipe.Input,
@@ -71,8 +79,12 @@ pub const PendingPipe = struct {
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 t = p.promptTarget(pane) orelse return;
+ if (t.what == .body) {
+ const f = pane.file orelse return;
+ if (!panes.Output.fileTraits(f.output).saves) return;
+ }
+ const content = edit.flatSurface(p, t) catch return;
const how = switch (pane.prompt) {
.pipe => |pipe| pipe,
else => return,
@@ -84,7 +96,7 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
if (command.len == 0) return;
var ranges: [panes.Text.max_selections]modal.Selection = undefined;
- const got = pane.body.ranges(f.content, 0, &ranges);
+ const got = t.ranges(content, 0, &ranges);
// `!`/`A-!` take no stdin and run ONCE — see `PendingPipe.nranges`.
const ninputs = if (how.pipes()) got.n else 1;
const inputs = p.gpa.alloc(selection_pipe.Input, ninputs) catch return;
@@ -93,8 +105,8 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
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;
- break :bytes f.content[lo..hi];
+ if (hi > content.len) break;
+ break :bytes content[lo..hi];
} else "";
const copy = p.gpa.dupe(u8, bytes) catch break;
inputs[i] = .{ .bytes = copy };
@@ -123,13 +135,16 @@ pub fn submitPipe(p: *Pardes, id: usize) void {
.id = p.pipe.seq,
.pane = id,
.serial = pane.serial,
- .revision = f.revision,
+ .what = t.what,
+ .column = p.header_column orelse 0,
+ .revision = if (pane.file) |f| f.revision else 0,
+ .sum = std.hash.Wyhash.hash(0, content),
.command = command_copy,
.cwd = cwd_copy,
.inputs = inputs,
.ranges = ranges,
.primary = @intCast(got.pri),
- .explicit = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active,
+ .explicit = (t.vsel.active and t.vsel.explicit) or t.msel.active,
.how = how,
.nranges = @intCast(got.n),
};
@@ -229,10 +244,20 @@ pub fn pipeResponse(
const pane = p.panes[wait.pane] orelse return;
if (pane.serial != wait.serial) return;
- const f = if (pane.file) |*file| file else return;
- if (!panes.Output.fileTraits(f.output).saves) return;
- if (f.revision != wait.revision)
- return p.reportError(wait.pane, "pipe", error.FileChangedWhileFiltering);
+ const t: *Text = switch (wait.what) {
+ .body => &pane.body,
+ .tag => &pane.tag,
+ .column => if (wait.column < p.ncol) &p.col_tags[wait.column] else return,
+ .workspace => &p.global_tag,
+ .input => return,
+ };
+ const content = edit.flatSurface(p, t) catch return;
+ const changed = if (t.what == .body) changed: {
+ const f = pane.file orelse return;
+ if (!panes.Output.fileTraits(f.output).saves) return;
+ break :changed f.revision != wait.revision;
+ } else std.hash.Wyhash.hash(0, content) != wait.sum;
+ if (changed) return p.reportError(wait.pane, "pipe", error.FileChangedWhileFiltering);
if (wait.how == .ignore) return;
@@ -241,8 +266,8 @@ pub fn pipeResponse(
var removed: usize = 0;
var previous_end: usize = 0;
for (0..n) |i| {
- const cut = pipeCut(&wait, f.content, i) orelse return;
- const output = pipeOutput(&wait, outputs, f.content, i);
+ const cut = pipeCut(&wait, content, i) orelse return;
+ const output = pipeOutput(&wait, outputs, content, i);
if (output.len > selection_pipe.max_stdout_bytes) return;
total_output = std.math.add(usize, total_output, output.len) catch return;
if (total_output > selection_pipe.max_total_stdout_bytes) return;
@@ -250,7 +275,7 @@ pub fn pipeResponse(
removed = std.math.add(usize, removed, cut.hi - cut.lo) catch return;
previous_end = cut.hi;
}
- const kept = f.content.len - removed;
+ const kept = content.len - removed;
const final_len = std.math.add(usize, kept, total_output) catch return;
const replacement = p.gpa.alloc(u8, final_len) catch return;
@@ -259,9 +284,9 @@ pub fn pipeResponse(
var write_at: usize = 0;
for (0..n) |i| {
const range = wait.ranges[i];
- const cut = pipeCut(&wait, f.content, i).?;
- const output = pipeOutput(&wait, outputs, f.content, i);
- @memcpy(replacement[write_at .. write_at + (cut.lo - read_at)], f.content[read_at..cut.lo]);
+ const cut = pipeCut(&wait, content, i).?;
+ const output = pipeOutput(&wait, outputs, content, i);
+ @memcpy(replacement[write_at .. write_at + (cut.lo - read_at)], content[read_at..cut.lo]);
write_at += cut.lo - read_at;
const out_start = write_at;
@memcpy(replacement[write_at .. write_at + output.len], output);
@@ -273,14 +298,14 @@ pub fn pipeResponse(
.{ .anchor = out_start, .head = out_end };
read_at = cut.hi;
}
- @memcpy(replacement[write_at..], f.content[read_at..]);
+ @memcpy(replacement[write_at..], content[read_at..]);
// One async request is one history transaction, even at 64 cursors.
- edit.pushUndo(p, &pane.body);
- panes.File.setContent(p, f, replacement);
- pane.body.setRanges(f.content, new_ranges[0..n], &.{}, wait.primary, wait.explicit);
- pane.body.select = false;
- pane.ensureCursorVisible();
+ edit.pushUndo(p, t);
+ edit.setEditText(p, t, replacement);
+ t.setRanges(edit.flatSurface(p, t) catch return, new_ranges[0..n], &.{}, wait.primary, wait.explicit);
+ t.select = false;
+ t.show();
}
test "selection pipe prompt submits exact request and Escape cancels" {
@@ -316,6 +341,31 @@ test "selection pipe prompt submits exact request and Escape cancels" {
try std.testing.expect(nextPipeEffect(p) == null);
}
+test "| in a tag pipes the tag's own text, and undo there brings it back" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .tty_only = true });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const pane = try p.setTestFile("body\n");
+ p.update(.{ .key = .{ .cp = ':' } }); // the tag, on Save
+ p.update(.{ .key = .{ .cp = 'e' } }); // Save selected
+ p.update(.{ .key = .{ .cp = '|' } });
+ try std.testing.expect(pane.focus == .tag and pane.prompt == .pipe and pane.prompt_for == .tag);
+ p.update(.{ .key = .{ .cp = 't', .text = "tr a-z A-Z" } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(pane.focus == .tag and pane.prompt == .none);
+ const id = nextPipeEffect(p) orelse return error.MissingPipeEffect;
+ const request = p.pipe.pipeRequest(id) orelse return error.MissingPipeRequest;
+ try std.testing.expectEqual(@as(usize, 1), request.inputs.len);
+ try std.testing.expectEqualSlices(u8, "Save", request.inputs[0].bytes);
+ const outputs: []const []const u8 = &.{"SAVE"};
+ p.update(.{ .pipe_resp = .{ .id = id, .success = true, .outputs = outputs } });
+ try std.testing.expect(std.mem.startsWith(u8, pardes.tagline.curTail(pane), " SAVE "));
+ try std.testing.expectEqualStrings("body\n", pane.file.?.content);
+ p.update(.{ .key = .{ .cp = 'u' } });
+ try std.testing.expect(std.mem.startsWith(u8, pardes.tagline.curTail(pane), " Save "));
+}
+
test "selection pipe replaces all ranges atomically and undo restores them" {
const gpa = std.testing.allocator;
const p = try Pardes.init(gpa, .{ .tty_only = true });
diff --git a/src/body_layer.zig b/src/body_layer.zig
index ab8a888a..ff1f903c 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -661,8 +661,10 @@ pub fn paintHeaders(p: *Pardes) !void {
cell.style.bg = .{ .rgb = th.sel_bg };
}
}
+ // A prompt answering for the header has the caret on its band.
+ const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
const cur: usize = @intCast(@max(0, t.cur_row));
- if (cur >= p.header_top and cur < p.header_top + rows) {
+ if (!asking and cur >= p.header_top and cur < p.header_top + rows) {
const bar = modal.lineSlice(text, cur);
const caret = panes.File.rawDisplayCol(bar, @min(@as(usize, @intCast(@max(0, t.cur_col))), bar.len)) -| p.header_scroll;
if (caret < w) s.cursor = .{ .x = x + @as(u16, @intCast(caret)), .y = y + @as(u16, @intCast(cur - p.header_top)), .bar = t.mode == .insert };
@@ -861,7 +863,8 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, index: usize, kind: T
}
}
const caret = panes.File.rawDisplayCol(text, @min(@as(usize, @intCast(@max(0, t.cur_col))), text.len)) -| scroll;
- if (t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
+ const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
+ if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .col = @intCast(caret), .bar = t.mode == .insert };
} else if (p.pointer_inside) {
const tag_hit = p.pointer_tag_hit;
const col: ?usize = if (tag_hit) |pointer|
@@ -920,7 +923,7 @@ pub fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect,
null
else if (pane.prompt == .name)
.{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) }
- else if (pane.focus == .tag and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: {
+ else if (pane.focus == .tag and pane.prompt == .none and pane.tag.cur_row >= top and pane.tag.cur_row < top + rows) caret: {
const row: usize = @intCast(pane.tag.cur_row - top);
const lead: usize = if (row + top == 0) prefix_len else 0;
break :caret .{ .row = row, .at = @min(lines[row].len, lead + @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
diff --git a/src/builtins.zig b/src/builtins.zig
index e9f29102..ffb00ff6 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -483,7 +483,7 @@ pub const Save = struct {
const path = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (path.len > 0) return exec.saveTo(c.p, c.id, path);
if (c.pane.file) |file| if (file.output == null) return exec.saveFile(c.p, c.id);
- if (c.pane.file != null or c.pane.isTerminal()) c.p.startPrompt(c.pane, .save);
+ if (c.pane.file != null or c.pane.isTerminal()) c.p.startPrompt(c.pane, &c.pane.body, .save);
}
};
@@ -717,7 +717,7 @@ pub const Find = struct {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .find, .top) catch |err|
c.p.reportError(c.id, "find", err);
- c.p.startPrompt(c.pane, .{ .search = config.find_marker });
+ c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.find_marker });
}
};
@@ -730,7 +730,7 @@ pub const Grep = struct {
const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (pat.len > 0) return look.runSearch(c.p, c.id, pat, .grep, .top) catch |err|
c.p.reportError(c.id, "grep", err);
- c.p.startPrompt(c.pane, .{ .search = config.grep_marker });
+ c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.grep_marker });
}
};
@@ -894,7 +894,7 @@ pub const Rename = struct {
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .rename, a);
- c.p.startPrompt(c.pane, .{ .search = config.rename_marker });
+ c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.rename_marker });
}
};
@@ -903,7 +903,7 @@ pub const WsSymbols = struct {
pub fn run(c: Ctx) void {
const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a);
- c.p.startPrompt(c.pane, .{ .search = config.symbol_marker });
+ c.p.startPrompt(c.pane, &c.pane.body, .{ .search = config.symbol_marker });
}
};
diff --git a/src/edit.zig b/src/edit.zig
index 837a22c6..e21352e1 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -74,10 +74,12 @@ pub fn typeToTty(p: *Pardes, id: usize, pane: *const Pane, text: []const u8) voi
}
pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
- if (tagline.focusedHeader(p)) |t| {
+ // A prompt answering for a header takes the paste, as it takes keys.
+ const asking = if (p.panes[p.active]) |active| active.prompt != .none else false;
+ if (!asking) if (tagline.focusedHeader(p)) |t| {
p.clip_pending = null;
return pasteInto(p, t, bytes);
- }
+ };
const req = p.clip_pending;
p.clip_pending = null;
if (bytes.len == 0) return;
@@ -94,7 +96,7 @@ pub fn applyPaste(p: *Pardes, bytes: []const u8) void {
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);
+ if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
return;
}
if (pane.focus == .tag) return pasteInto(p, &pane.tag, bytes);
@@ -400,7 +402,7 @@ 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 {
+pub fn setEditText(p: *Pardes, t: *Text, new: []u8) void {
if (t.what != .body) {
if (t.own) |old| p.gpa.free(old);
t.own = new;
diff --git a/src/look.zig b/src/look.zig
index cbdf0bea..d7e763d3 100644
--- a/src/look.zig
+++ b/src/look.zig
@@ -502,7 +502,7 @@ const SearchStart = enum { top, cursor };
pub fn submitSearch(p: *Pardes, id: usize) void {
const pane = p.panes[id] orelse return;
- if (normal.selRegexArmed(pane)) |a| return normal.applySelRegex(p, pane, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| return if (p.promptTarget(pane)) |t| normal.applySelRegex(p, pane, t, a.pat, a.split);
const armed = pane.promptText() orelse return;
const slash = std.mem.indexOfScalar(u8, armed, '/') orelse return;
if (std.mem.startsWith(u8, armed, config.rename_marker)) return p.lspRequest(id, .rename, armed[slash + 1 ..]);
diff --git a/src/normal.zig b/src/normal.zig
index c7e75d70..ffbb49d5 100644
--- a/src/normal.zig
+++ b/src/normal.zig
@@ -67,13 +67,13 @@ pub fn multiOnce(p: *Pardes) bool {
return p.multi_first;
}
-fn startSelRegex(p: *Pardes, pane: *Pane, split: bool) void {
- const text = edit.flatSurface(p, &pane.body) catch return;
- const got = pane.body.ranges(text, 0, &pane.sel_snap);
+fn startSelRegex(p: *Pardes, pane: *Pane, t: *Text, split: bool) void {
+ const text = edit.flatSurface(p, t) catch return;
+ const got = t.ranges(text, 0, &pane.sel_snap);
pane.nsel_snap = @intCast(got.n);
pane.sel_snap_pri = @intCast(got.pri);
- pane.sel_snap_expl = (pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active;
- p.startPrompt(pane, .{ .search = if (split) config.split_marker else config.select_marker });
+ pane.sel_snap_expl = (t.vsel.active and t.vsel.explicit) or t.msel.active;
+ p.startPrompt(pane, t, .{ .search = if (split) config.split_marker else config.select_marker });
}
pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
@@ -85,8 +85,10 @@ pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
return .{ .pat = armed[slash + 1 ..], .split = split };
}
-pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void {
- const text = edit.flatSurface(p, &pane.body) catch return;
+/// Select (or split on) `pat` inside the selections `t` had when s/S was
+/// pressed; the prompt's snapshot of them lives on `pane`.
+pub fn applySelRegex(p: *Pardes, pane: *Pane, t: *Text, pat: []const u8, split: bool) void {
+ const text = edit.flatSurface(p, t) catch return;
const snap = pane.sel_snap[0..pane.nsel_snap];
var out: [Text.max_selections]modal.Selection = undefined;
var m: usize = 0;
@@ -132,9 +134,9 @@ pub fn applySelRegex(p: *Pardes, pane: *Pane, pat: []const u8, split: bool) void
r.anchor = @min(r.anchor, text.len);
r.head = @min(r.head, text.len);
}
- return pane.body.setRanges(text, snap, &.{}, pane.sel_snap_pri, pane.sel_snap_expl);
+ return t.setRanges(text, snap, &.{}, pane.sel_snap_pri, pane.sel_snap_expl);
}
- pane.body.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO)
+ t.setRanges(text, out[0..m], &.{}, 0, true); // helix keeps primary 0 (its own TODO)
}
const KeyState = struct {
@@ -521,7 +523,9 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
}
return t.multiSelAction(text, multi.kind, multi.count);
},
- .select_regex => |split| return startSelRegex(p, pane, split),
+ // s, S and | answer for the text they are pressed in; `/` searches
+ // the body wherever it is pressed, as acme's Look from a tag does.
+ .select_regex => |split| return startSelRegex(p, pane, t, split),
.edit => |op| switch (op.kind) {
.delete => return edit.normalDelete(p, t, true),
.delete_noyank => return edit.normalDelete(p, t, false),
@@ -560,8 +564,8 @@ fn executeNormalAction(p: *Pardes, t: *Text, semantic: modal.Normal.Action) void
.column, .workspace => tagline.exitHeader(p),
.body, .input => tagline.enterTag(p, pane),
},
- .pipe_selection => |how| return p.startPrompt(pane, .{ .pipe = how }),
- .search => return p.startPrompt(pane, .{ .search = config.search_marker }),
+ .pipe_selection => |how| return p.startPrompt(pane, t, .{ .pipe = how }),
+ .search => return p.startPrompt(pane, &pane.body, .{ .search = config.search_marker }),
.search_step => |direction| return look.lookWalk(
p,
if (direction == .forward) @as(i32, 1) else -1,
@@ -602,7 +606,7 @@ pub fn handlePdfNormal(p: *Pardes, pane: *Pane, key: Key) void {
p.leader_n = 0;
},
.command_line => tagline.enterTag(p, pane),
- .search => p.startPrompt(pane, .{ .search = config.search_marker }),
+ .search => p.startPrompt(pane, &pane.body, .{ .search = config.search_marker }),
.search_forward => look.lookWalk(p, 1),
.search_backward => look.lookWalk(p, -1),
}
diff --git a/src/panes.zig b/src/panes.zig
index 1e5be107..a67aa8ac 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -101,6 +101,10 @@ pub const Pane = struct {
image: ?Image.State = null,
pdf: PdfSlot = if (Pdf.enabled) null else {},
prompt: Prompt = .none,
+ /// The text an open prompt answers for: `s`/`S` select in it and `|`
+ /// pipes it. A header's is whichever has the keyboard; `/` and Save's
+ /// path always answer for the body.
+ prompt_for: Text.What = .body,
/// The answer to an open prompt: its marker and what is typed after it,
/// edited in insert mode and shown on the notice band, or a name draft
/// shown on the tagline in place of the path. It is its own text so a
@@ -314,6 +318,7 @@ pub const Pane = struct {
pub fn closePrompt(pane: *Pane) void {
pane.input.deinit(pane.gpa);
pane.prompt = .none;
+ pane.prompt_for = .body;
pane.nsel_snap = 0;
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 3194b059..65f3aebb 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2911,6 +2911,25 @@ test "a header's execute key runs the word under its cursor and lets go of the k
try std.testing.expectEqual(@as(usize, 1), p.col_n[0]);
}
+test "s in a header selects in the header's own text" {
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 12, .tty_only = true });
+ defer p.deinit();
+ const pane = try p.setTestFile("body col\n");
+ tagline.enterHeader(p, null);
+ p.update(.{ .key = .{ .cp = 'x' } });
+ p.update(.{ .key = .{ .cp = 's' } });
+ try std.testing.expect(pane.prompt == .search and pane.prompt_for == .workspace);
+ // the prompt takes the keys while the header keeps the keyboard
+ p.update(.{ .key = .{ .cp = 'c', .text = "col" } });
+ p.update(.{ .key = .{ .cp = Key.enter } });
+ try std.testing.expect(p.header_focus and pane.prompt == .none);
+ var ranges: [Text.max_selections]modal.Selection = undefined;
+ const text = try edit.flatSurface(p, &p.global_tag);
+ const got = p.global_tag.ranges(text, 0, &ranges);
+ try std.testing.expectEqual(@as(usize, 3), got.n); // Newcol, Joincol, NextColor
+ try std.testing.expectEqual(@as(u8, 0), pane.body.nsel);
+}
+
test "Enter in a header runs the word under its cursor, as the execute key does" {
const p = try Pardes.init(std.testing.allocator, .{ .cols = 100, .rows = 12, .tty_only = true });
defer p.deinit();
@@ -4832,7 +4851,8 @@ pub const Pardes = struct {
return;
}
const pane = p.panes[p.active] orelse return;
- const header = tagline.focusedHeader(p);
+ // An open prompt takes the keys even when it answers for a header.
+ const header = if (pane.prompt != .none) null else tagline.focusedHeader(p);
if (header == null and pane.isTerminal() and pane.body.mode == .tty and pane.focus == .body and pane.prompt == .none) {
p.leader_on = false;
p.ctrl_w_pending = false;
@@ -4927,22 +4947,25 @@ pub const Pardes = struct {
if (header == null and pane.prompt != .none) {
if (hit(key, config.search_submit) or hit(key, config.escape)) {
const submit = hit(key, config.search_submit);
+ const target = p.promptTarget(pane);
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),
+ else if (normal.selRegexArmed(pane)) |_| if (target) |answered|
+ normal.applySelRegex(p, pane, answered, "", false),
.none, .del_side, .name => unreachable,
}
pane.closePrompt();
- pane.body.mode = .normal;
- pane.body.normal.clear();
+ if (target) |answered| {
+ answered.mode = .normal;
+ answered.normal.clear();
+ }
return;
}
edit.handleInsert(p, &pane.input, key);
- if (normal.selRegexArmed(pane)) |a| normal.applySelRegex(p, pane, a.pat, a.split);
+ if (normal.selRegexArmed(pane)) |a| if (p.promptTarget(pane)) |answered| normal.applySelRegex(p, pane, answered, a.pat, a.split);
return;
}
if (t.mode == .normal and (hit(key, config.look_key) or hit(key, config.exec_key))) {
@@ -5045,7 +5068,9 @@ pub const Pardes = struct {
return pane.cwdSlice();
}
- pub fn startPrompt(p: *Pardes, pane: *Pane, request: union(enum) {
+ /// Open a prompt on `pane`'s notice band answering for `t`: the body,
+ /// the pane's tag, or the header that has the keyboard.
+ pub fn startPrompt(p: *Pardes, pane: *Pane, t: *Text, request: union(enum) {
search: []const u8,
pipe: modal.Normal.PipeBehavior,
save,
@@ -5054,8 +5079,12 @@ pub const Pardes = struct {
.search => |marker| marker,
.save => config.save_marker,
.pipe => |how| pipe: {
- const file = pane.file orelse return;
- if (!panes.Output.fileTraits(file.output).saves) return;
+ // A body is piped only when it is a file that saves; a tag's
+ // or a header's text is its own.
+ if (t.what == .body) {
+ const file = pane.file orelse return;
+ if (!panes.Output.fileTraits(file.output).saves) return;
+ }
break :pipe Pipe.pipeMarker(how);
},
};
@@ -5064,7 +5093,16 @@ pub const Pardes = struct {
const dir = if (request == .save) paneDir(pane) else "";
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;
- pane.focus = .body;
+ // The keyboard comes back to the text answered for; a prompt for
+ // the body takes it out of a tag or header.
+ switch (t.what) {
+ .body => {
+ pane.focus = .body;
+ if (p.panes[p.active] == pane) tagline.exitHeader(p);
+ },
+ .tag => pane.focus = .tag,
+ .column, .workspace, .input => {},
+ }
// 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) {
@@ -5072,11 +5110,22 @@ pub const Pardes = struct {
.pipe => |how| .{ .pipe = how },
.save => .save,
};
+ pane.prompt_for = t.what;
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
+ // Whatever mode the text was in, it is in normal mode once the
// prompt is answered or dropped.
- pane.body.mode = .normal;
- pane.body.normal.clear();
+ t.mode = .normal;
+ t.normal.clear();
+ }
+
+ /// The text `pane`'s open prompt answers for, if it is still there.
+ pub fn promptTarget(p: *Pardes, pane: *Pane) ?*Text {
+ return switch (pane.prompt_for) {
+ .body => &pane.body,
+ .tag => &pane.tag,
+ .column, .workspace => if (tagline.focusedHeader(p)) |h| (if (h.what == pane.prompt_for) h else null) else null,
+ .input => null,
+ };
}
pub const SavePath = Effect.Buf(effect_path_cap);
@@ -6308,7 +6357,7 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
// the chip ends at the pane's right edge -- so this is the frame that
// used to put it one column past the pane, off the grid and past what the
// detached wire will encode.
- p.startPrompt(pane, .{ .search = "/" });
+ p.startPrompt(pane, &pane.body, .{ .search = "/" });
_ = try p.render(frame.allocator());
const r = p.rects[id];
const cursor = p.surface.cursor orelse return error.MissingPromptCursor;