deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
24
examples/api-tests/package.json
Normal file
24
examples/api-tests/package.json
Normal 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
21
examples/api-tests/src/api-tests.d.ts
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
54
examples/api-tests/src/browser-utils.spec.js
Normal file
54
examples/api-tests/src/browser-utils.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
36
examples/api-tests/src/contribution-filter.spec.js
Normal file
36
examples/api-tests/src/contribution-filter.spec.js
Normal 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"');
|
||||
});
|
||||
|
||||
});
|
||||
76
examples/api-tests/src/credentials-service.spec.js
Normal file
76
examples/api-tests/src/credentials-service.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
137
examples/api-tests/src/explorer-open-close.spec.js
Normal file
137
examples/api-tests/src/explorer-open-close.spec.js
Normal 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);
|
||||
}
|
||||
|
||||
});
|
||||
133
examples/api-tests/src/file-search.spec.js
Normal file
133
examples/api-tests/src/file-search.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
151
examples/api-tests/src/find-replace.spec.js
Normal file
151
examples/api-tests/src/find-replace.spec.js
Normal 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());
|
||||
}
|
||||
});
|
||||
116
examples/api-tests/src/keybindings.spec.js
Normal file
116
examples/api-tests/src/keybindings.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
731
examples/api-tests/src/launch-preferences.spec.js
Normal file
731
examples/api-tests/src/launch-preferences.spec.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
179
examples/api-tests/src/menus.spec.js
Normal file
179
examples/api-tests/src/menus.spec.js
Normal 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.');
|
||||
});
|
||||
|
||||
});
|
||||
198
examples/api-tests/src/monaco-api.spec.js
Normal file
198
examples/api-tests/src/monaco-api.spec.js
Normal 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));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
92
examples/api-tests/src/navigator.spec.js
Normal file
92
examples/api-tests/src/navigator.spec.js
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
209
examples/api-tests/src/preferences.spec.js
Normal file
209
examples/api-tests/src/preferences.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
512
examples/api-tests/src/saveable.spec.js
Normal file
512
examples/api-tests/src/saveable.spec.js
Normal 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
222
examples/api-tests/src/scm.spec.js
Normal file
222
examples/api-tests/src/scm.spec.js
Normal 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'));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
41
examples/api-tests/src/shell.spec.js
Normal file
41
examples/api-tests/src/shell.spec.js
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
112
examples/api-tests/src/task-configurations.spec.js
Normal file
112
examples/api-tests/src/task-configurations.spec.js
Normal 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)');
|
||||
// });
|
||||
});
|
||||
873
examples/api-tests/src/typescript.spec.js
Normal file
873
examples/api-tests/src/typescript.spec.js
Normal 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
204
examples/api-tests/src/undo-redo-selectAll.spec.js
Normal file
204
examples/api-tests/src/undo-redo-selectAll.spec.js
Normal 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();
|
||||
});
|
||||
|
||||
});
|
||||
80
examples/api-tests/src/views.spec.js
Normal file
80
examples/api-tests/src/views.spec.js
Normal 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`);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
export interface DefinedInterface {
|
||||
coolField: number[];
|
||||
}
|
||||
32
examples/api-tests/test-ts-workspace/demo-file.ts
Normal file
32
examples/api-tests/test-ts-workspace/demo-file.ts
Normal 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: [] };
|
||||
30
examples/api-tests/test-ts-workspace/tsconfig.json
Normal file
30
examples/api-tests/test-ts-workspace/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user