feat: add terminal tabs and split pane controls to workspace UI
This commit is contained in:
@@ -145,6 +145,14 @@ interface SplitPaneInLayoutResult {
|
||||
paneId: string;
|
||||
}
|
||||
|
||||
interface SplitPaneEmptyInLayoutInput {
|
||||
layout: WorkspaceLayout;
|
||||
targetPaneId: string;
|
||||
position: "left" | "right" | "top" | "bottom";
|
||||
createNodeId: (prefix: "pane" | "group") => string;
|
||||
maxTreeDepth: number;
|
||||
}
|
||||
|
||||
interface MoveTabToPaneInLayoutInput {
|
||||
layout: WorkspaceLayout;
|
||||
tabId: string;
|
||||
@@ -1066,6 +1074,60 @@ export function splitPaneInLayout(input: SplitPaneInLayoutInput): SplitPaneInLay
|
||||
};
|
||||
}
|
||||
|
||||
export function splitPaneEmptyInLayout(input: SplitPaneEmptyInLayoutInput): SplitPaneInLayoutResult | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
if (!findPaneById(layout.root, input.targetPaneId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const direction = input.position === "left" || input.position === "right" ? "horizontal" : "vertical";
|
||||
const insertAfter = input.position === "right" || input.position === "bottom";
|
||||
|
||||
const targetPath = findPanePathById(layout.root, input.targetPaneId);
|
||||
invariant(targetPath, `Target pane not found: ${input.targetPaneId}`);
|
||||
const targetNode = getNodeAtPath(layout.root, targetPath);
|
||||
invariant(targetNode.kind === "pane", "Expected target pane");
|
||||
|
||||
const newPaneId = input.createNodeId("pane");
|
||||
const newPaneNode = createPaneNode({ id: newPaneId });
|
||||
|
||||
const parentPath = targetPath.slice(0, -1);
|
||||
const targetIndex = targetPath[targetPath.length - 1] ?? 0;
|
||||
const parentNode = parentPath.length > 0 ? getNodeAtPath(layout.root, parentPath) : null;
|
||||
|
||||
let nextRoot: SplitNodeInternal;
|
||||
if (parentNode?.kind === "group" && parentNode.group.direction === direction) {
|
||||
const targetSize = parentNode.group.sizes[targetIndex] ?? 0;
|
||||
const nextSizes = parentNode.group.sizes.slice();
|
||||
const insertIndex = insertAfter ? targetIndex + 1 : targetIndex;
|
||||
nextSizes.splice(insertIndex, 0, targetSize / 2);
|
||||
nextSizes[targetIndex + (insertAfter ? 0 : 1)] = targetSize / 2;
|
||||
nextRoot = replaceNodeAtPath(layout.root, parentPath, () =>
|
||||
insertChildIntoGroup(parentNode, { index: insertIndex, node: newPaneNode, sizes: nextSizes })
|
||||
);
|
||||
} else {
|
||||
const newGroup = createGroupNode({
|
||||
id: input.createNodeId("group"),
|
||||
direction,
|
||||
children: insertAfter ? [targetNode, newPaneNode] : [newPaneNode, targetNode],
|
||||
sizes: [0.5, 0.5],
|
||||
});
|
||||
nextRoot = replaceNodeAtPath(layout.root, targetPath, () => newGroup);
|
||||
}
|
||||
|
||||
if (getTreeDepth(nextRoot) > input.maxTreeDepth) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
paneId: newPaneId,
|
||||
layout: {
|
||||
root: nextRoot,
|
||||
focusedPaneId: newPaneId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function moveTabToPaneInLayout(input: MoveTabToPaneInLayoutInput): WorkspaceLayout | null {
|
||||
const layout = asInternalLayout(input.layout);
|
||||
const sourcePane = findPaneContainingTab(layout.root, input.tabId);
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
reorderFocusedPaneTabsInLayout,
|
||||
reorderPaneTabsInLayout,
|
||||
retargetTabInLayout,
|
||||
splitPaneEmptyInLayout,
|
||||
splitPaneInLayout,
|
||||
type SplitGroup,
|
||||
type SplitNode,
|
||||
@@ -66,6 +67,13 @@ interface WorkspaceLayoutStore {
|
||||
position: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
) => string | null;
|
||||
splitPaneEmpty: (
|
||||
workspaceKey: string,
|
||||
input: {
|
||||
targetPaneId: string;
|
||||
position: "left" | "right" | "top" | "bottom";
|
||||
}
|
||||
) => string | null;
|
||||
moveTabToPane: (workspaceKey: string, tabId: string, toPaneId: string) => void;
|
||||
focusPane: (workspaceKey: string, paneId: string) => void;
|
||||
resizeSplit: (workspaceKey: string, groupId: string, sizes: number[]) => void;
|
||||
@@ -252,6 +260,39 @@ export const useWorkspaceLayoutStore = create<WorkspaceLayoutStore>()(
|
||||
|
||||
return result.paneId;
|
||||
},
|
||||
splitPaneEmpty: (workspaceKey, input) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTargetPaneId = trimNonEmpty(input.targetPaneId);
|
||||
if (!normalizedWorkspaceKey || !normalizedTargetPaneId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = splitPaneEmptyInLayout({
|
||||
layout: getWorkspaceLayout(get().layoutByWorkspace, normalizedWorkspaceKey),
|
||||
targetPaneId: normalizedTargetPaneId,
|
||||
position: input.position,
|
||||
maxTreeDepth: MAX_TREE_DEPTH,
|
||||
createNodeId: (prefix) => {
|
||||
const randomValue =
|
||||
typeof globalThis.crypto?.randomUUID === "function"
|
||||
? globalThis.crypto.randomUUID()
|
||||
: `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
return `${prefix}_${randomValue}`;
|
||||
},
|
||||
});
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
layoutByWorkspace: {
|
||||
...state.layoutByWorkspace,
|
||||
[normalizedWorkspaceKey]: result.layout,
|
||||
},
|
||||
}));
|
||||
|
||||
return result.paneId;
|
||||
},
|
||||
moveTabToPane: (workspaceKey, tabId, toPaneId) => {
|
||||
const normalizedWorkspaceKey = trimNonEmpty(workspaceKey);
|
||||
const normalizedTabId = trimNonEmpty(tabId);
|
||||
|
||||
Reference in New Issue
Block a user