88
99A full proposed extension API was added so that extensions providing ** Custom Editors** can also populate the ** Outline view** , ** Breadcrumbs** , and ** Go to Symbol** quick-pick with their own tree of items — with support for active element tracking, reveal-on-click, context menus, and inline toolbar buttons.
1010
11+ All provider methods and events are scoped per document URI, so multiple editors of the same view type can be open simultaneously (e.g. split view) with independent outlines.
12+
1113### Architecture Overview
1214
1315```
@@ -20,10 +22,11 @@ A full proposed extension API was added so that extensions providing **Custom Ed
2022│ Your extension calls │ │ ▼ │
2123│ registerCustomEditor │ │ ICustomEditorOutline │
2224│ OutlineProvider(...) │ │ ProviderService │
23- │ │ │ │ │
24- └──────────────────────┘ │ ▼ │
25+ │ │ │ (routes by viewType+URI) │
26+ └──────────────────────┘ │ │ │
27+ │ ▼ │
2528 │ CustomEditorExtensionOutline│
26- │ (IOutline<> impl) │
29+ │ (IOutline<> per editor) │
2730 │ │ │
2831 │ ▼ │
2932 │ Outline Pane / Breadcrumbs│
@@ -84,47 +87,70 @@ export function activate(context: vscode.ExtensionContext) {
8487
8588### 3. Implement ` CustomEditorOutlineProvider `
8689
90+ One provider is registered per ` viewType ` , but all methods receive a ` Uri ` so the provider can manage state for multiple open documents independently.
91+
8792``` typescript
8893class MyOutlineProvider implements vscode .CustomEditorOutlineProvider {
8994
90- // --- Events ---
95+ // --- Events (scoped per document URI) ---
9196
92- private readonly _onDidChangeOutline = new vscode .EventEmitter <void >();
97+ private readonly _onDidChangeOutline = new vscode .EventEmitter <vscode . Uri >();
9398 readonly onDidChangeOutline = this ._onDidChangeOutline .event ;
9499
95- private readonly _onDidChangeActiveItem = new vscode .EventEmitter <string | undefined >();
100+ private readonly _onDidChangeActiveItem = new vscode .EventEmitter <{ uri : vscode . Uri ; itemId : string | undefined } >();
96101 readonly onDidChangeActiveItem = this ._onDidChangeActiveItem .event ;
97102
98- // --- State (updated from your webview) ---
103+ // --- Per-document state ---
99104
100- private _items: vscode .CustomEditorOutlineItem [] = [];
105+ private readonly _state = new Map < string , {
106+ items: vscode .CustomEditorOutlineItem [];
107+ webview : vscode .Webview ;
108+ }> ();
101109
102110 // --- Provider methods ---
103111
104- provideOutline(token : vscode .CancellationToken ): vscode .CustomEditorOutlineItem [] {
105- return this ._items ;
112+ provideOutline(uri : vscode . Uri , token : vscode .CancellationToken ): vscode .CustomEditorOutlineItem [] {
113+ return this ._state . get ( uri . toString ())?. items ?? [] ;
106114 }
107115
108- revealItem(itemId : string ): void {
116+ revealItem(uri : vscode . Uri , itemId : string ): void {
109117 // Called when the user clicks an outline node.
110- // Post a message to your webview to scroll to / select the element.
111- this ._currentWebview ?.postMessage ({
118+ // Post a message to the correct webview to scroll to / select the element.
119+ this ._state . get ( uri . toString ())?. webview ?.postMessage ({
112120 type: ' revealElement' ,
113121 id: itemId ,
114122 });
115123 }
116124
117- // --- Methods you call from your code ---
125+ // --- Methods you call from your editor provider ---
126+
127+ /** Call from resolveCustomTextEditor to register a webview for a document */
128+ setWebview(uri : vscode .Uri , webview : vscode .Webview ): void {
129+ const existing = this ._state .get (uri .toString ());
130+ if (existing ) {
131+ existing .webview = webview ;
132+ } else {
133+ this ._state .set (uri .toString (), { items: [], webview });
134+ }
135+ }
136+
137+ /** Call when a document's editor is disposed */
138+ removeDocument(uri : vscode .Uri ): void {
139+ this ._state .delete (uri .toString ());
140+ }
118141
119142 /** Call when the document structure changes */
120- updateStructure(items : vscode .CustomEditorOutlineItem []): void {
121- this ._items = items ;
122- this ._onDidChangeOutline .fire (); // triggers a refresh in the Outline view
143+ updateStructure(uri : vscode .Uri , items : vscode .CustomEditorOutlineItem []): void {
144+ const state = this ._state .get (uri .toString ());
145+ if (state ) {
146+ state .items = items ;
147+ }
148+ this ._onDidChangeOutline .fire (uri ); // triggers a refresh for this document's outline
123149 }
124150
125151 /** Call when the user selects/focuses a different element in the webview */
126- setActiveElement(itemId : string | undefined ): void {
127- this ._onDidChangeActiveItem .fire (itemId ); // highlights the item in the Outline
152+ setActiveElement(uri : vscode . Uri , itemId : string | undefined ): void {
153+ this ._onDidChangeActiveItem .fire ({ uri , itemId } ); // highlights the item in the Outline
128154 }
129155}
130156```
@@ -172,20 +198,29 @@ const items: vscode.CustomEditorOutlineItem[] = [
172198
173199### 5. Connect Your Webview
174200
175- Your webview should post messages when the structure changes or the user selects an element :
201+ Pass the document URI when calling outline provider methods from your custom editor :
176202
177203``` typescript
178204// In your CustomTextEditorProvider.resolveCustomTextEditor():
205+ const uri = document .uri ;
206+
207+ outlineProvider .setWebview (uri , webviewPanel .webview );
208+
179209webviewPanel .webview .onDidReceiveMessage (message => {
180210 switch (message .type ) {
181211 case ' structureChanged' :
182- outlineProvider .updateStructure (message .items );
212+ outlineProvider .updateStructure (uri , message .items );
183213 break ;
184214 case ' selectionChanged' :
185- outlineProvider .setActiveElement (message .elementId );
215+ outlineProvider .setActiveElement (uri , message .elementId );
186216 break ;
187217 }
188218});
219+
220+ // Clean up when the editor is disposed
221+ webviewPanel .onDidDispose (() => {
222+ outlineProvider .removeDocument (uri );
223+ });
189224```
190225
191226And your webview should listen for reveal requests:
@@ -364,35 +399,52 @@ export function activate(context: vscode.ExtensionContext) {
364399
365400// ─── Outline Provider ──────────────────────────────────────────
366401
402+ interface DocumentState {
403+ items: vscode .CustomEditorOutlineItem [];
404+ webview: vscode .Webview ;
405+ }
406+
367407class DesignerOutlineProvider implements vscode .CustomEditorOutlineProvider {
368- private readonly _onDidChangeOutline = new vscode.EventEmitter < void > ();
408+ private readonly _onDidChangeOutline = new vscode.EventEmitter < vscode . Uri > ();
369409 readonly onDidChangeOutline = this ._onDidChangeOutline .event ;
370410
371- private readonly _onDidChangeActiveItem = new vscode.EventEmitter < string | undefined > ();
411+ private readonly _onDidChangeActiveItem = new vscode.EventEmitter < { uri : vscode . Uri ; itemId : string | undefined } > ();
372412 readonly onDidChangeActiveItem = this ._onDidChangeActiveItem .event ;
373413
374- private _items: vscode .CustomEditorOutlineItem [] = [];
375- private _webview: vscode .Webview | undefined ;
414+ private readonly _documents = new Map < string, DocumentState> ();
376415
377- setWebview (webview: vscode .Webview ): void {
378- this ._webview = webview;
416+ setWebview (uri: vscode .Uri , webview: vscode .Webview ): void {
417+ const key = uri .toString ();
418+ const existing = this ._documents .get (key);
419+ if (existing) {
420+ existing .webview = webview;
421+ } else {
422+ this ._documents .set (key, { items: [], webview });
423+ }
379424 }
380425
381- updateItems (items: vscode .CustomEditorOutlineItem []): void {
382- this ._items = items;
383- this ._onDidChangeOutline .fire ();
426+ removeDocument (uri: vscode .Uri ): void {
427+ this ._documents .delete (uri .toString ());
384428 }
385429
386- setActive (itemId: string | undefined ): void {
387- this ._onDidChangeActiveItem .fire (itemId);
430+ updateItems (uri: vscode .Uri , items: vscode .CustomEditorOutlineItem []): void {
431+ const state = this ._documents .get (uri .toString ());
432+ if (state) {
433+ state .items = items;
434+ }
435+ this ._onDidChangeOutline .fire (uri);
436+ }
437+
438+ setActive (uri: vscode .Uri , itemId: string | undefined ): void {
439+ this ._onDidChangeActiveItem .fire ({ uri, itemId });
388440 }
389441
390- provideOutline (_token: vscode .CancellationToken ): vscode .CustomEditorOutlineItem [] {
391- return this ._items ;
442+ provideOutline (uri : vscode . Uri , _token: vscode .CancellationToken ): vscode .CustomEditorOutlineItem [] {
443+ return this ._documents . get ( uri . toString ()) ? . items ?? [] ;
392444 }
393445
394- revealItem (itemId: string): void {
395- this ._webview ? .postMessage ({ type: ' reveal' , id: itemId });
446+ revealItem (uri : vscode . Uri , itemId: string): void {
447+ this ._documents . get ( uri . toString ()) ? . webview ? .postMessage ({ type: ' reveal' , id: itemId });
396448 }
397449}
398450
@@ -408,11 +460,12 @@ class DesignerEditorProvider implements vscode.CustomTextEditorProvider {
408460 document : vscode .TextDocument ,
409461 webviewPanel: vscode .WebviewPanel ,
410462 ): Promise < void > {
463+ const uri = document .uri ;
411464 webviewPanel .webview .options = { enableScripts: true };
412- this .outline .setWebview (webviewPanel .webview );
465+ this .outline .setWebview (uri, webviewPanel .webview );
413466
414467 // Provide some initial outline items
415- this .outline .updateItems ([
468+ this .outline .updateItems (uri, [
416469 {
417470 id: ' root' ,
418471 label: ' Canvas' ,
@@ -438,10 +491,15 @@ class DesignerEditorProvider implements vscode.CustomTextEditorProvider {
438491 // Listen for webview messages
439492 webviewPanel .webview .onDidReceiveMessage (msg => {
440493 if (msg .type === ' selectionChanged' ) {
441- this .outline .setActive (msg .id );
494+ this .outline .setActive (uri, msg .id );
442495 }
443496 });
444497
498+ // Clean up when the editor is disposed
499+ webviewPanel .onDidDispose (() => {
500+ this .outline .removeDocument (uri);
501+ });
502+
445503 webviewPanel .webview .html = ` <!DOCTYPE html>
446504 <html><body>
447505 <h1>Visual Designer</h1>
@@ -465,9 +523,10 @@ class DesignerEditorProvider implements vscode.CustomTextEditorProvider {
465523
466524| Feature | How It Works |
467525|---------|-------------|
468- | **Outline tree** | ` provideOutline ()` returns your items → they appear in the Outline view |
469- | **Active element tracking** | Fire ` onDidChangeActiveItem` with an item ` id` → that node gets highlighted |
470- | **Reveal on click** | User clicks outline node → ` revealItem (id)` is called → you scroll the webview |
526+ | **Outline tree** | ` provideOutline (uri)` returns your items → they appear in the Outline view |
527+ | **Multi-editor support** | Each method receives a ` Uri` , so multiple editors of the same type get independent outlines |
528+ | **Active element tracking** | Fire ` onDidChangeActiveItem` with ` { uri, itemId }` → that node gets highlighted |
529+ | **Reveal on click** | User clicks outline node → ` revealItem (uri, id)` is called → you scroll the correct webview |
471530| **Breadcrumbs** | Automatically built from the active item's parent chain |
472531| **Go to Symbol** | Items appear in the Ctrl+Shift+O quick-pick |
473532| **Inline buttons** | Contribute to ` customEditor/ outline/ toolbar` with ` " group" : " inline" ` |
0 commit comments