Skip to content

Commit a428452

Browse files
committed
A full proposed extension API was added so that extensions providing Custom Editors can also populate the Outline view
1 parent 30c0bd0 commit a428452

File tree

15 files changed

+1356
-0
lines changed

15 files changed

+1356
-0
lines changed

custom-editor-outline.md

Lines changed: 476 additions & 0 deletions
Large diffs are not rendered by default.

src/vs/platform/actions/common/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ export class MenuId {
220220
static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar');
221221
static readonly NotebookOutlineFilter = new MenuId('NotebookOutlineFilter');
222222
static readonly NotebookOutlineActionMenu = new MenuId('NotebookOutlineActionMenu');
223+
static readonly CustomEditorOutlineActionMenu = new MenuId('CustomEditorOutlineActionMenu');
224+
static readonly CustomEditorOutlineContext = new MenuId('CustomEditorOutlineContext');
223225
static readonly NotebookEditorLayoutConfigure = new MenuId('NotebookEditorLayoutConfigure');
224226
static readonly NotebookKernelSource = new MenuId('NotebookKernelSource');
225227
static readonly BulkEditTitle = new MenuId('BulkEditTitle');

src/vs/platform/extensions/common/extensionsApiProposals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ const _allApiProposals = {
203203
customEditorMove: {
204204
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts',
205205
},
206+
customEditorOutline: {
207+
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorOutline.d.ts',
208+
},
206209
dataChannels: {
207210
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataChannels.d.ts',
208211
},

src/vs/workbench/api/browser/extensionHost.contribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import './mainThreadPower.js';
7373
import './mainThreadWebviewManager.js';
7474
import './mainThreadWorkspace.js';
7575
import './mainThreadComments.js';
76+
import './mainThreadCustomEditorOutline.js';
7677
import './mainThreadNotebook.js';
7778
import './mainThreadNotebookKernels.js';
7879
import './mainThreadNotebookDocumentsAndEditors.js';
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { CancellationToken } from '../../../base/common/cancellation.js';
7+
import { Emitter, Event } from '../../../base/common/event.js';
8+
import { Disposable, DisposableMap, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
9+
import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';
10+
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
11+
import { ExtHostContext, ExtHostCustomEditorOutlineShape, MainContext, MainThreadCustomEditorOutlineShape } from '../common/extHost.protocol.js';
12+
import { ICustomEditorOutlineItemDto, ICustomEditorOutlineProviderService } from '../../contrib/customEditor/common/customEditorOutlineService.js';
13+
14+
class CustomEditorOutlineProviderEntry {
15+
private readonly _onDidChangeOutline = new Emitter<void>();
16+
readonly onDidChangeOutline = this._onDidChangeOutline.event;
17+
18+
private readonly _onDidChangeActiveItem = new Emitter<string | undefined>();
19+
readonly onDidChangeActiveItem = this._onDidChangeActiveItem.event;
20+
21+
private _activeItemId: string | undefined;
22+
23+
get activeItemId(): string | undefined { return this._activeItemId; }
24+
25+
fireDidChangeOutline(): void {
26+
this._onDidChangeOutline.fire();
27+
}
28+
29+
fireDidChangeActiveItem(itemId: string | undefined): void {
30+
this._activeItemId = itemId;
31+
this._onDidChangeActiveItem.fire(itemId);
32+
}
33+
34+
dispose(): void {
35+
this._onDidChangeOutline.dispose();
36+
this._onDidChangeActiveItem.dispose();
37+
}
38+
}
39+
40+
class CustomEditorOutlineProviderService extends Disposable implements ICustomEditorOutlineProviderService {
41+
declare readonly _serviceBrand: undefined;
42+
43+
private readonly _entries = this._register(new DisposableMap<string, CustomEditorOutlineProviderEntry>());
44+
45+
private readonly _onDidChange = this._register(new Emitter<void>());
46+
readonly onDidChange: Event<void> = this._onDidChange.event;
47+
48+
private _provideOutline?: (viewType: string, token: CancellationToken) => Promise<ICustomEditorOutlineItemDto[] | undefined>;
49+
private _revealItem?: (viewType: string, itemId: string) => void;
50+
51+
setDelegate(delegate: {
52+
provideOutline: (viewType: string, token: CancellationToken) => Promise<ICustomEditorOutlineItemDto[] | undefined>;
53+
revealItem: (viewType: string, itemId: string) => void;
54+
}): void {
55+
this._provideOutline = delegate.provideOutline;
56+
this._revealItem = delegate.revealItem;
57+
}
58+
59+
hasProvider(viewType: string): boolean {
60+
return this._entries.has(viewType);
61+
}
62+
63+
getProviderViewTypes(): string[] {
64+
return [...this._entries.keys()];
65+
}
66+
67+
async provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined> {
68+
if (this._provideOutline) {
69+
return this._provideOutline(viewType, token);
70+
}
71+
return undefined;
72+
}
73+
74+
revealItem(viewType: string, itemId: string): void {
75+
if (this._revealItem) {
76+
this._revealItem(viewType, itemId);
77+
}
78+
}
79+
80+
getActiveItemId(viewType: string): string | undefined {
81+
return this._entries.get(viewType)?.activeItemId;
82+
}
83+
84+
onDidChangeOutline(viewType: string): Event<void> {
85+
const entry = this._entries.get(viewType);
86+
return entry ? entry.onDidChangeOutline : Event.None;
87+
}
88+
89+
onDidChangeActiveItem(viewType: string): Event<string | undefined> {
90+
const entry = this._entries.get(viewType);
91+
return entry ? entry.onDidChangeActiveItem : Event.None;
92+
}
93+
94+
registerProvider(viewType: string): IDisposable {
95+
const entry = new CustomEditorOutlineProviderEntry();
96+
this._entries.set(viewType, entry);
97+
this._onDidChange.fire();
98+
return toDisposable(() => {
99+
this._entries.deleteAndDispose(viewType);
100+
this._onDidChange.fire();
101+
});
102+
}
103+
104+
unregisterProvider(viewType: string): void {
105+
this._entries.deleteAndDispose(viewType);
106+
this._onDidChange.fire();
107+
}
108+
109+
fireDidChangeOutline(viewType: string): void {
110+
this._entries.get(viewType)?.fireDidChangeOutline();
111+
}
112+
113+
fireDidChangeActiveItem(viewType: string, itemId: string | undefined): void {
114+
this._entries.get(viewType)?.fireDidChangeActiveItem(itemId);
115+
}
116+
}
117+
118+
registerSingleton(ICustomEditorOutlineProviderService, CustomEditorOutlineProviderService, InstantiationType.Delayed);
119+
120+
@extHostNamedCustomer(MainContext.MainThreadCustomEditorOutline)
121+
export class MainThreadCustomEditorOutline extends Disposable implements MainThreadCustomEditorOutlineShape {
122+
123+
private readonly _proxy: ExtHostCustomEditorOutlineShape;
124+
private readonly _registrations = this._register(new DisposableMap<string>());
125+
126+
constructor(
127+
context: IExtHostContext,
128+
@ICustomEditorOutlineProviderService private readonly _service: ICustomEditorOutlineProviderService,
129+
) {
130+
super();
131+
this._proxy = context.getProxy(ExtHostContext.ExtHostCustomEditorOutline);
132+
133+
// Wire the service delegate to call through to the ext host
134+
if (this._service instanceof CustomEditorOutlineProviderService) {
135+
this._service.setDelegate({
136+
provideOutline: (viewType, token) => this._proxy.$provideOutline(viewType, token),
137+
revealItem: (viewType, itemId) => this._proxy.$revealItem(viewType, itemId),
138+
});
139+
}
140+
}
141+
142+
$registerCustomEditorOutlineProvider(viewType: string): void {
143+
const registration = this._service.registerProvider(viewType);
144+
this._registrations.set(viewType, registration);
145+
}
146+
147+
$unregisterCustomEditorOutlineProvider(viewType: string): void {
148+
this._registrations.deleteAndDispose(viewType);
149+
this._service.unregisterProvider(viewType);
150+
}
151+
152+
$onDidChangeOutline(viewType: string): void {
153+
this._service.fireDidChangeOutline(viewType);
154+
}
155+
156+
$onDidChangeActiveItem(viewType: string, itemId: string | undefined): void {
157+
this._service.fireDidChangeActiveItem(viewType, itemId);
158+
}
159+
}

src/vs/workbench/api/common/extHost.api.impl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { IExtHostCommands } from './extHostCommands.js';
4747
import { createExtHostComments } from './extHostComments.js';
4848
import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration.js';
4949
import { ExtHostCustomEditors } from './extHostCustomEditors.js';
50+
import { ExtHostCustomEditorOutline } from './extHostCustomEditorOutline.js';
5051
import { IExtHostDataChannels } from './extHostDataChannels.js';
5152
import { IExtHostDebugService } from './extHostDebugService.js';
5253
import { IExtHostDecorations } from './extHostDecorations.js';
@@ -231,6 +232,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
231232
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.remote, extHostWorkspace, extHostLogService, extHostApiDeprecation));
232233
const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace));
233234
const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels));
235+
const extHostCustomEditorOutline = rpcProtocol.set(ExtHostContext.ExtHostCustomEditorOutline, new ExtHostCustomEditorOutline(rpcProtocol));
234236
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
235237
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, accessor.get(IExtHostTesting));
236238
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
@@ -979,6 +981,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
979981
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean } = {}) => {
980982
return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options);
981983
},
984+
registerCustomEditorOutlineProvider: (viewType: string, provider: vscode.CustomEditorOutlineProvider) => {
985+
return extHostCustomEditorOutline.registerCustomEditorOutlineProvider(extension, viewType, provider);
986+
},
982987
registerFileDecorationProvider(provider: vscode.FileDecorationProvider) {
983988
return extHostDecorations.registerFileDecorationProvider(provider, extension);
984989
},

src/vs/workbench/api/common/extHost.protocol.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
101101
import * as tasks from './shared/tasks.js';
102102
import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js';
103103
import { CDPEvent, CDPRequest, CDPResponse } from '../../../platform/browserView/common/cdp/types.js';
104+
import { ICustomEditorOutlineItemDto } from '../../contrib/customEditor/common/customEditorOutlineService.js';
104105

105106
export type IconPathDto =
106107
| UriComponents
@@ -1168,6 +1169,20 @@ export interface ExtHostCustomEditorsShape {
11681169
$onMoveCustomEditor(handle: WebviewHandle, newResource: UriComponents, viewType: string): Promise<void>;
11691170
}
11701171

1172+
export type { ICustomEditorOutlineItemDto } from '../../contrib/customEditor/common/customEditorOutlineService.js';
1173+
1174+
export interface MainThreadCustomEditorOutlineShape extends IDisposable {
1175+
$registerCustomEditorOutlineProvider(viewType: string): void;
1176+
$unregisterCustomEditorOutlineProvider(viewType: string): void;
1177+
$onDidChangeOutline(viewType: string): void;
1178+
$onDidChangeActiveItem(viewType: string, itemId: string | undefined): void;
1179+
}
1180+
1181+
export interface ExtHostCustomEditorOutlineShape {
1182+
$provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined>;
1183+
$revealItem(viewType: string, itemId: string): void;
1184+
}
1185+
11711186
export interface ExtHostWebviewViewsShape {
11721187
$resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, title: string | undefined, state: any, cancellation: CancellationToken): Promise<void>;
11731188

@@ -3775,6 +3790,7 @@ export const MainContext = {
37753790
MainThreadWebviewPanels: createProxyIdentifier<MainThreadWebviewPanelsShape>('MainThreadWebviewPanels'),
37763791
MainThreadWebviewViews: createProxyIdentifier<MainThreadWebviewViewsShape>('MainThreadWebviewViews'),
37773792
MainThreadCustomEditors: createProxyIdentifier<MainThreadCustomEditorsShape>('MainThreadCustomEditors'),
3793+
MainThreadCustomEditorOutline: createProxyIdentifier<MainThreadCustomEditorOutlineShape>('MainThreadCustomEditorOutline'),
37783794
MainThreadUrls: createProxyIdentifier<MainThreadUrlsShape>('MainThreadUrls'),
37793795
MainThreadUriOpeners: createProxyIdentifier<MainThreadUriOpenersShape>('MainThreadUriOpeners'),
37803796
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
@@ -3892,5 +3908,6 @@ export const ExtHostContext = {
38923908
ExtHostDataChannels: createProxyIdentifier<ExtHostDataChannelsShape>('ExtHostDataChannels'),
38933909
ExtHostChatSessions: createProxyIdentifier<ExtHostChatSessionsShape>('ExtHostChatSessions'),
38943910
ExtHostGitExtension: createProxyIdentifier<ExtHostGitExtensionShape>('ExtHostGitExtension'),
3911+
ExtHostCustomEditorOutline: createProxyIdentifier<ExtHostCustomEditorOutlineShape>('ExtHostCustomEditorOutline'),
38953912
ExtHostBrowsers: createProxyIdentifier<ExtHostBrowsersShape>('ExtHostBrowsers'),
38963913
};
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type * as vscode from 'vscode';
7+
import { CancellationToken } from '../../../base/common/cancellation.js';
8+
import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
9+
import { ExtHostCustomEditorOutlineShape, ICustomEditorOutlineItemDto, MainContext, MainThreadCustomEditorOutlineShape } from './extHost.protocol.js';
10+
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
11+
import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js';
12+
import { ThemeIcon } from '../../../base/common/themables.js';
13+
import { IRPCProtocol } from '../../services/extensions/common/proxyIdentifier.js';
14+
15+
export class ExtHostCustomEditorOutline implements ExtHostCustomEditorOutlineShape {
16+
17+
private readonly _proxy: MainThreadCustomEditorOutlineShape;
18+
private readonly _providers = new Map<string, { provider: vscode.CustomEditorOutlineProvider; disposables: DisposableStore }>();
19+
20+
constructor(
21+
mainContext: IRPCProtocol,
22+
) {
23+
this._proxy = mainContext.getProxy(MainContext.MainThreadCustomEditorOutline);
24+
}
25+
26+
registerCustomEditorOutlineProvider(
27+
extension: IExtensionDescription,
28+
viewType: string,
29+
provider: vscode.CustomEditorOutlineProvider,
30+
): vscode.Disposable {
31+
checkProposedApiEnabled(extension, 'customEditorOutline');
32+
33+
if (this._providers.has(viewType)) {
34+
throw new Error(`An outline provider for custom editor view type '${viewType}' is already registered`);
35+
}
36+
37+
const disposables = new DisposableStore();
38+
39+
this._providers.set(viewType, { provider, disposables });
40+
this._proxy.$registerCustomEditorOutlineProvider(viewType);
41+
42+
disposables.add(provider.onDidChangeOutline(() => {
43+
this._proxy.$onDidChangeOutline(viewType);
44+
}));
45+
46+
disposables.add(provider.onDidChangeActiveItem(itemId => {
47+
this._proxy.$onDidChangeActiveItem(viewType, itemId);
48+
}));
49+
50+
return toDisposable(() => {
51+
this._providers.delete(viewType);
52+
disposables.dispose();
53+
this._proxy.$unregisterCustomEditorOutlineProvider(viewType);
54+
});
55+
}
56+
57+
async $provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined> {
58+
const entry = this._providers.get(viewType);
59+
if (!entry) {
60+
return undefined;
61+
}
62+
const items = await entry.provider.provideOutline(token);
63+
if (!items) {
64+
return undefined;
65+
}
66+
return items.map(item => this._convertItem(item));
67+
}
68+
69+
$revealItem(viewType: string, itemId: string): void {
70+
const entry = this._providers.get(viewType);
71+
if (entry) {
72+
entry.provider.revealItem(itemId);
73+
}
74+
}
75+
76+
private _convertItem(item: vscode.CustomEditorOutlineItem): ICustomEditorOutlineItemDto {
77+
return {
78+
id: item.id,
79+
label: item.label,
80+
detail: item.detail,
81+
tooltip: item.tooltip,
82+
icon: ThemeIcon.isThemeIcon(item.icon) ? item.icon : undefined,
83+
contextValue: item.contextValue,
84+
children: item.children?.map(child => this._convertItem(child)),
85+
};
86+
}
87+
}

src/vs/workbench/contrib/customEditor/browser/customEditor.contribution.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { ICustomEditorService } from '../common/customEditor.js';
1414
import { WebviewEditor } from '../../webviewPanel/browser/webviewEditor.js';
1515
import { CustomEditorInput } from './customEditorInput.js';
1616
import { CustomEditorService } from './customEditors.js';
17+
import './customEditorOutline.js';
1718

1819
registerSingleton(ICustomEditorService, CustomEditorService, InstantiationType.Delayed);
1920

0 commit comments

Comments
 (0)