UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

484 lines 20.1 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2026 EclipseSource GmbH. // // 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-only WITH Classpath-exception-2.0 // ***************************************************************************** var PerspectiveServiceImpl_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.PerspectiveServiceImpl = exports.WidgetAreaResolverImpl = exports.PerspectiveContribution = exports.PerspectiveServiceInternal = exports.PerspectiveService = exports.ACTIVE_PERSPECTIVE_CONTEXT_KEY = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("inversify"); const application_shell_1 = require("./shell/application-shell"); const frontend_application_contribution_1 = require("./frontend-application-contribution"); const view_contribution_1 = require("./shell/view-contribution"); const widget_manager_1 = require("./widget-manager"); const contribution_provider_1 = require("../common/contribution-provider"); const command_1 = require("../common/command"); const event_1 = require("../common/event"); const logger_1 = require("../common/logger"); const quick_pick_service_1 = require("../common/quick-pick-service"); const nls_1 = require("../common/nls"); const common_commands_1 = require("./common-commands"); const context_key_service_1 = require("./context-key-service"); exports.ACTIVE_PERSPECTIVE_CONTEXT_KEY = 'activePerspectiveId'; exports.PerspectiveService = Symbol('PerspectiveService'); exports.PerspectiveServiceInternal = Symbol('PerspectiveServiceInternal'); exports.PerspectiveContribution = Symbol('PerspectiveContribution'); let WidgetAreaResolverImpl = class WidgetAreaResolverImpl { constructor() { this.activePlacementMap = new Map(); } setActivePlacementMap(map) { this.activePlacementMap = map; } resolveArea(widgetId, requestedArea) { return this.activePlacementMap.get(widgetId); } }; exports.WidgetAreaResolverImpl = WidgetAreaResolverImpl; exports.WidgetAreaResolverImpl = WidgetAreaResolverImpl = tslib_1.__decorate([ (0, inversify_1.injectable)() ], WidgetAreaResolverImpl); let PerspectiveServiceImpl = class PerspectiveServiceImpl { constructor() { this.defaultPerspectiveId = PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID; this.perspectives = new Map(); this.savedLayouts = new Map(); this.onDidChangePerspectiveEmitter = new event_1.Emitter(); this.onDidChangePerspective = this.onDidChangePerspectiveEmitter.event; } static { PerspectiveServiceImpl_1 = this; } static { this.SWITCH_PERSPECTIVE_COMMAND = command_1.Command.toLocalizedCommand({ id: 'perspective.switch', category: common_commands_1.CommonCommands.VIEW_CATEGORY, label: 'Switch Perspective (Experimental)' }, 'theia/core/perspective/switchPerspective', common_commands_1.CommonCommands.VIEW_CATEGORY_KEY); } static { this.RESET_PERSPECTIVE_COMMAND = command_1.Command.toLocalizedCommand({ id: 'perspective.reset', category: common_commands_1.CommonCommands.VIEW_CATEGORY, label: 'Reset Perspective (Experimental)' }, 'theia/core/perspective/resetPerspective', common_commands_1.CommonCommands.VIEW_CATEGORY_KEY); } static { this.DEFAULT_PERSPECTIVE_ID = 'default'; } onLayoutRestored(activePerspectiveId) { this.updateActivePerspectiveContextKey(activePerspectiveId); } initialize() { this.registerPerspective({ id: PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID, label: nls_1.nls.localizeByDefault('Default'), viewPlacements: new Map() }); this.activePerspectiveId = PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID; this.updateActivePerspectiveContextKey(PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID); if (this.contributions) { for (const contribution of this.contributions.getContributions()) { contribution.registerPerspectives(this); } } } onStop() { this.onDidChangePerspectiveEmitter.dispose(); } updateActivePerspectiveContextKey(id) { this.contextKeyService?.createKey(exports.ACTIVE_PERSPECTIVE_CONTEXT_KEY, id).set(id); } registerPerspective(descriptor) { this.perspectives.set(descriptor.id, descriptor); } async switchPerspective(id) { const pending = (this.switchInProgress ?? Promise.resolve()) .catch(() => { }) .then(() => this.doSwitchPerspective(id)); this.switchInProgress = pending; try { await pending; } finally { if (this.switchInProgress === pending) { this.switchInProgress = undefined; } } } async doSwitchPerspective(id) { if (id === this.activePerspectiveId) { return; } const descriptor = this.perspectives.get(id); if (!descriptor) { return; } const oldPerspective = this.getActivePerspective(); if (oldPerspective?.onDeactivate) { oldPerspective.onDeactivate(this.shell); } if (this.activePerspectiveId) { this.savedLayouts.set(this.activePerspectiveId, this.shell.getLayoutData()); } this.activePerspectiveId = id; this.widgetAreaResolver.setActivePlacementMap(descriptor.viewPlacements); try { const savedLayout = this.savedLayouts.get(id); if (savedLayout) { const layoutWidgetIds = this.collectWidgetIds(savedLayout); await this.healLayoutData(savedLayout); await this.shell.setLayoutData(savedLayout); this.detachStrayWidgets(layoutWidgetIds); } else { await this.applyViewPlacements(descriptor); } } catch (error) { this.logger.warn('Failed to apply layout for perspective', error); } if (descriptor.onActivate) { descriptor.onActivate(this.shell); } this.updateActivePerspectiveContextKey(id); this.onDidChangePerspectiveEmitter.fire(id); } async applyViewPlacements(descriptor) { for (const [viewId, area] of descriptor.viewPlacements) { try { const widget = await this.widgetManager.getOrCreateWidget(viewId); const currentTabBar = this.shell.getTabBarFor(widget); if (currentTabBar) { const currentArea = this.shell.getAreaFor(widget); if (currentArea === area) { continue; } } await this.shell.addWidget(widget, { area }); } catch (error) { this.logger.warn('Failed to create or place widget for perspective', error); } } for (const [viewId] of descriptor.viewPlacements) { try { await this.shell.activateWidget(viewId); } catch (error) { this.logger.warn('Failed to activate widget for perspective', error); } } if (descriptor.primaryViews) { for (const viewId of Object.values(descriptor.primaryViews)) { if (viewId) { try { await this.shell.activateWidget(viewId); } catch (error) { this.logger.warn('Failed to activate primary view for perspective', error); } } } } if (descriptor.chromeOptions?.collapseAreas) { for (const area of descriptor.chromeOptions.collapseAreas) { await this.shell.collapsePanel(area); } } } async resetCurrentPerspective() { const pending = (this.switchInProgress ?? Promise.resolve()) .catch(() => { }) .then(() => this.doResetCurrentPerspective()); this.switchInProgress = pending; try { await pending; } finally { if (this.switchInProgress === pending) { this.switchInProgress = undefined; } } } async doResetCurrentPerspective() { const descriptor = this.getActivePerspective(); if (!descriptor) { return; } this.savedLayouts.delete(descriptor.id); try { if (descriptor.id === PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID) { await this.resetDefaultPerspective(); } else { await this.applyViewPlacements(descriptor); } } catch (error) { this.logger.warn('Failed to apply layout for perspective', error); } if (descriptor.onActivate) { descriptor.onActivate(this.shell); } this.onDidChangePerspectiveEmitter.fire(descriptor.id); } async resetDefaultPerspective() { // Phase 1: Relocate open views to their default areas for (const contribution of this.appContributions.getContributions()) { if (contribution instanceof view_contribution_1.AbstractViewContribution) { const widgetId = contribution.effectiveWidgetId; const targetArea = contribution.defaultViewOptions?.area; if (!widgetId || !targetArea) { continue; } const widget = this.widgetManager.tryGetWidget(widgetId); if (widget && !widget.isDisposed && widget.isAttached) { const currentArea = this.shell.getAreaFor(widget); if (currentArea && currentArea !== targetArea) { try { await this.shell.addWidget(widget, { area: targetArea }); } catch (error) { this.logger.warn('Failed to relocate widget during default perspective reset', widgetId, error); } } } } } // Phase 2: Re-open closed views via openView({ activate: false }). // Non-view contributions (e.g., Terminal) are skipped to avoid side effects // like creating new terminal instances. for (const contribution of this.appContributions.getContributions()) { if (contribution instanceof view_contribution_1.AbstractViewContribution) { try { await contribution.openView({ activate: false }); } catch (error) { this.logger.warn('Failed to open view during default perspective reset', error); } } } } /** * Views may be shared between multiple perspective. If a view exists in two or more perspectives * and is closed in one of them, the shared widget is disposed. When switching back to the other * perspective, we need to restore this widget (i.e. reopen the view). * @param layout */ async healLayoutData(layout) { if (layout.leftPanel) { await this.healSidePanelLayout(layout.leftPanel); } if (layout.rightPanel) { await this.healSidePanelLayout(layout.rightPanel); } if (layout.mainPanel?.main) { await this.healDockAreaConfig(layout.mainPanel.main); } if (layout.bottomPanel?.config?.main) { await this.healDockAreaConfig(layout.bottomPanel.config.main); } } async healSidePanelLayout(layout) { if (!layout.items) { return; } for (const item of layout.items) { if (item.widget?.isDisposed) { try { item.widget = await this.widgetManager.getOrCreateWidget(item.widget.id); } catch (error) { this.logger.warn('Failed to recreate disposed widget for perspective layout', item.widget.id, error); item.widget = undefined; } } } } async healDockAreaConfig(config) { if (config.type === 'tab-area') { for (let i = 0; i < config.widgets.length; i++) { const widget = config.widgets[i]; if (widget.isDisposed) { try { config.widgets[i] = await this.widgetManager.getOrCreateWidget(widget.id); } catch (error) { this.logger.warn('Failed to recreate disposed widget for perspective layout', widget.id, error); config.widgets.splice(i, 1); i--; } } } } else if (config.type === 'split-area') { for (const child of config.children) { await this.healDockAreaConfig(child); } } } collectWidgetIds(layout) { const ids = new Set(); if (layout.leftPanel) { this.collectSidePanelWidgetIds(layout.leftPanel, ids); } if (layout.rightPanel) { this.collectSidePanelWidgetIds(layout.rightPanel, ids); } if (layout.mainPanel?.main) { this.collectDockWidgetIds(layout.mainPanel.main, ids); } if (layout.bottomPanel?.config?.main) { this.collectDockWidgetIds(layout.bottomPanel.config.main, ids); } return ids; } collectSidePanelWidgetIds(layout, ids) { if (layout.items) { for (const item of layout.items) { if (item.widget) { ids.add(item.widget.id); } } } } collectDockWidgetIds(config, ids) { if (config.type === 'tab-area') { for (const widget of config.widgets) { if (widget) { ids.add(widget.id); } } } else if (config.type === 'split-area') { for (const child of config.children) { this.collectDockWidgetIds(child, ids); } } } detachStrayWidgets(layoutWidgetIds) { const sidePanelAreas = ['left', 'right']; for (const area of sidePanelAreas) { for (const widget of this.shell.getWidgets(area)) { if (!layoutWidgetIds.has(widget.id)) { // Detach via parent = null rather than widget.close() to avoid // disposing the widget — it may be needed by another perspective. // eslint-disable-next-line no-null/no-null widget.parent = null; } } } } getActivePerspective() { if (this.activePerspectiveId) { return this.perspectives.get(this.activePerspectiveId); } return undefined; } getAreaForView(viewId) { const active = this.getActivePerspective(); if (active) { return active.viewPlacements.get(viewId); } return undefined; } getRegisteredPerspectives() { return Array.from(this.perspectives.values()); } getActivePerspectiveId() { return this.activePerspectiveId ?? PerspectiveServiceImpl_1.DEFAULT_PERSPECTIVE_ID; } getSavedPerspectiveIds() { return Array.from(this.savedLayouts.keys()); } getSavedLayout(perspectiveId) { return this.savedLayouts.get(perspectiveId); } setSavedLayout(perspectiveId, layout) { this.savedLayouts.set(perspectiveId, layout); } setActivePerspectiveId(id) { if (this.perspectives.has(id)) { this.activePerspectiveId = id; return true; } return false; } clearSavedLayouts() { this.savedLayouts.clear(); } registerCommands(commands) { commands.registerCommand(PerspectiveServiceImpl_1.SWITCH_PERSPECTIVE_COMMAND, { execute: () => this.showPerspectivePicker(), isVisible: () => this.perspectives.size > 1 }); commands.registerCommand(PerspectiveServiceImpl_1.RESET_PERSPECTIVE_COMMAND, { execute: () => this.resetCurrentPerspective(), isEnabled: () => this.activePerspectiveId !== undefined }); } async showPerspectivePicker() { if (!this.quickInputService) { return; } const items = this.getRegisteredPerspectives().map(p => ({ label: p.label, id: p.id, description: this.activePerspectiveId === p.id ? nls_1.nls.localizeByDefault('Active') : undefined })); const selected = await this.quickInputService.showQuickPick(items, { placeholder: nls_1.nls.localize('theia/core/perspective/selectPerspective', 'Select a perspective') }); if (selected?.id) { await this.switchPerspective(selected.id); } } }; exports.PerspectiveServiceImpl = PerspectiveServiceImpl; tslib_1.__decorate([ (0, inversify_1.inject)(application_shell_1.ApplicationShell), tslib_1.__metadata("design:type", application_shell_1.ApplicationShell) ], PerspectiveServiceImpl.prototype, "shell", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(widget_manager_1.WidgetManager), tslib_1.__metadata("design:type", widget_manager_1.WidgetManager) ], PerspectiveServiceImpl.prototype, "widgetManager", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(contribution_provider_1.ContributionProvider), (0, inversify_1.named)(exports.PerspectiveContribution), (0, inversify_1.optional)(), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "contributions", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(contribution_provider_1.ContributionProvider), (0, inversify_1.named)(frontend_application_contribution_1.FrontendApplicationContribution), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "appContributions", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(quick_pick_service_1.QuickInputService), (0, inversify_1.optional)(), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "quickInputService", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(logger_1.ILogger), (0, inversify_1.named)('core:PerspectiveService'), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "logger", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(application_shell_1.WidgetAreaResolver), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "widgetAreaResolver", void 0); tslib_1.__decorate([ (0, inversify_1.inject)(context_key_service_1.ContextKeyService), (0, inversify_1.optional)(), tslib_1.__metadata("design:type", Object) ], PerspectiveServiceImpl.prototype, "contextKeyService", void 0); exports.PerspectiveServiceImpl = PerspectiveServiceImpl = PerspectiveServiceImpl_1 = tslib_1.__decorate([ (0, inversify_1.injectable)() ], PerspectiveServiceImpl); //# sourceMappingURL=perspective-service.js.map