Skip to content

Commit e86d6c7

Browse files
committed
fix comment #304909 (comment)
1 parent 210675f commit e86d6c7

7 files changed

Lines changed: 203 additions & 103 deletions

File tree

custom-editor-outline.md

Lines changed: 102 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
A 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
8893
class 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+
179209
webviewPanel.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

191226
And 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+
367407
class 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

Comments
 (0)