diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 136 |
1 files changed, 123 insertions, 13 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index 29f937d7..42729d50 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2104,7 +2104,7 @@ test "New opens an empty scratch below the caller, inheriting its directory" { try std.testing.expectEqualStrings("/tmp/pardes-scratch-dir", Pardes.paneDir(np)); p.setCwd(source, "/tmp/pardes-moved"); try std.testing.expectEqualStrings("/tmp/pardes-moved", Pardes.paneDir(np)); - try p.removePane(source); + try p.removePane(source, null); p.sync(); try std.testing.expect(np.cwd == .owned); try std.testing.expectEqualStrings("/tmp/pardes-moved", Pardes.paneDir(np)); @@ -2176,7 +2176,7 @@ test "owned cwd close copies are transactional across inherited chains" { const active = p.active; const live_bytes = allocator.allocated_bytes - allocator.freed_bytes; allocator.fail_index = allocator.alloc_index + failed_copy; - try std.testing.expectError(error.OutOfMemory, p.removePane(0)); + try std.testing.expectError(error.OutOfMemory, p.removePane(0, null)); try std.testing.expectEqual(parent, p.panes[0].?); try std.testing.expectEqual(parent, first.cwd.inherited); try std.testing.expectEqual(parent, second.cwd.inherited); @@ -2189,12 +2189,12 @@ test "owned cwd close copies are transactional across inherited chains" { try std.testing.expectEqual(live_bytes, allocator.allocated_bytes - allocator.freed_bytes); allocator.fail_index = std.math.maxInt(usize); - try p.removePane(0); + try p.removePane(0, null); try std.testing.expect(p.panes[0] == null); try std.testing.expect(first.cwd == .owned and second.cwd == .owned); try std.testing.expectEqual(first, grandchild.cwd.inherited); try std.testing.expect(first.cwdSlice().ptr != second.cwdSlice().ptr); - try p.removePane(first_id); + try p.removePane(first_id, null); try std.testing.expect(grandchild.cwd == .owned); p.sync(); p.sync(); @@ -5539,7 +5539,7 @@ test "vertical close samples only a canonical baseline containing that pane" { _ = frame.reset(.retain_capacity); const opening = try p.render(frame.allocator()); p.acknowledgePanelPresentation(opening.panelTracks()); - try p.removePane(1); + try p.removePane(1, null); p.sync(); try std.testing.expectEqual(@as(usize, 0), p.presentation.closing_len); @@ -5565,7 +5565,7 @@ test "vertical close samples only a canonical baseline containing that pane" { try std.testing.expectEqual(@as(usize, 0), canonical.panelTracks().len); p.acknowledgePanelPresentation(canonical.panelTracks()); const serial = p.panes[1].?.serial; - try p.removePane(1); + try p.removePane(1, null); p.sync(); try std.testing.expectEqual(@as(usize, 1), p.presentation.closing_len); @@ -6387,6 +6387,10 @@ pub const Pardes = struct { chord_arg: ?[]u8 = null, /// how many `execute` calls are on the stack — see max_exec_depth exec_depth: u8 = 0, + /// 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. + can_ask: bool = false, lsp_seq: u32 = 0, lsp_wait: ?struct { @@ -7794,14 +7798,18 @@ pub const Pardes = struct { ctlfs.events.notePtyOutput(p, o.pane, o.bytes); panes.Terminal.feedOutput(p, pane, o.bytes); }, - .eof => |e| p.removePane(e.pane) catch |err| { + .eof => |e| p.removePane(e.pane, null) catch |err| { if (p.panes[e.pane]) |pane| pane.mode = .normal; p.reportError(e.pane, "terminal exited; Del retries close", err); }, .lsp_resp => |r| p.lspResponse(r.id, r.rows), .pipe_resp => |r| p.pipeResponse(r.id, r.success, r.outputs, r.failure), .file_changed => |fc| _ = p.applyWatchedFileChanged(fc.pane, fc.bytes), - .key => |key| p.handleKey(key), + .key => |key| { + p.can_ask = true; + defer p.can_ask = false; + p.handleKey(key); + }, .mouse => |m| { if (m.kind == .release) switch (p.drag) { .border_v, .border_h, .move, .column_move => p.presentation.snap_once = true, @@ -8899,6 +8907,16 @@ pub const Pardes = struct { } fn handleKey(p: *Pardes, key: Key) void { + // A question waiting on a pane takes the next key, whatever mode the + // 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 != .del_side) continue; + asked.prompt = .none; + if (hit(key, config.del_above)) p.runBuiltin(.Del, id, "", "k"); + if (hit(key, config.del_below)) p.runBuiltin(.Del, id, "", "j"); + return; + } if (p.topbar_col != null) return p.topbarKey(key); const pane = p.panes[p.active] orelse return; if (pane.isTerminal() and pane.mode == .tty and !pane.tag_edit) { @@ -8988,7 +9006,7 @@ pub const Pardes = struct { p.applySelRegex(pane, "", false); pane.tag_tail_len = @min(at, pane.tag_tail_len); }, - .none => unreachable, + .none, .del_side => unreachable, } exitTagEdit(pane); pane.mode = .normal; @@ -11955,6 +11973,15 @@ pub const Pardes = struct { } fn handleMouse(p: *Pardes, m: Mouse) void { + // A click goes somewhere else: the question it leaves unanswered is + // taken back, and the pane stays. A wheel only looks around. + if (m.kind == .press and switch (m.button) { + .left, .middle, .right => true, + else => false, + }) for (p.panes) |slot| { + const pane = slot orelse continue; + if (pane.prompt == .del_side) pane.prompt = .none; + }; if (m.button == .back or m.button == .forward) { if (m.kind == .press) p.jumpBy(if (m.button == .back) -1 else 1); return; @@ -12768,7 +12795,7 @@ pub const Pardes = struct { panes.Terminal.scrollbar(tt).total > tt.rows) return; p.deinitPane(tt) catch |err| return p.reportError(tty_id, "close", err); layout.compute(p); // a just-stacked doc has no rect yet; absorb snaps to rows - layout.absorbVWeight(p, tty_id); + layout.absorbVWeight(p, tty_id, null); layout.removePane(p, tty_id); p.panes[tty_id] = null; if (p.active == tty_id) p.active = keep_id; @@ -13621,7 +13648,9 @@ pub const Pardes = struct { pane.tag_init = true; } - pub fn removePane(p: *Pardes, id: usize) !void { + /// Close a pane; its rows go to `to`, or where layout.absorbVWeight + /// sends them when that is null. + pub fn removePane(p: *Pardes, id: usize, to: ?usize) !void { const pane = p.panes[id] orelse return; // A column outlives its panes. Closing the last one leaves an empty // pane standing in its place and in its directory; only Delcol ever @@ -13647,7 +13676,7 @@ pub const Pardes = struct { layout.insert(p, f.col, f.idx + 1, free); break :heir free; }; - layout.absorbVWeight(p, id); + layout.absorbVWeight(p, id, to); layout.removePane(p, id); p.panes[id] = null; if (p.active == id) p.active = heir orelse p.prevFocus(id) orelse { @@ -15499,6 +15528,87 @@ 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 "Del from a key asks which neighbour takes the rows; nothing else asks" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 40 }); + defer p.deinit(); + var frame = std.heap.ArenaAllocator.init(std.testing.allocator); + defer frame.deinit(); + const top = p.active; + p.newScratchBelow(top); + const mid = p.active; + p.newScratchBelow(mid); + const low = p.active; + p.sync(); + var h = [_]u16{ p.rects[top].h, p.rects[mid].h, p.rects[low].h }; + + // SPC d on the middle pane asks on its notice band, and closes nothing yet. + p.active = mid; + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expect(p.panes[mid] != null); + try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[mid].?.prompt); + _ = try p.render(frame.allocator()); + try std.testing.expectEqual(Pane.Notices.Kind.prompt, p.panes[mid].?.notices.kinds[p.panes[mid].?.notices.len - 1]); + try std.testing.expectEqualStrings(config.del_question, p.panes[mid].?.promptText().?); + // j: the pane below takes every row, the one above keeps its own. + p.update(.{ .key = .{ .cp = 'j' } }); + try std.testing.expect(p.panes[mid] == null); + try std.testing.expectEqual(h[0], p.rects[top].h); + try std.testing.expectEqual(h[1] + h[2], p.rects[low].h); + + // Esc takes the question back and the pane stays; so does any other key, + // and so does a click. + p.newScratchBelow(top); + const again = p.active; + p.sync(); + h = .{ p.rects[top].h, p.rects[again].h, p.rects[low].h }; + try std.testing.expect(p.executeBuiltinLine(again, "Msg keep")); + for ([_]Key{ .{ .cp = Key.escape }, .{ .cp = 'x' } }) |answer| { + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expectEqual(Pane.Prompt.del_side, p.panes[again].?.prompt); + p.update(.{ .key = answer }); + try std.testing.expect(p.panes[again] != null); + try std.testing.expectEqual(Pane.Prompt.none, p.panes[again].?.prompt); + } + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + const r = p.rects[again]; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = r.x + 5, .row = r.y + 3 } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = r.x + 5, .row = r.y + 3 } }); + try std.testing.expect(p.panes[again] != null); + try std.testing.expectEqual(Pane.Prompt.none, p.panes[again].?.prompt); + try std.testing.expectEqual(h[1], p.rects[again].h); + + // A word that is not a side closes nothing. + try std.testing.expect(p.executeBuiltinLine(again, "Del x")); + try std.testing.expect(p.panes[again] != null); + // DelAbove is Del k, and a line that is not a key never asks. + try std.testing.expect(p.executeBuiltinLine(again, "DelAbove")); + p.sync(); + try std.testing.expect(p.panes[again] == null); + try std.testing.expectEqual(h[0] + h[1], p.rects[top].h); + try std.testing.expectEqual(h[2], p.rects[low].h); + + // Bare Del from anything but a key closes at once and gives the rows up, + // as it always has. + p.newScratchBelow(top); + const third = p.active; + p.sync(); + h = .{ p.rects[top].h, p.rects[third].h, p.rects[low].h }; + try std.testing.expect(p.executeBuiltinLine(third, "Del")); + p.sync(); + try std.testing.expect(p.panes[third] == null); + try std.testing.expectEqual(h[0] + h[1], p.rects[top].h); + + // With nothing open below, there is nothing to ask: SPC d closes at once. + p.active = low; + p.update(.{ .key = .{ .cp = ' ' } }); + p.update(.{ .key = .{ .cp = 'd' } }); + try std.testing.expect(p.panes[low] == null); +} + test "closing the last pane in a column leaves an empty one in its place" { if (platform == .web) return; const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 24 }); @@ -15513,7 +15623,7 @@ test "closing the last pane in a column leaves an empty one in its place" { const had = try std.testing.allocator.dupe(u8, p.panes[lone].?.cwdSlice()); defer std.testing.allocator.free(had); - try p.removePane(lone); + try p.removePane(lone, null); p.sync(); // The column outlives the pane: still two columns, and the one that was |
