diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 127 |
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)]); |
