summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/File.zig2
-rw-r--r--src/Messages.zig1
-rw-r--r--src/body_layer.zig6
-rw-r--r--src/builtins.zig55
-rw-r--r--src/config.zig1
-rw-r--r--src/edit.zig2
-rw-r--r--src/exec.zig101
-rw-r--r--src/fs-help.txt4
-rw-r--r--src/mouse.zig5
-rw-r--r--src/panes.zig17
-rw-r--r--src/pardes.zig127
-rw-r--r--src/tagline.zig15
12 files changed, 322 insertions, 14 deletions
diff --git a/src/File.zig b/src/File.zig
index af538385..8ba58fbc 100644
--- a/src/File.zig
+++ b/src/File.zig
@@ -1130,7 +1130,7 @@ 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 typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side);
+ const typing_body = pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice);
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(pane, r);
diff --git a/src/Messages.zig b/src/Messages.zig
index 00352afe..f8178629 100644
--- a/src/Messages.zig
+++ b/src/Messages.zig
@@ -180,6 +180,7 @@ fn showMessage(p: *Pardes, id: usize, text: []const u8, stack: bool) void {
pane.pushOlderMessage();
if (pane.msg_len == 0) pane.msg_slot = pane.freeMessageSlot();
pane.msg_announcement = false;
+ pane.msg_sent = false;
pane.msg_status = !stack;
const arrives = pane.msg_len == 0 or pane.msg_life.phase == .leaving;
for (text[0..n], pane.msg[0..n]) |c, *cell| cell.* = if (c < 0x20 or c == 0x7f) ' ' else c;
diff --git a/src/body_layer.zig b/src/body_layer.zig
index c468bd5f..c8e229c4 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -479,7 +479,7 @@ 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's or a prompt's cursor wins while that is being typed into)
- if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side)) {
+ if (active and pane.focus == .body and (pane.prompt == .none or pane.prompt == .del_side or pane.prompt == .repl_choice)) {
if (pane.body.mode != .tty) {
const cur = panes.Terminal.gridCursor(pane);
const goff = panes.Terminal.gridOffset(pane);
@@ -797,12 +797,12 @@ pub fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
.y = first + @as(u16, pane.notices.row[i]),
.w = chip,
.h = BOX_H,
- }, line, Messages.blendRgb(chrome.tag_bg, page, fade), Messages.blendRgb(chrome.tag_fg, page, fade));
+ }, line, Messages.blendRgb(chrome.tag_bg, page, fade), Messages.blendRgb(if (kind == .message and pane.msg_sent) chrome.tag_name_fg else chrome.tag_fg, page, fade));
p.surface.tag_layers[layer_index].slide = motion.slide;
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 or pane.prompt == .del_side) continue;
+ if (kind != .prompt or id != p.active or pane.prompt == .del_side or pane.prompt == .repl_choice) 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;
diff --git a/src/builtins.zig b/src/builtins.zig
index 5273cf37..be0956e0 100644
--- a/src/builtins.zig
+++ b/src/builtins.zig
@@ -852,6 +852,61 @@ pub const Tty9p = struct {
/// Fold the active pane's column into the one on its right, keeping its panes.
/// The horizontal mirror of the vertical stacking `New` does.
+/// Binds this terminal as a language's REPL, by the language's name as the
+/// syntax table has it: an exec made on the body of a file in that
+/// language -- a middle click, the execute key, a single word like `ls`
+/// as much as a selection -- and one made on this terminal's own body, is
+/// typed into it (bracketed paste if its program asked for that, else line
+/// by line, then Enter) instead of run. `Repl python` binds (again, it is
+/// already); `Repl -` unbinds; `Repl` bare says what it is bound as. With
+/// several bound for a language, the exec asks which.
+/// Still commands, whatever is bound: a word in a tag, `Exec <text>` run by
+/// name (typed, a 2-1 chord onto Exec, a ctl line), and a command word
+/// @`cmd` in the text, looked at or clicked.
+pub const Repl = struct {
+ pub const takes_arg = true;
+ pub fn run(c: Ctx) void {
+ const pane = c.pane;
+ if (!pane.isTerminal()) return c.p.reportFailure(c.id, "Repl: only a terminal is a REPL");
+ const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n");
+ var buf: [320]u8 = undefined;
+ var idbuf: [48]u8 = undefined;
+ if (arg.len == 0) {
+ if (pane.repl == null) return c.p.setMessage(c.id, "Repl: not bound");
+ return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl");
+ }
+ if (std.mem.eql(u8, arg, "-")) {
+ pane.repl = null;
+ return c.p.setMessage(c.id, "Repl: unbound");
+ }
+ const grammars = @import("grammar_manifest.zig").all;
+ const lang: u8 = for (grammars, 0..) |g, i| {
+ if (g.exts.len > 0 and std.mem.eql(u8, g.name, arg)) break @intCast(i);
+ } else {
+ var w = std.Io.Writer.fixed(&buf);
+ w.print("Repl: no language \"{s}\"; one of", .{arg[0..@min(arg.len, 32)]}) catch {};
+ for (grammars) |g| if (g.exts.len > 0) w.print(" {s}", .{g.name}) catch break;
+ return c.p.reportFailure(c.id, w.buffered());
+ };
+ if (pane.repl) |r| if (r.lang == lang)
+ return c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl");
+ // The lowest letter no other REPL of the language has: freed ones
+ // come back.
+ var letter: u8 = 'a';
+ while (letter <= 'z') : (letter += 1) {
+ const taken = for (c.p.panes) |slot| {
+ const other = slot orelse continue;
+ if (other != pane) if (other.repl) |r| if (r.lang == lang and r.letter == letter) break true;
+ } else false;
+ if (!taken) break;
+ }
+ if (letter > 'z') return c.p.reportFailure(c.id, "Repl: every letter of the language is taken");
+ pane.repl = .{ .lang = lang, .letter = letter };
+ pane.repl_warned = false;
+ c.p.setMessage(c.id, std.fmt.bufPrint(&buf, "Repl: {s}", .{exec.replId(&idbuf, pane)}) catch "Repl");
+ }
+};
+
pub const Joincol = struct {
pub const scope: Scope = .session;
pub fn run(c: Ctx) void {
diff --git a/src/config.zig b/src/config.zig
index 1a505334..9ad9cc7e 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -48,6 +48,7 @@ pub const leader_path = paths: {
.Mode = null,
.Togglettymode = null,
.Collapse = null,
+ .Repl = null,
.Kill = null,
.Exit = null,
.ClipYank = "y",
diff --git a/src/edit.zig b/src/edit.zig
index b2d71ed2..3cbbc28c 100644
--- a/src/edit.zig
+++ b/src/edit.zig
@@ -87,7 +87,7 @@ 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) {
+ if (pane.prompt != .none and pane.prompt != .del_side and pane.prompt != .repl_choice) {
// 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;
diff --git a/src/exec.zig b/src/exec.zig
index 8b058a12..1b82d2a8 100644
--- a/src/exec.zig
+++ b/src/exec.zig
@@ -16,6 +16,7 @@ const colors = @import("colors.zig");
const themes = colors.themes;
const builtins = @import("builtins.zig");
const config = @import("config.zig");
+const grammars = @import("grammar_manifest.zig").all;
const pdf_enabled = panes.Pdf.enabled;
const font_picker = pardes.font_picker;
const hosted = pardes.hosted;
@@ -369,6 +370,96 @@ pub fn replaceStillborn(p: *Pardes) void {
if (was_active) p.active = new_id;
}
+// ---- REPLs ----
+//
+// A terminal is bound as a language's REPL by `Repl python` (builtins.zig).
+// An exec made by a gesture on the body of a file in that language -- or on
+// the REPL's own body -- is sent to it as input rather than run: all of it,
+// a word like `ls` too. The tag's words, `Exec` by name and a command word
+// `@`...`` still run as commands.
+
+/// A file pane's language, as grammar_manifest names it, by its index.
+pub fn langOf(pane: *const Pane) ?u8 {
+ const f = pane.file orelse return null;
+ for (grammars, 0..) |g, i| for (g.exts) |ext| {
+ if (std.mem.endsWith(u8, f.path, ext)) return @intCast(i);
+ };
+ return null;
+}
+
+/// A bound REPL whose program can still take input: a command pane's is
+/// gone once its command is done.
+pub fn replLive(pane: *const Pane) bool {
+ return pane.repl != null and pane.isTerminal() and !(pane.command != null and pane.command_done);
+}
+
+/// `python-a`.
+pub fn replId(buf: []u8, pane: *const Pane) []const u8 {
+ const r = pane.repl orelse return "";
+ return std.fmt.bufPrint(buf, "{s}-{c}", .{ grammars[r.lang].name, r.letter }) catch "";
+}
+
+const Routed = union(enum) { none, sent: usize, asked };
+
+/// Where an exec from `from`'s body goes when a REPL is bound for it: the
+/// one there is, or a question when there are several.
+fn routeToRepl(p: *Pardes, from: usize, text: []const u8) Routed {
+ const src = p.panes[from] orelse return .none;
+ if (src.repl != null and replLive(src)) {
+ sendToRepl(p, from, from, text);
+ return .{ .sent = from };
+ }
+ const lang = langOf(src) orelse return .none;
+ var bound: [26]usize = undefined;
+ var n: usize = 0;
+ for (p.panes, 0..) |slot, i| if (slot) |pane| if (pane.repl) |r| if (r.lang == lang and replLive(pane) and n < bound.len) {
+ bound[n] = i;
+ n += 1;
+ };
+ if (n == 0) return .none;
+ if (n == 1) {
+ sendToRepl(p, from, bound[0], text);
+ return .{ .sent = bound[0] };
+ }
+ // Several: asked on the pane's notice band, as Del asks, one key
+ // answering; nothing is remembered of the answer.
+ const owned = p.gpa.dupe(u8, text) catch return .none;
+ if (p.repl_ask) |old| p.gpa.free(old.text);
+ p.repl_ask = .{ .from = src.serial, .lang = lang, .text = owned };
+ var w = std.Io.Writer.fixed(&src.question);
+ w.print("send to {s}:", .{grammars[lang].name}) catch {};
+ for (bound[0..n]) |i| w.print(" {c}", .{p.panes[i].?.repl.?.letter}) catch {};
+ w.writeAll(" (Esc: none)") catch {};
+ src.question_len = @intCast(w.buffered().len);
+ src.prompt = .repl_choice;
+ p.active = from;
+ return .asked;
+}
+
+/// Types `text` into the REPL in `to`, bracketed if its program asked for
+/// that, then Enter; says so on `from`'s message row and in the log.
+pub fn sendToRepl(p: *Pardes, from: usize, to: usize, text: []const u8) void {
+ const dst = p.panes[to] orelse return;
+ const src = p.panes[from] orelse return;
+ var idbuf: [48]u8 = undefined;
+ const id = replId(&idbuf, dst);
+ edit.typeToTty(p, to, dst, text);
+ p.emitWrite(to, "\r");
+ // Line by line, a blank line inside a Python block ends the block.
+ if (!panes.Terminal.bracketedPaste(dst) and !dst.repl_warned and std.mem.startsWith(u8, id, "python") and
+ std.mem.indexOfScalar(u8, text, '\n') != null)
+ {
+ dst.repl_warned = true;
+ var warn: [128]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&warn, "Repl: {s} takes no bracketed paste; a blank line in a block ends it", .{id}) catch "Repl: no bracketed paste");
+ }
+ var said: [64]u8 = undefined;
+ p.setMessage(from, std.fmt.bufPrint(&said, "\u{2192} {s}", .{id}) catch "sent");
+ src.msg_sent = true;
+ var rec: [96]u8 = undefined;
+ pardes.ctlfs.events.notePath(p, "send", std.fmt.bufPrint(&rec, "{d} {d} {s}", .{ src.serial, dst.serial, id }) catch return);
+}
+
/// The longest line a command pane runs.
pub const command_max = 1024;
@@ -381,6 +472,11 @@ const max_exec_depth = 8;
/// done, and which Kill signals. Answers the pane it went to.
pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
const pane = p.panes[id] orelse return null;
+ // Made by a gesture on a body, it may be a bound REPL's; run by name
+ // (`Exec ...`, which runs through here again) or written in a command
+ // word, it never is.
+ const gesture = p.exec_from_body and config.commandWord(std.mem.trim(u8, txt, " \t\r\n")) == null;
+ p.exec_from_body = false;
const cmd = commandText(txt);
if (cmd.len == 0) return null;
if (p.exec_depth == 0) p.noteHaptic(.exec);
@@ -388,6 +484,11 @@ pub fn execute(p: *Pardes, id: usize, txt: []const u8) ?usize {
if (p.exec_depth >= max_exec_depth) return null;
p.exec_depth += 1;
defer p.exec_depth -= 1;
+ if (gesture) switch (routeToRepl(p, id, cmd)) {
+ .none => {},
+ .sent => |to| return to,
+ .asked => return null,
+ };
if (!takesCommandLine(p, id)) return runCommand(p, id, cmd);
panes.Terminal.padOutputBelowEdits(p, id);
panes.Terminal.noteCommand(pane, cmd);
diff --git a/src/fs-help.txt b/src/fs-help.txt
index 0e87f40e..79701628 100644
--- a/src/fs-help.txt
+++ b/src/fs-help.txt
@@ -41,5 +41,5 @@ Pitfalls, one each:
A terminal's body is a history snapshot frozen per open; pty/data is the live stream.
exec: a non-builtin line runs as a command pane ($SHELL -c; exit N shown, logged), a shell's typed in.
Through a mount a malformed write is EINVAL, a well-formed one that fails EIO: log's err says why.
- lock/unlock on a pane's ctl need a held fd (exec 3>ctl); a lock someone holds fails: retry.
- Closing a column's last pane leaves a +New in its place; Delcol closes the column.
+ lock/unlock need a held ctl fd (exec 3>ctl), a held lock fails: retry. A column's last pane -> +New.
+ Repl python on a terminal's ctl: a .py body's clicks go to it; tag words, Exec x, @`x` stay commands.
diff --git a/src/mouse.zig b/src/mouse.zig
index 0ec20df2..f222b5da 100644
--- a/src/mouse.zig
+++ b/src/mouse.zig
@@ -190,7 +190,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
else => false,
}) for (p.panes) |slot| {
const pane = slot orelse continue;
- if (pane.prompt == .del_side) pane.prompt = .none;
+ if (pane.prompt == .del_side or pane.prompt == .repl_choice) pane.prompt = .none;
};
if (m.button == .back or m.button == .forward) {
if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1);
@@ -719,6 +719,9 @@ fn dispatchPointerBuiltin(
// nothing, as acme's look3 and execute return on an empty expansion.
const operand = text orelse return;
if (gesture) |g| if (reportGesture(p, id, cmd, operand, g.on_tag, g.operand, arg != null)) return;
+ // A middle click on a body is the exec a bound REPL may take.
+ p.exec_from_body = cmd == config.exec_cmd and gesture != null and !gesture.?.on_tag;
+ defer p.exec_from_body = false;
exec.runBuiltin(p, cmd, id, "", exec.withArg(p, operand, arg));
}
diff --git a/src/panes.zig b/src/panes.zig
index 2f8b6b56..9234912f 100644
--- a/src/panes.zig
+++ b/src/panes.zig
@@ -72,6 +72,10 @@ pub const Pane = struct {
/// Del from the keyboard with open panes both above and below: which
/// one takes the rows. Nothing is typed; one key answers.
del_side,
+ /// An exec from a file's body with several REPLs bound for its
+ /// language: which one takes it (exec.zig routeToRepl). One key
+ /// answers, the question being in `question`.
+ repl_choice,
/// A new name for the file, drafted in place of the path on the
/// tagline after a click there; Enter commits it, as a buffer name
/// and never a disk rename.
@@ -116,6 +120,18 @@ pub const Pane = struct {
/// (Pardes.acknowledgeShell); empty until the host says which it ran.
shell_tail: [96]u8 = undefined,
shell_tail_len: u8 = 0,
+ /// A terminal bound as a language's REPL (`Repl python`): an exec from
+ /// the body of a file in that language is sent to it (exec.zig
+ /// routeToRepl). `letter` makes its id, `python-a`, with the language's
+ /// name from grammar_manifest.
+ repl: ?struct { lang: u8, letter: u8 } = null,
+ /// It has been told that its program takes no bracketed paste.
+ repl_warned: bool = false,
+ /// A question with its answers in it (`repl_choice`).
+ question: [96]u8 = undefined,
+ question_len: u8 = 0,
+ /// The newest message says where an exec was sent: it is tinted.
+ msg_sent: bool = false,
/// The revision an Exit or a Restore last warned this pane was modified
/// at, and which of them: only that word goes ahead on the warning
/// (builtins.zig warnModified).
@@ -360,6 +376,7 @@ pub const Pane = struct {
return switch (p.prompt) {
.none, .name => null,
.del_side => config.del_question,
+ .repl_choice => p.question[0..p.question_len],
else => p.input.own orelse "",
};
}
diff --git a/src/pardes.zig b/src/pardes.zig
index 5ba95be3..9821839f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1391,6 +1391,100 @@ test "a spawn is for the pane it was made for, not whatever took the slot since"
try std.testing.expectEqualStrings("second", Count.command[0.."second".len]);
}
+test "a REPL bound for a language takes the exec made on a body in it; tags, Exec by name and command words still run" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .shells = 3, .cols = 100, .rows = 30 });
+ defer p.deinit();
+ while (p.nextEffect()) |_| {}
+ const file = try p.setTestFile("print(1)\nls\n");
+ gpa.free(file.file.?.path);
+ file.file.?.path = try gpa.dupe(u8, "/x.py");
+ const Sent = struct {
+ var to: [MAX_PANES][128]u8 = undefined;
+ var len: [MAX_PANES]usize = @splat(0);
+ var spawned: bool = false;
+ fn drain(pp: *Pardes) void {
+ len = @splat(0);
+ spawned = false;
+ while (pp.nextEffect()) |effect| switch (effect) {
+ .write => |w| {
+ @memcpy(to[w.pane][len[w.pane]..][0..w.bytes.slice().len], w.bytes.slice());
+ len[w.pane] += w.bytes.slice().len;
+ },
+ .spawn => spawned = true,
+ else => {},
+ };
+ }
+ fn said(pane: usize) []const u8 {
+ return to[pane][0..len[pane]];
+ }
+ };
+ // Only a terminal is a REPL; a language's name is the syntax table's.
+ try std.testing.expect(p.executeBuiltinLine(0, "Repl python"));
+ try std.testing.expect(file.repl == null);
+ try std.testing.expect(p.executeBuiltinLine(1, "Repl pithon"));
+ try std.testing.expect(p.panes[1].?.repl == null);
+ try std.testing.expect(std.mem.indexOf(u8, p.panes[1].?.msg[0..p.panes[1].?.msg_len], "python") != null);
+ try std.testing.expect(p.executeBuiltinLine(1, "Repl python"));
+ try std.testing.expect(p.executeBuiltinLine(1, "Repl python")); // again: no change
+ var idbuf: [48]u8 = undefined;
+ try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?));
+ try std.testing.expect(std.mem.endsWith(u8, try tagline.tagPrefix(p, p.panes[1].?), " python-a"));
+ Sent.drain(p);
+
+ // A click on the file's body, a single word: typed into the REPL.
+ p.exec_from_body = true;
+ try std.testing.expectEqual(@as(?usize, 1), exec.execute(p, 0, "ls"));
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("ls\r", Sent.said(1));
+ try std.testing.expect(!Sent.spawned);
+ try std.testing.expect(file.msg_sent);
+ try std.testing.expectEqualStrings("\u{2192} python-a", file.msg[0..file.msg_len]);
+ // Its own body too.
+ p.exec_from_body = true;
+ try std.testing.expectEqual(@as(?usize, 1), exec.execute(p, 1, "x = 1"));
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("x = 1\r", Sent.said(1));
+
+ // Escapes: a tag's word, Exec by name, a command word. Each a command.
+ _ = exec.execute(p, 0, "ls");
+ Sent.drain(p);
+ try std.testing.expect(Sent.spawned and Sent.len[1] == 0);
+ try std.testing.expect(p.executeBuiltinLine(0, "Exec ls"));
+ Sent.drain(p);
+ try std.testing.expect(Sent.spawned and Sent.len[1] == 0);
+ p.exec_from_body = true;
+ _ = exec.execute(p, 0, "@`ls`");
+ Sent.drain(p);
+ try std.testing.expect(Sent.spawned and Sent.len[1] == 0);
+
+ // Two bound: it asks, one key answers, Esc sends nowhere.
+ try std.testing.expect(p.executeBuiltinLine(2, "Repl python"));
+ try std.testing.expectEqualStrings("python-b", exec.replId(&idbuf, p.panes[2].?));
+ Sent.drain(p);
+ p.exec_from_body = true;
+ try std.testing.expectEqual(@as(?usize, null), exec.execute(p, 0, "print(2)"));
+ try std.testing.expectEqual(Pane.Prompt.repl_choice, file.prompt);
+ try std.testing.expect(std.mem.indexOf(u8, file.promptText().?, "a b") != null);
+ p.update(.{ .key = .{ .cp = 'b' } });
+ Sent.drain(p);
+ try std.testing.expectEqualStrings("print(2)\r", Sent.said(2));
+ try std.testing.expect(Sent.len[1] == 0 and file.prompt == .none);
+ p.exec_from_body = true;
+ _ = exec.execute(p, 0, "print(3)");
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ Sent.drain(p);
+ try std.testing.expect(Sent.len[1] == 0 and Sent.len[2] == 0);
+
+ // Unbound, its letter is free again; bare, it says what it is.
+ try std.testing.expect(p.executeBuiltinLine(1, "Repl -"));
+ try std.testing.expect(p.panes[1].?.repl == null);
+ try std.testing.expect(p.executeBuiltinLine(1, "Repl python"));
+ try std.testing.expectEqualStrings("python-a", exec.replId(&idbuf, p.panes[1].?));
+ try std.testing.expect(p.executeBuiltinLine(2, "Repl"));
+ try std.testing.expectEqualStrings("Repl: python-b", p.panes[2].?.msg[0..p.panes[2].?.msg_len]);
+}
+
test "Tty+fish, one word a tag can hold, opens a terminal on that shell" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
@@ -3929,6 +4023,13 @@ pub const Pardes = struct {
chord_arg: ?[]u8 = null,
/// how many `execute` calls are on the stack — see max_exec_depth
exec_depth: u8 = 0,
+ /// The exec under way was made by a gesture on a pane's body -- a
+ /// middle click, the execute key -- and so may go to a REPL bound for
+ /// the text's language; one run by name, `Exec ...`, never does.
+ exec_from_body: bool = false,
+ /// An exec waiting on the REPL question (`Pane.Prompt.repl_choice`):
+ /// the pane it came from, the language, and the text, owned.
+ repl_ask: ?struct { from: u32, lang: u8, text: []u8 } = null,
/// A key press is being handled: someone is at the keyboard to answer,
/// so a builtin may ask rather than guess. Nothing else sets it -- not
/// a click, a 9P write, a startup line or a pane's shell exiting.
@@ -4138,6 +4239,7 @@ pub const Pardes = struct {
pub fn deinit(p: *Pardes) void {
look.cancelLookHover(p);
+ if (p.repl_ask) |ask| p.gpa.free(ask.text);
for (0..MAX_PANES) |id| p.dropPendingWrite(id);
for (&p.panes) |*slot| if (slot.*) |pane| {
p.teardownPane(pane);
@@ -5015,6 +5117,18 @@ pub const Pardes = struct {
// pane or a header edit is in: that key is its answer.
for (p.panes, 0..) |slot, id| {
const asked = slot orelse continue;
+ if (asked.prompt == .repl_choice) {
+ asked.prompt = .none;
+ const ask = p.repl_ask orelse return;
+ p.repl_ask = null;
+ defer p.gpa.free(ask.text);
+ // Its letter sends it there; anything else takes it back.
+ if (key.ctrl or key.alt or key.cp > 'z' or key.cp < 'a') return;
+ for (p.panes, 0..) |target, to| if (target) |repl| if (repl.repl) |r|
+ if (r.lang == ask.lang and r.letter == key.cp and exec.replLive(repl))
+ return exec.sendToRepl(p, id, to, ask.text);
+ return;
+ }
if (asked.prompt != .del_side) continue;
asked.prompt = .none;
if (hit(key, config.del_above)) exec.runBuiltin(p, .Del, id, "", "k");
@@ -5126,7 +5240,7 @@ pub const Pardes = struct {
look.submitSearch(p, p.active)
else if (normal.selRegexArmed(pane)) |_| if (target) |answered|
normal.applySelRegex(p, pane, answered, "", false),
- .none, .del_side, .name => unreachable,
+ .none, .del_side, .repl_choice, .name => unreachable,
}
pane.closePrompt();
if (target) |answered| {
@@ -5164,6 +5278,9 @@ pub const Pardes = struct {
return;
}
edit.pinPaneCursor(p, &pane.body);
+ // The execute key on a body is the exec a bound REPL may take.
+ p.exec_from_body = cmd == config.exec_cmd;
+ defer p.exec_from_body = false;
const explicit = (p.native_images and pane.hasPdfSelection()) or
(pane.body.vsel.active and pane.body.vsel.explicit) or pane.body.msel.active;
if (explicit) {
@@ -5999,12 +6116,16 @@ pub const Pardes = struct {
// A message coming or going blends toward the page it sits
// on; a grid cannot move it by less than a row.
var style = msg_style;
+ // Where an exec was sent to a REPL reads in the tag's name
+ // tint, set apart from what the editor says.
+ const ink = if (kind == .message and pane.msg_sent) chrome.tag_name_fg else chrome.tag_fg;
+ if (kind == .message and pane.msg_sent) style.fg = .{ .rgb = ink };
if (Messages.noticeLife(pane, i)) |life| {
// No page colour to fade to (a terminal's own default):
// the text dissolves into its chip instead.
const alpha = Messages.messageMotion(p, life).alpha;
const page = th.bg orelse chrome.tag_bg;
- style.fg = .{ .rgb = Messages.blendRgb(chrome.tag_fg, page, alpha) };
+ style.fg = .{ .rgb = Messages.blendRgb(ink, page, alpha) };
style.bg = .{ .rgb = Messages.blendRgb(chrome.tag_bg, page, alpha) };
}
s.fill(cx, row, chip, 1, .{ .bg = style.bg, .font_role = .tagline });
@@ -6014,7 +6135,7 @@ 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 or pane.prompt == .del_side) continue;
+ if (kind != .prompt or id != p.active or pane.prompt == .del_side or pane.prompt == .repl_choice) 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(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
diff --git a/src/tagline.zig b/src/tagline.zig
index 8c1efa7b..14bf5677 100644
--- a/src/tagline.zig
+++ b/src/tagline.zig
@@ -7,6 +7,7 @@ const modal = @import("modal.zig");
const ctlfs = @import("ninep/tree.zig");
const panes = @import("panes.zig");
const edit = @import("edit.zig");
+const exec = @import("exec.zig");
const config = @import("config.zig");
const pdf_enabled = panes.Pdf.enabled;
const dump = @import("dump.zig");
@@ -114,9 +115,17 @@ pub fn pathPrefix(arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
if (pane.command) |line| {
const shown = line[0..@min(line.len, 48)];
const more = if (shown.len < line.len) "..." else "";
- if (!pane.command_done) return std.fmt.allocPrint(arena, "{s} ({s}{s}) running", .{ pane.cwdSlice(), shown, more });
- if (pane.command_status) |n| return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit {d}", .{ pane.cwdSlice(), shown, more, n });
- return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit ?", .{ pane.cwdSlice(), shown, more });
+ var idbuf: [48]u8 = undefined;
+ const id = exec.replId(&idbuf, pane);
+ const gap = if (id.len > 0) " " else "";
+ if (!pane.command_done) return std.fmt.allocPrint(arena, "{s} ({s}{s}) running{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id });
+ if (pane.command_status) |n| return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit {d}{s}{s}", .{ pane.cwdSlice(), shown, more, n, gap, id });
+ return std.fmt.allocPrint(arena, "{s} ({s}{s}) exit ?{s}{s}", .{ pane.cwdSlice(), shown, more, gap, id });
+ }
+ // A bound REPL says what it is bound as, beside its Tty word.
+ if (pane.repl != null) {
+ var idbuf: [48]u8 = undefined;
+ return std.fmt.allocPrint(arena, "{s} {s}", .{ pane.cwdSlice(), exec.replId(&idbuf, pane) });
}
if (pane.file) |f| {
const name = if (pane.prompt == .name) pane.input.own orelse f.path else f.path;