deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
{
"name": "@theia/api-tests",
"version": "1.68.0",
"description": "Theia API tests",
"dependencies": {
"@theia/core": "1.68.0"
},
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"src"
],
"publishConfig": {
"access": "public"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

21
examples/api-tests/src/api-tests.d.ts vendored Normal file
View File

@@ -0,0 +1,21 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
interface Window {
theia: {
container: import('inversify').Container
}
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('animationFrame', function () {
this.timeout(5_000);
const { assert } = chai;
const { animationFrame } = require('@theia/core/lib/browser/browser');
class FrameCounter {
constructor() {
this.count = 0;
this.stop = false;
this.run();
}
run() {
requestAnimationFrame(this.nextFrame.bind(this));
}
nextFrame() {
this.count++;
if (!this.stop) {
this.run();
}
}
}
it('should resolve after one frame', async () => {
const counter = new FrameCounter();
await animationFrame();
counter.stop = true;
assert.equal(counter.count, 1);
});
it('should resolve after the given number of frames', async () => {
const counter = new FrameCounter();
await animationFrame(10);
counter.stop = true;
assert.equal(counter.count, 10);
});
});

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2021 STMicroelectronics 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Contribution filter', function () {
this.timeout(5000);
const { assert } = chai;
const { CommandRegistry, CommandContribution } = require('@theia/core/lib/common/command');
const { SampleFilteredCommandContribution, SampleFilteredCommand } = require('@theia/api-samples/lib/browser/contribution-filter/sample-filtered-command-contribution');
const container = window.theia.container;
const commands = container.get(CommandRegistry);
it('filtered command in container but not in registry', async function () {
const allCommands = container.getAll(CommandContribution);
assert.isDefined(allCommands.find(contribution => contribution instanceof SampleFilteredCommandContribution),
'SampleFilteredCommandContribution is not bound in container');
const filteredCommand = commands.getCommand(SampleFilteredCommand.FILTERED.id);
assert.isUndefined(filteredCommand, 'SampleFilteredCommandContribution should be filtered out but is present in "CommandRegistry"');
});
});

View File

@@ -0,0 +1,76 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('CredentialsService', function () {
this.timeout(5000);
const { assert } = chai;
const { CredentialsService } = require('@theia/core/lib/browser/credentials-service');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
/** @type {import('@theia/core/lib/browser/credentials-service').CredentialsService} */
const credentials = container.get(CredentialsService);
const serviceName = 'theia-test';
const accountName = 'test-account';
const password = 'test-password';
this.beforeEach(async () => {
await credentials.deletePassword(serviceName, accountName);
});
it('can set and retrieve stored credentials', async function () {
await credentials.setPassword(serviceName, accountName, password);
const storedPassword = await credentials.getPassword(serviceName, accountName);
assert.strictEqual(storedPassword, password);
});
it('can retrieve all account keys for a service', async function () {
// Initially, there should be no keys for the service
let keys = await credentials.keys(serviceName);
assert.strictEqual(keys.length, 0);
// Add a single credential
await credentials.setPassword(serviceName, accountName, password);
keys = await credentials.keys(serviceName);
assert.strictEqual(keys.length, 1);
assert.include(keys, accountName);
// Add more credentials with different account names
const accountName2 = 'test-account-2';
const accountName3 = 'test-account-3';
await credentials.setPassword(serviceName, accountName2, 'password2');
await credentials.setPassword(serviceName, accountName3, 'password3');
keys = await credentials.keys(serviceName);
assert.strictEqual(keys.length, 3);
assert.include(keys, accountName);
assert.include(keys, accountName2);
assert.include(keys, accountName3);
// Clean up all accounts
await credentials.deletePassword(serviceName, accountName);
await credentials.deletePassword(serviceName, accountName2);
await credentials.deletePassword(serviceName, accountName3);
// Verify keys are removed after deletion
keys = await credentials.keys(serviceName);
assert.strictEqual(keys.length, 0);
});
});

View File

@@ -0,0 +1,137 @@
// *****************************************************************************
// Copyright (C) 2023 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Explorer and Editor - open and close', function () {
this.timeout(90_000);
const { assert } = chai;
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const navigatorContribution = container.get(FileNavigatorContribution);
const shell = container.get(ApplicationShell);
const rootUri = workspaceService.tryGetRoots()[0].resource;
const pluginService = container.get(HostedPluginSupport);
const progressStatusBarItem = container.get(ProgressStatusBarItem);
const fileUri = rootUri.resolve('webpack.config.js');
const toTearDown = new DisposableCollection();
function pause(ms = 500) {
console.debug(`pause test for: ${ms} ms`);
return new Promise(resolve => setTimeout(resolve, ms));
}
before(async () => {
await pluginService.didStart;
await editorManager.closeAll({ save: false });
});
afterEach(async () => {
await editorManager.closeAll({ save: false });
await navigatorContribution.closeView();
});
after(async () => {
toTearDown.dispose();
});
for (var i = 0; i < 5; i++) {
let ordering = 0;
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
await openExplorer();
await openEditor();
await closeEditor();
await closeExplorer();
});
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
await openExplorer();
await openEditor();
await closeExplorer();
await closeEditor();
});
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
await openEditor();
await openExplorer();
await closeEditor();
await closeExplorer();
});
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
await openEditor();
await openExplorer();
await closeExplorer();
await closeEditor();
});
it('Open/Close explorer #' + i, async function () {
await openExplorer();
await closeExplorer();
});
}
it('open/close explorer in quick succession', async function () {
for (let i = 0; i < 20; i++) {
await openExplorer();
await closeExplorer();
}
});
it('open/close editor in quick succession', async function () {
await openExplorer();
for (let i = 0; i < 20; i++) {
await openEditor();
await closeEditor();
}
});
async function openExplorer() {
await navigatorContribution.openView({ activate: true });
const widget = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
assert.isDefined(widget, 'Explorer widget should exist');
}
async function closeExplorer() {
await navigatorContribution.closeView();
assert.isUndefined(await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID), 'Explorer widget should not exist');
}
async function openEditor() {
await editorManager.open(fileUri, { mode: 'activate' });
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isDefined(activeEditor);
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
}
async function closeEditor() {
await editorManager.closeAll({ save: false });
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isUndefined(activeEditor);
}
});

View File

@@ -0,0 +1,133 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('file-search', function () {
const { assert } = chai;
const Uri = require('@theia/core/lib/common/uri');
const { QuickFileOpenService } = require('@theia/file-search/lib/browser/quick-file-open');
const { QuickFileSelectService } = require('@theia/file-search/lib/browser/quick-file-select-service');
const { CancellationTokenSource } = require('@theia/core/lib/common/cancellation');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const quickFileOpenService = container.get(QuickFileOpenService);
const quickFileSelectService = container.get(QuickFileSelectService);
describe('quick-file-open', () => {
describe('#compareItems', () => {
const sortByCompareItems = (a, b) => quickFileSelectService['compareItems'](a, b, quickFileOpenService['filterAndRange'].filter);
it('should compare two quick-open-items by `label`', () => {
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: 'a', uri: new Uri.default('b') };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: 'b', uri: new Uri.default('a') };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(quickFileSelectService['compareItems'](a, a, quickFileOpenService['filterAndRange'].filter), 0, 'items should be equal');
});
it('should compare two quick-open-items by `uri`', () => {
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: 'a', uri: new Uri.default('a') };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: 'a', uri: new Uri.default('b') };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
});
it('should not place very good matches above exact matches', () => {
const exactMatch = 'almost_absurdly_long_file_name_with_many_parts.file';
const veryGoodMatch = 'almost_absurdly_long_file_name_with_many_parts_plus_one.file';
quickFileOpenService['filterAndRange'] = { filter: exactMatch };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: exactMatch, uri: new Uri.default(exactMatch) };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: veryGoodMatch, uri: new Uri.default(veryGoodMatch) };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
quickFileOpenService['filterAndRange'] = quickFileOpenService['filterAndRangeDefault'];
});
});
describe('#filterAndRange', () => {
it('should return the default when not searching', () => {
const filterAndRange = quickFileOpenService['filterAndRange'];
assert.equal(filterAndRange, quickFileOpenService['filterAndRangeDefault']);
});
it('should update when searching', () => {
quickFileOpenService['getPicks']('a:2:1', new CancellationTokenSource().token); // perform a mock search.
const filterAndRange = quickFileOpenService['filterAndRange'];
assert.equal(filterAndRange.filter, 'a');
assert.deepEqual(filterAndRange.range, { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } });
});
});
describe('#splitFilterAndRange', () => {
const expression1 = 'a:2:1';
const expression2 = 'a:2,1';
const expression3 = 'a:2#2';
const expression4 = 'a#2:2';
const expression5 = 'a#2,1';
const expression6 = 'a#2#2';
const expression7 = 'a:2';
const expression8 = 'a#2';
it('should split the filter correctly for different combinations', () => {
assert.equal((quickFileOpenService['splitFilterAndRange'](expression1).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression2).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression3).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression4).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression5).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression6).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression7).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression8).filter), 'a');
});
it('should split the range correctly for different combinations', () => {
const rangeTest1 = { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } };
const rangeTest2 = { start: { line: 1, character: 1 }, end: { line: 1, character: 1 } };
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression1).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression2).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression3).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression4).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression5).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression6).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression7).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression8).range, rangeTest1);
});
});
});
});

View File

@@ -0,0 +1,151 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Find and Replace', function () {
this.timeout(20_000);
const { assert } = chai;
const { animationFrame } = require('@theia/core/lib/browser/browser');
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { CommandRegistry } = require('@theia/core/lib/common/command');
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const commands = container.get(CommandRegistry);
const keybindings = container.get(KeybindingRegistry);
const contextKeyService = container.get(ContextKeyService);
const navigatorContribution = container.get(FileNavigatorContribution);
const shell = container.get(ApplicationShell);
const rootUri = workspaceService.tryGetRoots()[0].resource;
const pluginService = container.get(HostedPluginSupport);
const progressStatusBarItem = container.get(ProgressStatusBarItem);
const fileUri = rootUri.resolve('../api-tests/test-ts-workspace/demo-file.ts');
const toTearDown = new DisposableCollection();
function pause(ms = 500) {
console.debug(`pause test for: ${ms} ms`);
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* @template T
* @param {() => Promise<T> | T} condition
* @returns {Promise<T>}
*/
function waitForAnimation(condition) {
return new Promise(async (resolve, dispose) => {
toTearDown.push({ dispose });
do {
await animationFrame();
} while (!condition());
resolve();
});
}
before(async () => {
await pluginService.didStart;
await shell.leftPanelHandler.collapse();
await editorManager.closeAll({ save: false });
});
beforeEach(async function () {
await navigatorContribution.closeView();
});
afterEach(async () => {
await editorManager.closeAll({ save: false });
});
after(async () => {
await shell.leftPanelHandler.collapse();
toTearDown.dispose();
});
/**
* @param {import('@theia/core/lib/common/command').Command} command
*/
async function assertEditorFindReplace(command) {
assert.isFalse(contextKeyService.match('findWidgetVisible'));
assert.isFalse(contextKeyService.match('findInputFocussed'));
assert.isFalse(contextKeyService.match('replaceInputFocussed'));
keybindings.dispatchCommand(command.id);
await waitForAnimation(() => contextKeyService.match('findInputFocussed'));
assert.isTrue(contextKeyService.match('findWidgetVisible'));
assert.isTrue(contextKeyService.match('findInputFocussed'));
assert.isFalse(contextKeyService.match('replaceInputFocussed'));
keybindings.dispatchKeyDown('Tab');
await waitForAnimation(() => !contextKeyService.match('findInputFocussed'));
assert.isTrue(contextKeyService.match('findWidgetVisible'));
assert.isFalse(contextKeyService.match('findInputFocussed'));
assert.equal(contextKeyService.match('replaceInputFocussed'), command === CommonCommands.REPLACE);
}
for (const command of [CommonCommands.FIND, CommonCommands.REPLACE]) {
it(command.label + ' in the active editor', async function () {
await openExplorer();
await openEditor();
await assertEditorFindReplace(command);
});
it(command.label + ' in the active explorer without the current editor', async function () {
await openExplorer();
// should not throw
await commands.executeCommand(command.id);
});
it(command.label + ' in the active explorer with the current editor', async function () {
await openEditor();
await openExplorer();
await assertEditorFindReplace(command);
});
}
async function openExplorer() {
await navigatorContribution.openView({ activate: true });
const widget = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
assert.isDefined(widget, 'Explorer widget should exist');
}
async function openEditor() {
await editorManager.open(fileUri, { mode: 'activate' });
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isDefined(activeEditor);
// @ts-ignore
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
}
});

View File

@@ -0,0 +1,116 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Keybindings', function () {
const { assert } = chai;
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
const { isOSX } = require('@theia/core/lib/common/os');
const { CommonCommands } = require('@theia/core/lib/browser/common-commands');
const { TerminalService } = require('@theia/terminal/lib/browser/base/terminal-service');
const { TerminalCommands } = require('@theia/terminal/lib/browser/terminal-frontend-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
const { CommandRegistry } = require('@theia/core/lib/common/command');
const { Deferred } = require('@theia/core/lib/common/promise-util');
const { Key } = require('@theia/core/lib/browser/keys');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
/** @type {import('@theia/terminal/lib/browser/base/terminal-service').TerminalService} */
const terminalService = container.get(TerminalService);
const applicationShell = container.get(ApplicationShell);
const keybindings = container.get(KeybindingRegistry);
const commands = container.get(CommandRegistry);
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const toTearDown = new DisposableCollection();
afterEach(() => toTearDown.dispose());
it('partial keybinding should not override full in the same scope', async () => {
const terminal = /** @type {import('@theia/terminal/lib/browser/terminal-widget-impl').TerminalWidgetImpl} */
(await terminalService.newTerminal({}));
toTearDown.push(Disposable.create(() => terminal.dispose()));
terminalService.open(terminal, { mode: 'activate' });
await applicationShell.waitForActivation(terminal.id);
const waitForCommand = new Deferred();
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
keybindings.dispatchKeyDown({
code: Key.KEY_K.code,
metaKey: isOSX,
ctrlKey: !isOSX
}, terminal.node);
const executedCommand = await waitForCommand.promise;
assert.equal(executedCommand, TerminalCommands.TERMINAL_CLEAR.id);
});
it('disabled keybinding should not override enabled', async () => {
const id = '__test:keybindings.left';
toTearDown.push(commands.registerCommand({ id }, {
execute: () => { }
}));
toTearDown.push(keybindings.registerKeybinding({
command: id,
keybinding: 'left',
when: 'false'
}));
const editor = await editorManager.open(workspaceService.tryGetRoots()[0].resource.resolve('webpack.config.js'), {
mode: 'activate',
selection: {
start: {
line: 0,
character: 1
}
}
});
toTearDown.push(editor);
const waitForCommand = new Deferred();
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
keybindings.dispatchKeyDown({
code: Key.ARROW_LEFT.code
}, editor.node);
const executedCommand = await waitForCommand.promise;
assert.notEqual(executedCommand, id);
});
it('later registered keybinding should have higher priority', async () => {
const id = '__test:keybindings.copy';
toTearDown.push(commands.registerCommand({ id }, {
execute: () => { }
}));
const keybinding = keybindings.getKeybindingsForCommand(CommonCommands.COPY.id)[0];
toTearDown.push(keybindings.registerKeybinding({
command: id,
keybinding: keybinding.keybinding
}));
const waitForCommand = new Deferred();
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
keybindings.dispatchKeyDown({
code: Key.KEY_C.code,
metaKey: isOSX,
ctrlKey: !isOSX
});
const executedCommand = await waitForCommand.promise;
assert.equal(executedCommand, id);
});
});

View File

@@ -0,0 +1,731 @@
// *****************************************************************************
// Copyright (C) 2019 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
/* @typescript-eslint/no-explicit-any */
/**
* @typedef {'.vscode' | '.theia' | ['.theia', '.vscode']} ConfigMode
*/
/**
* Expectations should be tested and aligned against VS Code.
* See https://github.com/akosyakov/vscode-launch/blob/master/src/test/extension.test.ts
*/
describe('Launch Preferences', function () {
this.timeout(30_000);
const { assert } = chai;
const { PreferenceProvider } = require('@theia/core/lib/common');
const { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
const { PreferenceScope } = require('@theia/core/lib/common/preferences/preference-scope');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
const { FileResourceResolver } = require('@theia/filesystem/lib/browser/file-resource');
const { AbstractResourcePreferenceProvider } = require('@theia/preferences/lib/common/abstract-resource-preference-provider');
const { waitForEvent } = require('@theia/core/lib/common/promise-util');
const container = window.theia.container;
/** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
const preferences = container.get(PreferenceService);
/** @type {import('@theia/preferences/lib/browser/user-configs-preference-provider').UserConfigsPreferenceProvider} */
const userPreferences = container.getNamed(PreferenceProvider, PreferenceScope.User);
/** @type {import('@theia/preferences/lib/browser/workspace-preference-provider').WorkspacePreferenceProvider} */
const workspacePreferences = container.getNamed(PreferenceProvider, PreferenceScope.Workspace);
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
const workspaceService = container.get(WorkspaceService);
const fileService = container.get(FileService);
const fileResourceResolver = container.get(FileResourceResolver);
const defaultLaunch = {
'configurations': [],
'compounds': []
};
const validConfiguration = {
'name': 'Launch Program',
'program': '${file}',
'request': 'launch',
'type': 'node',
};
const validConfiguration2 = {
'name': 'Launch Program 2',
'program': '${file}',
'request': 'launch',
'type': 'node',
};
const bogusConfiguration = {};
const validCompound = {
'name': 'Compound',
'configurations': [
'Launch Program',
'Launch Program 2'
]
};
const bogusCompound = {};
const bogusCompound2 = {
'name': 'Compound 2',
'configurations': [
'Foo',
'Launch Program 2'
]
};
const validLaunch = {
configurations: [validConfiguration, validConfiguration2],
compounds: [validCompound]
};
testSuite({
name: 'No Preferences',
expectation: defaultLaunch
});
testLaunchAndSettingsSuite({
name: 'Empty With Version',
launch: {
'version': '0.2.0'
},
expectation: {
'version': '0.2.0',
'configurations': [],
'compounds': []
}
});
testLaunchAndSettingsSuite({
name: 'Empty With Version And Configurations',
launch: {
'version': '0.2.0',
'configurations': [],
},
expectation: {
'version': '0.2.0',
'configurations': [],
'compounds': []
}
});
testLaunchAndSettingsSuite({
name: 'Empty With Version And Compounds',
launch: {
'version': '0.2.0',
'compounds': []
},
expectation: {
'version': '0.2.0',
'configurations': [],
'compounds': []
}
});
testLaunchAndSettingsSuite({
name: 'Valid Conf',
launch: {
'version': '0.2.0',
'configurations': [validConfiguration]
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration],
'compounds': []
}
});
testLaunchAndSettingsSuite({
name: 'Bogus Conf',
launch: {
'version': '0.2.0',
'configurations': [validConfiguration, bogusConfiguration]
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration, bogusConfiguration],
'compounds': []
}
});
testLaunchAndSettingsSuite({
name: 'Completely Bogus Conf',
launch: {
'version': '0.2.0',
'configurations': { 'valid': validConfiguration, 'bogus': bogusConfiguration }
},
expectation: {
'version': '0.2.0',
'configurations': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
'compounds': []
}
});
const arrayBogusLaunch = [
'version', '0.2.0',
'configurations', { 'valid': validConfiguration, 'bogus': bogusConfiguration }
];
testSuite({
name: 'Array Bogus Launch Configuration',
launch: arrayBogusLaunch,
expectation: {
'0': 'version',
'1': '0.2.0',
'2': 'configurations',
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
'compounds': [],
'configurations': []
},
inspectExpectation: {
preferenceName: 'launch',
defaultValue: defaultLaunch,
workspaceValue: {
'0': 'version',
'1': '0.2.0',
'2': 'configurations',
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration }
}
}
});
testSuite({
name: 'Array Bogus Settings Configuration',
settings: {
launch: arrayBogusLaunch
},
expectation: {
'0': 'version',
'1': '0.2.0',
'2': 'configurations',
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
'compounds': [],
'configurations': []
},
inspectExpectation: {
preferenceName: 'launch',
defaultValue: defaultLaunch,
workspaceValue: arrayBogusLaunch
}
});
testSuite({
name: 'Null Bogus Launch Configuration',
// eslint-disable-next-line no-null/no-null
launch: null,
expectation: {
'compounds': [],
'configurations': []
}
});
testSuite({
name: 'Null Bogus Settings Configuration',
settings: {
// eslint-disable-next-line no-null/no-null
'launch': null
},
expectation: {}
});
testLaunchAndSettingsSuite({
name: 'Valid Compound',
launch: {
'version': '0.2.0',
'configurations': [validConfiguration, validConfiguration2],
'compounds': [validCompound]
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration, validConfiguration2],
'compounds': [validCompound]
}
});
testLaunchAndSettingsSuite({
name: 'Valid And Bogus',
launch: {
'version': '0.2.0',
'configurations': [validConfiguration, validConfiguration2, bogusConfiguration],
'compounds': [validCompound, bogusCompound, bogusCompound2]
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration, validConfiguration2, bogusConfiguration],
'compounds': [validCompound, bogusCompound, bogusCompound2]
}
});
testSuite({
name: 'Mixed',
launch: {
'version': '0.2.0',
'configurations': [validConfiguration, bogusConfiguration],
'compounds': [bogusCompound, bogusCompound2]
},
settings: {
launch: {
'version': '0.2.0',
'configurations': [validConfiguration2],
'compounds': [validCompound]
}
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration2, validConfiguration, bogusConfiguration],
'compounds': [validCompound, bogusCompound, bogusCompound2]
}
});
testSuite({
name: 'Mixed Launch Without Configurations',
launch: {
'version': '0.2.0',
'compounds': [bogusCompound, bogusCompound2]
},
settings: {
launch: {
'version': '0.2.0',
'configurations': [validConfiguration2],
'compounds': [validCompound]
}
},
expectation: {
'version': '0.2.0',
'configurations': [validConfiguration2],
'compounds': [validCompound, bogusCompound, bogusCompound2]
},
inspectExpectation: {
preferenceName: 'launch',
defaultValue: defaultLaunch,
workspaceValue: {
'version': '0.2.0',
'configurations': [validConfiguration2],
'compounds': [validCompound, bogusCompound, bogusCompound2]
}
}
});
/**
* @typedef {Object} LaunchAndSettingsSuiteOptions
* @property {string} name
* @property {any} expectation
* @property {any} [launch]
* @property {boolean} [only]
* @property {ConfigMode} [configMode]
*/
/**
* @type {(options: LaunchAndSettingsSuiteOptions) => void}
*/
function testLaunchAndSettingsSuite({
name, expectation, launch, only, configMode
}) {
testSuite({
name: name + ' Launch Configuration',
launch,
expectation,
only,
configMode
});
testSuite({
name: name + ' Settings Configuration',
settings: {
'launch': launch
},
expectation,
only,
configMode
});
}
/**
* @typedef {Partial<import('@theia/core/src/browser/preferences/preference-service').PreferenceInspection<any>>} PreferenceInspection
*/
/**
* @typedef {Object} SuiteOptions
* @property {string} name
* @property {any} expectation
* @property {PreferenceInspection} [inspectExpectation]
* @property {any} [launch]
* @property {any} [settings]
* @property {boolean} [only]
* @property {ConfigMode} [configMode]
*/
/**
* @type {(options: SuiteOptions) => void}
*/
function testSuite(options) {
describe(options.name, () => {
if (options.configMode) {
testConfigSuite(options);
} else {
testConfigSuite({
...options,
configMode: '.theia'
});
if (options.settings || options.launch) {
testConfigSuite({
...options,
configMode: '.vscode'
});
testConfigSuite({
...options,
configMode: ['.theia', '.vscode']
});
}
}
});
}
const rootUri = workspaceService.tryGetRoots()[0].resource;
/**
* @param uri the URI of the file to modify
* @returns {AbstractResourcePreferenceProvider | undefined} The preference provider matching the provided URI.
*/
function findProvider(uri) {
/**
* @param {PreferenceProvider} provider
* @returns {boolean} whether the provider matches the desired URI.
*/
const isMatch = (provider) => {
const configUri = provider.getConfigUri();
return configUri && uri.isEqual(configUri);
};
for (const provider of userPreferences['providers'].values()) {
if (isMatch(provider) && provider instanceof AbstractResourcePreferenceProvider) {
return provider;
}
}
for (const provider of folderPreferences['providers'].values()) {
if (isMatch(provider) && provider instanceof AbstractResourcePreferenceProvider) {
return provider;
}
}
/** @type {PreferenceProvider} */
const workspaceDelegate = workspacePreferences['delegate'];
if (workspaceDelegate !== folderPreferences) {
if (isMatch(workspaceDelegate) && workspaceDelegate instanceof AbstractResourcePreferenceProvider) {
return workspaceDelegate;
}
}
}
async function deleteWorkspacePreferences() {
const promises = [];
for (const configPath of ['.theia', '.vscode']) {
for (const name of ['settings', 'launch']) {
promises.push((async () => {
const uri = rootUri.resolve(configPath + '/' + name + '.json');
const provider = findProvider(uri);
try {
if (provider) {
if (provider.valid) {
try {
await waitForEvent(provider.onDidChangeValidity, 1000);
} catch (e) {
console.log('timed out waiting for validity change'); // sometimes, we seen to miss events: https://github.com/eclipse-theia/theia/issues/16088
}
}
await provider['readPreferencesFromFile']();
await provider['fireDidPreferencesChanged']();
} else {
console.log('Unable to find provider for', uri.path.toString());
}
} catch (e) {
console.error(e);
}
})());
}
}
await fileService.delete(rootUri.resolve('.theia'), { fromUserGesture: false, recursive: true }).catch(() => { });
await fileService.delete(rootUri.resolve('.vscode'), { fromUserGesture: false, recursive: true }).catch(() => { });
await Promise.all(promises);
}
function mergeLaunchConfigurations(config1, config2) {
if (config1 === undefined && config2 === undefined) {
return undefined;
}
if (config2 === undefined) {
return config1;
}
let result;
// skip invalid configs
if (typeof config1 === 'object' && !Array.isArray(config1)) {
result = { ...config1 };
}
if (typeof config2 === 'object' && !Array.isArray(config2)) {
result = { ...(result ?? {}), ...config2 }
}
// merge configurations and compounds arrays
const mergedConfigurations = mergeArrays(config1?.configurations, config2?.configurations);
if (mergedConfigurations) {
result.configurations = mergedConfigurations
}
const mergedCompounds = mergeArrays(config1?.compounds, config2?.compounds);
if (mergedCompounds) {
result.compounds = mergedCompounds;
}
return result;
}
function mergeArrays(array1, array2) {
if (array1 === undefined && array2 === undefined) {
return undefined;
}
if (!Array.isArray(array1) && !Array.isArray(array2)) {
return undefined;
}
let result = [];
if (Array.isArray(array1)) {
result = [...array1];
}
if (Array.isArray(array2)) {
result = [...result, ...array2];
}
return result;
}
const originalShouldOverwrite = fileResourceResolver['shouldOverwrite'];
before(async () => {
// fail tests if out of async happens
fileResourceResolver['shouldOverwrite'] = async () => (assert.fail('should be in sync'), false);
await deleteWorkspacePreferences();
});
after(() => {
fileResourceResolver['shouldOverwrite'] = originalShouldOverwrite;
});
/**
* @typedef {Object} ConfigSuiteOptions
* @property {any} expectation
* @property {any} [inspectExpectation]
* @property {any} [launch]
* @property {any} [settings]
* @property {boolean} [only]
* @property {ConfigMode} [configMode]
*/
/**
* @type {(options: ConfigSuiteOptions) => void}
*/
function testConfigSuite({
configMode, expectation, inspectExpectation, settings, launch, only
}) {
describe(JSON.stringify(configMode, undefined, 2), () => {
const configPaths = Array.isArray(configMode) ? configMode : [configMode];
/** @typedef {import('@theia/monaco-editor-core/esm/vs/base/common/lifecycle').IReference<import('@theia/monaco/lib/browser/monaco-editor-model').MonacoEditorModel>} ConfigModelReference */
/** @type {ConfigModelReference[]} */
beforeEach(async () => {
/** @type {Promise<void>[]} */
const promises = [];
/**
* @param {string} name
* @param {Record<string, unknown>} value
*/
const ensureConfigModel = (name, value) => {
for (const configPath of configPaths) {
promises.push((async () => {
try {
const uri = rootUri.resolve(configPath + '/' + name + '.json');
const provider = findProvider(uri);
if (provider) {
await provider['doSetPreference']('', [], value);
} else {
console.log('Unable to find provider for', uri.path.toString());
}
} catch (e) {
console.error(e);
}
})());
}
};
if (settings) {
ensureConfigModel('settings', settings);
}
if (launch) {
ensureConfigModel('launch', launch);
}
await Promise.all(promises);
});
after(async () => await deleteWorkspacePreferences());
const testItOnly = !!only ? it.only : it;
const testIt = testItOnly;
const settingsLaunch = settings ? settings['launch'] : undefined;
testIt('get from default', () => {
const config = preferences.get('launch');
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
});
testIt('get from undefined', () => {
/** @type {any} */
const config = preferences.get('launch', undefined, undefined);
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
});
testIt('get from rootUri', () => {
/** @type {any} */
const config = preferences.get('launch', undefined, rootUri.toString());
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
});
testIt('inspect in undefined', () => {
const inspect = preferences.inspect('launch');
/** @type {PreferenceInspection} */
let expected = inspectExpectation;
if (!expected) {
expected = {
preferenceName: 'launch',
defaultValue: defaultLaunch
};
const workspaceValue = mergeLaunchConfigurations(settingsLaunch, launch);
if (workspaceValue !== undefined && JSON.stringify(workspaceValue) !== '{}') {
Object.assign(expected, { workspaceValue });
}
}
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
});
testIt('inspect in rootUri', () => {
const inspect = preferences.inspect('launch', rootUri.toString());
/** @type {PreferenceInspection} */
const expected = {
preferenceName: 'launch',
defaultValue: defaultLaunch
};
if (inspectExpectation) {
Object.assign(expected, {
workspaceValue: inspectExpectation.workspaceValue,
workspaceFolderValue: inspectExpectation.workspaceValue
});
} else {
const value = mergeLaunchConfigurations(settingsLaunch, launch);
if (value !== undefined && JSON.stringify(value) !== '{}') {
Object.assign(expected, {
workspaceValue: value,
workspaceFolderValue: value
});
}
}
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
});
testIt('update launch', async () => {
await preferences.set('launch', validLaunch);
const inspect = preferences.inspect('launch');
const actual = inspect && inspect.workspaceValue;
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
assert.deepStrictEqual(actual, expected);
});
testIt('update launch Workspace', async () => {
await preferences.set('launch', validLaunch, PreferenceScope.Workspace);
const inspect = preferences.inspect('launch');
const actual = inspect && inspect.workspaceValue;
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
assert.deepStrictEqual(actual, expected);
});
testIt('update launch WorkspaceFolder', async () => {
try {
await preferences.set('launch', validLaunch, PreferenceScope.Folder);
assert.fail('should not be possible to update Workspace Folder Without resource');
} catch (e) {
assert.deepStrictEqual(e.message, 'Unable to write to Folder Settings because no resource is provided.');
}
});
testIt('update launch WorkspaceFolder with resource', async () => {
await preferences.set('launch', validLaunch, PreferenceScope.Folder, rootUri.toString());
const inspect = preferences.inspect('launch');
const actual = inspect && inspect.workspaceValue;
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
assert.deepStrictEqual(actual, expected);
});
if ((launch && !Array.isArray(launch)) || (settingsLaunch && !Array.isArray(settingsLaunch))) {
testIt('update launch.configurations', async () => {
await preferences.set('launch.configurations', [validConfiguration, validConfiguration2]);
const inspect = preferences.inspect('launch');
const actual = inspect && inspect.workspaceValue && inspect.workspaceValue.configurations;
let expect = [validConfiguration, validConfiguration2];
if (Array.isArray(settingsLaunch?.configurations)) {
expect = [...(settingsLaunch.configurations), ...expect]
}
assert.deepStrictEqual(actual, expect);
});
}
testIt('delete launch', async () => {
await preferences.set('launch', undefined);
const actual = preferences.inspect('launch');
let expected = undefined;
if (configPaths[1]) {
expected = launch;
if (Array.isArray(expected)) {
expected = { ...expected };
}
}
expected = mergeLaunchConfigurations(settingsLaunch, expected);
assert.deepStrictEqual(actual && actual.workspaceValue, expected);
});
if ((launch && !Array.isArray(launch)) || (settingsLaunch && !Array.isArray(settingsLaunch))) {
testIt('delete launch.configurations', async () => {
await preferences.set('launch.configurations', undefined);
const actual = preferences.inspect('launch');
const actualWorkspaceValue = actual && actual.workspaceValue;
let expected = { ...launch };
if (launch) {
delete expected['configurations'];
}
expected = mergeLaunchConfigurations(settingsLaunch, expected);
assert.deepStrictEqual(actualWorkspaceValue, expected);
});
}
});
}
});

View File

@@ -0,0 +1,179 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Menus', function () {
this.timeout(7500);
const { assert } = chai;
const { BrowserMenuBarContribution } = require('@theia/core/lib/browser/menu/browser-menu-plugin');
const { MenuModelRegistry } = require('@theia/core/lib/common/menu');
const { CommandRegistry } = require('@theia/core/lib/common/command');
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { ContextMenuRenderer } = require('@theia/core/lib/browser/context-menu-renderer');
const { BrowserContextMenuAccess } = require('@theia/core/lib/browser/menu/browser-context-menu-renderer');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { ViewContainer } = require('@theia/core/lib/browser/view-container');
const { waitForRevealed, waitForHidden } = require('@theia/core/lib/browser/widgets/widget');
const { CallHierarchyContribution } = require('@theia/callhierarchy/lib/browser/callhierarchy-contribution');
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { ScmHistoryContribution } = require('@theia/scm-extra/lib/browser/history/scm-history-contribution');
const { OutlineViewContribution } = require('@theia/outline-view/lib/browser/outline-view-contribution');
const { OutputContribution } = require('@theia/output/lib/browser/output-contribution');
const { PluginFrontendViewContribution } = require('@theia/plugin-ext/lib/main/browser/plugin-frontend-view-contribution');
const { ProblemContribution } = require('@theia/markers/lib/browser/problem/problem-contribution');
const { PropertyViewContribution } = require('@theia/property-view/lib/browser/property-view-contribution');
const { SearchInWorkspaceFrontendContribution } = require('@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const container = window.theia.container;
const shell = container.get(ApplicationShell);
/** @type {BrowserMenuBarContribution} */
const menuBarContribution = container.get(BrowserMenuBarContribution);
const pluginService = container.get(HostedPluginSupport);
const menus = container.get(MenuModelRegistry);
const commands = container.get(CommandRegistry);
const contextMenuService = container.get(ContextMenuRenderer);
before(async function () {
await pluginService.didStart;
await pluginService.activateByViewContainer('explorer');
// Updating the menu interferes with our ability to programmatically test it
// We simply disable the menu updating
menus.isReady = false;
});
const toTearDown = new DisposableCollection();
afterEach(() => toTearDown.dispose());
for (const contribution of [
container.get(CallHierarchyContribution),
container.get(FileNavigatorContribution),
container.get(ScmContribution),
container.get(ScmHistoryContribution),
container.get(OutlineViewContribution),
container.get(OutputContribution),
container.get(PluginFrontendViewContribution),
container.get(ProblemContribution),
container.get(PropertyViewContribution),
container.get(SearchInWorkspaceFrontendContribution)
]) {
it(`should toggle '${contribution.viewLabel}' view`, async () => {
await contribution.closeView();
await menuBarContribution.menuBar.triggerMenuItem('View', contribution.viewLabel);
await shell.waitForActivation(contribution.viewId);
});
}
it('reveal more context menu in the explorer view container toolbar', async function () {
const viewContainer = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
if (!(viewContainer instanceof ViewContainer)) {
assert.isTrue(viewContainer instanceof ViewContainer);
return;
}
const contribution = container.get(FileNavigatorContribution);
const waitForParts = [];
for (const part of viewContainer.getParts()) {
if (part.wrapped.id !== contribution.viewId) {
part.hide();
waitForParts.push(waitForHidden(part.wrapped));
} else {
part.show();
waitForParts.push(waitForRevealed(part.wrapped));
}
}
await Promise.all(waitForParts);
const contextMenuAccess = shell.leftPanelHandler.toolBar.showMoreContextMenu({ x: 0, y: 0 });
toTearDown.push(contextMenuAccess);
if (!(contextMenuAccess instanceof BrowserContextMenuAccess)) {
assert.isTrue(contextMenuAccess instanceof BrowserContextMenuAccess);
return;
}
const contextMenu = contextMenuAccess.menu;
await waitForRevealed(contextMenu);
assert.notEqual(contextMenu.items.length, 0);
});
it('rendering a new context menu should close the current', async function () {
const commandId = '__test_command_' + new Date();
const contextMenuPath = ['__test_first_context_menu_' + new Date()];
const contextMenuPath2 = ['__test_second_context_menu_' + new Date()];
toTearDown.push(commands.registerCommand({
id: commandId,
label: commandId
}, {
execute: () => { }
}));
toTearDown.push(menus.registerMenuAction(contextMenuPath, { commandId }));
toTearDown.push(menus.registerMenuAction(contextMenuPath2, { commandId }));
const access = contextMenuService.render({
anchor: { x: 0, y: 0 },
menuPath: contextMenuPath
});
toTearDown.push(access);
if (!(access instanceof BrowserContextMenuAccess)) {
assert.isTrue(access instanceof BrowserContextMenuAccess);
return;
}
assert.deepEqual(contextMenuService.current, access);
assert.isFalse(access.disposed);
await waitForRevealed(access.menu);
assert.notEqual(access.menu.items.length, 0);
assert.deepEqual(contextMenuService.current, access);
assert.isFalse(access.disposed);
const access2 = contextMenuService.render({
anchor: { x: 0, y: 0 },
menuPath: contextMenuPath2
});
toTearDown.push(access2);
if (!(access2 instanceof BrowserContextMenuAccess)) {
assert.isTrue(access2 instanceof BrowserContextMenuAccess);
return;
}
assert.deepEqual(contextMenuService.current, access2);
assert.isFalse(access2.disposed);
assert.isTrue(access.disposed);
await waitForRevealed(access2.menu);
assert.deepEqual(contextMenuService.current, access2);
assert.isFalse(access2.disposed);
assert.isTrue(access.disposed);
access2.dispose();
assert.deepEqual(contextMenuService.current, undefined);
assert.isTrue(access2.disposed);
await waitForHidden(access2.menu);
assert.deepEqual(contextMenuService.current, undefined);
assert.isTrue(access2.disposed);
});
it('should not fail to register a menu with an invalid command', () => {
assert.doesNotThrow(() => menus.registerMenuAction(['test-menu-path'], { commandId: 'invalid-command', label: 'invalid command' }), 'should not throw.');
});
});

View File

@@ -0,0 +1,198 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
const { timeout } = require('@theia/core/lib/common/promise-util');
const { IOpenerService } = require('@theia/monaco-editor-core/esm/vs/platform/opener/common/opener');
// @ts-check
describe('Monaco API', async function () {
this.timeout(5000);
const { assert } = chai;
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding');
const { MonacoTextmateService } = require('@theia/monaco/lib/browser/textmate/monaco-textmate-service');
const { CommandRegistry } = require('@theia/core/lib/common/command');
const { KeyCodeChord, ResolvedChord } = require('@theia/monaco-editor-core/esm/vs/base/common/keybindings');
const { IKeybindingService } = require('@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding');
const { StandaloneServices } = require('@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices');
const { TokenizationRegistry } = require('@theia/monaco-editor-core/esm/vs/editor/common/languages');
const { MonacoContextKeyService } = require('@theia/monaco/lib/browser/monaco-context-key-service');
const { URI } = require('@theia/monaco-editor-core/esm/vs/base/common/uri');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const textmateService = container.get(MonacoTextmateService);
/** @type {import('@theia/core/src/common/command').CommandRegistry} */
const commands = container.get(CommandRegistry);
/** @type {import('@theia/monaco/src/browser/monaco-context-key-service').MonacoContextKeyService} */
const contextKeys = container.get(MonacoContextKeyService);
/** @type {MonacoEditor} */
let monacoEditor;
before(async () => {
const root = workspaceService.tryGetRoots()[0];
const editor = await editorManager.open(root.resource.resolve('package.json'), {
mode: 'reveal'
});
monacoEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editor));
});
after(async () => {
await editorManager.closeAll({ save: false });
});
it('KeybindingService.resolveKeybinding', () => {
const chord = new KeyCodeChord(true, true, true, true, 41 /* KeyCode.KeyK */);
const chordKeybinding = chord.toKeybinding();
assert.equal(chordKeybinding.chords.length, 1);
assert.equal(chordKeybinding.chords[0], chord);
const resolvedKeybindings = StandaloneServices.get(IKeybindingService).resolveKeybinding(chordKeybinding);
assert.equal(resolvedKeybindings.length, 1);
const resolvedKeybinding = resolvedKeybindings[0];
if (resolvedKeybinding instanceof MonacoResolvedKeybinding) {
const label = resolvedKeybinding.getLabel();
const ariaLabel = resolvedKeybinding.getAriaLabel();
const electronAccelerator = resolvedKeybinding.getElectronAccelerator();
const userSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
const WYSIWYG = resolvedKeybinding.isWYSIWYG();
const parts = resolvedKeybinding.getChords();
const dispatchParts = resolvedKeybinding.getDispatchChords().map(str => str === null ? '' : str);
const platform = window.navigator.platform;
let expected;
if (platform.includes('Mac')) {
// Mac os
expected = {
label: '⌃⇧⌥⌘K',
ariaLabel: '⌃⇧⌥⌘K',
electronAccelerator: 'Ctrl+Shift+Alt+Cmd+K',
userSettingsLabel: 'ctrl+shift+alt+cmd+K',
WYSIWYG: true,
parts: [new ResolvedChord(
true,
true,
true,
true,
'K',
'K',
)],
dispatchParts: [
'ctrl+shift+alt+meta+K'
]
};
} else {
expected = {
label: 'Ctrl+Shift+Alt+K',
ariaLabel: 'Ctrl+Shift+Alt+K',
electronAccelerator: 'Ctrl+Shift+Alt+K',
userSettingsLabel: 'ctrl+shift+alt+K',
WYSIWYG: true,
parts: [new ResolvedChord(
true,
true,
true,
false,
'K',
'K'
)],
dispatchParts: [
'ctrl+shift+alt+K'
]
};
}
assert.deepStrictEqual({
label, ariaLabel, electronAccelerator, userSettingsLabel, WYSIWYG, parts, dispatchParts
}, expected);
} else {
assert.fail(`resolvedKeybinding must be of ${MonacoResolvedKeybinding.name} type`);
}
});
it('TokenizationRegistry.getColorMap', async () => {
if (textmateService['monacoThemeRegistry'].getThemeData().base !== 'vs') {
const didChangeColorMap = new Promise(resolve => {
const toDispose = TokenizationRegistry.onDidChange(() => {
toDispose.dispose();
resolve(undefined);
});
});
textmateService['themeService'].setCurrentTheme('light');
await didChangeColorMap;
}
const textMateColorMap = textmateService['grammarRegistry'].getColorMap();
assert.notEqual(textMateColorMap.indexOf('#795E26'), -1, 'Expected custom toke colors for the light theme to be enabled.');
const monacoColorMap = (TokenizationRegistry.getColorMap() || []).
splice(0, textMateColorMap.length).map(c => c.toString().toUpperCase());
assert.deepStrictEqual(monacoColorMap, textMateColorMap, 'Expected textmate colors to have the same index in the monaco color map.');
});
it('OpenerService.open', async () => {
/** @type {import('@theia/monaco-editor-core/esm/vs/editor/browser/services/openerService').OpenerService} */
const openerService = StandaloneServices.get(IOpenerService);
let opened = false;
const id = '__test:OpenerService.open';
const unregisterCommand = commands.registerCommand({ id }, {
execute: arg => (console.log(arg), opened = arg === 'foo')
});
try {
await openerService.open(URI.parse('command:' + id + '?"foo"'));
assert.isTrue(opened);
} finally {
unregisterCommand.dispose();
}
});
it('Supports setting contexts using the command registry', async () => {
const setContext = '_setContext';
const key = 'monaco-api-test-context';
const firstValue = 'first setting';
const secondValue = 'second setting';
assert.isFalse(contextKeys.match(`${key} == '${firstValue}'`));
await commands.executeCommand(setContext, key, firstValue);
assert.isTrue(contextKeys.match(`${key} == '${firstValue}'`));
await commands.executeCommand(setContext, key, secondValue);
assert.isTrue(contextKeys.match(`${key} == '${secondValue}'`));
});
it('Supports context key: inQuickOpen', async () => {
const inQuickOpenContextKey = 'inQuickOpen';
const quickOpenCommands = ['file-search.openFile', 'workbench.action.showCommands'];
const CommandThatChangesFocus = 'workbench.files.action.focusFilesExplorer';
for (const cmd of quickOpenCommands) {
assert.isFalse(contextKeys.match(inQuickOpenContextKey));
await commands.executeCommand(cmd);
assert.isTrue(contextKeys.match(inQuickOpenContextKey));
await commands.executeCommand(CommandThatChangesFocus);
await timeout(0);
assert.isFalse(contextKeys.match(inQuickOpenContextKey));
}
});
});

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Navigator', function () {
this.timeout(5000);
const { assert } = chai;
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
const { DirNode, FileNode } = require('@theia/filesystem/lib/browser/file-tree/file-tree');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const fileService = container.get(FileService);
const workspaceService = container.get(WorkspaceService);
const navigatorContribution = container.get(FileNavigatorContribution);
const rootUri = workspaceService.tryGetRoots()[0].resource;
const fileUri = rootUri.resolve('.test/nested/source/text.txt');
const targetUri = rootUri.resolve('.test/target');
beforeEach(async () => {
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
await fileService.createFolder(targetUri);
});
afterEach(async () => {
await fileService.delete(targetUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
});
/** @type {Array<['copy' | 'move', boolean]>} */
const operations = [
['copy', false],
['move', false]
];
/** @type {Array<['file' | 'dir', boolean]>} */
const fileTypes = [
['file', false],
['dir', false],
];
for (const [operation, onlyOperation] of operations) {
for (const [fileType, onlyFileType] of fileTypes) {
const ExpectedNodeType = fileType === 'file' ? FileNode : DirNode;
(onlyOperation || onlyFileType ? it.only : it)(operation + ' ' + fileType, async function () {
const navigator = await navigatorContribution.openView({ reveal: true });
await navigator.model.refresh();
const sourceUri = fileType === 'file' ? fileUri : fileUri.parent;
const sourceNode = await navigator.model.revealFile(sourceUri);
if (!ExpectedNodeType.is(sourceNode)) {
return assert.isTrue(ExpectedNodeType.is(sourceNode));
}
const targetNode = await navigator.model.revealFile(targetUri);
if (!DirNode.is(targetNode)) {
return assert.isTrue(DirNode.is(targetNode));
}
let actualUri;
if (operation === 'copy') {
actualUri = await navigator.model.copy(sourceUri, targetNode);
} else {
actualUri = await navigator.model.move(sourceNode, targetNode);
}
if (!actualUri) {
return assert.isDefined(actualUri);
}
await navigator.model.refresh(targetNode);
const actualNode = await navigator.model.revealFile(actualUri);
assert.isTrue(ExpectedNodeType.is(actualNode));
});
}
}
});

View File

@@ -0,0 +1,209 @@
// *****************************************************************************
// Copyright (C) 2022 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Preferences', function () {
this.timeout(5_000);
const { assert } = chai;
const { PreferenceProvider } = require('@theia/core/lib/common/preferences/preference-provider');
const { PreferenceService, PreferenceScope } = require('@theia/core/lib/common/preferences');
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
const { PreferenceLanguageOverrideService } = require('@theia/core/lib/common/preferences/preference-language-override-service');
const { MonacoTextModelService } = require('@theia/monaco/lib/browser/monaco-text-model-service');
const { PreferenceSchemaService } = require('@theia/core/lib/common/preferences')
const { container } = window.theia;
/** @type {import ('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
const preferenceService = container.get(PreferenceService);
/** @type {import ('@theia/core/lib/common/preferences/preference-language-override-service').PreferenceLanguageOverrideService} */
const overrideService = container.get(PreferenceLanguageOverrideService);
const fileService = container.get(FileService);
/** @type {import ('@theia/core/lib/common/uri').default} */
const uri = preferenceService.getConfigUri(PreferenceScope.Workspace);
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
/** @type PreferenceSchemaService */
const schemaService = container.get(PreferenceSchemaService);
const modelService = container.get(MonacoTextModelService);
const overrideIdentifier = 'bargle-noddle-zaus'; // Probably not in our preference files...
schemaService.registerOverrideIdentifier(overrideIdentifier);
const tabSize = 'editor.tabSize';
const fontSize = 'editor.fontSize';
const override = overrideService.markLanguageOverride(overrideIdentifier);
const overriddenTabSize = overrideService.overridePreferenceName({ overrideIdentifier, preferenceName: tabSize });
const overriddenFontSize = overrideService.overridePreferenceName({ overrideIdentifier, preferenceName: fontSize });
/**
* @returns {Promise<Record<string, any>>}
*/
async function getPreferences() {
try {
const content = (await fileService.read(uri)).value;
return JSON.parse(content);
} catch (e) {
return {};
}
}
/**
* @param {string} key
* @param {unknown} value
*/
async function setPreference(key, value) {
return preferenceService.set(key, value, PreferenceScope.Workspace);
}
async function deleteAllValues() {
return setValueTo(undefined);
}
/**
* @param {any} value - A JSON value to write to the workspace preference file.
*/
async function setValueTo(value) {
const reference = await modelService.createModelReference(uri);
if (reference.object.dirty) {
await reference.object.revert();
}
/** @type {import ('@theia/preferences/lib/browser/folder-preference-provider').FolderPreferenceProvider} */
const provider = Array.from(folderPreferences['providers'].values()).find(candidate => candidate.getConfigUri().isEqual(uri));
assert.isDefined(provider);
await provider['doSetPreference']('', [], value);
reference.dispose();
}
let fileExistsBeforehand = false;
let contentBeforehand = '';
before(async function () {
assert.isDefined(uri, 'The workspace config URI should be defined!');
fileExistsBeforehand = await fileService.exists(uri);
contentBeforehand = await fileService.read(uri).then(({ value }) => value).catch(() => '');
schemaService.registerOverrideIdentifier(overrideIdentifier);
await deleteAllValues();
});
after(async function () {
if (!fileExistsBeforehand) {
await fileService.delete(uri, { fromUserGesture: false }).catch(() => { });
} else {
let content = '';
try { content = JSON.parse(contentBeforehand); } catch { }
// Use the preference service because its promise is guaranteed to resolve after the file change is complete.
await setValueTo(content);
}
});
beforeEach(async function () {
const prefs = await getPreferences();
for (const key of [tabSize, fontSize, override, overriddenTabSize, overriddenFontSize]) {
shouldBeUndefined(prefs[key], key);
}
});
afterEach(async function () {
await deleteAllValues();
});
/**
* @param {unknown} value
* @param {string} key
*/
function shouldBeUndefined(value, key) {
assert.isUndefined(value, `There should be no ${key} object or value in the preferences.`);
}
/**
* @returns {Promise<{newTabSize: number, newFontSize: number, startingTabSize: number, startingFontSize: number}>}
*/
async function setUpOverride() {
const startingTabSize = preferenceService.get(tabSize);
const startingFontSize = preferenceService.get(fontSize);
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize, 'The overridden value should equal the default.');
assert.equal(preferenceService.get(overriddenFontSize), startingFontSize, 'The overridden value should equal the default.');
const newTabSize = startingTabSize + 2;
const newFontSize = startingFontSize + 2;
await Promise.all([
setPreference(overriddenTabSize, newTabSize),
setPreference(overriddenFontSize, newFontSize),
]);
assert.equal(preferenceService.get(overriddenTabSize), newTabSize, 'After setting, the new value should be active for the override.');
assert.equal(preferenceService.get(overriddenFontSize), newFontSize, 'After setting, the new value should be active for the override.');
return { newTabSize, newFontSize, startingTabSize, startingFontSize };
}
it('Sets language overrides as objects', async function () {
const { newTabSize, newFontSize } = await setUpOverride();
const prefs = await getPreferences();
assert.isObject(prefs[override], 'The override should be a key in the preference object.');
assert.equal(prefs[override][tabSize], newTabSize, 'editor.tabSize should be a key in the override object and have the correct value.');
assert.equal(prefs[override][fontSize], newFontSize, 'editor.fontSize should be a key in the override object and should have the correct value.');
shouldBeUndefined(prefs[overriddenTabSize], overriddenTabSize);
shouldBeUndefined(prefs[overriddenFontSize], overriddenFontSize);
});
it('Allows deletion of individual keys in the override object.', async function () {
const { startingTabSize } = await setUpOverride();
await setPreference(overriddenTabSize, undefined);
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize);
const prefs = await getPreferences();
shouldBeUndefined(prefs[override][tabSize], tabSize);
shouldBeUndefined(prefs[overriddenFontSize], overriddenFontSize);
shouldBeUndefined(prefs[overriddenTabSize], overriddenTabSize);
});
it('Allows deletion of the whole override object', async function () {
const { startingFontSize, startingTabSize } = await setUpOverride();
await setPreference(override, undefined);
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize, 'The overridden value should revert to the default.');
assert.equal(preferenceService.get(overriddenFontSize), startingFontSize, 'The overridden value should revert to the default.');
const prefs = await getPreferences();
shouldBeUndefined(prefs[override], override);
});
it('Handles many synchronous settings of preferences gracefully', async function () {
let settings = 0;
const promises = [];
const searchPref = 'search.searchOnTypeDebouncePeriod'
const channelPref = 'output.maxChannelHistory'
const hoverPref = 'workbench.hover.delay';
let searchDebounce;
let channelHistory;
let hoverDelay;
/** @type import ('@theia/core/src/browser/preferences/preference-service').PreferenceChanges | undefined */
let event;
const toDispose = preferenceService.onPreferencesChanged(e => event = e);
while (settings++ < 50) {
searchDebounce = 100 + Math.floor(Math.random() * 500);
channelHistory = 200 + Math.floor(Math.random() * 800);
hoverDelay = 250 + Math.floor(Math.random() * 2_500);
promises.push(
preferenceService.set(searchPref, searchDebounce),
preferenceService.set(channelPref, channelHistory),
preferenceService.set(hoverPref, hoverDelay)
);
}
const results = await Promise.allSettled(promises);
const expectedValues = { [searchPref]: searchDebounce, [channelPref]: channelHistory, [hoverPref]: hoverDelay };
const actualValues = { [searchPref]: preferenceService.get(searchPref), [channelPref]: preferenceService.get(channelPref), [hoverPref]: preferenceService.get(hoverPref), }
const eventKeys = event && Object.keys(event).sort();
toDispose.dispose();
assert(results.every(setting => setting.status === 'fulfilled'), 'All promises should have resolved rather than rejected.');
assert.deepEqual([channelPref, searchPref, hoverPref], eventKeys, 'The event should contain the changed preference names.');
assert.deepEqual(expectedValues, actualValues, 'The service state should reflect the most recent setting');
});
});

View File

@@ -0,0 +1,512 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Saveable', function () {
this.timeout(30000);
const { assert } = chai;
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
const { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
const { FileResource } = require('@theia/filesystem/lib/browser/file-resource');
const { ETAG_DISABLED } = require('@theia/filesystem/lib/common/files');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const { Deferred, timeout } = require('@theia/core/lib/common/promise-util');
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
const container = window.theia.container;
/** @type {EditorManager} */
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const fileService = container.get(FileService);
/** @type {import('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
const preferences = container.get(PreferenceService);
/** @type {EditorWidget & SaveableWidget} */
let widget;
/** @type {MonacoEditor} */
let editor;
const rootUri = workspaceService.tryGetRoots()[0].resource;
const fileUri = rootUri.resolve('.test/foo.txt');
const closeOnFileDelete = 'workbench.editor.closeOnFileDelete';
/**
* @param {FileResource['shouldOverwrite']} shouldOverwrite
* @returns {Disposable}
*/
function setShouldOverwrite(shouldOverwrite) {
const resource = editor.document['resource'];
assert.isTrue(resource instanceof FileResource);
const fileResource = /** @type {FileResource} */ (resource);
const originalShouldOverwrite = fileResource['shouldOverwrite'];
fileResource['shouldOverwrite'] = shouldOverwrite;
return Disposable.create(() => fileResource['shouldOverwrite'] = originalShouldOverwrite);
}
const toTearDown = new DisposableCollection();
/** @type {string | undefined} */
const autoSave = preferences.get('files.autoSave', undefined, rootUri.toString());
beforeEach(async () => {
await preferences.set('files.autoSave', 'off', undefined, rootUri.toString());
await preferences.set(closeOnFileDelete, true);
await editorManager.closeAll({ save: false });
const watcher = fileService.watch(fileUri); // create/delete events are sometimes coalesced on Mac
const gotCreate = new Deferred();
const listener = fileService.onDidFilesChange(e => {
if (e.contains(fileUri, { type: 1 })) { // FileChangeType.ADDED
gotCreate.resolve();
}
});
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
await Promise.race([await timeout(2000), gotCreate.promise]);
watcher.dispose();
listener.dispose();
widget = /** @type {EditorWidget & SaveableWidget} */ (await editorManager.open(fileUri, { mode: 'reveal' }));
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
});
afterEach(async () => {
toTearDown.dispose();
// @ts-ignore
editor = undefined;
// @ts-ignore
widget = undefined;
await editorManager.closeAll({ save: false });
await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
});
it('normal save', async function () {
for (const edit of ['bar', 'baz']) {
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty before '${edit}' edit`);
editor.getControl().setValue(edit);
assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`);
await Saveable.save(widget);
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`);
assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`);
const state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), edit, `fs should be updated with '${edit}'`);
}
});
it('reject save with incremental update', async function () {
let longContent = 'foobarbaz';
for (let i = 0; i < 5; i++) {
longContent += longContent + longContent;
}
editor.getControl().setValue(longContent);
await Saveable.save(widget);
// @ts-ignore
editor.getControl().getModel().applyEdits([{
range: Range.fromPositions({ lineNumber: 1, column: 1 }, { lineNumber: 1, column: 4 }),
forceMoveMarkers: false,
text: ''
}]);
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
const resource = editor.document['resource'];
const version = resource.version;
// @ts-ignore
await resource.saveContents('baz');
assert.notEqual(version, resource.version, 'latest version should be different after write');
let outOfSync = false;
let outOfSyncCount = 0;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
outOfSyncCount++;
return false;
}));
let incrementalUpdate = false;
const saveContentChanges = resource.saveContentChanges;
resource.saveContentChanges = async (changes, options) => {
incrementalUpdate = true;
// @ts-ignore
return saveContentChanges.bind(resource)(changes, options);
};
try {
await Saveable.save(widget);
} finally {
resource.saveContentChanges = saveContentChanges;
}
assert.isTrue(incrementalUpdate, 'should tried to update incrementaly');
assert.isTrue(outOfSync, 'file should be out of sync');
assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated');
const state = await fileService.read(fileUri);
assert.equal(state.value, 'baz', 'fs should NOT be updated');
});
it('accept rejected save', async function () {
let outOfSync = false;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
return false;
}));
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
const resource = editor.document['resource'];
const version = resource.version;
// @ts-ignore
await resource.saveContents('bazz');
assert.notEqual(version, resource.version, 'latest version should be different after write');
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
let state = await fileService.read(fileUri);
assert.equal(state.value, 'bazz', 'fs should NOT be updated');
outOfSync = false;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
return true;
}));
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
});
it('accept new save', async () => {
let outOfSync = false;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
return true;
}));
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
await Saveable.save(widget);
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
const state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
});
it('cancel save on close', async () => {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before close');
await widget.closeWithSaving({
shouldSave: () => undefined
});
assert.isTrue(Saveable.isDirty(widget), 'should be still dirty after canceled close');
assert.isFalse(widget.isDisposed, 'should NOT be disposed after canceled close');
const state = await fileService.read(fileUri);
assert.equal(state.value, 'foo', 'fs should NOT be updated after canceled close');
});
it('reject save on close', async () => {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejected close');
await widget.closeWithSaving({
shouldSave: () => false
});
assert.isTrue(widget.isDisposed, 'should be disposed after rejected close');
const state = await fileService.read(fileUri);
assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close');
});
it('accept save on close and reject it', async () => {
let outOfSync = false;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
return false;
}));
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejecting save on close');
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
await widget.closeWithSaving({
shouldSave: () => true
});
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isFalse(widget.isDisposed, 'model should not be disposed after close when we reject the save');
const state = await fileService.read(fileUri);
assert.equal(state.value, 'foo2', 'fs should NOT be updated');
});
it('accept save on close and accept new save', async () => {
let outOfSync = false;
toTearDown.push(setShouldOverwrite(async () => {
outOfSync = true;
return true;
}));
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before accepting save on close');
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
await widget.closeWithSaving({
shouldSave: () => true
});
assert.isTrue(outOfSync, 'file should be out of sync');
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
const state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
});
it('no save prompt when multiple editors open for same file', async () => {
const secondWidget = await editorManager.openToSide(fileUri);
editor.getControl().setValue('two widgets');
assert.isTrue(Saveable.isDirty(widget), 'the first widget should be dirty');
assert.isTrue(Saveable.isDirty(secondWidget), 'the second widget should also be dirty');
await Promise.resolve(secondWidget.close());
assert.isTrue(secondWidget.isDisposed, 'the widget should have closed without requesting user action');
assert.isTrue(Saveable.isDirty(widget), 'the original widget should still be dirty.');
assert.equal(editor.getControl().getValue(), 'two widgets', 'should still have the same value');
});
it('normal close', async () => {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before before close');
await widget.closeWithSaving({
shouldSave: () => true
});
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
const state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
});
it('delete and add again file for dirty', async () => {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before delete');
assert.isTrue(editor.document.valid, 'should be valid before delete');
let waitForDidChangeTitle = new Deferred();
const listener = () => waitForDidChangeTitle.resolve();
widget.title.changed.connect(listener);
try {
await fileService.delete(fileUri);
await waitForDidChangeTitle.promise;
assert.isTrue(widget.title.label.endsWith('(Deleted)'), 'should be marked as deleted');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete');
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete');
} finally {
widget.title.changed.disconnect(listener);
}
waitForDidChangeTitle = new Deferred();
widget.title.changed.connect(listener);
try {
await fileService.create(fileUri, 'foo');
await waitForDidChangeTitle.promise;
assert.isFalse(widget.title.label.endsWith('(deleted)'), 'should NOT be marked as deleted');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after added again');
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after added again');
} finally {
widget.title.changed.disconnect(listener);
}
});
it('save deleted file for dirty', async function () {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save deleted');
assert.isTrue(editor.document.valid, 'should be valid before delete');
const waitForInvalid = new Deferred();
const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve());
try {
await fileService.delete(fileUri);
await waitForInvalid.promise;
assert.isFalse(editor.document.valid, 'should be invalid after delete');
} finally {
listener.dispose();
}
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
await Saveable.save(widget);
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
assert.isTrue(editor.document.valid, 'should be valid after save');
const state = await fileService.read(fileUri);
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
});
it('move file for saved', async function () {
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before move');
const targetUri = fileUri.parent.resolve('bar.txt');
await fileService.move(fileUri, targetUri, { overwrite: true });
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
assert.equal(renamed.editor.document.getText(), 'foo', 'new model should be created after move');
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after move');
});
it('move file for dirty', async function () {
editor.getControl().setValue('bar');
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before move');
const targetUri = fileUri.parent.resolve('bar.txt');
await fileService.move(fileUri, targetUri, { overwrite: true });
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
assert.equal(renamed.editor.document.getText(), 'bar', 'new model should be created after move');
assert.isTrue(Saveable.isDirty(renamed), 'new model should be dirty after move');
await Saveable.save(renamed);
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after save');
});
it('fail to open invalid file', async function () {
const invalidFile = fileUri.parent.resolve('invalid_file.txt');
try {
await editorManager.open(invalidFile, { mode: 'reveal' });
assert.fail('should not be possible to open an editor for invalid file');
} catch (e) {
assert.equal(e.code, 'MODEL_IS_INVALID');
}
});
it('decode without save', async function () {
assert.strictEqual('utf8', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText());
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
assert.strictEqual('utf16le', editor.document.getEncoding());
assert.notEqual('foo', editor.document.getText().trimRight());
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
await widget.closeWithSaving({
shouldSave: () => undefined
});
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
widget = /** @type {EditorWidget & SaveableWidget} */
(await editorManager.open(fileUri, { mode: 'reveal' }));
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
assert.strictEqual('utf8', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText().trimRight());
});
it('decode with save', async function () {
assert.strictEqual('utf8', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText());
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
assert.strictEqual('utf16le', editor.document.getEncoding());
assert.notEqual('foo', editor.document.getText().trimRight());
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
await Saveable.save(widget);
await widget.closeWithSaving({
shouldSave: () => undefined
});
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
widget = /** @type {EditorWidget & SaveableWidget} */
(await editorManager.open(fileUri, { mode: 'reveal' }));
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
assert.strictEqual('utf16le', editor.document.getEncoding());
assert.notEqual('foo', editor.document.getText().trimRight());
});
it('encode', async function () {
assert.strictEqual('utf8', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText());
await editor.setEncoding('utf16le', 0 /* EncodingMode.Encode */);
assert.strictEqual('utf16le', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText().trimRight());
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after encode');
await widget.closeWithSaving({
shouldSave: () => undefined
});
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
widget = /** @type {EditorWidget & SaveableWidget} */
(await editorManager.open(fileUri, { mode: 'reveal' }));
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
assert.strictEqual('utf16le', editor.document.getEncoding());
assert.strictEqual('foo', editor.document.getText().trimRight());
});
it('delete file for saved', async () => {
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before delete');
const waitForDisposed = new Deferred();
const listener = editor.onDispose(() => waitForDisposed.resolve());
try {
await fileService.delete(fileUri);
await waitForDisposed.promise;
assert.isTrue(widget.isDisposed, 'model should be disposed after delete');
} finally {
listener.dispose();
}
});
it(`'${closeOnFileDelete}' should keep the editor opened when set to 'false'`, async () => {
await preferences.set(closeOnFileDelete, false);
assert.isFalse(preferences.get(closeOnFileDelete));
assert.isFalse(Saveable.isDirty(widget));
const waitForDidChangeTitle = new Deferred();
const listener = () => waitForDidChangeTitle.resolve();
widget.title.changed.connect(listener);
try {
await fileService.delete(fileUri);
await waitForDidChangeTitle.promise;
assert.isTrue(widget.title.label.endsWith('(Deleted)'));
assert.isFalse(widget.isDisposed);
} finally {
widget.title.changed.disconnect(listener);
}
});
it(`'${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 {
const deleteThenDispose = fileService.delete(fileUri).then(() => waitForDisposed.promise);
await Promise.race([deleteThenDispose, fourSeconds]);
assert.isTrue(widget.isDisposed);
} finally {
listener.dispose();
}
});
});

View File

@@ -0,0 +1,222 @@
// *****************************************************************************
// Copyright (C) 2020 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-only WITH Classpath-exception-2.0
// *****************************************************************************
const { timeout } = require('@theia/core/lib/common/promise-util');
// @ts-check
describe('SCM', function () {
const { assert } = chai;
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const Uri = require('@theia/core/lib/common/uri');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { ScmService } = require('@theia/scm/lib/browser/scm-service');
const { ScmWidget } = require('@theia/scm/lib/browser/scm-widget');
const { CommandRegistry } = require('@theia/core/lib/common');
const { PreferenceService } = require('@theia/core/lib/browser');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const contextKeyService = container.get(ContextKeyService);
const scmContribution = container.get(ScmContribution);
const shell = container.get(ApplicationShell);
const service = container.get(ScmService);
const commandRegistry = container.get(CommandRegistry);
const pluginService = container.get(HostedPluginSupport);
const preferences = container.get(PreferenceService);
/** @type {ScmWidget} */
let scmWidget;
/** @type {ScmService} */
let scmService;
const gitPluginId = 'vscode.git';
/**
* @param {() => unknown} condition
* @param {number | undefined} [timeout]
* @param {string | undefined} [message]
* @returns {Promise<void>}
*/
async function waitForAnimation(condition, maxWait, message) {
if (maxWait === undefined) {
maxWait = 100000;
}
const endTime = Date.now() + maxWait;
do {
await (timeout(100));
if (condition()) {
return true;
}
if (Date.now() > endTime) {
throw new Error(message ?? 'Wait for animation timed out.');
}
} while (true);
}
before(async () => {
preferences.set('git.autoRepositoryDetection', true);
preferences.set('git.openRepositoryInParentFolders', 'always');
});
beforeEach(async () => {
if (!pluginService.getPlugin(gitPluginId)) {
throw new Error(gitPluginId + ' should be started');
}
await pluginService.activatePlugin(gitPluginId);
await shell.leftPanelHandler.collapse();
scmWidget = await scmContribution.openView({ activate: true, reveal: true });
scmService = service;
await waitForAnimation(() => scmService.selectedRepository, 10000, 'selected repository is not defined');
});
afterEach(() => {
// @ts-ignore
scmWidget = undefined;
// @ts-ignore
scmService = undefined;
});
describe('scm-view', () => {
it('the view should open and activate successfully', () => {
assert.notEqual(scmWidget, undefined);
assert.strictEqual(scmWidget, shell.activeWidget);
});
describe('\'ScmTreeWidget\'', () => {
it('the view should display the resource tree when a repository is present', () => {
assert.isTrue(scmWidget.resourceWidget.isVisible);
});
it('the view should not display the resource tree when no repository is present', () => {
// Store the current selected repository so it can be restored.
const cachedSelectedRepository = scmService.selectedRepository;
scmService.selectedRepository = undefined;
assert.isFalse(scmWidget.resourceWidget.isVisible);
// Restore the selected repository.
scmService.selectedRepository = cachedSelectedRepository;
});
});
describe('\'ScmNoRepositoryWidget\'', () => {
it('should not be visible when a repository is present', () => {
assert.isFalse(scmWidget.noRepositoryWidget.isVisible);
});
it('should be visible when no repository is present', () => {
// Store the current selected repository so it can be restored.
const cachedSelectedRepository = scmService.selectedRepository;
scmService.selectedRepository = undefined;
assert.isTrue(scmWidget.noRepositoryWidget.isVisible);
// Restore the selected repository.
scmService.selectedRepository = cachedSelectedRepository;
});
});
});
describe('scm-service', () => {
it('should successfully return the list of repositories', () => {
const repositories = scmService.repositories;
assert.isTrue(repositories.length > 0);
});
it('should include the selected repository in the list of repositories', () => {
const repositories = scmService.repositories;
const selectedRepository = scmService.selectedRepository;
assert.isTrue(repositories.length === 1);
assert.strictEqual(repositories[0], selectedRepository);
});
it('should successfully return the selected repository', () => {
assert.notEqual(scmService.selectedRepository, undefined);
});
it('should successfully find the repository', () => {
const selectedRepository = scmService.selectedRepository;
if (selectedRepository) {
const rootUri = selectedRepository.provider.rootUri;
const foundRepository = scmService.findRepository(new Uri.default(rootUri));
assert.notEqual(foundRepository, undefined);
}
else {
assert.fail('Selected repository is undefined');
}
});
it('should not find a repository for an unknown uri', () => {
const mockUri = new Uri.default('foobar/foo/bar');
const repo = scmService.findRepository(mockUri);
assert.strictEqual(repo, undefined);
});
it('should successfully return the list of statusbar commands', () => {
assert.isTrue(scmService.statusBarCommands.length > 0);
});
});
describe('scm-provider', () => {
it('should successfully return the last commit', async () => {
const selectedRepository = scmService.selectedRepository;
if (selectedRepository) {
const amendSupport = selectedRepository.provider.amendSupport;
if (amendSupport) {
const commit = await amendSupport.getLastCommit();
assert.notEqual(commit, undefined);
}
}
else {
assert.fail('Selected repository is undefined');
}
});
});
describe('scm-contribution', () => {
describe('scmFocus context-key', () => {
it('should return \'true\' when the view is focused', () => {
assert.isTrue(contextKeyService.match('scmFocus'));
});
it('should return \'false\' when the view is not focused', async () => {
await scmContribution.closeView();
assert.isFalse(contextKeyService.match('scmFocus'));
});
});
});
});

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2017 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Shell', function () {
const { assert } = chai;
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { StatusBarImpl } = require('@theia/core/lib/browser/status-bar');
const container = window.theia.container;
const shell = container.get(ApplicationShell);
const statusBar = container.get(StatusBarImpl);
it('should be shown', () => {
assert.isTrue(shell.isAttached && shell.isVisible);
});
it('should show the main content panel', () => {
assert.isTrue(shell.mainPanel.isAttached && shell.mainPanel.isVisible);
});
it('should show the status bar', () => {
assert.isTrue(statusBar.isAttached && statusBar.isVisible);
});
});

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// 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-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('The Task Configuration Manager', function () {
this.timeout(5000);
const { assert } = chai;
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { TaskScope, TaskConfigurationScope } = require('@theia/task/lib/common/task-protocol');
const { TaskConfigurationManager } = require('@theia/task/lib/browser/task-configuration-manager');
const container = window.theia.container;
const workspaceService = container.get(WorkspaceService);
const taskConfigurationManager = container.get(TaskConfigurationManager);
const baseWorkspaceURI = workspaceService.tryGetRoots()[0].resource;
const baseWorkspaceRoot = baseWorkspaceURI.toString();
const basicTaskConfig = {
label: 'task',
type: 'shell',
command: 'top',
};
/** @type {Set<TaskConfigurationScope>} */
const scopesToClear = new Set();
describe('in a single-root workspace', () => {
beforeEach(() => clearTasks());
after(() => clearTasks());
setAndRetrieveTasks(() => TaskScope.Global, 'user');
setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder');
});
async function clearTasks() {
await Promise.all(Array.from(scopesToClear, async scope => {
if (!!scope || scope === 0) {
await taskConfigurationManager.setTaskConfigurations(scope, []);
}
}));
scopesToClear.clear();
}
/**
* @param {() => TaskConfigurationScope} scopeGenerator a function to allow lazy evaluation of the second workspace root.
* @param {string} scopeLabel
* @param {boolean} only
*/
function setAndRetrieveTasks(scopeGenerator, scopeLabel, only = false) {
const testFunction = only ? it.only : it;
testFunction(`successfully handles ${scopeLabel} scope`, async () => {
const scope = scopeGenerator();
scopesToClear.add(scope);
const initialTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(initialTasks, []);
await taskConfigurationManager.setTaskConfigurations(scope, [basicTaskConfig]);
const newTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(newTasks, [basicTaskConfig]);
});
}
/* UNCOMMENT TO RUN MULTI-ROOT TESTS */
// const { FileService } = require('@theia/filesystem/lib/browser/file-service');
// const { EnvVariablesServer } = require('@theia/core/lib/common/env-variables');
// const URI = require('@theia/core/lib/common/uri').default;
// const fileService = container.get(FileService);
// /** @type {EnvVariablesServer} */
// const envVariables = container.get(EnvVariablesServer);
// describe('in a multi-root workspace', () => {
// let secondWorkspaceRoot = '';
// before(async () => {
// const configLocation = await envVariables.getConfigDirUri();
// const secondWorkspaceRootURI = new URI(configLocation).parent.resolve(`test-root-${Date.now()}`);
// secondWorkspaceRoot = secondWorkspaceRootURI.toString();
// await fileService.createFolder(secondWorkspaceRootURI);
// /** @type {Promise<void>} */
// const waitForEvent = new Promise(resolve => {
// const listener = taskConfigurationManager.onDidChangeTaskConfig(() => {
// listener.dispose();
// resolve();
// });
// });
// workspaceService.addRoot(secondWorkspaceRootURI);
// return waitForEvent;
// });
// beforeEach(() => clearTasks());
// after(() => clearTasks());
// setAndRetrieveTasks(() => TaskScope.Global, 'user');
// setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
// setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder (1)');
// setAndRetrieveTasks(() => secondWorkspaceRoot, 'folder (2)');
// });
});

View File

@@ -0,0 +1,873 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('TypeScript', function () {
this.timeout(360_000);
const { assert } = chai;
const { timeout } = require('@theia/core/lib/common/promise-util');
const { MenuModelRegistry } = require('@theia/core/lib/common/menu/menu-model-registry');
const Uri = require('@theia/core/lib/common/uri');
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { BrowserMainMenuFactory } = require('@theia/core/lib/browser/menu/browser-menu-plugin');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
const { EDITOR_CONTEXT_MENU } = require('@theia/editor/lib/browser/editor-menu');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
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 { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
const { PreferenceScope } = require('@theia/core/lib/common/preferences/preference-scope');
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
const { PluginViewRegistry } = require('@theia/plugin-ext/lib/main/browser/view/plugin-view-registry');
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
const { Selection } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/selection');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const menuFactory = container.get(BrowserMainMenuFactory);
const menuRegistry = container.get(MenuModelRegistry);
const pluginService = container.get(HostedPluginSupport);
const contextKeyService = container.get(ContextKeyService);
const commands = container.get(CommandRegistry);
const openerService = container.get(OpenerService);
/** @type {KeybindingRegistry} */
const keybindings = container.get(KeybindingRegistry);
/** @type {import('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
const preferences = container.get(PreferenceService);
const progressStatusBarItem = container.get(ProgressStatusBarItem);
/** @type {PluginViewRegistry} */
const pluginViewRegistry = container.get(PluginViewRegistry);
const typescriptPluginId = 'vscode.typescript-language-features';
const referencesPluginId = 'vscode.references-view';
/** @type Uri.URI */
const rootUri = workspaceService.tryGetRoots()[0].resource;
const demoFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-file.ts');
const definitionFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-definitions-file.ts');
let originalAutoSaveValue = preferences.get('files.autoSave');
before(async function () {
await pluginService.didStart;
await Promise.all([typescriptPluginId, referencesPluginId].map(async pluginId => {
if (!pluginService.getPlugin(pluginId)) {
throw new Error(pluginId + ' should be started');
}
await pluginService.activatePlugin(pluginId);
}));
await preferences.set('files.autoSave', 'off');
await preferences.set('files.refactoring.autoSave', 'off');
});
beforeEach(async function () {
await editorManager.closeAll({ save: false });
await new Promise(resolve => setTimeout(resolve, 500));
});
const toTearDown = new DisposableCollection();
afterEach(async () => {
toTearDown.dispose();
await editorManager.closeAll({ save: false });
await new Promise(resolve => setTimeout(resolve, 500));
});
after(async () => {
await preferences.set('files.autoSave', originalAutoSaveValue);
})
async function waitLanguageServerReady() {
// quite a bit of jitter in the "Initializing LS" status bar entry,
// so we want to read a few times in a row that it's done (undefined)
const MAX_N = 5
let n = MAX_N;
while (n > 0) {
await timeout(1000);
if (progressStatusBarItem.currentProgress) {
n = MAX_N;
} else {
n--;
}
if (n < 5) {
console.debug('n = ' + n);
}
}
}
/**
* @param {Uri.default} uri
* @param {boolean} preview
*/
async function openEditor(uri, preview = false) {
const widget = await open(openerService, uri, { mode: 'activate', preview });
const editorWidget = widget instanceof EditorWidget ? widget : undefined;
const editor = MonacoEditor.get(editorWidget);
assert.isDefined(editor);
// wait till tsserver is running, see:
// https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103
await waitForAnimation(() => contextKeyService.match('typescript.isManagedFile'), 1000000, 'waiting for "typescript.isManagedFile"');
waitLanguageServerReady();
return /** @type {MonacoEditor} */ (editor);
}
/**
* @param {() => unknown} condition
* @param {number | undefined} [maxWait]
* @param {string | function | undefined} [message]
* @returns {Promise<void>}
*/
async function waitForAnimation(condition, maxWait, message) {
if (maxWait === undefined) {
maxWait = 100000;
}
const endTime = Date.now() + maxWait;
do {
await (timeout(100));
if (condition()) {
return;
}
if (Date.now() > endTime) {
throw new Error((typeof message === 'function' ? message() : message) ?? 'Wait for animation timed out.');
}
} while (true);
}
/**
* We ignore attributes on purpose since they are not stable.
* But structure is important for us to see whether the plain text is rendered or markdown.
*
* @param {Element} element
* @returns {string}
*/
function nodeAsString(element, indentation = '') {
if (!element) {
return '';
}
const header = element.tagName;
let body = '';
const childIndentation = indentation + ' ';
for (let i = 0; i < element.childNodes.length; i++) {
const childNode = element.childNodes.item(i);
if (childNode.nodeType === childNode.TEXT_NODE) {
body += childIndentation + `"${childNode.textContent}"` + '\n';
} else if (childNode instanceof HTMLElement) {
body += childIndentation + nodeAsString(childNode, childIndentation) + '\n';
}
}
const result = header + (body ? ' {\n' + body + indentation + '}' : '');
if (indentation) {
return result;
}
return `\n${result}\n`;
}
/**
* @param {MonacoEditor} editor
*/
async function assertPeekOpened(editor) {
/** @type any */
const referencesController = editor.getControl().getContribution('editor.contrib.referencesController');
await waitForAnimation(() => referencesController._widget && referencesController._widget._tree.getFocus().length);
assert.isFalse(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('referenceSearchVisible'));
assert.isTrue(contextKeyService.match('listFocus'));
}
/**
* @param {MonacoEditor} editor
*/
async function openPeek(editor) {
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('referenceSearchVisible'));
assert.isFalse(contextKeyService.match('listFocus'));
await commands.executeCommand('editor.action.peekDefinition');
await assertPeekOpened(editor);
}
async function openReference() {
keybindings.dispatchKeyDown('Enter');
await waitForAnimation(() => contextKeyService.match('listFocus'));
assert.isFalse(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('referenceSearchVisible'));
assert.isTrue(contextKeyService.match('listFocus'));
}
/**
* @param {MonacoEditor} editor
*/
async function closePeek(editor) {
await assertPeekOpened(editor);
console.log('closePeek() - Attempt to close by sending "Escape"');
await dismissWithEscape('listFocus');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('referenceSearchVisible'));
assert.isFalse(contextKeyService.match('listFocus'));
}
it('document formatting should be visible and enabled', async function () {
await openEditor(demoFileUri);
const menu = menuFactory.createContextMenu(EDITOR_CONTEXT_MENU, menuRegistry.getMenu(EDITOR_CONTEXT_MENU), contextKeyService);
const item = menu.items.find(i => i.command === 'editor.action.formatDocument');
if (item) {
assert.isTrue(item.isVisible, 'item is visible');
assert.isTrue(item.isEnabled, 'item is enabled');
} else {
assert.isDefined(item, 'item is defined');
}
});
describe('editor.action.revealDefinition', function () {
for (const preview of [false, true]) {
const from = 'an editor' + (preview ? ' preview' : '');
it('within ' + from, async function () {
const editor = await openEditor(demoFileUri, preview);
// const demoInstance = new Demo|Class('demo');
editor.getControl().setPosition({ lineNumber: 28, column: 5 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoVariable');
await commands.executeCommand('editor.action.revealDefinition');
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(editorManager.activeEditor.isPreview, preview);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// constructor(someString: string) {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 7 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'demoVariable');
});
// Note: this test generate annoying but apparently harmless error traces, during cleanup:
// [Error: Error: Cannot update an unmounted root.
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
it(`from ${from} to another editor`, async function () {
await editorManager.open(definitionFileUri, { mode: 'open' });
const editor = await openEditor(demoFileUri, preview);
// const bar: Defined|Interface = { coolField: [] };
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
await commands.executeCommand('editor.action.revealDefinition');
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isFalse(editorManager.activeEditor.isPreview);
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
// export interface |DefinedInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
});
it(`from ${from} to an editor preview`, async function () {
const editor = await openEditor(demoFileUri);
// const bar: Defined|Interface = { coolField: [] };
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
await commands.executeCommand('editor.action.revealDefinition');
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isTrue(editorManager.activeEditor.isPreview);
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
// export interface |DefinedInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
});
}
});
describe('editor.action.peekDefinition', function () {
for (const preview of [false, true]) {
const from = 'an editor' + (preview ? ' preview' : '');
it('within ' + from, async function () {
const editor = await openEditor(demoFileUri, preview);
editor.getControl().revealLine(24);
// const demoInstance = new Demo|Class('demo');
editor.getControl().setPosition({ lineNumber: 24, column: 30 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
await openPeek(editor);
await openReference();
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(editorManager.activeEditor.isPreview, preview);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// constructor(someString: string) {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 5 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'constructor');
await closePeek(activeEditor);
});
// Note: this test generate annoying but apparently harmless error traces, during cleanup:
// [Error: Error: Cannot update an unmounted root.
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
it(`from ${from} to another editor`, async function () {
await editorManager.open(definitionFileUri, { mode: 'open' });
const editor = await openEditor(demoFileUri, preview);
editor.getControl().revealLine(32);
// const bar: Defined|Interface = { coolField: [] };
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
await openPeek(editor);
await openReference();
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isFalse(editorManager.activeEditor.isPreview);
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
// export interface |DefinedInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
await closePeek(activeEditor);
});
it(`from ${from} to an editor preview`, async function () {
const editor = await openEditor(demoFileUri);
editor.getControl().revealLine(32);
// const bar: Defined|Interface = { coolField: [] };
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
await openPeek(editor);
await openReference();
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isTrue(editorManager.activeEditor.isPreview);
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
// export interface |DefinedInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
await closePeek(activeEditor);
});
}
});
it('editor.action.triggerSuggest', async function () {
const editor = await openEditor(demoFileUri);
editor.getControl().setPosition({ lineNumber: 26, column: 46 });
editor.getControl().setSelection(new Selection(26, 46, 26, 35));
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'stringField');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
await commands.executeCommand('editor.action.triggerSuggest');
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible'));
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
const suggestController = editor.getControl().getContribution('editor.contrib.suggestController');
waitForAnimation(() => {
const content = suggestController ? nodeAsString(suggestController['_widget']?.['_value']?.['element']?.['domNode']) : '';
return !content.includes('loading');
});
// May need a couple extra "Enter" being sent for the suggest to be accepted
keybindings.dispatchKeyDown('Enter');
await waitForAnimation(() => {
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible');
if (!suggestWidgetDismissed) {
console.log('Re-try accepting suggest using "Enter" key');
keybindings.dispatchKeyDown('Enter');
return false;
}
return true;
}, 20000, 'Suggest widget has not been dismissed despite attempts to accept suggestion');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// demoInstance.stringField;
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 46 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'doSomething');
});
it('editor.action.triggerSuggest navigate', async function () {
const editor = await openEditor(demoFileUri);
// demoInstance.[|stringField];
editor.getControl().setPosition({ lineNumber: 26, column: 46 });
editor.getControl().setSelection(new Selection(26, 46, 26, 35));
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'stringField');
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/suggest/browser/suggestController').SuggestController} */
const suggest = editor.getControl().getContribution('editor.contrib.suggestController');
const getFocusedLabel = () => {
const focusedItem = suggest.widget.value.getFocusedItem();
return focusedItem && focusedItem.item.completion.label;
};
assert.isUndefined(getFocusedLabel());
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
await commands.executeCommand('editor.action.triggerSuggest');
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'doSomething', 5000);
assert.equal(getFocusedLabel(), 'doSomething');
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
keybindings.dispatchKeyDown('ArrowDown');
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'numberField', 2000);
assert.equal(getFocusedLabel(), 'numberField');
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
keybindings.dispatchKeyDown('ArrowUp');
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'doSomething', 2000);
assert.equal(getFocusedLabel(), 'doSomething');
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
keybindings.dispatchKeyDown('Escape');
// once in a while, a second "Escape" is needed to dismiss widget
await waitForAnimation(() => {
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === undefined;
if (!suggestWidgetDismissed) {
console.log('Re-try to dismiss suggest using "Escape" key');
keybindings.dispatchKeyDown('Escape');
return false;
}
return true;
}, 5000, 'Suggest widget not dismissed');
assert.isUndefined(getFocusedLabel());
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
});
it('editor.action.rename', async function () {
const editor = await openEditor(demoFileUri);
// const |demoVariable = demoInstance.stringField;
editor.getControl().setPosition({ lineNumber: 26, column: 7 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoVariable');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('renameInputVisible'));
commands.executeCommand('editor.action.rename');
await waitForAnimation(() => contextKeyService.match('renameInputVisible')
&& document.activeElement instanceof HTMLInputElement
&& document.activeElement.selectionEnd === 'demoVariable'.length);
assert.isFalse(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('renameInputVisible'));
const input = document.activeElement;
if (!(input instanceof HTMLInputElement)) {
assert.fail('expected focused input, but: ' + input);
return;
}
input.value = 'foo';
keybindings.dispatchKeyDown('Enter', input);
// all rename edits should be grouped in one edit operation and applied in the same tick
await new Promise(resolve => editor.getControl().onDidChangeModelContent(resolve));
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('renameInputVisible'));
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// const |foo = new Container();
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 7 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber: 28, column: 1 }).word, 'foo');
});
async function dismissWithEscape(contextKey) {
keybindings.dispatchKeyDown('Escape');
// once in a while, a second "Escape" is needed to dismiss widget
return waitForAnimation(() => {
const suggestWidgetDismissed = !contextKeyService.match(contextKey);
if (!suggestWidgetDismissed) {
console.log(`Re-try to dismiss ${contextKey} using "Escape" key`);
keybindings.dispatchKeyDown('Escape');
return false;
}
return true;
}, 5000, `${contextKey} widget not dismissed`);
}
it('editor.action.triggerParameterHints', async function () {
this.timeout(30000);
console.log('start trigger parameter hint');
const editor = await openEditor(demoFileUri);
// const demoInstance = new DemoClass('|demo');
editor.getControl().setPosition({ lineNumber: 24, column: 37 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, "demo");
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
await commands.executeCommand('editor.action.triggerParameterHints');
console.log('trigger command');
await waitForAnimation(() => contextKeyService.match('parameterHintsVisible'));
console.log('context key matched');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isTrue(contextKeyService.match('parameterHintsVisible'));
await dismissWithEscape('parameterHintsVisible');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
});
it('editor.action.showHover', async function () {
const editor = await openEditor(demoFileUri);
// class |DemoClass);
editor.getControl().setPosition({ lineNumber: 8, column: 7 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/hover/browser/contentHoverController').ContentHoverController} */
const hover = editor.getControl().getContribution('editor.contrib.contentHover');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(contextKeyService.match('editorHoverVisible'));
await commands.executeCommand('editor.action.showHover');
let doLog = true;
await waitForAnimation(() => contextKeyService.match('editorHoverVisible'));
assert.isTrue(contextKeyService.match('editorHoverVisible'));
assert.isTrue(contextKeyService.match('editorTextFocus'));
waitForAnimation(() => {
const content = nodeAsString(hover['_contentWidget']?.['widget']?.['_hover']?.['contentsDomNode']);
return !content.includes('loading');
});
const content = nodeAsString(hover['_contentWidget']?.['widget']?.['_hover']?.['contentsDomNode']);
assert.isTrue(content.includes('class', 'did not include'));
assert.isTrue(content.includes('DemoClass', 'did not include'));
await dismissWithEscape('editorHoverVisible');
assert.isTrue(contextKeyService.match('editorTextFocus'));
assert.isFalse(Boolean(hover['_contentWidget']?.['_widget']?.['_visibleData']));
});
it('highlight semantic (write) occurrences', async function () {
const editor = await openEditor(demoFileUri);
// const |container = new Container();
const lineNumber = 24;
const column = 7;
const endColumn = column + 'demoInstance'.length;
const hasWriteDecoration = () => {
for (const decoration of editor.getControl().getModel().getLineDecorations(lineNumber)) {
if (decoration.range.startColumn === column && decoration.range.endColumn === endColumn && decoration.options.className === 'wordHighlightStrong') {
return true;
}
}
return false;
};
assert.isFalse(hasWriteDecoration());
editor.getControl().setPosition({ lineNumber, column });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
// highlight occurrences is not trigged on the explicit position change, so move a cursor as a user
keybindings.dispatchKeyDown('ArrowRight');
await waitForAnimation(() => hasWriteDecoration());
assert.isTrue(hasWriteDecoration());
});
it('editor.action.goToImplementation', async function () {
const editor = await openEditor(demoFileUri);
// const demoInstance = new Demo|Class('demo');
editor.getControl().setPosition({ lineNumber: 24, column: 30 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
await commands.executeCommand('editor.action.goToImplementation');
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// class |DemoClass implements DemoInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 8, column: 7 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DemoClass');
});
it('editor.action.goToTypeDefinition', async function () {
const editor = await openEditor(demoFileUri);
// const demoVariable = demo|Instance.stringField;
editor.getControl().setPosition({ lineNumber: 26, column: 26 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
await commands.executeCommand('editor.action.goToTypeDefinition');
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
// class |DemoClass implements DemoInterface {
const { lineNumber, column } = activeEditor.getControl().getPosition();
assert.deepEqual({ lineNumber, column }, { lineNumber: 8, column: 7 });
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DemoClass');
});
it('run reference code lens', async function () {
const preferenceName = 'typescript.referencesCodeLens.enabled';
const globalValue = preferences.inspect(preferenceName).globalValue;
toTearDown.push({ dispose: () => preferences.set(preferenceName, globalValue, PreferenceScope.User) });
await preferences.set(preferenceName, false, PreferenceScope.User);
const editor = await openEditor(demoFileUri);
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codelens/browser/codelensController').CodeLensContribution} */
const codeLens = editor.getControl().getContribution('css.editor.codeLens');
const codeLensNode = () => codeLens['_lenses'][0]?.['_contentWidget']?.['_domNode'];
const codeLensNodeVisible = () => {
const n = codeLensNode();
return !!n && n.style.visibility !== 'hidden';
};
assert.isFalse(codeLensNodeVisible());
// |interface DemoInterface {
const position = { lineNumber: 2, column: 1 };
await preferences.set(preferenceName, true, PreferenceScope.User);
editor.getControl().revealPosition(position);
await waitForAnimation(() => codeLensNodeVisible());
assert.isTrue(codeLensNodeVisible());
const node = codeLensNode();
assert.isDefined(node);
assert.equal(nodeAsString(node), `
SPAN {
A {
"1 reference"
}
}
`);
const link = node.getElementsByTagName('a').item(0);
assert.isDefined(link);
link.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
await assertPeekOpened(editor);
await closePeek(editor);
});
it('editor.action.quickFix', async function () {
const column = 45;
const lineNumber = 26;
const editor = await openEditor(demoFileUri);
const currentChar = () => editor.getControl().getModel().getLineContent(lineNumber).charAt(column - 1);
editor.getControl().getModel().applyEdits([{
range: {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn: 45,
endColumn: 46
},
forceMoveMarkers: false,
text: ''
}]);
editor.getControl().setPosition({ lineNumber, column });
editor.getControl().revealPosition({ lineNumber, column });
assert.equal(currentChar(), ';', 'Failed at assert 1');
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
const lightBulbNode = () => {
const lightBulb = codeActionController['_lightBulbWidget'].rawValue;
return lightBulb && lightBulb['_domNode'];
};
const lightBulbVisible = () => {
const node = lightBulbNode();
return !!node && node.style.visibility !== 'hidden';
};
await timeout(1000); // quick fix is always available: need to wait for the error fix to become available.
await commands.executeCommand('editor.action.quickFix');
const codeActionSelector = '.action-widget';
assert.isFalse(!!document.querySelector(codeActionSelector), 'Failed at assert 3 - codeActionWidget should not be visible');
console.log('Waiting for Quick Fix widget to be visible');
await waitForAnimation(() => {
const quickFixWidgetVisible = !!document.querySelector(codeActionSelector);
if (!quickFixWidgetVisible) {
// console.log('...');
return false;
}
return true;
}, 10000, 'Timed-out waiting for the QuickFix widget to appear');
await timeout();
assert.isTrue(lightBulbVisible(), 'Failed at assert 4');
keybindings.dispatchKeyDown('Enter');
console.log('Waiting for confirmation that QuickFix has taken effect');
await waitForAnimation(() => currentChar() === 'd', 10000, 'Failed to detect expected selected char: "d"');
assert.equal(currentChar(), 'd', 'Failed at assert 5');
});
it('editor.action.formatDocument', async function () {
const lineNumber = 5;
const editor = await openEditor(demoFileUri);
const originalLength = editor.getControl().getModel().getLineLength(lineNumber);
// doSomething(): number; --> doSomething() : number;
editor.getControl().getModel().applyEdits([{
range: Range.fromPositions({ lineNumber, column: 18 }, { lineNumber, column: 18 }),
forceMoveMarkers: false,
text: ' '
}]);
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength + 1);
await commands.executeCommand('editor.action.formatDocument');
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength);
});
it('editor.action.formatSelection', async function () {
// doSomething(): number {
const lineNumber = 15;
const editor = await openEditor(demoFileUri);
const originalLength /* 28 */ = editor.getControl().getModel().getLineLength(lineNumber);
// doSomething( ) : number {
editor.getControl().getModel().applyEdits([{
range: Range.fromPositions({ lineNumber, column: 17 }, { lineNumber, column: 18 }),
forceMoveMarkers: false,
text: ' ) '
}]);
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength + 4);
// [const { Container }] = require('inversify');
editor.getControl().setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 32 });
await commands.executeCommand('editor.action.formatSelection');
// [const { Container }] = require('inversify');
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength);
});
it('Can execute code actions', async function () {
const editor = await openEditor(demoFileUri);
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
const isActionAvailable = () => {
const lightbulbVisibility = codeActionController['_lightBulbWidget'].rawValue?.['_domNode'].style.visibility;
return lightbulbVisibility !== undefined && lightbulbVisibility !== 'hidden';
}
assert.strictEqual(editor.getControl().getModel().getLineContent(30), 'import { DefinedInterface } from "./demo-definitions-file";');
editor.getControl().revealLine(30);
editor.getControl().setSelection(new Selection(30, 1, 30, 60));
await waitForAnimation(() => isActionAvailable(), 5000, 'No code action available. (1)');
assert.isTrue(isActionAvailable());
await timeout(1000)
await commands.executeCommand('editor.action.quickFix');
await waitForAnimation(() => {
const elements = document.querySelector('.action-widget');
return !!elements;
}, 5000, 'No context menu appeared. (1)');
await timeout();
keybindings.dispatchKeyDown('Enter');
assert.isNotNull(editor.getControl());
assert.isNotNull(editor.getControl().getModel());
console.log(`content: ${editor.getControl().getModel().getLineContent(30)}`);
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import * as demoDefinitionsFile from "./demo-definitions-file";', 5000, 'The namespace import did not take effect :' + editor.getControl().getModel().getLineContent(30));
// momentarily toggle selection, waiting for code action to become unavailable.
// Without doing this, the call to the quickfix command would sometimes fail because of an
// unexpected "no code action available" pop-up, which would trip the rest of the testcase
editor.getControl().setSelection(new Selection(30, 1, 30, 1));
console.log('waiting for code action to no longer be available');
await waitForAnimation(() => {
if (!isActionAvailable()) {
return true;
}
editor.getControl().setSelection(new Selection(30, 1, 30, 1));
console.log('...');
return !isActionAvailable();
}, 5000, 'Code action still available with no proper selection.');
// re-establish selection
editor.getControl().setSelection(new Selection(30, 1, 30, 64));
console.log('waiting for code action to become available again');
await waitForAnimation(() => {
console.log('...');
return isActionAvailable()
}, 5000, 'No code action available. (2)');
// Change import back: https://github.com/eclipse-theia/theia/issues/11059
await commands.executeCommand('editor.action.quickFix');
await waitForAnimation(() => Boolean(document.querySelector('.context-view-pointerBlock')), 5000, 'No context menu appeared. (2)');
await timeout();
keybindings.dispatchKeyDown('Enter');
assert.isNotNull(editor.getControl());
assert.isNotNull(editor.getControl().getModel());
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import { DefinedInterface } from "./demo-definitions-file";', 10000, () => 'The named import did not take effect.' + editor.getControl().getModel().getLineContent(30));
});
for (const referenceViewCommand of ['references-view.find', 'references-view.findImplementations']) {
it(referenceViewCommand, async function () {
let steps = 0;
const editor = await openEditor(demoFileUri);
editor.getControl().setPosition({ lineNumber: 24, column: 11 });
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
await commands.executeCommand(referenceViewCommand);
const view = await pluginViewRegistry.openView('references-view.tree', { reveal: true });
const expectedMessage = referenceViewCommand === 'references-view.find' ? '2 results in 1 file' : '1 result in 1 file';
const getResultText = () => view.node.getElementsByClassName('theia-TreeViewInfo').item(0)?.textContent;
await waitForAnimation(() => getResultText() === expectedMessage, 5000);
assert.equal(getResultText(), expectedMessage);
});
}
});

View File

@@ -0,0 +1,204 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Undo, Redo and Select All', function () {
this.timeout(5000);
const { assert } = chai;
const { timeout } = require('@theia/core/lib/common/promise-util');
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { CommandRegistry } = require('@theia/core/lib/common/command');
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const commands = container.get(CommandRegistry);
const keybindings = container.get(KeybindingRegistry);
const navigatorContribution = container.get(FileNavigatorContribution);
const shell = container.get(ApplicationShell);
const scmContribution = container.get(ScmContribution);
/** @type {PreferenceService} */
const preferenceService = container.get(PreferenceService)
const rootUri = workspaceService.tryGetRoots()[0].resource;
const fileUri = rootUri.resolve('webpack.config.js');
const toTearDown = new DisposableCollection();
/**
* @param {() => unknown} condition
* @param {number | undefined} [maxWait]
* @param {string | undefined} [message]
* @returns {Promise<void>}
*/
async function waitForAnimation(condition, maxWait, message) {
if (maxWait === undefined) {
maxWait = 100000;
}
const endTime = Date.now() + maxWait;
do {
await (timeout(100));
if (condition()) {
return true;
}
if (Date.now() > endTime) {
throw new reject(new Error(message ?? 'Wait for animation timed out.'));
}
} while (true);
}
const originalValue = preferenceService.get('files.autoSave', undefined, rootUri.toString());
before(async () => {
await preferenceService.set('files.autoSave', 'off', undefined, rootUri.toString());
await preferenceService.set('git.autoRepositoryDetection', true);
await preferenceService.set('git.openRepositoryInParentFolders', 'always');
shell.leftPanelHandler.collapse();
});
beforeEach(async function () {
await scmContribution.closeView();
await navigatorContribution.closeView();
await editorManager.closeAll({ save: false });
});
afterEach(async () => {
toTearDown.dispose();
await scmContribution.closeView();
await navigatorContribution.closeView();
await editorManager.closeAll({ save: false });
});
after(async () => {
await preferenceService.set('files.autoSave', originalValue, undefined, rootUri.toString());
shell.leftPanelHandler.collapse();
});
/**
* @param {import('@theia/editor/lib/browser/editor-widget').EditorWidget} widget
*/
async function assertInEditor(widget) {
const originalContent = widget.editor.document.getText();
const editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
editor.getControl().pushUndoStop();
editor.getControl().executeEdits('test', [{
range: new Range(1, 1, 1, 1),
text: 'A'
}]);
editor.getControl().pushUndoStop();
const modifiedContent = widget.editor.document.getText();
assert.notEqual(modifiedContent, originalContent);
keybindings.dispatchCommand(CommonCommands.UNDO.id);
await waitForAnimation(() => widget.editor.document.getText() === originalContent);
assert.equal(widget.editor.document.getText(), originalContent);
keybindings.dispatchCommand(CommonCommands.REDO.id);
await waitForAnimation(() => widget.editor.document.getText() === modifiedContent);
assert.equal(widget.editor.document.getText(), modifiedContent);
const originalSelection = widget.editor.selection;
keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id);
await waitForAnimation(() => widget.editor.selection.end.line !== originalSelection.end.line);
assert.notDeepEqual(widget.editor.selection, originalSelection);
}
it('in the active editor', async function () {
await navigatorContribution.openView({ activate: true });
const widget = await editorManager.open(fileUri, { mode: 'activate' });
await assertInEditor(widget);
});
it('in the active explorer without the current editor', async function () {
await navigatorContribution.openView({ activate: true });
// should not throw
await commands.executeCommand(CommonCommands.UNDO.id);
await commands.executeCommand(CommonCommands.REDO.id);
await commands.executeCommand(CommonCommands.SELECT_ALL.id);
});
it('in the active explorer with the current editor', async function () {
const widget = await editorManager.open(fileUri, { mode: 'activate' });
await navigatorContribution.openView({ activate: true });
await assertInEditor(widget);
});
async function assertInScm() {
const scmInput = document.activeElement;
if (!(scmInput instanceof HTMLTextAreaElement)) {
assert.isTrue(scmInput instanceof HTMLTextAreaElement);
return;
}
const originalValue = scmInput.value;
document.execCommand('insertText', false, 'A');
await waitForAnimation(() => scmInput.value !== originalValue);
const modifiedValue = scmInput.value;
assert.notEqual(originalValue, modifiedValue);
keybindings.dispatchCommand(CommonCommands.UNDO.id);
await waitForAnimation(() => scmInput.value === originalValue);
assert.equal(scmInput.value, originalValue, 'value equal');
keybindings.dispatchCommand(CommonCommands.REDO.id);
await waitForAnimation(() => scmInput.value === modifiedValue);
assert.equal(scmInput.value, modifiedValue, 'value not equal');
const selection = document.getSelection();
if (!selection) {
assert.isDefined(selection, 'selection defined');
return;
}
selection.empty();
assert.equal(selection.rangeCount, 0, 'rangeCount equal');
keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id);
await waitForAnimation(() => !!selection.rangeCount);
assert.notEqual(selection.rangeCount, 0, 'rangeCount not equal');
assert.isTrue(selection.containsNode(scmInput), 'selection contains');
}
it('in the active scm in workspace without the current editor', async function () {
await scmContribution.openView({ activate: true });
await assertInScm();
});
it('in the active scm in workspace with the current editor', async function () {
await editorManager.open(fileUri, { mode: 'activate' });
await scmContribution.openView({ activate: true });
await assertInScm();
});
});

View File

@@ -0,0 +1,80 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Views', function () {
this.timeout(7500);
const { assert } = chai;
const { timeout } = require('@theia/core/lib/common/promise-util');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { OutlineViewContribution } = require('@theia/outline-view/lib/browser/outline-view-contribution');
const { ProblemContribution } = require('@theia/markers/lib/browser/problem/problem-contribution');
const { PropertyViewContribution } = require('@theia/property-view/lib/browser/property-view-contribution');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const shell = container.get(ApplicationShell);
const navigatorContribution = container.get(FileNavigatorContribution);
const scmContribution = container.get(ScmContribution);
const outlineContribution = container.get(OutlineViewContribution);
const problemContribution = container.get(ProblemContribution);
const propertyViewContribution = container.get(PropertyViewContribution);
const pluginService = container.get(HostedPluginSupport);
before(() => Promise.all([
shell.leftPanelHandler.collapse(),
(async function () {
await pluginService.didStart;
await pluginService.activateByViewContainer('explorer');
})()
]));
for (const contribution of [navigatorContribution, scmContribution, outlineContribution, problemContribution, propertyViewContribution]) {
it(`should toggle ${contribution.viewLabel}`, async function () {
let view = await contribution.closeView();
if (view) {
assert.notEqual(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isFalse(view.isVisible);
assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
}
view = await contribution.toggleView();
// we can't use "equals" here because Mocha chokes on the diff for certain widgets
assert.isTrue(view !== undefined, `${contribution.viewLabel} !== undefined`);
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isDefined(shell.getTabBarFor(view));
// @ts-ignore
assert.equal(shell.getAreaFor(shell.getTabBarFor(view)), contribution.defaultViewOptions.area);
assert.isTrue(view.isVisible);
assert.isTrue(view === shell.activeWidget, `${contribution.viewLabel} === shell.activeWidget`);
view = await contribution.toggleView();
await timeout(0); // seems that the "await" is not enought to guarantee that the panel is hidden
assert.notEqual(view, undefined);
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
assert.isDefined(shell.getTabBarFor(view));
assert.isFalse(view.isVisible);
assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
});
}
});

View File

@@ -0,0 +1,4 @@
export interface DefinedInterface {
coolField: number[];
}

View File

@@ -0,0 +1,32 @@
interface DemoInterface {
stringField: string;
numberField: number;
doSomething(): number;
}
class DemoClass implements DemoInterface {
stringField: string;
numberField: number;
constructor(someString: string) {
this.stringField = someString;
this.numberField = this.stringField.length;
}
doSomething(): number {
let output = 0;
for (let i = 0; i < this.stringField.length; i++) {
output += this.stringField.charCodeAt(i);
}
return output;
}
}
const demoInstance = new DemoClass('demo');
const demoVariable = demoInstance.stringField;
demoVariable.concat('-string');
import { DefinedInterface } from "./demo-definitions-file";
const bar: DefinedInterface = { coolField: [] };

View File

@@ -0,0 +1,30 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noEmitOnError": false,
"noImplicitThis": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"importHelpers": true,
"downlevelIteration": true,
"resolveJsonModule": true,
"useDefineForClassFields": false,
"module": "CommonJS",
"moduleResolution": "Node",
"target": "ES2023",
"jsx": "react",
"lib": [
"ES2023",
"DOM",
"DOM.AsyncIterable"
],
"sourceMap": true
}
}