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