From 672b2643583d13feef5d47600e0f6c15f276fba2 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 02:32:33 -0300 Subject: No placement leaves a pane shorter than its tag and two rows Placement could split a pane down to a tag and one row, or give a new one less. Every placement now checks the pane it made and each pane it shrank against its tag (wrapped at its width) plus two rows: where they lack the room, the column's tallest pane is halved instead, and where none can give it the new pane is refused with "no space for a pane in that column" (ENOSPC over 9P) and closed at the step's end, once its maker is done with it. splitBelow and splitParent leave two body rows (a boot layout, split before any row exists, is as before). Re-recorded by name: lookpanes (panes one or two rows lower) and splitparent (the 6-row squeeze now refuses); fs.py closes the scratches and long-named panes it no longer needs, since its 24-row session filled. Co-Authored-By: Claude Opus 5.5 --- src/ninep/tree.zig | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) (limited to 'src/ninep/tree.zig') diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig index aec3cab7..414a0b6e 100644 --- a/src/ninep/tree.zig +++ b/src/ninep/tree.zig @@ -713,8 +713,11 @@ fn open(p: *Pardes, req: Req, target: Target) Reply { // closing it leaves the pane alone. .new => { const slot = p.freeSlot() orelse return failText(req.tag, E.NOSPC, std.fmt.bufPrint(&p.fs.ename, "pane/new: " ++ pardes.Messages.no_pane_slots, .{pardes.MAX_PANES}) catch "no space for a pane"); + p.fs.no_pane_slot = false; p.newScratchFor(p.active, .fs); const made = p.panes[slot] orelse return Reply.fail(req.tag, E.NFILE); + // No room in its column: it closes at the step's end. + if (p.unplaced.isSet(slot)) return failText(req.tag, E.NOSPC, p.fs.ename[0..p.fs.no_pane_slot_len]); return .{ .tag = req.tag, .handle = made.serial }; }, .screen => .{ .snapshot = null }, @@ -1427,6 +1430,8 @@ test "editor paths resolve to the same nodes the wire serves" { test "at the pane cap, pane/new, look and New each say so, and look reads back empty" { const p = try th.withFile(testing.allocator, "x\n"); defer p.deinit(); + // Tall enough that the column holds every pane at its tag and two rows. + p.update(.{ .resize = .{ .cols = 80, .rows = pardes.MAX_PANES * 4 } }); const new = @intFromEnum(TopFile.new); while (p.freeSlot() != null) try testing.expectEqual(Status.ok, call(p, .{ .tag = 1, .op = .open, .node = new }).reply.status); var want: [64]u8 = undefined; @@ -1496,3 +1501,24 @@ test "look and exec read back what their own open's write touched; a fresh open try testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{d}\n", .{said}), call(p, .{ .tag = 9, .op = .read, .node = exec, .handle = fresh, .size = 64 }).bytes); for ([_]u32{ mine, theirs, fresh }) |hh| _ = call(p, .{ .tag = 10, .op = .release, .node = exec, .handle = hh, .opened = true }); } + +test "no placement leaves a pane shorter than its tag and two rows; a full column refuses the next" { + const p = try th.withFile(testing.allocator, "x\n"); + defer p.deinit(); + const new = @intFromEnum(TopFile.new); + var made: usize = 1; + const refused = while (made < pardes.MAX_PANES) : (made += 1) { + const a = call(p, .{ .tag = 1, .op = .open, .node = new }); + if (a.reply.status == .err) break a; + } else return error.NeverRefused; + try testing.expectEqual(E.NOSPC, refused.errno()); + try testing.expect(std.mem.indexOf(u8, refused.reply.ename, "no space for a pane in that column") != null); + p.sync(); + var count: usize = 0; + for (p.panes) |slot| count += @intFromBool(slot != null); + try testing.expectEqual(made, count); // the refused one is gone + for (0..p.ncol) |c| for (p.col_panes[c][0..p.col_n[c]]) |k| { + if (p.col_n[c] > 1 and !p.panes[k].?.collapsed) + try testing.expect(p.rects[k].h >= p.panes[k].?.tag_rows + pardes.layout.min_body_rows); + }; +} -- cgit v1.3