summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 14:31:42 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commiteeadda6e7db3334e15708de2d7b5396f539c8186 (patch)
tree0852baa53df5df01dcb41b48f787044fae78fdf6 /src/pardes.zig
parent8d93016e923565e2e8b73aa7b549dd65b5d18a8c (diff)
downloadpardes-eeadda6e7db3334e15708de2d7b5396f539c8186.tar.gz
pardes-eeadda6e7db3334e15708de2d7b5396f539c8186.zip
A terminal bound as a language's REPL takes the execs made on that language's files
Repl python in a terminal's tag or on its ctl binds it (Repl - unbinds, Repl bare says the binding, the same word again changes nothing), with an id, python-a, python-b, in its tag. An exec made by a gesture on the body of a file in that language -- a middle click or the execute key, on any text, a single word too -- or on the REPL's own body is typed into it, bracketed if its program asked for that, else line by line with a once-said warning for Python's blank lines, then Enter; the message row says -> python-a in the tag's name tint, the log send <from> <to> <id>. Several bound for the language: the pane asks which on its notice band, as Del asks, one key answering, Esc none, nothing remembered. The gesture is known where it is dispatched (mouse.zig, the execute key), so the tag's words, Exec <text> run by name and a command word @`cmd` stay commands, and a 9P exec is never sent. A finished command pane is no REPL; bindings are not dumped. The docs spell out the escapes, the pdb and input() caveat and the no-bracketed-paste papercut. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig127
1 files changed, 124 insertions, 3 deletions
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)]);