| 1 | import { asArray } from "./array"; |
| 2 | import { isRuntimeSessionNode, isTopicNode, type WorkbenchSortMode } from "./projectTreeTopic"; |
| 3 | import { topicActivityTime } from "./session"; |
| 4 | import type { ProjectNode } from "./types"; |
| 5 | import { projectSessionIdentity } from "./projectSessionIdentity"; |
| 6 | |
| 7 | export type PinnedTreeSections = { |
| 8 | pinned: ProjectNode[]; |
| 9 | projects: ProjectNode[]; |
| 10 | }; |
| 11 | |
| 12 | function topicSortValue(node: ProjectNode, sortMode: WorkbenchSortMode): number { |
| 13 | if (sortMode === "created") return node.createdAt || node.lastActivityAt || 0; |
| 14 | return topicActivityTime(node); |
| 15 | } |
| 16 | |
| 17 | function projectSortValue(node: ProjectNode, sortMode: WorkbenchSortMode): number { |
| 18 | return asArray(node.children).reduce((max, child) => { |
| 19 | if (!isTopicNode(child)) return max; |
| 20 | return Math.max(max, topicSortValue(child, sortMode)); |
| 21 | }, 0); |
| 22 | } |
| 23 | |
| 24 | function manualTopicOrder(a: ProjectNode, b: ProjectNode): number { |
| 25 | const aOrder = typeof a.sortOrder === "number" && a.sortOrder >= 0 ? a.sortOrder : Number.MAX_SAFE_INTEGER; |
| 26 | const bOrder = typeof b.sortOrder === "number" && b.sortOrder >= 0 ? b.sortOrder : Number.MAX_SAFE_INTEGER; |
| 27 | return aOrder === bOrder ? 0 : aOrder - bOrder; |
| 28 | } |
| 29 | |
| 30 | function sortWorkbenchChildren(children: ProjectNode[], sortMode: WorkbenchSortMode): ProjectNode[] { |
| 31 | return [...children].sort((a, b) => { |
| 32 | if (!isTopicNode(a) || !isTopicNode(b)) return 0; |
| 33 | if (Boolean(a.pinned) !== Boolean(b.pinned)) return a.pinned ? -1 : 1; |
| 34 | const manualOrder = manualTopicOrder(a, b); |
| 35 | if (manualOrder !== 0) return manualOrder; |
| 36 | const activityOrder = topicSortValue(b, sortMode) - topicSortValue(a, sortMode); |
| 37 | if (activityOrder !== 0) return activityOrder; |
| 38 | const aKey = projectSessionIdentity(a); |
| 39 | const bKey = projectSessionIdentity(b); |
| 40 | return aKey < bKey ? -1 : aKey > bKey ? 1 : 0; |
| 41 | }); |
| 42 | } |
| 43 | |
| 44 | export function arrangeWorkbenchTree( |
| 45 | nodes: ProjectNode[], |
| 46 | sortMode: WorkbenchSortMode, |
| 47 | ): ProjectNode[] { |
| 48 | return nodes.map((node) => { |
| 49 | if (node.kind !== "project" && node.kind !== "global_folder") return node; |
| 50 | return { ...node, children: sortWorkbenchChildren(asArray(node.children), sortMode) }; |
| 51 | }); |
| 52 | } |
| 53 | |
| 54 | function projectTreeTopicIdentity(node: ProjectNode): string | null { |
| 55 | if (!isTopicNode(node) && !isRuntimeSessionNode(node)) return null; |
| 56 | const global = node.kind === "global_topic" || node.kind === "global_session"; |
| 57 | return `${global ? "global" : "project"}\u001f${global ? "" : node.root ?? ""}\u001f${projectSessionIdentity(node)}`; |
| 58 | } |
| 59 | |
| 60 | export function splitPinnedProjectTree( |
| 61 | nodes: ProjectNode[], |
| 62 | sortMode: WorkbenchSortMode, |
| 63 | includePinnedProjects = true, |
| 64 | ): PinnedTreeSections { |
| 65 | const pinnedTopics: ProjectNode[] = []; |
| 66 | const pinnedProjects: ProjectNode[] = []; |
| 67 | const projects: ProjectNode[] = []; |
| 68 | // A pinned topic shell can coexist briefly with a runtime session projection. |
| 69 | // Collect identities first so its source folder cannot paint it twice. |
| 70 | const pinnedTopicIdentities = new Set<string>(); |
| 71 | for (const node of nodes) { |
| 72 | const identity = projectTreeTopicIdentity(node); |
| 73 | if (identity && node.pinned) pinnedTopicIdentities.add(identity); |
| 74 | if (node.kind !== "project" && node.kind !== "global_folder") continue; |
| 75 | for (const child of asArray(node.children)) { |
| 76 | const childIdentity = projectTreeTopicIdentity(child); |
| 77 | if (childIdentity && child.pinned) pinnedTopicIdentities.add(childIdentity); |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | for (const node of nodes) { |
| 82 | if (!node) continue; |
| 83 | const isFolder = node.kind === "project" || node.kind === "global_folder"; |
| 84 | if (!isFolder) { |
| 85 | const identity = projectTreeTopicIdentity(node); |
| 86 | if (node.pinned) pinnedTopics.push(node); |
| 87 | else if (!identity || !pinnedTopicIdentities.has(identity)) projects.push(node); |
| 88 | continue; |
| 89 | } |
| 90 | |
| 91 | if (includePinnedProjects && node.pinned && node.kind === "project") { |
| 92 | pinnedProjects.push(node); |
| 93 | continue; |
| 94 | } |
| 95 | |
| 96 | const nextChildren: ProjectNode[] = []; |
| 97 | for (const child of asArray(node.children)) { |
| 98 | const identity = projectTreeTopicIdentity(child); |
| 99 | if (isTopicNode(child) && child.pinned) { |
| 100 | pinnedTopics.push(child); |
| 101 | continue; |
| 102 | } |
| 103 | if (identity && pinnedTopicIdentities.has(identity)) continue; |
| 104 | nextChildren.push(child); |
| 105 | } |
| 106 | projects.push({ ...node, children: nextChildren }); |
| 107 | } |
| 108 | |
| 109 | pinnedTopics.sort((a, b) => topicSortValue(b, sortMode) - topicSortValue(a, sortMode)); |
| 110 | pinnedProjects.sort((a, b) => projectSortValue(b, sortMode) - projectSortValue(a, sortMode)); |
| 111 | return { pinned: [...pinnedTopics, ...pinnedProjects], projects }; |
| 112 | } |
| 113 |