Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
476 changes: 476 additions & 0 deletions custom-editor-outline.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/vs/platform/actions/common/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ export class MenuId {
static readonly NotebookOutputToolbar = new MenuId('NotebookOutputToolbar');
static readonly NotebookOutlineFilter = new MenuId('NotebookOutlineFilter');
static readonly NotebookOutlineActionMenu = new MenuId('NotebookOutlineActionMenu');
static readonly CustomEditorOutlineActionMenu = new MenuId('CustomEditorOutlineActionMenu');
static readonly CustomEditorOutlineContext = new MenuId('CustomEditorOutlineContext');
static readonly NotebookEditorLayoutConfigure = new MenuId('NotebookEditorLayoutConfigure');
static readonly NotebookKernelSource = new MenuId('NotebookKernelSource');
static readonly BulkEditTitle = new MenuId('BulkEditTitle');
Expand Down
3 changes: 3 additions & 0 deletions src/vs/platform/extensions/common/extensionsApiProposals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ const _allApiProposals = {
customEditorMove: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorMove.d.ts',
},
customEditorOutline: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.customEditorOutline.d.ts',
},
dataChannels: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dataChannels.d.ts',
},
Expand Down
1 change: 1 addition & 0 deletions src/vs/workbench/api/browser/extensionHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import './mainThreadPower.js';
import './mainThreadWebviewManager.js';
import './mainThreadWorkspace.js';
import './mainThreadComments.js';
import './mainThreadCustomEditorOutline.js';
import './mainThreadNotebook.js';
import './mainThreadNotebookKernels.js';
import './mainThreadNotebookDocumentsAndEditors.js';
Expand Down
160 changes: 160 additions & 0 deletions src/vs/workbench/api/browser/mainThreadCustomEditorOutline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CancellationToken } from '../../../base/common/cancellation.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, DisposableMap, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
import { ExtHostContext, ExtHostCustomEditorOutlineShape, MainContext, MainThreadCustomEditorOutlineShape } from '../common/extHost.protocol.js';
import { ICustomEditorOutlineItemDto, ICustomEditorOutlineProviderService } from '../../contrib/customEditor/common/customEditorOutlineService.js';

class CustomEditorOutlineProviderEntry {
private readonly _onDidChangeOutline = new Emitter<void>();
readonly onDidChangeOutline = this._onDidChangeOutline.event;

private readonly _onDidChangeActiveItem = new Emitter<string | undefined>();
readonly onDidChangeActiveItem = this._onDidChangeActiveItem.event;

private _activeItemId: string | undefined;

get activeItemId(): string | undefined { return this._activeItemId; }

fireDidChangeOutline(): void {
this._onDidChangeOutline.fire();
}

fireDidChangeActiveItem(itemId: string | undefined): void {
this._activeItemId = itemId;
this._onDidChangeActiveItem.fire(itemId);
}

dispose(): void {
this._onDidChangeOutline.dispose();
this._onDidChangeActiveItem.dispose();
}
}

class CustomEditorOutlineProviderService extends Disposable implements ICustomEditorOutlineProviderService {
declare readonly _serviceBrand: undefined;

private readonly _entries = this._register(new DisposableMap<string, CustomEditorOutlineProviderEntry>());

private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;

private _provideOutline?: (viewType: string, token: CancellationToken) => Promise<ICustomEditorOutlineItemDto[] | undefined>;
private _revealItem?: (viewType: string, itemId: string) => void;

setDelegate(delegate: {
provideOutline: (viewType: string, token: CancellationToken) => Promise<ICustomEditorOutlineItemDto[] | undefined>;
revealItem: (viewType: string, itemId: string) => void;
}): void {
this._provideOutline = delegate.provideOutline;
this._revealItem = delegate.revealItem;
}

hasProvider(viewType: string): boolean {
return this._entries.has(viewType);
}

getProviderViewTypes(): string[] {
return [...this._entries.keys()];
}

async provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined> {
if (this._provideOutline) {
return this._provideOutline(viewType, token);
}
return undefined;
}

revealItem(viewType: string, itemId: string): void {
if (this._revealItem) {
this._revealItem(viewType, itemId);
}
}

getActiveItemId(viewType: string): string | undefined {
return this._entries.get(viewType)?.activeItemId;
}

onDidChangeOutline(viewType: string): Event<void> {
const entry = this._entries.get(viewType);
return entry ? entry.onDidChangeOutline : Event.None;
}

onDidChangeActiveItem(viewType: string): Event<string | undefined> {
const entry = this._entries.get(viewType);
return entry ? entry.onDidChangeActiveItem : Event.None;
}

registerProvider(viewType: string): IDisposable {
const entry = new CustomEditorOutlineProviderEntry();
this._entries.set(viewType, entry);
this._onDidChange.fire();
return toDisposable(() => {
this._entries.deleteAndDispose(viewType);
this._onDidChange.fire();
});
}

unregisterProvider(viewType: string): void {
this._entries.deleteAndDispose(viewType);
this._onDidChange.fire();
}

fireDidChangeOutline(viewType: string): void {
this._entries.get(viewType)?.fireDidChangeOutline();
}

fireDidChangeActiveItem(viewType: string, itemId: string | undefined): void {
this._entries.get(viewType)?.fireDidChangeActiveItem(itemId);
}
}

registerSingleton(ICustomEditorOutlineProviderService, CustomEditorOutlineProviderService, InstantiationType.Delayed);

@extHostNamedCustomer(MainContext.MainThreadCustomEditorOutline)
export class MainThreadCustomEditorOutline extends Disposable implements MainThreadCustomEditorOutlineShape {

private readonly _proxy: ExtHostCustomEditorOutlineShape;
private readonly _registrations = this._register(new DisposableMap<string>());

constructor(
context: IExtHostContext,
@ICustomEditorOutlineProviderService private readonly _service: ICustomEditorOutlineProviderService,
) {
super();
this._proxy = context.getProxy(ExtHostContext.ExtHostCustomEditorOutline);

// Wire the service delegate to call through to the ext host
if (this._service instanceof CustomEditorOutlineProviderService) {
this._service.setDelegate({
provideOutline: (viewType, token) => this._proxy.$provideOutline(viewType, token),
revealItem: (viewType, itemId) => this._proxy.$revealItem(viewType, itemId),
});
}
}

$registerCustomEditorOutlineProvider(viewType: string): void {
const registration = this._service.registerProvider(viewType);
this._registrations.set(viewType, registration);
}

$unregisterCustomEditorOutlineProvider(viewType: string): void {
// deleteAndDispose disposes the registration returned by registerProvider(),
// whose dispose handler already removes the entry and fires onDidChange.
this._registrations.deleteAndDispose(viewType);
}

$onDidChangeOutline(viewType: string): void {
this._service.fireDidChangeOutline(viewType);
}

$onDidChangeActiveItem(viewType: string, itemId: string | undefined): void {
this._service.fireDidChangeActiveItem(viewType, itemId);
}
}
5 changes: 5 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { IExtHostCommands } from './extHostCommands.js';
import { createExtHostComments } from './extHostComments.js';
import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration.js';
import { ExtHostCustomEditors } from './extHostCustomEditors.js';
import { ExtHostCustomEditorOutline } from './extHostCustomEditorOutline.js';
import { IExtHostDataChannels } from './extHostDataChannels.js';
import { IExtHostDebugService } from './extHostDebugService.js';
import { IExtHostDecorations } from './extHostDecorations.js';
Expand Down Expand Up @@ -231,6 +232,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.remote, extHostWorkspace, extHostLogService, extHostApiDeprecation));
const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace));
const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels));
const extHostCustomEditorOutline = rpcProtocol.set(ExtHostContext.ExtHostCustomEditorOutline, new ExtHostCustomEditorOutline(rpcProtocol));
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, accessor.get(IExtHostTesting));
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
Expand Down Expand Up @@ -979,6 +981,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions; supportsMultipleEditorsPerDocument?: boolean } = {}) => {
return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options);
},
registerCustomEditorOutlineProvider: (viewType: string, provider: vscode.CustomEditorOutlineProvider) => {
return extHostCustomEditorOutline.registerCustomEditorOutlineProvider(extension, viewType, provider);
},
registerFileDecorationProvider(provider: vscode.FileDecorationProvider) {
return extHostDecorations.registerFileDecorationProvider(provider, extension);
},
Expand Down
17 changes: 17 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js';
import * as tasks from './shared/tasks.js';
import { PromptsType } from '../../contrib/chat/common/promptSyntax/promptTypes.js';
import { CDPEvent, CDPRequest, CDPResponse } from '../../../platform/browserView/common/cdp/types.js';
import { ICustomEditorOutlineItemDto } from '../../contrib/customEditor/common/customEditorOutlineService.js';

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

export type { ICustomEditorOutlineItemDto } from '../../contrib/customEditor/common/customEditorOutlineService.js';

export interface MainThreadCustomEditorOutlineShape extends IDisposable {
$registerCustomEditorOutlineProvider(viewType: string): void;
$unregisterCustomEditorOutlineProvider(viewType: string): void;
$onDidChangeOutline(viewType: string): void;
$onDidChangeActiveItem(viewType: string, itemId: string | undefined): void;
}

export interface ExtHostCustomEditorOutlineShape {
$provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined>;
$revealItem(viewType: string, itemId: string): void;
}

export interface ExtHostWebviewViewsShape {
$resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, title: string | undefined, state: any, cancellation: CancellationToken): Promise<void>;

Expand Down Expand Up @@ -3776,6 +3791,7 @@ export const MainContext = {
MainThreadWebviewPanels: createProxyIdentifier<MainThreadWebviewPanelsShape>('MainThreadWebviewPanels'),
MainThreadWebviewViews: createProxyIdentifier<MainThreadWebviewViewsShape>('MainThreadWebviewViews'),
MainThreadCustomEditors: createProxyIdentifier<MainThreadCustomEditorsShape>('MainThreadCustomEditors'),
MainThreadCustomEditorOutline: createProxyIdentifier<MainThreadCustomEditorOutlineShape>('MainThreadCustomEditorOutline'),
MainThreadUrls: createProxyIdentifier<MainThreadUrlsShape>('MainThreadUrls'),
MainThreadUriOpeners: createProxyIdentifier<MainThreadUriOpenersShape>('MainThreadUriOpeners'),
MainThreadProfileContentHandlers: createProxyIdentifier<MainThreadProfileContentHandlersShape>('MainThreadProfileContentHandlers'),
Expand Down Expand Up @@ -3893,5 +3909,6 @@ export const ExtHostContext = {
ExtHostDataChannels: createProxyIdentifier<ExtHostDataChannelsShape>('ExtHostDataChannels'),
ExtHostChatSessions: createProxyIdentifier<ExtHostChatSessionsShape>('ExtHostChatSessions'),
ExtHostGitExtension: createProxyIdentifier<ExtHostGitExtensionShape>('ExtHostGitExtension'),
ExtHostCustomEditorOutline: createProxyIdentifier<ExtHostCustomEditorOutlineShape>('ExtHostCustomEditorOutline'),
ExtHostBrowsers: createProxyIdentifier<ExtHostBrowsersShape>('ExtHostBrowsers'),
};
87 changes: 87 additions & 0 deletions src/vs/workbench/api/common/extHostCustomEditorOutline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type * as vscode from 'vscode';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
import { ExtHostCustomEditorOutlineShape, ICustomEditorOutlineItemDto, MainContext, MainThreadCustomEditorOutlineShape } from './extHost.protocol.js';
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { IRPCProtocol } from '../../services/extensions/common/proxyIdentifier.js';

export class ExtHostCustomEditorOutline implements ExtHostCustomEditorOutlineShape {

private readonly _proxy: MainThreadCustomEditorOutlineShape;
private readonly _providers = new Map<string, { provider: vscode.CustomEditorOutlineProvider; disposables: DisposableStore }>();

constructor(
mainContext: IRPCProtocol,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadCustomEditorOutline);
}

registerCustomEditorOutlineProvider(
extension: IExtensionDescription,
viewType: string,
provider: vscode.CustomEditorOutlineProvider,
): vscode.Disposable {
checkProposedApiEnabled(extension, 'customEditorOutline');

if (this._providers.has(viewType)) {
throw new Error(`An outline provider for custom editor view type '${viewType}' is already registered`);
}

const disposables = new DisposableStore();

this._providers.set(viewType, { provider, disposables });
this._proxy.$registerCustomEditorOutlineProvider(viewType);

disposables.add(provider.onDidChangeOutline(() => {
this._proxy.$onDidChangeOutline(viewType);
}));

disposables.add(provider.onDidChangeActiveItem(itemId => {
this._proxy.$onDidChangeActiveItem(viewType, itemId);
}));

return toDisposable(() => {
this._providers.delete(viewType);
disposables.dispose();
this._proxy.$unregisterCustomEditorOutlineProvider(viewType);
});
}

async $provideOutline(viewType: string, token: CancellationToken): Promise<ICustomEditorOutlineItemDto[] | undefined> {
const entry = this._providers.get(viewType);
if (!entry) {
return undefined;
}
const items = await entry.provider.provideOutline(token);
if (!items) {
return undefined;
}
return items.map(item => this._convertItem(item));
}

$revealItem(viewType: string, itemId: string): void {
const entry = this._providers.get(viewType);
if (entry) {
entry.provider.revealItem(itemId);
}
}

private _convertItem(item: vscode.CustomEditorOutlineItem): ICustomEditorOutlineItemDto {
return {
id: item.id,
label: item.label,
detail: item.detail,
tooltip: item.tooltip,
icon: ThemeIcon.isThemeIcon(item.icon) ? item.icon : undefined,
contextValue: item.contextValue,
children: item.children?.map(child => this._convertItem(child)),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ICustomEditorService } from '../common/customEditor.js';
import { WebviewEditor } from '../../webviewPanel/browser/webviewEditor.js';
import { CustomEditorInput } from './customEditorInput.js';
import { CustomEditorService } from './customEditors.js';
import './customEditorOutline.js';

registerSingleton(ICustomEditorService, CustomEditorService, InstantiationType.Delayed);

Expand Down
Loading
Loading