From af866c7033e9c2a9291f6a0ba51fde45a364d0fa Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Fri, 23 Sep 2022 14:44:29 +0000 Subject: [PATCH 01/15] Added types to theia.d.ts --- packages/plugin-ext/src/common/collections.ts | 37 ++ .../plugin-ext/src/common/plugin-api-rpc.ts | 128 +++++- packages/plugin-ext/src/common/types.ts | 11 + .../src/main/browser/tabs/tabs-main.ts | 42 ++ .../plugin-ext/src/plugin/plugin-context.ts | 27 +- packages/plugin-ext/src/plugin/tabs.ts | 430 ++++++++++++++++++ .../plugin-ext/src/plugin/type-converters.ts | 22 + packages/plugin-ext/src/plugin/types-impl.ts | 39 ++ packages/plugin/src/theia.d.ts | 334 ++++++++++++++ 9 files changed, 1065 insertions(+), 5 deletions(-) create mode 100644 packages/plugin-ext/src/common/collections.ts create mode 100644 packages/plugin-ext/src/main/browser/tabs/tabs-main.ts create mode 100644 packages/plugin-ext/src/plugin/tabs.ts diff --git a/packages/plugin-ext/src/common/collections.ts b/packages/plugin-ext/src/common/collections.ts new file mode 100644 index 0000000000000..1e2576a3f7582 --- /dev/null +++ b/packages/plugin-ext/src/common/collections.ts @@ -0,0 +1,37 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/1.71.2/src/vs/base/common/collections.ts + +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/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 6f8ec9fd68506..024a7d9247b74 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -1919,6 +1919,128 @@ export interface CommentsMain { $onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void; } +// #region + +export const enum TabInputKind { + UnknownInput, + TextInput, + TextDiffInput, + TextMergeInput, + NotebookInput, + NotebookDiffInput, + CustomEditorInput, + WebviewEditorInput, + TerminalEditorInput, + InteractiveEditorInput, +} + +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 EditorGroupColumn = number; +export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | TabInputDto; + +export interface TabGroupDto { + isActive: boolean; + viewColumn: EditorGroupColumn; + tabs: TabDto[]; + groupId: number; +} + +export const enum TabModelOperationKind { + TAB_OPEN, + TAB_CLOSE, + TAB_UPDATE, + TAB_MOVE +} + +export interface TabOperation { + readonly kind: TabModelOperationKind.TAB_OPEN | TabModelOperationKind.TAB_CLOSE | TabModelOperationKind.TAB_UPDATE | TabModelOperationKind.TAB_MOVE; + readonly index: number; + readonly tabDto: TabDto; + readonly groupId: number; + readonly oldIndex?: number; +} + +export interface TabDto { + id: string; + label: string; + input: any; + editorId?: string; + isActive: boolean; + isPinned: boolean; + isPreview: boolean; + isDirty: boolean; +} + +export interface TabsExt { + $acceptEditorTabModel(tabGroups: TabGroupDto[]): void; + $acceptTabGroupUpdate(groupDto: TabGroupDto): void; + $acceptTabOperation(operation: TabOperation): void; +} + +export interface TabsMain { + $moveTab(tabId: string, index: number, viewColumn: EditorGroupColumn, preserveFocus?: boolean): void; + $closeTab(tabIds: string[], preserveFocus?: boolean): Promise; + $closeGroup(groupIds: number[], preservceFocus?: boolean): Promise; +} + +// endregion + export const PLUGIN_RPC_CONTEXT = { AUTHENTICATION_MAIN: >createProxyIdentifier('AuthenticationMain'), COMMAND_REGISTRY_MAIN: >createProxyIdentifier('CommandRegistryMain'), @@ -1952,7 +2074,8 @@ export const PLUGIN_RPC_CONTEXT = { LABEL_SERVICE_MAIN: >createProxyIdentifier('LabelServiceMain'), TIMELINE_MAIN: >createProxyIdentifier('TimelineMain'), THEMING_MAIN: >createProxyIdentifier('ThemingMain'), - COMMENTS_MAIN: >createProxyIdentifier('CommentsMain') + COMMENTS_MAIN: >createProxyIdentifier('CommentsMain'), + TABS_MAIN: >createProxyIdentifier('TabsMain') }; export const MAIN_RPC_CONTEXT = { @@ -1986,7 +2109,8 @@ export const MAIN_RPC_CONTEXT = { LABEL_SERVICE_EXT: createProxyIdentifier('LabelServiceExt'), TIMELINE_EXT: createProxyIdentifier('TimeLineExt'), THEMING_EXT: createProxyIdentifier('ThemingExt'), - COMMENTS_EXT: createProxyIdentifier('CommentsExt') + COMMENTS_EXT: createProxyIdentifier('CommentsExt'), + TABS_EXT: createProxyIdentifier('TabsExt') }; export interface TasksExt { diff --git a/packages/plugin-ext/src/common/types.ts b/packages/plugin-ext/src/common/types.ts index d1c8b4c6722f7..9885fbd32feaa 100644 --- a/packages/plugin-ext/src/common/types.ts +++ b/packages/plugin-ext/src/common/types.ts @@ -116,3 +116,14 @@ export function isUndefined(obj: any): obj is undefined { export function isUndefinedOrNull(obj: any): obj is undefined | null { return isUndefined(obj) || obj === null; // eslint-disable-line no-null/no-null } + +/** + * 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; +} diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts new file mode 100644 index 0000000000000..7d85c44347aec --- /dev/null +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -0,0 +1,42 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { interfaces } from '@theia/core/shared/inversify'; + +import { TabsMain } from '../../../common/plugin-api-rpc'; +import { RPCProtocol } from '../../../common/rpc-protocol'; + +export class TabsMainImp implements TabsMain { + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) {} + + // #region Messages received from Ext Host + $moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean): void { + return; + } + + async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { + return false; + } + + async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { + return false; + } + // #endregion +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 260aaf08ac1d3..2eaec99943d93 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -175,7 +175,15 @@ import { ExtensionKind, InlineCompletionItem, InlineCompletionList, - InlineCompletionTriggerKind + InlineCompletionTriggerKind, + TextTabInput, + CustomEditorTabInput, + NotebookDiffEditorTabInput, + NotebookEditorTabInput, + TerminalEditorTabInput, + TextDiffTabInput, + TextMergeTabInput, + WebviewEditorTabInput } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -218,6 +226,7 @@ import { WebviewViewsExtImpl } from './webview-views'; import { PluginPackage } from '../common'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { FilePermission } from '@theia/filesystem/lib/common/files'; +import { TabsExtImpl } from './tabs'; export function createAPIFactory( rpc: RPCProtocol, @@ -255,6 +264,7 @@ export function createAPIFactory( const timelineExt = rpc.set(MAIN_RPC_CONTEXT.TIMELINE_EXT, new TimelineExtImpl(rpc, commandRegistry)); const themingExt = rpc.set(MAIN_RPC_CONTEXT.THEMING_EXT, new ThemingExtImpl(rpc)); const commentsExt = rpc.set(MAIN_RPC_CONTEXT.COMMENTS_EXT, new CommentsExtImpl(rpc, commandRegistry, documents)); + const tabsExt = rpc.set(MAIN_RPC_CONTEXT.TABS_EXT, new TabsExtImpl(rpc)); const customEditorExt = rpc.set(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT, new CustomEditorsExtImpl(rpc, documents, webviewExt, workspaceExt)); const webviewViewsExt = rpc.set(MAIN_RPC_CONTEXT.WEBVIEW_VIEWS_EXT, new WebviewViewsExtImpl(rpc, webviewExt)); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); @@ -496,7 +506,7 @@ export function createAPIFactory( onDidChangeWindowState(listener, thisArg?, disposables?): theia.Disposable { return windowStateExt.onDidChangeWindowState(listener, thisArg, disposables); }, - createTerminal(nameOrOptions: theia.TerminalOptions | theia.PseudoTerminalOptions | theia.ExtensionTerminalOptions | (string | undefined), + createTerminal(nameOrOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions | theia.ExtensionTerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[] | string): theia.Terminal { return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); @@ -541,6 +551,9 @@ export function createAPIFactory( }, onDidChangeActiveColorTheme(listener, thisArg?, disposables?) { return themingExt.onDidChangeActiveColorTheme(listener, thisArg, disposables); + }, + get tabGroups(): theia.TabGroups { + return tabsExt.tabGroups; } }; @@ -1262,7 +1275,15 @@ export function createAPIFactory( ExtensionKind, InlineCompletionItem, InlineCompletionList, - InlineCompletionTriggerKind + InlineCompletionTriggerKind, + TabInputText: TextTabInput, + TabInputTextDiff: TextDiffTabInput, + TabInputTextMerge: TextMergeTabInput, + TabInputCustom: CustomEditorTabInput, + TabInputNotebook: NotebookEditorTabInput, + TabInputNotebookDiff: NotebookDiffEditorTabInput, + TabInputWebview: WebviewEditorTabInput, + TabInputTerminal: TerminalEditorTabInput, }; }; } diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts new file mode 100644 index 0000000000000..bc357ee1f80fe --- /dev/null +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -0,0 +1,430 @@ +// ***************************************************************************** +// Copyright (C) 2022 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import * as theia from '@theia/plugin'; +import { Emitter } from '@theia/core'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { PLUGIN_RPC_CONTEXT, TabDto, TabGroupDto, TabInputKind, TabModelOperationKind, TabOperation, TabsExt, TabsMain } from '../common/plugin-api-rpc'; +import { + CustomEditorTabInput, + InteractiveWindowInput, + NotebookDiffEditorTabInput, + NotebookEditorTabInput, + TerminalEditorTabInput, + TextDiffTabInput, + TextMergeTabInput, + TextTabInput, + URI, + WebviewEditorTabInput +} from './types-impl'; +import { assertIsDefined } from '../common/types'; +import { diffSets } from '../common/collections'; +import { ViewColumn } from './type-converters'; + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// some code copied and modified from https://github.com/microsoft/vscode/blob/1.71.2/src/vs/workbench/api/common/extHostEditorTabs.ts + +type AnyTabInput = + TextTabInput | + TextDiffTabInput | + CustomEditorTabInput | + NotebookEditorTabInput | + NotebookDiffEditorTabInput | + WebviewEditorTabInput | + TerminalEditorTabInput | + InteractiveWindowInput; + +class TabExt { + private tabApiObject: theia.Tab | undefined; + private tabDto!: TabDto; + private input: AnyTabInput | undefined; + private parentGroup: TabGroupExt; + private readonly activeTabIdGetter: () => string; + + constructor(dto: TabDto, parentGroup: TabGroupExt, activeTabIdGetter: () => string) { + this.activeTabIdGetter = activeTabIdGetter; + this.parentGroup = parentGroup; + this.acceptDtoUpdate(dto); + } + + get apiObject(): theia.Tab { + if (!this.tabApiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; + const obj: theia.Tab = { + get isActive(): boolean { + // We use a getter function here to always ensure at most 1 active tab per group and prevent iteration for being required + return that.tabDto.id === that.activeTabIdGetter(); + }, + get label(): string { + return that.tabDto.label; + }, + get input(): AnyTabInput | undefined { + return that.input; + }, + get isDirty(): boolean { + return that.tabDto.isDirty; + }, + get isPinned(): boolean { + return that.tabDto.isPinned; + }, + get isPreview(): boolean { + return that.tabDto.isPreview; + }, + get group(): theia.TabGroup { + return that.parentGroup.apiObject; + } + }; + this.tabApiObject = Object.freeze(obj); + } + return this.tabApiObject; + } + + get tabId(): string { + return this.tabDto.id; + } + + acceptDtoUpdate(tabDto: TabDto): void { + this.tabDto = tabDto; + this.input = this.initInput(); + } + + private initInput(): AnyTabInput | undefined { + switch (this.tabDto.input.kind) { + case TabInputKind.TextInput: + return new TextTabInput(URI.revive(this.tabDto.input.uri)); + case TabInputKind.TextDiffInput: + return new TextDiffTabInput(URI.revive(this.tabDto.input.original), URI.revive(this.tabDto.input.modified)); + case TabInputKind.TextMergeInput: + return new TextMergeTabInput( + URI.revive(this.tabDto.input.base), + URI.revive(this.tabDto.input.input1), + URI.revive(this.tabDto.input.input2), + URI.revive(this.tabDto.input.result)); + case TabInputKind.CustomEditorInput: + return new CustomEditorTabInput(URI.revive(this.tabDto.input.uri), this.tabDto.input.viewType); + case TabInputKind.WebviewEditorInput: + return new WebviewEditorTabInput(this.tabDto.input.viewType); + case TabInputKind.NotebookInput: + return new NotebookEditorTabInput(URI.revive(this.tabDto.input.uri), this.tabDto.input.notebookType); + case TabInputKind.NotebookDiffInput: + return new NotebookDiffEditorTabInput(URI.revive(this.tabDto.input.original), URI.revive(this.tabDto.input.modified), this.tabDto.input.notebookType); + case TabInputKind.TerminalEditorInput: + return new TerminalEditorTabInput(); + case TabInputKind.InteractiveEditorInput: + return new InteractiveWindowInput(URI.revive(this.tabDto.input.uri), URI.revive(this.tabDto.input.inputBoxUri)); + default: + return undefined; + } + } +} + +class TabGroupExt { + + private tabGroupApiObject: theia.TabGroup | undefined; + private tabGroupDto: TabGroupDto; + private tabsArr: TabExt[] = []; + private activeTabId: string = ''; + private activeGroupIdGetter: () => number | undefined; + + constructor(dto: TabGroupDto, activeGroupIdGetter: () => number | undefined) { + this.tabGroupDto = 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.tabsArr.push(new TabExt(tabDto, this, () => this.getActiveTabId())); + } + } + + get apiObject(): theia.TabGroup { + if (!this.tabGroupApiObject) { + // Don't want to lose reference to parent `this` in the getters + const that = this; + const obj: theia.TabGroup = { + get isActive(): boolean { + // We use a getter function here to always ensure at most 1 active group and prevent iteration for being required + return that.tabGroupDto.groupId === that.activeGroupIdGetter(); + }, + get viewColumn(): theia.ViewColumn { + return ViewColumn.to(that.tabGroupDto.viewColumn); + }, + get activeTab(): theia.Tab | undefined { + return that.tabsArr.find(tab => tab.tabId === that.activeTabId)?.apiObject; + }, + get tabs(): Readonly { + return Object.freeze(that.tabsArr.map(tab => tab.apiObject)); + } + }; + this.tabGroupApiObject = Object.freeze(obj); + } + return this.tabGroupApiObject; + } + + get groupId(): number { + return this.tabGroupDto.groupId; + } + + get tabs(): TabExt[] { + return this.tabsArr; + } + + acceptGroupDtoUpdate(dto: TabGroupDto): void { + this.tabGroupDto = dto; + } + + acceptTabOperation(operation: TabOperation): TabExt { + // In the open case we add the tab to the group + if (operation.kind === TabModelOperationKind.TAB_OPEN) { + const tab = new TabExt(operation.tabDto, this, () => this.getActiveTabId()); + // Insert tab at editor index + this.tabsArr.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.tabsArr.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.tabsArr.splice(operation.oldIndex, 1)[0]; + if (!tab) { + throw new Error(`Tab move updated received for index ${operation.oldIndex} which does not exist`); + } + this.tabsArr.splice(operation.index, 0, tab); + return tab; + } + const _tab = this.tabsArr.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 + getActiveTabId(): string { + return this.activeTabId; + } +} + +export class TabsExtImpl implements TabsExt { + declare readonly _serviceBrand: undefined; + + private readonly proxy: TabsMain; + 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 tabGroupArr: TabGroupExt[] = []; + + private apiObject: theia.TabGroups | undefined; + + constructor(readonly rpc: RPCProtocol) { + this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TABS_MAIN); + } + + get tabGroups(): theia.TabGroups { + if (!this.apiObject) { + const that = this; + const obj: theia.TabGroups = { + // never changes -> simple value + onDidChangeTabGroups: that.onDidChangeTabGroups.event, + onDidChangeTabs: that.onDidChangeTabs.event, + // dynamic -> getters + get all(): Readonly { + return Object.freeze(that.tabGroupArr.map(group => group.apiObject)); + }, + get activeTabGroup(): theia.TabGroup { + const activeTabGroupId = that.activeGroupId; + const activeTabGroup = assertIsDefined(that.tabGroupArr.find(candidate => candidate.groupId === activeTabGroupId)?.apiObject); + return activeTabGroup; + }, + close: async (tabOrTabGroup: theia.Tab | readonly theia.Tab[] | theia.TabGroup | readonly theia.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 theia.TabGroup[], preserveFocus); + } else { + return this._closeTabs(tabsOrTabGroups as theia.Tab[], preserveFocus); + } + }, + // move: async (tab: theia.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: TabGroupDto[]): void { + + const groupIdsBefore = new Set(this.tabGroupArr.map(group => group.groupId)); + const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId)); + const diff = diffSets(groupIdsBefore, groupIdsAfter); + + const closed: theia.TabGroup[] = this.tabGroupArr.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject); + const opened: theia.TabGroup[] = []; + const changed: theia.TabGroup[] = []; + + this.tabGroupArr = tabGroups.map(tabGroup => { + const group = new TabGroupExt(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 })); + } + + $acceptTabGroupUpdate(groupDto: TabGroupDto): void { + const group = this.tabGroupArr.find(tabGroup => tabGroup.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): void { + const group = this.tabGroupArr.find(tabGroup => tabGroup.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: theia.Tab): TabExt | undefined { + for (const group of this.tabGroupArr) { + for (const tab of group.tabs) { + if (tab.apiObject === apiTab) { + return tab; + } + } + } + return; + } + + private _findExtHostTabGroupFromApi(apiTabGroup: theia.TabGroup): TabGroupExt | undefined { + return this.tabGroupArr.find(candidate => candidate.apiObject === apiTabGroup); + } + + private async _closeTabs(tabs: theia.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: theia.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 theia.TabGroup { + const tabGroup = obj as theia.TabGroup; + if (tabGroup.tabs !== undefined) { + return true; + } + return false; +} +// #endregion diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index f87c25527ebcd..820fc4dc81aa0 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -1298,6 +1298,28 @@ export namespace ThemableDecorationAttachmentRenderOptions { } } +export namespace ViewColumn { + export function from(column?: theia.ViewColumn): rpc.EditorGroupColumn { + if (typeof column === 'number' && column >= types.ViewColumn.One) { + return column - 1; // adjust zero index (ViewColumn.ONE => 0) + } + + if (column === types.ViewColumn.Beside) { + return SIDE_GROUP; + } + + return ACTIVE_GROUP; // default is always the active group + } + + export function to(position: rpc.EditorGroupColumn): theia.ViewColumn { + if (typeof position === 'number' && position >= 0) { + return position + 1; // adjust to index (ViewColumn.ONE => 1) + } + + throw new Error('invalid \'EditorGroupColumn\''); + } +} + export function pathOrURIToURI(value: string | URI): URI { if (typeof value === 'undefined') { return value; diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 8ee6d9ad6af7a..7faa2151fe740 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -3240,3 +3240,42 @@ export enum InputBoxValidationSeverity { } // #endregion + +// #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/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 1c52a360727c2..f23afa620a357 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -4554,6 +4554,11 @@ export module '@theia/plugin' { */ export namespace window { + /** + * Represents the grid widget within the main editor area + */ + export const tabGroups: TabGroups; + /** * The currently active terminal or undefined. The active terminal is the one * that currently has focus or most recently had focus. @@ -13114,6 +13119,335 @@ export module '@theia/plugin' { export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable; } + /** + * The tab represents a single text based resource. + */ + export class TabInputText { + /** + * The uri represented by the tab. + * @stubbed + */ + readonly uri: Uri; + /** + * Constructs a text tab input with the given URI. + * @param uri The URI of the tab. + * @stubbed + */ + 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. + * @stubbed + */ + readonly original: Uri; + /** + * The uri of the modified text resource. + * @stubbed + */ + 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. + * @stubbed + */ + constructor(original: Uri, modified: Uri); + } + + /** + * The tab represents a custom editor. + */ + export class TabInputCustom { + /** + * The uri that the tab is representing. + * @stubbed + */ + readonly uri: Uri; + /** + * The type of custom editor. + * @stubbed + */ + readonly viewType: string; + /** + * Constructs a custom editor tab input. + * @param uri The uri of the tab. + * @param viewType The viewtype of the custom editor. + * @stubbed + */ + constructor(uri: Uri, viewType: string); + } + + /** + * The tab represents a webview. + */ + export class TabInputWebview { + /** + * The type of webview. Maps to WebviewPanel's viewType + * @stubbed + */ + readonly viewType: string; + /** + * Constructs a webview tab input with the given view type. + * @param viewType The type of webview. Maps to WebviewPanel's viewType + * @stubbed + */ + constructor(viewType: string); + } + + /** + * The tab represents a notebook. + */ + export class TabInputNotebook { + /** + * The uri that the tab is representing. + * @stubbed + */ + readonly uri: Uri; + /** + * The type of notebook. Maps to NotebookDocuments's notebookType + * @stubbed + */ + 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 NotebookDocuments's notebookType + * @stubbed + */ + constructor(uri: Uri, notebookType: string); + } + + /** + * The tabs represents two notebooks in a diff configuration. + */ + export class TabInputNotebookDiff { + /** + * The uri of the original notebook. + * @stubbed + */ + readonly original: Uri; + /** + * The uri of the modified notebook. + * @stubbed + */ + readonly modified: Uri; + /** + * The type of notebook. Maps to NotebookDocuments's notebookType + * @stubbed + */ + 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 NotebookDocuments's notebookType + * @stubbed + */ + constructor(original: Uri, modified: Uri, notebookType: string); + } + + /** + * The tab represents a terminal in the editor area. + */ + export class TabInputTerminal { + /** + * Constructs a terminal tab input. + * @stubbed + */ + 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. + * @stubbed + */ + readonly label: string; + + /** + * The group which the tab belongs to. + * @stubbed + */ + 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. + * @stubbed + */ + 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. + * @stubbed + */ + readonly isActive: boolean; + + /** + * Whether or not the dirty indicator is present on the tab. + * @stubbed + */ + readonly isDirty: boolean; + + /** + * Whether or not the tab is pinned (pin icon is present). + * @stubbed + */ + readonly isPinned: boolean; + + /** + * Whether or not the tab is in preview mode. + * @stubbed + */ + readonly isPreview: boolean; + } + + /** + * An event describing change to tabs. + */ + export interface TabChangeEvent { + /** + * The tabs that have been opened. + * @stubbed + */ + readonly opened: readonly Tab[]; + /** + * The tabs that have been closed. + * @stubbed + */ + readonly closed: readonly Tab[]; + /** + * Tabs that have changed, e.g have changed + * their {@link Tab.isActive active} state. + * @stubbed + */ + readonly changed: readonly Tab[]; + } + + /** + * An event describing changes to tab groups. + */ + export interface TabGroupChangeEvent { + /** + * Tab groups that have been opened. + * @stubbed + */ + readonly opened: readonly TabGroup[]; + /** + * Tab groups that have been closed. + * @stubbed + */ + readonly closed: readonly TabGroup[]; + /** + * Tab groups that have changed, e.g have changed + * their {@link TabGroup.isActive active} state. + * @stubbed + */ + 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} + * @stubbed + */ + readonly isActive: boolean; + + /** + * The view column of the group. + * @stubbed + */ + 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}. + * @stubbed + */ + readonly activeTab: Tab | undefined; + + /** + * The list of tabs contained within the group. + * This can be empty if the group has no tabs open. + * @stubbed + */ + 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. + * @stubbed + */ + readonly all: readonly TabGroup[]; + + /** + * The currently active group. + * @stubbed + */ + readonly activeTabGroup: TabGroup; + + /** + * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. + * @stubbed + */ + readonly onDidChangeTabGroups: Event; + + /** + * An {@link Event event} which fires when {@link Tab tabs} have changed. + * @stubbed + */ + 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. + * @stubbed + */ + 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. + * @stubbed + */ + close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; + } + /** * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. */ From c176417e06c4a19e9e4c08893430532ec7551dfc Mon Sep 17 00:00:00 2001 From: Jan Bicker Date: Fri, 30 Dec 2022 10:55:43 +0000 Subject: [PATCH 02/15] Implementation of TabsMain --- packages/plugin-ext/package.json | 1 + .../src/main/browser/main-context.ts | 4 ++ .../src/main/browser/tabs/tabs-main.ts | 61 +++++++++++++++++-- packages/plugin-ext/src/plugin/tabs.ts | 3 +- packages/plugin-ext/tsconfig.json | 3 + 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index eac5ee9406f57..f3729df53cb7f 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -11,6 +11,7 @@ "@theia/core": "1.33.0", "@theia/debug": "1.33.0", "@theia/editor": "1.33.0", + "@theia/editor-preview": "1.33.0", "@theia/file-search": "1.33.0", "@theia/filesystem": "1.33.0", "@theia/markers": "1.33.0", diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index eee7fd8cd7335..33e57dfd33b68 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -58,6 +58,7 @@ import { WebviewViewsMainImpl } from './webview-views/webview-views-main'; import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import { UntitledResourceResolver } from '@theia/core/lib/common/resource'; import { ThemeService } from '@theia/core/lib/browser/theming'; +import { TabsMainImp } from './tabs/tabs-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -180,4 +181,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const commentsMain = new CommentsMainImp(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.COMMENTS_MAIN, commentsMain); + + const tabsMain = new TabsMainImp(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.TABS_MAIN, tabsMain); } diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 7d85c44347aec..e0b3f8b75d329 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -15,16 +15,60 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; - -import { TabsMain } from '../../../common/plugin-api-rpc'; +import { ApplicationShell, PINNED_CLASS, Saveable, TabBar, Widget } from '@theia/core/lib/browser'; +import { MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabsExt, TabsMain } from '../../../common/plugin-api-rpc'; import { RPCProtocol } from '../../../common/rpc-protocol'; +import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser/editor-preview-widget'; export class TabsMainImp implements TabsMain { + private readonly proxy: TabsExt; + private tabGroupModel: TabGroupDto[] = []; + + private applicationShell: ApplicationShell; + constructor( rpc: RPCProtocol, container: interfaces.Container - ) {} + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TABS_EXT); + console.log('TabsMainImp constructor called!'); + console.log(this.proxy); + + this.applicationShell = container.get(ApplicationShell); + this.applicationShell.mainPanel.onDidChangeCurrent(() => this.createTabsModel()); + this.createTabsModel(); + } + + private createTabsModel(): void { + const activeWidget = this.applicationShell.activeWidget; + console.log(activeWidget); + this.tabGroupModel = this.applicationShell.mainAreaTabBars.map((tabBar: TabBar, groupId: number) => { + let groupIsActive = false; + const tabs: TabDto[] = tabBar.titles.map(title => { + const widget = title.owner; + let isActive = false; + if (activeWidget?.id === widget.id) { + isActive = true; + groupIsActive = true; + } + return { + id: widget.id, + label: title.label, + input: '', + isActive, + isPinned: title.className.includes(PINNED_CLASS), + isDirty: Saveable.isDirty(widget), + isPreview: widget instanceof EditorPreviewWidget && widget.isPreview + }; + }); + const viewColumn = 1; + return { + groupId, tabs, isActive: groupIsActive, viewColumn + }; + }); + this.proxy.$acceptEditorTabModel(this.tabGroupModel); + } // #region Messages received from Ext Host $moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean): void { @@ -32,7 +76,16 @@ export class TabsMainImp implements TabsMain { } async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { - return false; + for (const tabId of tabIds) { + const widget = this.applicationShell.getWidgetById(tabId); + if (!widget) { + continue; + } + widget.dispose(); + // TODO if this was an active widget/tab we need to activate another widget in the the parent widget/group + // after disposing this. If this was the last one the first widget in the first group should be activated. + } + return true; } async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index bc357ee1f80fe..96a0bbe581a74 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -325,8 +325,9 @@ export class TabsExtImpl implements TabsExt { return group; }); + const groupId = tabGroups.find(group => group.isActive === true)?.groupId; // Set the active tab group id - const activeTabGroupId = assertIsDefined(tabGroups.find(group => group.isActive === true)?.groupId); + const activeTabGroupId = assertIsDefined(groupId); if (activeTabGroupId !== undefined && this.activeGroupId !== activeTabGroupId) { this.activeGroupId = activeTabGroupId; } diff --git a/packages/plugin-ext/tsconfig.json b/packages/plugin-ext/tsconfig.json index ceeb7e9ff55f2..91c36231e1784 100644 --- a/packages/plugin-ext/tsconfig.json +++ b/packages/plugin-ext/tsconfig.json @@ -32,6 +32,9 @@ { "path": "../editor" }, + { + "path": "../editor-preview" + }, { "path": "../file-search" }, From f23426e3331a78c3aa34ca6130100bd68a627eb9 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 23 Jan 2023 11:54:45 +0100 Subject: [PATCH 03/15] basic functionality for input type and events Signed-off-by: Jonah Iden --- .../src/browser/shell/application-shell.ts | 10 + packages/core/src/browser/shell/tab-bars.ts | 14 +- .../plugin-ext/src/common/plugin-api-rpc.ts | 2 +- .../src/main/browser/tabs/tabs-main.ts | 209 +++++++++++++++--- packages/plugin-ext/src/plugin/tabs.ts | 8 - 5 files changed, 205 insertions(+), 38 deletions(-) diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index c4e491b041b92..847c73edd7bee 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -87,6 +87,9 @@ export class DockPanelRenderer implements DockLayout.IRenderer { readonly tabBarClasses: string[] = []; + private readonly _onTabBarCreated = new Emitter>(); + readonly onTabBarCreated = this._onTabBarCreated.event; + constructor( @inject(TabBarRendererFactory) protected readonly tabBarRendererFactory: TabBarRendererFactory, @inject(TabBarToolbarRegistry) protected readonly tabBarToolbarRegistry: TabBarToolbarRegistry, @@ -113,6 +116,7 @@ export class DockPanelRenderer implements DockLayout.IRenderer { tabBar.disposed.connect(() => renderer.dispose()); renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; tabBar.currentChanged.connect(this.onCurrentTabChanged, this); + this._onTabBarCreated.fire(tabBar); return tabBar; } @@ -215,6 +219,11 @@ export class ApplicationShell extends Widget { @inject(TheiaDockPanel.Factory) protected readonly dockPanelFactory: TheiaDockPanel.Factory; + private _mainPanelRenderer: DockPanelRenderer; + get mainPanelRenderer(): DockPanelRenderer { + return this._mainPanelRenderer; + } + /** * Construct a new application shell. */ @@ -490,6 +499,7 @@ export class ApplicationShell extends Widget { const renderer = this.dockPanelRendererFactory(); renderer.tabBarClasses.push(MAIN_BOTTOM_AREA_CLASS); renderer.tabBarClasses.push(MAIN_AREA_CLASS); + this._mainPanelRenderer = renderer; const dockPanel = this.dockPanelFactory({ mode: 'multiple-document', renderer, diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 6d3faade9659c..1db057ad0022d 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -19,7 +19,7 @@ import { TabBar, Title, Widget } from '@phosphor/widgets'; import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom'; import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService, CommandService, nls } from '../../common'; import { ContextMenuRenderer } from '../context-menu-renderer'; -import { Signal, Slot } from '@phosphor/signaling'; +import { ISignal, Signal, Slot } from '@phosphor/signaling'; import { Message, MessageLoop } from '@phosphor/messaging'; import { ArrayExt } from '@phosphor/algorithm'; import { ElementExt } from '@phosphor/domutils'; @@ -547,6 +547,9 @@ export class TabBarRenderer extends TabBar.Renderer { */ export class ScrollableTabBar extends TabBar { + private readonly _tabCreated = new Signal>(this); + readonly tabCreated: ISignal> = this._tabCreated; + protected scrollBar?: PerfectScrollbar; private scrollBarFactory: () => PerfectScrollbar; @@ -567,6 +570,15 @@ export class ScrollableTabBar extends TabBar { this.toDispose.dispose(); } + override insertTab(index: number, value: Title | Title.IOptions): Title { + const title = super.insertTab(index, value); + this._tabCreated.emit({ + index: this.titles.length - 1, + title: title + }); + return title; + } + protected override onAfterAttach(msg: Message): void { if (!this.scrollBar) { this.scrollBar = this.scrollBarFactory(); diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 024a7d9247b74..320db6fed3633 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -2019,7 +2019,7 @@ export interface TabOperation { export interface TabDto { id: string; label: string; - input: any; + input: AnyInputDto; editorId?: string; isActive: boolean; isPinned: boolean; diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index e0b3f8b75d329..1ec4c8047bccb 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -15,60 +15,203 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { ApplicationShell, PINNED_CLASS, Saveable, TabBar, Widget } from '@theia/core/lib/browser'; -import { MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabsExt, TabsMain } from '../../../common/plugin-api-rpc'; +import { ApplicationShell, PINNED_CLASS, Saveable, ScrollableTabBar, TabBar, Title, ViewContainer, Widget } from '@theia/core/lib/browser'; +import { AnyInputDto, MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabInputKind, TabModelOperationKind, TabsExt, TabsMain } from '../../../common/plugin-api-rpc'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser/editor-preview-widget'; +import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; +import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor'; +import { toUriComponents } from '../hierarchy/hierarchy-types-converters'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; -export class TabsMainImp implements TabsMain { +export class TabsMainImp implements TabsMain, Disposable { private readonly proxy: TabsExt; private tabGroupModel: TabGroupDto[] = []; private applicationShell: ApplicationShell; + private toDispose: Disposable[] = []; + constructor( rpc: RPCProtocol, container: interfaces.Container ) { this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TABS_EXT); - console.log('TabsMainImp constructor called!'); - console.log(this.proxy); this.applicationShell = container.get(ApplicationShell); - this.applicationShell.mainPanel.onDidChangeCurrent(() => this.createTabsModel()); this.createTabsModel(); + this.attachListenersToTabBar(this.applicationShell.mainPanel.currentTabBar); + + this.toDispose.push( + this.applicationShell.mainPanelRenderer.onTabBarCreated(tabBar => { + this.attachListenersToTabBar(tabBar); + this.onTabGroupCreated(tabBar); + }) + ); + } + + protected createTabsModel(): void { + this.tabGroupModel = this.applicationShell.mainAreaTabBars.map(this.createTabGroupDto, this); + this.proxy.$acceptEditorTabModel(this.tabGroupModel); } - private createTabsModel(): void { + protected createTabDto(tabTitle: Title): TabDto { const activeWidget = this.applicationShell.activeWidget; - console.log(activeWidget); - this.tabGroupModel = this.applicationShell.mainAreaTabBars.map((tabBar: TabBar, groupId: number) => { - let groupIsActive = false; - const tabs: TabDto[] = tabBar.titles.map(title => { - const widget = title.owner; - let isActive = false; - if (activeWidget?.id === widget.id) { - isActive = true; - groupIsActive = true; - } - return { - id: widget.id, - label: title.label, - input: '', - isActive, - isPinned: title.className.includes(PINNED_CLASS), - isDirty: Saveable.isDirty(widget), - isPreview: widget instanceof EditorPreviewWidget && widget.isPreview - }; + const widget = tabTitle.owner; + let isActive = false; + if (activeWidget?.id === widget.id) { + isActive = true; + } + return { + id: widget.id, + label: tabTitle.label, + input: this.evaluateTabDtoInput(widget), + isActive, + isPinned: tabTitle.className.includes(PINNED_CLASS), + isDirty: Saveable.isDirty(widget), + isPreview: widget instanceof EditorPreviewWidget && widget.isPreview + }; + } + + protected createTabGroupDto(tabBar: TabBar, groupId: number): TabGroupDto { + let groupIsActive = false; + const tabs: TabDto[] = tabBar.titles.map(title => { + const tabDto = this.createTabDto(title); + if (tabDto.isActive) { + groupIsActive = true; + } + return tabDto; }); const viewColumn = 1; return { groupId, tabs, isActive: groupIsActive, viewColumn }; + } + + protected attachListenersToTabBar(tabBar: TabBar | undefined): void { + if (!tabBar) { + return; + } + this.connectToSignal((tabBar as ScrollableTabBar)?.tabCreated, this.onTabCreated); + this.connectToSignal(tabBar.tabActivateRequested, this.onTabActivated); + this.connectToSignal(tabBar.tabCloseRequested, this.onTabClosed); + this.connectToSignal(tabBar.tabMoved, this.onTabMoved); + this.connectToSignal(tabBar.disposed, this.onTabGroupClosed); + } + + protected evaluateTabDtoInput(widget: Widget): AnyInputDto { + if (widget instanceof EditorPreviewWidget) { + if (widget.editor instanceof MonacoDiffEditor) { + return { + kind: TabInputKind.TextDiffInput, + original: toUriComponents(widget.editor.originalModel.uri), + modified: toUriComponents(widget.editor.modifiedModel.uri) + }; + } else { + return { + kind: TabInputKind.TextInput, + uri: toUriComponents(widget.editor.uri.toString()) + }; + } + // TODO notebook support when implemented + } else if (widget instanceof ViewContainer) { + return { + kind: TabInputKind.WebviewEditorInput, + viewType: widget.id + }; + } else if (widget instanceof TerminalWidget) { + return { + kind: TabInputKind.TerminalEditorInput + }; + } + + return { kind: TabInputKind.UnknownInput }; + } + + protected connectToSignal(signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { + signal.connect(listener, this); + this.toDispose.push(Disposable.create(() => signal.disconnect(listener))); + } + + // #region event listeners + private onTabCreated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { + console.log('onTabCreated'); + this.connectToSignal(args.title.changed, title => this.onTabTitleChanged(title, args, tabBar)); + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 0, this.createTabDto(args.title)); + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_OPEN, + index: args.index, + tabDto: this.createTabDto(args.title), + groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + }); + } + + private onTabTitleChanged(title: Title, args: TabBar.ITabActivateRequestedArgs, tabBar: TabBar): void { + const oldTabDto = this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs[args.index]; + const newTabDto = this.createTabDto(title); + if (oldTabDto !== newTabDto) { + console.log('onTabTitleChanged'); + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1, newTabDto); + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + index: args.index, + tabDto: newTabDto, + groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + }); + } + } + + private onTabActivated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { + console.log('onTabActivated'); + const tabDto = this.createTabDto(args.title) + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1, tabDto); + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_UPDATE, + index: args.index, + tabDto: tabDto, + groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + }); + } + + private onTabClosed(tabBar: TabBar, args: TabBar.ITabCloseRequestedArgs): void { + console.log('onTabClosed'); + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1); + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_CLOSE, + index: args.index, + tabDto: this.createTabDto(args.title), + groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + }); + } + + private onTabMoved(tabBar: TabBar, args: TabBar.ITabMovedArgs): void { + console.log('onTabMoved'); + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.fromIndex, 1); + this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.toIndex, 1, this.createTabDto(args.title)); + this.proxy.$acceptTabOperation({ + kind: TabModelOperationKind.TAB_MOVE, + index: args.toIndex, + tabDto: this.createTabDto(args.title), + groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar), + oldIndex: args.fromIndex + }); + } + + private onTabGroupCreated(tabBar: TabBar): void { + setTimeout(() => { + console.log('update groups'); + this.createTabsModel(); + }); + } + + private onTabGroupClosed(tabBar: TabBar): void { + setTimeout(() => { + console.log('update groups'); + this.createTabsModel(); }); - this.proxy.$acceptEditorTabModel(this.tabGroupModel); } + // #endregion // #region Messages received from Ext Host $moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean): void { @@ -89,7 +232,17 @@ export class TabsMainImp implements TabsMain { } async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { - return false; + for (const groupId of groupIds) { + const group = this.tabGroupModel.find(tabGroup => tabGroup.groupId === groupId); + if (group) { + this.$closeTab(group.tabs.map(tab => tab.id), true); + } + } + return true; } // #endregion + + dispose(): void { + this.toDispose.forEach(disposable => disposable.dispose()); + } } diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index 96a0bbe581a74..35db732b024cc 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -291,14 +291,6 @@ export class TabsExtImpl implements TabsExt { return this._closeTabs(tabsOrTabGroups as theia.Tab[], preserveFocus); } }, - // move: async (tab: theia.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); } From 866605b646609ec24d849fb1177acc7bfdb35015 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 23 Jan 2023 15:37:00 +0100 Subject: [PATCH 04/15] smaller fixes and improvements Signed-off-by: Jonah Iden --- .../src/main/browser/tabs/tabs-main.ts | 69 +++++++++++++------ 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 1ec4c8047bccb..55ab461f7390d 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -41,7 +41,11 @@ export class TabsMainImp implements TabsMain, Disposable { this.applicationShell = container.get(ApplicationShell); this.createTabsModel(); - this.attachListenersToTabBar(this.applicationShell.mainPanel.currentTabBar); + + const tabBars = this.applicationShell.mainPanel.tabBars(); + for (let tabBar; tabBar = tabBars.next();) { + this.attachListenersToTabBar(tabBar); + } this.toDispose.push( this.applicationShell.mainPanelRenderer.onTabBarCreated(tabBar => { @@ -93,6 +97,10 @@ export class TabsMainImp implements TabsMain, Disposable { if (!tabBar) { return; } + tabBar.titles.forEach((title, index) => { + this.connectToSignal(title.changed, () => this.onTabTitleChanged(title, { title, index }, tabBar)); + }); + this.connectToSignal((tabBar as ScrollableTabBar)?.tabCreated, this.onTabCreated); this.connectToSignal(tabBar.tabActivateRequested, this.onTabActivated); this.connectToSignal(tabBar.tabCloseRequested, this.onTabClosed); @@ -134,82 +142,99 @@ export class TabsMainImp implements TabsMain, Disposable { this.toDispose.push(Disposable.create(() => signal.disconnect(listener))); } + protected updateActiveGroup(): void { + const activeWidgetId = this.applicationShell.activeWidget?.id; + if (activeWidgetId) { + for (const tabGroup of this.tabGroupModel) { + for (const tab of tabGroup.tabs) { + tab.isActive = activeWidgetId === tab.id; + tabGroup.isActive = activeWidgetId === tab.id; + } + } + } + } + // #region event listeners private onTabCreated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { console.log('onTabCreated'); + const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); this.connectToSignal(args.title.changed, title => this.onTabTitleChanged(title, args, tabBar)); - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 0, this.createTabDto(args.title)); + this.tabGroupModel[groupId].tabs.splice(args.index, 0, this.createTabDto(args.title)); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_OPEN, index: args.index, tabDto: this.createTabDto(args.title), - groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + groupId }); } private onTabTitleChanged(title: Title, args: TabBar.ITabActivateRequestedArgs, tabBar: TabBar): void { - const oldTabDto = this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs[args.index]; + const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); + const oldTabDto = this.tabGroupModel[groupId].tabs[args.index]; const newTabDto = this.createTabDto(title); if (oldTabDto !== newTabDto) { console.log('onTabTitleChanged'); - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1, newTabDto); + this.tabGroupModel[groupId].tabs.splice(args.index, 1, newTabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, index: args.index, tabDto: newTabDto, - groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + groupId }); } } private onTabActivated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { console.log('onTabActivated'); - const tabDto = this.createTabDto(args.title) - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1, tabDto); + const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); + const tabDto = this.createTabDto(args.title); + this.tabGroupModel[groupId].tabs.splice(args.index, 1, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, index: args.index, tabDto: tabDto, - groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + groupId }); } private onTabClosed(tabBar: TabBar, args: TabBar.ITabCloseRequestedArgs): void { console.log('onTabClosed'); - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.index, 1); + const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); + this.tabGroupModel[groupId].tabs.splice(args.index, 1); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_CLOSE, index: args.index, tabDto: this.createTabDto(args.title), - groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar) + groupId }); } private onTabMoved(tabBar: TabBar, args: TabBar.ITabMovedArgs): void { console.log('onTabMoved'); - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.fromIndex, 1); - this.tabGroupModel[this.applicationShell.mainAreaTabBars.indexOf(tabBar)].tabs.splice(args.toIndex, 1, this.createTabDto(args.title)); + const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); + this.tabGroupModel[groupId].tabs.splice(args.fromIndex, 1); + this.tabGroupModel[groupId].tabs.splice(args.toIndex, 1, this.createTabDto(args.title)); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_MOVE, index: args.toIndex, tabDto: this.createTabDto(args.title), - groupId: this.applicationShell.mainAreaTabBars.indexOf(tabBar), + groupId, oldIndex: args.fromIndex }); } private onTabGroupCreated(tabBar: TabBar): void { - setTimeout(() => { - console.log('update groups'); - this.createTabsModel(); - }); + console.log('update groups'); + this.tabGroupModel.push(this.createTabGroupDto(tabBar, this.tabGroupModel.length)); + this.updateActiveGroup(); + this.proxy.$acceptEditorTabModel(this.tabGroupModel); } private onTabGroupClosed(tabBar: TabBar): void { - setTimeout(() => { - console.log('update groups'); - this.createTabsModel(); - }); + console.log('update groups'); + this.tabGroupModel.splice(this.applicationShell.mainAreaTabBars.indexOf(tabBar), 1); + this.updateActiveGroup(); + this.proxy.$acceptEditorTabModel(this.tabGroupModel); } // #endregion From 06894ae8b121fdc9f39a6be0736d4957858f0453 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Wed, 25 Jan 2023 16:11:05 +0100 Subject: [PATCH 05/15] correctly working change events Signed-off-by: Jonah Iden --- packages/core/src/browser/shell/tab-bars.ts | 15 +- .../src/main/browser/tabs/tabs-main.ts | 196 ++++++++++-------- packages/plugin-ext/src/plugin/tabs.ts | 11 +- 3 files changed, 120 insertions(+), 102 deletions(-) diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 1db057ad0022d..bf4ab70072b4d 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -19,7 +19,7 @@ import { TabBar, Title, Widget } from '@phosphor/widgets'; import { VirtualElement, h, VirtualDOM, ElementInlineStyle } from '@phosphor/virtualdom'; import { Disposable, DisposableCollection, MenuPath, notEmpty, SelectionService, CommandService, nls } from '../../common'; import { ContextMenuRenderer } from '../context-menu-renderer'; -import { ISignal, Signal, Slot } from '@phosphor/signaling'; +import { Signal, Slot } from '@phosphor/signaling'; import { Message, MessageLoop } from '@phosphor/messaging'; import { ArrayExt } from '@phosphor/algorithm'; import { ElementExt } from '@phosphor/domutils'; @@ -546,10 +546,6 @@ export class TabBarRenderer extends TabBar.Renderer { * A specialized tab bar for the main and bottom areas. */ export class ScrollableTabBar extends TabBar { - - private readonly _tabCreated = new Signal>(this); - readonly tabCreated: ISignal> = this._tabCreated; - protected scrollBar?: PerfectScrollbar; private scrollBarFactory: () => PerfectScrollbar; @@ -570,15 +566,6 @@ export class ScrollableTabBar extends TabBar { this.toDispose.dispose(); } - override insertTab(index: number, value: Title | Title.IOptions): Title { - const title = super.insertTab(index, value); - this._tabCreated.emit({ - index: this.titles.length - 1, - title: title - }); - return title; - } - protected override onAfterAttach(msg: Message): void { if (!this.scrollBar) { this.scrollBar = this.scrollBarFactory(); diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 55ab461f7390d..56cc4360fb7a8 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -15,7 +15,7 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { ApplicationShell, PINNED_CLASS, Saveable, ScrollableTabBar, TabBar, Title, ViewContainer, Widget } from '@theia/core/lib/browser'; +import { ApplicationShell, PINNED_CLASS, Saveable, TabBar, Title, ViewContainer, Widget } from '@theia/core/lib/browser'; import { AnyInputDto, MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabInputKind, TabModelOperationKind, TabsExt, TabsMain } from '../../../common/plugin-api-rpc'; import { RPCProtocol } from '../../../common/rpc-protocol'; import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser/editor-preview-widget'; @@ -27,11 +27,16 @@ import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget export class TabsMainImp implements TabsMain, Disposable { private readonly proxy: TabsExt; - private tabGroupModel: TabGroupDto[] = []; + private tabGroupModel = new Map, TabGroupDto>(); private applicationShell: ApplicationShell; - private toDispose: Disposable[] = []; + private disposableTabBarListeners: Disposable[] = []; + private toDisposeOnDestroy: Disposable[] = []; + + private GroupIdCounter = 0; + + private tabGroupChanged: boolean = false; constructor( rpc: RPCProtocol, @@ -47,28 +52,61 @@ export class TabsMainImp implements TabsMain, Disposable { this.attachListenersToTabBar(tabBar); } - this.toDispose.push( + this.toDisposeOnDestroy.push( this.applicationShell.mainPanelRenderer.onTabBarCreated(tabBar => { this.attachListenersToTabBar(tabBar); this.onTabGroupCreated(tabBar); }) ); + + this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetAdded, (sender, widget) => { + if (this.tabGroupChanged || this.tabGroupModel.size === 0) { + this.tabGroupChanged = false; + this.createTabsModel(); + } else { + const tabBar = this.applicationShell.mainPanel.findTabBar(widget.title)!; + this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); + } + }); + + this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (sender, widget) => { + if (!(widget instanceof TabBar)) { + if (this.tabGroupChanged) { + this.tabGroupChanged = false; + this.createTabsModel(); + } else { + const tabBar = this.applicationShell.mainPanel.findTabBar(widget.title)!; + this.onTabClosed(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); + } + } + }); } protected createTabsModel(): void { - this.tabGroupModel = this.applicationShell.mainAreaTabBars.map(this.createTabGroupDto, this); - this.proxy.$acceptEditorTabModel(this.tabGroupModel); + const newTabGroupModel = new Map, TabGroupDto>(); + this.disposableTabBarListeners.forEach(disposable => disposable.dispose()); + this.applicationShell.mainAreaTabBars.forEach(tabBar => { + this.attachListenersToTabBar(tabBar); + + const groupDto = this.createTabGroupDto(tabBar); + newTabGroupModel.set(tabBar, groupDto); + }); + if (newTabGroupModel.size > 0 && !Array.from(newTabGroupModel.values()).some(groupDto => groupDto.isActive)) { + newTabGroupModel.values().next().value.isActive = true; // allways needs one active group, so if there is none we just take the first one + } + this.tabGroupModel = newTabGroupModel; + this.proxy.$acceptEditorTabModel(this.transformModelToArray()); } - protected createTabDto(tabTitle: Title): TabDto { - const activeWidget = this.applicationShell.activeWidget; + protected createTabDto(tabTitle: Title, groupId: number): TabDto { + const activeTitle = this.applicationShell.mainPanel.currentTitle; const widget = tabTitle.owner; let isActive = false; - if (activeWidget?.id === widget.id) { + if (activeTitle === tabTitle) { isActive = true; } return { - id: widget.id, + id: this.generateTabId(tabTitle, groupId), label: tabTitle.label, input: this.evaluateTabDtoInput(widget), isActive, @@ -78,10 +116,16 @@ export class TabsMainImp implements TabsMain, Disposable { }; } - protected createTabGroupDto(tabBar: TabBar, groupId: number): TabGroupDto { + protected generateTabId(tab: Title, groupId: number): string { + return `${groupId}~${tab.owner.id}`; + } + + protected createTabGroupDto(tabBar: TabBar): TabGroupDto { let groupIsActive = false; + const oldDto = this.tabGroupModel.get(tabBar); + const groupId = oldDto ? oldDto.groupId : this.GroupIdCounter++; const tabs: TabDto[] = tabBar.titles.map(title => { - const tabDto = this.createTabDto(title); + const tabDto = this.createTabDto(title, groupId); if (tabDto.isActive) { groupIsActive = true; } @@ -97,15 +141,12 @@ export class TabsMainImp implements TabsMain, Disposable { if (!tabBar) { return; } - tabBar.titles.forEach((title, index) => { - this.connectToSignal(title.changed, () => this.onTabTitleChanged(title, { title, index }, tabBar)); + tabBar.titles.forEach(title => { + this.connectToSignal(this.disposableTabBarListeners, title.changed, this.onTabTitleChanged); }); - this.connectToSignal((tabBar as ScrollableTabBar)?.tabCreated, this.onTabCreated); - this.connectToSignal(tabBar.tabActivateRequested, this.onTabActivated); - this.connectToSignal(tabBar.tabCloseRequested, this.onTabClosed); - this.connectToSignal(tabBar.tabMoved, this.onTabMoved); - this.connectToSignal(tabBar.disposed, this.onTabGroupClosed); + this.connectToSignal(this.disposableTabBarListeners, tabBar.tabMoved, this.onTabMoved); + this.connectToSignal(this.disposableTabBarListeners, tabBar.disposed, this.onTabGroupClosed); } protected evaluateTabDtoInput(widget: Widget): AnyInputDto { @@ -137,104 +178,92 @@ export class TabsMainImp implements TabsMain, Disposable { return { kind: TabInputKind.UnknownInput }; } - protected connectToSignal(signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { + protected connectToSignal(disposableList: Disposable[], signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { signal.connect(listener, this); - this.toDispose.push(Disposable.create(() => signal.disconnect(listener))); + disposableList.push(Disposable.create(() => signal.disconnect(listener))); } - protected updateActiveGroup(): void { - const activeWidgetId = this.applicationShell.activeWidget?.id; - if (activeWidgetId) { - for (const tabGroup of this.tabGroupModel) { - for (const tab of tabGroup.tabs) { - tab.isActive = activeWidgetId === tab.id; - tabGroup.isActive = activeWidgetId === tab.id; - } - } - } + protected transformModelToArray(): TabGroupDto[] { + return Array.from(this.tabGroupModel.values()); + } + + protected tabDtosEqual(a: TabDto, b: TabDto): boolean { + return a.isActive === b.isActive && + a.isDirty === b.isDirty && + a.isPinned === b.isPinned && + a.isPreview === b.isPreview && + a.id === b.id; } // #region event listeners private onTabCreated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { - console.log('onTabCreated'); - const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); - this.connectToSignal(args.title.changed, title => this.onTabTitleChanged(title, args, tabBar)); - this.tabGroupModel[groupId].tabs.splice(args.index, 0, this.createTabDto(args.title)); + const group = this.tabGroupModel.get(tabBar)!; + this.connectToSignal(this.disposableTabBarListeners, args.title.changed, this.onTabTitleChanged); + const tabDto = this.createTabDto(args.title, group.groupId); + group.tabs.splice(args.index, 0, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_OPEN, index: args.index, - tabDto: this.createTabDto(args.title), - groupId + tabDto, + groupId: group.groupId }); } - private onTabTitleChanged(title: Title, args: TabBar.ITabActivateRequestedArgs, tabBar: TabBar): void { - const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); - const oldTabDto = this.tabGroupModel[groupId].tabs[args.index]; - const newTabDto = this.createTabDto(title); - if (oldTabDto !== newTabDto) { - console.log('onTabTitleChanged'); - this.tabGroupModel[groupId].tabs.splice(args.index, 1, newTabDto); + private onTabTitleChanged(title: Title): void { + const tabBar = this.applicationShell.mainPanel.findTabBar(title)!; + const tabIndex = tabBar?.titles.indexOf(title); + const group = this.tabGroupModel.get(tabBar); + if (!group) { + return; + } + const oldTabDto = group.tabs[tabIndex]; + const newTabDto = this.createTabDto(title, group.groupId); + if (!this.tabDtosEqual(oldTabDto, newTabDto)) { + if (!oldTabDto.isActive && newTabDto.isActive && !group.isActive) { + group.isActive = true; + this.proxy.$acceptTabGroupUpdate(group); + } + group.tabs.splice(tabIndex, 1, newTabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, - index: args.index, + index: tabIndex, tabDto: newTabDto, - groupId + groupId: group.groupId }); } } - private onTabActivated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { - console.log('onTabActivated'); - const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); - const tabDto = this.createTabDto(args.title); - this.tabGroupModel[groupId].tabs.splice(args.index, 1, tabDto); - this.proxy.$acceptTabOperation({ - kind: TabModelOperationKind.TAB_UPDATE, - index: args.index, - tabDto: tabDto, - groupId - }); - } - private onTabClosed(tabBar: TabBar, args: TabBar.ITabCloseRequestedArgs): void { - console.log('onTabClosed'); - const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); - this.tabGroupModel[groupId].tabs.splice(args.index, 1); + const group = this.tabGroupModel.get(tabBar)!; + group.tabs.splice(args.index, 1); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_CLOSE, index: args.index, - tabDto: this.createTabDto(args.title), - groupId + tabDto: this.createTabDto(args.title, group.groupId), + groupId: group.groupId }); } private onTabMoved(tabBar: TabBar, args: TabBar.ITabMovedArgs): void { - console.log('onTabMoved'); - const groupId = this.applicationShell.mainAreaTabBars.indexOf(tabBar); - this.tabGroupModel[groupId].tabs.splice(args.fromIndex, 1); - this.tabGroupModel[groupId].tabs.splice(args.toIndex, 1, this.createTabDto(args.title)); + const group = this.tabGroupModel.get(tabBar)!; + const tabDto = this.createTabDto(args.title, group.groupId); + group.tabs.splice(args.fromIndex, 1); + group.tabs.splice(args.toIndex, 1, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_MOVE, index: args.toIndex, - tabDto: this.createTabDto(args.title), - groupId, + tabDto, + groupId: group.groupId, oldIndex: args.fromIndex }); } private onTabGroupCreated(tabBar: TabBar): void { - console.log('update groups'); - this.tabGroupModel.push(this.createTabGroupDto(tabBar, this.tabGroupModel.length)); - this.updateActiveGroup(); - this.proxy.$acceptEditorTabModel(this.tabGroupModel); + this.tabGroupChanged = true; } private onTabGroupClosed(tabBar: TabBar): void { - console.log('update groups'); - this.tabGroupModel.splice(this.applicationShell.mainAreaTabBars.indexOf(tabBar), 1); - this.updateActiveGroup(); - this.proxy.$acceptEditorTabModel(this.tabGroupModel); + this.tabGroupChanged = true; } // #endregion @@ -245,7 +274,7 @@ export class TabsMainImp implements TabsMain, Disposable { async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { for (const tabId of tabIds) { - const widget = this.applicationShell.getWidgetById(tabId); + const widget = this.applicationShell.getWidgetById(tabId.substring(tabId.indexOf('~') + 1)); if (!widget) { continue; } @@ -258,9 +287,9 @@ export class TabsMainImp implements TabsMain, Disposable { async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { for (const groupId of groupIds) { - const group = this.tabGroupModel.find(tabGroup => tabGroup.groupId === groupId); - if (group) { - this.$closeTab(group.tabs.map(tab => tab.id), true); + const tabBar = Array.from(this.tabGroupModel.entries()).find(([, tabGroup]) => tabGroup.groupId === groupId)?.[0]; + if (tabBar) { + tabBar.titles.forEach(title => title.owner.dispose()); } } return true; @@ -268,6 +297,7 @@ export class TabsMainImp implements TabsMain, Disposable { // #endregion dispose(): void { - this.toDispose.forEach(disposable => disposable.dispose()); + this.toDisposeOnDestroy.forEach(disposable => disposable.dispose()); + this.disposableTabBarListeners.forEach(disposable => disposable.dispose()); } } diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index 35db732b024cc..e570a078b5e6b 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -298,7 +298,6 @@ export class TabsExtImpl implements TabsExt { } $acceptEditorTabModel(tabGroups: TabGroupDto[]): void { - const groupIdsBefore = new Set(this.tabGroupArr.map(group => group.groupId)); const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId)); const diff = diffSets(groupIdsBefore, groupIdsAfter); @@ -318,10 +317,12 @@ export class TabsExtImpl implements TabsExt { }); const groupId = tabGroups.find(group => group.isActive === true)?.groupId; - // Set the active tab group id - const activeTabGroupId = assertIsDefined(groupId); - if (activeTabGroupId !== undefined && this.activeGroupId !== activeTabGroupId) { - this.activeGroupId = activeTabGroupId; + // Set the active tab group id. skip if no tabgroups are open + if (tabGroups.length > 0) { + const activeTabGroupId = assertIsDefined(groupId); + if (activeTabGroupId !== undefined && this.activeGroupId !== activeTabGroupId) { + this.activeGroupId = activeTabGroupId; + } } this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); } From c33bb236c77c5bf651c0bb901ab0d0250f0a3b9f Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Thu, 26 Jan 2023 10:32:06 +0100 Subject: [PATCH 06/15] fixes and optimzation through tabInfo lookup Signed-off-by: Jonah Iden --- .../src/main/browser/tabs/tabs-main.ts | 77 +++++++++++-------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 56cc4360fb7a8..95976834df095 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -24,10 +24,17 @@ import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor'; import { toUriComponents } from '../hierarchy/hierarchy-types-converters'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +interface TabInfo { + tab: TabDto; + tabIndex: number; + group: TabGroupDto; +} + export class TabsMainImp implements TabsMain, Disposable { private readonly proxy: TabsExt; private tabGroupModel = new Map, TabGroupDto>(); + private tabInfoLookup = new Map, TabInfo>(); private applicationShell: ApplicationShell; @@ -35,6 +42,7 @@ export class TabsMainImp implements TabsMain, Disposable { private toDisposeOnDestroy: Disposable[] = []; private GroupIdCounter = 0; + private currentActiveGroup: TabGroupDto; private tabGroupChanged: boolean = false; @@ -59,24 +67,24 @@ export class TabsMainImp implements TabsMain, Disposable { }) ); - this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetAdded, (sender, widget) => { + this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetAdded, (mainPanel, widget) => { if (this.tabGroupChanged || this.tabGroupModel.size === 0) { this.tabGroupChanged = false; this.createTabsModel(); } else { - const tabBar = this.applicationShell.mainPanel.findTabBar(widget.title)!; + const tabBar = mainPanel.findTabBar(widget.title)!; this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); } }); - this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (sender, widget) => { + this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (mainPanel, widget) => { if (!(widget instanceof TabBar)) { if (this.tabGroupChanged) { this.tabGroupChanged = false; this.createTabsModel(); } else { - const tabBar = this.applicationShell.mainPanel.findTabBar(widget.title)!; - this.onTabClosed(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); + const tabInfo = this.tabInfoLookup.get(widget.title)!; + this.onTabClosed(tabInfo, widget.title); } } }); @@ -84,11 +92,16 @@ export class TabsMainImp implements TabsMain, Disposable { protected createTabsModel(): void { const newTabGroupModel = new Map, TabGroupDto>(); + this.tabInfoLookup.clear(); this.disposableTabBarListeners.forEach(disposable => disposable.dispose()); this.applicationShell.mainAreaTabBars.forEach(tabBar => { this.attachListenersToTabBar(tabBar); const groupDto = this.createTabGroupDto(tabBar); + if (groupDto.isActive) { + this.currentActiveGroup = groupDto; + } + tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index })); newTabGroupModel.set(tabBar, groupDto); }); if (newTabGroupModel.size > 0 && !Array.from(newTabGroupModel.values()).some(groupDto => groupDto.isActive)) { @@ -116,8 +129,8 @@ export class TabsMainImp implements TabsMain, Disposable { }; } - protected generateTabId(tab: Title, groupId: number): string { - return `${groupId}~${tab.owner.id}`; + protected generateTabId(tabTitle: Title, groupId: number): string { + return `${groupId}~${tabTitle.owner.id}`; } protected createTabGroupDto(tabBar: TabBar): TabGroupDto { @@ -200,6 +213,7 @@ export class TabsMainImp implements TabsMain, Disposable { const group = this.tabGroupModel.get(tabBar)!; this.connectToSignal(this.disposableTabBarListeners, args.title.changed, this.onTabTitleChanged); const tabDto = this.createTabDto(args.title, group.groupId); + this.tabInfoLookup.set(args.title, { group, tab: tabDto, tabIndex: args.index }); group.tabs.splice(args.index, 0, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_OPEN, @@ -210,50 +224,51 @@ export class TabsMainImp implements TabsMain, Disposable { } private onTabTitleChanged(title: Title): void { - const tabBar = this.applicationShell.mainPanel.findTabBar(title)!; - const tabIndex = tabBar?.titles.indexOf(title); - const group = this.tabGroupModel.get(tabBar); - if (!group) { + const tabInfo = this.tabInfoLookup.get(title); + if (!tabInfo) { return; } - const oldTabDto = group.tabs[tabIndex]; - const newTabDto = this.createTabDto(title, group.groupId); + const oldTabDto = tabInfo.tab; + const newTabDto = this.createTabDto(title, tabInfo.group.groupId); + if (newTabDto.isActive && !tabInfo.group.isActive) { + tabInfo.group.isActive = true; + this.currentActiveGroup.isActive = false; + this.currentActiveGroup = tabInfo.group; + this.proxy.$acceptTabGroupUpdate(tabInfo.group); + } if (!this.tabDtosEqual(oldTabDto, newTabDto)) { - if (!oldTabDto.isActive && newTabDto.isActive && !group.isActive) { - group.isActive = true; - this.proxy.$acceptTabGroupUpdate(group); - } - group.tabs.splice(tabIndex, 1, newTabDto); + tabInfo.group.tabs.splice(tabInfo.tabIndex, 1, newTabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, - index: tabIndex, + index: tabInfo.tabIndex, tabDto: newTabDto, - groupId: group.groupId + groupId: tabInfo.group.groupId }); } } - private onTabClosed(tabBar: TabBar, args: TabBar.ITabCloseRequestedArgs): void { - const group = this.tabGroupModel.get(tabBar)!; - group.tabs.splice(args.index, 1); + private onTabClosed(tabInfo: TabInfo, title: Title): void { + tabInfo.group.tabs.splice(tabInfo.tabIndex, 1); + this.tabInfoLookup.delete(title); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_CLOSE, - index: args.index, - tabDto: this.createTabDto(args.title, group.groupId), - groupId: group.groupId + index: tabInfo.tabIndex, + tabDto: this.createTabDto(title, tabInfo.group.groupId), + groupId: tabInfo.group.groupId }); } private onTabMoved(tabBar: TabBar, args: TabBar.ITabMovedArgs): void { - const group = this.tabGroupModel.get(tabBar)!; - const tabDto = this.createTabDto(args.title, group.groupId); - group.tabs.splice(args.fromIndex, 1); - group.tabs.splice(args.toIndex, 1, tabDto); + const tabInfo = this.tabInfoLookup.get(args.title)!; + tabInfo.tabIndex = args.toIndex; + const tabDto = this.createTabDto(args.title, tabInfo.group.groupId); + tabInfo.group.tabs.splice(args.fromIndex, 1); + tabInfo.group.tabs.splice(args.toIndex, 1, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_MOVE, index: args.toIndex, tabDto, - groupId: group.groupId, + groupId: tabInfo.group.groupId, oldIndex: args.fromIndex }); } From 0c57193169aa573c502fd7d5a34c591bf2ccae53 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Thu, 26 Jan 2023 11:55:30 +0100 Subject: [PATCH 07/15] fixes, rebuild on broken model, formatting Signed-off-by: Jonah Iden --- packages/core/src/browser/shell/tab-bars.ts | 1 + .../src/main/browser/tabs/tabs-main.ts | 40 +++++++++++-------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index bf4ab70072b4d..6d3faade9659c 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -546,6 +546,7 @@ export class TabBarRenderer extends TabBar.Renderer { * A specialized tab bar for the main and bottom areas. */ export class ScrollableTabBar extends TabBar { + protected scrollBar?: PerfectScrollbar; private scrollBarFactory: () => PerfectScrollbar; diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 95976834df095..b0b3b84750179 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -83,7 +83,7 @@ export class TabsMainImp implements TabsMain, Disposable { this.tabGroupChanged = false; this.createTabsModel(); } else { - const tabInfo = this.tabInfoLookup.get(widget.title)!; + const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, widget.title)!; this.onTabClosed(tabInfo, widget.title); } } @@ -108,7 +108,7 @@ export class TabsMainImp implements TabsMain, Disposable { newTabGroupModel.values().next().value.isActive = true; // allways needs one active group, so if there is none we just take the first one } this.tabGroupModel = newTabGroupModel; - this.proxy.$acceptEditorTabModel(this.transformModelToArray()); + this.proxy.$acceptEditorTabModel(Array.from(this.tabGroupModel.values())); } protected createTabDto(tabTitle: Title, groupId: number): TabDto { @@ -143,11 +143,11 @@ export class TabsMainImp implements TabsMain, Disposable { groupIsActive = true; } return tabDto; - }); - const viewColumn = 1; - return { - groupId, tabs, isActive: groupIsActive, viewColumn - }; + }); + const viewColumn = 1; + return { + groupId, tabs, isActive: groupIsActive, viewColumn + }; } protected attachListenersToTabBar(tabBar: TabBar | undefined): void { @@ -196,10 +196,6 @@ export class TabsMainImp implements TabsMain, Disposable { disposableList.push(Disposable.create(() => signal.disconnect(listener))); } - protected transformModelToArray(): TabGroupDto[] { - return Array.from(this.tabGroupModel.values()); - } - protected tabDtosEqual(a: TabDto, b: TabDto): boolean { return a.isActive === b.isActive && a.isDirty === b.isDirty && @@ -208,9 +204,19 @@ export class TabsMainImp implements TabsMain, Disposable { a.id === b.id; } + protected getOrRebuildModel(map: Map, key: T): R { + // something broke so we rebuild the model + let item = map.get(key); + if (!item) { + this.createTabsModel(); + item = map.get(key)!; + } + return item; + } + // #region event listeners private onTabCreated(tabBar: TabBar, args: TabBar.ITabActivateRequestedArgs): void { - const group = this.tabGroupModel.get(tabBar)!; + const group = this.getOrRebuildModel(this.tabGroupModel, tabBar); this.connectToSignal(this.disposableTabBarListeners, args.title.changed, this.onTabTitleChanged); const tabDto = this.createTabDto(args.title, group.groupId); this.tabInfoLookup.set(args.title, { group, tab: tabDto, tabIndex: args.index }); @@ -224,7 +230,7 @@ export class TabsMainImp implements TabsMain, Disposable { } private onTabTitleChanged(title: Title): void { - const tabInfo = this.tabInfoLookup.get(title); + const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, title); if (!tabInfo) { return; } @@ -259,7 +265,7 @@ export class TabsMainImp implements TabsMain, Disposable { } private onTabMoved(tabBar: TabBar, args: TabBar.ITabMovedArgs): void { - const tabInfo = this.tabInfoLookup.get(args.title)!; + const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, args.title)!; tabInfo.tabIndex = args.toIndex; const tabDto = this.createTabDto(args.title, tabInfo.group.groupId); tabInfo.group.tabs.splice(args.fromIndex, 1); @@ -293,9 +299,11 @@ export class TabsMainImp implements TabsMain, Disposable { if (!widget) { continue; } + if (widget.title === this.applicationShell.mainPanel.currentTitle) { + this.applicationShell.activateNextTabInTabBar(); + } widget.dispose(); - // TODO if this was an active widget/tab we need to activate another widget in the the parent widget/group - // after disposing this. If this was the last one the first widget in the first group should be activated. + } return true; } From 924bb5573dab93a94307744b98b8bd49615e6153 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Fri, 27 Jan 2023 09:14:18 +0100 Subject: [PATCH 08/15] close function improvements and fix for tab/group active state Signed-off-by: Jonah Iden --- .../src/main/browser/tabs/tabs-main.ts | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index b0b3b84750179..d54f56633ee86 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -98,31 +98,24 @@ export class TabsMainImp implements TabsMain, Disposable { this.attachListenersToTabBar(tabBar); const groupDto = this.createTabGroupDto(tabBar); - if (groupDto.isActive) { - this.currentActiveGroup = groupDto; - } tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index })); newTabGroupModel.set(tabBar, groupDto); }); - if (newTabGroupModel.size > 0 && !Array.from(newTabGroupModel.values()).some(groupDto => groupDto.isActive)) { - newTabGroupModel.values().next().value.isActive = true; // allways needs one active group, so if there is none we just take the first one + if (newTabGroupModel.size > 0 && Array.from(newTabGroupModel.values()).indexOf(this.currentActiveGroup) < 0) { + this.currentActiveGroup = this.tabInfoLookup.get(this.applicationShell.mainPanel.currentTitle!)?.group ?? newTabGroupModel.values().next().value; + this.currentActiveGroup.isActive = true; } this.tabGroupModel = newTabGroupModel; this.proxy.$acceptEditorTabModel(Array.from(this.tabGroupModel.values())); } protected createTabDto(tabTitle: Title, groupId: number): TabDto { - const activeTitle = this.applicationShell.mainPanel.currentTitle; const widget = tabTitle.owner; - let isActive = false; - if (activeTitle === tabTitle) { - isActive = true; - } return { id: this.generateTabId(tabTitle, groupId), label: tabTitle.label, input: this.evaluateTabDtoInput(widget), - isActive, + isActive: tabTitle.owner.isVisible, isPinned: tabTitle.className.includes(PINNED_CLASS), isDirty: Saveable.isDirty(widget), isPreview: widget instanceof EditorPreviewWidget && widget.isPreview @@ -134,19 +127,15 @@ export class TabsMainImp implements TabsMain, Disposable { } protected createTabGroupDto(tabBar: TabBar): TabGroupDto { - let groupIsActive = false; const oldDto = this.tabGroupModel.get(tabBar); const groupId = oldDto ? oldDto.groupId : this.GroupIdCounter++; const tabs: TabDto[] = tabBar.titles.map(title => { const tabDto = this.createTabDto(title, groupId); - if (tabDto.isActive) { - groupIsActive = true; - } return tabDto; }); const viewColumn = 1; return { - groupId, tabs, isActive: groupIsActive, viewColumn + groupId, tabs, isActive: false, viewColumn }; } @@ -243,7 +232,7 @@ export class TabsMainImp implements TabsMain, Disposable { this.proxy.$acceptTabGroupUpdate(tabInfo.group); } if (!this.tabDtosEqual(oldTabDto, newTabDto)) { - tabInfo.group.tabs.splice(tabInfo.tabIndex, 1, newTabDto); + tabInfo.group.tabs[tabInfo.tabIndex] = newTabDto; this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, index: tabInfo.tabIndex, @@ -269,7 +258,7 @@ export class TabsMainImp implements TabsMain, Disposable { tabInfo.tabIndex = args.toIndex; const tabDto = this.createTabDto(args.title, tabInfo.group.groupId); tabInfo.group.tabs.splice(args.fromIndex, 1); - tabInfo.group.tabs.splice(args.toIndex, 1, tabDto); + tabInfo.group.tabs.splice(args.toIndex, 0, tabDto); this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_MOVE, index: args.toIndex, @@ -294,25 +283,17 @@ export class TabsMainImp implements TabsMain, Disposable { } async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { - for (const tabId of tabIds) { - const widget = this.applicationShell.getWidgetById(tabId.substring(tabId.indexOf('~') + 1)); - if (!widget) { - continue; - } - if (widget.title === this.applicationShell.mainPanel.currentTitle) { - this.applicationShell.activateNextTabInTabBar(); - } - widget.dispose(); - - } + const widgetIds = tabIds.map(tabId => tabId.substring(tabId.indexOf('~') + 1)); + const widgets = widgetIds.map(e => this.applicationShell.getWidgetById(e)).filter((e): e is Widget => e !== undefined); + await this.applicationShell.closeMany(widgets); return true; } async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { for (const groupId of groupIds) { - const tabBar = Array.from(this.tabGroupModel.entries()).find(([, tabGroup]) => tabGroup.groupId === groupId)?.[0]; + const tabBar = Array.from(this.tabGroupModel.entries()).find(([bar, groupDto]) => groupDto.groupId === groupId)?.[0]; if (tabBar) { - tabBar.titles.forEach(title => title.owner.dispose()); + this.applicationShell.closeTabs(tabBar); } } return true; From ccacc02f646f3d7e3e96812233d63d9bfde624e5 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Fri, 27 Jan 2023 09:20:58 +0100 Subject: [PATCH 09/15] tab group close event fixed (tab event first showing tab, then tab group close with undefined) Signed-off-by: Jonah Iden --- packages/plugin-ext/src/main/browser/tabs/tabs-main.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index d54f56633ee86..68e719c77a7db 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -79,12 +79,11 @@ export class TabsMainImp implements TabsMain, Disposable { this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (mainPanel, widget) => { if (!(widget instanceof TabBar)) { + const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, widget.title)!; + this.onTabClosed(tabInfo, widget.title); if (this.tabGroupChanged) { this.tabGroupChanged = false; this.createTabsModel(); - } else { - const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, widget.title)!; - this.onTabClosed(tabInfo, widget.title); } } }); From b543c6f6a4b7adefbb8a5f487db96d56b7e84a51 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Fri, 27 Jan 2023 09:25:21 +0100 Subject: [PATCH 10/15] removed "@stubbed" for implemented apis Signed-off-by: Jonah Iden --- packages/plugin/src/theia.d.ts | 50 ++++------------------------------ 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 23861d8c248be..a53308c4c5de2 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -13474,13 +13474,11 @@ export module '@theia/plugin' { export class TabInputText { /** * The uri represented by the tab. - * @stubbed */ readonly uri: Uri; /** * Constructs a text tab input with the given URI. * @param uri The URI of the tab. - * @stubbed */ constructor(uri: Uri); } @@ -13492,19 +13490,16 @@ export module '@theia/plugin' { export class TabInputTextDiff { /** * The uri of the original text resource. - * @stubbed */ readonly original: Uri; /** * The uri of the modified text resource. - * @stubbed */ 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. - * @stubbed */ constructor(original: Uri, modified: Uri); } @@ -13515,19 +13510,16 @@ export module '@theia/plugin' { export class TabInputCustom { /** * The uri that the tab is representing. - * @stubbed */ readonly uri: Uri; /** * The type of custom editor. - * @stubbed */ readonly viewType: string; /** * Constructs a custom editor tab input. * @param uri The uri of the tab. * @param viewType The viewtype of the custom editor. - * @stubbed */ constructor(uri: Uri, viewType: string); } @@ -13538,13 +13530,11 @@ export module '@theia/plugin' { export class TabInputWebview { /** * The type of webview. Maps to WebviewPanel's viewType - * @stubbed */ readonly viewType: string; /** * Constructs a webview tab input with the given view type. * @param viewType The type of webview. Maps to WebviewPanel's viewType - * @stubbed */ constructor(viewType: string); } @@ -13555,19 +13545,16 @@ export module '@theia/plugin' { export class TabInputNotebook { /** * The uri that the tab is representing. - * @stubbed */ readonly uri: Uri; /** * The type of notebook. Maps to NotebookDocuments's notebookType - * @stubbed */ 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 NotebookDocuments's notebookType - * @stubbed */ constructor(uri: Uri, notebookType: string); } @@ -13578,17 +13565,14 @@ export module '@theia/plugin' { export class TabInputNotebookDiff { /** * The uri of the original notebook. - * @stubbed */ readonly original: Uri; /** * The uri of the modified notebook. - * @stubbed */ readonly modified: Uri; /** * The type of notebook. Maps to NotebookDocuments's notebookType - * @stubbed */ readonly notebookType: string; /** @@ -13596,7 +13580,6 @@ export module '@theia/plugin' { * @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 NotebookDocuments's notebookType - * @stubbed */ constructor(original: Uri, modified: Uri, notebookType: string); } @@ -13607,7 +13590,6 @@ export module '@theia/plugin' { export class TabInputTerminal { /** * Constructs a terminal tab input. - * @stubbed */ constructor(); } @@ -13621,45 +13603,38 @@ export module '@theia/plugin' { /** * The text displayed on the tab. - * @stubbed */ readonly label: string; /** * The group which the tab belongs to. - * @stubbed */ 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. - * @stubbed */ 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. - * @stubbed */ readonly isActive: boolean; /** * Whether or not the dirty indicator is present on the tab. - * @stubbed */ readonly isDirty: boolean; /** * Whether or not the tab is pinned (pin icon is present). - * @stubbed */ readonly isPinned: boolean; /** * Whether or not the tab is in preview mode. - * @stubbed */ readonly isPreview: boolean; } @@ -13670,18 +13645,15 @@ export module '@theia/plugin' { export interface TabChangeEvent { /** * The tabs that have been opened. - * @stubbed */ readonly opened: readonly Tab[]; /** * The tabs that have been closed. - * @stubbed */ readonly closed: readonly Tab[]; /** * Tabs that have changed, e.g have changed * their {@link Tab.isActive active} state. - * @stubbed */ readonly changed: readonly Tab[]; } @@ -13692,18 +13664,15 @@ export module '@theia/plugin' { export interface TabGroupChangeEvent { /** * Tab groups that have been opened. - * @stubbed */ readonly opened: readonly TabGroup[]; /** * Tab groups that have been closed. - * @stubbed */ readonly closed: readonly TabGroup[]; /** * Tab groups that have changed, e.g have changed * their {@link TabGroup.isActive active} state. - * @stubbed */ readonly changed: readonly TabGroup[]; } @@ -13719,13 +13688,11 @@ export module '@theia/plugin' { * groups can have an {@link TabGroup.aciveTab active tab}. * * @see {@link Tab.isActive} - * @stubbed */ readonly isActive: boolean; /** * The view column of the group. - * @stubbed */ readonly viewColumn: ViewColumn; @@ -13734,14 +13701,12 @@ export module '@theia/plugin' { * being rendered. * * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. - * @stubbed */ readonly activeTab: Tab | undefined; /** * The list of tabs contained within the group. * This can be empty if the group has no tabs open. - * @stubbed */ readonly tabs: readonly Tab[]; } @@ -13752,25 +13717,21 @@ export module '@theia/plugin' { export interface TabGroups { /** * All the groups within the group container. - * @stubbed */ readonly all: readonly TabGroup[]; /** * The currently active group. - * @stubbed */ readonly activeTabGroup: TabGroup; /** * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. - * @stubbed */ readonly onDidChangeTabGroups: Event; /** * An {@link Event event} which fires when {@link Tab tabs} have changed. - * @stubbed */ readonly onDidChangeTabs: Event; @@ -13782,7 +13743,6 @@ export module '@theia/plugin' { * @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. - * @stubbed */ close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; @@ -13933,7 +13893,7 @@ export module '@theia/plugin' { /** * The {@link NotebookDocument notebook} that contains this cell. - * @stubbed + */ readonly notebook: NotebookDocument; @@ -13994,26 +13954,26 @@ export module '@theia/plugin' { /** * The version number of this notebook (it will strictly increase after each * change, including undo/redo). - * @stubbed + */ readonly version: number; /** * `true` if there are unpersisted changes. - * @stubbed + */ readonly isDirty: boolean; /** * Is this notebook representing an untitled file which has not been saved yet. - * @stubbed + */ readonly isUntitled: boolean; /** * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore * and won't be re-used when the same resource is opened again. - * @stubbed + */ readonly isClosed: boolean; From 855b751f2bb6007a2996e4ae2a097355b9902093 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 30 Jan 2023 10:08:45 +0100 Subject: [PATCH 11/15] review changes ; fix for onTabChange activating after defocusing and focusing tab again; fix for moving tab between groups, now shows delete event correctly and no event when dropped to previous tabbar Signed-off-by: Jonah Iden --- .../src/browser/shell/application-shell.ts | 6 +++--- .../src/main/browser/main-context.ts | 4 ++-- .../src/main/browser/tabs/tabs-main.ts | 19 ++++++++++++++----- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index dc141567d2664..3afaaf15c806b 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -89,8 +89,8 @@ export class DockPanelRenderer implements DockLayout.IRenderer { readonly tabBarClasses: string[] = []; - private readonly _onTabBarCreated = new Emitter>(); - readonly onTabBarCreated = this._onTabBarCreated.event; + private readonly _onDidCreateTabBar = new Emitter>(); + readonly onDidCreateTabBar = this._onDidCreateTabBar.event; constructor( @inject(TabBarRendererFactory) protected readonly tabBarRendererFactory: TabBarRendererFactory, @@ -118,7 +118,7 @@ export class DockPanelRenderer implements DockLayout.IRenderer { tabBar.disposed.connect(() => renderer.dispose()); renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; tabBar.currentChanged.connect(this.onCurrentTabChanged, this); - this._onTabBarCreated.fire(tabBar); + this._onDidCreateTabBar.fire(tabBar); return tabBar; } diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 33e57dfd33b68..78b292d43a580 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -58,7 +58,7 @@ import { WebviewViewsMainImpl } from './webview-views/webview-views-main'; import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages'; import { UntitledResourceResolver } from '@theia/core/lib/common/resource'; import { ThemeService } from '@theia/core/lib/browser/theming'; -import { TabsMainImp } from './tabs/tabs-main'; +import { TabsMainImpl } from './tabs/tabs-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const authenticationMain = new AuthenticationMainImpl(rpc, container); @@ -182,6 +182,6 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const commentsMain = new CommentsMainImp(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.COMMENTS_MAIN, commentsMain); - const tabsMain = new TabsMainImp(rpc, container); + const tabsMain = new TabsMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.TABS_MAIN, tabsMain); } diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 68e719c77a7db..05336c63d6b86 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -30,7 +30,7 @@ interface TabInfo { group: TabGroupDto; } -export class TabsMainImp implements TabsMain, Disposable { +export class TabsMainImpl implements TabsMain, Disposable { private readonly proxy: TabsExt; private tabGroupModel = new Map, TabGroupDto>(); @@ -41,7 +41,7 @@ export class TabsMainImp implements TabsMain, Disposable { private disposableTabBarListeners: Disposable[] = []; private toDisposeOnDestroy: Disposable[] = []; - private GroupIdCounter = 0; + private groupIdCounter = 0; private currentActiveGroup: TabGroupDto; private tabGroupChanged: boolean = false; @@ -61,7 +61,7 @@ export class TabsMainImp implements TabsMain, Disposable { } this.toDisposeOnDestroy.push( - this.applicationShell.mainPanelRenderer.onTabBarCreated(tabBar => { + this.applicationShell.mainPanelRenderer.onDidCreateTabBar(tabBar => { this.attachListenersToTabBar(tabBar); this.onTabGroupCreated(tabBar); }) @@ -73,7 +73,15 @@ export class TabsMainImp implements TabsMain, Disposable { this.createTabsModel(); } else { const tabBar = mainPanel.findTabBar(widget.title)!; - this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); + const oldTabInfo = this.tabInfoLookup.get(widget.title); + const group = this.tabGroupModel.get(tabBar); + if (group !== oldTabInfo?.group) { + if (oldTabInfo) { + this.onTabClosed(oldTabInfo, widget.title); + } + + this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title }); + } } }); @@ -127,7 +135,7 @@ export class TabsMainImp implements TabsMain, Disposable { protected createTabGroupDto(tabBar: TabBar): TabGroupDto { const oldDto = this.tabGroupModel.get(tabBar); - const groupId = oldDto ? oldDto.groupId : this.GroupIdCounter++; + const groupId = oldDto ? oldDto.groupId : this.groupIdCounter++; const tabs: TabDto[] = tabBar.titles.map(title => { const tabDto = this.createTabDto(title, groupId); return tabDto; @@ -232,6 +240,7 @@ export class TabsMainImp implements TabsMain, Disposable { } if (!this.tabDtosEqual(oldTabDto, newTabDto)) { tabInfo.group.tabs[tabInfo.tabIndex] = newTabDto; + tabInfo.tab = newTabDto; this.proxy.$acceptTabOperation({ kind: TabModelOperationKind.TAB_UPDATE, index: tabInfo.tabIndex, From fcadd662774a2e4d8dae2b273e4ef9fda840fa5f Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 30 Jan 2023 11:08:21 +0000 Subject: [PATCH 12/15] removed _ naming for emitter Signed-off-by: Jonah Iden --- packages/core/src/browser/shell/application-shell.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 3afaaf15c806b..602a69884f2f2 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -89,8 +89,8 @@ export class DockPanelRenderer implements DockLayout.IRenderer { readonly tabBarClasses: string[] = []; - private readonly _onDidCreateTabBar = new Emitter>(); - readonly onDidCreateTabBar = this._onDidCreateTabBar.event; + private readonly onDidCreateTabBarEmitter = new Emitter>(); + readonly onDidCreateTabBar = this.onDidCreateTabBarEmitter.event; constructor( @inject(TabBarRendererFactory) protected readonly tabBarRendererFactory: TabBarRendererFactory, @@ -118,7 +118,7 @@ export class DockPanelRenderer implements DockLayout.IRenderer { tabBar.disposed.connect(() => renderer.dispose()); renderer.contextMenuPath = SHELL_TABBAR_CONTEXT_MENU; tabBar.currentChanged.connect(this.onCurrentTabChanged, this); - this._onDidCreateTabBar.fire(tabBar); + this.onDidCreateTabBarEmitter.fire(tabBar); return tabBar; } From 16f88c266377951a1a47226e81aaae39dcda3c16 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 30 Jan 2023 14:36:13 +0000 Subject: [PATCH 13/15] renamed method; use DisposableCollection; lint Signed-off-by: Jonah Iden --- .../src/main/browser/tabs/tabs-main.ts | 17 +++++++++-------- packages/plugin/src/theia.d.ts | 5 ----- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 05336c63d6b86..2a07c43d8b768 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -23,6 +23,7 @@ import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol'; import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor'; import { toUriComponents } from '../hierarchy/hierarchy-types-converters'; import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { DisposableCollection } from '@theia/core'; interface TabInfo { tab: TabDto; @@ -38,8 +39,8 @@ export class TabsMainImpl implements TabsMain, Disposable { private applicationShell: ApplicationShell; - private disposableTabBarListeners: Disposable[] = []; - private toDisposeOnDestroy: Disposable[] = []; + private disposableTabBarListeners: DisposableCollection = new DisposableCollection(); + private toDisposeOnDestroy: DisposableCollection = new DisposableCollection(); private groupIdCounter = 0; private currentActiveGroup: TabGroupDto; @@ -100,7 +101,7 @@ export class TabsMainImpl implements TabsMain, Disposable { protected createTabsModel(): void { const newTabGroupModel = new Map, TabGroupDto>(); this.tabInfoLookup.clear(); - this.disposableTabBarListeners.forEach(disposable => disposable.dispose()); + this.disposableTabBarListeners.dispose(); this.applicationShell.mainAreaTabBars.forEach(tabBar => { this.attachListenersToTabBar(tabBar); @@ -119,7 +120,7 @@ export class TabsMainImpl implements TabsMain, Disposable { protected createTabDto(tabTitle: Title, groupId: number): TabDto { const widget = tabTitle.owner; return { - id: this.generateTabId(tabTitle, groupId), + id: this.createTabId(tabTitle, groupId), label: tabTitle.label, input: this.evaluateTabDtoInput(widget), isActive: tabTitle.owner.isVisible, @@ -129,7 +130,7 @@ export class TabsMainImpl implements TabsMain, Disposable { }; } - protected generateTabId(tabTitle: Title, groupId: number): string { + protected createTabId(tabTitle: Title, groupId: number): string { return `${groupId}~${tabTitle.owner.id}`; } @@ -187,7 +188,7 @@ export class TabsMainImpl implements TabsMain, Disposable { return { kind: TabInputKind.UnknownInput }; } - protected connectToSignal(disposableList: Disposable[], signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { + protected connectToSignal(disposableList: DisposableCollection, signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void { signal.connect(listener, this); disposableList.push(Disposable.create(() => signal.disconnect(listener))); } @@ -309,7 +310,7 @@ export class TabsMainImpl implements TabsMain, Disposable { // #endregion dispose(): void { - this.toDisposeOnDestroy.forEach(disposable => disposable.dispose()); - this.disposableTabBarListeners.forEach(disposable => disposable.dispose()); + this.toDisposeOnDestroy.dispose(); + this.disposableTabBarListeners.dispose(); } } diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index a53308c4c5de2..dfa0a03e9a02d 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -13893,7 +13893,6 @@ export module '@theia/plugin' { /** * The {@link NotebookDocument notebook} that contains this cell. - */ readonly notebook: NotebookDocument; @@ -13954,26 +13953,22 @@ export module '@theia/plugin' { /** * The version number of this notebook (it will strictly increase after each * change, including undo/redo). - */ readonly version: number; /** * `true` if there are unpersisted changes. - */ readonly isDirty: boolean; /** * Is this notebook representing an untitled file which has not been saved yet. - */ readonly isUntitled: boolean; /** * `true` if the notebook has been closed. A closed notebook isn't synchronized anymore * and won't be re-used when the same resource is opened again. - */ readonly isClosed: boolean; From 30d25bbd740b6fe53af6be09150c8d21c1908f02 Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Wed, 1 Feb 2023 12:13:59 +0100 Subject: [PATCH 14/15] tab group and there respective tab open/close made consistent with vscode Signed-off-by: Jonah Iden --- packages/plugin-ext/src/main/browser/tabs/tabs-main.ts | 1 + packages/plugin-ext/src/plugin/tabs.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 2a07c43d8b768..58fc086825119 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -72,6 +72,7 @@ export class TabsMainImpl implements TabsMain, Disposable { if (this.tabGroupChanged || this.tabGroupModel.size === 0) { this.tabGroupChanged = false; this.createTabsModel(); + // tab Open event is done in backend } else { const tabBar = mainPanel.findTabBar(widget.title)!; const oldTabInfo = this.tabInfoLookup.get(widget.title); diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index 2fbb0fe8d9ddc..1c2512271aa90 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -306,11 +306,13 @@ export class TabsExtImpl implements TabsExt { const closed: theia.TabGroup[] = this.tabGroupArr.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject); const opened: theia.TabGroup[] = []; const changed: theia.TabGroup[] = []; + const tabsOpened: theia.Tab[] = []; this.tabGroupArr = tabGroups.map(tabGroup => { const group = new TabGroupExt(tabGroup, () => this.activeGroupId); if (diff.added.includes(group.groupId)) { - opened.push(group.apiObject); + opened.push({ activeTab: undefined, isActive: group.apiObject.isActive, tabs: [], viewColumn: group.apiObject.viewColumn }); + tabsOpened.push(...group.apiObject.tabs); } else { changed.push(group.apiObject); } @@ -326,6 +328,8 @@ export class TabsExtImpl implements TabsExt { } } this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); + this.onDidChangeTabs.fire({ opened: tabsOpened, changed: [], closed: [] }); + } $acceptTabGroupUpdate(groupDto: TabGroupDto): void { From b7a94362bf2e9bcd9d37c42a2948ff1df5f413e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Thu, 2 Feb 2023 16:20:29 -0500 Subject: [PATCH 15/15] code cleanup --- .../src/browser/shell/application-shell.ts | 5 ++- .../src/main/browser/tabs/tabs-main.ts | 32 +++++++++++-------- packages/plugin-ext/src/plugin/tabs.ts | 7 ++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index 602a69884f2f2..bb58e2ad4ed7d 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -90,7 +90,6 @@ export class DockPanelRenderer implements DockLayout.IRenderer { readonly tabBarClasses: string[] = []; private readonly onDidCreateTabBarEmitter = new Emitter>(); - readonly onDidCreateTabBar = this.onDidCreateTabBarEmitter.event; constructor( @inject(TabBarRendererFactory) protected readonly tabBarRendererFactory: TabBarRendererFactory, @@ -99,6 +98,10 @@ export class DockPanelRenderer implements DockLayout.IRenderer { @inject(BreadcrumbsRendererFactory) protected readonly breadcrumbsRendererFactory: BreadcrumbsRendererFactory, ) { } + get onDidCreateTabBar(): CommonEvent> { + return this.onDidCreateTabBarEmitter.event; + } + createTabBar(): TabBar { const renderer = this.tabBarRendererFactory(); const tabBar = new ToolbarAwareTabBar( diff --git a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts index 58fc086825119..63c3e567e06df 100644 --- a/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts +++ b/packages/plugin-ext/src/main/browser/tabs/tabs-main.ts @@ -105,7 +105,6 @@ export class TabsMainImpl implements TabsMain, Disposable { this.disposableTabBarListeners.dispose(); this.applicationShell.mainAreaTabBars.forEach(tabBar => { this.attachListenersToTabBar(tabBar); - const groupDto = this.createTabGroupDto(tabBar); tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index })); newTabGroupModel.set(tabBar, groupDto); @@ -137,14 +136,13 @@ export class TabsMainImpl implements TabsMain, Disposable { protected createTabGroupDto(tabBar: TabBar): TabGroupDto { const oldDto = this.tabGroupModel.get(tabBar); - const groupId = oldDto ? oldDto.groupId : this.groupIdCounter++; - const tabs: TabDto[] = tabBar.titles.map(title => { - const tabDto = this.createTabDto(title, groupId); - return tabDto; - }); - const viewColumn = 1; + const groupId = oldDto?.groupId ?? this.groupIdCounter++; + const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId)); return { - groupId, tabs, isActive: false, viewColumn + groupId, + tabs, + isActive: false, + viewColumn: 1 }; } @@ -293,17 +291,25 @@ export class TabsMainImpl implements TabsMain, Disposable { } async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise { - const widgetIds = tabIds.map(tabId => tabId.substring(tabId.indexOf('~') + 1)); - const widgets = widgetIds.map(e => this.applicationShell.getWidgetById(e)).filter((e): e is Widget => e !== undefined); + const widgets: Widget[] = []; + for (const tabId of tabIds) { + const cleanedId = tabId.substring(tabId.indexOf('~') + 1); + const widget = this.applicationShell.getWidgetById(cleanedId); + if (widget) { + widgets.push(widget); + } + } await this.applicationShell.closeMany(widgets); return true; } async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise { for (const groupId of groupIds) { - const tabBar = Array.from(this.tabGroupModel.entries()).find(([bar, groupDto]) => groupDto.groupId === groupId)?.[0]; - if (tabBar) { - this.applicationShell.closeTabs(tabBar); + tabGroupModel: for (const [bar, groupDto] of this.tabGroupModel) { + if (groupDto.groupId === groupId) { + this.applicationShell.closeTabs(bar); + break tabGroupModel; + } } } return true; diff --git a/packages/plugin-ext/src/plugin/tabs.ts b/packages/plugin-ext/src/plugin/tabs.ts index 1c2512271aa90..96071fcb22ae4 100644 --- a/packages/plugin-ext/src/plugin/tabs.ts +++ b/packages/plugin-ext/src/plugin/tabs.ts @@ -291,7 +291,6 @@ export class TabsExtImpl implements TabsExt { return this._closeTabs(tabsOrTabGroups as theia.Tab[], preserveFocus); } }, - }; this.apiObject = Object.freeze(obj); } @@ -319,17 +318,15 @@ export class TabsExtImpl implements TabsExt { return group; }); - const groupId = tabGroups.find(group => group.isActive === true)?.groupId; // Set the active tab group id. skip if no tabgroups are open if (tabGroups.length > 0) { - const activeTabGroupId = assertIsDefined(groupId); - if (activeTabGroupId !== undefined && this.activeGroupId !== activeTabGroupId) { + const activeTabGroupId = assertIsDefined(tabGroups.find(group => group.isActive === true)?.groupId); + if (this.activeGroupId !== activeTabGroupId) { this.activeGroupId = activeTabGroupId; } } this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed })); this.onDidChangeTabs.fire({ opened: tabsOpened, changed: [], closed: [] }); - } $acceptTabGroupUpdate(groupDto: TabGroupDto): void {