diff --git a/CHANGELOG.md b/CHANGELOG.md index a00f2867e9605..9ea9467d9b4c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Change Log +## v1.15.0 - 6/24/2021 + +[1.15.0 Milestone](https://github.com/eclipse-theia/theia/milestone/21) + +- [editor-preview] rewrote `editor-preview`-package classes as extensions of `editor`-package classes [#9518](https://github.com/eclipse-theia/theia/pull/9517) + +[Breaking Changes:](#breaking_changes_1.15.0) + +- [editor-preview] `EditorPreviewWidget` now extends `EditorWidget` and `EditorPreviewManager` extends and overrides `EditorManager`. `instanceof` checks can no longer distinguish between preview and non-preview editors; use `.isPreview` field instead. [#9518](https://github.com/eclipse-theia/theia/pull/9517) + ## v1.14.0 - 5/27/2021 [1.14.0 Milestone](https://github.com/eclipse-theia/theia/milestone/20) diff --git a/examples/api-tests/src/keybindings.spec.js b/examples/api-tests/src/keybindings.spec.js index 2087a697cfa15..5f862ee5cb3a9 100644 --- a/examples/api-tests/src/keybindings.spec.js +++ b/examples/api-tests/src/keybindings.spec.js @@ -93,7 +93,7 @@ describe('Keybindings', function () { assert.notEqual(executedCommand, id); }); - it('later registered keybinding should has higher priority', async () => { + it('later registered keybinding should have higher priority', async () => { const id = '__test:keybindings.copy'; toTearDown.push(commands.registerCommand({ id }, { execute: () => { } diff --git a/examples/api-tests/src/saveable.spec.js b/examples/api-tests/src/saveable.spec.js index 9bfdd43f16c8e..1407266bd1da1 100644 --- a/examples/api-tests/src/saveable.spec.js +++ b/examples/api-tests/src/saveable.spec.js @@ -468,17 +468,19 @@ describe('Saveable', function () { } }); - it(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => { + it.only(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => { await preferences.set(closeOnFileDelete, true); assert.isTrue(preferences.get(closeOnFileDelete)); assert.isFalse(Saveable.isDirty(widget)); const waitForDisposed = new Deferred(); + // Must pass in 5 seconds, so check state after 4.5. const listener = editor.onDispose(() => waitForDisposed.resolve()); + const fourSeconds = new Promise(resolve => setTimeout(resolve, 4500)); try { - await fileService.delete(fileUri); - await waitForDisposed.promise; + const deleteThenDispose = fileService.delete(fileUri).then(() => waitForDisposed.promise); + await Promise.race([deleteThenDispose, fourSeconds]); assert.isTrue(widget.isDisposed); } finally { listener.dispose(); diff --git a/examples/api-tests/src/typescript.spec.js b/examples/api-tests/src/typescript.spec.js index 1146d8da937f4..0cda26c50a6a8 100644 --- a/examples/api-tests/src/typescript.spec.js +++ b/examples/api-tests/src/typescript.spec.js @@ -33,7 +33,6 @@ describe('TypeScript', function () { const { CommandRegistry } = require('@theia/core/lib/common/command'); const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding'); const { OpenerService, open } = require('@theia/core/lib/browser/opener-service'); - const { EditorPreviewWidget } = require('@theia/editor-preview/lib/browser/editor-preview-widget'); const { animationFrame } = require('@theia/core/lib/browser/browser'); const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser/preferences/preference-service'); const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item'); @@ -157,7 +156,7 @@ module.exports = (port, host, argv) => Promise.resolve() */ async function openEditor(uri, preview = false) { const widget = await open(openerService, uri, { mode: 'activate', preview }); - const editorWidget = widget instanceof EditorPreviewWidget ? widget.editorWidget : widget instanceof EditorWidget ? widget : undefined; + const editorWidget = widget instanceof EditorWidget ? widget : undefined; const editor = MonacoEditor.get(editorWidget); assert.isDefined(editor); @@ -284,7 +283,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(editorManager.activeEditor.isPreview, preview); assert.equal(activeEditor.uri.toString(), serverUri.toString()); // const |container = new Container(); // @ts-ignore @@ -307,7 +306,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.isFalse(editorManager.activeEditor.isPreview); assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); // export { |Container } from "./container/container"; // @ts-ignore @@ -328,7 +327,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.isTrue(editorManager.activeEditor.isPreview); assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); // export { |Container } from "./container/container"; // @ts-ignore @@ -356,7 +355,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.equal(editorManager.activeEditor.parent instanceof EditorPreviewWidget, preview); + assert.equal(editorManager.activeEditor.isPreview, preview); assert.equal(activeEditor.uri.toString(), serverUri.toString()); // const |container = new Container(); // @ts-ignore @@ -382,7 +381,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.isFalse(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.isFalse(editorManager.activeEditor.isPreview); assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); // export { |Container } from "./container/container"; // @ts-ignore @@ -406,7 +405,7 @@ module.exports = (port, host, argv) => Promise.resolve() const activeEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editorManager.activeEditor)); // @ts-ignore - assert.isTrue(editorManager.activeEditor.parent instanceof EditorPreviewWidget); + assert.isTrue(editorManager.activeEditor.isPreview); assert.equal(activeEditor.uri.toString(), inversifyUri.toString()); // export { |Container } from "./container/container"; // @ts-ignore diff --git a/packages/core/src/browser/keybinding.ts b/packages/core/src/browser/keybinding.ts index 06b22cca8362f..d76fadcf4fa47 100644 --- a/packages/core/src/browser/keybinding.ts +++ b/packages/core/src/browser/keybinding.ts @@ -232,7 +232,7 @@ export class KeybindingRegistry { try { this.resolveKeybinding(binding); const scoped = Object.assign(binding, { scope }); - this.keymaps[scope].unshift(scoped); + this.insertBindingIntoScope(scoped, scope); return Disposable.create(() => { const index = this.keymaps[scope].indexOf(scoped); if (index !== -1) { @@ -245,6 +245,22 @@ export class KeybindingRegistry { } } + /** + * Ensures that keybindings are inserted in order of increasing length of binding to ensure that if a + * user triggers a short keybinding (e.g. ctrl+k), the UI won't wait for a longer one (e.g. ctrl+k enter) + */ + protected insertBindingIntoScope(item: common.Keybinding & { scope: KeybindingScope; }, scope: KeybindingScope): void { + const scopedKeymap = this.keymaps[scope]; + const getNumberOfKeystrokes = (binding: common.Keybinding): number => (binding.keybinding.trim().match(/\s/g)?.length ?? 0) + 1; + const numberOfKeystrokesInBinding = getNumberOfKeystrokes(item); + const indexOfFirstItemWithEqualStrokes = scopedKeymap.findIndex(existingBinding => getNumberOfKeystrokes(existingBinding) === numberOfKeystrokesInBinding); + if (indexOfFirstItemWithEqualStrokes > -1) { + scopedKeymap.splice(indexOfFirstItemWithEqualStrokes, 0, item); + } else { + scopedKeymap.push(item); + } + } + /** * Ensure that the `resolved` property of the given binding is set by calling the KeyboardLayoutService. */ diff --git a/packages/core/src/browser/shell/application-shell.ts b/packages/core/src/browser/shell/application-shell.ts index bffd40875e236..eb5b08dc63781 100644 --- a/packages/core/src/browser/shell/application-shell.ts +++ b/packages/core/src/browser/shell/application-shell.ts @@ -850,6 +850,17 @@ export class ApplicationShell extends Widget { return this.currentTabBar; } + /** + * @returns the widget whose title has been targeted by a DOM event on a tabbar, or undefined if none can be found. + */ + findTargetedWidget(event?: Event): Widget | undefined { + if (event) { + const tab = this.findTabBar(event); + const title = tab && this.findTitle(tab, event); + return title && title.owner; + } + } + /** * The current widget in the application shell. The current widget is the last widget that * was active and not yet closed. See the remarks to `activeWidget` on what _active_ means. diff --git a/packages/core/src/browser/widget-manager.ts b/packages/core/src/browser/widget-manager.ts index dfa7439206e92..e00c7e6cb82dd 100644 --- a/packages/core/src/browser/widget-manager.ts +++ b/packages/core/src/browser/widget-manager.ts @@ -165,6 +165,18 @@ export class WidgetManager { return undefined; } + /** + * Try to get the existing widget for the given description. + * @param factoryId The widget factory id. + * @param options The widget factory specific information. + * + * @returns A promise that resolves to the widget, if any exists. The promise may be pending, so be cautious when assuming that it will not reject. + */ + tryGetPendingWidget(factoryId: string, options?: any): MaybePromise | undefined { + const key = this.toKey({ factoryId, options }); + return this.doGetWidget(key); + } + /** * Get the widget for the given description. * @param factoryId The widget factory id. @@ -180,7 +192,7 @@ export class WidgetManager { } protected doGetWidget(key: string): MaybePromise | undefined { - const pendingWidget = this.widgetPromises.get(key) || this.pendingWidgetPromises.get(key); + const pendingWidget = this.widgetPromises.get(key) ?? this.pendingWidgetPromises.get(key); if (pendingWidget) { return pendingWidget as MaybePromise; } diff --git a/packages/core/src/browser/widget-open-handler.ts b/packages/core/src/browser/widget-open-handler.ts index c4d6cfdd2840e..b2f73ff7f0d66 100644 --- a/packages/core/src/browser/widget-open-handler.ts +++ b/packages/core/src/browser/widget-open-handler.ts @@ -136,6 +136,11 @@ export abstract class WidgetOpenHandler implements OpenHan return this.widgetManager.getWidgets(this.id) as W[]; } + protected tryGetPendingWidget(uri: URI, options?: WidgetOpenerOptions): MaybePromise | undefined { + const factoryOptions = this.createWidgetOptions(uri, options); + return this.widgetManager.tryGetPendingWidget(this.id, factoryOptions); + } + protected getWidget(uri: URI, options?: WidgetOpenerOptions): Promise { const widgetOptions = this.createWidgetOptions(uri, options); return this.widgetManager.getWidget(this.id, widgetOptions); diff --git a/packages/editor-preview/src/browser/editor-preview-contribution.ts b/packages/editor-preview/src/browser/editor-preview-contribution.ts new file mode 100644 index 0000000000000..6486bee4181aa --- /dev/null +++ b/packages/editor-preview/src/browser/editor-preview-contribution.ts @@ -0,0 +1,72 @@ +/******************************************************************************** + * Copyright (C) 2021 Ericsson 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 { ApplicationShell, KeybindingContribution, KeybindingRegistry, SHELL_TABBAR_CONTEXT_MENU, Widget } from '@theia/core/lib/browser'; +import { Command, CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry } from '@theia/core/lib/common'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { EditorPreviewWidget } from './editor-preview-widget'; + +export namespace EditorPreviewCommands { + export const PIN_PREVIEW_COMMAND: Command = { + id: 'workbench.action.keepEditor', + category: 'View', + label: 'Keep Editor', + }; +} + +@injectable() +export class EditorPreviewContribution implements CommandContribution, MenuContribution, KeybindingContribution { + @inject(ApplicationShell) protected readonly shell: ApplicationShell; + + registerCommands(registry: CommandRegistry): void { + registry.registerCommand(EditorPreviewCommands.PIN_PREVIEW_COMMAND, { + execute: async (event?: Event) => { + const widget = this.getTargetWidget(event); + if (widget instanceof EditorPreviewWidget) { + widget.convertToNonPreview(); + await this.shell.activateWidget(widget.id); + } + }, + isEnabled: (event?: Event) => { + const widget = this.getTargetWidget(event); + return widget instanceof EditorPreviewWidget && widget.isPreview; + }, + isVisible: (event?: Event) => { + const widget = this.getTargetWidget(event); + return widget instanceof EditorPreviewWidget; + } + }); + } + + registerKeybindings(registry: KeybindingRegistry): void { + registry.registerKeybinding({ + command: EditorPreviewCommands.PIN_PREVIEW_COMMAND.id, + keybinding: 'ctrlcmd+k enter' + }); + } + + registerMenus(registry: MenuModelRegistry): void { + registry.registerMenuAction(SHELL_TABBAR_CONTEXT_MENU, { + commandId: EditorPreviewCommands.PIN_PREVIEW_COMMAND.id, + label: 'Keep Open', + order: '6', + }); + } + + protected getTargetWidget(event?: Event): Widget | undefined { + return event ? this.shell.findTargetedWidget(event) : this.shell.activeWidget; + } +} diff --git a/packages/editor-preview/src/browser/editor-preview-factory.spec.ts b/packages/editor-preview/src/browser/editor-preview-factory.spec.ts deleted file mode 100644 index 12e0d2be5c477..0000000000000 --- a/packages/editor-preview/src/browser/editor-preview-factory.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Google 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 - ********************************************************************************/ - -// This file is strictly for testing; disable no-any so we can mock out objects not under test -// disable no-unused-expression for chai. -/* eslint-disable no-unused-expressions, @typescript-eslint/no-explicit-any */ - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -const disableJsDom = enableJSDOM(); - -import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; -import { ApplicationProps } from '@theia/application-package/lib/application-props'; -FrontendApplicationConfigProvider.set({ - ...ApplicationProps.DEFAULT.frontend.config -}); - -import { Container } from '@theia/core/shared/inversify'; -import { WidgetFactory, WidgetManager } from '@theia/core/lib/browser'; -import { EditorWidget, EditorManager } from '@theia/editor/lib/browser'; -import { EditorPreviewWidgetFactory, EditorPreviewWidgetOptions } from './editor-preview-factory'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import * as previewFrontEndModule from './editor-preview-frontend-module'; - -const mockEditorWidget = sinon.createStubInstance(EditorWidget); -const mockEditorManager = { - getOrCreateByUri: () => { } -}; -const getOrCreateStub = sinon.stub(mockEditorManager, 'getOrCreateByUri').returns(mockEditorWidget); - -let testContainer: Container; - -before(() => { - testContainer = new Container(); - // Mock out injected dependencies. - testContainer.bind(WidgetManager).toDynamicValue(ctx => ({} as any)); - testContainer.bind(EditorManager).toDynamicValue(ctx => (mockEditorManager as any)); - testContainer.load(previewFrontEndModule.default); -}); - -after(() => { - disableJsDom(); -}); - -describe('editor-preview-factory', () => { - let widgetFactory: EditorPreviewWidgetFactory; - - beforeEach(() => { - widgetFactory = testContainer.get(WidgetFactory); - getOrCreateStub.resetHistory(); - }); - - it('should create a new editor widget via editor manager if same session', async () => { - const opts: EditorPreviewWidgetOptions = { - kind: 'editor-preview-widget', - id: '1', - initialUri: 'file://a/b/c', - session: EditorPreviewWidgetFactory.sessionId - }; - const widget = await widgetFactory.createWidget(opts); - expect((mockEditorManager.getOrCreateByUri as sinon.SinonStub).calledOnce).to.be.true; - expect(widget.id).to.equal(opts.id); - expect(widget.editorWidget).to.equal(mockEditorWidget); - }); - - it('should not create a widget if restoring from previous session', async () => { - const opts: EditorPreviewWidgetOptions = { - kind: 'editor-preview-widget', - id: '2', - initialUri: 'file://a/b/c', - session: 'session-mismatch' - }; - const widget = await widgetFactory.createWidget(opts); - expect((mockEditorManager.getOrCreateByUri as sinon.SinonStub).called).to.be.false; - expect(widget.id).to.equal(opts.id); - expect(widget.editorWidget).to.be.undefined; - }); -}); diff --git a/packages/editor-preview/src/browser/editor-preview-factory.ts b/packages/editor-preview/src/browser/editor-preview-factory.ts deleted file mode 100644 index c8c1d84ca16b0..0000000000000 --- a/packages/editor-preview/src/browser/editor-preview-factory.ts +++ /dev/null @@ -1,62 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Google 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 URI from '@theia/core/lib/common/uri'; -import { WidgetFactory, WidgetManager } from '@theia/core/lib/browser'; -import { MaybePromise } from '@theia/core/lib/common/types'; -import { EditorPreviewWidget } from './editor-preview-widget'; -import { inject, injectable } from '@theia/core/shared/inversify'; -import { EditorManager } from '@theia/editor/lib/browser'; -import { UUID } from '@theia/core/shared/@phosphor/coreutils'; - -export interface EditorPreviewWidgetOptions { - kind: 'editor-preview-widget', - id: string, - initialUri: string, - session: string, -} - -@injectable() -export class EditorPreviewWidgetFactory implements WidgetFactory { - - static ID: string = 'editor-preview-widget'; - - static generateUniqueId(): string { - return UUID.uuid4(); - } - - readonly id = EditorPreviewWidgetFactory.ID; - static readonly sessionId = EditorPreviewWidgetFactory.generateUniqueId(); - - @inject(WidgetManager) - protected readonly widgetManager: WidgetManager; - - @inject(EditorManager) - protected readonly editorManager: EditorManager; - - createWidget(options: EditorPreviewWidgetOptions): MaybePromise { - return this.doCreate(options); - } - - protected async doCreate(options: EditorPreviewWidgetOptions): Promise { - const widget = (options.session === EditorPreviewWidgetFactory.sessionId) - ? await this.editorManager.getOrCreateByUri(new URI(options.initialUri)) - : undefined; - const previewWidget = new EditorPreviewWidget(this.widgetManager, widget); - previewWidget.id = options.id; - return previewWidget; - } -} diff --git a/packages/editor-preview/src/browser/editor-preview-frontend-module.ts b/packages/editor-preview/src/browser/editor-preview-frontend-module.ts index de819df09e544..14dccb11546dc 100644 --- a/packages/editor-preview/src/browser/editor-preview-frontend-module.ts +++ b/packages/editor-preview/src/browser/editor-preview-frontend-module.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Google and others. + * Copyright (C) 2018-2021 Google 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 @@ -14,20 +14,28 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { OpenHandler, WidgetFactory } from '@theia/core/lib/browser'; +import '../../src/browser/style/index.css'; +import { KeybindingContribution, WidgetFactory } from '@theia/core/lib/browser'; import { ContainerModule } from '@theia/core/shared/inversify'; -import { EditorPreviewManager } from './editor-preview-manager'; -import { EditorPreviewWidgetFactory } from './editor-preview-factory'; import { bindEditorPreviewPreferences } from './editor-preview-preferences'; +import { EditorPreviewManager } from './editor-preview-manager'; +import { EditorManager } from '@theia/editor/lib/browser'; +import { EditorPreviewWidgetFactory } from './editor-preview-widget-factory'; +import { EditorPreviewContribution } from './editor-preview-contribution'; +import { CommandContribution, MenuContribution } from '@theia/core/lib/common'; -import '../../src/browser/style/index.css'; - -export default new ContainerModule(bind => { +export default new ContainerModule((bind, unbind, isBound, rebind) => { - bind(WidgetFactory).to(EditorPreviewWidgetFactory).inSingletonScope(); + bind(EditorPreviewWidgetFactory).toSelf().inSingletonScope(); + bind(WidgetFactory).toService(EditorPreviewWidgetFactory); bind(EditorPreviewManager).toSelf().inSingletonScope(); - bind(OpenHandler).to(EditorPreviewManager); + rebind(EditorManager).toService(EditorPreviewManager); + + bind(EditorPreviewContribution).toSelf().inSingletonScope(); + bind(CommandContribution).toService(EditorPreviewContribution); + bind(KeybindingContribution).toService(EditorPreviewContribution); + bind(MenuContribution).toService(EditorPreviewContribution); bindEditorPreviewPreferences(bind); }); diff --git a/packages/editor-preview/src/browser/editor-preview-manager.spec.ts b/packages/editor-preview/src/browser/editor-preview-manager.spec.ts deleted file mode 100644 index 0c43e00c5f92c..0000000000000 --- a/packages/editor-preview/src/browser/editor-preview-manager.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Google 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 - ********************************************************************************/ - -// This file is strictly for testing; disable no-any so we can mock out objects not under test -// disable no-unused-expression for chai. -/* eslint-disable no-unused-expressions, @typescript-eslint/no-explicit-any */ - -import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom'; -const disableJsDom = enableJSDOM(); - -import URI from '@theia/core/lib/common/uri'; -import { Container } from '@theia/core/shared/inversify'; -import { EditorPreviewManager } from './editor-preview-manager'; -import { EditorPreviewWidget } from './editor-preview-widget'; -import { EditorPreviewWidgetFactory } from './editor-preview-factory'; -import { OpenHandler, PreferenceService, PreferenceServiceImpl } from '@theia/core/lib/browser'; -import { ApplicationShell, WidgetManager } from '@theia/core/lib/browser'; -import { EditorManager, EditorWidget } from '@theia/editor/lib/browser'; -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import * as previewFrontEndModule from './editor-preview-frontend-module'; - -const mockEditorWidget = sinon.createStubInstance(EditorWidget); -sinon.stub(mockEditorWidget, 'id').get(() => 'mockEditorWidget'); - -const mockPreviewWidget = sinon.createStubInstance(EditorPreviewWidget); -sinon.stub(mockPreviewWidget, 'id').get(() => 'mockPreviewWidget'); -sinon.stub(mockPreviewWidget, 'disposed').get(() => ({ connect: () => 1 })); -let onPinnedListeners: Function[] = []; -sinon.stub(mockPreviewWidget, 'onPinned').get(() => (fn: Function) => onPinnedListeners.push(fn)); - -const mockEditorManager = sinon.createStubInstance(EditorManager); -mockEditorManager.getOrCreateByUri = sinon.stub().returns(mockEditorWidget); - -const mockWidgetManager = sinon.createStubInstance(WidgetManager); -let onCreateListeners: Function[] = []; -mockWidgetManager.onDidCreateWidget = sinon.stub().callsFake((fn: Function) => onCreateListeners.push(fn)); -(mockWidgetManager.getOrCreateWidget as sinon.SinonStub).returns(mockPreviewWidget); - -const mockShell = sinon.createStubInstance(ApplicationShell) as ApplicationShell; - -const mockPreference = sinon.createStubInstance(PreferenceServiceImpl); -mockPreference.onPreferencesChanged = sinon.stub().returns({ dispose: () => { } }); - -let testContainer: Container; - -before(() => { - testContainer = new Container(); - // Mock out injected dependencies. - testContainer.bind(EditorManager).toDynamicValue(ctx => mockEditorManager); - testContainer.bind(WidgetManager).toDynamicValue(ctx => mockWidgetManager); - (mockShell)['tracker'] = { activeWidget: undefined }; - testContainer.bind(ApplicationShell).toConstantValue(mockShell); - testContainer.bind(PreferenceService).toDynamicValue(ctx => mockPreference); - - testContainer.load(previewFrontEndModule.default); -}); - -after(() => { - disableJsDom(); -}); - -describe('editor-preview-manager', () => { - let previewManager: EditorPreviewManager; - - beforeEach(() => { - previewManager = testContainer.get(OpenHandler); - sinon.stub(previewManager as any, 'onActive').resolves(); - sinon.stub(previewManager as any, 'onReveal').resolves(); - }); - afterEach(() => { - onCreateListeners = []; - onPinnedListeners = []; - }); - - it('should handle preview requests if editor.enablePreview enabled', async () => { - (mockPreference.get as sinon.SinonStub).returns(true); - expect(await previewManager.canHandle(new URI(), { preview: true })).to.be.greaterThan(0); - }); - it('should not handle preview requests if editor.enablePreview disabled', async () => { - (mockPreference.get as sinon.SinonStub).returns(false); - expect(await previewManager.canHandle(new URI(), { preview: true })).to.equal(0); - }); - it('should not handle requests that are not preview or currently being previewed', async () => { - expect(await previewManager.canHandle(new URI())).to.equal(0); - }); - it('should create a preview editor and replace where required.', async () => { - const w = await previewManager.open(new URI(), { preview: true }); - expect(w instanceof EditorPreviewWidget).to.be.true; - expect((w as any).replaceEditorWidget.calledOnce).to.be.false; - - // Replace the EditorWidget with another open call to an editor that doesn't exist. - const afterReplace = await previewManager.open(new URI(), { preview: true }); - expect((afterReplace as any).replaceEditorWidget.calledOnce).to.be.true; - - // Ensure the same preview widget was re-used. - expect(w).to.equal(afterReplace); - }); - it('Should return an existing editor on preview request', async () => { - // Activate existing editor - mockEditorManager.getByUri.returns(mockEditorWidget); - mockEditorManager.open.returns(mockEditorWidget); - expect(await previewManager.open(new URI(), {})).to.equal(mockEditorWidget); - - // Activate existing preview - mockEditorWidget.parent = mockPreviewWidget; - expect(await previewManager.open(new URI(), { preview: true })).to.equal(mockPreviewWidget); - // Ensure it is not pinned. - expect((mockPreviewWidget.pinEditorWidget as sinon.SinonStub).calledOnce).to.be.false; - - // Pin existing preview - expect(await previewManager.open(new URI(), {})).to.equal(mockPreviewWidget); - expect((mockPreviewWidget.pinEditorWidget as sinon.SinonStub).calledOnce).to.be.true; - }); - it('should should transition the editor to permanent on pin events.', async () => { - // Fake creation call. - // eslint-disable-next-line no-unsanitized/method - await onCreateListeners.pop()!({ factoryId: EditorPreviewWidgetFactory.ID, widget: mockPreviewWidget }); - // Fake pinned call - // eslint-disable-next-line no-unsanitized/method - onPinnedListeners.pop()!({ preview: mockPreviewWidget, editorWidget: mockEditorWidget }); - - expect(mockPreviewWidget.dispose.calledOnce).to.be.true; - expect(mockEditorWidget.close.calledOnce).to.be.false; - expect(mockEditorWidget.dispose.calledOnce).to.be.false; - }); - -}); diff --git a/packages/editor-preview/src/browser/editor-preview-manager.ts b/packages/editor-preview/src/browser/editor-preview-manager.ts index 72f6756fb7472..af88b69d8f5cb 100644 --- a/packages/editor-preview/src/browser/editor-preview-manager.ts +++ b/packages/editor-preview/src/browser/editor-preview-manager.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Google and others. + * Copyright (C) 2018-2021 Google 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 @@ -14,151 +14,112 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; -import URI from '@theia/core/lib/common/uri'; -import { ApplicationShell, DockPanel } from '@theia/core/lib/browser'; import { EditorManager, EditorOpenerOptions, EditorWidget } from '@theia/editor/lib/browser'; -import { EditorPreviewWidget } from './editor-preview-widget'; -import { EditorPreviewWidgetFactory, EditorPreviewWidgetOptions } from './editor-preview-factory'; +import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; import { EditorPreviewPreferences } from './editor-preview-preferences'; -import { WidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser'; - -/** - * Opener options containing an optional preview flag. - */ -export interface PreviewEditorOpenerOptions extends EditorOpenerOptions { - preview?: boolean -} +import { DisposableCollection, MaybePromise } from '@theia/core/lib/common'; +import URI from '@theia/core/lib/common/uri'; +import { EditorPreviewWidgetFactory, EditorPreviewOptions } from './editor-preview-widget-factory'; +import { EditorPreviewWidget } from './editor-preview-widget'; +import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state'; -/** - * Class for managing an editor preview widget. - */ @injectable() -export class EditorPreviewManager extends WidgetOpenHandler { - +export class EditorPreviewManager extends EditorManager { readonly id = EditorPreviewWidgetFactory.ID; - readonly label = 'Code Editor Preview'; - - protected currentEditorPreview: Promise; + @inject(EditorPreviewPreferences) protected readonly preferences: EditorPreviewPreferences; + @inject(FrontendApplicationStateService) protected readonly stateService: FrontendApplicationStateService; - @inject(EditorManager) - protected readonly editorManager: EditorManager; - - @inject(ApplicationShell) - protected readonly shell: ApplicationShell; - - @inject(EditorPreviewPreferences) - protected readonly preferences: EditorPreviewPreferences; + protected currentPreview: EditorPreviewWidget | undefined; + protected toDisposeOnPreviewChange = new DisposableCollection(); + /** + * Until the layout has been restored, widget state is not reliable, so we ignore creation events. + */ + protected layoutIsSet = false; @postConstruct() protected init(): void { super.init(); - this.onCreated(widget => { - if (widget instanceof EditorPreviewWidget) { - return this.handlePreviewWidgetCreated(widget); + // All editors are created, but not all are opened. This sets up the logic to swap previews when the editor is attached. + this.onCreated((widget: EditorPreviewWidget) => { + if (this.layoutIsSet && widget.isPreview) { + const oneTimeDisposable = widget.onDidChangeVisibility(() => { + const { currentPreview } = this; + this.handleNewPreview(widget); + currentPreview?.dispose(); + oneTimeDisposable.dispose(); + }); } }); this.preferences.onPreferenceChanged(change => { - if (this.currentEditorPreview) { - this.currentEditorPreview.then(editorPreview => { - if (!change.newValue && editorPreview) { - editorPreview.pinEditorWidget(); - } - }); - } + if (!change.newValue) { + this.currentPreview?.convertToNonPreview(); + }; }); - } - protected async handlePreviewWidgetCreated(widget: EditorPreviewWidget): Promise { - // Enforces only one preview widget exists at a given time. - const editorPreview = await this.currentEditorPreview; - if (editorPreview && editorPreview !== widget) { - editorPreview.pinEditorWidget(); - } + this.stateService.reachedState('initialized_layout').then(() => { + const editors = this.all as EditorPreviewWidget[]; + const currentPreview = editors.find(editor => editor.isPreview); + if (currentPreview) { + this.handleNewPreview(currentPreview); + } + this.layoutIsSet = true; + }); - this.currentEditorPreview = Promise.resolve(widget); - widget.disposed.connect(() => this.currentEditorPreview = Promise.resolve(undefined)); + document.addEventListener('dblclick', this.convertEditorOnDoubleClick.bind(this)); + } - widget.onPinned(({ preview, editorWidget }) => { - const wasActive = this.shell.activeWidget === preview || this.shell.activeWidget === editorWidget; - // TODO(caseyflynn): I don't believe there is ever a case where the parent will not be a DockPanel. - if (preview.parent && preview.parent instanceof DockPanel) { - preview.parent.addWidget(editorWidget, { ref: preview }); + protected async doOpen(widget: EditorPreviewWidget, options?: EditorOpenerOptions): Promise { + const { preview, widgetOptions = { area: 'main' }, mode = 'activate' } = options ?? {}; + if (!widget.isAttached) { + if (preview) { + const insertionOptions = this.currentPreview ? { ref: this.currentPreview } : widgetOptions; + await this.shell.addWidget(widget, insertionOptions); } else { - this.shell.addWidget(editorWidget, { area: 'main' }); - } - preview.dispose(); - if (wasActive) { - this.shell.activateWidget(editorWidget.id); + this.shell.addWidget(widget, widgetOptions); } - this.currentEditorPreview = Promise.resolve(undefined); - }); - } + } else if (!preview && widget === this.currentPreview) { + widget.convertToNonPreview(); + } - protected async isCurrentPreviewUri(uri: URI): Promise { - const editorPreview = await this.currentEditorPreview; - const currentUri = editorPreview && editorPreview.getResourceUri(); - return !!currentUri && currentUri.isEqualOrParent(uri); + if (mode === 'activate') { + await this.shell.activateWidget(widget.id); + } else if (mode === 'reveal') { + await this.shell.revealWidget(widget.id); + } } - async canHandle(uri: URI, options?: PreviewEditorOpenerOptions): Promise { - if (this.preferences['editor.enablePreview'] && (options && options.preview || await this.isCurrentPreviewUri(uri))) { - return 200; - } - return 0; + protected handleNewPreview(widget: EditorPreviewWidget): void { + this.toDisposeOnPreviewChange.dispose(); + this.currentPreview = widget; + this.toDisposeOnPreviewChange.push({ dispose: () => this.currentPreview = undefined }); + this.toDisposeOnPreviewChange.push(widget.onDidChangePreviewState(() => this.toDisposeOnPreviewChange.dispose())); + this.toDisposeOnPreviewChange.push(widget.onDidDispose(() => this.toDisposeOnPreviewChange.dispose())); } - async open(uri: URI, options: PreviewEditorOpenerOptions = {}): Promise { - let widget = await this.pinCurrentEditor(uri, options); - if (widget) { - return widget; - } - widget = await this.replaceCurrentPreview(uri, options) || await this.openNewPreview(uri, options); - await this.editorManager.open(uri, options); - return widget; + protected tryGetPendingWidget(uri: URI, options?: EditorOpenerOptions): MaybePromise | undefined { + return super.tryGetPendingWidget(uri, { ...options, preview: true }) ?? super.tryGetPendingWidget(uri, { ...options, preview: false }); } - protected async pinCurrentEditor(uri: URI, options: PreviewEditorOpenerOptions): Promise { - if (await this.editorManager.getByUri(uri)) { - const editorWidget = await this.editorManager.open(uri, options); - if (editorWidget.parent instanceof EditorPreviewWidget) { - if (!options.preview) { - editorWidget.parent.pinEditorWidget(); - } - return editorWidget.parent; - } - return editorWidget; - } + protected async getWidget(uri: URI, options?: EditorOpenerOptions): Promise { + return (await super.getWidget(uri, { ...options, preview: true })) ?? super.getWidget(uri, { ...options, preview: false }); } - protected async replaceCurrentPreview(uri: URI, options: PreviewEditorOpenerOptions): Promise { - const currentPreview = await this.currentEditorPreview; - if (currentPreview) { - const editorWidget = await this.editorManager.getOrCreateByUri(uri); - currentPreview.replaceEditorWidget(editorWidget); - return currentPreview; - } + protected async getOrCreateWidget(uri: URI, options?: EditorOpenerOptions): Promise { + return this.tryGetPendingWidget(uri, options) ?? super.getOrCreateWidget(uri, options); } - protected openNewPreview(uri: URI, options: PreviewEditorOpenerOptions): Promise { - const result = super.open(uri, options); - this.currentEditorPreview = result.then(widget => { - if (widget instanceof EditorPreviewWidget) { - return widget; - } - return undefined; - }, () => undefined); - return result; + protected createWidgetOptions(uri: URI, options?: EditorOpenerOptions): EditorPreviewOptions { + const navigatableOptions = super.createWidgetOptions(uri, options) as EditorPreviewOptions; + navigatableOptions.preview = !!(options?.preview && this.preferences['editor.enablePreview']); + return navigatableOptions; } - protected createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): EditorPreviewWidgetOptions { - return { - kind: 'editor-preview-widget', - id: EditorPreviewWidgetFactory.generateUniqueId(), - initialUri: uri.withoutFragment().toString(), - session: EditorPreviewWidgetFactory.sessionId - }; + protected convertEditorOnDoubleClick(event: Event): void { + const widget = this.shell.findTargetedWidget(event); + if (widget === this.currentPreview) { + this.currentPreview?.convertToNonPreview(); + } } } diff --git a/packages/editor-preview/src/browser/editor-preview-widget-factory.ts b/packages/editor-preview/src/browser/editor-preview-widget-factory.ts new file mode 100644 index 0000000000000..90de7cb2ddbc4 --- /dev/null +++ b/packages/editor-preview/src/browser/editor-preview-widget-factory.ts @@ -0,0 +1,45 @@ +/******************************************************************************** + * Copyright (C) 2018-2021 Google 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 URI from '@theia/core/lib/common/uri'; +import { EditorWidgetFactory } from '@theia/editor/lib/browser/editor-widget-factory'; +import { injectable } from '@theia/core/shared/inversify'; +import { EditorPreviewWidget } from './editor-preview-widget'; +import { NavigatableWidgetOptions } from '@theia/core/lib/browser'; + +export interface EditorPreviewOptions extends NavigatableWidgetOptions { + preview?: boolean; +} + +@injectable() +export class EditorPreviewWidgetFactory extends EditorWidgetFactory { + static ID: string = 'editor-preview-widget'; + readonly id = EditorPreviewWidgetFactory.ID; + + async createWidget(options: EditorPreviewOptions): Promise { + const uri = new URI(options.uri); + const editor = await this.createEditor(uri, options) as EditorPreviewWidget; + if (options.preview) { + editor.initializePreview(); + } + return editor; + } + + protected async constructEditor(uri: URI): Promise { + const textEditor = await this.editorProvider(uri); + return new EditorPreviewWidget(textEditor, this.selectionService); + } +} diff --git a/packages/editor-preview/src/browser/editor-preview-widget.ts b/packages/editor-preview/src/browser/editor-preview-widget.ts index b9813cab53df0..c65125d5db58c 100644 --- a/packages/editor-preview/src/browser/editor-preview-widget.ts +++ b/packages/editor-preview/src/browser/editor-preview-widget.ts @@ -1,5 +1,5 @@ /******************************************************************************** - * Copyright (C) 2018 Google and others. + * Copyright (C) 2021 Ericsson 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 @@ -14,214 +14,88 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { - ApplicationShell, BaseWidget, DockPanel, Navigatable, PanelLayout, Saveable, - StatefulWidget, Title, Widget, WidgetConstructionOptions, WidgetManager -} from '@theia/core/lib/browser'; -import { Emitter, DisposableCollection } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { EditorWidget } from '@theia/editor/lib/browser'; -import { Message, MessageLoop } from '@theia/core/shared/@phosphor/messaging'; +import { Message } from '@theia/core/shared/@phosphor/messaging'; +import { DockPanel, TabBar, Widget } from '@theia/core/lib/browser'; +import { EditorWidget, TextEditor } from '@theia/editor/lib/browser'; +import { Disposable, DisposableCollection, Emitter, SelectionService } from '@theia/core/lib/common'; import { find } from '@theia/core/shared/@phosphor/algorithm'; -export interface PreviewViewState { - pinned: boolean, - editorState: object | undefined, - previewDescription: WidgetConstructionOptions | undefined -} - -export interface PreviewEditorPinnedEvent { - preview: EditorPreviewWidget, - editorWidget: EditorWidget -} - -/** The class name added to Editor Preview Widget titles. */ -const PREVIEW_TITLE_CLASS = ' theia-editor-preview-title-unpinned'; - -export class EditorPreviewWidget extends BaseWidget implements ApplicationShell.TrackableWidgetProvider, Navigatable, StatefulWidget { +const PREVIEW_TITLE_CLASS = 'theia-editor-preview-title-unpinned'; +export class EditorPreviewWidget extends EditorWidget { + protected _isPreview = false; + protected lastTabbar: TabBar | undefined; - protected pinned_: boolean; - protected pinListeners = new DisposableCollection(); - protected onDidChangeTrackableWidgetsEmitter = new Emitter(); + protected readonly onDidChangePreviewStateEmitter = new Emitter(); + readonly onDidChangePreviewState = this.onDidChangePreviewStateEmitter.event; - private lastParent: DockPanel | undefined; - - readonly onDidChangeTrackableWidgets = this.onDidChangeTrackableWidgetsEmitter.event; - - protected onPinnedEmitter = new Emitter(); - - readonly onPinned = this.onPinnedEmitter.event; - - constructor(protected widgetManager: WidgetManager, protected editorWidget_?: EditorWidget) { - super(); - this.addClass('theia-editor-preview'); - this.title.closable = true; - this.title.className += PREVIEW_TITLE_CLASS; - this.layout = new PanelLayout(); - this.toDispose.push(this.onDidChangeTrackableWidgetsEmitter); - this.toDispose.push(this.onPinnedEmitter); - this.toDispose.push(this.pinListeners); - } - - get editorWidget(): EditorWidget | undefined { - return this.editorWidget_; - } - - get pinned(): boolean { - return this.pinned_; - } - - get saveable(): Saveable | undefined { - if (this.editorWidget_) { - return this.editorWidget_.saveable; - } - } - - getResourceUri(): URI | undefined { - return this.editorWidget_ && this.editorWidget_.getResourceUri(); - } - createMoveToUri(resourceUri: URI): URI | undefined { - return this.editorWidget_ && this.editorWidget_.createMoveToUri(resourceUri); - } + protected readonly toDisposeOnLocationChange = new DisposableCollection(); - pinEditorWidget(): void { - this.title.className = this.title.className.replace(PREVIEW_TITLE_CLASS, ''); - this.pinListeners.dispose(); - this.pinned_ = true; - this.onPinnedEmitter.fire({ preview: this, editorWidget: this.editorWidget_! }); + get isPreview(): boolean { + return this._isPreview; } - replaceEditorWidget(editorWidget: EditorWidget): void { - if (editorWidget === this.editorWidget_) { - return; - } - if (this.editorWidget_) { - this.editorWidget_.dispose(); - } - this.editorWidget_ = editorWidget; - this.attachPreviewWidget(this.editorWidget_); - this.onResize(Widget.ResizeMessage.UnknownSize); + constructor( + readonly editor: TextEditor, + protected readonly selectionService: SelectionService + ) { + super(editor, selectionService); + this.toDispose.push(this.onDidChangePreviewStateEmitter); + this.toDispose.push(this.toDisposeOnLocationChange); } - protected onActivateRequest(msg: Message): void { - super.onActivateRequest(msg); - if (this.editorWidget_) { - this.editorWidget_.activate(); - } + initializePreview(): void { + this._isPreview = true; + this.title.className += ` ${PREVIEW_TITLE_CLASS}`; + const oneTimeDirtyChangeListener = this.saveable.onDirtyChanged(() => { + this.convertToNonPreview(); + oneTimeDirtyChangeListener.dispose(); + }); + this.toDispose.push(oneTimeDirtyChangeListener); } - protected attachPreviewWidget(w: Widget): void { - (this.layout as PanelLayout).addWidget(w); - this.title.label = w.title.label; - this.title.iconClass = w.title.iconClass; - this.title.caption = w.title.caption; - - if (Saveable.isSource(w)) { - Saveable.apply(this); - const dirtyListener = w.saveable.onDirtyChanged(() => { - dirtyListener.dispose(); - this.pinEditorWidget(); - }); - this.toDispose.push(dirtyListener); + convertToNonPreview(): void { + if (this._isPreview) { + this._isPreview = false; + this.toDisposeOnLocationChange.dispose(); + this.lastTabbar = undefined; + this.title.className = this.title.className.replace(PREVIEW_TITLE_CLASS, ''); + this.onDidChangePreviewStateEmitter.fire(); + this.onDidChangePreviewStateEmitter.dispose(); } - w.parent = this; - this.onDidChangeTrackableWidgetsEmitter.fire([w]); } protected onAfterAttach(msg: Message): void { super.onAfterAttach(msg); - if (this.editorWidget_ && !this.editorWidget_.isAttached) { - this.attachPreviewWidget(this.editorWidget_); + if (this._isPreview) { + this.checkForTabbarChange(); } - this.addTabPinningLogic(); } - protected addTabPinningLogic(): void { - const parent = this.parent; - if (!this.pinned_ && parent instanceof DockPanel) { - if (!this.lastParent) { - this.lastParent = parent; + protected checkForTabbarChange(): void { + const { parent } = this; + if (parent instanceof DockPanel) { + this.toDisposeOnLocationChange.dispose(); + const newTabbar = find(parent.tabBars(), tabbar => !!tabbar.titles.find(title => title === this.title)); + if (this.lastTabbar && this.lastTabbar !== newTabbar) { + this.convertToNonPreview(); + } else { + this.lastTabbar = newTabbar; + const listener = () => this.checkForTabbarChange(); + parent.layoutModified.connect(listener); + this.toDisposeOnLocationChange.push(Disposable.create(() => parent.layoutModified.disconnect(listener))); } - - const tabBar = find(parent.tabBars(), bar => bar.titles.indexOf(this.title) !== -1); - - // Widget has been dragged into a different panel - if (this.lastParent !== parent || !tabBar) { - this.pinEditorWidget(); - return; - } - - const layoutListener = (panel: DockPanel) => { - if (tabBar !== find(panel.tabBars(), bar => bar.titles.indexOf(this.title) !== -1)) { - this.pinEditorWidget(); - } - }; - parent.layoutModified.connect(layoutListener); - this.pinListeners.push({ dispose: () => parent.layoutModified.disconnect(layoutListener) }); - - const tabMovedListener = (w: Widget, args: { title: Title }) => { - if (args.title === this.title) { - this.pinEditorWidget(); - } - }; - tabBar.tabMoved.connect(tabMovedListener); - this.pinListeners.push({ dispose: () => tabBar.tabMoved.disconnect(tabMovedListener) }); - - const attachDoubleClickListener = (attempt: number): number | undefined => { - const tabNode = tabBar.contentNode.children.item(tabBar.currentIndex); - if (!tabNode) { - return attempt < 60 ? requestAnimationFrame(() => attachDoubleClickListener(++attempt)) : undefined; - } - const dblClickListener = (event: Event) => this.pinEditorWidget(); - tabNode.addEventListener('dblclick', dblClickListener); - this.pinListeners.push({ dispose: () => tabNode.removeEventListener('dblclick', dblClickListener) }); - }; - requestAnimationFrame(() => attachDoubleClickListener(0)); - } - } - - protected onResize(msg: Widget.ResizeMessage): void { - if (this.editorWidget_) { - // Currently autosizing does not work with the Monaco Editor Widget - // https://github.com/eclipse-theia/theia/blob/c86a33b9ee0e5bb1dc49c66def123ffb2cadbfe4/packages/monaco/src/browser/monaco-editor.ts#L461 - // After this is supported we can rely on the underlying widget to resize and remove - // the following if statement. (Without it, the editor will be initialized to its - // minimum size) - if (msg.width < 0 || msg.height < 0) { - const width = parseInt(this.node.style.width || ''); - const height = parseInt(this.node.style.height || ''); - if (width && height) { - this.editorWidget_.editor.setSize({ width, height }); - } - } - MessageLoop.sendMessage(this.editorWidget_, msg); } } - getTrackableWidgets(): Widget[] { - return this.editorWidget_ ? [this.editorWidget_] : []; + storeState(): { isPreview: boolean, editorState: object } { + const { _isPreview: isPreview } = this; + return { isPreview, editorState: this.editor.storeViewState() }; } - storeState(): PreviewViewState { - return { - pinned: this.pinned_, - editorState: this.editorWidget_ ? this.editorWidget_.storeState() : undefined, - previewDescription: this.editorWidget_ ? this.widgetManager.getDescription(this.editorWidget_) : undefined - }; - } - - async restoreState(state: PreviewViewState): Promise { - const { pinned, editorState, previewDescription } = state; - if (!this.editorWidget_ && previewDescription) { - const { factoryId, options } = previewDescription; - const editorWidget = await this.widgetManager.getOrCreateWidget(factoryId, options) as EditorWidget; - this.replaceEditorWidget(editorWidget); - } - if (this.editorWidget && editorState) { - this.editorWidget.restoreState(editorState); - } - if (pinned) { - this.pinEditorWidget(); + restoreState(oldState: { isPreview: boolean, editorState: object }): void { + if (!oldState.isPreview) { + this.convertToNonPreview(); } + this.editor.restoreViewState(oldState.editorState); } } diff --git a/packages/editor-preview/src/browser/index.ts b/packages/editor-preview/src/browser/index.ts deleted file mode 100644 index 217243d7b6fa5..0000000000000 --- a/packages/editor-preview/src/browser/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -/******************************************************************************** - * Copyright (C) 2018 Google 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 - ********************************************************************************/ - -export * from './editor-preview-frontend-module'; -export * from './editor-preview-manager'; -export * from './editor-preview-widget'; diff --git a/packages/editor/src/browser/editor-manager.ts b/packages/editor/src/browser/editor-manager.ts index a4acf0515430a..1dc7499736e7c 100644 --- a/packages/editor/src/browser/editor-manager.ts +++ b/packages/editor/src/browser/editor-manager.ts @@ -16,8 +16,8 @@ import { injectable, postConstruct, inject } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; -import { RecursivePartial, Emitter, Event } from '@theia/core/lib/common'; -import { WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions } from '@theia/core/lib/browser'; +import { RecursivePartial, Emitter, Event, MaybePromise } from '@theia/core/lib/common'; +import { WidgetOpenerOptions, NavigatableWidgetOpenHandler, NavigatableWidgetOptions, Widget } from '@theia/core/lib/browser'; import { EditorWidget } from './editor-widget'; import { Range, Position, Location } from './editor'; import { EditorWidgetFactory } from './editor-widget-factory'; @@ -82,7 +82,7 @@ export class EditorManager extends NavigatableWidgetOpenHandler { this.updateCurrentEditor(); } - async getByUri(uri: URI, options?: EditorOpenerOptions): Promise { + getByUri(uri: URI, options?: EditorOpenerOptions): Promise { return this.getWidget(uri, options); } @@ -90,20 +90,30 @@ export class EditorManager extends NavigatableWidgetOpenHandler { return this.getOrCreateWidget(uri, options); } + protected tryGetPendingWidget(uri: URI, options?: EditorOpenerOptions): MaybePromise | undefined { + const editorPromise = super.tryGetPendingWidget(uri, options); + if (editorPromise) { + // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955) + if (!(editorPromise instanceof Widget)) { + editorPromise.then(editor => this.revealSelection(editor, options, uri)); + } else { + this.revealSelection(editorPromise, options); + } + } + return editorPromise; + } + protected async getWidget(uri: URI, options?: EditorOpenerOptions): Promise { - const optionsWithCounter: EditorOpenerOptions = { counter: this.getCounterForUri(uri), ...options }; - const editor = await super.getWidget(uri, optionsWithCounter); + const editor = await super.getWidget(uri, options); if (editor) { // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955) - this.revealSelection(editor, optionsWithCounter, uri); + this.revealSelection(editor, options, uri); } return editor; } protected async getOrCreateWidget(uri: URI, options?: EditorOpenerOptions): Promise { - const counter = options?.counter === undefined ? this.getOrCreateCounterForUri(uri) : options.counter; - const optionsWithCounter: EditorOpenerOptions = { ...options, counter }; - const editor = await super.getOrCreateWidget(uri, optionsWithCounter); + const editor = await super.getOrCreateWidget(uri, options); // Reveal selection before attachment to manage nav stack. (https://github.com/eclipse-theia/theia/issues/8955) this.revealSelection(editor, options, uri); return editor; @@ -292,9 +302,7 @@ export class EditorManager extends NavigatableWidgetOpenHandler { protected createWidgetOptions(uri: URI, options?: EditorOpenerOptions): NavigatableWidgetOptions { const navigatableOptions = super.createWidgetOptions(uri, options); - if (options?.counter !== undefined) { - navigatableOptions.counter = options.counter; - } + navigatableOptions.counter = options?.counter ?? this.getOrCreateCounterForUri(uri); return navigatableOptions; } } diff --git a/packages/editor/src/browser/editor-widget-factory.ts b/packages/editor/src/browser/editor-widget-factory.ts index 663bd1e7260c7..f9e007059ea31 100644 --- a/packages/editor/src/browser/editor-widget-factory.ts +++ b/packages/editor/src/browser/editor-widget-factory.ts @@ -49,8 +49,7 @@ export class EditorWidgetFactory implements WidgetFactory { } protected async createEditor(uri: URI, options?: NavigatableWidgetOptions): Promise { - const textEditor = await this.editorProvider(uri); - const newEditor = new EditorWidget(textEditor, this.selectionService); + const newEditor = await this.constructEditor(uri); this.setLabels(newEditor, uri); const labelListener = this.labelProvider.onDidChange(event => { @@ -66,11 +65,15 @@ export class EditorWidgetFactory implements WidgetFactory { return newEditor; } + protected async constructEditor(uri: URI): Promise { + const textEditor = await this.editorProvider(uri); + return new EditorWidget(textEditor, this.selectionService); + } + private setLabels(editor: EditorWidget, uri: URI): void { editor.title.caption = this.labelProvider.getLongName(uri); const icon = this.labelProvider.getIcon(uri); editor.title.label = this.labelProvider.getName(uri); editor.title.iconClass = icon + ' file-icon'; - } } diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index e17d31912bc20..14f9271262807 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -29,7 +29,6 @@ import { SelectableTreeNode, SHELL_TABBAR_CONTEXT_MENU, Widget, - Title } from '@theia/core/lib/browser'; import { FileDownloadCommands } from '@theia/filesystem/lib/browser/download/file-download-command-contribution'; import { @@ -265,15 +264,15 @@ export class FileNavigatorContribution extends AbstractViewContribution { - const widget = this.findTargetedWidget(event); + const widget = this.shell.findTargetedWidget(event); this.openView({ activate: true }).then(() => this.selectWidgetFileNode(widget || this.shell.currentWidget)); }, isEnabled: (event?: Event) => { - const widget = this.findTargetedWidget(event); + const widget = this.shell.findTargetedWidget(event); return widget ? Navigatable.is(widget) : Navigatable.is(this.shell.currentWidget); }, isVisible: (event?: Event) => { - const widget = this.findTargetedWidget(event); + const widget = this.shell.findTargetedWidget(event); return widget ? Navigatable.is(widget) : Navigatable.is(this.shell.currentWidget); } }); @@ -558,19 +557,6 @@ export class FileNavigatorContribution extends AbstractViewContribution | undefined; - if (event) { - const tab = this.shell.findTabBar(event); - title = tab && this.shell.findTitle(tab, event); - } - return title && title.owner; - } - /** * Reveals and selects node in the file navigator to which given widget is related. * Does nothing if given widget undefined or doesn't have related resource. diff --git a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts index c471856b1906f..7b5ad6809ecfa 100755 --- a/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts +++ b/packages/plugin-ext-vscode/src/browser/plugin-vscode-commands-contribution.ts @@ -418,7 +418,6 @@ export class PluginVscodeCommandsContribution implements CommandContribution { /** * TODO: - * Keep Open: workbench.action.keepEditor * Open Next: workbench.action.openNextRecentlyUsedEditorInGroup * Open Previous: workbench.action.openPreviousRecentlyUsedEditorInGroup * Copy Path of Active File: workbench.action.files.copyPathOfActiveFile