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

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

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,50 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - API PROVIDER SAMPLE</h2>
<hr />
</div>
## Description
The `@theia/api-provider-sample` extension is a programming example showing how to define and provide a custom API object for _plugins_ to use.
The purpose of the extension is to:
- provide developers with realistic coding examples of providing custom API objects
- provide easy-to-use and test examples for features when reviewing pull requests
The extension is for reference and test purposes only and is not published on `npm` (`private: true`).
### Greeting of the Day
The sample defines a `gotd` API that plugins can import and use to obtain tailored messages with which to greet the world, for example in their activation function.
The source code is laid out in the `src/` tree as follows:
- `gotd.d.ts` — the TypeScript definition of the `gotd` API object that plugins import to interact with the "Greeting of the Day" service
- `plugin/` — the API initialization script and the implementation of the API objects (`GreetingExt` and similar interfaces).
All code in this directory runs exclusively in the separate plugin-host Node process, isolated from the main Theia process, together with either headless plugins or the backend of VS Code plugins.
The `GreetingExtImpl` and similar classes communicate with the actual API implementation (`GreetingMainImpl` etc.) classes in the main Theia process via RPC
- `node/` — the API classes implementing `GreetingMain` and similar interfaces and the Inversify bindings that register the API provider.
All code in this directory runs in the main Theia Node process
- `common/` — the RPC API Ext/Main interface definitions corresponding to the backend of the `gotd` plugin API
## Additional Information
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,42 @@
{
"private": true,
"name": "@theia/api-provider-sample",
"version": "1.68.0",
"description": "Theia - Example code to demonstrate Theia API Provider Extensions",
"dependencies": {
"@theia/core": "1.68.0",
"@theia/plugin-ext": "1.68.0",
"@theia/plugin-ext-headless": "1.68.0"
},
"theiaExtensions": [
{
"backend": "lib/node/gotd-backend-module"
}
],
"keywords": [
"theia-extension"
],
"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": [
"lib",
"src"
],
"types": "src/gotd.d.ts",
"scripts": {
"lint": "theiaext lint",
"build": "theiaext build",
"watch": "theiaext watch",
"clean": "theiaext clean"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
}
}

View File

@@ -0,0 +1,70 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { createProxyIdentifier } from '@theia/plugin-ext/lib/common/rpc-protocol';
import type { greeting } from '../gotd';
import { Event } from '@theia/core';
export enum GreetingKind {
DIRECT = 1,
QUIRKY = 2,
SNARKY = 3,
}
export interface GreeterData {
readonly uuid: string;
greetingKinds: greeting.GreetingKind[];
};
export const GreetingMain = Symbol('GreetingMain');
export interface GreetingMain {
$getMessage(greeterId: string): Promise<string>;
$createGreeter(): Promise<GreeterData>;
$destroyGreeter(greeterId: GreeterData['uuid']): Promise<void>;
$updateGreeter(data: GreeterData): void;
}
export const GreetingExt = Symbol('GreetingExt');
export interface GreetingExt {
//
// External protocol
//
registerGreeter(): Promise<string>;
unregisterGreeter(uuid: string): Promise<void>;
getMessage(greeterId: string): Promise<string>;
getGreetingKinds(greeterId: string): readonly greeting.GreetingKind[];
setGreetingKindEnabled(greeterId: string, greetingKind: greeting.GreetingKind, enable: boolean): void;
onGreetingKindsChanged(greeterId: string): Event<readonly greeting.GreetingKind[]>;
//
// Internal protocol
//
$greeterUpdated(data: GreeterData): void;
}
export const PLUGIN_RPC_CONTEXT = {
GREETING_MAIN: createProxyIdentifier<GreetingMain>('GreetingMain'),
};
export const MAIN_RPC_CONTEXT = {
GREETING_EXT: createProxyIdentifier<GreetingExt>('GreetingExt'),
};

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
// Strictly speaking, the 'greeting' namespace is an unnecessary level of organization
// but it serves to illustrate how API namespaces are implemented in the backend.
export namespace greeting {
export function createGreeter(): Promise<greeting.Greeter>;
export enum GreetingKind {
DIRECT = 1,
QUIRKY = 2,
SNARKY = 3,
}
export interface Greeter extends Disposable {
greetingKinds: readonly GreetingKind[];
getMessage(): Promise<string>;
setGreetingKind(kind: GreetingKind, enable = true): void;
onGreetingKindsChanged: Event<readonly GreetingKind[]>;
}
}
export interface Event<T> {
(listener: (e: T) => unknown, thisArg?: unknown): Disposable;
}
export interface Disposable {
dispose(): void;
}
namespace Disposable {
export function create(func: () => void): Disposable;
}

View File

@@ -0,0 +1,33 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import * as path from 'path';
import { injectable } from '@theia/core/shared/inversify';
import { ExtPluginApi, ExtPluginApiProvider } from '@theia/plugin-ext-headless';
@injectable()
export class ExtPluginGotdApiProvider implements ExtPluginApiProvider {
provideApi(): ExtPluginApi {
// We can support both backend plugins and headless plugins, so we have only one
// entry-point script. Moreover, the application build packages that script in
// the `../backend/` directory from its source `../plugin/` location, alongside
// the scripts for all other plugin API providers.
const universalInitPath = path.join(__dirname, '../backend/gotd-api-init');
return {
backendInitPath: universalInitPath,
headlessInitPath: universalInitPath
};
}
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ExtPluginApiProvider } from '@theia/plugin-ext';
import { ExtPluginGotdApiProvider } from './ext-plugin-gotd-api-provider';
import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
import { GotdMainPluginApiProvider } from './gotd-main-plugin-provider';
import { GreetingMain } from '../common/plugin-api-rpc';
import { GreetingMainImpl } from './greeting-main-impl';
export default new ContainerModule(bind => {
bind(Symbol.for(ExtPluginApiProvider)).to(ExtPluginGotdApiProvider).inSingletonScope();
bind(MainPluginApiProvider).to(GotdMainPluginApiProvider).inSingletonScope();
bind(GreetingMain).to(GreetingMainImpl).inSingletonScope();
});

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { MainPluginApiProvider } from '@theia/plugin-ext/lib/common/plugin-ext-api-contribution';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
@injectable()
export class GotdMainPluginApiProvider implements MainPluginApiProvider {
@inject(GreetingMain)
protected readonly greetingMain: GreetingMain;
initialize(rpc: RPCProtocol): void {
rpc.set(PLUGIN_RPC_CONTEXT.GREETING_MAIN, this.greetingMain);
}
}

View File

@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { generateUuid } from '@theia/core/lib/common/uuid';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { GreetingKind, GreeterData, GreetingExt, GreetingMain, MAIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
const GREETINGS = {
[GreetingKind.DIRECT]: ['Hello, world!', "I'm here!", 'Good day!'],
[GreetingKind.QUIRKY]: ['Howdy doody, world?', "What's crack-a-lackin'?", 'Wazzup werld?'],
[GreetingKind.SNARKY]: ["Oh, it's you, world.", 'You again, world?!', 'Whatever.'],
} as const;
@injectable()
export class GreetingMainImpl implements GreetingMain {
protected proxy: GreetingExt;
private greeterData: Record<string, GreeterData> = {};
constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.GREETING_EXT);
}
async $createGreeter(): Promise<GreeterData> {
const result: GreeterData = {
uuid: generateUuid(),
greetingKinds: [GreetingKind.DIRECT]
};
this.greeterData[result.uuid] = result;
return result;
}
async $destroyGreeter(greeterId: string): Promise<void> {
delete this.greeterData[greeterId];
}
$updateGreeter(data: GreeterData): void {
const myData = this.greeterData[data.uuid];
if (myData) {
myData.greetingKinds = [...data.greetingKinds];
this.proxy.$greeterUpdated({ ...myData });
}
}
async $getMessage(greeterId: string): Promise<string> {
const data = this.greeterData[greeterId];
if (data.greetingKinds.length === 0) {
throw new Error(`No greetings are available for greeter ${greeterId}`);
}
// Get a random one of our supported greeting kinds.
const kind = data.greetingKinds[(Math.floor(Math.random() * data.greetingKinds.length))];
// And a random greeting of that kind
const greetingIdx = Math.floor(Math.random() * GREETINGS[kind].length);
return GREETINGS[kind][greetingIdx];
}
}

View File

@@ -0,0 +1,97 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { Plugin } from '@theia/plugin-ext/lib/common/plugin-api-rpc';
import type * as gotd from '../gotd';
import { GreetingKind, GreetingExt, MAIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
import { GreetingExtImpl } from './greeting-ext-impl';
import { Disposable, DisposableCollection } from '@theia/core';
import { PluginContainerModule } from '@theia/plugin-ext/lib/plugin/node/plugin-container-module';
// This script is responsible for creating and returning the extension's
// custom API object when a plugin's module imports it. Keep in mind that
// all of the code here runs in the plugin-host node process, whether that
// be the backend host dedicated to some frontend connection or the single
// host for headless plugins, which is where the plugin itself is running.
type Gotd = typeof gotd;
const GotdApiFactory = Symbol('GotdApiFactory');
// Retrieved by Theia to configure the Inversify DI container when the plugin is initialized.
// This is called when the plugin-host process is forked.
export const containerModule = PluginContainerModule.create(({ bind, bindApiFactory }) => {
bind(GreetingExt).to(GreetingExtImpl).inSingletonScope();
bindApiFactory('@theia/api-provider-sample', GotdApiFactory, GotdApiFactoryImpl);
});
// Creates the Greeting of the Day API object
@injectable()
class GotdApiFactoryImpl {
@inject(RPCProtocol)
protected readonly rpc: RPCProtocol;
@inject(GreetingExt)
protected readonly greetingExt: GreetingExt;
@postConstruct()
initialize(): void {
this.rpc.set(MAIN_RPC_CONTEXT.GREETING_EXT, this.greetingExt);
}
createApi(plugin: Plugin): Gotd {
const self = this;
async function createGreeter(): Promise<gotd.greeting.Greeter> {
const toDispose = new DisposableCollection();
const uuid = await self.greetingExt.registerGreeter();
toDispose.push(Disposable.create(() => self.greetingExt.unregisterGreeter(uuid)));
const onGreetingKindsChanged = self.greetingExt.onGreetingKindsChanged(uuid);
const result: gotd.greeting.Greeter = {
get greetingKinds(): readonly GreetingKind[] {
return self.greetingExt.getGreetingKinds(uuid);
},
setGreetingKind(greetingKind: GreetingKind, enable = true): void {
self.greetingExt.setGreetingKindEnabled(uuid, greetingKind, enable);
},
getMessage(): Promise<string> {
return self.greetingExt.getMessage(uuid);
},
onGreetingKindsChanged,
dispose: toDispose.dispose.bind(toDispose),
};
return result;
}
const greeting: Gotd['greeting'] = {
createGreeter,
GreetingKind
};
return {
greeting,
Disposable,
};
};
}

View File

@@ -0,0 +1,86 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource 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
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { GreetingKind, GreeterData, GreetingExt, GreetingMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { Event, Emitter } from '@theia/core';
type LocalGreeterData = GreeterData & {
onGreetingKindsChangedEmitter: Emitter<readonly GreetingKind[]>
};
@injectable()
export class GreetingExtImpl implements GreetingExt {
private readonly proxy: GreetingMain;
private greeterData: Record<string, LocalGreeterData> = {};
constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.GREETING_MAIN);
}
async registerGreeter(): Promise<string> {
const newGreeter = await this.proxy.$createGreeter();
this.greeterData[newGreeter.uuid] = {
...newGreeter,
onGreetingKindsChangedEmitter: new Emitter()
};
return newGreeter.uuid;
}
unregisterGreeter(uuid: string): Promise<void> {
delete this.greeterData[uuid];
return this.proxy.$destroyGreeter(uuid);
}
getGreetingKinds(greeterId: string): readonly GreetingKind[] {
const data = this.greeterData[greeterId];
return data ? [...data.greetingKinds] : [];
}
setGreetingKindEnabled(greeterId: string, greetingKind: GreetingKind, enable: boolean): void {
const data = this.greeterData[greeterId];
if (data.greetingKinds.includes(greetingKind) === enable) {
return; // Nothing to change
}
if (enable) {
data.greetingKinds.push(greetingKind);
} else {
const index = data.greetingKinds.indexOf(greetingKind);
data.greetingKinds.splice(index, 1);
}
this.proxy.$updateGreeter({uuid: greeterId, greetingKinds: [...data.greetingKinds] });
}
onGreetingKindsChanged(greeterId: string): Event<readonly GreetingKind[]> {
return this.greeterData[greeterId].onGreetingKindsChangedEmitter.event;
}
getMessage(greeterId: string): Promise<string> {
return this.proxy.$getMessage(greeterId);
}
$greeterUpdated(data: GreeterData): void {
const myData = this.greeterData[data.uuid];
if (myData) {
myData.greetingKinds = [...data.greetingKinds];
myData.onGreetingKindsChangedEmitter.fire([...data.greetingKinds]);
}
}
}

View File

@@ -0,0 +1,22 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../../packages/core"
},
{
"path": "../../packages/plugin-ext"
},
{
"path": "../../packages/plugin-ext-headless"
}
]
}

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,42 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - API SAMPLES</h2>
<hr />
</div>
## Description
The `@theia/api-samples` extension contains programming examples on how to use internal APIs.
The purpose of the extension is to:
- provide developers with real-world coding examples using internal APIs, dependency injection, etc.
- provide easy-to-use and test examples for features when reviewing pull-requests.
The extension is for reference and test purposes only and is not published on `npm` (`private: true`).
### Sample mock OpenVSX server
These samples contain a mock implementation of an OpenVSX server. This is done
for testing purposes only. It is currently hosted at
`<backend-host>/mock-open-vsx/api/...`.
## Additional Information
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,69 @@
{
"private": true,
"name": "@theia/api-samples",
"version": "1.68.0",
"description": "Theia - Example code to demonstrate Theia API",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@theia/ai-chat": "1.68.0",
"@theia/ai-chat-ui": "1.68.0",
"@theia/ai-code-completion": "1.68.0",
"@theia/ai-core": "1.68.0",
"@theia/ai-mcp": "1.68.0",
"@theia/ai-mcp-server": "1.68.0",
"@theia/core": "1.68.0",
"@theia/file-search": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/output": "1.68.0",
"@theia/ovsx-client": "1.68.0",
"@theia/search-in-workspace": "1.68.0",
"@theia/test": "1.68.0",
"@theia/toolbar": "1.68.0",
"@theia/vsx-registry": "1.68.0",
"@theia/workspace": "1.68.0",
"zod": "^4.2.1"
},
"theiaExtensions": [
{
"frontend": "lib/browser/api-samples-frontend-module",
"backend": "lib/node/api-samples-backend-module"
},
{
"electronMain": "lib/electron-main/update/sample-updater-main-module",
"frontendElectron": "lib/electron-browser/updater/sample-updater-frontend-module"
},
{
"frontendOnly": "lib/browser-only/api-samples-frontend-only-module"
},
{
"frontendPreload": "lib/browser/api-samples-preload-module"
}
],
"keywords": [
"theia-extension"
],
"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": [
"lib",
"src"
],
"scripts": {
"lint": "theiaext lint",
"build": "theiaext build",
"watch": "theiaext watch",
"clean": "theiaext clean"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
}
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource 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
// *****************************************************************************
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { bindOPFSInitialization } from './filesystem/example-filesystem-initialization';
export default new ContainerModule((
bind: interfaces.Bind,
_unbind: interfaces.Unbind,
_isBound: interfaces.IsBound,
rebind: interfaces.Rebind,
) => {
bindOPFSInitialization(bind, rebind);
});

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2023 EclipseSource 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
// *****************************************************************************
import { URI } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { EncodingService } from '@theia/core/lib/common/encoding-service';
import { OPFSInitialization, DefaultOPFSInitialization } from '@theia/filesystem/lib/browser-only/opfs-filesystem-initialization';
import { OPFSFileSystemProvider } from '@theia/filesystem/lib/browser-only/opfs-filesystem-provider';
@injectable()
export class ExampleOPFSInitialization extends DefaultOPFSInitialization {
@inject(EncodingService)
protected encodingService: EncodingService;
override getRootDirectory(): string {
return '/theia/';
}
override async initializeFS(provider: OPFSFileSystemProvider): Promise<void> {
// Check whether the directory exists (relative to the root directory)
if (await provider.exists(new URI('/workspace'))) {
await provider.readdir(new URI('/workspace'));
} else {
await provider.mkdir(new URI('/workspace'));
await provider.writeFile(new URI('/workspace/my-file.txt'), this.encodingService.encode('foo').buffer, { create: true, overwrite: false });
}
if (await provider.exists(new URI('/workspace2'))) {
await provider.readdir(new URI('/workspace2'));
} else {
await provider.mkdir(new URI('/workspace2'));
await provider.writeFile(new URI('/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer, { create: true, overwrite: false });
}
// You can also create an index of the files and directories in the file system
// await provider.clear();
// await provider.createIndex([
// [new URI('/workspace/my-file.txt'), this.encodingService.encode('bar').buffer],
// [new URI('/workspace2/my-file.json'), this.encodingService.encode('{ foo: true }').buffer]
// ]);
}
}
export const bindOPFSInitialization = (bind: interfaces.Bind, rebind: interfaces.Rebind): void => {
bind(ExampleOPFSInitialization).toSelf();
rebind(OPFSInitialization).toService(ExampleOPFSInitialization);
};

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2025 Lonti.com Pty Ltd.
//
// 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
// *****************************************************************************
import { CodeCompletionVariableContext } from '@theia/ai-code-completion/lib/browser/code-completion-variable-context';
import { AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIVariable } from '@theia/ai-core';
import { FrontendVariableContribution, FrontendVariableService } from '@theia/ai-core/lib/browser';
import { MaybePromise } from '@theia/core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
const SAMPLE_VARIABLE: AIVariable = {
id: 'sampleCodeCompletionVariable',
name: 'sampleCodeCompletionVariable',
description: 'A sample variable for code completion.',
};
/**
* This variable is used to demonstrate how to create a custom variable for code completion.
* It is registered as a variable that can be resolved in the context of code completion.
*/
@injectable()
export class SampleCodeCompletionVariableContribution implements FrontendVariableContribution, AIVariableResolver {
registerVariables(service: FrontendVariableService): void {
service.registerResolver(SAMPLE_VARIABLE, this);
}
canResolve(request: AIVariableResolutionRequest, context: AIVariableContext): MaybePromise<number> {
return CodeCompletionVariableContext.is(context) && request.variable.id === SAMPLE_VARIABLE.id ? 1 : 0;
}
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
if (request.variable.id === SAMPLE_VARIABLE.id && CodeCompletionVariableContext.is(context) && context.model.uri.path.endsWith('.sample.js')) {
return Promise.resolve({
variable: SAMPLE_VARIABLE,
value: 'This is a special sample file, every line must end with a "// sample" comment.'
});
}
}
}
export const bindSampleCodeCompletionVariableContribution = (bind: interfaces.Bind) => {
bind(AIVariableContribution).to(SampleCodeCompletionVariableContribution).inSingletonScope();
};

View File

@@ -0,0 +1,78 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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
// *****************************************************************************
import { ContainerModule, interfaces } from '@theia/core/shared/inversify';
import { bindDynamicLabelProvider } from './label/sample-dynamic-label-provider-command-contribution';
import { bindSampleFilteredCommandContribution } from './contribution-filter/sample-filtered-command-contribution';
import { bindSampleUnclosableView } from './view/sample-unclosable-view-contribution';
import { bindSampleOutputChannelWithSeverity } from './output/sample-output-channel-with-severity';
import { bindSampleMenu } from './menu/sample-menu-contribution';
import { bindSampleFileWatching } from './file-watching/sample-file-watching-contribution';
import { bindVSXCommand } from './vsx/sample-vsx-command-contribution';
import { bindSampleToolbarContribution } from './toolbar/sample-toolbar-contribution';
import '../../src/browser/style/branding.css';
import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monaco-editor-preference-extractor';
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { bindSampleAppInfo } from './vsx/sample-frontend-app-info';
import { bindTestSample } from './test/sample-test-contribution';
import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities';
import { bindChatNodeToolbarActionContribution } from './chat/chat-node-toolbar-action-contribution';
import { bindAskAndContinueChatAgentContribution } from './chat/ask-and-continue-chat-agent-contribution';
import { bindChangeSetChatAgentContribution } from './chat/change-set-chat-agent-contribution';
import { bindModeChatAgentContribution } from './chat/mode-chat-agent-contribution';
import { bindOriginalStateTestAgentContribution } from './chat/original-state-test-agent-contribution';
import { bindCustomResponseContentRendererContribution } from './chat/custom-response-content-agent-contribution';
import { bindSampleChatCommandContribution } from './chat/sample-chat-command-contribution';
import { bindSampleCodeCompletionVariableContribution } from './ai-code-completion/sample-code-completion-variable-contribution';
import { bindSamplePreferenceContribution } from './preferences/sample-preferences-contribution';
import { MCPFrontendContribution } from '@theia/ai-mcp-server/lib/browser/mcp-frontend-contribution';
import { SampleFrontendMCPContribution } from './mcp/sample-frontend-mcp-contribution';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ResolveMcpFrontendContribution } from './mcp/resolve-frontend-mcp-contribution';
export default new ContainerModule((
bind: interfaces.Bind,
unbind: interfaces.Unbind,
isBound: interfaces.IsBound,
rebind: interfaces.Rebind,
) => {
bindAskAndContinueChatAgentContribution(bind);
bindChangeSetChatAgentContribution(bind);
bindModeChatAgentContribution(bind);
bindOriginalStateTestAgentContribution(bind);
bindCustomResponseContentRendererContribution(bind);
bindChatNodeToolbarActionContribution(bind);
bindSampleChatCommandContribution(bind);
bindDynamicLabelProvider(bind);
bindSampleUnclosableView(bind);
bindSampleOutputChannelWithSeverity(bind);
bindSampleMenu(bind);
bindSampleFileWatching(bind);
bindVSXCommand(bind);
bindSampleFilteredCommandContribution(bind);
bindSampleToolbarContribution(bind, rebind);
bindMonacoPreferenceExtractor(bind);
bindSampleAppInfo(bind);
bindTestSample(bind);
bindSampleFileSystemCapabilitiesCommands(bind);
rebindOVSXClientFactory(rebind);
bindSampleCodeCompletionVariableContribution(bind);
bindSamplePreferenceContribution(bind);
bind(MCPFrontendContribution).to(SampleFrontendMCPContribution).inSingletonScope();
bind(ResolveMcpFrontendContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(ResolveMcpFrontendContribution);
});

View File

@@ -0,0 +1,23 @@
// *****************************************************************************
// Copyright (C) 2025 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
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { TextReplacementContribution } from '@theia/core/lib/browser/preload/text-replacement-contribution';
import { TextSampleReplacementContribution } from './preload/text-replacement-sample';
export default new ContainerModule(bind => {
bind(TextReplacementContribution).to(TextSampleReplacementContribution).inSingletonScope();
});

View File

@@ -0,0 +1,182 @@
// *****************************************************************************
// Copyright (C) 2024 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
// *****************************************************************************
import {
AbstractStreamParsingChatAgent,
ChatAgent,
ChatModel,
MutableChatRequestModel,
lastProgressMessage,
QuestionResponseContentImpl,
unansweredQuestions,
ProgressChatResponseContentImpl
} from '@theia/ai-chat';
import { Agent, LanguageModelMessage, BasePromptFragment } from '@theia/ai-core';
import { injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
export function bindAskAndContinueChatAgentContribution(bind: interfaces.Bind): void {
bind(AskAndContinueChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(AskAndContinueChatAgent);
bind(ChatAgent).toService(AskAndContinueChatAgent);
}
const systemPrompt: BasePromptFragment = {
id: 'askAndContinue-system',
template: `
You are an agent demonstrating how to generate questions and continue the conversation based on the user's answers.
First answer the user's question or continue their story.
Then come up with an interesting question and 2-3 answers which will be presented to the user as multiple choice.
Use the following format exactly to define the questions and answers.
Especially add the <question> and </question> tags around the JSON.
<question>
{
"question": "YOUR QUESTION HERE",
"options": [
{
"text": "OPTION 1"
},
{
"text": "OPTION 2"
}
]
}
</question>
Examples:
<question>
{
"question": "What is the capital of France?",
"options": [
{
"text": "Paris"
},
{
"text": "Lyon"
}
]
}
</question>
<question>
{
"question": "What does the fox say?",
"options": [
{
"text": "Ring-ding-ding-ding-dingeringeding!"
},
{
"text": "Wa-pa-pa-pa-pa-pa-pow!"
}
]
}
</question>
The user will answer the question and you can continue the conversation.
Once they answered, the question will be replaced with a simple "Question/Answer" pair, for example
Question: What does the fox say?
Answer: Ring-ding-ding-ding-dingeringeding!
If the user did not answer the question, it will be marked with "No answer", for example
Question: What is the capital of France?
No answer
Do not generate such pairs yourself, instead treat them as a signal for a past question.
Do not ask further questions once the text contains 5 or more "Question/Answer" pairs.
`
};
/**
* This is a very simple example agent that asks questions and continues the conversation based on the user's answers.
*/
@injectable()
export class AskAndContinueChatAgent extends AbstractStreamParsingChatAgent {
id = 'AskAndContinueSample';
name = 'AskAndContinueSample';
override description = 'This chat will ask questions related to the input and continues after that.';
protected defaultLanguageModelPurpose = 'chat';
override languageModelRequirements = [
{
purpose: 'chat',
identifier: 'default/universal',
}
];
override prompts = [{ id: systemPrompt.id, defaultVariant: systemPrompt }];
protected override systemPromptId: string | undefined = systemPrompt.id;
@postConstruct()
addContentMatchers(): void {
this.contentMatchers.push({
start: /^<question>.*$/m,
end: /^<\/question>$/m,
contentFactory: (content: string, request: MutableChatRequestModel) => {
const question = content.replace(/^<question>\n|<\/question>$/g, '');
const parsedQuestion = JSON.parse(question);
return new QuestionResponseContentImpl(parsedQuestion.question, parsedQuestion.options, request, selectedOption => {
this.handleAnswer(selectedOption, request);
});
},
incompleteContentFactory: (content: string, request: MutableChatRequestModel) =>
// Display a progress indicator while the question is being parsed
new ProgressChatResponseContentImpl('Preparing question...')
});
}
protected override async onResponseComplete(request: MutableChatRequestModel): Promise<void> {
const unansweredQs = unansweredQuestions(request);
if (unansweredQs.length < 1) {
return super.onResponseComplete(request);
}
request.response.addProgressMessage({ content: 'Waiting for input...', show: 'whileIncomplete' });
request.response.waitForInput();
}
protected handleAnswer(selectedOption: { text: string; value?: string; }, request: MutableChatRequestModel): void {
const progressMessage = lastProgressMessage(request);
if (progressMessage) {
request.response.updateProgressMessage({ ...progressMessage, show: 'untilFirstContent', status: 'completed' });
}
request.response.stopWaitingForInput();
// We're reusing the original request here as a shortcut. In combination with the override of 'getMessages' we continue generating.
// In a real-world scenario, you would likely manually interact with an LLM here to generate and append the next response.
this.invoke(request);
}
/**
* As the question/answer are handled within the same response, we add an additional user message at the end to indicate to
* the LLM to continue generating.
*/
protected override async getMessages(model: ChatModel): Promise<LanguageModelMessage[]> {
const messages = await super.getMessages(model, true);
const requests = model.getRequests();
if (!requests[requests.length - 1].response.isComplete && requests[requests.length - 1].response.response?.content.length > 0) {
return [...messages,
{
type: 'text',
actor: 'user',
text: 'Continue generating based on the user\'s answer or finish the conversation if 5 or more questions were already answered.'
}];
}
return messages;
}
}

View File

@@ -0,0 +1,156 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription,
ChangeSetElement
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { URI } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
export function bindChangeSetChatAgentContribution(bind: interfaces.Bind): void {
bind(ChangeSetChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(ChangeSetChatAgent);
bind(ChatAgent).toService(ChangeSetChatAgent);
}
/**
* This is a test agent demonstrating how to create change sets in AI chats.
*/
@injectable()
export class ChangeSetChatAgent extends AbstractStreamParsingChatAgent {
readonly id = 'ChangeSetSample';
readonly name = 'ChangeSetSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'This chat will create and modify a change set.';
override languageModelRequirements: LanguageModelRequirement[] = [];
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(ChangeSetFileElementFactory)
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
override async invoke(request: MutableChatRequestModel): Promise<void> {
const roots = this.workspaceService.tryGetRoots();
if (roots.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'No workspace is open. For using this chat agent, please open a workspace with at least two files in the root.'
));
request.response.complete();
return;
}
const root = roots[0];
const files = root.children?.filter(child => child.isFile);
if (!files || files.length < 3) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'The workspace does not contain any files. For using this chat agent, please add at least two files in the root.'
));
request.response.complete();
return;
}
const fileToAdd = root.resource.resolve('hello/new-file.txt');
const fileToChange = files[Math.floor(Math.random() * files.length)];
const fileToDelete = files.filter(file => file.name !== fileToChange.name)[Math.floor(Math.random() * files.length)];
const changes: ChangeSetElement[] = [];
const chatSessionId = request.session.id;
const requestId = request.id;
changes.push(
this.fileChangeFactory({
uri: fileToAdd,
type: 'add',
state: 'pending',
targetState: 'Hello World!',
requestId,
chatSessionId
})
);
if (fileToChange && fileToChange.resource) {
changes.push(
this.fileChangeFactory({
uri: fileToChange.resource,
type: 'modify',
state: 'pending',
targetState: await this.computeTargetState(fileToChange.resource),
requestId,
chatSessionId
})
);
}
if (fileToDelete && fileToDelete.resource) {
changes.push(
this.fileChangeFactory({
uri: fileToDelete.resource,
type: 'delete',
state: 'pending',
requestId,
chatSessionId
})
);
}
request.session.changeSet.setTitle('My Test Change Set');
request.session.changeSet.setElements(...changes);
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'I have created a change set for you. You can now review and apply it.'
));
request.response.complete();
}
async computeTargetState(resource: URI): Promise<string> {
const content = await this.fileService.read(resource);
if (content.value.length < 20) {
return 'HelloWorldModify';
}
let readLocation = Math.random() * 0.1 * content.value.length;
let oldLocation = 0;
let output = '';
while (readLocation < content.value.length) {
output += content.value.substring(oldLocation, readLocation);
oldLocation = readLocation;
const type = Math.random();
if (type < 0.33) {
// insert
output += `this is an insert at ${readLocation}`;
} else {
// delete
oldLocation += 20;
}
readLocation += Math.random() * 0.1 * content.value.length;
}
return output;
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2024 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
// *****************************************************************************
import {
ChatNodeToolbarActionContribution
} from '@theia/ai-chat-ui/lib/browser/chat-node-toolbar-action-contribution';
import {
isResponseNode,
RequestNode,
ResponseNode
} from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import { interfaces } from '@theia/core/shared/inversify';
export function bindChatNodeToolbarActionContribution(bind: interfaces.Bind): void {
bind(ChatNodeToolbarActionContribution).toDynamicValue(context => ({
getToolbarActions: (args: RequestNode | ResponseNode) => {
if (isResponseNode(args)) {
return [{
commandId: 'sample-command',
icon: 'codicon codicon-feedback',
tooltip: 'API Samples: Example command'
}];
} else {
return [];
}
}
}));
}

View File

@@ -0,0 +1,281 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AbstractStreamParsingChatAgent,
ChatAgent,
ChatResponseContent,
MutableChatRequestModel,
SerializableChatResponseContentData,
} from '@theia/ai-chat';
import {
ChatContentDeserializerContribution,
ChatContentDeserializerRegistry
} from '@theia/ai-chat/lib/common/chat-content-deserializer';
import { ChatResponsePartRenderer } from '@theia/ai-chat-ui/lib/browser/chat-response-part-renderer';
import { ResponseNode } from '@theia/ai-chat-ui/lib/browser/chat-tree-view';
import { Agent } from '@theia/ai-core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { ReactNode } from '@theia/core/shared/react';
export function bindCustomResponseContentRendererContribution(bind: interfaces.Bind): void {
bind(CustomResponseContentRendererAgent).toSelf().inSingletonScope();
bind(Agent).toService(CustomResponseContentRendererAgent);
bind(ChatAgent).toService(CustomResponseContentRendererAgent);
bind(ChatContentDeserializerContribution).to(CustomContentDeserializerContribution).inSingletonScope();
bind(ChatResponsePartRenderer).to(CustomSerializableContentRenderer).inSingletonScope();
bind(ChatResponsePartRenderer).to(CustomNonSerializableContentRenderer).inSingletonScope();
}
// =============================================================================
// SERIALIZABLE CUSTOM CONTENT
// =============================================================================
/**
* Data interface for serializable custom content.
* This is shared between the implementation and the deserializer.
*/
export interface CustomSerializableContentData {
title: string;
items: string[];
timestamp: number;
}
/**
* Serializable custom content type.
* This can be persisted and restored from storage.
*/
export interface CustomSerializableContent extends ChatResponseContent {
kind: 'customSerializable';
title: string;
items: string[];
timestamp: number;
}
export class CustomSerializableContentImpl implements CustomSerializableContent {
readonly kind = 'customSerializable';
constructor(
public title: string,
public items: string[],
public timestamp: number
) { }
asString(): string {
return `${this.title}: ${this.items.join(', ')}`;
}
toSerializable(): SerializableChatResponseContentData<CustomSerializableContentData> {
return {
kind: 'customSerializable',
fallbackMessage: `Custom content: ${this.title} (${this.items.length} items)`,
data: {
title: this.title,
items: this.items,
timestamp: this.timestamp
}
};
}
}
/**
* Deserializer for custom serializable content.
*/
@injectable()
export class CustomContentDeserializerContribution implements ChatContentDeserializerContribution {
registerDeserializers(registry: ChatContentDeserializerRegistry): void {
registry.register({
kind: 'customSerializable',
deserialize: (data: CustomSerializableContentData) =>
new CustomSerializableContentImpl(
data.title,
data.items,
data.timestamp
)
});
}
}
/**
* Renderer for custom serializable content.
*/
@injectable()
export class CustomSerializableContentRenderer implements ChatResponsePartRenderer<CustomSerializableContent> {
canHandle(response: ChatResponseContent): number {
return response.kind === 'customSerializable' ? 10 : -1;
}
render(content: CustomSerializableContent, node: ResponseNode): ReactNode {
const date = new Date(content.timestamp).toLocaleString();
return React.createElement('div', {
className: 'theia-ChatResponseContent',
style: {
padding: '10px',
margin: '5px 0',
border: '2px solid var(--theia-editorWidget-border)',
borderRadius: '4px',
backgroundColor: 'var(--theia-editor-background)'
}
},
React.createElement('div', {
style: {
fontWeight: 'bold',
marginBottom: '8px',
color: 'var(--theia-descriptionForeground)'
}
}, `📦 ${content.title}`),
React.createElement('ul', {
style: {
margin: '0',
paddingLeft: '20px'
}
}, ...content.items.map((item, idx) =>
React.createElement('li', { key: idx }, item)
)),
React.createElement('div', {
style: {
marginTop: '8px',
fontSize: '0.85em',
color: 'var(--theia-descriptionForeground)',
fontStyle: 'italic'
}
}, `Created: ${date} • ✅ Serializable (will be persisted)`)
);
}
}
// =============================================================================
// NON-SERIALIZABLE CUSTOM CONTENT
// =============================================================================
/**
* Non-serializable custom content type.
*/
export interface CustomNonSerializableContent extends ChatResponseContent {
kind: 'customNonSerializable';
message: string;
onClick: () => void; // Functions cannot be serialized!
}
export class CustomNonSerializableContentImpl implements CustomNonSerializableContent {
readonly kind = 'customNonSerializable';
constructor(
public message: string,
public onClick: () => void
) { }
asString(): string {
return `Interactive: ${this.message}`;
}
}
/**
* Renderer for custom non-serializable content.
* This will only be used for active (non-restored) content.
*/
@injectable()
export class CustomNonSerializableContentRenderer implements ChatResponsePartRenderer<CustomNonSerializableContent> {
canHandle(response: ChatResponseContent): number {
return response.kind === 'customNonSerializable' ? 10 : -1;
}
render(content: CustomNonSerializableContent, node: ResponseNode): ReactNode {
return React.createElement('div', {
className: 'theia-ChatResponseContent',
style: {
padding: '10px',
margin: '5px 0',
border: '2px solid var(--theia-notificationsWarningIcon-foreground)',
borderRadius: '4px',
backgroundColor: 'var(--theia-editor-background)'
}
},
React.createElement('div', {
style: {
fontWeight: 'bold',
marginBottom: '8px',
color: 'var(--theia-descriptionForeground)'
}
}, '⚡ Interactive Content'),
React.createElement('div', {
style: { marginBottom: '10px' }
}, content.message),
React.createElement('button', {
className: 'theia-button',
onClick: content.onClick,
style: { marginRight: '8px' }
}, 'Click Me!'),
React.createElement('div', {
style: {
marginTop: '8px',
fontSize: '0.85em',
color: 'var(--theia-notificationsWarningIcon-foreground)',
fontStyle: 'italic'
}
}, '⚠️ No deserializer registered (will use fallback for serialization and deserialization)')
);
}
}
// =============================================================================
// DEMO AGENT
// =============================================================================
@injectable()
export class CustomResponseContentRendererAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
id = 'CustomContentSample';
name = this.id;
override description = 'Demonstrates custom serializable and non-serializable chat response content';
languageModelRequirements = [];
protected defaultLanguageModelPurpose = 'chat';
public override async invoke(request: MutableChatRequestModel): Promise<void> {
const response = request.response.response;
// Add serializable custom content
response.addContent(
new CustomSerializableContentImpl(
'Serializable Custom Content',
[
'This content has a custom data interface',
'It has a deserializer registered',
'It will be properly restored when loading a saved session',
'The renderer shows this custom UI with all data intact'
],
Date.now()
)
);
// Add interactive button as a demonstration of non-serializable content
let clickCount = 0;
response.addContent(
new CustomNonSerializableContentImpl(
'This is the LLM message received',
() => {
clickCount++;
alert(`Button clicked ${clickCount} time(s)`);
}
)
);
// Trigger completion immediately - no streaming needed
request.response.complete();
}
}

View File

@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { injectable, interfaces } from '@theia/core/shared/inversify';
export function bindModeChatAgentContribution(bind: interfaces.Bind): void {
bind(ModeChatAgent).toSelf().inSingletonScope();
bind(Agent).toService(ModeChatAgent);
bind(ChatAgent).toService(ModeChatAgent);
}
/**
* This is a test agent demonstrating how to use chat modes.
* It responds differently based on the selected mode.
*/
@injectable()
export class ModeChatAgent extends AbstractStreamParsingChatAgent {
readonly id = 'ModeTestSample';
readonly name = 'ModeTestSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'A test agent that demonstrates different response modes (concise vs detailed).';
override languageModelRequirements: LanguageModelRequirement[] = [];
// Define the modes this agent supports
modes = [
{ id: 'concise', name: 'Concise' },
{ id: 'detailed', name: 'Detailed' }
];
override async invoke(request: MutableChatRequestModel): Promise<void> {
const modeId = request.request.modeId || 'concise';
const question = request.request.text;
let response: string;
if (modeId === 'concise') {
response = `**Concise Mode**: You asked: "${question}"\n\nThis is a brief response.`;
} else {
response = `**Detailed Mode**: You asked: "${question}"\n\n` +
'This is a more detailed response that provides additional context and explanation. ' +
'In detailed mode, the agent provides more comprehensive information, examples, and background. ' +
'This mode is useful when you need in-depth understanding of a topic.';
}
request.response.response.addContent(new MarkdownChatResponseContentImpl(response));
request.response.complete();
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,159 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
AbstractStreamParsingChatAgent,
ChatAgent,
MutableChatRequestModel,
MarkdownChatResponseContentImpl,
SystemMessageDescription
} from '@theia/ai-chat';
import { ChangeSetFileElementFactory } from '@theia/ai-chat/lib/browser/change-set-file-element';
import { Agent, LanguageModelRequirement } from '@theia/ai-core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { wait } from '@theia/core/lib/common/promise-util';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
export function bindOriginalStateTestAgentContribution(bind: interfaces.Bind): void {
bind(OriginalStateTestAgent).toSelf().inSingletonScope();
bind(Agent).toService(OriginalStateTestAgent);
bind(ChatAgent).toService(OriginalStateTestAgent);
}
/**
* This is a test agent demonstrating how to test originalState functionality in change sets.
* It creates change set elements with original content provided and tests sequential updates.
*/
@injectable()
export class OriginalStateTestAgent extends AbstractStreamParsingChatAgent {
readonly id = 'OriginalStateTestSample';
readonly name = 'OriginalStateTestSample';
readonly defaultLanguageModelPurpose = 'chat';
override readonly description = 'This chat will test originalState functionality with sequential changes.';
override languageModelRequirements: LanguageModelRequirement[] = [];
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileService)
protected readonly fileService: FileService;
@inject(ChangeSetFileElementFactory)
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
override async invoke(request: MutableChatRequestModel): Promise<void> {
const roots = this.workspaceService.tryGetRoots();
if (roots.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'No workspace is open. For using this test agent, please open a workspace with at least one file.'
));
request.response.complete();
return;
}
const root = roots[0];
const files = root.children?.filter(child => child.isFile);
if (!files || files.length === 0) {
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'The workspace does not contain any files. For using this test agent, please add at least one file in the root.'
));
request.response.complete();
return;
}
const chatSessionId = request.session.id;
const requestId = request.id;
request.response.response.addContent(new MarkdownChatResponseContentImpl(
'Testing originalState functionality...\n\n' +
'Three sequential changes to an existing file with 1000ms delays between each.'
));
await wait(1000);
request.session.changeSet.setTitle('Original State Test Changes');
// Select an existing file for sequential modifications
const existingFile = files[Math.floor(Math.random() * files.length)];
const existingFileUri = existingFile.resource;
// Read the current content to use as originalState
const currentContent = await this.fileService.read(existingFileUri);
const originalState = currentContent.value.toString();
// First modification with originalState provided
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 1'));
const modifiedContent1 = await this.computeModifiedState(originalState, 1);
await this.fileService.write(existingFileUri, modifiedContent1);
const firstModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent1,
requestId,
chatSessionId
});
request.session.changeSet.addElements(firstModification);
await wait(1000);
// Second modification with originalState from previous change
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 2'));
const modifiedContent2 = await this.computeModifiedState(modifiedContent1, 2);
await this.fileService.write(existingFileUri, modifiedContent2);
const secondModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent2,
requestId,
chatSessionId
});
request.session.changeSet.addElements(secondModification);
await wait(1000);
// Third modification with originalState from previous change
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nCreate modification 3'));
const modifiedContent3 = await this.computeModifiedState(modifiedContent2, 3);
await this.fileService.write(existingFileUri, modifiedContent3);
const thirdModification = this.fileChangeFactory({
uri: existingFileUri,
type: 'modify',
state: 'applied',
originalState,
targetState: modifiedContent3,
requestId,
chatSessionId
});
request.session.changeSet.addElements(thirdModification);
request.response.response.addContent(new MarkdownChatResponseContentImpl('\n\nTest completed!'));
request.response.complete();
}
async computeModifiedState(content: string, changeNumber: number): Promise<string> {
const changeComment = `// Modified by Original State Test Agent - Change ${changeNumber}\n`;
return changeComment + content + `\n// This line was added by change ${changeNumber} at ${new Date().toISOString()}`;
}
protected override async getSystemMessageDescription(): Promise<SystemMessageDescription | undefined> {
return undefined;
}
}

View File

@@ -0,0 +1,136 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource 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
// *****************************************************************************
import { PromptService } from '@theia/ai-core/lib/common/prompt-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
export function bindSampleChatCommandContribution(bind: interfaces.Bind): void {
bind(SampleChatCommandContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(SampleChatCommandContribution);
}
/**
* This contribution demonstrates how to register slash commands as prompt fragments for chat agents.
* Commands can use argument substitution ($ARGUMENTS, $1, $2, etc.) and be filtered by agent.
*
* The commands registered here will be available in the chat input autocomplete when typing '/'.
* For example, the '/explain' command is only available when using the 'Universal' agent.
*/
@injectable()
export class SampleChatCommandContribution implements FrontendApplicationContribution {
@inject(PromptService)
protected readonly promptService: PromptService;
onStart(): void {
this.registerCommands();
}
protected registerCommands(): void {
// Example 1: Simple command available for all agents
this.promptService.addBuiltInPromptFragment({
id: 'sample-hello',
template: 'Say hello to $ARGUMENTS in a friendly way.',
isCommand: true,
commandName: 'hello',
commandDescription: 'Say hello to someone',
commandArgumentHint: '<name>'
});
// Example 2: Command with $ARGUMENTS and specific agent
this.promptService.addBuiltInPromptFragment({
id: 'sample-explain',
template: `Provide a clear and detailed explanation of the following topic: $ARGUMENTS
Consider:
- Core concepts and definitions
- Practical examples
- Common use cases
- Best practices`,
isCommand: true,
commandName: 'explain',
commandDescription: 'Explain a concept in detail',
commandArgumentHint: '<topic>',
commandAgents: ['Universal']
});
// Example 3: Command with numbered arguments ($1, $2)
this.promptService.addBuiltInPromptFragment({
id: 'sample-compare',
template: `Compare and contrast the following two items:
Item 1: $1
Item 2: $2
Please analyze:
- Key similarities
- Important differences
- When to use each
- Specific advantages and disadvantages`,
isCommand: true,
commandName: 'compare',
commandDescription: 'Compare two concepts or items',
commandArgumentHint: '<item1> <item2>',
commandAgents: ['Universal']
});
// Example 4: Command combining $ARGUMENTS with variables
this.promptService.addBuiltInPromptFragment({
id: 'sample-analyze',
template: `Analyze the selected code with focus on: $ARGUMENTS
Selected code:
#selection
Consider the overall file context:
#file`,
isCommand: true,
commandName: 'analyze',
commandDescription: 'Analyze code with specific focus',
commandArgumentHint: '<focus-area>',
commandAgents: ['Universal']
});
// Example 5: Command with optional arguments (shown by [] in hint)
this.promptService.addBuiltInPromptFragment({
id: 'sample-summarize',
template: `Create a concise summary of the following content$1.
Content: $ARGUMENTS`,
isCommand: true,
commandName: 'summarize',
commandDescription: 'Summarize content',
commandArgumentHint: '<content> [style]'
});
// Example 6: Multi-agent command (available for multiple specific agents)
this.promptService.addBuiltInPromptFragment({
id: 'sample-debug',
template: `Help debug the following issue: $ARGUMENTS
Focus on:
- Identifying the root cause
- Providing specific solutions
- Suggesting preventive measures`,
isCommand: true,
commandName: 'debug',
commandDescription: 'Get help debugging an issue',
commandArgumentHint: '<problem-description>',
commandAgents: ['Universal', 'AskAndContinue']
});
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { Command, CommandContribution, CommandRegistry, ContributionFilterRegistry, FilterContribution, bindContribution } from '@theia/core/lib/common';
import { injectable, interfaces } from '@theia/core/shared/inversify';
export namespace SampleFilteredCommand {
const API_SAMPLES_CATEGORY = 'API Samples';
export const FILTERED: Command = {
id: 'example_command.filtered',
category: API_SAMPLES_CATEGORY,
label: 'This command should be filtered out'
};
export const FILTERED2: Command = {
id: 'example_command.filtered2',
category: API_SAMPLES_CATEGORY,
label: 'This command should be filtered out (2)'
};
}
/**
* This sample command is used to test the runtime filtering of already bound contributions.
*/
@injectable()
export class SampleFilteredCommandContribution implements CommandContribution {
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SampleFilteredCommand.FILTERED, { execute: () => { } });
}
}
@injectable()
export class SampleFilterAndCommandContribution implements FilterContribution, CommandContribution {
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SampleFilteredCommand.FILTERED2, { execute: () => { } });
}
registerContributionFilters(registry: ContributionFilterRegistry): void {
registry.addFilters([CommandContribution], [
// filter ourselves out
contrib => contrib.constructor !== this.constructor
]);
registry.addFilters('*', [
// filter a contribution based on its class type
contrib => !(contrib instanceof SampleFilteredCommandContribution)
]);
}
}
export function bindSampleFilteredCommandContribution(bind: interfaces.Bind): void {
bind(CommandContribution).to(SampleFilteredCommandContribution).inSingletonScope();
bind(SampleFilterAndCommandContribution).toSelf().inSingletonScope();
bindContribution(bind, SampleFilterAndCommandContribution, [CommandContribution, FilterContribution]);
}

View File

@@ -0,0 +1,71 @@
/********************************************************************************
* 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
********************************************************************************/
import { CommandContribution, CommandRegistry } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
@injectable()
export class SampleFileSystemCapabilities implements CommandContribution {
@inject(RemoteFileSystemProvider)
protected readonly remoteFileSystemProvider: RemoteFileSystemProvider;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({
id: 'toggleFileSystemReadonly',
label: 'Toggle File System Readonly',
category: 'API Samples'
}, {
execute: () => {
const readonly = (this.remoteFileSystemProvider.capabilities & FileSystemProviderCapabilities.Readonly) !== 0;
if (readonly) {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities & ~FileSystemProviderCapabilities.Readonly);
} else {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities | FileSystemProviderCapabilities.Readonly);
}
}
});
commands.registerCommand({
id: 'addFileSystemReadonlyMessage',
label: 'Add File System ReadonlyMessage',
category: 'API Samples'
}, {
execute: () => {
const readonlyMessage = new MarkdownStringImpl(`Added new **Markdown** string '+${Date.now()}`);
this.remoteFileSystemProvider['setReadOnlyMessage'](readonlyMessage);
}
});
commands.registerCommand({
id: 'removeFileSystemReadonlyMessage',
label: 'Remove File System ReadonlyMessage',
category: 'API Samples'
}, {
execute: () => {
this.remoteFileSystemProvider['setReadOnlyMessage'](undefined);
}
});
}
}
export function bindSampleFileSystemCapabilitiesCommands(bind: interfaces.Bind): void {
bind(CommandContribution).to(SampleFileSystemCapabilities).inSingletonScope();
}

View File

@@ -0,0 +1,87 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { postConstruct, injectable, inject, interfaces, named } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution, LabelProvider,
} from '@theia/core/lib/browser';
import { ILogger } from '@theia/core/lib/common/logger';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { createPreferenceProxy, PreferenceService, PreferenceProxy, PreferenceContribution } from '@theia/core';
import { FileWatchingPreferencesSchema } from '../../common/preference-schema';
export function bindSampleFileWatching(bind: interfaces.Bind): void {
bind(FrontendApplicationContribution).to(SampleFileWatchingContribution).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: FileWatchingPreferencesSchema });
bind(FileWatchingPreferences).toDynamicValue(
ctx => createPreferenceProxy(ctx.container.get(PreferenceService), FileWatchingPreferencesSchema)
);
}
const FileWatchingPreferences = Symbol('FileWatchingPreferences');
type FileWatchingPreferences = PreferenceProxy<FileWatchingPreferencesSchema>;
interface FileWatchingPreferencesSchema {
'sample.file-watching.verbose': boolean
}
@injectable()
class SampleFileWatchingContribution implements FrontendApplicationContribution {
protected verbose: boolean;
@inject(FileService)
protected readonly fileService: FileService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(FileWatchingPreferences)
protected readonly fileWatchingPreferences: FileWatchingPreferences;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
@postConstruct()
protected init(): void {
this.verbose = this.fileWatchingPreferences['sample.file-watching.verbose'];
this.fileWatchingPreferences.onPreferenceChanged(e => {
if (e.preferenceName === 'sample.file-watching.verbose') {
this.verbose = this.fileWatchingPreferences['sample.file-watching.verbose'];
}
});
}
onStart(): void {
this.fileService.onDidFilesChange(event => {
// Only log if the verbose preference is set.
if (this.verbose) {
// Get the workspace roots for the current frontend:
const roots = this.workspaceService.tryGetRoots();
// Create some name to help find out which frontend logged the message:
const workspace = roots.length > 0
? roots.map(root => this.labelProvider.getLongName(root.resource)).join('+')
: '<no workspace>';
this.logger.info(`Sample File Watching: ${event.changes.length} file(s) changed! ${workspace}`);
}
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,61 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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
// *****************************************************************************
import { injectable, inject, interfaces } from '@theia/core/shared/inversify';
import { Command, CommandContribution, CommandRegistry, CommandHandler } from '@theia/core';
import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
import { SampleDynamicLabelProviderContribution } from './sample-dynamic-label-provider-contribution';
export namespace ExampleLabelProviderCommands {
const API_SAMPLES_CATEGORY = 'API Samples';
export const TOGGLE_SAMPLE: Command = {
id: 'example_label_provider.toggle',
category: API_SAMPLES_CATEGORY,
label: 'Toggle Dynamically-Changing Labels'
};
}
@injectable()
export class SampleDynamicLabelProviderCommandContribution implements FrontendApplicationContribution, CommandContribution {
@inject(SampleDynamicLabelProviderContribution)
protected readonly labelProviderContribution: SampleDynamicLabelProviderContribution;
initialize(): void { }
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(ExampleLabelProviderCommands.TOGGLE_SAMPLE, new ExampleLabelProviderCommandHandler(this.labelProviderContribution));
}
}
export class ExampleLabelProviderCommandHandler implements CommandHandler {
constructor(private readonly labelProviderContribution: SampleDynamicLabelProviderContribution) {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute(...args: any[]): any {
this.labelProviderContribution.toggle();
}
}
export const bindDynamicLabelProvider = (bind: interfaces.Bind) => {
bind(SampleDynamicLabelProviderContribution).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(SampleDynamicLabelProviderContribution);
bind(CommandContribution).to(SampleDynamicLabelProviderCommandContribution).inSingletonScope();
};

View File

@@ -0,0 +1,91 @@
// *****************************************************************************
// Copyright (C) 2019 Arm 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
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { DefaultUriLabelProviderContribution, DidChangeLabelEvent } from '@theia/core/lib/browser/label-provider';
import URI from '@theia/core/lib/common/uri';
import { Emitter, Event } from '@theia/core';
@injectable()
export class SampleDynamicLabelProviderContribution extends DefaultUriLabelProviderContribution {
protected isActive: boolean = false;
constructor() {
super();
const outer = this;
setInterval(() => {
if (this.isActive) {
outer.x++;
outer.fireLabelsDidChange();
}
}, 1000);
}
override canHandle(element: object): number {
if (this.isActive && element.toString().includes('test')) {
return 30;
}
return 0;
}
toggle(): void {
this.isActive = !this.isActive;
this.fireLabelsDidChange();
}
private fireLabelsDidChange(): void {
this.onDidChangeEmitter.fire({
affects: (element: URI) => element.toString().includes('test')
});
}
protected override getUri(element: URI): URI {
return new URI(element.toString());
}
override getIcon(element: URI): string {
const uri = this.getUri(element);
const icon = super.getFileIcon(uri);
if (!icon) {
return this.defaultFileIcon;
}
return icon;
}
protected override readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
private x: number = 0;
override getName(element: URI): string | undefined {
const uri = this.getUri(element);
if (this.isActive && uri.toString().includes('test')) {
return super.getName(uri) + '-' + this.x.toString(10);
} else {
return super.getName(uri);
}
}
override getLongName(element: URI): string | undefined {
const uri = this.getUri(element);
return super.getLongName(uri);
}
override get onDidChange(): Event<DidChangeLabelEvent> {
return this.onDidChangeEmitter.event;
}
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2025 Dirk Fauth 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
// *****************************************************************************
import { MCPFrontendService, RemoteMCPServerDescription } from '@theia/ai-mcp';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, QuickInputService } from '@theia/core/lib/browser';
import { ILogger } from '@theia/core/lib/common/logger';
@injectable()
export class ResolveMcpFrontendContribution
implements FrontendApplicationContribution {
@inject(MCPFrontendService)
protected readonly mcpFrontendService: MCPFrontendService;
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
async onStart(): Promise<void> {
const githubServer: RemoteMCPServerDescription = {
name: 'github',
serverUrl: 'https://api.githubcopilot.com/mcp/',
resolve: async serverDescription => {
this.logger.debug('Resolving GitHub MCP server description');
// Prompt user for authentication token
const authToken = await this.quickInputService.input({
prompt: 'Enter authentication token for GitHubMCP server',
password: true,
value: 'serverAuthToken' in serverDescription ? serverDescription.serverAuthToken || '' : ''
});
if (authToken) {
// Return updated server description with new token
return {
...serverDescription,
serverAuthToken: authToken
} as RemoteMCPServerDescription;
}
// If no token provided, return original description
return serverDescription;
}
};
this.mcpFrontendService.addOrUpdateServer(githubServer);
}
}

View File

@@ -0,0 +1,218 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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
// *****************************************************************************
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { Tool, Resource, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
import { z } from 'zod';
import { MCPFrontendContribution, ToolProvider } from '@theia/ai-mcp-server/lib/browser/mcp-frontend-contribution';
import { ILogger } from '@theia/core/lib/common/logger';
/**
* Sample frontend MCP contribution that demonstrates accessing frontend-only services
*/
@injectable()
export class SampleFrontendMCPContribution implements MCPFrontendContribution {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
async getTools(): Promise<Tool[]> {
return [
{
name: 'sample-workspace-info',
description: 'Get information about the current workspace',
inputSchema: {
type: 'object',
properties: {},
required: []
}
},
{
name: 'sample-workspace-files',
description: 'List files in the workspace',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Optional pattern to filter files'
}
},
required: []
}
}
];
}
async getTool(name: string): Promise<ToolProvider | undefined> {
switch (name) {
case 'sample-workspace-info':
return {
handler: async args => {
try {
this.logger.debug('Getting workspace info with args:', args);
const roots = await this.workspaceService.roots;
return {
workspace: {
roots: roots.map(r => r.resource.toString()),
name: roots[0]?.name || 'Unknown'
}
};
} catch (error) {
this.logger.error('Error getting workspace info:', error);
throw error;
}
},
inputSchema: z.object({})
};
case 'sample-workspace-files':
return {
handler: async args => {
try {
this.logger.debug('Listing workspace files with args:', args);
const typedArgs = args as { pattern?: string };
// Here we could use the FileService to collect all file information from the workspace
// const roots = await this.workspaceService.roots;
// const files: string[] = [];
// for (const root of roots) {
// const rootUri = new URI(root.resource.toString());
// const stat = await this.fileService.resolve(rootUri);
// if (stat.children) {
// for (const child of stat.children) {
// files.push(child.resource.toString());
// }
// }
// }
// Return dummy content for demonstration purposes
const dummyFiles = [
'foo1.txt',
'foo2.txt',
'bar1.js',
'bar2.js',
'baz1.md',
'baz2.md',
'config.json',
'package.json',
'README.md'
];
return {
files: typedArgs.pattern ? dummyFiles.filter(f => f.includes(typedArgs.pattern!)) : dummyFiles
};
} catch (error) {
this.logger.error('Error listing workspace files:', error);
throw error;
}
},
inputSchema: z.object({
pattern: z.string().optional()
})
};
default:
return undefined;
}
}
async getResources(): Promise<Resource[]> {
return [
{
uri: 'sample-workspace://info',
name: 'Sample Workspace Information',
description: 'General information about the current workspace',
mimeType: 'application/json'
}
];
}
async readResource(uri: string): Promise<unknown> {
if (uri === 'sample-workspace://info') {
try {
const roots = await this.workspaceService.roots;
return {
workspace: {
roots: roots.map(r => ({
uri: r.resource.toString(),
name: r.name,
scheme: r.resource.scheme
})),
rootCount: roots.length
}
};
} catch (error) {
this.logger.error('Error reading workspace resource:', error);
throw error;
}
}
throw new Error(`Unknown resource: ${uri}`);
}
async getPrompts(): Promise<Prompt[]> {
return [
{
name: 'sample-workspace-context',
description: 'Generate context information about the workspace',
arguments: [
{
name: 'includeFiles',
description: 'Whether to include file listings',
required: false
}
]
}
];
}
async getPrompt(name: string, args: unknown): Promise<PromptMessage[]> {
if (name === 'sample-workspace-context') {
try {
const parsedArgs = args as { includeFiles?: boolean };
const roots = await this.workspaceService.roots;
let content = 'Current workspace information:\n\n';
content += `Number of workspace roots: ${roots.length}\n`;
for (const root of roots) {
content += `- Root: ${root.name} (${root.resource.toString()})\n`;
}
if (parsedArgs.includeFiles) {
content += '\nFile structure would be included here in a real implementation.';
}
return [
{
role: 'user',
content: {
type: 'text',
text: content
}
}
];
} catch (error) {
this.logger.error('Error generating workspace context prompt:', error);
throw error;
}
}
throw new Error(`Unknown prompt: ${name}`);
}
}

View File

@@ -0,0 +1,335 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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
// *****************************************************************************
import { ConfirmDialog, Dialog, QuickInputService } from '@theia/core/lib/browser';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { SelectComponent } from '@theia/core/lib/browser/widgets/select-component';
import {
Command, CommandContribution, CommandMenu, CommandRegistry, ContextExpressionMatcher, MAIN_MENU_BAR,
MenuContribution, MenuModelRegistry, MenuPath, MessageService
} from '@theia/core/lib/common';
import { ILogger } from '@theia/core/lib/common/logger';
import { inject, injectable, interfaces, named } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { ReactNode } from '@theia/core/shared/react';
const API_SAMPLES_CATEGORY = 'API Samples';
const SampleCommand: Command = {
id: 'sample-command',
label: 'Command',
category: API_SAMPLES_CATEGORY
};
const SampleCommand2: Command = {
id: 'sample-command2',
label: 'Command 2',
category: API_SAMPLES_CATEGORY
};
const SampleCommandConfirmDialog: Command = {
id: 'sample-command-confirm-dialog',
label: 'Confirm Dialog',
category: API_SAMPLES_CATEGORY
};
const SampleComplexCommandConfirmDialog: Command = {
id: 'sample-command-complex-confirm-dialog',
label: 'Complex Confirm Dialog',
category: API_SAMPLES_CATEGORY
};
const SampleCommandWithProgressMessage: Command = {
id: 'sample-command-with-progress',
label: 'Command With Progress Message',
category: API_SAMPLES_CATEGORY
};
const SampleCommandWithIndeterminateProgressMessage: Command = {
id: 'sample-command-with-indeterminate-progress',
label: 'Command With Indeterminate Progress Message',
category: API_SAMPLES_CATEGORY
};
const SampleQuickInputCommand: Command = {
id: 'sample-quick-input-command',
label: 'Test Positive Integer',
category: API_SAMPLES_CATEGORY
};
const SampleSelectDialog: Command = {
id: 'sample-command-select-dialog',
label: 'Select Component Dialog',
category: API_SAMPLES_CATEGORY
};
const SamplePersistentNotification: Command = {
id: 'sample-persistent-notification',
label: 'Persistent Notification (No Timeout)',
category: API_SAMPLES_CATEGORY
};
const SampleVanishingNotification: Command = {
id: 'sample-vanishing-notification',
label: 'Vanishing Notification (500ms Timeout)',
category: API_SAMPLES_CATEGORY
};
@injectable()
export class SampleCommandContribution implements CommandContribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'create-quick-pick-sample', label: 'Internal QuickPick', category: API_SAMPLES_CATEGORY }, {
execute: () => {
const pick = this.quickInputService.createQuickPick();
pick.items = [{ label: '1' }, { label: '2' }, { label: '3' }];
pick.onDidAccept(() => {
this.logger.debug(`accepted: ${pick.selectedItems[0]?.label}`);
pick.hide();
});
pick.show();
}
});
commands.registerCommand(SampleCommand, {
execute: () => {
alert('This is a sample command!');
}
});
commands.registerCommand(SampleCommand2, {
execute: () => {
alert('This is sample command2!');
}
});
commands.registerCommand(SampleCommandConfirmDialog, {
execute: async () => {
const choice = await new ConfirmDialog({
title: 'Sample Confirm Dialog',
msg: 'This is a sample with lots of text:' + Array(100)
.fill(undefined)
.map((element, index) => `\n\nExtra line #${index}`)
.join('')
}).open();
this.messageService.info(`Sample confirm dialog returned with: \`${JSON.stringify(choice)}\``);
}
});
commands.registerCommand(SampleComplexCommandConfirmDialog, {
execute: async () => {
const mainDiv = document.createElement('div');
for (const color of ['#FF00007F', '#00FF007F', '#0000FF7F']) {
const innerDiv = document.createElement('div');
innerDiv.textContent = 'This is a sample with lots of text:' + Array(50)
.fill(undefined)
.map((_, index) => `\n\nExtra line #${index}`)
.join('');
innerDiv.style.backgroundColor = color;
innerDiv.style.padding = '5px';
mainDiv.appendChild(innerDiv);
}
const choice = await new ConfirmDialog({
title: 'Sample Confirm Dialog',
msg: mainDiv
}).open();
this.messageService.info(`Sample confirm dialog returned with: \`${JSON.stringify(choice)}\``);
}
});
commands.registerCommand(SampleSelectDialog, {
execute: async () => {
await new class extends ReactDialog<boolean> {
constructor() {
super({ title: 'Sample Select Component Dialog' });
this.appendAcceptButton(Dialog.OK);
}
protected override render(): ReactNode {
return React.createElement(SelectComponent, {
options: Array.from(Array(10).keys()).map(i => ({ label: 'Option ' + ++i })),
defaultValue: 0
});
}
override get value(): boolean {
return true;
}
}().open();
}
});
commands.registerCommand(SampleQuickInputCommand, {
execute: async () => {
const result = await this.quickInputService.input({
placeHolder: 'Please provide a positive integer',
validateInput: async (input: string) => {
const numericValue = Number(input);
if (isNaN(numericValue)) {
return 'Invalid: NaN';
} else if (numericValue % 2 === 1) {
return 'Invalid: Odd Number';
} else if (numericValue < 0) {
return 'Invalid: Negative Number';
} else if (!Number.isInteger(numericValue)) {
return 'Invalid: Only Integers Allowed';
}
}
});
if (result) {
this.messageService.info(`Positive Integer: ${result}`);
}
}
});
commands.registerCommand(SampleCommandWithProgressMessage, {
execute: () => {
this.messageService
.showProgress({
text: 'Starting to report progress',
})
.then(progress => {
window.setTimeout(() => {
progress.report({
message: 'First step completed',
work: { done: 25, total: 100 }
});
}, 2000);
window.setTimeout(() => {
progress.report({
message: 'Next step completed',
work: { done: 60, total: 100 }
});
}, 4000);
window.setTimeout(() => {
progress.report({
message: 'Complete',
work: { done: 100, total: 100 }
});
}, 6000);
window.setTimeout(() => progress.cancel(), 7000);
});
}
});
commands.registerCommand(SampleCommandWithIndeterminateProgressMessage, {
execute: () => {
this.messageService
.showProgress({
text: 'Starting to report indeterminate progress',
})
.then(progress => {
window.setTimeout(() => {
progress.report({
message: 'First step completed',
});
}, 2000);
window.setTimeout(() => {
progress.report({
message: 'Next step completed',
});
}, 4000);
window.setTimeout(() => {
progress.report({
message: 'Complete',
});
}, 6000);
window.setTimeout(() => progress.cancel(), 7000);
});
}
});
commands.registerCommand(SamplePersistentNotification, {
execute: () => {
this.messageService.info(
'This notification will stay visible until you dismiss it manually.',
{ timeout: 0 }
);
}
});
commands.registerCommand(SampleVanishingNotification, {
execute: () => {
this.messageService.info(
'This notification will stay visible for 500ms.',
{ timeout: 500 }
);
}
});
}
}
@injectable()
export class SampleMenuContribution implements MenuContribution {
registerMenus(menus: MenuModelRegistry): void {
setTimeout(() => {
const subMenuPath = [...MAIN_MENU_BAR, 'sample-menu'];
menus.registerSubmenu(subMenuPath, 'Sample Menu', { sortString: '2' }); // that should put the menu right next to the File menu
menus.registerMenuAction(subMenuPath, {
commandId: SampleCommand.id,
order: '0'
});
menus.registerMenuAction(subMenuPath, {
commandId: SampleCommand2.id,
order: '2'
});
const subSubMenuPath = [...subMenuPath, 'sample-sub-menu'];
menus.registerSubmenu(subSubMenuPath, 'Sample sub menu', { sortString: '2' });
menus.registerMenuAction(subSubMenuPath, {
commandId: SampleCommand.id,
order: '1'
});
menus.registerMenuAction(subSubMenuPath, {
commandId: SampleCommand2.id,
order: '3'
});
const placeholder = new PlaceholderMenuNode([...subSubMenuPath, 'placeholder'].join('-'), 'Placeholder', '0');
menus.registerCommandMenu(subSubMenuPath, placeholder);
/**
* Register an action menu with an invalid command (un-registered and without a label) in order
* to determine that menus and the layout does not break on startup.
*/
menus.registerMenuAction(subMenuPath, { commandId: 'invalid-command' });
}, 10000);
}
}
/**
* Special menu node that is not backed by any commands and is always disabled.
*/
export class PlaceholderMenuNode implements CommandMenu {
constructor(readonly id: string, public readonly label: string, readonly order?: string, readonly icon?: string) { }
isEnabled(effectiveMenuPath: MenuPath, ...args: unknown[]): boolean {
return false;
}
isToggled(effectiveMenuPath: MenuPath): boolean {
return false;
}
run(effectiveMenuPath: MenuPath, ...args: unknown[]): Promise<void> {
throw new Error('Should never happen');
}
getAccelerator(context: HTMLElement | undefined): string[] {
return [];
}
get sortString(): string {
return this.order || this.label;
}
isVisible<T>(effectiveMenuPath: MenuPath, contextMatcher: ContextExpressionMatcher<T>, context: T | undefined, ...args: unknown[]): boolean {
return true;
}
}
export const bindSampleMenu = (bind: interfaces.Bind) => {
bind(CommandContribution).to(SampleCommandContribution).inSingletonScope();
bind(MenuContribution).to(SampleMenuContribution).inSingletonScope();
};

View File

@@ -0,0 +1,277 @@
/********************************************************************************
* 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
********************************************************************************/
/**
* The command contributed in this file allows us to generate a copy of the schema expected for editor preferences by Monaco,
* as well as an interface corresponding to those properties for use with our EditorPreferences PreferenceProxy.
* It examines the schemata registered with the Monaco `ConfigurationRegistry` and writes any configurations associated with the editor
* to a file in the `editor` package. It also generates an interface based on the types specified in the schema.
* The only manual work required during a Monaco uplift is to run the command and then update any fields of the interface where the
* schema type is `array` or `object`, since it is tricky to extract the type details for such fields automatically.
*/
import { ConfigurationScope, Extensions, IConfigurationRegistry } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configurationRegistry';
import { Registry } from '@theia/monaco-editor-core/esm/vs/platform/registry/common/platform';
import { CommandContribution, CommandRegistry, MaybeArray, MessageService, nls, PreferenceScope } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PreferenceValidationService } from '@theia/core/lib/browser';
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
import { JsonType } from '@theia/core/lib/common/json-schema';
import { editorOptionsRegistry } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
import { PreferenceDataProperty } from '@theia/core/lib/common/preferences/preference-schema';
function generateContent(properties: string, interfaceEntries: string[]): string {
return `/********************************************************************************
* 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
********************************************************************************/
import { isOSX, isWindows, nls } from '@theia/core';
import { PreferenceSchema } from '@theia/core/lib/browser';
/* eslint-disable @typescript-eslint/quotes,max-len,no-null/no-null */
/**
* Please do not modify this file by hand. It should be generated automatically
* during a Monaco uplift using the command registered by monaco-editor-preference-extractor.ts
* The only manual work required is fixing preferences with type 'array' or 'object'.
*/
export const editorGeneratedPreferenceProperties: PreferenceSchema['properties'] = ${properties};
export interface GeneratedEditorPreferences {
${interfaceEntries.join('\n ')}
}
`;
}
const dequoteMarker = '@#@';
// From src/vs/editor/common/config/editorOptions.ts
const DEFAULT_WINDOWS_FONT_FAMILY = "Consolas, \\'Courier New\\', monospace";
const DEFAULT_MAC_FONT_FAMILY = "Menlo, Monaco, \\'Courier New\\', monospace";
const DEFAULT_LINUX_FONT_FAMILY = "\\'Droid Sans Mono\\', \\'monospace\\', monospace";
const fontFamilyText = `${dequoteMarker}isOSX ? '${DEFAULT_MAC_FONT_FAMILY}' : isWindows ? '${DEFAULT_WINDOWS_FONT_FAMILY}' : '${DEFAULT_LINUX_FONT_FAMILY}'${dequoteMarker}`;
const fontSizeText = `${dequoteMarker}isOSX ? 12 : 14${dequoteMarker}`;
/**
* This class is intended for use when uplifting Monaco.
*/
@injectable()
export class MonacoEditorPreferenceSchemaExtractor implements CommandContribution {
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(MessageService) protected readonly messageService: MessageService;
@inject(FileService) protected readonly fileService: FileService;
@inject(PreferenceValidationService) protected readonly preferenceValidationService: PreferenceValidationService;
@inject(MonacoEditorProvider) protected readonly monacoEditorProvider: MonacoEditorProvider;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'check-for-unvalidated-editor-preferences', label: 'Check for unvalidated editor preferences in Monaco', category: 'API Samples' }, {
execute: () => {
const firstRootUri = this.workspaceService.tryGetRoots()[0]?.resource;
if (firstRootUri) {
const validatedEditorPreferences = new Set(editorOptionsRegistry.map(validator => validator.name));
const allEditorPreferenceKeys = Object.keys(this.monacoEditorProvider['createOptions'](
this.monacoEditorProvider['preferencePrefixes'], firstRootUri.toString(), 'typescript'
));
const unvalidatedKeys = allEditorPreferenceKeys.filter(key => !validatedEditorPreferences.has(key));
console.log('Unvalidated keys are:', unvalidatedKeys);
}
}
});
commands.registerCommand({ id: 'extract-editor-preference-schema', label: 'Extract editor preference schema from Monaco', category: 'API Samples' }, {
execute: async () => {
const roots = this.workspaceService.tryGetRoots();
if (roots.length !== 1 || !(roots[0].resource.path.toString() ?? '').includes('theia')) {
this.messageService.warn('This command should only be executed in the Theia workspace.');
}
const theiaRoot = roots[0];
const fileToWrite = theiaRoot.resource.resolve('packages/editor/src/common/editor-generated-preference-schema.ts');
const properties = {};
Registry.as<IConfigurationRegistry>(Extensions.Configuration).getConfigurations().forEach(config => {
if (config.id === 'editor' && config.properties) {
Object.assign(properties, config.properties);
}
});
this.guaranteePlatformOptions(properties);
const interfaceEntries = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
for (const [name, description] of Object.entries(properties) as Array<[string, any]>) {
const { scope, overridable } = this.getScope(description.scope);
description.scope = scope;
description.overridable = overridable;
delete description.defaultDefaultValue;
delete description.restricted;
if (name === 'editor.fontSize') {
description.default = fontSizeText;
} else if (name === 'editor.fontFamily') {
description.default = fontFamilyText;
}
interfaceEntries.push(`'${name}': ${this.formatSchemaForInterface(description)};`);
}
const stringified = JSON.stringify(properties, this.codeSnippetReplacer(), 4);
const propertyList = this.dequoteCodeSnippets(stringified);
const content = generateContent(propertyList, interfaceEntries);
await this.fileService.write(fileToWrite, content);
}
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected codeSnippetReplacer(): (key: string, value: any) => any {
// JSON.stringify doesn't give back the whole context when serializing so we use state...
let lastPreferenceName: string;
return (key, value) => {
if (key.startsWith('editor.') || key.startsWith('diffEditor.')) {
lastPreferenceName = key;
}
if ((key === 'description' || key === 'markdownDescription') && typeof value === 'string') {
if (value.length === 0) {
return value;
}
const defaultKey = nls.getDefaultKey(value);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${value}${dequoteMarker}")${dequoteMarker}`;
}
}
if ((key === 'enumDescriptions' || key === 'markdownEnumDescriptions') && Array.isArray(value)) {
return value.map((description, i) => {
if (description.length === 0) {
return description;
}
const defaultKey = nls.getDefaultKey(description);
if (defaultKey) {
return `${dequoteMarker}nls.localizeByDefault(${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
} else {
const localizationKey = `${dequoteMarker}"theia/editor/${lastPreferenceName}${i}${dequoteMarker}"`;
return `${dequoteMarker}nls.localize(${localizationKey}, ${dequoteMarker}"${description}${dequoteMarker}")${dequoteMarker}`;
}
});
}
return value;
};
};
protected getScope(monacoScope: unknown): { scope: PreferenceScope, overridable: boolean } {
switch (monacoScope) {
case ConfigurationScope.MACHINE_OVERRIDABLE:
case ConfigurationScope.WINDOW:
case ConfigurationScope.RESOURCE:
return { scope: PreferenceScope.Folder, overridable: false };
case ConfigurationScope.LANGUAGE_OVERRIDABLE:
return { scope: PreferenceScope.Folder, overridable: true };
case ConfigurationScope.APPLICATION:
case ConfigurationScope.MACHINE:
return { scope: PreferenceScope.User, overridable: false };
}
return { scope: PreferenceScope.Default, overridable: false };
}
protected formatSchemaForInterface(schema: PreferenceDataProperty): string {
const defaultValue = schema.default !== undefined ? schema.default : schema.default;
// There are a few preferences for which VSCode uses defaults that do not match the schema. We have to handle those manually.
if (defaultValue !== undefined && this.preferenceValidationService.validateBySchema('any-preference', defaultValue, schema) !== defaultValue) {
return 'HelpBadDefaultValue';
}
const jsonType = schema.const !== undefined ? schema.const : (schema.enum ?? schema.type);
if (jsonType === undefined) {
const subschemata = schema.anyOf ?? schema.oneOf;
if (subschemata) {
const permittedTypes = [].concat.apply(subschemata.map(subschema => this.formatSchemaForInterface(subschema).split(' | ')));
return Array.from(new Set(permittedTypes)).join(' | ');
}
}
return this.formatTypeForInterface(jsonType);
}
protected formatTypeForInterface(jsonType?: MaybeArray<JsonType | JSONValue> | undefined): string {
if (Array.isArray(jsonType)) {
return jsonType.map(subtype => this.formatTypeForInterface(subtype)).join(' | ');
}
switch (jsonType) {
case 'boolean':
case 'number':
case 'string':
case 'true':
case 'false':
return jsonType;
case true:
case false:
case null: // eslint-disable-line no-null/no-null
return `${jsonType}`;
case 'integer':
return 'number';
case 'array':
case 'object':
case undefined:
// These have to be fixed manually, so we output a type that will cause a TS error.
return 'Help';
}
// Most of the rest are string literals.
return `'${jsonType}'`;
}
protected dequoteCodeSnippets(stringification: string): string {
return stringification
.replace(new RegExp(`${dequoteMarker}"|"${dequoteMarker}|${dequoteMarker}\\\\`, 'g'), '')
.replace(new RegExp(`\\\\"${dequoteMarker}`, 'g'), '"')
.replace(/\\\\'/g, "\\'");
}
/**
* Ensures that options that are only relevant on certain platforms are caught.
* Check for use of `platform` in src/vs/editor/common/config/editorOptions.ts
*/
protected guaranteePlatformOptions(properties: object): void {
Object.assign(properties, {
'editor.find.globalFindClipboard': {
type: 'boolean',
default: false,
description: 'Controls whether the Find Widget should read or modify the shared find clipboard on macOS.',
included: `${dequoteMarker}isOSX${dequoteMarker}`,
},
'editor.selectionClipboard': {
type: 'boolean',
default: true,
description: 'Controls whether the Linux primary clipboard should be supported.',
included: `${dequoteMarker}!isOSX && !isWindows${dequoteMarker}`
}
});
}
}
// Utility to assist with Monaco uplifts to generate preference schema. Not for regular use in the application.
export function bindMonacoPreferenceExtractor(bind: interfaces.Bind): void {
// bind(MonacoEditorPreferenceSchemaExtractor).toSelf().inSingletonScope();
// bind(CommandContribution).toService(MonacoEditorPreferenceSchemaExtractor);
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2020 SAP SE or an SAP affiliate company 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
// *****************************************************************************
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { OutputChannelManager, OutputChannelSeverity } from '@theia/output/lib/browser/output-channel';
@injectable()
export class SampleOutputChannelWithSeverity
implements FrontendApplicationContribution {
@inject(OutputChannelManager)
protected readonly outputChannelManager: OutputChannelManager;
public onStart(): void {
const channel = this.outputChannelManager.getChannel('API Sample: my test channel');
channel.appendLine('hello info1'); // showed without color
channel.appendLine('hello info2', OutputChannelSeverity.Info);
channel.appendLine('hello error', OutputChannelSeverity.Error);
channel.appendLine('hello warning', OutputChannelSeverity.Warning);
channel.append('inlineInfo1 ');
channel.append('inlineWarning ', OutputChannelSeverity.Warning);
channel.append('inlineError ', OutputChannelSeverity.Error);
channel.append('inlineInfo2', OutputChannelSeverity.Info);
}
}
export const bindSampleOutputChannelWithSeverity = (bind: interfaces.Bind) => {
bind(FrontendApplicationContribution)
.to(SampleOutputChannelWithSeverity)
.inSingletonScope();
};

View File

@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2025 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
import { CommandContribution, CommandRegistry, MessageService, QuickInputService } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { SampleBackendPreferencesService, sampleBackendPreferencesServicePath } from '../../common/preference-protocol';
import { ServiceConnectionProvider } from '@theia/core/lib/browser';
@injectable()
export class SamplePreferenceContribution implements CommandContribution {
@inject(QuickInputService)
protected readonly quickInputService: QuickInputService;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(SampleBackendPreferencesService)
protected readonly preferencesService: SampleBackendPreferencesService;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'samplePreferences.get', label: 'Get Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Get Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Get Backend Preference',
prompt: 'Enter override identifier'
});
const value = await this.preferencesService.getPreference(key, override);
this.messageService.info(`The value is \n${JSON.stringify(value)}`);
}
}
}
);
commands.registerCommand({ id: 'samplePreferences.inspect', label: 'Inspect Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Inspect Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Inspect Backend Preference',
prompt: 'Enter override identifier'
});
const value = await this.preferencesService.inspectPreference(key, override);
this.messageService.info(`The value is \n${JSON.stringify(value)}`);
}
}
}
);
commands.registerCommand({ id: 'samplePreferences.set', label: 'Set Backend Preference', category: 'API Samples' },
{
execute: async () => {
const key = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter preference key'
});
if (key) {
const override = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter override identifier'
});
const valueString = await this.quickInputService.input({
title: 'Set Backend Preference',
prompt: 'Enter JSON value'
});
if (valueString) {
await this.preferencesService.setPreference(key, override, JSON.parse(valueString));
}
}
}
}
);
}
}
export function bindSamplePreferenceContribution(bind: interfaces.Bind): void {
bind(CommandContribution).to(SamplePreferenceContribution).inSingletonScope();
bind(SampleBackendPreferencesService).toDynamicValue(ctx => ServiceConnectionProvider.createProxy(ctx.container, sampleBackendPreferencesServicePath)).inSingletonScope();
}

View File

@@ -0,0 +1,37 @@
// *****************************************************************************
// Copyright (C) 2025 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
// *****************************************************************************
import { TextReplacementContribution } from '@theia/core/lib/browser/preload/text-replacement-contribution';
export class TextSampleReplacementContribution implements TextReplacementContribution {
getReplacement(locale: string): Record<string, string> {
switch (locale) {
case 'en': {
return {
'About': 'About Theia',
};
}
case 'de': {
return {
'About': 'Über Theia',
};
}
}
return {};
}
}

View File

@@ -0,0 +1,34 @@
/********************************************************************************
* 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
********************************************************************************/
.theia-icon {
background-image: url("../icons/theia.png");
background-position: center;
background-repeat: no-repeat;
background-size: contain;
}
#theia-main-content-panel {
background-image: url("../icons/theia.png");
background-position: center center;
background-repeat: no-repeat;
background-size: 15%;
}
.unclosable-window-icon {
-webkit-mask: url("window-icon.svg");
mask: url("window-icon.svg");
}

View File

@@ -0,0 +1,4 @@
<!--Copyright (c) Microsoft Corporation. All rights reserved.-->
<!--Copyright (C) 2019 TypeFox and others.-->
<!--Licensed under the MIT License. See License.txt in the project root for license information.-->
<svg fill="#F6F6F6" height="28" viewBox="0 0 28 28" width="28" xmlns="http://www.w3.org/2000/svg"><g fill="#F6F6F6"><path clip-rule="evenodd" d="m3 3h10v4h-6v6 2 6h6v4h-10zm18 12v6h-6v4h10v-10zm4-2v-10h-10v4h3v-1h4v4h-1v3z" fill-rule="evenodd"/><path d="m9 9h10v10h-10z"/></g></svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -0,0 +1,158 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// *****************************************************************************
// Copyright (C) 2022 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
// *****************************************************************************
import { TestContribution, TestItem, TestRunProfileKind, TestService } from '@theia/test/lib/browser/test-service';
import { CommandContribution, CommandRegistry, Path, URI } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { inject, injectable, interfaces, named, postConstruct } from '@theia/core/shared/inversify';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { FileStatWithMetadata } from '@theia/filesystem/lib/common/files';
import { TestControllerImpl, TestItemImpl, TestRunImpl } from './test-controller';
function stringifyTransformer(key: string, value: any): any {
if (value instanceof URI) {
return value.toString();
}
if (value instanceof TestItemImpl) {
return {
id: value.id,
label: value.label,
range: value.range,
sortKey: value.sortKey,
tags: value.tags,
uri: value.uri,
busy: value.busy,
canResolveChildren: value.canResolveChildren,
children: value.tests,
description: value.description,
error: value.error
};
}
return value;
}
@injectable()
export class SampleTestContribution implements TestContribution, CommandContribution {
@inject(WorkspaceService)
private workspaceService: WorkspaceService;
@inject(FileSearchService)
private searchService: FileSearchService;
@inject(FileService)
private fileService: FileService;
@inject(ILogger) @named('api-samples')
private logger: ILogger;
private testController = new TestControllerImpl('SampleTestController', 'Sample Test Controller');
private usedUris = new Set<string>();
private nextTestId = 0;
private nextRunId = 0;
@postConstruct()
protected init(): void {
this.testController.onItemsChanged(e => {
this.logger.debug(JSON.stringify(e, stringifyTransformer, 4));
});
}
registerCommands(commands: CommandRegistry): void {
commands.registerCommand({ id: 'testController.addSomeTests', label: 'Add Some Tests', category: 'API Samples' }, {
execute: async (...args: any): Promise<any> => {
const root = (await this.workspaceService.roots)[0];
const files = (await this.searchService.find('.json', {
rootUris: [root.resource.toString()],
limit: 1000
})).filter(uri => !this.usedUris.has(uri));
for (let i = 0; i < Math.min(10, files.length); i++) {
const fileUri = new URI(files[i]);
const relativePath = root.resource.path.relative(fileUri.path);
let collection = this.testController.items;
let dirUri = root.resource;
relativePath?.toString().split(Path.separator).forEach(name => {
dirUri = dirUri.withPath(dirUri.path.join(name));
let item = collection.get(name);
if (!item) {
item = new TestItemImpl(dirUri, name);
item.label = name;
collection.add(item);
}
collection = item._children;
});
const meta: FileStatWithMetadata = await this.fileService.resolve(fileUri, { resolveMetadata: true });
const testItem = new TestItemImpl(fileUri, `test-id-${this.nextTestId}`);
testItem.label = `Test number ${this.nextTestId++}`;
testItem.range = {
start: { line: 0, character: 0 },
end: { line: 0, character: Math.min(10, meta.size) }
};
collection.add(testItem);
}
}
});
commands.registerCommand({ id: 'testController.dumpController', label: 'Dump Controller Contents', category: 'API Samples' }, {
execute: (...args: any): any => {
this.logger.debug(JSON.stringify(this.testController, stringifyTransformer, 4));
}
});
}
registerTestControllers(service: TestService): void {
this.testController.addProfile({
kind: TestRunProfileKind.Run,
label: 'Sample run profile #1',
isDefault: false,
canConfigure: true,
tag: '',
run: (name: string, included: readonly TestItem[], excluded: readonly TestItem[]) => {
this.testController.addRun(new TestRunImpl(this.testController, `sample-run-id-${this.nextRunId}`, `sample-profile-1-${this.nextRunId++}`));
},
configure: (): void => {
this.logger.debug('configuring the sample profile 1');
}
});
this.testController.addProfile({
kind: TestRunProfileKind.Run,
label: 'Sample run profile #2',
isDefault: false,
canConfigure: true,
tag: '',
run: (name: string, included: readonly TestItem[], excluded: readonly TestItem[]) => {
this.testController.addRun(new TestRunImpl(this.testController, `sample-run-id-${this.nextRunId}`, `sample-profile-2-${this.nextRunId++}`));
},
configure: (): void => {
this.logger.debug('configuring the sample profile 2');
}
});
service.registerTestController(this.testController);
}
}
export function bindTestSample(bind: interfaces.Bind): void {
bind(SampleTestContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(SampleTestContribution);
bind(TestContribution).toService(SampleTestContribution);
};

View File

@@ -0,0 +1,387 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// *****************************************************************************
// Copyright (C) 2022 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
// *****************************************************************************
import { CancellationToken, Emitter, Event, URI } from '@theia/core';
import { Range, Location, CancellationTokenSource } from '@theia/core/shared/vscode-languageserver-protocol';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { SimpleObservableCollection, TreeCollection, observableProperty } from '@theia/test/lib/common/collections';
import {
TestController, TestExecutionState, TestFailure, TestItem,
TestOutputItem, TestRun, TestRunProfile, TestState, TestStateChangedEvent
} from '@theia/test/lib/browser/test-service';
import { AccumulatingTreeDeltaEmitter, CollectionDelta, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta';
import { timeout } from '@theia/core/lib/common/promise-util';
export class TestItemCollection extends TreeCollection<string, TestItemImpl, TestItemImpl | TestControllerImpl> {
override add(item: TestItemImpl): TestItemImpl | undefined {
item.realParent = this.owner;
return super.add(item);
}
}
export class TestItemImpl implements TestItem {
constructor(readonly uri: URI, readonly id: string) {
this._children = new TestItemCollection(this, (v: TestItemImpl) => v.path, (v: TestItemImpl) => v.deltaBuilder);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected notifyPropertyChange(property: keyof TestItemImpl, value: any): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
if (this.path) {
this.deltaBuilder?.reportChanged(this.path, val);
}
}
_deltaBuilder: TreeDeltaBuilder<string, TestItemImpl> | undefined;
get deltaBuilder(): TreeDeltaBuilder<string, TestItemImpl> | undefined {
if (this._deltaBuilder) {
return this._deltaBuilder;
} else if (this.realParent) {
this._deltaBuilder = this.realParent.deltaBuilder;
return this._deltaBuilder;
} else {
return undefined;
}
}
_path: string[] | undefined;
get path(): string[] {
if (this._path) {
return this._path;
} else if (this.realParent instanceof TestItemImpl) {
this._path = [...this.realParent.path, this.id];
return this._path;
} else {
return [this.id];
}
};
private _parent?: TestItemImpl | TestControllerImpl;
get realParent(): TestItemImpl | TestControllerImpl | undefined {
return this._parent;
}
set realParent(v: TestItemImpl | TestControllerImpl | undefined) {
this.iterate(item => {
item._path = undefined;
return true;
});
this._parent = v;
}
get parent(): TestItem | undefined {
const realParent = this.realParent;
if (realParent instanceof TestItemImpl) {
return realParent;
}
return undefined;
}
get controller(): TestControllerImpl | undefined {
if (this.realParent instanceof TestItemImpl) {
return this.realParent.controller;
}
return this.realParent;
}
protected iterate(toDo: (v: TestItemImpl) => boolean): boolean {
if (toDo(this)) {
for (let i = 0; i < this._children.values.length; i++) {
if (!this._children.values[i].iterate(toDo)) {
return false;
}
}
return true;
} else {
return false;
}
}
@observableProperty('notifyPropertyChange')
label: string = '';
@observableProperty('notifyPropertyChange')
range?: Range;
@observableProperty('notifyPropertyChange')
sortKey?: string | undefined;
@observableProperty('notifyPropertyChange')
tags: string[] = [];
@observableProperty('notifyPropertyChange')
busy: boolean = false;
@observableProperty('notifyPropertyChange')
canResolveChildren: boolean = false;
@observableProperty('notifyPropertyChange')
description?: string | undefined;
@observableProperty('notifyPropertyChange')
error?: string | MarkdownString | undefined;
_children: TestItemCollection;
get tests(): readonly TestItemImpl[] {
return this._children.values;
}
resolveChildren(): void {
// do nothing
}
}
export class TestRunImpl implements TestRun {
private testStates: Map<TestItem, TestState> = new Map();
private outputIndices: Map<TestItem, number[]> = new Map();
private outputs: TestOutputItem[] = [];
private onDidChangePropertyEmitter = new Emitter<{ name?: string; isRunning?: boolean; }>();
onDidChangeProperty: Event<{ name?: string; isRunning?: boolean; }> = this.onDidChangePropertyEmitter.event;
private cts: CancellationTokenSource;
constructor(readonly controller: TestControllerImpl, readonly id: string, name: string) {
this.name = name;
this.isRunning = false;
this.start();
}
private start(): void {
this.cts = new CancellationTokenSource();
Promise.allSettled(this.collectTestsForRun().map(item => this.simulateTestRun(item, this.cts.token))).then(() => this.ended());
}
collectTestsForRun(): TestItemImpl[] {
const result: TestItemImpl[] = [];
this.collectTests(this.controller.tests, result);
return result;
}
collectTests(tests: readonly TestItemImpl[], result: TestItemImpl[]): void {
tests.forEach(test => this.collectTest(test, result));
}
collectTest(test: TestItemImpl, result: TestItemImpl[]): void {
if (test.tests.length > 0) {
this.collectTests(test.tests, result);
} else if (Math.random() < 0.8) {
result.push(test);
}
}
simulateTestRun(item: TestItemImpl, token: CancellationToken): Promise<void> {
let outputCounter = 0;
let messageCounter = 0;
return timeout(Math.random() * 3000, token)
.then(() => this.setTestState(item, { state: TestExecutionState.Queued }))
.then(() => timeout(Math.random() * 3000, token))
.then(() => this.setTestState(item, { state: TestExecutionState.Running }))
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
})
.then(() => timeout(Math.random() * 3000, token))
.then(() => {
this.appendOutput(`Output from Test ${item.label} nr ${outputCounter++}`);
}).then(() => {
const random = Math.random();
if (random > 0.9) {
this.setTestState(item, { state: TestExecutionState.Skipped });
} else if (random > 0.8) {
const failure: TestFailure = {
state: TestExecutionState.Errored,
messages: [
{
message: {
value: `**Error** from Test ${item.label} nr ${messageCounter++}`
},
location: {
uri: item.uri.toString(),
range: item.range!
},
}
],
duration: 33
};
this.setTestState(item, failure);
} else if (random > 0.7) {
const failure: TestFailure = {
state: TestExecutionState.Failed,
messages: [
{
message: {
value: `**Failure** from Test ${item.label} nr ${messageCounter++}`
},
location: {
uri: item.uri.toString(),
range: item.range!
},
}
],
duration: 33
};
this.setTestState(item, failure);
} else {
this.setTestState(item, { state: TestExecutionState.Passed });
}
});
}
@observableProperty('notifyPropertyChange')
isRunning: boolean;
@observableProperty('notifyPropertyChange')
name: string;
protected notifyPropertyChange(property: 'name' | 'isRunning', value: unknown): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
this.onDidChangePropertyEmitter.fire(val);
}
cancel(): void {
this.cts.cancel();
}
getTestState(item: TestItem): TestState | undefined {
return this.testStates.get(item);
}
private onDidChangeTestStateEmitter: Emitter<TestStateChangedEvent[]> = new Emitter();
onDidChangeTestState: Event<TestStateChangedEvent[]> = this.onDidChangeTestStateEmitter.event;
getOutput(item?: TestItem | undefined): readonly TestOutputItem[] {
if (!item) {
return this.outputs;
} else {
const indices = this.outputIndices.get(item);
if (!indices) {
return [];
} else {
return indices.map(index => this.outputs[index]);
}
}
}
private onDidChangeTestOutputEmitter: Emitter<[TestItem | undefined, TestOutputItem][]> = new Emitter();
onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]> = this.onDidChangeTestOutputEmitter.event;
setTestState<T extends TestState>(test: TestItemImpl, newState: TestState): void {
const oldState = this.testStates.get(test);
this.testStates.set(test, newState);
this.onDidChangeTestStateEmitter.fire([{
oldState: oldState, newState: newState, test: test
}]);
}
appendOutput(text: string, location?: Location, item?: TestItem): void {
const output = {
output: text,
location: location
};
this.outputs.push(output);
if (item) {
let indices = this.outputIndices.get(item);
if (!indices) {
indices = [];
this.outputIndices.set(item, indices);
}
indices.push(this.outputs.length - 1);
}
this.onDidChangeTestOutputEmitter.fire([[item, output]]);
}
get items(): readonly TestItem[] {
return [...this.testStates.keys()];
}
ended(): void {
const stateEvents: TestStateChangedEvent[] = [];
this.testStates.forEach((state, item) => {
if (state.state <= TestExecutionState.Running) {
stateEvents.push({
oldState: state,
newState: undefined,
test: item
});
this.testStates.delete(item);
}
});
if (stateEvents.length > 0) {
this.onDidChangeTestStateEmitter.fire(stateEvents);
}
this.isRunning = false;
}
}
export class TestControllerImpl implements TestController {
private _profiles = new SimpleObservableCollection<TestRunProfile>();
private _runs = new SimpleObservableCollection<TestRun>();
readonly deltaBuilder = new AccumulatingTreeDeltaEmitter<string, TestItemImpl>(300);
items = new TestItemCollection(this, item => item.path, () => this.deltaBuilder);
constructor(readonly id: string, readonly label: string) {
}
refreshTests(token: CancellationToken): Promise<void> {
// not implemented
return Promise.resolve();
}
get testRunProfiles(): readonly TestRunProfile[] {
return this._profiles.values;
}
addProfile(profile: TestRunProfile): void {
this._profiles.add(profile);
}
onProfilesChanged: Event<CollectionDelta<TestRunProfile, TestRunProfile>> = this._profiles.onChanged;
get testRuns(): readonly TestRun[] {
return this._runs.values;
}
addRun(run: TestRun): void {
this._runs.add(run);
}
onRunsChanged: Event<CollectionDelta<TestRun, TestRun>> = this._runs.onChanged;
get tests(): readonly TestItemImpl[] {
return this.items.values;
}
onItemsChanged: Event<TreeDelta<string, TestItemImpl>[]> = this.deltaBuilder.onDidFlush;
resolveChildren(item: TestItem): void {
// nothing to do
}
clearRuns(): void {
this._runs.clear();
}
}

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2022 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
// *****************************************************************************
import * as chai from 'chai';
import { TestItemImpl } from './test-controller';
import { URI } from '@theia/core';
import { DeltaKind, TreeDeltaBuilderImpl } from '@theia/test/lib/common/tree-delta';
const expect = chai.expect;
describe('TestItem tests', () => {
it('should notify property changes', () => {
const deltaBuilder = new TreeDeltaBuilderImpl<string, TestItemImpl>();
const item = new TestItemImpl(new URI('https://foo/bar'), 'b');
item._deltaBuilder = deltaBuilder;
item._path = ['a', 'b'];
item.label = 'theLabel';
const range = { start: { line: 17, character: 5 }, end: { line: 17, character: 37 } };
item.range = range;
expect(deltaBuilder.currentDelta).deep.equal([{
path: ['a', 'b'],
type: DeltaKind.CHANGED,
value: {
label: 'theLabel',
range: range
},
}]);
});
});

View File

@@ -0,0 +1,46 @@
/********************************************************************************
* 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
********************************************************************************/
#theia-sample-toolbar-contribution {
position: relative;
}
#theia-sample-toolbar-contribution .icon-wrapper {
cursor: pointer;
margin-left: 0;
}
#theia-sample-toolbar-contribution:focus,
#theia-sample-toolbar-contribution .icon-wrapper:focus,
#theia-sample-toolbar-contribution .codicon-search:focus {
outline: none;
}
#theia-sample-toolbar-contribution
.icon-wrapper.action-label.item.enabled:hover {
background-color: var(--theia-toolbar-hoverBackground);
}
#theia-sample-toolbar-contribution #easy-search-item-icon.codicon-search {
position: relative;
}
#theia-sample-toolbar-contribution .icon-wrapper .codicon-triangle-down {
position: absolute;
font-size: 10px;
bottom: -7px;
right: -2px;
}

View File

@@ -0,0 +1,151 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { CommandContribution, CommandRegistry, CommandService, MenuContribution, MenuModelRegistry } from '@theia/core';
import { LabelProvider, quickCommand, QuickInputService, QuickPickItem } from '@theia/core/lib/browser';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
import { quickFileOpen } from '@theia/file-search/lib/browser/quick-file-open';
import { SearchInWorkspaceCommands } from '@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { AbstractToolbarContribution } from '@theia/toolbar/lib/browser/abstract-toolbar-contribution';
import { ToolbarMenus, ReactInteraction } from '@theia/toolbar/lib/browser/toolbar-constants';
import { ToolbarContribution } from '@theia/toolbar/lib/browser/toolbar-interfaces';
import { ToolbarDefaultsFactory } from '@theia/toolbar/lib/browser/toolbar-defaults';
import { SampleToolbarDefaultsOverride } from './sample-toolbar-defaults-override';
import '../../../src/browser/toolbar/sample-toolbar-contribution.css';
export const bindSampleToolbarContribution = (bind: interfaces.Bind, rebind: interfaces.Rebind) => {
bind(SampleToolbarContribution).toSelf().inSingletonScope();
bind(ToolbarContribution).to(SampleToolbarContribution);
bind(CommandContribution).to(SampleToolbarContribution);
bind(MenuContribution).to(SampleToolbarContribution);
bind(SearchInWorkspaceQuickInputService).toSelf().inSingletonScope();
rebind(ToolbarDefaultsFactory).toConstantValue(SampleToolbarDefaultsOverride);
};
export const FIND_IN_WORKSPACE_ROOT = {
id: 'easy.search.find.in.workspace.root',
category: 'API Samples',
label: 'Search Workspace Root for Text',
};
@injectable()
export class SearchInWorkspaceQuickInputService {
@inject(QuickInputService) protected readonly quickInputService: QuickInputService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
@inject(LabelProvider) protected readonly labelProvider: LabelProvider;
@inject(CommandService) protected readonly commandService: CommandService;
protected quickPickItems: QuickPickItem[] = [];
open(): void {
this.quickPickItems = this.createWorkspaceList();
this.quickInputService.showQuickPick(this.quickPickItems, {
placeholder: 'Workspace root to search',
});
}
protected createWorkspaceList(): QuickPickItem[] {
const roots = this.workspaceService.tryGetRoots();
return roots.map(root => {
const uri = root.resource;
return {
label: this.labelProvider.getName(uri),
execute: (): Promise<void> => this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [uri]),
};
});
}
}
@injectable()
export class SampleToolbarContribution extends AbstractToolbarContribution
implements CommandContribution,
MenuContribution {
@inject(SearchInWorkspaceQuickInputService) protected readonly searchPickService: SearchInWorkspaceQuickInputService;
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
static ID = 'theia-sample-toolbar-contribution';
id = SampleToolbarContribution.ID;
protected handleOnClick = (e: ReactInteraction<HTMLSpanElement>): void => this.doHandleOnClick(e);
protected doHandleOnClick(e: ReactInteraction<HTMLSpanElement>): void {
e.stopPropagation();
const toolbar = document.querySelector<HTMLDivElement>('#main-toolbar');
if (toolbar) {
const { bottom } = toolbar.getBoundingClientRect();
const { left } = e.currentTarget.getBoundingClientRect();
this.contextMenuRenderer.render({
includeAnchorArg: false,
menuPath: ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU,
anchor: { x: left, y: bottom },
context: e.currentTarget
});
}
}
render(): React.ReactNode {
return (
<div
role='button'
tabIndex={0}
className='icon-wrapper action-label item enabled codicon codicon-search'
id='easy-search-item-icon'
onClick={this.handleOnClick}
title='API Samples: Search for files, text, commands, and more...'
>
<div className='codicon codicon-triangle-down' />
</div>);
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(FIND_IN_WORKSPACE_ROOT, {
execute: async () => {
const wsRoots = await this.workspaceService.roots;
if (!wsRoots.length) {
await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id);
} else if (wsRoots.length === 1) {
const { resource } = wsRoots[0];
await this.commandService.executeCommand(SearchInWorkspaceCommands.FIND_IN_FOLDER.id, [resource]);
} else {
this.searchPickService.open();
}
},
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: quickCommand.id,
label: 'Find a Command',
order: 'a',
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: quickFileOpen.id,
order: 'b',
label: 'Search for a file'
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: SearchInWorkspaceCommands.OPEN_SIW_WIDGET.id,
label: 'Search Entire Workspace for Text',
order: 'c',
});
registry.registerMenuAction(ToolbarMenus.SEARCH_WIDGET_DROPDOWN_MENU, {
commandId: FIND_IN_WORKSPACE_ROOT.id,
order: 'd',
});
}
}

View File

@@ -0,0 +1,59 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { DeflatedToolbarTree, ToolbarAlignment } from '@theia/toolbar/lib/browser/toolbar-interfaces';
export const SampleToolbarDefaultsOverride: () => DeflatedToolbarTree = () => ({
items: {
[ToolbarAlignment.LEFT]: [
[
{
id: 'textEditor.commands.go.back',
command: 'textEditor.commands.go.back',
icon: 'codicon codicon-arrow-left',
},
{
id: 'textEditor.commands.go.forward',
command: 'textEditor.commands.go.forward',
icon: 'codicon codicon-arrow-right',
},
],
[
{
id: 'workbench.action.splitEditorRight',
command: 'workbench.action.splitEditor',
icon: 'codicon codicon-split-horizontal',
},
],
],
[ToolbarAlignment.CENTER]: [[
{
id: 'theia-sample-toolbar-contribution',
group: 'contributed'
}
]],
[ToolbarAlignment.RIGHT]: [
[
{
id: 'workbench.action.showCommands',
command: 'workbench.action.showCommands',
icon: 'codicon codicon-terminal',
tooltip: 'Command Palette',
},
]
]
},
});

View File

@@ -0,0 +1,120 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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
// *****************************************************************************
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { AbstractViewContribution, bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { Command, CommandRegistry, MessageService } from '@theia/core/lib/common';
import { ApplicationShell, codicon, DockLayout, ShellLayoutTransformer, Widget, WidgetFactory } from '@theia/core/lib/browser';
import { SampleViewUnclosableView } from './sample-unclosable-view';
export const SampleToolBarCommand: Command = {
id: 'sample.toggle.toolbarCommand',
iconClass: codicon('add'),
category: 'API Samples'
};
@injectable()
export class SampleUnclosableViewContribution extends AbstractViewContribution<SampleViewUnclosableView> implements TabBarToolbarContribution, ShellLayoutTransformer {
static readonly SAMPLE_UNCLOSABLE_VIEW_TOGGLE_COMMAND_ID = 'sampleUnclosableView:toggle';
protected toolbarItemState = false;
@inject(MessageService) protected readonly messageService: MessageService;
constructor() {
super({
widgetId: SampleViewUnclosableView.ID,
widgetName: 'Sample Unclosable View',
toggleCommandId: SampleUnclosableViewContribution.SAMPLE_UNCLOSABLE_VIEW_TOGGLE_COMMAND_ID,
defaultWidgetOptions: {
area: 'main'
}
});
}
override registerCommands(registry: CommandRegistry): void {
super.registerCommands(registry);
registry.registerCommand(SampleToolBarCommand, {
execute: () => {
this.toolbarItemState = !this.toolbarItemState;
this.messageService.info(`Sample Toolbar Command is toggled = ${this.toolbarItemState}`);
},
isEnabled: widget => this.withWidget(widget, () => true),
isVisible: widget => this.withWidget(widget, () => true),
isToggled: () => this.toolbarItemState
});
}
async registerToolbarItems(toolbarRegistry: TabBarToolbarRegistry): Promise<void> {
toolbarRegistry.registerItem({
id: SampleToolBarCommand.id,
command: SampleToolBarCommand.id,
tooltip: 'API Samples: Click to Toggle Toolbar Item',
priority: 0
});
}
protected withWidget<T>(widget: Widget | undefined = this.tryGetWidget(), cb: (sampleView: SampleViewUnclosableView) => T): T | false {
if (widget instanceof SampleViewUnclosableView && widget.id === SampleViewUnclosableView.ID) {
return cb(widget);
}
return false;
}
// Makes sure the 'Sample Unclosable View' view is never restored after app restarts.
transformLayoutOnRestore(layoutData: ApplicationShell.LayoutData): void {
this.pruneConfig(layoutData.mainPanel?.main);
}
protected pruneConfig(area: DockLayout.AreaConfig | null | undefined): void {
if (area?.type === 'tab-area') {
this.pruneTabConfig(area);
} else if (area?.type === 'split-area') {
this.pruneSplitConfig(area);
}
}
protected pruneTabConfig(area: DockLayout.AreaConfig): void {
if (area.type === 'tab-area') {
const newwidgets = area.widgets.filter(widget => {
if (widget.id.startsWith(SampleViewUnclosableView.ID)) {
return false;
}
return true;
});
area.widgets = newwidgets;
}
}
protected pruneSplitConfig(area: DockLayout.AreaConfig): void {
if (area.type === 'split-area') {
area.children.forEach(c => this.pruneConfig(c));
}
}
}
export const bindSampleUnclosableView = (bind: interfaces.Bind) => {
bindViewContribution(bind, SampleUnclosableViewContribution);
bind(TabBarToolbarContribution).to(SampleUnclosableViewContribution).inSingletonScope();
bind(SampleViewUnclosableView).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: SampleViewUnclosableView.ID,
createWidget: () => ctx.container.get<SampleViewUnclosableView>(SampleViewUnclosableView)
}));
bind(ShellLayoutTransformer).toService(SampleUnclosableViewContribution);
};

View File

@@ -0,0 +1,46 @@
// *****************************************************************************
// Copyright (C) 2020 TORO Limited 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
// *****************************************************************************
import { ReactWidget } from '@theia/core/lib/browser';
import { injectable, postConstruct } from '@theia/core/shared/inversify';
import * as React from '@theia/core/shared/react';
/**
* This sample view is used to demo the behavior of "Widget.title.closable".
*/
@injectable()
export class SampleViewUnclosableView extends ReactWidget {
static readonly ID = 'sampleUnclosableView';
@postConstruct()
init(): void {
this.id = SampleViewUnclosableView.ID;
this.title.caption = 'Sample Unclosable View';
this.title.label = 'Sample Unclosable View';
this.title.iconClass = 'unclosable-window-icon';
this.title.closable = false;
this.update();
}
protected render(): React.ReactNode {
return (
<div>
Closable
<input type="checkbox" defaultChecked={this.title.closable} onChange={e => this.title.closable = e.target.checked} />
</div>
);
}
}

View File

@@ -0,0 +1,31 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { Endpoint } from '@theia/core/lib/browser';
import { injectable, interfaces } from '@theia/core/shared/inversify';
import { SampleAppInfo } from '../../common/vsx/sample-app-info';
@injectable()
export class SampleFrontendAppInfo implements SampleAppInfo {
async getSelfOrigin(): Promise<string> {
return new Endpoint().origin;
}
}
export function bindSampleAppInfo(bind: interfaces.Bind): void {
bind(SampleAppInfo).to(SampleFrontendAppInfo).inSingletonScope();
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { VSXEnvironment } from '@theia/vsx-registry/lib/common/vsx-environment';
import { Command, CommandContribution, CommandRegistry, MessageService } from '@theia/core/lib/common';
@injectable()
export class VSXCommandContribution implements CommandContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(VSXEnvironment)
protected readonly environment: VSXEnvironment;
protected readonly command: Command = {
id: 'vsx.echo-api-version',
label: 'Show VS Code API Version',
category: 'API Samples'
};
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(this.command, {
execute: async () => {
const version = await this.environment.getVscodeApiVersion();
this.messageService.info(`Supported VS Code API Version: ${version}`);
}
});
}
}
export const bindVSXCommand = (bind: interfaces.Bind) => {
bind(CommandContribution).to(VSXCommandContribution).inSingletonScope();
};

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2025 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
// *****************************************************************************
import { PreferenceInspection } from '@theia/core';
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
export const sampleBackendPreferencesServicePath = '/services/sampleBackendPreferences';
export const SampleBackendPreferencesService = Symbol('SampleBackendPreferencesService');
export interface SampleBackendPreferencesService {
getPreference(key: string, overrideIdentifier?: string): Promise<JSONValue | undefined>;
inspectPreference(key: string, overrideIdentifier?: string): Promise<PreferenceInspection | undefined>;
setPreference(key: string, overrideIdentifier: string | undefined, value: JSONValue): Promise<void>
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2025 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
// *****************************************************************************
import { PreferenceSchema } from '@theia/core';
export const FileWatchingPreferencesSchema: PreferenceSchema = {
properties: {
'sample.file-watching.verbose': {
type: 'boolean',
default: false,
description: 'Enable verbose file watching logs.'
}
}
};

View File

@@ -0,0 +1,37 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory';
export enum UpdateStatus {
InProgress = 'in-progress',
Available = 'available',
NotAvailable = 'not-available'
}
export const SampleUpdaterPath = '/services/sample-updater';
export const SampleUpdater = Symbol('SampleUpdater');
export interface SampleUpdater extends RpcServer<SampleUpdaterClient> {
checkForUpdates(): Promise<{ status: UpdateStatus }>;
onRestartToUpdateRequested(): void;
disconnectClient(client: SampleUpdaterClient): void;
setUpdateAvailable(available: boolean): Promise<void>; // Mock
}
export const SampleUpdaterClient = Symbol('SampleUpdaterClient');
export interface SampleUpdaterClient {
notifyReadyToInstall(): void;
}

View File

@@ -0,0 +1,22 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
export const SampleAppInfo = Symbol('SampleAppInfo') as symbol & interfaces.Abstract<SampleAppInfo>;
export interface SampleAppInfo {
getSelfOrigin(): Promise<string>;
}

View File

@@ -0,0 +1,30 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { OVSXUrlResolver } from '@theia/vsx-registry/lib/common';
import { SampleAppInfo } from './sample-app-info';
export function rebindOVSXClientFactory(rebind: interfaces.Rebind): void {
// rebind the OVSX client factory so that we can replace patterns like "${self}" in the configs:
rebind(OVSXUrlResolver)
.toDynamicValue(ctx => {
const appInfo = ctx.container.get<SampleAppInfo>(SampleAppInfo);
const selfOrigin = appInfo.getSelfOrigin();
return async (url: string) => url.replace('${self}', await selfOrigin);
})
.inSingletonScope();
}

View File

@@ -0,0 +1,176 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { CommonMenus } from '@theia/core/lib/browser';
import {
Emitter,
Command,
MenuPath,
MessageService,
MenuModelRegistry,
MenuContribution,
CommandRegistry,
CommandContribution
} from '@theia/core/lib/common';
import { ElectronMainMenuFactory } from '@theia/core/lib/electron-browser/menu/electron-main-menu-factory';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { SampleUpdater, UpdateStatus, SampleUpdaterClient } from '../../common/updater/sample-updater';
export namespace SampleUpdaterCommands {
const category = 'API Samples';
export const CHECK_FOR_UPDATES: Command = {
id: 'electron-sample:check-for-updates',
label: 'Check for Updates...',
category
};
export const RESTART_TO_UPDATE: Command = {
id: 'electron-sample:restart-to-update',
label: 'Restart to Update',
category
};
// Mock
export const MOCK_UPDATE_AVAILABLE: Command = {
id: 'electron-sample:mock-update-available',
label: 'Mock Update - Available',
category
};
export const MOCK_UPDATE_NOT_AVAILABLE: Command = {
id: 'electron-sample:mock-update-not-available',
label: 'Mock Update - Not Available',
category
};
}
export namespace SampleUpdaterMenu {
export const MENU_PATH: MenuPath = [...CommonMenus.FILE_SETTINGS_SUBMENU, '3_settings_submenu_update'];
}
@injectable()
export class SampleUpdaterClientImpl implements SampleUpdaterClient {
protected readonly onReadyToInstallEmitter = new Emitter<void>();
readonly onReadyToInstall = this.onReadyToInstallEmitter.event;
notifyReadyToInstall(): void {
this.onReadyToInstallEmitter.fire();
}
}
// Dynamic menus aren't yet supported by electron: https://github.com/eclipse-theia/theia/issues/446
@injectable()
export class ElectronMenuUpdater {
@inject(ElectronMainMenuFactory)
protected readonly factory: ElectronMainMenuFactory;
public update(): void {
this.setMenu();
}
private setMenu(): void {
window.electronTheiaCore.setMenu(this.factory.createElectronMenuBar());
}
}
@injectable()
export class SampleUpdaterFrontendContribution implements CommandContribution, MenuContribution {
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(ElectronMenuUpdater)
protected readonly menuUpdater: ElectronMenuUpdater;
@inject(SampleUpdater)
protected readonly updater: SampleUpdater;
@inject(SampleUpdaterClientImpl)
protected readonly updaterClient: SampleUpdaterClientImpl;
protected readyToUpdate = false;
@postConstruct()
protected init(): void {
this.updaterClient.onReadyToInstall(async () => {
this.readyToUpdate = true;
this.menuUpdater.update();
this.handleUpdatesAvailable();
});
}
registerCommands(registry: CommandRegistry): void {
registry.registerCommand(SampleUpdaterCommands.CHECK_FOR_UPDATES, {
execute: async () => {
const { status } = await this.updater.checkForUpdates();
switch (status) {
case UpdateStatus.Available: {
this.handleUpdatesAvailable();
break;
}
case UpdateStatus.NotAvailable: {
const { applicationName } = FrontendApplicationConfigProvider.get();
this.messageService.info(`[Sample Updater - Not Available]: You're all good. You've got the latest version of ${applicationName}.`, { timeout: 3000 });
break;
}
case UpdateStatus.InProgress: {
this.messageService.warn('[Sample Updater - Downloading]: Work in progress...', { timeout: 3000 });
break;
}
default: throw new Error(`Unexpected status: ${status}`);
}
},
isEnabled: () => !this.readyToUpdate,
isVisible: () => !this.readyToUpdate
});
registry.registerCommand(SampleUpdaterCommands.RESTART_TO_UPDATE, {
execute: () => this.updater.onRestartToUpdateRequested(),
isEnabled: () => this.readyToUpdate,
isVisible: () => this.readyToUpdate
});
registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_AVAILABLE, {
execute: () => this.updater.setUpdateAvailable(true)
});
registry.registerCommand(SampleUpdaterCommands.MOCK_UPDATE_NOT_AVAILABLE, {
execute: () => this.updater.setUpdateAvailable(false)
});
}
registerMenus(registry: MenuModelRegistry): void {
registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, {
commandId: SampleUpdaterCommands.CHECK_FOR_UPDATES.id
});
registry.registerMenuAction(SampleUpdaterMenu.MENU_PATH, {
commandId: SampleUpdaterCommands.RESTART_TO_UPDATE.id
});
}
protected async handleUpdatesAvailable(): Promise<void> {
const answer = await this.messageService.info('[Sample Updater - Available]: Found updates, do you want update now?', 'No', 'Yes');
if (answer === 'Yes') {
this.updater.onRestartToUpdateRequested();
}
}
}

View File

@@ -0,0 +1,34 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ElectronIpcConnectionProvider } from '@theia/core/lib/electron-browser/messaging/electron-ipc-connection-source';
import { CommandContribution, MenuContribution } from '@theia/core/lib/common';
import { SampleUpdater, SampleUpdaterPath, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterFrontendContribution, ElectronMenuUpdater, SampleUpdaterClientImpl } from './sample-updater-frontend-contribution';
export default new ContainerModule(bind => {
bind(ElectronMenuUpdater).toSelf().inSingletonScope();
bind(SampleUpdaterClientImpl).toSelf().inSingletonScope();
bind(SampleUpdaterClient).toService(SampleUpdaterClientImpl);
bind(SampleUpdater).toDynamicValue(context => {
const client = context.container.get(SampleUpdaterClientImpl);
return ElectronIpcConnectionProvider.createProxy(context.container, SampleUpdaterPath, client);
}).inSingletonScope();
bind(SampleUpdaterFrontendContribution).toSelf().inSingletonScope();
bind(MenuContribution).toService(SampleUpdaterFrontendContribution);
bind(CommandContribution).toService(SampleUpdaterFrontendContribution);
});

View File

@@ -0,0 +1,91 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
import { SampleUpdater, SampleUpdaterClient, UpdateStatus } from '../../common/updater/sample-updater';
@injectable()
export class SampleUpdaterImpl implements SampleUpdater, ElectronMainApplicationContribution {
protected clients: Array<SampleUpdaterClient> = [];
protected inProgressTimer: NodeJS.Timeout | undefined;
protected available = false;
async checkForUpdates(): Promise<{ status: UpdateStatus }> {
if (this.inProgressTimer) {
return { status: UpdateStatus.InProgress };
}
return { status: this.available ? UpdateStatus.Available : UpdateStatus.NotAvailable };
}
onRestartToUpdateRequested(): void {
console.info("[api-samples] 'Update to Restart' was requested by the frontend.");
// Here comes your install and restart implementation. For example: `autoUpdater.quitAndInstall();`
}
async setUpdateAvailable(available: boolean): Promise<void> {
if (this.inProgressTimer) {
clearTimeout(this.inProgressTimer);
}
if (!available) {
this.inProgressTimer = undefined;
this.available = false;
} else {
this.inProgressTimer = setTimeout(() => {
this.inProgressTimer = undefined;
this.available = true;
for (const client of this.clients) {
client.notifyReadyToInstall();
}
}, 5000);
}
}
onStart(application: ElectronMainApplication): void {
// Called when the contribution is starting. You can use both async and sync code from here.
}
onStop(application: ElectronMainApplication): void {
// Invoked when the contribution is stopping. You can clean up things here. You are not allowed call async code from here.
}
setClient(client: SampleUpdaterClient | undefined): void {
if (client) {
this.clients.push(client);
console.info('[api-samples] Registered a new sample updater client.');
} else {
console.warn("[api-samples] Couldn't register undefined client.");
}
}
disconnectClient(client: SampleUpdaterClient): void {
const index = this.clients.indexOf(client);
if (index !== -1) {
this.clients.splice(index, 1);
console.info('[api-samples] Disposed a sample updater client.');
} else {
console.warn("[api-samples] Couldn't dispose client; it was not registered.");
}
}
dispose(): void {
console.info('[api-samples] >>> Disposing sample updater service...');
this.clients.forEach(this.disconnectClient.bind(this));
console.info('[api-samples] >>> Disposed sample updater service.');
}
}

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
import { ElectronConnectionHandler } from '@theia/core/lib/electron-main/messaging/electron-connection-handler';
import { SampleUpdaterPath, SampleUpdater, SampleUpdaterClient } from '../../common/updater/sample-updater';
import { SampleUpdaterImpl } from './sample-updater-impl';
export default new ContainerModule(bind => {
bind(SampleUpdaterImpl).toSelf().inSingletonScope();
bind(SampleUpdater).toService(SampleUpdaterImpl);
bind(ElectronMainApplicationContribution).toService(SampleUpdater);
bind(ElectronConnectionHandler).toDynamicValue(context =>
new RpcConnectionHandler<SampleUpdaterClient>(SampleUpdaterPath, client => {
const server = context.container.get<SampleUpdater>(SampleUpdater);
server.setClient(client);
client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
})
).inSingletonScope();
});

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { BackendApplicationContribution, BackendApplicationServer } from '@theia/core/lib/node';
import { SampleBackendApplicationServer } from './sample-backend-application-server';
import { SampleMockOpenVsxServer } from './sample-mock-open-vsx-server';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
import { SampleBackendAppInfo } from './sample-backend-app-info';
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { ConnectionHandler, PreferenceContribution, RpcConnectionHandler } from '@theia/core';
import { FileWatchingPreferencesSchema } from '../common/preference-schema';
import { MCPBackendContribution } from '@theia/ai-mcp-server/lib/node/mcp-theia-server';
import { MCPTestContribution } from './sample-mcp-test-contribution';
import { SampleBackendPreferencesService, sampleBackendPreferencesServicePath } from '../common/preference-protocol';
import { SampleBackendPreferencesBackendServiceImpl } from './sample-backend-preferences-service';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(SampleBackendPreferencesBackendServiceImpl).toSelf().inSingletonScope();
bind(MCPBackendContribution).to(MCPTestContribution).inSingletonScope();
bind(SampleBackendPreferencesService).toService(SampleBackendPreferencesBackendServiceImpl);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(sampleBackendPreferencesServicePath, () => ctx.container.get(SampleBackendPreferencesService))
).inSingletonScope();
bind(PreferenceContribution).toConstantValue({ schema: FileWatchingPreferencesSchema });
rebindOVSXClientFactory(rebind);
bind(SampleBackendAppInfo).toSelf().inSingletonScope();
bind(SampleAppInfo).toService(SampleBackendAppInfo);
bind(BackendApplicationContribution).toService(SampleBackendAppInfo);
// bind a mock/sample OpenVSX registry:
bind(BackendApplicationContribution).to(SampleMockOpenVsxServer).inSingletonScope();
if (process.env.SAMPLE_BACKEND_APPLICATION_SERVER) {
bind(BackendApplicationServer).to(SampleBackendApplicationServer).inSingletonScope();
}
});

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { environment } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BackendApplicationCliContribution, BackendApplicationContribution } from '@theia/core/lib/node';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as net from 'net';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
@injectable()
export class SampleBackendAppInfo implements SampleAppInfo, BackendApplicationContribution {
protected addressDeferred = new Deferred<net.AddressInfo>();
@inject(BackendApplicationCliContribution)
protected backendCli: BackendApplicationCliContribution;
onStart(server: net.Server): void {
const address = server.address();
// eslint-disable-next-line no-null/no-null
if (typeof address === 'object' && address !== null) {
this.addressDeferred.resolve(address);
} else {
this.addressDeferred.resolve({
address: '127.0.0.1',
port: 3000,
family: '4'
});
}
}
async getSelfOrigin(): Promise<string> {
const { ssl } = this.backendCli;
const protocol = ssl ? 'https' : 'http';
const { address, port } = await this.addressDeferred.promise;
const hostname = environment.electron.is() ? 'localhost' : address;
return `${protocol}://${hostname}:${port}`;
}
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { BackendApplicationServer } from '@theia/core/lib/node';
import express = require('@theia/core/shared/express');
@injectable()
export class SampleBackendApplicationServer implements BackendApplicationServer {
configure(app: express.Application): void {
app.get('*', (req, res) => {
res.status(200).send('SampleBackendApplicationServer OK');
});
}
}

View File

@@ -0,0 +1,56 @@
// *****************************************************************************
// Copyright (C) 2025 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
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
import { SampleBackendPreferencesService } from '../common/preference-protocol';
import { inject, injectable } from '@theia/core/shared/inversify';
import { PreferenceInspection, PreferenceLanguageOverrideService, PreferenceScope, PreferenceService } from '@theia/core';
@injectable()
export class SampleBackendPreferencesBackendServiceImpl implements SampleBackendPreferencesService {
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@inject(PreferenceLanguageOverrideService)
protected readonly languageOverrideService: PreferenceLanguageOverrideService;
async getPreference(key: string, overrideIdentifier: string): Promise<JSONValue | undefined> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
return this.preferenceService.get(preferenceName);
}
async inspectPreference(key: string, overrideIdentifier?: string): Promise<PreferenceInspection | undefined> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
return this.preferenceService.inspect(preferenceName);
}
async setPreference(key: string, overrideIdentifier: string | undefined, value: JSONValue): Promise<void> {
let preferenceName = key;
if (overrideIdentifier) {
preferenceName = this.languageOverrideService.overridePreferenceName({ preferenceName, overrideIdentifier });
}
this.preferenceService.set(preferenceName, value, PreferenceScope.User);
}
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { MCPBackendContribution } from '@theia/ai-mcp-server/lib/node/mcp-theia-server';
import { z } from 'zod';
@injectable()
export class MCPTestContribution implements MCPBackendContribution {
@inject(ILogger)
protected readonly logger: ILogger;
async configure(server: McpServer): Promise<void> {
this.logger.info('MCPTestContribution.configure() called - MCP system is working!');
server.registerTool('test-tool', {
description: 'Theia MCP server test-tool',
inputSchema: z.object({})
}, async () => {
this.logger.info('test-tool called');
return {
content: [{
type: 'text',
text: 'Test tool executed successfully!'
}]
};
});
this.logger.info('MCPTestContribution: test-tool registered successfully');
}
}

View File

@@ -0,0 +1,189 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { BackendApplicationContribution } from '@theia/core/lib/node';
import * as express from '@theia/core/shared/express';
import * as fs from 'fs';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { OVSXMockClient, VSXExtensionRaw } from '@theia/ovsx-client';
import * as path from 'path';
import { SampleAppInfo } from '../common/vsx/sample-app-info';
import * as http from 'http';
import * as https from 'https';
import { Deferred } from '@theia/core/lib/common/promise-util';
type VersionedId = `${string}.${string}@${string}`;
/**
* This class implements a very crude OpenVSX mock server for testing.
*
* See {@link configure}'s implementation for supported REST APIs.
*/
@injectable()
export class SampleMockOpenVsxServer implements BackendApplicationContribution {
@inject(SampleAppInfo)
protected appInfo: SampleAppInfo;
@inject(ILogger) @named('api-samples')
protected readonly logger: ILogger;
protected mockClient: OVSXMockClient;
protected staticFileHandlers: Map<string, express.RequestHandler<{
namespace: string;
name: string;
version: string;
}, express.Response>>;
private readyDeferred = new Deferred<void>();
private ready = this.readyDeferred.promise;
get mockServerPath(): string {
return '/mock-open-vsx';
}
get pluginsDbPath(): string {
return '../../sample-plugins';
}
async onStart?(server: http.Server | https.Server): Promise<void> {
const selfOrigin = await this.appInfo.getSelfOrigin();
const baseUrl = `${selfOrigin}${this.mockServerPath}`;
const pluginsDb = await this.findMockPlugins(this.pluginsDbPath, baseUrl);
this.staticFileHandlers = new Map(Array.from(pluginsDb.entries(), ([key, value]) => [key, express.static(value.path)]));
this.mockClient = new OVSXMockClient(Array.from(pluginsDb.values(), value => value.data));
this.readyDeferred.resolve();
}
async configure(app: express.Application): Promise<void> {
app.use(
this.mockServerPath + '/api',
express.Router()
.get('/v2/-/query', async (req, res) => {
await this.ready;
res.json(await this.mockClient.query(this.sanitizeQuery(req.query)));
})
.get('/-/search', async (req, res) => {
await this.ready;
res.json(await this.mockClient.search(this.sanitizeQuery(req.query)));
})
.get('/:namespace', async (req, res) => {
await this.ready;
const extensions = this.mockClient.extensions
.filter(ext => req.params.namespace === ext.namespace)
.map(ext => `${ext.namespaceUrl}/${ext.name}`);
if (extensions.length === 0) {
res.status(404).json({ error: `Namespace not found: ${req.params.namespace}` });
} else {
res.json({
name: req.params.namespace,
extensions
});
}
})
.get('/:namespace/:name', async (req, res) => {
await this.ready;
res.json(this.mockClient.extensions.find(ext => req.params.namespace === ext.namespace && req.params.name === ext.name));
})
.get('/:namespace/:name/reviews', async (req, res) => {
res.json([]);
})
// implicitly GET/HEAD because of the express.static handlers
.use('/:namespace/:name/:version/file', async (req, res, next) => {
await this.ready;
const versionedId = this.getVersionedId(req.params.namespace, req.params.name, req.params.version);
const staticFileHandler = this.staticFileHandlers.get(versionedId);
if (!staticFileHandler) {
return next();
}
staticFileHandler(req, res, next);
})
);
}
protected getVersionedId(namespace: string, name: string, version: string): VersionedId {
return `${namespace}.${name}@${version}`;
}
protected sanitizeQuery(query?: Record<string, unknown>): Record<string, string> {
return typeof query === 'object'
? Object.fromEntries(Object.entries(query).filter(([key, value]) => typeof value === 'string') as [string, string][])
: {};
}
/**
* This method expects the following folder hierarchy: `pluginsDbPath/namespace/pluginName/pluginFiles...`
* @param pluginsDbPath where to look for plugins on the disk.
* @param baseUrl used when generating the URLs for {@link VSXExtensionRaw} properties.
*/
protected async findMockPlugins(pluginsDbPath: string, baseUrl: string): Promise<Map<VersionedId, { path: string, data: VSXExtensionRaw }>> {
const url = new OVSXMockClient.UrlBuilder(baseUrl);
const result = new Map<VersionedId, { path: string, data: VSXExtensionRaw }>();
if (!await this.isDirectory(pluginsDbPath)) {
this.logger.error(`ERROR: ${pluginsDbPath} is not a directory!`);
return result;
}
const namespaces = await fs.promises.readdir(pluginsDbPath);
await Promise.all(namespaces.map(async namespace => {
const namespacePath = path.join(pluginsDbPath, namespace);
if (!await this.isDirectory(namespacePath)) {
return;
}
const names = await fs.promises.readdir(namespacePath);
await Promise.all(names.map(async pluginName => {
const pluginPath = path.join(namespacePath, pluginName);
if (!await this.isDirectory(pluginPath)) {
return;
}
const packageJsonPath = path.join(pluginPath, 'package.json');
const { name, version } = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf8'));
const versionedId = this.getVersionedId(namespace, name, version);
result.set(versionedId, {
path: pluginPath,
data: {
allVersions: {},
downloadCount: 0,
files: {
// the default generated name from vsce is NAME-VERSION.vsix
download: url.extensionFileUrl(namespace, name, version, `/${name}-${version}.vsix`),
icon: url.extensionFileUrl(namespace, name, version, '/icon128.png'),
readme: url.extensionFileUrl(namespace, name, version, '/README.md')
},
name,
namespace,
namespaceAccess: 'public',
namespaceUrl: url.namespaceUrl(namespace),
publishedBy: {
loginName: 'mock-open-vsx'
},
reviewCount: 0,
reviewsUrl: url.extensionReviewsUrl(namespace, name),
timestamp: new Date().toISOString(),
version,
namespaceDisplayName: name,
preRelease: false
}
});
}));
}));
return result;
}
protected async isDirectory(fsPath: string): Promise<boolean> {
return (await fs.promises.stat(fsPath)).isDirectory();
}
}

View File

@@ -0,0 +1,58 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../../dev-packages/ovsx-client"
},
{
"path": "../../packages/ai-chat"
},
{
"path": "../../packages/ai-chat-ui"
},
{
"path": "../../packages/ai-code-completion"
},
{
"path": "../../packages/ai-core"
},
{
"path": "../../packages/core"
},
{
"path": "../../packages/file-search"
},
{
"path": "../../packages/filesystem"
},
{
"path": "../../packages/monaco"
},
{
"path": "../../packages/output"
},
{
"path": "../../packages/search-in-workspace"
},
{
"path": "../../packages/test"
},
{
"path": "../../packages/toolbar"
},
{
"path": "../../packages/vsx-registry"
},
{
"path": "../../packages/workspace"
}
]
}

View File

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

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

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

View File

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

View File

@@ -0,0 +1,36 @@
// *****************************************************************************
// Copyright (C) 2021 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Contribution filter', function () {
this.timeout(5000);
const { assert } = chai;
const { CommandRegistry, CommandContribution } = require('@theia/core/lib/common/command');
const { SampleFilteredCommandContribution, SampleFilteredCommand } = require('@theia/api-samples/lib/browser/contribution-filter/sample-filtered-command-contribution');
const container = window.theia.container;
const commands = container.get(CommandRegistry);
it('filtered command in container but not in registry', async function () {
const allCommands = container.getAll(CommandContribution);
assert.isDefined(allCommands.find(contribution => contribution instanceof SampleFilteredCommandContribution),
'SampleFilteredCommandContribution is not bound in container');
const filteredCommand = commands.getCommand(SampleFilteredCommand.FILTERED.id);
assert.isUndefined(filteredCommand, 'SampleFilteredCommandContribution should be filtered out but is present in "CommandRegistry"');
});
});

View File

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

View File

@@ -0,0 +1,137 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Explorer and Editor - open and close', function () {
this.timeout(90_000);
const { assert } = chai;
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
const container = window.theia.container;
const editorManager = container.get(EditorManager);
const workspaceService = container.get(WorkspaceService);
const navigatorContribution = container.get(FileNavigatorContribution);
const shell = container.get(ApplicationShell);
const rootUri = workspaceService.tryGetRoots()[0].resource;
const pluginService = container.get(HostedPluginSupport);
const progressStatusBarItem = container.get(ProgressStatusBarItem);
const fileUri = rootUri.resolve('webpack.config.js');
const toTearDown = new DisposableCollection();
function pause(ms = 500) {
console.debug(`pause test for: ${ms} ms`);
return new Promise(resolve => setTimeout(resolve, ms));
}
before(async () => {
await pluginService.didStart;
await editorManager.closeAll({ save: false });
});
afterEach(async () => {
await editorManager.closeAll({ save: false });
await navigatorContribution.closeView();
});
after(async () => {
toTearDown.dispose();
});
for (var i = 0; i < 5; i++) {
let ordering = 0;
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
await openExplorer();
await openEditor();
await closeEditor();
await closeExplorer();
});
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
await openExplorer();
await openEditor();
await closeExplorer();
await closeEditor();
});
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
await openEditor();
await openExplorer();
await closeEditor();
await closeExplorer();
});
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
await openEditor();
await openExplorer();
await closeExplorer();
await closeEditor();
});
it('Open/Close explorer #' + i, async function () {
await openExplorer();
await closeExplorer();
});
}
it('open/close explorer in quick succession', async function () {
for (let i = 0; i < 20; i++) {
await openExplorer();
await closeExplorer();
}
});
it('open/close editor in quick succession', async function () {
await openExplorer();
for (let i = 0; i < 20; i++) {
await openEditor();
await closeEditor();
}
});
async function openExplorer() {
await navigatorContribution.openView({ activate: true });
const widget = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
assert.isDefined(widget, 'Explorer widget should exist');
}
async function closeExplorer() {
await navigatorContribution.closeView();
assert.isUndefined(await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID), 'Explorer widget should not exist');
}
async function openEditor() {
await editorManager.open(fileUri, { mode: 'activate' });
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isDefined(activeEditor);
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
}
async function closeEditor() {
await editorManager.closeAll({ save: false });
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
assert.isUndefined(activeEditor);
}
});

View File

@@ -0,0 +1,133 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('file-search', function () {
const { assert } = chai;
const Uri = require('@theia/core/lib/common/uri');
const { QuickFileOpenService } = require('@theia/file-search/lib/browser/quick-file-open');
const { QuickFileSelectService } = require('@theia/file-search/lib/browser/quick-file-select-service');
const { CancellationTokenSource } = require('@theia/core/lib/common/cancellation');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const quickFileOpenService = container.get(QuickFileOpenService);
const quickFileSelectService = container.get(QuickFileSelectService);
describe('quick-file-open', () => {
describe('#compareItems', () => {
const sortByCompareItems = (a, b) => quickFileSelectService['compareItems'](a, b, quickFileOpenService['filterAndRange'].filter);
it('should compare two quick-open-items by `label`', () => {
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: 'a', uri: new Uri.default('b') };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: 'b', uri: new Uri.default('a') };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(quickFileSelectService['compareItems'](a, a, quickFileOpenService['filterAndRange'].filter), 0, 'items should be equal');
});
it('should compare two quick-open-items by `uri`', () => {
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: 'a', uri: new Uri.default('a') };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: 'a', uri: new Uri.default('b') };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
});
it('should not place very good matches above exact matches', () => {
const exactMatch = 'almost_absurdly_long_file_name_with_many_parts.file';
const veryGoodMatch = 'almost_absurdly_long_file_name_with_many_parts_plus_one.file';
quickFileOpenService['filterAndRange'] = { filter: exactMatch };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const a = { label: exactMatch, uri: new Uri.default(exactMatch) };
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
const b = { label: veryGoodMatch, uri: new Uri.default(veryGoodMatch) };
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
quickFileOpenService['filterAndRange'] = quickFileOpenService['filterAndRangeDefault'];
});
});
describe('#filterAndRange', () => {
it('should return the default when not searching', () => {
const filterAndRange = quickFileOpenService['filterAndRange'];
assert.equal(filterAndRange, quickFileOpenService['filterAndRangeDefault']);
});
it('should update when searching', () => {
quickFileOpenService['getPicks']('a:2:1', new CancellationTokenSource().token); // perform a mock search.
const filterAndRange = quickFileOpenService['filterAndRange'];
assert.equal(filterAndRange.filter, 'a');
assert.deepEqual(filterAndRange.range, { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } });
});
});
describe('#splitFilterAndRange', () => {
const expression1 = 'a:2:1';
const expression2 = 'a:2,1';
const expression3 = 'a:2#2';
const expression4 = 'a#2:2';
const expression5 = 'a#2,1';
const expression6 = 'a#2#2';
const expression7 = 'a:2';
const expression8 = 'a#2';
it('should split the filter correctly for different combinations', () => {
assert.equal((quickFileOpenService['splitFilterAndRange'](expression1).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression2).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression3).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression4).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression5).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression6).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression7).filter), 'a');
assert.equal((quickFileOpenService['splitFilterAndRange'](expression8).filter), 'a');
});
it('should split the range correctly for different combinations', () => {
const rangeTest1 = { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } };
const rangeTest2 = { start: { line: 1, character: 1 }, end: { line: 1, character: 1 } };
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression1).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression2).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression3).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression4).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression5).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression6).range, rangeTest2);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression7).range, rangeTest1);
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression8).range, rangeTest1);
});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,222 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
const { timeout } = require('@theia/core/lib/common/promise-util');
// @ts-check
describe('SCM', function () {
const { assert } = chai;
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
const Uri = require('@theia/core/lib/common/uri');
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
const { ScmService } = require('@theia/scm/lib/browser/scm-service');
const { ScmWidget } = require('@theia/scm/lib/browser/scm-widget');
const { CommandRegistry } = require('@theia/core/lib/common');
const { PreferenceService } = require('@theia/core/lib/browser');
/** @type {import('inversify').Container} */
const container = window['theia'].container;
const contextKeyService = container.get(ContextKeyService);
const scmContribution = container.get(ScmContribution);
const shell = container.get(ApplicationShell);
const service = container.get(ScmService);
const commandRegistry = container.get(CommandRegistry);
const pluginService = container.get(HostedPluginSupport);
const preferences = container.get(PreferenceService);
/** @type {ScmWidget} */
let scmWidget;
/** @type {ScmService} */
let scmService;
const gitPluginId = 'vscode.git';
/**
* @param {() => unknown} condition
* @param {number | undefined} [timeout]
* @param {string | undefined} [message]
* @returns {Promise<void>}
*/
async function waitForAnimation(condition, maxWait, message) {
if (maxWait === undefined) {
maxWait = 100000;
}
const endTime = Date.now() + maxWait;
do {
await (timeout(100));
if (condition()) {
return true;
}
if (Date.now() > endTime) {
throw new Error(message ?? 'Wait for animation timed out.');
}
} while (true);
}
before(async () => {
preferences.set('git.autoRepositoryDetection', true);
preferences.set('git.openRepositoryInParentFolders', 'always');
});
beforeEach(async () => {
if (!pluginService.getPlugin(gitPluginId)) {
throw new Error(gitPluginId + ' should be started');
}
await pluginService.activatePlugin(gitPluginId);
await shell.leftPanelHandler.collapse();
scmWidget = await scmContribution.openView({ activate: true, reveal: true });
scmService = service;
await waitForAnimation(() => scmService.selectedRepository, 10000, 'selected repository is not defined');
});
afterEach(() => {
// @ts-ignore
scmWidget = undefined;
// @ts-ignore
scmService = undefined;
});
describe('scm-view', () => {
it('the view should open and activate successfully', () => {
assert.notEqual(scmWidget, undefined);
assert.strictEqual(scmWidget, shell.activeWidget);
});
describe('\'ScmTreeWidget\'', () => {
it('the view should display the resource tree when a repository is present', () => {
assert.isTrue(scmWidget.resourceWidget.isVisible);
});
it('the view should not display the resource tree when no repository is present', () => {
// Store the current selected repository so it can be restored.
const cachedSelectedRepository = scmService.selectedRepository;
scmService.selectedRepository = undefined;
assert.isFalse(scmWidget.resourceWidget.isVisible);
// Restore the selected repository.
scmService.selectedRepository = cachedSelectedRepository;
});
});
describe('\'ScmNoRepositoryWidget\'', () => {
it('should not be visible when a repository is present', () => {
assert.isFalse(scmWidget.noRepositoryWidget.isVisible);
});
it('should be visible when no repository is present', () => {
// Store the current selected repository so it can be restored.
const cachedSelectedRepository = scmService.selectedRepository;
scmService.selectedRepository = undefined;
assert.isTrue(scmWidget.noRepositoryWidget.isVisible);
// Restore the selected repository.
scmService.selectedRepository = cachedSelectedRepository;
});
});
});
describe('scm-service', () => {
it('should successfully return the list of repositories', () => {
const repositories = scmService.repositories;
assert.isTrue(repositories.length > 0);
});
it('should include the selected repository in the list of repositories', () => {
const repositories = scmService.repositories;
const selectedRepository = scmService.selectedRepository;
assert.isTrue(repositories.length === 1);
assert.strictEqual(repositories[0], selectedRepository);
});
it('should successfully return the selected repository', () => {
assert.notEqual(scmService.selectedRepository, undefined);
});
it('should successfully find the repository', () => {
const selectedRepository = scmService.selectedRepository;
if (selectedRepository) {
const rootUri = selectedRepository.provider.rootUri;
const foundRepository = scmService.findRepository(new Uri.default(rootUri));
assert.notEqual(foundRepository, undefined);
}
else {
assert.fail('Selected repository is undefined');
}
});
it('should not find a repository for an unknown uri', () => {
const mockUri = new Uri.default('foobar/foo/bar');
const repo = scmService.findRepository(mockUri);
assert.strictEqual(repo, undefined);
});
it('should successfully return the list of statusbar commands', () => {
assert.isTrue(scmService.statusBarCommands.length > 0);
});
});
describe('scm-provider', () => {
it('should successfully return the last commit', async () => {
const selectedRepository = scmService.selectedRepository;
if (selectedRepository) {
const amendSupport = selectedRepository.provider.amendSupport;
if (amendSupport) {
const commit = await amendSupport.getLastCommit();
assert.notEqual(commit, undefined);
}
}
else {
assert.fail('Selected repository is undefined');
}
});
});
describe('scm-contribution', () => {
describe('scmFocus context-key', () => {
it('should return \'true\' when the view is focused', () => {
assert.isTrue(contextKeyService.match('scmFocus'));
});
it('should return \'false\' when the view is not focused', async () => {
await scmContribution.closeView();
assert.isFalse(contextKeyService.match('scmFocus'));
});
});
});
});

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2017 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('Shell', function () {
const { assert } = chai;
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
const { StatusBarImpl } = require('@theia/core/lib/browser/status-bar');
const container = window.theia.container;
const shell = container.get(ApplicationShell);
const statusBar = container.get(StatusBarImpl);
it('should be shown', () => {
assert.isTrue(shell.isAttached && shell.isVisible);
});
it('should show the main content panel', () => {
assert.isTrue(shell.mainPanel.isAttached && shell.mainPanel.isVisible);
});
it('should show the status bar', () => {
assert.isTrue(statusBar.isAttached && statusBar.isVisible);
});
});

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2021 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
// @ts-check
describe('The Task Configuration Manager', function () {
this.timeout(5000);
const { assert } = chai;
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
const { TaskScope, TaskConfigurationScope } = require('@theia/task/lib/common/task-protocol');
const { TaskConfigurationManager } = require('@theia/task/lib/browser/task-configuration-manager');
const container = window.theia.container;
const workspaceService = container.get(WorkspaceService);
const taskConfigurationManager = container.get(TaskConfigurationManager);
const baseWorkspaceURI = workspaceService.tryGetRoots()[0].resource;
const baseWorkspaceRoot = baseWorkspaceURI.toString();
const basicTaskConfig = {
label: 'task',
type: 'shell',
command: 'top',
};
/** @type {Set<TaskConfigurationScope>} */
const scopesToClear = new Set();
describe('in a single-root workspace', () => {
beforeEach(() => clearTasks());
after(() => clearTasks());
setAndRetrieveTasks(() => TaskScope.Global, 'user');
setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder');
});
async function clearTasks() {
await Promise.all(Array.from(scopesToClear, async scope => {
if (!!scope || scope === 0) {
await taskConfigurationManager.setTaskConfigurations(scope, []);
}
}));
scopesToClear.clear();
}
/**
* @param {() => TaskConfigurationScope} scopeGenerator a function to allow lazy evaluation of the second workspace root.
* @param {string} scopeLabel
* @param {boolean} only
*/
function setAndRetrieveTasks(scopeGenerator, scopeLabel, only = false) {
const testFunction = only ? it.only : it;
testFunction(`successfully handles ${scopeLabel} scope`, async () => {
const scope = scopeGenerator();
scopesToClear.add(scope);
const initialTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(initialTasks, []);
await taskConfigurationManager.setTaskConfigurations(scope, [basicTaskConfig]);
const newTasks = taskConfigurationManager.getTasks(scope);
assert.deepEqual(newTasks, [basicTaskConfig]);
});
}
/* UNCOMMENT TO RUN MULTI-ROOT TESTS */
// const { FileService } = require('@theia/filesystem/lib/browser/file-service');
// const { EnvVariablesServer } = require('@theia/core/lib/common/env-variables');
// const URI = require('@theia/core/lib/common/uri').default;
// const fileService = container.get(FileService);
// /** @type {EnvVariablesServer} */
// const envVariables = container.get(EnvVariablesServer);
// describe('in a multi-root workspace', () => {
// let secondWorkspaceRoot = '';
// before(async () => {
// const configLocation = await envVariables.getConfigDirUri();
// const secondWorkspaceRootURI = new URI(configLocation).parent.resolve(`test-root-${Date.now()}`);
// secondWorkspaceRoot = secondWorkspaceRootURI.toString();
// await fileService.createFolder(secondWorkspaceRootURI);
// /** @type {Promise<void>} */
// const waitForEvent = new Promise(resolve => {
// const listener = taskConfigurationManager.onDidChangeTaskConfig(() => {
// listener.dispose();
// resolve();
// });
// });
// workspaceService.addRoot(secondWorkspaceRootURI);
// return waitForEvent;
// });
// beforeEach(() => clearTasks());
// after(() => clearTasks());
// setAndRetrieveTasks(() => TaskScope.Global, 'user');
// setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
// setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder (1)');
// setAndRetrieveTasks(() => secondWorkspaceRoot, 'folder (2)');
// });
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
{
"private": true,
"name": "@theia/example-browser-only",
"version": "1.68.0",
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"theia": {
"target": "browser-only",
"frontend": {
"config": {
"applicationName": "Theia Browser-Only Example",
"preferences": {
"files.enableTrash": false
}
}
}
},
"dependencies": {
"@theia/ai-chat": "1.68.0",
"@theia/ai-chat-ui": "1.68.0",
"@theia/ai-code-completion": "1.68.0",
"@theia/ai-core": "1.68.0",
"@theia/ai-core-ui": "1.68.0",
"@theia/ai-history": "1.68.0",
"@theia/ai-ollama": "1.68.0",
"@theia/ai-openai": "1.68.0",
"@theia/ai-scanoss": "1.68.0",
"@theia/api-samples": "1.68.0",
"@theia/bulk-edit": "1.68.0",
"@theia/callhierarchy": "1.68.0",
"@theia/collaboration": "1.68.0",
"@theia/console": "1.68.0",
"@theia/core": "1.68.0",
"@theia/debug": "1.68.0",
"@theia/editor": "1.68.0",
"@theia/editor-preview": "1.68.0",
"@theia/file-search": "1.68.0",
"@theia/filesystem": "1.68.0",
"@theia/getting-started": "1.68.0",
"@theia/git": "1.68.0",
"@theia/keymaps": "1.68.0",
"@theia/markers": "1.68.0",
"@theia/memory-inspector": "1.68.0",
"@theia/messages": "1.68.0",
"@theia/metrics": "1.68.0",
"@theia/mini-browser": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/navigator": "1.68.0",
"@theia/outline-view": "1.68.0",
"@theia/output": "1.68.0",
"@theia/plugin-dev": "1.68.0",
"@theia/plugin-ext": "1.68.0",
"@theia/plugin-ext-vscode": "1.68.0",
"@theia/plugin-metrics": "1.68.0",
"@theia/preferences": "1.68.0",
"@theia/preview": "1.68.0",
"@theia/process": "1.68.0",
"@theia/property-view": "1.68.0",
"@theia/scanoss": "1.68.0",
"@theia/scm": "1.68.0",
"@theia/scm-extra": "1.68.0",
"@theia/search-in-workspace": "1.68.0",
"@theia/secondary-window": "1.68.0",
"@theia/task": "1.68.0",
"@theia/terminal": "1.68.0",
"@theia/timeline": "1.68.0",
"@theia/toolbar": "1.68.0",
"@theia/typehierarchy": "1.68.0",
"@theia/userstorage": "1.68.0",
"@theia/variable-resolver": "1.68.0",
"@theia/vsx-registry": "1.68.0",
"@theia/workspace": "1.68.0"
},
"scripts": {
"prepare:no-native": "lerna run prepare --scope=\"@theia/re-exports\" && lerna run generate-theia-re-exports --scope=\"@theia/core\"",
"clean": "theiaext clean",
"build": "theiaext build && npm run -s bundle",
"bundle": "theia build --mode development",
"compile": "theiaext compile",
"start": "theia start",
"start:debug": "npm run -s start -- --log-level=debug",
"start:watch": "concurrently --kill-others -n tsc,bundle,run -c red,yellow,green \"tsc -b -w --preserveWatchOutput\" \"npm run -s watch:bundle\" \"npm run -s start\"",
"watch": "concurrently --kill-others -n tsc,bundle -c red,yellow \"tsc -b -w --preserveWatchOutput\" \"npm run -s watch:bundle\"",
"watch:bundle": "theia build --watch --mode development",
"watch:compile": "tsc -b -w"
},
"devDependencies": {
"@theia/cli": "1.68.0"
}
}

View File

@@ -0,0 +1,174 @@
{
"extends": "../../configs/base.tsconfig",
"include": [],
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "../../dev-packages/cli"
},
{
"path": "../../packages/ai-chat"
},
{
"path": "../../packages/ai-chat-ui"
},
{
"path": "../../packages/ai-code-completion"
},
{
"path": "../../packages/ai-core"
},
{
"path": "../../packages/ai-core-ui"
},
{
"path": "../../packages/ai-history"
},
{
"path": "../../packages/ai-ollama"
},
{
"path": "../../packages/ai-openai"
},
{
"path": "../../packages/ai-scanoss"
},
{
"path": "../../packages/bulk-edit"
},
{
"path": "../../packages/callhierarchy"
},
{
"path": "../../packages/collaboration"
},
{
"path": "../../packages/console"
},
{
"path": "../../packages/core"
},
{
"path": "../../packages/debug"
},
{
"path": "../../packages/editor"
},
{
"path": "../../packages/editor-preview"
},
{
"path": "../../packages/file-search"
},
{
"path": "../../packages/filesystem"
},
{
"path": "../../packages/getting-started"
},
{
"path": "../../packages/git"
},
{
"path": "../../packages/keymaps"
},
{
"path": "../../packages/markers"
},
{
"path": "../../packages/memory-inspector"
},
{
"path": "../../packages/messages"
},
{
"path": "../../packages/metrics"
},
{
"path": "../../packages/mini-browser"
},
{
"path": "../../packages/monaco"
},
{
"path": "../../packages/navigator"
},
{
"path": "../../packages/outline-view"
},
{
"path": "../../packages/output"
},
{
"path": "../../packages/plugin-dev"
},
{
"path": "../../packages/plugin-ext"
},
{
"path": "../../packages/plugin-ext-vscode"
},
{
"path": "../../packages/plugin-metrics"
},
{
"path": "../../packages/preferences"
},
{
"path": "../../packages/preview"
},
{
"path": "../../packages/process"
},
{
"path": "../../packages/property-view"
},
{
"path": "../../packages/scanoss"
},
{
"path": "../../packages/scm"
},
{
"path": "../../packages/scm-extra"
},
{
"path": "../../packages/search-in-workspace"
},
{
"path": "../../packages/secondary-window"
},
{
"path": "../../packages/task"
},
{
"path": "../../packages/terminal"
},
{
"path": "../../packages/timeline"
},
{
"path": "../../packages/toolbar"
},
{
"path": "../../packages/typehierarchy"
},
{
"path": "../../packages/userstorage"
},
{
"path": "../../packages/variable-resolver"
},
{
"path": "../../packages/vsx-registry"
},
{
"path": "../../packages/workspace"
},
{
"path": "../api-samples.disabled"
}
]
}

Some files were not shown because too many files have changed in this diff Show More