Skip to content

Commit 3f5eee5

Browse files
feat(Sky): Wire extension tree views into workbench rendering pipeline
Add native data provider attachment so extension-registered tree views actually render in the workbench pane. Stock VS Code uses ExtHostContext RPC to set `treeView.dataProvider` via `MainThreadTreeViews.$registerTreeViewDataProvider` - we don't have that channel yet (Track A bring-up), so Sky bridges it by: 1. Exposing `TreeViewByViewId(id)` from the Output transform plugin - returns the same `ITreeView` the stock mainThread accesses via `Registry.as(ViewsRegistry).getView(id).treeView` 2. Attaching a data provider in `cel:tree-view:create` that calls `tree:getChildren` via `MountainIPCInvoke`, converting Cocoon's wire shape (`{handle, label, isCollapsed, icon}`) into the workbench's `ITreeItem` shape (`handle, collapsibleState, label: {label}, ...`). Side-panel observers retain access to extended fields like `icon` that the renderer ignores. 3. Retry logic (5 attempts, 150ms spacing) handles the case where the view is registered BEFORE the tree descriptor mounts - covers both async workbench init and collapsed-pane-not-yet-mounted. 4. New `cel:tree-view:refresh` handler calls `treeView.refresh()` so extensions that fire `onDidChangeTreeData` update both the native pane and Sky observers. 5. New `cel:tree-view:dispose` handler clears `dataProvider` so the workbench falls back to empty-state while keeping the pane registered. Dual-emit persists: the DOM CustomEvent `cel:tree-view:items` stays for any Sky/Astro observer (side-panel mirror, diagnostic inspector) to react without going through the workbench pipeline.
1 parent b4358aa commit 3f5eee5

1 file changed

Lines changed: 227 additions & 44 deletions

File tree

Source/Function/SkyBridge.ts

Lines changed: 227 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,33 @@ interface CelSearchService {
166166
provider: unknown,
167167
): { dispose(): void };
168168
}
169+
// `ITreeView` from `vs/workbench/common/views`. Only the shape Sky
170+
// actually writes to (`dataProvider`) is typed - the rest is optional
171+
// read-only metadata the stock pane handles.
172+
interface CelTreeView {
173+
dataProvider:
174+
| undefined
175+
| {
176+
getChildren(element?: {
177+
handle?: string;
178+
}): Promise<unknown[] | undefined>;
179+
isTreeEmpty?: boolean;
180+
};
181+
title?: string;
182+
description?: string | undefined;
183+
message?: string | undefined;
184+
refresh?(
185+
treeItems?: readonly unknown[],
186+
checkboxesChanged?: readonly unknown[],
187+
): Promise<void>;
188+
}
169189
interface CelServices {
170190
Statusbar: CelStatusbarService;
171191
Commands: CelCommandService;
172192
CommandRegistry: CelCommandRegistry;
173193
Search: CelSearchService;
194+
Views?: unknown;
195+
TreeViewByViewId?: (viewId: string) => CelTreeView | null;
174196
}
175197

176198
function GetServices(): CelServices | null {
@@ -1095,6 +1117,7 @@ export async function InstallSkyBridge(): Promise<void> {
10951117
SkyEvent.ThemeChange,
10961118
SkyEvent.TreeViewDispose,
10971119
SkyEvent.TreeViewCreate,
1120+
SkyEvent.TreeViewRefresh,
10981121
SkyEvent.TestRegistered,
10991122
SkyEvent.SCMProviderAdded,
11001123
SkyEvent.SCMProviderRemoved,
@@ -1132,57 +1155,217 @@ export async function InstallSkyBridge(): Promise<void> {
11321155
}
11331156

11341157
// ---- Tree-view data bridge ----
1135-
// Consumer for `cel:tree-view:create` - without this listener the fan-out
1136-
// above re-dispatches the event but nothing downstream listens, so every
1137-
// registered view logs `consumer-present=false` (F1.1 in HANDOFF §-10).
1138-
// The listener primes the viewer by requesting the root children through
1139-
// Mountain's `tree:getChildren` invoke - Mountain forwards to Cocoon's
1140-
// `$provideTreeChildren` and returns `{ items: [...] }`. We re-dispatch
1141-
// the children on `cel:tree-view:items` so any renderer shim can pick
1142-
// them up without an extra round-trip.
1158+
// Two-way wire so extension-registered tree views actually render:
1159+
//
1160+
// 1. **Native data provider attach**: workbench renders a tree view
1161+
// only when `treeView.dataProvider` is non-undefined. Stock VS
1162+
// Code sets this in `MainThreadTreeViews.$registerTreeViewDataProvider`
1163+
// via the ExtHostContext RPC - we don't have that channel yet
1164+
// (Track A bring-up from the coverage matrix), so we attach a
1165+
// data provider here that calls `tree:getChildren` via
1166+
// `MountainIPCInvoke`. `__CEL_SERVICES__.TreeViewByViewId(id)` is
1167+
// exposed by the Output transform plugin - it returns the same
1168+
// `ITreeView` the stock mainThread accesses via
1169+
// `Registry.as(ViewsRegistry).getView(id).treeView`.
1170+
//
1171+
// 2. **CustomEvent fan-out** (existing): the `cel:tree-view:items`
1172+
// DOM event stays so any Sky/Astro observer (side-panel mirror,
1173+
// diagnostic inspector) can react without going through the
1174+
// workbench tree rendering pipeline.
1175+
//
1176+
// If the view is registered BEFORE the tree descriptor is mounted,
1177+
// `TreeViewByViewId` returns null - retry on microtask + rAF (covers
1178+
// both async workbench init and the pane-is-collapsed-so-not-yet-mounted
1179+
// case). After 5 retries spaced 150 ms apart we give up and rely on
1180+
// whatever `$refresh` the extension issues next to re-trigger us.
11431181
if (typeof document !== "undefined") {
1182+
// Map Cocoon's `{handle, label: string, isCollapsed, icon: string}`
1183+
// wire shape (from `RequestRoutingHandler.$provideTreeChildren`)
1184+
// into the workbench's `ITreeItem` shape. The fields the tree
1185+
// renderer actually reads are `handle`, `collapsibleState`, and
1186+
// `label: { label: string }`. Icons can be promoted to `iconPath`
1187+
// once Mountain starts returning URI components - keep the
1188+
// field name `icon` exposed on the extended shape so side-panel
1189+
// observers can still use it.
1190+
const ToTreeItem = (
1191+
Raw: unknown,
1192+
Fallback: { ViewId: string; ParentHandle: string; Index: number },
1193+
) => {
1194+
const Wire = (Raw ?? {}) as Record<string, unknown>;
1195+
const Handle =
1196+
typeof Wire.handle === "string" && Wire.handle.length > 0
1197+
? Wire.handle
1198+
: `${Fallback.ViewId}/${Fallback.ParentHandle || "root"}/${Fallback.Index}`;
1199+
const Label =
1200+
typeof Wire.label === "string"
1201+
? { label: Wire.label }
1202+
: (Wire.label as { label?: string } | undefined)?.label
1203+
? (Wire.label as { label: string })
1204+
: { label: "" };
1205+
const CollapsibleState =
1206+
Wire.isCollapsed === true
1207+
? 1
1208+
: typeof Wire.collapsibleState === "number"
1209+
? Wire.collapsibleState
1210+
: 0;
1211+
// Pass through the full set of fields Cocoon's wire DTO
1212+
// carries. Any field the workbench tree renderer doesn't
1213+
// read is ignored silently; keeping them lets side-panel
1214+
// mirrors (diagnostic inspectors, test harnesses) see the
1215+
// same content the built-in tree does.
1216+
const Description =
1217+
typeof Wire.description === "string" ? Wire.description : undefined;
1218+
const Tooltip =
1219+
typeof Wire.tooltip === "string" ? Wire.tooltip : undefined;
1220+
const ContextValue =
1221+
typeof Wire.contextValue === "string" ? Wire.contextValue : undefined;
1222+
return {
1223+
handle: Handle,
1224+
collapsibleState: CollapsibleState,
1225+
label: Label,
1226+
icon:
1227+
typeof Wire.icon === "string" && Wire.icon.length > 0
1228+
? Wire.icon
1229+
: undefined,
1230+
description: Description,
1231+
tooltip: Tooltip,
1232+
resourceUri: Wire.resourceUri,
1233+
contextValue: ContextValue,
1234+
command: Wire.command,
1235+
accessibilityInformation: Wire.accessibilityInformation,
1236+
};
1237+
};
1238+
const ProvideChildren = async (
1239+
ViewId: string,
1240+
Element?: { handle?: string },
1241+
): Promise<unknown[]> => {
1242+
try {
1243+
const Response = (await invoke("MountainIPCInvoke", {
1244+
method: "tree:getChildren",
1245+
params: [
1246+
{
1247+
viewId: ViewId,
1248+
treeItemHandle: Element?.handle ?? "",
1249+
},
1250+
],
1251+
})) as { items?: unknown[] };
1252+
const RawItems = Array.isArray(Response?.items)
1253+
? Response.items
1254+
: [];
1255+
const ParentHandle = Element?.handle ?? "";
1256+
const Items = RawItems.map((Raw, Index) =>
1257+
ToTreeItem(Raw, {
1258+
ViewId,
1259+
ParentHandle,
1260+
Index,
1261+
}),
1262+
);
1263+
// Dual-emit: DOM CustomEvent for Sky-side observers
1264+
// (same shape as the workbench tree renderer sees so
1265+
// mirror panels don't need a second conversion).
1266+
document.dispatchEvent(
1267+
new CustomEvent("cel:tree-view:items", {
1268+
detail: {
1269+
viewId: ViewId,
1270+
parent: ParentHandle,
1271+
items: Items,
1272+
},
1273+
}),
1274+
);
1275+
return Items;
1276+
} catch (Error) {
1277+
invoke("RenderDevLog", {
1278+
Tag: "tree-view",
1279+
Message: `[TreeView] bridge-error view=${ViewId} err=${String(Error)}`,
1280+
tag: "tree-view",
1281+
message: `[TreeView] bridge-error view=${ViewId} err=${String(Error)}`,
1282+
}).catch(() => {});
1283+
return [];
1284+
}
1285+
};
1286+
const AttachDataProvider = (ViewId: string, Retries: number): void => {
1287+
const Services = GetServices();
1288+
const GetTreeView = Services?.TreeViewByViewId;
1289+
const TreeView =
1290+
typeof GetTreeView === "function" ? GetTreeView(ViewId) : null;
1291+
if (!TreeView) {
1292+
if (Retries <= 0) {
1293+
invoke("RenderDevLog", {
1294+
Tag: "tree-view",
1295+
Message: `[TreeView] attach-give-up view=${ViewId} (no workbench tree descriptor)`,
1296+
tag: "tree-view",
1297+
message: `[TreeView] attach-give-up view=${ViewId} (no workbench tree descriptor)`,
1298+
}).catch(() => {});
1299+
return;
1300+
}
1301+
setTimeout(() => AttachDataProvider(ViewId, Retries - 1), 150);
1302+
return;
1303+
}
1304+
if (TreeView.dataProvider) {
1305+
// Already wired (e.g. by a prior register for the same id
1306+
// during a reload). Keep the existing provider to respect
1307+
// any extension that registered their own.
1308+
return;
1309+
}
1310+
TreeView.dataProvider = {
1311+
async getChildren(Element?: { handle?: string }) {
1312+
const Items = await ProvideChildren(ViewId, Element);
1313+
return Items as any[];
1314+
},
1315+
};
1316+
invoke("RenderDevLog", {
1317+
Tag: "tree-view",
1318+
Message: `[TreeView] attach-ok view=${ViewId}`,
1319+
tag: "tree-view",
1320+
message: `[TreeView] attach-ok view=${ViewId}`,
1321+
}).catch(() => {});
1322+
};
11441323
document.addEventListener("cel:tree-view:create", (Event: Event) => {
11451324
const Detail = (Event as CustomEvent).detail as
11461325
| { viewId?: string; extensionId?: string }
11471326
| undefined;
11481327
const ViewId = Detail?.viewId ?? "";
11491328
if (!ViewId) return;
1150-
invoke<{ items?: unknown[] }>("MountainIPCInvoke", {
1151-
method: "tree:getChildren",
1152-
params: [{ viewId: ViewId, treeItemHandle: "" }],
1153-
})
1154-
.then((Response) => {
1155-
const Items = Array.isArray(Response?.items)
1156-
? Response.items
1157-
: [];
1158-
document.dispatchEvent(
1159-
new CustomEvent("cel:tree-view:items", {
1160-
detail: {
1161-
viewId: ViewId,
1162-
parent: "",
1163-
items: Items,
1164-
},
1165-
}),
1166-
);
1167-
try {
1168-
invoke<void>("RenderDevLog", {
1169-
Tag: "tree-view",
1170-
Message: `[TreeView] bridge-items view=${ViewId} count=${Items.length}`,
1171-
tag: "tree-view",
1172-
message: `[TreeView] bridge-items view=${ViewId} count=${Items.length}`,
1173-
}).catch(() => {});
1174-
} catch {}
1175-
})
1176-
.catch((Error) => {
1177-
try {
1178-
invoke<void>("RenderDevLog", {
1179-
Tag: "tree-view",
1180-
Message: `[TreeView] bridge-error view=${ViewId} err=${String(Error)}`,
1181-
tag: "tree-view",
1182-
message: `[TreeView] bridge-error view=${ViewId} err=${String(Error)}`,
1183-
}).catch(() => {});
1184-
} catch {}
1185-
});
1329+
AttachDataProvider(ViewId, 5);
1330+
// Prime the DOM fan-out with the initial children too so
1331+
// side-panel shims that mirror tree state don't need to wait
1332+
// for a user-triggered expand.
1333+
void ProvideChildren(ViewId, undefined);
1334+
});
1335+
1336+
// `cel:tree-view:refresh` - extension called `treeView.refresh()` or
1337+
// fired `onDidChangeTreeData`. Workbench re-queries `getChildren`
1338+
// via the provider we attached above when we call `treeView.refresh()`.
1339+
document.addEventListener("cel:tree-view:refresh", (Event: Event) => {
1340+
const Detail = (Event as CustomEvent).detail as
1341+
| { viewId?: string }
1342+
| undefined;
1343+
const ViewId = Detail?.viewId ?? "";
1344+
if (!ViewId) return;
1345+
const Services = GetServices();
1346+
const TreeView = Services?.TreeViewByViewId?.(ViewId);
1347+
if (TreeView?.refresh) {
1348+
TreeView.refresh().catch(() => {});
1349+
}
1350+
// Also re-prime the Sky observers.
1351+
void ProvideChildren(ViewId, undefined);
1352+
});
1353+
1354+
// `cel:tree-view:dispose` - extension disposed its tree data
1355+
// provider. Clear the native pane's dataProvider so the workbench
1356+
// falls back to the empty-state message. The pane stays registered
1357+
// (ViewsRegistry keeps it) - dispose only detaches the provider.
1358+
document.addEventListener("cel:tree-view:dispose", (Event: Event) => {
1359+
const Detail = (Event as CustomEvent).detail as
1360+
| { viewId?: string; handle?: string | number }
1361+
| undefined;
1362+
const ViewId = Detail?.viewId ?? "";
1363+
if (!ViewId) return;
1364+
const Services = GetServices();
1365+
const TreeView = Services?.TreeViewByViewId?.(ViewId);
1366+
if (TreeView && TreeView.dataProvider !== undefined) {
1367+
TreeView.dataProvider = undefined;
1368+
}
11861369
});
11871370
}
11881371

0 commit comments

Comments
 (0)