Fix equalize splits to recursively set all dividers to 0.5 (#575)

The equalize splits command was a no-op that always returned false.
Implement it by recursively walking the bonsplit tree and setting
every split divider position to 0.5. Also register the command in
the command palette with a "workspace has splits" precondition so
it only appears when there are multiple panes.

Adds a regression test that creates a nested split layout, skews
divider positions, equalizes, and verifies all dividers are at 0.5.

Fixes https://github.com/manaflow-ai/cmux/issues/571
This commit is contained in:
Lawrence Chen 2026-02-26 14:27:18 -08:00 committed by GitHub
parent b1846aaec4
commit 780f959a48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 114 additions and 3 deletions

View file

@ -1385,6 +1385,7 @@ struct ContentView: View {
static let workspaceHasCustomName = "workspace.hasCustomName"
static let workspaceShouldPin = "workspace.shouldPin"
static let workspaceHasPullRequests = "workspace.hasPullRequests"
static let workspaceHasSplits = "workspace.hasSplits"
static let hasFocusedPanel = "panel.hasFocus"
static let panelName = "panel.name"
@ -3373,6 +3374,10 @@ struct ContentView: View {
CommandPaletteContextKeys.workspaceHasPullRequests,
!workspace.sidebarPullRequestsInDisplayOrder().isEmpty
)
snapshot.setBool(
CommandPaletteContextKeys.workspaceHasSplits,
workspace.bonsplitController.allPaneIds.count > 1
)
}
if let panelContext = focusedPanelContext {
@ -3938,6 +3943,15 @@ struct ContentView: View {
when: { $0.bool(CommandPaletteContextKeys.panelIsTerminal) }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.equalizeSplits",
title: constant("Equalize Splits"),
subtitle: workspaceSubtitle,
keywords: ["split", "equalize", "balance", "divider", "layout"],
when: { $0.bool(CommandPaletteContextKeys.workspaceHasSplits) }
)
)
return contributions
}
@ -4180,6 +4194,13 @@ struct ContentView: View {
registry.register(commandId: "palette.terminalSplitBrowserDown") {
_ = tabManager.createBrowserSplit(direction: .down)
}
registry.register(commandId: "palette.equalizeSplits") {
guard let workspace = tabManager.selectedWorkspace,
tabManager.equalizeSplits(tabId: workspace.id) else {
NSSound.beep()
return
}
}
}
private var focusedPanelContext: (workspace: Workspace, panelId: UUID, panel: any Panel)? {

View file

@ -2006,9 +2006,17 @@ class TabManager: ObservableObject {
/// Equalize splits - not directly supported by bonsplit
func equalizeSplits(tabId: UUID) -> Bool {
// Bonsplit doesn't have a built-in equalize feature
// This would require manually setting all divider positions to 0.5
return false
guard let tab = tabs.first(where: { $0.id == tabId }) else { return false }
var foundSplit = false
var allSucceeded = true
equalizeSplits(
in: tab.bonsplitController.treeSnapshot(),
controller: tab.bonsplitController,
foundSplit: &foundSplit,
allSucceeded: &allSucceeded
)
return foundSplit && allSucceeded
}
/// Toggle zoom on a panel - bonsplit doesn't have zoom support
@ -2017,6 +2025,41 @@ class TabManager: ObservableObject {
return false
}
private func equalizeSplits(
in node: ExternalTreeNode,
controller: BonsplitController,
foundSplit: inout Bool,
allSucceeded: inout Bool
) {
switch node {
case .pane:
return
case .split(let splitNode):
foundSplit = true
guard let splitId = UUID(uuidString: splitNode.id) else {
allSucceeded = false
return
}
if !controller.setDividerPosition(0.5, forSplit: splitId) {
allSucceeded = false
}
equalizeSplits(
in: splitNode.first,
controller: controller,
foundSplit: &foundSplit,
allSucceeded: &allSucceeded
)
equalizeSplits(
in: splitNode.second,
controller: controller,
foundSplit: &foundSplit,
allSucceeded: &allSucceeded
)
}
}
/// Close a surface/panel
func closeSurface(tabId: UUID, surfaceId: UUID) -> Bool {
guard let tab = tabs.first(where: { $0.id == tabId }) else { return false }

View file

@ -4,6 +4,7 @@ import SwiftUI
import WebKit
import SwiftUI
import ObjectiveC.runtime
import Bonsplit
#if canImport(cmux_DEV)
@testable import cmux_DEV
@ -3317,6 +3318,52 @@ final class TabManagerSurfaceCreationTests: XCTestCase {
}
}
@MainActor
final class TabManagerEqualizeSplitsTests: XCTestCase {
func testEqualizeSplitsSetsEverySplitDividerToHalf() {
let manager = TabManager()
guard let workspace = manager.selectedWorkspace,
let leftPanelId = workspace.focusedPanelId,
let rightPanel = workspace.newTerminalSplit(from: leftPanelId, orientation: .horizontal),
workspace.newTerminalSplit(from: rightPanel.id, orientation: .vertical) != nil else {
XCTFail("Expected nested split setup to succeed")
return
}
let initialSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot())
XCTAssertGreaterThanOrEqual(initialSplits.count, 2, "Expected at least two split nodes in nested layout")
for (index, split) in initialSplits.enumerated() {
guard let splitId = UUID(uuidString: split.id) else {
XCTFail("Expected split ID to be a UUID")
return
}
let targetPosition: CGFloat = index.isMultiple(of: 2) ? 0.2 : 0.8
XCTAssertTrue(
workspace.bonsplitController.setDividerPosition(targetPosition, forSplit: splitId),
"Expected to seed divider position for split \(splitId)"
)
}
XCTAssertTrue(manager.equalizeSplits(tabId: workspace.id), "Expected equalize splits command to succeed")
let equalizedSplits = splitNodes(in: workspace.bonsplitController.treeSnapshot())
XCTAssertEqual(equalizedSplits.count, initialSplits.count)
for split in equalizedSplits {
XCTAssertEqual(split.dividerPosition, 0.5, accuracy: 0.000_1)
}
}
private func splitNodes(in node: ExternalTreeNode) -> [ExternalSplitNode] {
switch node {
case .pane:
return []
case .split(let split):
return [split] + splitNodes(in: split.first) + splitNodes(in: split.second)
}
}
}
@MainActor
final class WorkspaceTerminalConfigInheritanceSelectionTests: XCTestCase {
func testPrefersSelectedTerminalInTargetPaneOverFocusedTerminalElsewhere() {