diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/builtins.zig | 5 | ||||
| -rw-r--r-- | src/exec.zig | 4 | ||||
| -rw-r--r-- | src/layout.zig | 14 | ||||
| -rw-r--r-- | src/pardes.zig | 87 |
4 files changed, 86 insertions, 24 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index 76d7dfb0..ed42dfe3 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -790,7 +790,8 @@ pub const Newcol = struct { }; /// Close the pane. `Del k` gives its rows to the open pane above it and -/// `Del j` to the one below, each falling back to the other side. +/// `Del j` to the one below, each falling back to the other side. Focus on +/// it goes with its rows. pub const Del = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { @@ -821,8 +822,6 @@ pub const Del = struct { return; } c.p.removePane(c.id, to) catch |err| return c.p.reportError(c.id, "close", err); - // Del k|j is a direction: focus lands where the rows went. - if (to) |gainer| c.p.active = gainer; } }; diff --git a/src/exec.zig b/src/exec.zig index bf3ed1f0..9a4e0a89 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -347,7 +347,7 @@ pub fn evictLonePristineTty(p: *Pardes, col: usize, keep_id: usize) void { 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, null); + _ = 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; @@ -372,10 +372,8 @@ pub fn replaceStillborn(p: *Pardes) void { if (out.from != .cmd or out.from.cmd != .New) return; if (file.revision != 0 or file.content.len != 0) return; if (old.tag.own) |own| if (!std.mem.eql(u8, own, tagline.defaultTail(old))) return; - const was_active = p.active == old_id; layout.compute(p); // the joiner may have no rect yet; absorb snaps to rows p.removePane(old_id, new_id) catch |err| return p.reportError(old_id, "close", err); - if (was_active) p.active = new_id; } // ---- REPLs ---- diff --git a/src/layout.zig b/src/layout.zig index 8e44f1d9..855a3eb9 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -1000,7 +1000,7 @@ pub fn splitColumn(p: *Pardes, source_id: usize, id: usize, before: bool) bool { if (needs_rebase) old_weight *= 2; if (id == source_id) { if (p.col_n[source_col] <= 1) return false; - absorbVWeight(p, id, null); + _ = absorbVWeight(p, id, null); removePane(p, id); } else if (findPane(p, id) != null) return false; @@ -1041,9 +1041,11 @@ pub fn snapColWeights(p: *Pardes, c: usize) void { /// Hand a closing pane's rows to `to`. With none named they go up: past any /// results listing to the nearest pane above that is not one, or to the /// pane below when the closing one is first in its column. -pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void { - const f = findPane(p, id) orelse return; - if (p.col_n[f.col] <= 1) return; +/// Give a closing pane's rows to `to`, or to the nearest document above it +/// (the pane below for the top one); the pane that got them is returned. +pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) ?usize { + const f = findPane(p, id) orelse return null; + if (p.col_n[f.col] <= 1) return null; snapColWeights(p, f.col); var sib = to orelse if (f.idx > 0) p.col_panes[f.col][f.idx - 1] else p.col_panes[f.col][f.idx + 1]; var k = f.idx; @@ -1051,7 +1053,9 @@ pub fn absorbVWeight(p: *Pardes, id: usize, to: ?usize) void { sib = p.col_panes[f.col][k - 1]; if (p.panes[sib]) |pp| if (if (pp.file) |ff| panes.Output.fileTraits(ff.output).doc else true) break; } - if (p.panes[sib]) |s| s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h))); + const s = p.panes[sib] orelse return null; + s.vweight += @as(f32, @floatFromInt(@max(1, p.rects[id].h))); + return sib; } pub fn splitParent(p: *Pardes, want: usize) usize { diff --git a/src/pardes.zig b/src/pardes.zig index 85eae10f..20406895 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -5845,7 +5845,10 @@ pub const Pardes = struct { // ---- dump / load (acme-style: `pardes -l state.zon` restores a session) ---- /// Close a pane; its rows go to `to`, or where layout.absorbVWeight - /// sends them when that is null. + /// sends them when that is null. Focus on it follows its rows: to the + /// pane that grew into them, or to the scratch left standing in an + /// emptied column. Every route that closes a pane comes through here or + /// removeColumn, so none picks focus for itself, and none by recency. 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 @@ -5872,10 +5875,11 @@ pub const Pardes = struct { layout.insert(p, f.col, f.idx + 1, free); break :heir free; }; - layout.absorbVWeight(p, id, to); + const gainer = layout.absorbVWeight(p, id, to); + const beside = if (last_in_column) p.besideColumn(place.?.col, id) else null; layout.removePane(p, id); p.panes[id] = null; - if (p.active == id) p.active = heir orelse p.prevFocus(id) orelse { + if (p.active == id) p.active = heir orelse gainer orelse beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5884,6 +5888,11 @@ pub const Pardes = struct { pub fn removeColumn(p: *Pardes, id: usize) !void { const place = layout.findPane(p, id) orelse return; + const had_focus = layout.findPane(p, p.active); + const beside = if (had_focus != null and had_focus.?.col == place.col) + p.besideColumn(place.col, p.active) + else + null; var ids: [MAX_PANES]usize = undefined; var parents: [MAX_PANES]*Pane = undefined; const count = p.col_n[place.col]; @@ -5897,7 +5906,7 @@ pub const Pardes = struct { layout.removePane(p, closed); p.panes[closed] = null; } - if (p.panes[p.active] == null) p.active = p.prevFocus(p.active) orelse { + if (p.panes[p.active] == null) p.active = beside orelse p.anyPane() orelse { p.quit = true; p.emit(.quit); return; @@ -5920,16 +5929,26 @@ pub const Pardes = struct { return null; } - pub fn prevFocus(p: *Pardes, closing: usize) ?usize { - var i = p.njumps; - while (i > 0) { - i -= 1; - const id = p.jumps[i].pane; - if (id != closing and p.panes[id] != null) return id; - } - for (p.panes, 0..) |slot, k| { - if (slot != null and k != closing) return k; + /// The pane that takes over `id`'s place when column `col` goes: in the + /// column layout.removePane gives the width to, the one level with the + /// middle of `id`. Asked before the column goes, while rects still hold. + fn besideColumn(p: *const Pardes, col: usize, id: usize) ?usize { + if (p.ncol < 2) return null; + const next = if (col > 0) col - 1 else col + 1; + const r = p.rects[id]; + const mid = r.y + r.h / 2; + var pick: ?usize = null; + for (p.col_panes[next][0..p.col_n[next]]) |k| { + if (p.panes[k] == null) continue; + pick = k; + if (p.rects[k].y +| p.rects[k].h > mid) break; } + return pick; + } + + /// Any live pane: the last resort when nothing took the closed one's place. + fn anyPane(p: *const Pardes) ?usize { + for (p.panes, 0..) |slot, k| if (slot != null) return k; return null; } @@ -7004,6 +7023,48 @@ test "Del from a key asks which neighbour takes the rows; nothing else asks" { try std.testing.expect(p.panes[low] == null); } +test "focus on a closed pane goes to the pane that takes its place, not the one focused before" { + if (platform == .web) return; + const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 120, .rows = 40 }); + defer p.deinit(); + const shell = p.active; + p.newScratchColumn(shell); + const right = p.active; + p.sync(); + p.newScratchBelow(shell); + const low = p.active; + p.sync(); + // The dogfood case: `right` was focused just before `low`, but `shell` + // grows into `low`'s rows, so `shell` gets the focus. + try std.testing.expect(p.executeBuiltinLine(low, "Del")); + p.sync(); + try std.testing.expect(p.panes[low] == null); + try std.testing.expectEqual(shell, p.active); + + // Closing a pane that has no focus leaves the focus where it is. + p.newScratchBelow(shell); + const other = p.active; + p.sync(); + p.active = right; + p.sync(); + try std.testing.expect(p.executeBuiltinLine(other, "Del")); + try std.testing.expectEqual(right, p.active); + + // Delcol: the column that takes the width holds, level with the closed + // pane, the pane that takes its place; `right` was focused just before. + p.newScratchBelow(right); + const right_low = p.active; + p.newScratchBelow(shell); + const left_low = p.active; + p.active = right; + p.sync(); + p.active = left_low; + p.sync(); + try p.removeColumn(left_low); + p.sync(); + try std.testing.expectEqual(right_low, p.active); +} + 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 }); |
