From 0c8a216fa2be1292d69faf05e7d4a15dd4cfcaa3 Mon Sep 17 00:00:00 2001 From: Hang Ma Date: Wed, 10 Apr 2024 10:54:04 +0800 Subject: [PATCH] feat: implement Tab APIs (#3413) --- .../merge-editor/merge-editor.provider.ts | 1 - packages/editor/src/browser/types.ts | 5 + .../src/browser/workbench-editor.service.ts | 97 +- packages/editor/src/common/editor.ts | 9 +- .../vscode/api/main.thread.api.webview.ts | 6 + .../vscode/api/main.thread.editor-tabs.ts | 379 +++- .../vscode/contributes/customEditors.tsx | 3 + .../src/common/vscode/editor-tabs.ts | 164 +- .../extension/src/common/vscode/ext-types.ts | 38 + packages/extension/src/common/vscode/index.ts | 2 +- .../hosted/api/vscode/ext.host.api.impl.ts | 2 +- .../hosted/api/vscode/ext.host.editor-tabs.ts | 460 ++++- .../api/vscode/ext.host.window.api.impl.ts | 3 + .../types/vscode/typings/vscode.editor.d.ts | 1652 ++++++++++------- packages/utils/src/functional.ts | 16 + packages/utils/src/types.ts | 11 + packages/webview/src/browser/types.ts | 7 +- .../webview/src/browser/webview.service.ts | 32 +- 18 files changed, 2134 insertions(+), 753 deletions(-) diff --git a/packages/editor/src/browser/merge-editor/merge-editor.provider.ts b/packages/editor/src/browser/merge-editor/merge-editor.provider.ts index e68d51a0c0..7aaee7bd27 100644 --- a/packages/editor/src/browser/merge-editor/merge-editor.provider.ts +++ b/packages/editor/src/browser/merge-editor/merge-editor.provider.ts @@ -3,7 +3,6 @@ import { LabelService, MaybePromise, URI, WithEventBus } from '@opensumi/ide-cor import { MergeEditorService } from '@opensumi/ide-monaco/lib/browser/contrib/merge-editor/merge-editor.service'; import { IResource, IResourceProvider } from '../../common'; - @Injectable() export class MergeEditorResourceProvider extends WithEventBus implements IResourceProvider { scheme = 'mergeEditor'; diff --git a/packages/editor/src/browser/types.ts b/packages/editor/src/browser/types.ts index ee1a32bfce..b1f5a1fd81 100644 --- a/packages/editor/src/browser/types.ts +++ b/packages/editor/src/browser/types.ts @@ -48,6 +48,11 @@ export interface IEditorComponent { // 渲染模式 默认为 ONE_PER_GROUP renderMode?: EditorComponentRenderMode; + + /** + * 有关该 component 的额外信息 + */ + metadata?: Record; } export type EditorSide = 'bottom' | 'top'; diff --git a/packages/editor/src/browser/workbench-editor.service.ts b/packages/editor/src/browser/workbench-editor.service.ts index 8abb4dae11..58d668ef1b 100644 --- a/packages/editor/src/browser/workbench-editor.service.ts +++ b/packages/editor/src/browser/workbench-editor.service.ts @@ -250,8 +250,10 @@ export class WorkbenchEditorServiceImpl extends WithEventBus implements Workbenc return this.editorGroups.reduce((pre, cur) => pre + cur.calcDirtyCount(countedUris), 0); } + private editorGroupIdGen = 0; + createEditorGroup(): EditorGroup { - const editorGroup = this.injector.get(EditorGroup, [this.generateRandomEditorGroupName()]); + const editorGroup = this.injector.get(EditorGroup, [this.generateRandomEditorGroupName(), this.editorGroupIdGen++]); this.editorGroups.push(editorGroup); const currentWatchDisposer = new Disposable( editorGroup.onDidEditorGroupBodyChanged(() => { @@ -661,6 +663,12 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { _onDidEditorGroupTabChanged = new EventEmitter(); onDidEditorGroupTabChanged: Event = this._onDidEditorGroupTabChanged.event; + /** + * 当编辑器的tab部分发生变更 + */ + _onDidEditorGroupTabOperation = new EventEmitter(); + onDidEditorGroupTabOperation: Event = this._onDidEditorGroupTabOperation.event; + /** * 当编辑器的主体部分发生变更 */ @@ -758,7 +766,7 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { private _currentOrPreviousFocusedEditor: IEditor | null; - constructor(public readonly name: string) { + constructor(public readonly name: string, public readonly groupId: number) { super(); this.eventBus.on(ResizeEvent, (e: ResizeEvent) => { if (e.payload.slotLocation === getSlotLocation('@opensumi/ide-editor', this.config.layoutConfig)) { @@ -1351,6 +1359,7 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } else { const oldOpenType = this._currentOpenType; const oldResource = this._currentResource; + let tabOperationToFire: IResourceTabOperation | null = null; let resource: IResource | null | undefined = this.resources.find((r) => r.uri.toString() === uri.toString()); if (!resource) { // open new resource @@ -1372,13 +1381,28 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { if (options && options.index !== undefined && options.index < this.resources.length) { replaceResource = this.resources[options.index]; this.resources.splice(options.index, 0, resource); + tabOperationToFire = { + type: 'open', + resource, + index: options.index, + }; } else { if (this.currentResource) { const currentIndex = this.resources.indexOf(this.currentResource); this.resources.splice(currentIndex + 1, 0, resource); + tabOperationToFire = { + type: 'open', + resource, + index: currentIndex + 1, + }; replaceResource = this.currentResource; } else { this.resources.push(resource); + tabOperationToFire = { + type: 'open', + resource, + index: this.resources.length - 1, + }; } } if (previewMode) { @@ -1418,7 +1442,13 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { }, 60); this.notifyTabChanged(); this.notifyBodyChanged(); - await this.displayResourceComponent(resource, options); + try { + await this.displayResourceComponent(resource, options); + } finally { + if (tabOperationToFire) { + this._onDidEditorGroupTabOperation.fire(tabOperationToFire); + } + } this._currentOrPreviousFocusedEditor = this.currentEditor; clearTimeout(delayTimer); @@ -1765,6 +1795,10 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } } + public getLastOpenType(resource: IResource) { + return this.cachedResourcesActiveOpenTypes.get(resource.uri.toString()); + } + private async resolveOpenType( resource: IResource, options: IResourceOpenOptions, @@ -1793,16 +1827,21 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { treatAsNotCurrent?: boolean; force?: boolean; } = {}, - ) { + ): Promise { const index = this.resources.findIndex((r) => r.uri.toString() === uri.toString()); if (index !== -1) { const resource = this.resources[index]; if (!force) { if (!(await this.shouldClose(resource))) { - return; + return false; } } this.resources.splice(index, 1); + this._onDidEditorGroupTabOperation.fire({ + type: 'close', + resource, + index, + }); this.eventBus.fire( new EditorGroupCloseEvent({ group: this, @@ -1853,6 +1892,7 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } this.availableOpenTypes = []; } + return true; } private removeResouceFromActiveComponents(resource: IResource) { @@ -1914,15 +1954,21 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { /** * 关闭全部 */ - async closeAll() { + async closeAll(): Promise { for (const resource of this.resources) { if (!(await this.shouldClose(resource))) { - return; + return false; } } const closed = this.resources.splice(0, this.resources.length); - closed.forEach((resource) => { + // reverse, 发送事件需要从后往前 + closed.reverse().forEach((resource, index) => { this.clearResourceOnClose(resource); + this._onDidEditorGroupTabOperation.fire({ + type: 'close', + resource, + index, + }); }); this.activeComponents.clear(); if (this.workbenchEditorService.editorGroups.length > 1) { @@ -1930,6 +1976,7 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } this.previewURI = null; this.backToEmpty(); + return true; } /** @@ -1966,6 +2013,13 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { } } this.resources.splice(index + 1); + resourcesToClose.reverse().forEach((resource, i) => { + this._onDidEditorGroupTabOperation.fire({ + type: 'close', + resource, + index: index + 1 + (resourcesToClose.length - 1 - i), + }); + }); for (const resource of resourcesToClose) { this.clearResourceOnClose(resource); } @@ -1992,7 +2046,15 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { return; } } + const oldResources = this.resources; this.resources = [this.resources[index]]; + resourcesToClose.reverse().forEach((resource) => { + this._onDidEditorGroupTabOperation.fire({ + type: 'close', + resource, + index: oldResources.indexOf(resource), + }); + }); for (const resource of resourcesToClose) { this.clearResourceOnClose(resource); } @@ -2063,10 +2125,22 @@ export class EditorGroup extends WithEventBus implements IGridEditorGroup { if (sourceIndex > targetIndex) { this.resources.splice(sourceIndex, 1); this.resources.splice(targetIndex, 0, sourceResource); + this._onDidEditorGroupTabOperation.fire({ + type: 'move', + resource: sourceResource, + oldIndex: sourceIndex, + index: targetIndex, + }); await this.open(uri, { preview: false }); } else if (sourceIndex < targetIndex) { this.resources.splice(targetIndex + 1, 0, sourceResource); this.resources.splice(sourceIndex, 1); + this._onDidEditorGroupTabOperation.fire({ + type: 'move', + resource: sourceResource, + oldIndex: sourceIndex, + index: targetIndex, + }); await this.open(uri, { preview: false }); } } @@ -2318,3 +2392,10 @@ function findSuitableOpenType( function openTypeSimilar(a: IEditorOpenType, b: IEditorOpenType) { return a.type === b.type && (a.type !== EditorOpenType.component || a.componentId === b.componentId); } + +export interface IResourceTabOperation { + type: 'open' | 'close' | 'move'; + resource: IResource; + oldIndex?: number; + index: number; +} diff --git a/packages/editor/src/common/editor.ts b/packages/editor/src/common/editor.ts index 2f76d12500..ec7778688a 100644 --- a/packages/editor/src/common/editor.ts +++ b/packages/editor/src/common/editor.ts @@ -386,8 +386,9 @@ export interface IEditorGroup { /** * 关闭指定的 uri 的 tab, 如果存在的话 * @param uri + * @return 是否成功关闭,不存在的话返回 true */ - close(uri: URI): Promise; + close(uri: URI): Promise; getState(): IEditorGroupState; @@ -395,7 +396,11 @@ export interface IEditorGroup { saveAll(): Promise; - closeAll(): Promise; + /** + * 关闭指定所有的 tab + * @return 是否成功关闭 + */ + closeAll(): Promise; /** * 保存当前的 tab 的文件 (如果它能被保存的话) diff --git a/packages/extension/src/browser/vscode/api/main.thread.api.webview.ts b/packages/extension/src/browser/vscode/api/main.thread.api.webview.ts index 6d0cd6e20b..420900c3bd 100644 --- a/packages/extension/src/browser/vscode/api/main.thread.api.webview.ts +++ b/packages/extension/src/browser/vscode/api/main.thread.api.webview.ts @@ -273,6 +273,9 @@ export class MainThreadWebview extends Disposable implements IMainThreadWebview longLive: webviewOptions.retainContextWhenHidden, }, id, + { + extWebview: viewType, + }, ); const viewColumn = editorWebview.group ? editorWebview.group.index + 1 : persistedWebviewPanelMeta.viewColumn; await this.doCreateWebviewPanel(id, viewType, title, { viewColumn }, webviewOptions, extensionInfo, state); @@ -346,6 +349,9 @@ export class MainThreadWebview extends Disposable implements IMainThreadWebview longLive: options.retainContextWhenHidden, }, id, + { + extWebview: viewType, + }, ); const webviewPanel = new WebviewPanel( id, diff --git a/packages/extension/src/browser/vscode/api/main.thread.editor-tabs.ts b/packages/extension/src/browser/vscode/api/main.thread.editor-tabs.ts index d0c1dde65a..813496860c 100644 --- a/packages/extension/src/browser/vscode/api/main.thread.editor-tabs.ts +++ b/packages/extension/src/browser/vscode/api/main.thread.editor-tabs.ts @@ -1,15 +1,28 @@ import { Autowired, Injectable, Optional } from '@opensumi/di'; import { IRPCProtocol } from '@opensumi/ide-connection'; -import { Disposable, URI } from '@opensumi/ide-core-common'; -import { WorkbenchEditorService } from '@opensumi/ide-editor'; -import { WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service'; +import { MergeEditorInputData } from '@opensumi/ide-core-browser/lib/monaco/merge-editor-widget'; +import { Disposable, IEventBus, URI, Uri, diffSets, isUndefined } from '@opensumi/ide-core-common'; +import { + DragOverPosition, + IDiffResource, + IMergeEditorResource, + IResource, + ResourceDecorationChangeEvent, + WorkbenchEditorService, +} from '@opensumi/ide-editor'; +import { EditorGroup, WorkbenchEditorServiceImpl } from '@opensumi/ide-editor/lib/browser/workbench-editor.service'; import { + AnyInputDto, IEditorTabDto, + IEditorTabGroupDto, IExtHostEditorTabsShape, IMainThreadEditorTabsShape, + TabInputKind, + TabModelOperationKind, } from './../../../common/vscode/editor-tabs'; import { ExtHostAPIIdentifier } from './../../../common/vscode/index'; +import { MainThreadWebview } from './main.thread.api.webview'; export interface ITabInfo { name: string; @@ -23,41 +36,369 @@ export class MainThreadEditorTabsService extends Disposable implements IMainThre private readonly proxy: IExtHostEditorTabsShape; - constructor(@Optional(Symbol()) private rpcProtocol: IRPCProtocol) { + @Autowired(IEventBus) + private eventBus: IEventBus; + + constructor(@Optional(Symbol()) private rpcProtocol: IRPCProtocol, mainThreadWebviews: MainThreadWebview) { super(); this.proxy = this.rpcProtocol.getProxy(ExtHostAPIIdentifier.ExtHostEditorTabs); this.addDispose( this.workbenchEditorService.onDidEditorGroupsChanged(() => { - this._pushEditorTabs(); + this.updateGroups(); }), ); this.addDispose( this.workbenchEditorService.onActiveResourceChange(() => { - this._pushEditorTabs(); + this.updateGroups(); }), ); this.workbenchEditorService.contributionsReady.promise.then(() => { - this._pushEditorTabs(); + this.updateGroups(); }); + + this.addDispose( + this.eventBus.on(ResourceDecorationChangeEvent, (e) => { + this.tabStore.getByResourceUri(e.payload.uri).forEach((data) => { + const isChanged = data.tryUpdate(); + if (isChanged) { + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: data.dto, + index: data.index, + groupId: data.group.groupId, + }); + } + }); + }), + ); + } + + $initializeState() { + this.updateGroups(); + } + + $moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean | undefined): void { + const target = this.findEditorTabData(tabId); + if (target) { + const { group, resource } = target; + let targetEditorGroup = group; + if (!isUndefined(viewColumn)) { + targetEditorGroup = this.workbenchEditorService.sortedEditorGroups[viewColumn]; + } + if (targetEditorGroup) { + targetEditorGroup.dropUri(resource.uri, DragOverPosition.CENTER, group, targetEditorGroup.resources[index]); + } else { + group.dropUri(resource.uri, DragOverPosition.RIGHT); + } + } + } + + async $closeTab(tabIds: string[], preserveFocus?: boolean | undefined): Promise { + const res = await Promise.all( + tabIds.map(async (tabId) => { + const target = this.findEditorTabData(tabId); + if (target) { + return await target.group.close(target.resource.uri); + } + }), + ); + return res.filter((r) => !!r).length > 0; + } + + async $closeGroup(groupIds: number[], preservceFocus?: boolean | undefined): Promise { + const res = await Promise.all( + groupIds.map(async (g) => { + const editorGroup = this.groupDataStore.get(g)?.editorGroup; + if (editorGroup) { + try { + return await editorGroup.closeAll(); + } catch (e) { + return false; + } + } + }), + ); + return res.filter((r) => !!r).length > 0; + } + + private findEditorTabData(tabId: string): EditorTabDtoData | undefined { + return this.tabStore.all.get(tabId); + } + + private prevEditorGroups: Set = new Set(); + + private groupDataStore: Map = new Map(); + + private tabStore: EditorTabDtoDataStore = new EditorTabDtoDataStore(); + + getOrCreateGroupData(group: EditorGroup): EditorTabGroupData { + if (!this.groupDataStore.has(group.groupId)) { + const data = new EditorTabGroupData(group, this.workbenchEditorService, this.tabStore, this.proxy); + data.addDispose({ + dispose: () => { + if (this.groupDataStore.get(group.groupId) === data) { + this.groupDataStore.delete(group.groupId); + } + }, + }); + this.groupDataStore.set(group.groupId, data); + } + return this.groupDataStore.get(group.groupId)!; + } + + private updateGroups() { + const diff = diffSets(this.prevEditorGroups, new Set(this.workbenchEditorService.editorGroups)); + if (diff.added.length > 0 || diff.removed.length > 0) { + // 做一次完整更新 + this.proxy.$acceptEditorTabModel( + this.workbenchEditorService.editorGroups.map((group) => { + const data = this.getOrCreateGroupData(group); + return { + ...data.dto, + tabs: data.tabs.map((t) => t.dto), + }; + }), + ); + } else { + this.workbenchEditorService.editorGroups.forEach((group) => { + const data = this.getOrCreateGroupData(group); + data.tryCheckUpdate(); + }); + } + this.prevEditorGroups = new Set(this.workbenchEditorService.editorGroups); + } +} + +class EditorTabDtoData { + private _dto: IEditorTabDto; + + constructor(public readonly group: EditorGroup, public readonly resource: IResource) { + this.tryUpdate(); + } + /** + * @param updateDto + */ + tryUpdate(): false | true { + const updateDto = EditorTabDtoData.from(this.group, this.resource); + if (this._dto && JSON.stringify(this._dto) !== JSON.stringify(updateDto)) { + this._dto = updateDto; + return true; + } else { + this._dto = updateDto; + return false; + } + } + + get dto() { + return this._dto; + } + + static getTabId(editorGroup: EditorGroup, resource: IResource): string { + return `${editorGroup.groupId}~${resource.uri.toString()}`; + } + + get index() { + return this.group.resources.indexOf(this.resource); } - private _pushEditorTabs(): void { - const tabs: IEditorTabDto[] = []; - for (const group of this.workbenchEditorService.editorGroups) { - for (const resource of group.resources) { - if (group.disposed || !resource) { - continue; + static from(editorGroup: EditorGroup, resource: IResource): IEditorTabDto { + const tabId = EditorTabDtoData.getTabId(editorGroup, resource); + const openType = editorGroup.getLastOpenType(resource); + let input: AnyInputDto = { + kind: TabInputKind.UnknownInput, + }; + if (openType) { + if (openType.type === 'code') { + input = { + kind: TabInputKind.TextInput, + uri: resource.uri.codeUri, + }; + } else if (openType.type === 'diff') { + const { metadata } = resource as IDiffResource; + if (metadata) { + input = { + kind: TabInputKind.TextDiffInput, + original: metadata!.original.codeUri, + modified: metadata!.modified.codeUri, + }; + } + } else if (openType.type === 'mergeEditor') { + const { metadata } = resource as IMergeEditorResource; + if (metadata) { + const { ancestor, input1, input2, output } = metadata!; + const input1Data = MergeEditorInputData.from(input1); + const input2Data = MergeEditorInputData.from(input2); + input = { + kind: TabInputKind.TextMergeInput, + base: Uri.parse(ancestor), + input1: input1Data.uri.codeUri, + input2: input2Data.uri.codeUri, + result: Uri.parse(output), + }; + } + } else if (openType.type === 'component') { + // 区分 webview / customEditor + const component = editorGroup.editorComponentRegistry.getEditorComponent(openType.componentId!); + if (component?.metadata?.extWebview) { + input = { + kind: TabInputKind.WebviewEditorInput, + viewType: component?.metadata?.extWebview, + }; + } else if (component?.metadata?.customEditor) { + input = { + kind: TabInputKind.CustomEditorInput, + viewType: component?.metadata?.customEditor, + uri: resource.uri.codeUri, + }; } - tabs.push({ - group: group.index, - name: resource.name, - resource: resource.uri.toString(), - isActive: this.workbenchEditorService.currentResource?.uri === resource.uri, + } + } + return { + id: tabId, + label: resource.name, + input, + isActive: editorGroup.currentResource === resource, + isPinned: false, // 暂时还没这个功能, + isPreview: !!editorGroup.previewURI?.isEqual(resource.uri), + isDirty: !!editorGroup.resourceService.getResourceDecoration(resource.uri)?.dirty, + }; + } +} + +class EditorTabDtoDataStore { + all: Map = new Map(); + + private mapByUri: Map> = new Map(); + + getOrCreateData(group: EditorGroup, resource: IResource): EditorTabDtoData { + const tabId = EditorTabDtoData.getTabId(group, resource); + if (!this.all.has(tabId)) { + const data = new EditorTabDtoData(group, resource); + const uriString = resource.uri.toString(); + if (!this.mapByUri.has(uriString)) { + this.mapByUri.set(uriString, new Map()); + } + this.mapByUri.get(uriString)!.set(tabId, data); + this.all.set(tabId, data); + } + return this.all.get(tabId)!; + } + + removeData(id: string) { + const data = this.all.get(id); + if (data) { + this.all.delete(id); + const uriString = data.resource.uri.toString(); + this.mapByUri.get(uriString)?.delete(id); + if (this.mapByUri.get(uriString)?.size === 0) { + this.mapByUri.delete(uriString); + } + } + } + + getByResourceUri(uri: URI) { + return Array.from(this.mapByUri.get(uri.toString())?.values() || []); + } +} + +class EditorTabGroupData extends Disposable { + public tabs: EditorTabDtoData[] = []; + + constructor( + public readonly editorGroup: EditorGroup, + private editorService: WorkbenchEditorService, + private store: EditorTabDtoDataStore, + private proxy: IExtHostEditorTabsShape, + ) { + super(); + this.editorGroup.addDispose(this); + this.init(); + } + + init() { + this.tabs = this.editorGroup.resources.map((r) => this.store.getOrCreateData(this.editorGroup, r)); + this.addDispose( + this.editorGroup.onDidEditorGroupTabOperation((operation) => { + const kind = { + open: TabModelOperationKind.TAB_OPEN, + close: TabModelOperationKind.TAB_CLOSE, + move: TabModelOperationKind.TAB_MOVE, + }[operation.type]; + const tabDtoData = this.store.getOrCreateData(this.editorGroup, operation.resource); + tabDtoData.tryUpdate(); + this.proxy.$acceptTabOperation({ + kind, + groupId: this.editorGroup.groupId, + tabDto: tabDtoData.dto, + index: operation.index, + oldIndex: operation.oldIndex, + }); + }), + ); + this.addDispose( + this.editorGroup.onDidEditorGroupBodyChanged(() => { + this.onTabsMayUpdated(); + }), + ); + this.addDispose( + this.editorGroup.onDidEditorGroupTabChanged(() => { + this.onTabsMayUpdated(); + }), + ); + } + + onTabsMayUpdated() { + this.tabs = this.editorGroup.resources.map((r, index) => { + const data = this.store.getOrCreateData(this.editorGroup, r); + const isChanged = data.tryUpdate(); + if (isChanged) { + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + tabDto: data.dto, + index, + groupId: this.editorGroup.groupId, + }); + } + return data; + }); + } + + public tryCheckUpdate() { + const oldData = this._data; + const newData = { + groupId: this.editorGroup.groupId, + isActive: this.editorService.currentEditorGroup === this.editorGroup, + viewColumn: this.editorGroup.index, + }; + this._data = newData; + if (oldData) { + if (oldData.viewColumn !== newData.viewColumn) { + // changed + this.proxy.$acceptTabGroupUpdate({ + ...this.dto, + tabs: [], // update 事件中不会真正消费 tabs + }); + } else if (!oldData.isActive && newData.isActive) { + // 只有原来不是 active ,新的是 active 才要发 (和 vscode 行为对齐) + this.proxy.$acceptTabGroupUpdate({ + ...this.dto, + tabs: [], // update 事件中不会真正消费 tabs }); } } + } - this.proxy.$acceptEditorTabs(tabs); + private _data: Omit | undefined = undefined; + + get dto(): Omit { + if (!this._data) { + this._data = { + groupId: this.editorGroup.groupId, + isActive: this.editorService.currentEditorGroup === this.editorGroup, + viewColumn: this.editorGroup.index, + }; + } + return this._data; } } diff --git a/packages/extension/src/browser/vscode/contributes/customEditors.tsx b/packages/extension/src/browser/vscode/contributes/customEditors.tsx index f83f002a71..04aa693363 100644 --- a/packages/extension/src/browser/vscode/contributes/customEditors.tsx +++ b/packages/extension/src/browser/vscode/contributes/customEditors.tsx @@ -142,6 +142,9 @@ export class CustomEditorContributionPoint extends VSCodeContributePoint; } -export type IMainThreadEditorTabsShape = IDisposable; - export const IExtHostEditorTabs = Symbol('IExtHostEditorTabs'); + +// #region --- tabs model + +export const enum TabInputKind { + UnknownInput, + TextInput, + TextDiffInput, + TextMergeInput, + NotebookInput, + NotebookDiffInput, + CustomEditorInput, + WebviewEditorInput, + TerminalEditorInput, + InteractiveEditorInput, +} + +export const enum TabModelOperationKind { + TAB_OPEN, + TAB_CLOSE, + TAB_UPDATE, + TAB_MOVE, +} + +export interface UnknownInputDto { + kind: TabInputKind.UnknownInput; +} + +export interface TextInputDto { + kind: TabInputKind.TextInput; + uri: UriComponents; +} + +export interface TextDiffInputDto { + kind: TabInputKind.TextDiffInput; + original: UriComponents; + modified: UriComponents; +} + +export interface TextMergeInputDto { + kind: TabInputKind.TextMergeInput; + base: UriComponents; + input1: UriComponents; + input2: UriComponents; + result: UriComponents; +} + +export interface NotebookInputDto { + kind: TabInputKind.NotebookInput; + notebookType: string; + uri: UriComponents; +} + +export interface NotebookDiffInputDto { + kind: TabInputKind.NotebookDiffInput; + notebookType: string; + original: UriComponents; + modified: UriComponents; +} + +export interface CustomInputDto { + kind: TabInputKind.CustomEditorInput; + viewType: string; + uri: UriComponents; +} + +export interface WebviewInputDto { + kind: TabInputKind.WebviewEditorInput; + viewType: string; +} + +export interface InteractiveEditorInputDto { + kind: TabInputKind.InteractiveEditorInput; + uri: UriComponents; + inputBoxUri: UriComponents; +} + +export interface TabInputDto { + kind: TabInputKind.TerminalEditorInput; +} + +export type AnyInputDto = + | UnknownInputDto + | TextInputDto + | TextDiffInputDto + | TextMergeInputDto + | NotebookInputDto + | NotebookDiffInputDto + | CustomInputDto + | WebviewInputDto + | InteractiveEditorInputDto + | TabInputDto; + +export interface IMainThreadEditorTabsShape extends IDisposable { + $initializeState(): void; + // manage tabs: move, close, rearrange etc + $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; + $closeTab(tabIds: string[], preserveFocus?: boolean): Promise; + $closeGroup(groupIds: number[], preservceFocus?: boolean): Promise; +} + +export interface IEditorTabGroupDto { + isActive: boolean; + viewColumn: EditorGroupColumn; + // Decided not to go with simple index here due to opening and closing causing index shifts + // This allows us to patch the model without having to do full rebuilds + tabs: IEditorTabDto[]; + groupId: number; +} + +export interface TabOperation { + readonly kind: + | TabModelOperationKind.TAB_OPEN + | TabModelOperationKind.TAB_CLOSE + | TabModelOperationKind.TAB_UPDATE + | TabModelOperationKind.TAB_MOVE; + // TODO @lramos15 Possibly get rid of index for tab update, it's only needed for open and close + readonly index: number; + readonly tabDto: IEditorTabDto; + readonly groupId: number; + readonly oldIndex?: number; +} + +export interface IEditorTabDto { + id: string; + label: string; + input: AnyInputDto; + editorId?: string; + isActive: boolean; + isPinned: boolean; + isPreview: boolean; + isDirty: boolean; +} + +export interface IExtHostEditorTabsShape { + // Accepts a whole new model + $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void; + // Only when group property changes (not the tabs inside) + $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto): void; + // When a tab is added, removed, or updated + $acceptTabOperation(operation: TabOperation): void; +} + +export interface IExtHostEditorTabs extends IExtHostEditorTabsLegacyProposed, IExtHostEditorTabsShape {} + +// #endregion diff --git a/packages/extension/src/common/vscode/ext-types.ts b/packages/extension/src/common/vscode/ext-types.ts index 92c7e99782..8e55e7f526 100644 --- a/packages/extension/src/common/vscode/ext-types.ts +++ b/packages/extension/src/common/vscode/ext-types.ts @@ -3400,3 +3400,41 @@ export class ChatMessage implements vscode.ChatMessage { this.content = content; } } + +// #region Tab Inputs + +export class TextTabInput { + constructor(readonly uri: Uri) {} +} + +export class TextDiffTabInput { + constructor(readonly original: Uri, readonly modified: Uri) {} +} + +export class TextMergeTabInput { + constructor(readonly base: Uri, readonly input1: Uri, readonly input2: Uri, readonly result: Uri) {} +} + +export class CustomEditorTabInput { + constructor(readonly uri: Uri, readonly viewType: string) {} +} + +export class WebviewEditorTabInput { + constructor(readonly viewType: string) {} +} + +export class NotebookEditorTabInput { + constructor(readonly uri: Uri, readonly notebookType: string) {} +} + +export class NotebookDiffEditorTabInput { + constructor(readonly original: Uri, readonly modified: Uri, readonly notebookType: string) {} +} + +export class TerminalEditorTabInput { + constructor() {} +} +export class InteractiveWindowInput { + constructor(readonly uri: Uri, readonly inputBoxUri: Uri) {} +} +// #endregion diff --git a/packages/extension/src/common/vscode/index.ts b/packages/extension/src/common/vscode/index.ts index c6b3352900..35ff654ead 100644 --- a/packages/extension/src/common/vscode/index.ts +++ b/packages/extension/src/common/vscode/index.ts @@ -23,7 +23,7 @@ import { IMainThreadCustomEditor, IMainThreadEditorsService, } from './editor'; -import { IExtHostEditorTabs, IMainThreadEditorTabsShape } from './editor-tabs'; +import { IExtHostEditorTabs, IExtHostEditorTabsLegacyProposed, IMainThreadEditorTabsShape } from './editor-tabs'; import { IExtHostEnv, IMainThreadEnv } from './env'; import { IMainThreadFileSystemShape } from './file-system'; import { IMainThreadLanguages } from './languages'; diff --git a/packages/extension/src/hosted/api/vscode/ext.host.api.impl.ts b/packages/extension/src/hosted/api/vscode/ext.host.api.impl.ts index 8be54b8815..12be788dac 100644 --- a/packages/extension/src/hosted/api/vscode/ext.host.api.impl.ts +++ b/packages/extension/src/hosted/api/vscode/ext.host.api.impl.ts @@ -7,13 +7,13 @@ import { IExtensionHostService } from '../../../common'; import { ExtHostAPIIdentifier, IExtHostDebugService, + IExtHostEditorTabs, IExtHostTests, IExtensionDescription, IInterProcessConnectionService, TextEditorCursorStyle, TextEditorSelectionChangeKind, } from '../../../common/vscode'; // '../../common'; -import { IExtHostEditorTabs } from '../../../common/vscode/editor-tabs'; import { ViewColumn } from '../../../common/vscode/enums'; import * as extTypes from '../../../common/vscode/ext-types'; import * as fileSystemTypes from '../../../common/vscode/file-system'; diff --git a/packages/extension/src/hosted/api/vscode/ext.host.editor-tabs.ts b/packages/extension/src/hosted/api/vscode/ext.host.editor-tabs.ts index c32ada19d9..153ae85831 100644 --- a/packages/extension/src/hosted/api/vscode/ext.host.editor-tabs.ts +++ b/packages/extension/src/hosted/api/vscode/ext.host.editor-tabs.ts @@ -1,38 +1,462 @@ +/* eslint-disable comma-dangle */ +/* eslint-disable arrow-parens */ +/* eslint-disable @typescript-eslint/no-inferrable-types */ import { IRPCProtocol } from '@opensumi/ide-connection'; -import { Emitter, Event, Uri } from '@opensumi/ide-core-common'; +import { Emitter, Event, Uri, assertIsDefined, diffSets } from '@opensumi/ide-core-common'; +import { MainThreadAPIIdentifier } from '../../../common/vscode'; import { IEditorTab, IEditorTabDto, + IEditorTabGroupDto, IExtHostEditorTabs, IMainThreadEditorTabsShape, -} from './../../../common/vscode/editor-tabs'; -import { MainThreadAPIIdentifier } from './../../../common/vscode/index'; + TabInputKind, + TabModelOperationKind, + TabOperation, +} from '../../../common/vscode/editor-tabs'; +import { + CustomEditorTabInput, + InteractiveWindowInput, + NotebookDiffEditorTabInput, + NotebookEditorTabInput, + TerminalEditorTabInput, + TextDiffTabInput, + TextMergeTabInput, + TextTabInput, + WebviewEditorTabInput, +} from '../../../common/vscode/ext-types'; -export class ExtHostEditorTabs implements IExtHostEditorTabs { - private _proxy: IMainThreadEditorTabsShape; - readonly _serviceBrand: undefined; +import type vscode from 'vscode'; + +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +type AnyTabInput = + | TextTabInput + | TextDiffTabInput + | CustomEditorTabInput + | NotebookEditorTabInput + | NotebookDiffEditorTabInput + | WebviewEditorTabInput + | TerminalEditorTabInput + | InteractiveWindowInput; + +class ExtHostEditorTab { + private _apiObject: vscode.Tab | undefined; + private _dto!: IEditorTabDto; + private _input: AnyTabInput | undefined; + private _parentGroup: ExtHostEditorTabGroup; + private readonly _activeTabIdGetter: () => string; + + constructor(dto: IEditorTabDto, parentGroup: ExtHostEditorTabGroup, activeTabIdGetter: () => string) { + this._activeTabIdGetter = activeTabIdGetter; + this._parentGroup = parentGroup; + this.acceptDtoUpdate(dto); + } - private readonly _onDidChangeTabs = new Emitter(); - readonly onDidChangeTabs: Event = this._onDidChangeTabs.event; + get apiObject(): vscode.Tab { + if (!this._apiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; + const obj: vscode.Tab = { + get isActive() { + // We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required + return that._dto.id === that._activeTabIdGetter(); + }, + get label() { + return that._dto.label; + }, + get input() { + return that._input; + }, + get isDirty() { + return that._dto.isDirty; + }, + get isPinned() { + return that._dto.isPinned; + }, + get isPreview() { + return that._dto.isPreview; + }, + get group() { + return that._parentGroup.apiObject; + }, + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + get tabId(): string { + return this._dto.id; + } + + acceptDtoUpdate(dto: IEditorTabDto) { + this._dto = dto; + this._input = this._initInput(); + } + + private _initInput() { + switch (this._dto.input.kind) { + case TabInputKind.TextInput: + return new TextTabInput(Uri.revive(this._dto.input.uri)); + case TabInputKind.TextDiffInput: + return new TextDiffTabInput(Uri.revive(this._dto.input.original), Uri.revive(this._dto.input.modified)); + case TabInputKind.TextMergeInput: + return new TextMergeTabInput( + Uri.revive(this._dto.input.base), + Uri.revive(this._dto.input.input1), + Uri.revive(this._dto.input.input2), + Uri.revive(this._dto.input.result), + ); + case TabInputKind.CustomEditorInput: + return new CustomEditorTabInput(Uri.revive(this._dto.input.uri), this._dto.input.viewType); + case TabInputKind.WebviewEditorInput: + return new WebviewEditorTabInput(this._dto.input.viewType); + case TabInputKind.NotebookInput: + return new NotebookEditorTabInput(Uri.revive(this._dto.input.uri), this._dto.input.notebookType); + case TabInputKind.NotebookDiffInput: + return new NotebookDiffEditorTabInput( + Uri.revive(this._dto.input.original), + Uri.revive(this._dto.input.modified), + this._dto.input.notebookType, + ); + case TabInputKind.TerminalEditorInput: + return new TerminalEditorTabInput(); + case TabInputKind.InteractiveEditorInput: + return new InteractiveWindowInput(Uri.revive(this._dto.input.uri), Uri.revive(this._dto.input.inputBoxUri)); + default: + return undefined; + } + } +} - private _tabs: IEditorTab[] = []; +class ExtHostEditorTabGroup { + private _apiObject: vscode.TabGroup | undefined; + private _dto: IEditorTabGroupDto; + private _tabs: ExtHostEditorTab[] = []; + private _activeTabId: string = ''; + private _activeGroupIdGetter: () => number | undefined; - get tabs(): readonly IEditorTab[] { + constructor(dto: IEditorTabGroupDto, activeGroupIdGetter: () => number | undefined) { + this._dto = dto; + this._activeGroupIdGetter = activeGroupIdGetter; + // Construct all tabs from the given dto + for (const tabDto of dto.tabs) { + if (tabDto.isActive) { + this._activeTabId = tabDto.id; + } + this._tabs.push(new ExtHostEditorTab(tabDto, this, () => this.activeTabId())); + } + } + + get apiObject(): vscode.TabGroup { + if (!this._apiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; + const obj: vscode.TabGroup = { + get isActive() { + // We use a getter function here to always ensure at most 1 active group and prevent iteration for being required + return that._dto.groupId === that._activeGroupIdGetter(); + }, + get viewColumn() { + return that._dto.viewColumn + 1; + }, + get activeTab() { + return that._tabs.find((tab) => tab.tabId === that._activeTabId)?.apiObject; + }, + get tabs() { + return Object.freeze(that._tabs.map((tab) => tab.apiObject)); + }, + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + get groupId(): number { + return this._dto.groupId; + } + + get tabs(): ExtHostEditorTab[] { return this._tabs; } + acceptGroupDtoUpdate(dto: IEditorTabGroupDto) { + this._dto = dto; + } + + acceptTabOperation(operation: TabOperation): ExtHostEditorTab { + // In the open case we add the tab to the group + if (operation.kind === TabModelOperationKind.TAB_OPEN) { + const tab = new ExtHostEditorTab(operation.tabDto, this, () => this.activeTabId()); + // Insert tab at editor index + this._tabs.splice(operation.index, 0, tab); + if (operation.tabDto.isActive) { + this._activeTabId = tab.tabId; + } + return tab; + } else if (operation.kind === TabModelOperationKind.TAB_CLOSE) { + const tab = this._tabs.splice(operation.index, 1)[0]; + if (!tab) { + throw new Error(`Tab close updated received for index ${operation.index} which does not exist`); + } + if (tab.tabId === this._activeTabId) { + this._activeTabId = ''; + } + return tab; + } else if (operation.kind === TabModelOperationKind.TAB_MOVE) { + if (operation.oldIndex === undefined) { + throw new Error('Invalid old index on move IPC'); + } + // Splice to remove at old index and insert at new index === moving the tab + const tab = this._tabs.splice(operation.oldIndex, 1)[0]; + if (!tab) { + throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`); + } + this._tabs.splice(operation.index, 0, tab); + return tab; + } + const tab = this._tabs.find((extHostTab) => extHostTab.tabId === operation.tabDto.id); + if (!tab) { + throw new Error('INVALID tab'); + } + if (operation.tabDto.isActive) { + this._activeTabId = operation.tabDto.id; + } else if (this._activeTabId === operation.tabDto.id && !operation.tabDto.isActive) { + // Events aren't guaranteed to be in order so if we receive a dto that matches the active tab id + // but isn't active we mark the active tab id as empty. This prevent onDidActiveTabChange from + // firing incorrectly + this._activeTabId = ''; + } + tab.acceptDtoUpdate(operation.tabDto); + return tab; + } + + // Not a getter since it must be a function to be used as a callback for the tabs + activeTabId(): string { + return this._activeTabId; + } +} + +export class ExtHostEditorTabs implements IExtHostEditorTabs { + readonly _serviceBrand: undefined; + + private readonly _proxy: IMainThreadEditorTabsShape; + private readonly _onDidChangeTabs = new Emitter(); + private readonly _onDidChangeTabGroups = new Emitter(); + + // Have to use ! because this gets initialized via an RPC proxy + private _activeGroupId!: number; + + private _extHostTabGroups: ExtHostEditorTabGroup[] = []; + + private _apiObject: vscode.TabGroups | undefined; + constructor(rpcProtocol: IRPCProtocol) { this._proxy = rpcProtocol.getProxy(MainThreadAPIIdentifier.MainThreadEditorTabs); + this._proxy.$initializeState(); + } + + get tabs(): IEditorTab[] { + return this.tabGroups.all.reduce( + (prev, next) => + prev.concat( + next.tabs.map( + (t) => + ({ + group: t.group.viewColumn + 1, + isActive: t.isActive, + name: t.label, + resource: (t.input as any)?.uri, + } as IEditorTab), + ), + ), + [], + ); + } + + onDidChangeTabs: Event = Event.map(this._onDidChangeTabs.event, () => void 0); + + get tabGroups(): vscode.TabGroups { + if (!this._apiObject) { + const that = this; + const obj: vscode.TabGroups = { + // never changes -> simple value + onDidChangeTabGroups: that._onDidChangeTabGroups.event, + onDidChangeTabs: that._onDidChangeTabs.event, + // dynamic -> getters + get all() { + return Object.freeze(that._extHostTabGroups.map((group) => group.apiObject)); + }, + get activeTabGroup() { + const activeTabGroupId = that._activeGroupId; + const activeTabGroup = assertIsDefined( + that._extHostTabGroups.find((candidate) => candidate.groupId === activeTabGroupId)?.apiObject, + ); + return activeTabGroup; + }, + close: async ( + tabOrTabGroup: vscode.Tab | readonly vscode.Tab[] | vscode.TabGroup | readonly vscode.TabGroup[], + preserveFocus?: boolean, + ) => { + const tabsOrTabGroups = Array.isArray(tabOrTabGroup) ? tabOrTabGroup : [tabOrTabGroup]; + if (!tabsOrTabGroups.length) { + return true; + } + // Check which type was passed in and call the appropriate close + // Casting is needed as typescript doesn't seem to infer enough from this + if (isTabGroup(tabsOrTabGroups[0])) { + return this._closeGroups(tabsOrTabGroups as vscode.TabGroup[], preserveFocus); + } else { + return this._closeTabs(tabsOrTabGroups as vscode.Tab[], preserveFocus); + } + }, + // move: async (tab: vscode.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => { + // const extHostTab = this._findExtHostTabFromApi(tab); + // if (!extHostTab) { + // throw new Error('Invalid tab'); + // } + // this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus); + // return; + // } + }; + this._apiObject = Object.freeze(obj); + } + return this._apiObject; + } + + $acceptEditorTabModel(tabGroups: IEditorTabGroupDto[]): void { + const groupIdsBefore = new Set(this._extHostTabGroups.map((group) => group.groupId)); + const groupIdsAfter = new Set(tabGroups.map((dto) => dto.groupId)); + const diff = diffSets(groupIdsBefore, groupIdsAfter); + + const closed: vscode.TabGroup[] = this._extHostTabGroups + .filter((group) => diff.removed.includes(group.groupId)) + .map((group) => group.apiObject); + const opened: vscode.TabGroup[] = []; + const changed: vscode.TabGroup[] = []; + + this._extHostTabGroups = tabGroups.map((tabGroup) => { + const group = new ExtHostEditorTabGroup(tabGroup, () => this._activeGroupId); + if (diff.added.includes(group.groupId)) { + opened.push(group.apiObject); + } else { + changed.push(group.apiObject); + } + return group; + }); + + // Set the active tab group id + const activeTabGroupId = assertIsDefined(tabGroups.find((group) => group.isActive === true)?.groupId); + if (activeTabGroupId !== undefined && this._activeGroupId !== activeTabGroupId) { + this._activeGroupId = activeTabGroupId; + } + this._onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); } - $acceptEditorTabs(tabs: IEditorTabDto[]): void { - this._tabs = tabs.map((dto) => ({ - name: dto.name, - group: dto.group, - resource: Uri.parse(dto.resource), - isActive: dto.isActive, - })); - this._onDidChangeTabs.fire(); + $acceptTabGroupUpdate(groupDto: IEditorTabGroupDto) { + const group = this._extHostTabGroups.find((group) => group.groupId === groupDto.groupId); + if (!group) { + throw new Error('Update Group IPC call received before group creation.'); + } + group.acceptGroupDtoUpdate(groupDto); + if (groupDto.isActive) { + this._activeGroupId = groupDto.groupId; + } + this._onDidChangeTabGroups.fire(Object.freeze({ changed: [group.apiObject], opened: [], closed: [] })); + } + + $acceptTabOperation(operation: TabOperation) { + const group = this._extHostTabGroups.find((group) => group.groupId === operation.groupId); + if (!group) { + throw new Error('Update Tabs IPC call received before group creation.'); + } + const tab = group.acceptTabOperation(operation); + + // Construct the tab change event based on the operation + switch (operation.kind) { + case TabModelOperationKind.TAB_OPEN: + this._onDidChangeTabs.fire( + Object.freeze({ + opened: [tab.apiObject], + closed: [], + changed: [], + }), + ); + return; + case TabModelOperationKind.TAB_CLOSE: + this._onDidChangeTabs.fire( + Object.freeze({ + opened: [], + closed: [tab.apiObject], + changed: [], + }), + ); + return; + case TabModelOperationKind.TAB_MOVE: + case TabModelOperationKind.TAB_UPDATE: + this._onDidChangeTabs.fire( + Object.freeze({ + opened: [], + closed: [], + changed: [tab.apiObject], + }), + ); + return; + } + } + + private _findExtHostTabFromApi(apiTab: vscode.Tab): ExtHostEditorTab | undefined { + for (const group of this._extHostTabGroups) { + for (const tab of group.tabs) { + if (tab.apiObject === apiTab) { + return tab; + } + } + } + return; + } + + private _findExtHostTabGroupFromApi(apiTabGroup: vscode.TabGroup): ExtHostEditorTabGroup | undefined { + return this._extHostTabGroups.find((candidate) => candidate.apiObject === apiTabGroup); + } + + private async _closeTabs(tabs: vscode.Tab[], preserveFocus?: boolean): Promise { + const extHostTabIds: string[] = []; + for (const tab of tabs) { + const extHostTab = this._findExtHostTabFromApi(tab); + if (!extHostTab) { + throw new Error('Tab close: Invalid tab not found!'); + } + extHostTabIds.push(extHostTab.tabId); + } + return this._proxy.$closeTab(extHostTabIds, preserveFocus); + } + + private async _closeGroups(groups: vscode.TabGroup[], preserverFoucs?: boolean): Promise { + const extHostGroupIds: number[] = []; + for (const group of groups) { + const extHostGroup = this._findExtHostTabGroupFromApi(group); + if (!extHostGroup) { + throw new Error('Group close: Invalid group not found!'); + } + extHostGroupIds.push(extHostGroup.groupId); + } + return this._proxy.$closeGroup(extHostGroupIds, preserverFoucs); } } + +// #region Utils +function isTabGroup(obj: unknown): obj is vscode.TabGroup { + const tabGroup = obj as vscode.TabGroup; + if (tabGroup.tabs !== undefined) { + return true; + } + return false; +} + +// #endregion diff --git a/packages/extension/src/hosted/api/vscode/ext.host.window.api.impl.ts b/packages/extension/src/hosted/api/vscode/ext.host.window.api.impl.ts index 34b8c57062..584ff70dbe 100644 --- a/packages/extension/src/hosted/api/vscode/ext.host.window.api.impl.ts +++ b/packages/extension/src/hosted/api/vscode/ext.host.window.api.impl.ts @@ -293,6 +293,9 @@ export function createWindowApiFactory( get onDidChangeOpenEditors() { return extHostEditorTabs.onDidChangeTabs; }, + get tabGroups() { + return extHostEditorTabs.tabGroups; + }, }; } diff --git a/packages/types/vscode/typings/vscode.editor.d.ts b/packages/types/vscode/typings/vscode.editor.d.ts index 09f8ac854e..37323045a4 100644 --- a/packages/types/vscode/typings/vscode.editor.d.ts +++ b/packages/types/vscode/typings/vscode.editor.d.ts @@ -1,699 +1,699 @@ declare module 'vscode' { /** - * Represents sources that can cause [selection change events](#window.onDidChangeTextEditorSelection). - */ - export enum TextEditorSelectionChangeKind { - /** - * Selection changed due to typing in the editor. - */ - Keyboard = 1, - /** - * Selection change due to clicking in the editor. - */ - Mouse = 2, - /** - * Selection changed because a command ran. - */ - Command = 3 - } - - /** - * Represents an event describing the change in a [text editor's selections](#TextEditor.selections). - */ - export interface TextEditorSelectionChangeEvent { - /** - * The [text editor](#TextEditor) for which the selections have changed. - */ - readonly textEditor: TextEditor; - /** - * The new value for the [text editor's selections](#TextEditor.selections). - */ - readonly selections: ReadonlyArray; - /** - * The [change kind](#TextEditorSelectionChangeKind) which has triggered this - * event. Can be `undefined`. - */ - readonly kind?: TextEditorSelectionChangeKind; - } - - /** - * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). - */ - export interface TextEditorVisibleRangesChangeEvent { - /** - * The [text editor](#TextEditor) for which the visible ranges have changed. - */ - readonly textEditor: TextEditor; - /** - * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). - */ - readonly visibleRanges: ReadonlyArray; - } - - /** - * Represents an event describing the change in a [text editor's options](#TextEditor.options). - */ - export interface TextEditorOptionsChangeEvent { - /** - * The [text editor](#TextEditor) for which the options have changed. - */ - readonly textEditor: TextEditor; - /** - * The new value for the [text editor's options](#TextEditor.options). - */ - readonly options: TextEditorOptions; - } - - /** - * Represents an event describing the change of a [text editor's view column](#TextEditor.viewColumn). - */ - export interface TextEditorViewColumnChangeEvent { - /** - * The [text editor](#TextEditor) for which the view column has changed. - */ - readonly textEditor: TextEditor; - /** - * The new value for the [text editor's view column](#TextEditor.viewColumn). - */ - readonly viewColumn: ViewColumn; - } - - /** - * Rendering style of the cursor. - */ - export enum TextEditorCursorStyle { - /** - * Render the cursor as a vertical thick line. - */ - Line = 1, - /** - * Render the cursor as a block filled. - */ - Block = 2, - /** - * Render the cursor as a thick horizontal line. - */ - Underline = 3, - /** - * Render the cursor as a vertical thin line. - */ - LineThin = 4, - /** - * Render the cursor as a block outlined. - */ - BlockOutline = 5, - /** - * Render the cursor as a thin horizontal line. - */ - UnderlineThin = 6 - } - - /** - * Rendering style of the line numbers. - */ - export enum TextEditorLineNumbersStyle { - /** - * Do not render the line numbers. - */ - Off = 0, - /** - * Render the line numbers. - */ - On = 1, - /** - * Render the line numbers with values relative to the primary cursor location. - */ - Relative = 2 - } - - /** - * Represents a [text editor](#TextEditor)'s [options](#TextEditor.options). - */ - export interface TextEditorOptions { - - /** - * The size in spaces a tab takes. This is used for two purposes: - * - the rendering width of a tab character; - * - the number of spaces to insert when [insertSpaces](#TextEditorOptions.insertSpaces) is true. - * - * When getting a text editor's options, this property will always be a number (resolved). - * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. - */ - tabSize?: number | string; - - /** - * When pressing Tab insert [n](#TextEditorOptions.tabSize) spaces. - * When getting a text editor's options, this property will always be a boolean (resolved). - * When setting a text editor's options, this property is optional and it can be a boolean or `"auto"`. - */ - insertSpaces?: boolean | string; - - /** - * The rendering style of the cursor in this editor. - * When getting a text editor's options, this property will always be present. - * When setting a text editor's options, this property is optional. - */ - cursorStyle?: TextEditorCursorStyle; - - /** - * Render relative line numbers w.r.t. the current line number. - * When getting a text editor's options, this property will always be present. - * When setting a text editor's options, this property is optional. - */ - lineNumbers?: TextEditorLineNumbersStyle; - } - - /** - * Represents a handle to a set of decorations - * sharing the same [styling options](#DecorationRenderOptions) in a [text editor](#TextEditor). - * - * To get an instance of a `TextEditorDecorationType` use - * [createTextEditorDecorationType](#window.createTextEditorDecorationType). - */ - export interface TextEditorDecorationType { - - /** - * Internal representation of the handle. - */ - readonly key: string; - - /** - * Remove this decoration type and all decorations on all text editors using it. - */ - dispose(): void; - } - - /** - * Represents different [reveal](#TextEditor.revealRange) strategies in a text editor. - */ - export enum TextEditorRevealType { - /** - * The range will be revealed with as little scrolling as possible. - */ - Default = 0, - /** - * The range will always be revealed in the center of the viewport. - */ - InCenter = 1, - /** - * If the range is outside the viewport, it will be revealed in the center of the viewport. - * Otherwise, it will be revealed with as little scrolling as possible. - */ - InCenterIfOutsideViewport = 2, - /** - * The range will always be revealed at the top of the viewport. - */ - AtTop = 3 - } - - /** - * Represents different positions for rendering a decoration in an [overview ruler](#DecorationRenderOptions.overviewRulerLane). - * The overview ruler supports three lanes. - */ - export enum OverviewRulerLane { - Left = 1, - Center = 2, - Right = 4, - Full = 7 - } - - /** - * Describes the behavior of decorations when typing/editing at their edges. - */ - export enum DecorationRangeBehavior { - /** - * The decoration's range will widen when edits occur at the start or end. - */ - OpenOpen = 0, - /** - * The decoration's range will not widen when edits occur at the start of end. - */ - ClosedClosed = 1, - /** - * The decoration's range will widen when edits occur at the start, but not at the end. - */ - OpenClosed = 2, - /** - * The decoration's range will widen when edits occur at the end, but not at the start. - */ - ClosedOpen = 3 - } - - /** - * Represents options to configure the behavior of showing a [document](#TextDocument) in an [editor](#TextEditor). - */ - export interface TextDocumentShowOptions { - /** - * An optional view column in which the [editor](#TextEditor) should be shown. - * The default is the [active](#ViewColumn.Active), other values are adjusted to - * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is - * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the - * editor to the side of the currently active one. - */ - viewColumn?: ViewColumn; - - /** - * An optional flag that when `true` will stop the [editor](#TextEditor) from taking focus. - */ - preserveFocus?: boolean; - - /** - * An optional flag that controls if an [editor](#TextEditor)-tab will be replaced - * with the next editor or if it will be kept. - */ - preview?: boolean; - - /** - * An optional selection to apply for the document in the [editor](#TextEditor). - */ - selection?: Range; - } - - /** - * Represents rendering styles for a [text editor decoration](#TextEditorDecorationType). - */ - export interface DecorationRenderOptions extends ThemableDecorationRenderOptions { - /** - * Should the decoration be rendered also on the whitespace after the line text. - * Defaults to `false`. - */ - isWholeLine?: boolean; - - /** - * Customize the growing behavior of the decoration when edits occur at the edges of the decoration's range. - * Defaults to `DecorationRangeBehavior.OpenOpen`. - */ - rangeBehavior?: DecorationRangeBehavior; - - /** - * The position in the overview ruler where the decoration should be rendered. - */ - overviewRulerLane?: OverviewRulerLane; - - /** - * Overwrite options for light themes. - */ - light?: ThemableDecorationRenderOptions; - - /** - * Overwrite options for dark themes. - */ - dark?: ThemableDecorationRenderOptions; - } - - /** - * Represents options for a specific decoration in a [decoration set](#TextEditorDecorationType). - */ - export interface DecorationOptions { - - /** - * Range to which this decoration is applied. The range must not be empty. - */ - range: Range; - - /** - * A message that should be rendered when hovering over the decoration. - */ - hoverMessage?: MarkedString | MarkedString[]; - - /** - * Render options applied to the current decoration. For performance reasons, keep the - * number of decoration specific options small, and use decoration types wherever possible. - */ - renderOptions?: DecorationInstanceRenderOptions; - } - - export interface ThemableDecorationInstanceRenderOptions { - /** - * Defines the rendering options of the attachment that is inserted before the decorated text. - */ - before?: ThemableDecorationAttachmentRenderOptions; - - /** - * Defines the rendering options of the attachment that is inserted after the decorated text. - */ - after?: ThemableDecorationAttachmentRenderOptions; - } - - export interface DecorationInstanceRenderOptions extends ThemableDecorationInstanceRenderOptions { - /** - * Overwrite options for light themes. - */ - light?: ThemableDecorationInstanceRenderOptions; - - /** - * Overwrite options for dark themes. - */ - dark?: ThemableDecorationInstanceRenderOptions; - } - - /** - * Represents an editor that is attached to a [document](#TextDocument). - */ - export interface TextEditor { - - /** - * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor. - */ - readonly document: TextDocument; - - /** - * The primary selection on this text editor. Shorthand for `TextEditor.selections[0]`. - */ - selection: Selection; - - /** - * The selections in this text editor. The primary selection is always at index 0. - */ - selections: Selection[]; - - /** - * The current visible ranges in the editor (vertically). - * This accounts only for vertical scrolling, and not for horizontal scrolling. - */ - readonly visibleRanges: Range[]; - - /** - * Text editor options. - */ - options: TextEditorOptions; - - /** - * The column in which this editor shows. Will be `undefined` in case this - * isn't one of the main editors, e.g. an embedded editor, or when the editor - * column is larger than three. - */ - viewColumn?: ViewColumn; - - /** - * Perform an edit on the document associated with this text editor. - * - * The given callback-function is invoked with an [edit-builder](#TextEditorEdit) which must - * be used to make edits. Note that the edit-builder is only valid while the - * callback executes. - * - * @param callback A function which can create edits using an [edit-builder](#TextEditorEdit). - * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. - * @return A promise that resolves with a value indicating if the edits could be applied. - */ - edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; - - /** - * Insert a [snippet](#SnippetString) and put the editor into snippet mode. "Snippet mode" - * means the editor adds placeholders and additional cursors so that the user can complete - * or accept the snippet. - * - * @param snippet The snippet to insert in this edit. - * @param location Position or range at which to insert the snippet, defaults to the current editor selection or selections. - * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. - * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal - * that the snippet is completely filled-in or accepted. - */ - insertSnippet(snippet: SnippetString, location?: Position | Range | ReadonlyArray | ReadonlyArray, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; - - /** - * Adds a set of decorations to the text editor. If a set of decorations already exists with - * the given [decoration type](#TextEditorDecorationType), they will be replaced. - * - * @see [createTextEditorDecorationType](#window.createTextEditorDecorationType). - * - * @param decorationType A decoration type. - * @param rangesOrOptions Either [ranges](#Range) or more detailed [options](#DecorationOptions). - */ - setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]): void; - - /** - * Scroll as indicated by `revealType` in order to reveal the given range. - * - * @param range A range. - * @param revealType The scrolling strategy for revealing `range`. - */ - revealRange(range: Range, revealType?: TextEditorRevealType): void; - - /** - * ~~Show the text editor.~~ - * - * @deprecated Use [window.showTextDocument](#window.showTextDocument) instead. - * - * @param column The [column](#ViewColumn) in which to show this editor. - * This method shows unexpected behavior and will be removed in the next major update. - */ - show(column?: ViewColumn): void; - - /** - * ~~Hide the text editor.~~ - * - * @deprecated Use the command `workbench.action.closeActiveEditor` instead. - * This method shows unexpected behavior and will be removed in the next major update. - */ - hide(): void; + * Represents sources that can cause [selection change events](#window.onDidChangeTextEditorSelection). + */ + export enum TextEditorSelectionChangeKind { + /** + * Selection changed due to typing in the editor. + */ + Keyboard = 1, + /** + * Selection change due to clicking in the editor. + */ + Mouse = 2, + /** + * Selection changed because a command ran. + */ + Command = 3 + } + + /** + * Represents an event describing the change in a [text editor's selections](#TextEditor.selections). + */ + export interface TextEditorSelectionChangeEvent { + /** + * The [text editor](#TextEditor) for which the selections have changed. + */ + readonly textEditor: TextEditor; + /** + * The new value for the [text editor's selections](#TextEditor.selections). + */ + readonly selections: ReadonlyArray; + /** + * The [change kind](#TextEditorSelectionChangeKind) which has triggered this + * event. Can be `undefined`. + */ + readonly kind?: TextEditorSelectionChangeKind; + } + + /** + * Represents an event describing the change in a [text editor's visible ranges](#TextEditor.visibleRanges). + */ + export interface TextEditorVisibleRangesChangeEvent { + /** + * The [text editor](#TextEditor) for which the visible ranges have changed. + */ + readonly textEditor: TextEditor; + /** + * The new value for the [text editor's visible ranges](#TextEditor.visibleRanges). + */ + readonly visibleRanges: ReadonlyArray; + } + + /** + * Represents an event describing the change in a [text editor's options](#TextEditor.options). + */ + export interface TextEditorOptionsChangeEvent { + /** + * The [text editor](#TextEditor) for which the options have changed. + */ + readonly textEditor: TextEditor; + /** + * The new value for the [text editor's options](#TextEditor.options). + */ + readonly options: TextEditorOptions; + } + + /** + * Represents an event describing the change of a [text editor's view column](#TextEditor.viewColumn). + */ + export interface TextEditorViewColumnChangeEvent { + /** + * The [text editor](#TextEditor) for which the view column has changed. + */ + readonly textEditor: TextEditor; + /** + * The new value for the [text editor's view column](#TextEditor.viewColumn). + */ + readonly viewColumn: ViewColumn; + } + + /** + * Rendering style of the cursor. + */ + export enum TextEditorCursorStyle { + /** + * Render the cursor as a vertical thick line. + */ + Line = 1, + /** + * Render the cursor as a block filled. + */ + Block = 2, + /** + * Render the cursor as a thick horizontal line. + */ + Underline = 3, + /** + * Render the cursor as a vertical thin line. + */ + LineThin = 4, + /** + * Render the cursor as a block outlined. + */ + BlockOutline = 5, + /** + * Render the cursor as a thin horizontal line. + */ + UnderlineThin = 6 + } + + /** + * Rendering style of the line numbers. + */ + export enum TextEditorLineNumbersStyle { + /** + * Do not render the line numbers. + */ + Off = 0, + /** + * Render the line numbers. + */ + On = 1, + /** + * Render the line numbers with values relative to the primary cursor location. + */ + Relative = 2 + } + + /** + * Represents a [text editor](#TextEditor)'s [options](#TextEditor.options). + */ + export interface TextEditorOptions { + + /** + * The size in spaces a tab takes. This is used for two purposes: + * - the rendering width of a tab character; + * - the number of spaces to insert when [insertSpaces](#TextEditorOptions.insertSpaces) is true. + * + * When getting a text editor's options, this property will always be a number (resolved). + * When setting a text editor's options, this property is optional and it can be a number or `"auto"`. + */ + tabSize?: number | string; + + /** + * When pressing Tab insert [n](#TextEditorOptions.tabSize) spaces. + * When getting a text editor's options, this property will always be a boolean (resolved). + * When setting a text editor's options, this property is optional and it can be a boolean or `"auto"`. + */ + insertSpaces?: boolean | string; + + /** + * The rendering style of the cursor in this editor. + * When getting a text editor's options, this property will always be present. + * When setting a text editor's options, this property is optional. + */ + cursorStyle?: TextEditorCursorStyle; + + /** + * Render relative line numbers w.r.t. the current line number. + * When getting a text editor's options, this property will always be present. + * When setting a text editor's options, this property is optional. + */ + lineNumbers?: TextEditorLineNumbersStyle; + } + + /** + * Represents a handle to a set of decorations + * sharing the same [styling options](#DecorationRenderOptions) in a [text editor](#TextEditor). + * + * To get an instance of a `TextEditorDecorationType` use + * [createTextEditorDecorationType](#window.createTextEditorDecorationType). + */ + export interface TextEditorDecorationType { + + /** + * Internal representation of the handle. + */ + readonly key: string; + + /** + * Remove this decoration type and all decorations on all text editors using it. + */ + dispose(): void; + } + + /** + * Represents different [reveal](#TextEditor.revealRange) strategies in a text editor. + */ + export enum TextEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2, + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 + } + + /** + * Represents different positions for rendering a decoration in an [overview ruler](#DecorationRenderOptions.overviewRulerLane). + * The overview ruler supports three lanes. + */ + export enum OverviewRulerLane { + Left = 1, + Center = 2, + Right = 4, + Full = 7 + } + + /** + * Describes the behavior of decorations when typing/editing at their edges. + */ + export enum DecorationRangeBehavior { + /** + * The decoration's range will widen when edits occur at the start or end. + */ + OpenOpen = 0, + /** + * The decoration's range will not widen when edits occur at the start of end. + */ + ClosedClosed = 1, + /** + * The decoration's range will widen when edits occur at the start, but not at the end. + */ + OpenClosed = 2, + /** + * The decoration's range will widen when edits occur at the end, but not at the start. + */ + ClosedOpen = 3 + } + + /** + * Represents options to configure the behavior of showing a [document](#TextDocument) in an [editor](#TextEditor). + */ + export interface TextDocumentShowOptions { + /** + * An optional view column in which the [editor](#TextEditor) should be shown. + * The default is the [active](#ViewColumn.Active), other values are adjusted to + * be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is + * not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) to open the + * editor to the side of the currently active one. + */ + viewColumn?: ViewColumn; + + /** + * An optional flag that when `true` will stop the [editor](#TextEditor) from taking focus. + */ + preserveFocus?: boolean; + + /** + * An optional flag that controls if an [editor](#TextEditor)-tab will be replaced + * with the next editor or if it will be kept. + */ + preview?: boolean; + + /** + * An optional selection to apply for the document in the [editor](#TextEditor). + */ + selection?: Range; + } + + /** + * Represents rendering styles for a [text editor decoration](#TextEditorDecorationType). + */ + export interface DecorationRenderOptions extends ThemableDecorationRenderOptions { + /** + * Should the decoration be rendered also on the whitespace after the line text. + * Defaults to `false`. + */ + isWholeLine?: boolean; + + /** + * Customize the growing behavior of the decoration when edits occur at the edges of the decoration's range. + * Defaults to `DecorationRangeBehavior.OpenOpen`. + */ + rangeBehavior?: DecorationRangeBehavior; + + /** + * The position in the overview ruler where the decoration should be rendered. + */ + overviewRulerLane?: OverviewRulerLane; + + /** + * Overwrite options for light themes. + */ + light?: ThemableDecorationRenderOptions; + + /** + * Overwrite options for dark themes. + */ + dark?: ThemableDecorationRenderOptions; + } + + /** + * Represents options for a specific decoration in a [decoration set](#TextEditorDecorationType). + */ + export interface DecorationOptions { + + /** + * Range to which this decoration is applied. The range must not be empty. + */ + range: Range; + + /** + * A message that should be rendered when hovering over the decoration. + */ + hoverMessage?: MarkedString | MarkedString[]; + + /** + * Render options applied to the current decoration. For performance reasons, keep the + * number of decoration specific options small, and use decoration types wherever possible. + */ + renderOptions?: DecorationInstanceRenderOptions; + } + + export interface ThemableDecorationInstanceRenderOptions { + /** + * Defines the rendering options of the attachment that is inserted before the decorated text. + */ + before?: ThemableDecorationAttachmentRenderOptions; + + /** + * Defines the rendering options of the attachment that is inserted after the decorated text. + */ + after?: ThemableDecorationAttachmentRenderOptions; + } + + export interface DecorationInstanceRenderOptions extends ThemableDecorationInstanceRenderOptions { + /** + * Overwrite options for light themes. + */ + light?: ThemableDecorationInstanceRenderOptions; + + /** + * Overwrite options for dark themes. + */ + dark?: ThemableDecorationInstanceRenderOptions; + } + + /** + * Represents an editor that is attached to a [document](#TextDocument). + */ + export interface TextEditor { + + /** + * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor. + */ + readonly document: TextDocument; + + /** + * The primary selection on this text editor. Shorthand for `TextEditor.selections[0]`. + */ + selection: Selection; + + /** + * The selections in this text editor. The primary selection is always at index 0. + */ + selections: Selection[]; + + /** + * The current visible ranges in the editor (vertically). + * This accounts only for vertical scrolling, and not for horizontal scrolling. + */ + readonly visibleRanges: Range[]; + + /** + * Text editor options. + */ + options: TextEditorOptions; + + /** + * The column in which this editor shows. Will be `undefined` in case this + * isn't one of the main editors, e.g. an embedded editor, or when the editor + * column is larger than three. + */ + viewColumn?: ViewColumn; + + /** + * Perform an edit on the document associated with this text editor. + * + * The given callback-function is invoked with an [edit-builder](#TextEditorEdit) which must + * be used to make edits. Note that the edit-builder is only valid while the + * callback executes. + * + * @param callback A function which can create edits using an [edit-builder](#TextEditorEdit). + * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. + * @return A promise that resolves with a value indicating if the edits could be applied. + */ + edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + + /** + * Insert a [snippet](#SnippetString) and put the editor into snippet mode. "Snippet mode" + * means the editor adds placeholders and additional cursors so that the user can complete + * or accept the snippet. + * + * @param snippet The snippet to insert in this edit. + * @param location Position or range at which to insert the snippet, defaults to the current editor selection or selections. + * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. + * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal + * that the snippet is completely filled-in or accepted. + */ + insertSnippet(snippet: SnippetString, location?: Position | Range | ReadonlyArray | ReadonlyArray, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + + /** + * Adds a set of decorations to the text editor. If a set of decorations already exists with + * the given [decoration type](#TextEditorDecorationType), they will be replaced. + * + * @see [createTextEditorDecorationType](#window.createTextEditorDecorationType). + * + * @param decorationType A decoration type. + * @param rangesOrOptions Either [ranges](#Range) or more detailed [options](#DecorationOptions). + */ + setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: Range[] | DecorationOptions[]): void; + + /** + * Scroll as indicated by `revealType` in order to reveal the given range. + * + * @param range A range. + * @param revealType The scrolling strategy for revealing `range`. + */ + revealRange(range: Range, revealType?: TextEditorRevealType): void; + + /** + * ~~Show the text editor.~~ + * + * @deprecated Use [window.showTextDocument](#window.showTextDocument) instead. + * + * @param column The [column](#ViewColumn) in which to show this editor. + * This method shows unexpected behavior and will be removed in the next major update. + */ + show(column?: ViewColumn): void; + + /** + * ~~Hide the text editor.~~ + * + * @deprecated Use the command `workbench.action.closeActiveEditor` instead. + * This method shows unexpected behavior and will be removed in the next major update. + */ + hide(): void; } export interface TextEditorEdit { - /** - * Replace a certain text region with a new value. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). - * - * @param location The range this operation should remove. - * @param value The new text this operation should insert after removing `location`. - */ - replace(location: Position | Range | Selection, value: string): void; - - /** - * Insert text at a location. - * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). - * Although the equivalent text edit can be made with [replace](#TextEditorEdit.replace), `insert` will produce a different resulting selection (it will get moved). - * - * @param location The position where the new text should be inserted. - * @param value The new text this operation should insert. - */ - insert(location: Position, value: string): void; - - /** - * Delete a certain text region. - * - * @param location The range this operation should remove. - */ - delete(location: Range | Selection): void; - - /** - * Set the end of line sequence. - * - * @param endOfLine The new end of line for the [document](#TextDocument). - */ - setEndOfLine(endOfLine: EndOfLine): void; - } - - /** - * Denotes a location of an editor in the window. Editors can be arranged in a grid - * and each column represents one editor location in that grid by counting the editors - * in order of their appearance. - */ - export enum ViewColumn { - /** - * A *symbolic* editor column representing the currently active column. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value - * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. - */ - Active = -1, - /** - * A *symbolic* editor column representing the column to the side of the active one. This value - * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value - * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. - */ - Beside = -2, - /** - * The first editor column. - */ - One = 1, - /** - * The second editor column. - */ - Two = 2, - /** - * The third editor column. - */ - Three = 3, - /** - * The fourth editor column. - */ - Four = 4, - /** - * The fifth editor column. - */ - Five = 5, - /** - * The sixth editor column. - */ - Six = 6, - /** - * The seventh editor column. - */ - Seven = 7, - /** - * The eighth editor column. - */ - Eight = 8, - /** - * The ninth editor column. - */ - Nine = 9 - } + /** + * Replace a certain text region with a new value. + * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). + * + * @param location The range this operation should remove. + * @param value The new text this operation should insert after removing `location`. + */ + replace(location: Position | Range | Selection, value: string): void; + + /** + * Insert text at a location. + * You can use \r\n or \n in `value` and they will be normalized to the current [document](#TextDocument). + * Although the equivalent text edit can be made with [replace](#TextEditorEdit.replace), `insert` will produce a different resulting selection (it will get moved). + * + * @param location The position where the new text should be inserted. + * @param value The new text this operation should insert. + */ + insert(location: Position, value: string): void; + + /** + * Delete a certain text region. + * + * @param location The range this operation should remove. + */ + delete(location: Range | Selection): void; + + /** + * Set the end of line sequence. + * + * @param endOfLine The new end of line for the [document](#TextDocument). + */ + setEndOfLine(endOfLine: EndOfLine): void; + } + + /** + * Denotes a location of an editor in the window. Editors can be arranged in a grid + * and each column represents one editor location in that grid by counting the editors + * in order of their appearance. + */ + export enum ViewColumn { + /** + * A *symbolic* editor column representing the currently active column. This value + * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`. + */ + Active = -1, + /** + * A *symbolic* editor column representing the column to the side of the active one. This value + * can be used when opening editors, but the *resolved* [viewColumn](#TextEditor.viewColumn)-value + * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`. + */ + Beside = -2, + /** + * The first editor column. + */ + One = 1, + /** + * The second editor column. + */ + Two = 2, + /** + * The third editor column. + */ + Three = 3, + /** + * The fourth editor column. + */ + Four = 4, + /** + * The fifth editor column. + */ + Five = 5, + /** + * The sixth editor column. + */ + Six = 6, + /** + * The seventh editor column. + */ + Seven = 7, + /** + * The eighth editor column. + */ + Eight = 8, + /** + * The ninth editor column. + */ + Nine = 9 + } export namespace window { /** - * The currently active editor or `undefined`. The active editor is the one - * that currently has focus or, when none has focus, the one that has changed - * input most recently. - */ - export let activeTextEditor: TextEditor | undefined; + * The currently active editor or `undefined`. The active editor is the one + * that currently has focus or, when none has focus, the one that has changed + * input most recently. + */ + export let activeTextEditor: TextEditor | undefined; - /** - * The currently visible editors or an empty array. - */ + /** + * The currently visible editors or an empty array. + */ export let visibleTextEditors: TextEditor[]; /** - * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) - * has changed. *Note* that the event also fires when the active editor changes - * to `undefined`. - */ - export const onDidChangeActiveTextEditor: Event; - - /** - * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) - * has changed. - */ - export const onDidChangeVisibleTextEditors: Event; - - /** - * An [event](#Event) which fires when the selection in an editor has changed. - */ - export const onDidChangeTextEditorSelection: Event; - - /** - * An [event](#Event) which fires when the visible ranges of an editor has changed. - */ - export const onDidChangeTextEditorVisibleRanges: Event; - - /** - * An [event](#Event) which fires when the options of an editor have changed. - */ - export const onDidChangeTextEditorOptions: Event; - - /** - * An [event](#Event) which fires when the view column of an editor has changed. - */ - export const onDidChangeTextEditorViewColumn: Event; - - - /** - * Show the given document in a text editor. A [column](#ViewColumn) can be provided - * to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor). - * - * @param document A text document to be shown. - * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [active](#ViewColumn.Active), other values - * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) - * to open the editor to the side of the currently active one. - * @param preserveFocus When `true` the editor will not take focus. - * @return A promise that resolves to an [editor](#TextEditor). - */ - export function showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; - - /** - * Show the given document in a text editor. [Options](#TextDocumentShowOptions) can be provided - * to control options of the editor is being shown. Might change the [active editor](#window.activeTextEditor). - * - * @param document A text document to be shown. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). - */ - export function showTextDocument(document: TextDocument, options?: TextDocumentShowOptions): Thenable; - - /** - * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. - * - * @see [openTextDocument](#openTextDocument) - * - * @param uri A resource identifier. - * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). - * @return A promise that resolves to an [editor](#TextEditor). - */ - export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; - - /** - * Create a TextEditorDecorationType that can be used to add decorations to text editors. - * - * @param options Rendering options for the decoration type. - * @return A new decoration type instance. - */ - export function createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType; - - } - - export class TextEdit { - - /** - * Utility to create a replace edit. - * - * @param range A range. - * @param newText A string. - * @return A new text edit object. - */ - static replace(range: Range, newText: string): TextEdit; - - /** - * Utility to create an insert edit. - * - * @param position A position, will become an empty range. - * @param newText A string. - * @return A new text edit object. - */ - static insert(position: Position, newText: string): TextEdit; - - /** - * Utility to create a delete edit. - * - * @param range A range. - * @return A new text edit object. - */ - static delete(range: Range): TextEdit; - - /** - * Utility to create an eol-edit. - * - * @param eol An eol-sequence - * @return A new text edit object. - */ - static setEndOfLine(eol: EndOfLine): TextEdit; - - /** - * The range this edit applies to. - */ - range: Range; - - /** - * The string this edit will insert. - */ - newText: string; - - /** - * The eol-sequence used in the document. - * - * *Note* that the eol-sequence will be applied to the - * whole document. - */ - newEol: EndOfLine; - - /** - * Create a new TextEdit. - * - * @param range A range. - * @param newText A string. - */ - constructor(range: Range, newText: string); + * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) + * has changed. *Note* that the event also fires when the active editor changes + * to `undefined`. + */ + export const onDidChangeActiveTextEditor: Event; + + /** + * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) + * has changed. + */ + export const onDidChangeVisibleTextEditors: Event; + + /** + * An [event](#Event) which fires when the selection in an editor has changed. + */ + export const onDidChangeTextEditorSelection: Event; + + /** + * An [event](#Event) which fires when the visible ranges of an editor has changed. + */ + export const onDidChangeTextEditorVisibleRanges: Event; + + /** + * An [event](#Event) which fires when the options of an editor have changed. + */ + export const onDidChangeTextEditorOptions: Event; + + /** + * An [event](#Event) which fires when the view column of an editor has changed. + */ + export const onDidChangeTextEditorViewColumn: Event; + + + /** + * Show the given document in a text editor. A [column](#ViewColumn) can be provided + * to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * + * @param document A text document to be shown. + * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [active](#ViewColumn.Active), other values + * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is not adjusted. Use [`ViewColumn.Beside`](#ViewColumn.Beside) + * to open the editor to the side of the currently active one. + * @param preserveFocus When `true` the editor will not take focus. + * @return A promise that resolves to an [editor](#TextEditor). + */ + export function showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; + + /** + * Show the given document in a text editor. [Options](#TextDocumentShowOptions) can be provided + * to control options of the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * + * @param document A text document to be shown. + * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). + * @return A promise that resolves to an [editor](#TextEditor). + */ + export function showTextDocument(document: TextDocument, options?: TextDocumentShowOptions): Thenable; + + /** + * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. + * + * @see [openTextDocument](#openTextDocument) + * + * @param uri A resource identifier. + * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). + * @return A promise that resolves to an [editor](#TextEditor). + */ + export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; + + /** + * Create a TextEditorDecorationType that can be used to add decorations to text editors. + * + * @param options Rendering options for the decoration type. + * @return A new decoration type instance. + */ + export function createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType; + + } + + export class TextEdit { + + /** + * Utility to create a replace edit. + * + * @param range A range. + * @param newText A string. + * @return A new text edit object. + */ + static replace(range: Range, newText: string): TextEdit; + + /** + * Utility to create an insert edit. + * + * @param position A position, will become an empty range. + * @param newText A string. + * @return A new text edit object. + */ + static insert(position: Position, newText: string): TextEdit; + + /** + * Utility to create a delete edit. + * + * @param range A range. + * @return A new text edit object. + */ + static delete(range: Range): TextEdit; + + /** + * Utility to create an eol-edit. + * + * @param eol An eol-sequence + * @return A new text edit object. + */ + static setEndOfLine(eol: EndOfLine): TextEdit; + + /** + * The range this edit applies to. + */ + range: Range; + + /** + * The string this edit will insert. + */ + newText: string; + + /** + * The eol-sequence used in the document. + * + * *Note* that the eol-sequence will be applied to the + * whole document. + */ + newEol: EndOfLine; + + /** + * Create a new TextEdit. + * + * @param range A range. + * @param newText A string. + */ + constructor(range: Range, newText: string); } /** @@ -1008,4 +1008,292 @@ declare module 'vscode' { backupCustomDocument(document: T, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable; } + + /** + * The tab represents a single text based resource. + */ + export class TabInputText { + /** + * The uri represented by the tab. + */ + readonly uri: Uri; + /** + * Constructs a text tab input with the given URI. + * @param uri The URI of the tab. + */ + constructor(uri: Uri); + } + + /** + * The tab represents two text based resources + * being rendered as a diff. + */ + export class TabInputTextDiff { + /** + * The uri of the original text resource. + */ + readonly original: Uri; + /** + * The uri of the modified text resource. + */ + readonly modified: Uri; + /** + * Constructs a new text diff tab input with the given URIs. + * @param original The uri of the original text resource. + * @param modified The uri of the modified text resource. + */ + constructor(original: Uri, modified: Uri); + } + + /** + * The tab represents a custom editor. + */ + export class TabInputCustom { + /** + * The uri that the tab is representing. + */ + readonly uri: Uri; + /** + * The type of custom editor. + */ + readonly viewType: string; + /** + * Constructs a custom editor tab input. + * @param uri The uri of the tab. + * @param viewType The viewtype of the custom editor. + */ + constructor(uri: Uri, viewType: string); + } + + /** + * The tab represents a webview. + */ + export class TabInputWebview { + /** + * The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + readonly viewType: string; + /** + * Constructs a webview tab input with the given view type. + * @param viewType The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + constructor(viewType: string); + } + + /** + * The tab represents a notebook. + */ + export class TabInputNotebook { + /** + * The uri that the tab is representing. + */ + readonly uri: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a new tab input for a notebook. + * @param uri The uri of the notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(uri: Uri, notebookType: string); + } + + /** + * The tabs represents two notebooks in a diff configuration. + */ + export class TabInputNotebookDiff { + /** + * The uri of the original notebook. + */ + readonly original: Uri; + /** + * The uri of the modified notebook. + */ + readonly modified: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a notebook diff tab input. + * @param original The uri of the original unmodified notebook. + * @param modified The uri of the modified notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(original: Uri, modified: Uri, notebookType: string); + } + + /** + * The tab represents a terminal in the editor area. + */ + export class TabInputTerminal { + /** + * Constructs a terminal tab input. + */ + constructor(); + } + + /** + * Represents a tab within a {@link TabGroup group of tabs}. + * Tabs are merely the graphical representation within the editor area. + * A backing editor is not a guarantee. + */ + export interface Tab { + + /** + * The text displayed on the tab. + */ + readonly label: string; + + /** + * The group which the tab belongs to. + */ + readonly group: TabGroup; + + /** + * Defines the structure of the tab i.e. text, notebook, custom, etc. + * Resource and other useful properties are defined on the tab kind. + */ + readonly input: TabInputText | TabInputTextDiff | TabInputCustom | TabInputWebview | TabInputNotebook | TabInputNotebookDiff | TabInputTerminal | unknown; + + /** + * Whether or not the tab is currently active. + * This is dictated by being the selected tab in the group. + */ + readonly isActive: boolean; + + /** + * Whether or not the dirty indicator is present on the tab. + */ + readonly isDirty: boolean; + + /** + * Whether or not the tab is pinned (pin icon is present). + */ + readonly isPinned: boolean; + + /** + * Whether or not the tab is in preview mode. + */ + readonly isPreview: boolean; + } + + /** + * An event describing change to tabs. + */ + export interface TabChangeEvent { + /** + * The tabs that have been opened. + */ + readonly opened: readonly Tab[]; + /** + * The tabs that have been closed. + */ + readonly closed: readonly Tab[]; + /** + * Tabs that have changed, e.g have changed + * their {@link Tab.isActive active} state. + */ + readonly changed: readonly Tab[]; + } + + /** + * An event describing changes to tab groups. + */ + export interface TabGroupChangeEvent { + /** + * Tab groups that have been opened. + */ + readonly opened: readonly TabGroup[]; + /** + * Tab groups that have been closed. + */ + readonly closed: readonly TabGroup[]; + /** + * Tab groups that have changed, e.g have changed + * their {@link TabGroup.isActive active} state. + */ + readonly changed: readonly TabGroup[]; + } + + /** + * Represents a group of tabs. A tab group itself consists of multiple tabs. + */ + export interface TabGroup { + /** + * Whether or not the group is currently active. + * + * *Note* that only one tab group is active at a time, but that multiple tab + * groups can have an {@link TabGroup.aciveTab active tab}. + * + * @see {@link Tab.isActive} + */ + readonly isActive: boolean; + + /** + * The view column of the group. + */ + readonly viewColumn: ViewColumn; + + /** + * The active {@link Tab tab} in the group. This is the tab whose contents are currently + * being rendered. + * + * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. + */ + readonly activeTab: Tab | undefined; + + /** + * The list of tabs contained within the group. + * This can be empty if the group has no tabs open. + */ + readonly tabs: readonly Tab[]; + } + + /** + * Represents the main editor area which consists of multple groups which contain tabs. + */ + export interface TabGroups { + /** + * All the groups within the group container. + */ + readonly all: readonly TabGroup[]; + + /** + * The currently active group. + */ + readonly activeTabGroup: TabGroup; + + /** + * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. + */ + readonly onDidChangeTabGroups: Event; + + /** + * An {@link Event event} which fires when {@link Tab tabs} have changed. + */ + readonly onDidChangeTabs: Event; + + /** + * Closes the tab. This makes the tab object invalid and the tab + * should no longer be used for further actions. + * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid + * + * @param tab The tab to close. + * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + * @returns A promise that resolves to `true` when all tabs have been closed. + */ + close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; + + /** + * Closes the tab group. This makes the tab group object invalid and the tab group + * should no longer be used for further actions. + * @param tabGroup The tab group to close. + * @param preserveFocus When `true` focus will remain in its current position. + * @returns A promise that resolves to `true` when all tab groups have been closed. + */ + close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; + } } diff --git a/packages/utils/src/functional.ts b/packages/utils/src/functional.ts index a03d51afea..3611628e23 100644 --- a/packages/utils/src/functional.ts +++ b/packages/utils/src/functional.ts @@ -41,3 +41,19 @@ export function removeObjectFromArray(array: Array, object: T, compa array.splice(index, 1); } } + +export function diffSets(before: Set, after: Set): { removed: T[]; added: T[] } { + const removed: T[] = []; + const added: T[] = []; + for (const element of before) { + if (!after.has(element)) { + removed.push(element); + } + } + for (const element of after) { + if (!before.has(element)) { + added.push(element); + } + } + return { removed, added }; +} diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index eb68795894..3696258766 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -6,6 +6,17 @@ const _typeof = { function: 'function', }; +/** + * Asserts that the argument passed in is neither undefined nor null. + */ +export function assertIsDefined(arg: T | null | undefined): T { + if (isUndefinedOrNull(arg)) { + throw new Error('Assertion Failed: argument is undefined or null'); + } + + return arg; +} + /** * @returns whether the provided parameter is a JavaScript Array or not. */ diff --git a/packages/webview/src/browser/types.ts b/packages/webview/src/browser/types.ts index 621b916e89..6571dc6fa4 100644 --- a/packages/webview/src/browser/types.ts +++ b/packages/webview/src/browser/types.ts @@ -170,7 +170,11 @@ export interface IWebviewService { getWebview(id: string): IWebview | undefined; - createEditorWebviewComponent(options?: IWebviewContentOptions, id?: string): IEditorWebviewComponent; + createEditorWebviewComponent( + options?: IWebviewContentOptions, + id?: string, + metadata?: Record, + ): IEditorWebviewComponent; createEditorPlainWebviewComponent( options?: IPlainWebviewConstructionOptions, @@ -254,6 +258,7 @@ export interface IPlainWebviewComponentHandle extends IDisposable { export interface IEditorWebviewMetaData { id: string; options?: IWebviewContentOptions; + metadata?: Record; } export interface IWebviewReviver { diff --git a/packages/webview/src/browser/webview.service.ts b/packages/webview/src/browser/webview.service.ts index e22109232e..64e2ab4124 100644 --- a/packages/webview/src/browser/webview.service.ts +++ b/packages/webview/src/browser/webview.service.ts @@ -161,7 +161,8 @@ export class WebviewServiceImpl implements IWebviewService { private async storeWebviewResource(id: string) { return this.storage.then((storage) => { if (this.editorWebviewComponents.has(id)) { - const res = { ...this.editorWebviewComponents.get(id)!.resource }; + const editorWebview = this.editorWebviewComponents.get(id)!; + const res = { v: 2, resource: editorWebview.resource, metadata: editorWebview.metadata }; storage.set(id, JSON.stringify(res)); } else { storage.delete(id); @@ -171,16 +172,31 @@ export class WebviewServiceImpl implements IWebviewService { public async tryRestoredWebviewComponent(id: string): Promise { const storage = await this.storage; - const resource: IResource | null = storage.get(id) ? JSON.parse(storage.get(id)!) : null; + const res = storage.get(id) ? JSON.parse(storage.get(id)!) : null; + if (!res) { + return; + } + let resource: IResource | null; + let metadata: Record | undefined; + if (res?.v === 2) { + resource = res.resource; + metadata = res.metadata; + } else { + resource = res; + } if (resource) { - const component = this.createEditorWebviewComponent(resource.metadata?.options, resource.metadata?.id); + const component = this.createEditorWebviewComponent(resource.metadata?.options, resource.metadata?.id, metadata); component.title = resource.name; component.supportsRevive = !!resource.supportsRevive; this.tryReviveWebviewComponent(id); } } - createEditorWebviewComponent(options?: IWebviewContentOptions, id?: string): IEditorWebviewComponent { + createEditorWebviewComponent( + options?: IWebviewContentOptions, + id?: string, + metadata?: Record, + ): IEditorWebviewComponent { if (!id) { id = (this.editorWebviewIdCount++).toString(); } @@ -190,6 +206,7 @@ export class WebviewServiceImpl implements IWebviewService { const component = this.injector.get(EditorWebviewComponent, [ id, () => this.createWebview(options), + metadata, ]) as EditorWebviewComponent; this.editorWebviewComponents.set(id, component); component.addDispose({ @@ -420,7 +437,11 @@ export class EditorWebviewComponent return EDITOR_WEBVIEW_SCHEME + '_' + this.id; } - constructor(public readonly id: string, public webviewFactory: () => T) { + constructor( + public readonly id: string, + public webviewFactory: () => T, + public readonly metadata?: Record, + ) { super(); const componentId = EDITOR_WEBVIEW_SCHEME + '_' + this.id; this.addDispose( @@ -429,6 +450,7 @@ export class EditorWebviewComponent uid: componentId, component: EditorWebviewComponentView, renderMode: EditorComponentRenderMode.ONE_PER_WORKBENCH, + metadata, }), ); this.addDispose(