deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
examples/api-provider-sample/.eslintrc.js
Normal file
10
examples/api-provider-sample/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
50
examples/api-provider-sample/README.md
Normal file
50
examples/api-provider-sample/README.md
Normal 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>
|
||||
42
examples/api-provider-sample/package.json
Normal file
42
examples/api-provider-sample/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
70
examples/api-provider-sample/src/common/plugin-api-rpc.ts
Normal file
70
examples/api-provider-sample/src/common/plugin-api-rpc.ts
Normal 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'),
|
||||
};
|
||||
49
examples/api-provider-sample/src/gotd.d.ts
vendored
Normal file
49
examples/api-provider-sample/src/gotd.d.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
28
examples/api-provider-sample/src/node/gotd-backend-module.ts
Normal file
28
examples/api-provider-sample/src/node/gotd-backend-module.ts
Normal 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();
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
72
examples/api-provider-sample/src/node/greeting-main-impl.ts
Normal file
72
examples/api-provider-sample/src/node/greeting-main-impl.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
97
examples/api-provider-sample/src/plugin/gotd-api-init.ts
Normal file
97
examples/api-provider-sample/src/plugin/gotd-api-init.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
}
|
||||
86
examples/api-provider-sample/src/plugin/greeting-ext-impl.ts
Normal file
86
examples/api-provider-sample/src/plugin/greeting-ext-impl.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
examples/api-provider-sample/tsconfig.json
Normal file
22
examples/api-provider-sample/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
examples/api-samples.disabled/.eslintrc.js
Normal file
10
examples/api-samples.disabled/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
42
examples/api-samples.disabled/README.md
Normal file
42
examples/api-samples.disabled/README.md
Normal 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>
|
||||
69
examples/api-samples.disabled/package.json
Normal file
69
examples/api-samples.disabled/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
BIN
examples/api-samples.disabled/src/browser/icons/theia.png
Normal file
BIN
examples/api-samples.disabled/src/browser/icons/theia.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
}
|
||||
34
examples/api-samples.disabled/src/browser/style/branding.css
Normal file
34
examples/api-samples.disabled/src/browser/style/branding.css
Normal 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");
|
||||
}
|
||||
@@ -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 |
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}]);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]
|
||||
]
|
||||
},
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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.'
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
58
examples/api-samples.disabled/tsconfig.json
Normal file
58
examples/api-samples.disabled/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
examples/api-tests/package.json
Normal file
24
examples/api-tests/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@theia/api-tests",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia API tests",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0"
|
||||
},
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
21
examples/api-tests/src/api-tests.d.ts
vendored
Normal file
21
examples/api-tests/src/api-tests.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
interface Window {
|
||||
theia: {
|
||||
container: import('inversify').Container
|
||||
}
|
||||
}
|
||||
54
examples/api-tests/src/browser-utils.spec.js
Normal file
54
examples/api-tests/src/browser-utils.spec.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('animationFrame', function () {
|
||||
this.timeout(5_000);
|
||||
const { assert } = chai;
|
||||
const { animationFrame } = require('@theia/core/lib/browser/browser');
|
||||
|
||||
class FrameCounter {
|
||||
constructor() {
|
||||
this.count = 0;
|
||||
this.stop = false;
|
||||
this.run();
|
||||
}
|
||||
run() {
|
||||
requestAnimationFrame(this.nextFrame.bind(this));
|
||||
}
|
||||
nextFrame() {
|
||||
this.count++;
|
||||
if (!this.stop) {
|
||||
this.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('should resolve after one frame', async () => {
|
||||
const counter = new FrameCounter();
|
||||
await animationFrame();
|
||||
counter.stop = true;
|
||||
assert.equal(counter.count, 1);
|
||||
});
|
||||
|
||||
it('should resolve after the given number of frames', async () => {
|
||||
const counter = new FrameCounter();
|
||||
await animationFrame(10);
|
||||
counter.stop = true;
|
||||
assert.equal(counter.count, 10);
|
||||
});
|
||||
|
||||
});
|
||||
36
examples/api-tests/src/contribution-filter.spec.js
Normal file
36
examples/api-tests/src/contribution-filter.spec.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 STMicroelectronics and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Contribution filter', function () {
|
||||
this.timeout(5000);
|
||||
const { assert } = chai;
|
||||
|
||||
const { CommandRegistry, CommandContribution } = require('@theia/core/lib/common/command');
|
||||
const { SampleFilteredCommandContribution, SampleFilteredCommand } = require('@theia/api-samples/lib/browser/contribution-filter/sample-filtered-command-contribution');
|
||||
|
||||
const container = window.theia.container;
|
||||
const commands = container.get(CommandRegistry);
|
||||
|
||||
it('filtered command in container but not in registry', async function () {
|
||||
const allCommands = container.getAll(CommandContribution);
|
||||
assert.isDefined(allCommands.find(contribution => contribution instanceof SampleFilteredCommandContribution),
|
||||
'SampleFilteredCommandContribution is not bound in container');
|
||||
const filteredCommand = commands.getCommand(SampleFilteredCommand.FILTERED.id);
|
||||
assert.isUndefined(filteredCommand, 'SampleFilteredCommandContribution should be filtered out but is present in "CommandRegistry"');
|
||||
});
|
||||
|
||||
});
|
||||
76
examples/api-tests/src/credentials-service.spec.js
Normal file
76
examples/api-tests/src/credentials-service.spec.js
Normal file
@@ -0,0 +1,76 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('CredentialsService', function () {
|
||||
this.timeout(5000);
|
||||
const { assert } = chai;
|
||||
|
||||
const { CredentialsService } = require('@theia/core/lib/browser/credentials-service');
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
/** @type {import('@theia/core/lib/browser/credentials-service').CredentialsService} */
|
||||
const credentials = container.get(CredentialsService);
|
||||
|
||||
const serviceName = 'theia-test';
|
||||
const accountName = 'test-account';
|
||||
const password = 'test-password';
|
||||
|
||||
this.beforeEach(async () => {
|
||||
await credentials.deletePassword(serviceName, accountName);
|
||||
});
|
||||
|
||||
it('can set and retrieve stored credentials', async function () {
|
||||
await credentials.setPassword(serviceName, accountName, password);
|
||||
const storedPassword = await credentials.getPassword(serviceName, accountName);
|
||||
assert.strictEqual(storedPassword, password);
|
||||
});
|
||||
|
||||
it('can retrieve all account keys for a service', async function () {
|
||||
// Initially, there should be no keys for the service
|
||||
let keys = await credentials.keys(serviceName);
|
||||
assert.strictEqual(keys.length, 0);
|
||||
|
||||
// Add a single credential
|
||||
await credentials.setPassword(serviceName, accountName, password);
|
||||
keys = await credentials.keys(serviceName);
|
||||
assert.strictEqual(keys.length, 1);
|
||||
assert.include(keys, accountName);
|
||||
|
||||
// Add more credentials with different account names
|
||||
const accountName2 = 'test-account-2';
|
||||
const accountName3 = 'test-account-3';
|
||||
await credentials.setPassword(serviceName, accountName2, 'password2');
|
||||
await credentials.setPassword(serviceName, accountName3, 'password3');
|
||||
|
||||
keys = await credentials.keys(serviceName);
|
||||
assert.strictEqual(keys.length, 3);
|
||||
assert.include(keys, accountName);
|
||||
assert.include(keys, accountName2);
|
||||
assert.include(keys, accountName3);
|
||||
|
||||
// Clean up all accounts
|
||||
await credentials.deletePassword(serviceName, accountName);
|
||||
await credentials.deletePassword(serviceName, accountName2);
|
||||
await credentials.deletePassword(serviceName, accountName3);
|
||||
|
||||
// Verify keys are removed after deletion
|
||||
keys = await credentials.keys(serviceName);
|
||||
assert.strictEqual(keys.length, 0);
|
||||
});
|
||||
|
||||
});
|
||||
137
examples/api-tests/src/explorer-open-close.spec.js
Normal file
137
examples/api-tests/src/explorer-open-close.spec.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Explorer and Editor - open and close', function () {
|
||||
this.timeout(90_000);
|
||||
const { assert } = chai;
|
||||
|
||||
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
|
||||
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const container = window.theia.container;
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const navigatorContribution = container.get(FileNavigatorContribution);
|
||||
const shell = container.get(ApplicationShell);
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
const progressStatusBarItem = container.get(ProgressStatusBarItem);
|
||||
|
||||
|
||||
const fileUri = rootUri.resolve('webpack.config.js');
|
||||
const toTearDown = new DisposableCollection();
|
||||
|
||||
function pause(ms = 500) {
|
||||
console.debug(`pause test for: ${ms} ms`);
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await pluginService.didStart;
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await editorManager.closeAll({ save: false });
|
||||
await navigatorContribution.closeView();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
toTearDown.dispose();
|
||||
});
|
||||
|
||||
for (var i = 0; i < 5; i++) {
|
||||
let ordering = 0;
|
||||
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
|
||||
await openExplorer();
|
||||
await openEditor();
|
||||
await closeEditor();
|
||||
await closeExplorer();
|
||||
});
|
||||
|
||||
it('Open/Close explorer and editor - ordering: ' + ordering++ + ', iteration #' + i, async function () {
|
||||
await openExplorer();
|
||||
await openEditor();
|
||||
await closeExplorer();
|
||||
await closeEditor();
|
||||
});
|
||||
|
||||
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
|
||||
await openEditor();
|
||||
await openExplorer();
|
||||
await closeEditor();
|
||||
await closeExplorer();
|
||||
});
|
||||
|
||||
it('Open/Close editor, explorer - ordering: ' + ordering++ + ', iteration - #' + i, async function () {
|
||||
await openEditor();
|
||||
await openExplorer();
|
||||
await closeExplorer();
|
||||
await closeEditor();
|
||||
});
|
||||
|
||||
it('Open/Close explorer #' + i, async function () {
|
||||
await openExplorer();
|
||||
await closeExplorer();
|
||||
});
|
||||
}
|
||||
|
||||
it('open/close explorer in quick succession', async function () {
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await openExplorer();
|
||||
await closeExplorer();
|
||||
}
|
||||
});
|
||||
|
||||
it('open/close editor in quick succession', async function () {
|
||||
await openExplorer();
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await openEditor();
|
||||
await closeEditor();
|
||||
}
|
||||
});
|
||||
|
||||
async function openExplorer() {
|
||||
await navigatorContribution.openView({ activate: true });
|
||||
const widget = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
|
||||
assert.isDefined(widget, 'Explorer widget should exist');
|
||||
}
|
||||
async function closeExplorer() {
|
||||
await navigatorContribution.closeView();
|
||||
assert.isUndefined(await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID), 'Explorer widget should not exist');
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
await editorManager.open(fileUri, { mode: 'activate' });
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isDefined(activeEditor);
|
||||
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
|
||||
}
|
||||
|
||||
async function closeEditor() {
|
||||
await editorManager.closeAll({ save: false });
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isUndefined(activeEditor);
|
||||
}
|
||||
|
||||
});
|
||||
133
examples/api-tests/src/file-search.spec.js
Normal file
133
examples/api-tests/src/file-search.spec.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('file-search', function () {
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const Uri = require('@theia/core/lib/common/uri');
|
||||
const { QuickFileOpenService } = require('@theia/file-search/lib/browser/quick-file-open');
|
||||
const { QuickFileSelectService } = require('@theia/file-search/lib/browser/quick-file-select-service');
|
||||
const { CancellationTokenSource } = require('@theia/core/lib/common/cancellation');
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
const quickFileOpenService = container.get(QuickFileOpenService);
|
||||
const quickFileSelectService = container.get(QuickFileSelectService);
|
||||
|
||||
describe('quick-file-open', () => {
|
||||
|
||||
describe('#compareItems', () => {
|
||||
|
||||
const sortByCompareItems = (a, b) => quickFileSelectService['compareItems'](a, b, quickFileOpenService['filterAndRange'].filter);
|
||||
|
||||
it('should compare two quick-open-items by `label`', () => {
|
||||
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const a = { label: 'a', uri: new Uri.default('b') };
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const b = { label: 'b', uri: new Uri.default('a') };
|
||||
|
||||
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.equal(quickFileSelectService['compareItems'](a, a, quickFileOpenService['filterAndRange'].filter), 0, 'items should be equal');
|
||||
});
|
||||
|
||||
it('should compare two quick-open-items by `uri`', () => {
|
||||
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const a = { label: 'a', uri: new Uri.default('a') };
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const b = { label: 'a', uri: new Uri.default('b') };
|
||||
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
|
||||
});
|
||||
|
||||
it('should not place very good matches above exact matches', () => {
|
||||
const exactMatch = 'almost_absurdly_long_file_name_with_many_parts.file';
|
||||
const veryGoodMatch = 'almost_absurdly_long_file_name_with_many_parts_plus_one.file';
|
||||
quickFileOpenService['filterAndRange'] = { filter: exactMatch };
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const a = { label: exactMatch, uri: new Uri.default(exactMatch) };
|
||||
/** @type import ('@theia/file-search/lib/browser/quick-file-open').FileQuickPickItem*/
|
||||
const b = { label: veryGoodMatch, uri: new Uri.default(veryGoodMatch) };
|
||||
assert.deepEqual([a, b].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.deepEqual([b, a].sort(sortByCompareItems), [a, b], 'a should be before b');
|
||||
assert.equal(sortByCompareItems(a, a), 0, 'items should be equal');
|
||||
quickFileOpenService['filterAndRange'] = quickFileOpenService['filterAndRangeDefault'];
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#filterAndRange', () => {
|
||||
|
||||
it('should return the default when not searching', () => {
|
||||
const filterAndRange = quickFileOpenService['filterAndRange'];
|
||||
assert.equal(filterAndRange, quickFileOpenService['filterAndRangeDefault']);
|
||||
});
|
||||
|
||||
it('should update when searching', () => {
|
||||
quickFileOpenService['getPicks']('a:2:1', new CancellationTokenSource().token); // perform a mock search.
|
||||
const filterAndRange = quickFileOpenService['filterAndRange'];
|
||||
assert.equal(filterAndRange.filter, 'a');
|
||||
assert.deepEqual(filterAndRange.range, { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#splitFilterAndRange', () => {
|
||||
|
||||
const expression1 = 'a:2:1';
|
||||
const expression2 = 'a:2,1';
|
||||
const expression3 = 'a:2#2';
|
||||
const expression4 = 'a#2:2';
|
||||
const expression5 = 'a#2,1';
|
||||
const expression6 = 'a#2#2';
|
||||
const expression7 = 'a:2';
|
||||
const expression8 = 'a#2';
|
||||
|
||||
it('should split the filter correctly for different combinations', () => {
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression1).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression2).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression3).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression4).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression5).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression6).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression7).filter), 'a');
|
||||
assert.equal((quickFileOpenService['splitFilterAndRange'](expression8).filter), 'a');
|
||||
});
|
||||
|
||||
it('should split the range correctly for different combinations', () => {
|
||||
const rangeTest1 = { start: { line: 1, character: 0 }, end: { line: 1, character: 0 } };
|
||||
const rangeTest2 = { start: { line: 1, character: 1 }, end: { line: 1, character: 1 } };
|
||||
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression1).range, rangeTest1);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression2).range, rangeTest1);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression3).range, rangeTest2);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression4).range, rangeTest2);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression5).range, rangeTest1);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression6).range, rangeTest2);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression7).range, rangeTest1);
|
||||
assert.deepEqual(quickFileOpenService['splitFilterAndRange'](expression8).range, rangeTest1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
151
examples/api-tests/src/find-replace.spec.js
Normal file
151
examples/api-tests/src/find-replace.spec.js
Normal file
@@ -0,0 +1,151 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Find and Replace', function () {
|
||||
this.timeout(20_000);
|
||||
const { assert } = chai;
|
||||
|
||||
const { animationFrame } = require('@theia/core/lib/browser/browser');
|
||||
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution');
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
|
||||
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
|
||||
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const container = window.theia.container;
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const commands = container.get(CommandRegistry);
|
||||
const keybindings = container.get(KeybindingRegistry);
|
||||
const contextKeyService = container.get(ContextKeyService);
|
||||
const navigatorContribution = container.get(FileNavigatorContribution);
|
||||
const shell = container.get(ApplicationShell);
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
const progressStatusBarItem = container.get(ProgressStatusBarItem);
|
||||
const fileUri = rootUri.resolve('../api-tests/test-ts-workspace/demo-file.ts');
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
|
||||
function pause(ms = 500) {
|
||||
console.debug(`pause test for: ${ms} ms`);
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {() => Promise<T> | T} condition
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
function waitForAnimation(condition) {
|
||||
return new Promise(async (resolve, dispose) => {
|
||||
toTearDown.push({ dispose });
|
||||
do {
|
||||
await animationFrame();
|
||||
} while (!condition());
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await pluginService.didStart;
|
||||
await shell.leftPanelHandler.collapse();
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await navigatorContribution.closeView();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await shell.leftPanelHandler.collapse();
|
||||
toTearDown.dispose();
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@theia/core/lib/common/command').Command} command
|
||||
*/
|
||||
async function assertEditorFindReplace(command) {
|
||||
assert.isFalse(contextKeyService.match('findWidgetVisible'));
|
||||
assert.isFalse(contextKeyService.match('findInputFocussed'));
|
||||
assert.isFalse(contextKeyService.match('replaceInputFocussed'));
|
||||
|
||||
keybindings.dispatchCommand(command.id);
|
||||
await waitForAnimation(() => contextKeyService.match('findInputFocussed'));
|
||||
|
||||
assert.isTrue(contextKeyService.match('findWidgetVisible'));
|
||||
assert.isTrue(contextKeyService.match('findInputFocussed'));
|
||||
assert.isFalse(contextKeyService.match('replaceInputFocussed'));
|
||||
|
||||
keybindings.dispatchKeyDown('Tab');
|
||||
await waitForAnimation(() => !contextKeyService.match('findInputFocussed'));
|
||||
assert.isTrue(contextKeyService.match('findWidgetVisible'));
|
||||
assert.isFalse(contextKeyService.match('findInputFocussed'));
|
||||
assert.equal(contextKeyService.match('replaceInputFocussed'), command === CommonCommands.REPLACE);
|
||||
}
|
||||
|
||||
for (const command of [CommonCommands.FIND, CommonCommands.REPLACE]) {
|
||||
it(command.label + ' in the active editor', async function () {
|
||||
await openExplorer();
|
||||
|
||||
await openEditor();
|
||||
|
||||
await assertEditorFindReplace(command);
|
||||
});
|
||||
|
||||
it(command.label + ' in the active explorer without the current editor', async function () {
|
||||
await openExplorer();
|
||||
|
||||
// should not throw
|
||||
await commands.executeCommand(command.id);
|
||||
});
|
||||
|
||||
it(command.label + ' in the active explorer with the current editor', async function () {
|
||||
await openEditor();
|
||||
|
||||
await openExplorer();
|
||||
|
||||
await assertEditorFindReplace(command);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
async function openExplorer() {
|
||||
await navigatorContribution.openView({ activate: true });
|
||||
const widget = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
|
||||
assert.isDefined(widget, 'Explorer widget should exist');
|
||||
}
|
||||
|
||||
async function openEditor() {
|
||||
await editorManager.open(fileUri, { mode: 'activate' });
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isDefined(activeEditor);
|
||||
// @ts-ignore
|
||||
assert.equal(activeEditor.uri.resolveToAbsolute().toString(), fileUri.resolveToAbsolute().toString());
|
||||
}
|
||||
});
|
||||
116
examples/api-tests/src/keybindings.spec.js
Normal file
116
examples/api-tests/src/keybindings.spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Keybindings', function () {
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { isOSX } = require('@theia/core/lib/common/os');
|
||||
const { CommonCommands } = require('@theia/core/lib/browser/common-commands');
|
||||
const { TerminalService } = require('@theia/terminal/lib/browser/base/terminal-service');
|
||||
const { TerminalCommands } = require('@theia/terminal/lib/browser/terminal-frontend-contribution');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { Deferred } = require('@theia/core/lib/common/promise-util');
|
||||
const { Key } = require('@theia/core/lib/browser/keys');
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
/** @type {import('@theia/terminal/lib/browser/base/terminal-service').TerminalService} */
|
||||
const terminalService = container.get(TerminalService);
|
||||
const applicationShell = container.get(ApplicationShell);
|
||||
const keybindings = container.get(KeybindingRegistry);
|
||||
const commands = container.get(CommandRegistry);
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
afterEach(() => toTearDown.dispose());
|
||||
|
||||
it('partial keybinding should not override full in the same scope', async () => {
|
||||
const terminal = /** @type {import('@theia/terminal/lib/browser/terminal-widget-impl').TerminalWidgetImpl} */
|
||||
(await terminalService.newTerminal({}));
|
||||
toTearDown.push(Disposable.create(() => terminal.dispose()));
|
||||
terminalService.open(terminal, { mode: 'activate' });
|
||||
await applicationShell.waitForActivation(terminal.id);
|
||||
const waitForCommand = new Deferred();
|
||||
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
|
||||
keybindings.dispatchKeyDown({
|
||||
code: Key.KEY_K.code,
|
||||
metaKey: isOSX,
|
||||
ctrlKey: !isOSX
|
||||
}, terminal.node);
|
||||
const executedCommand = await waitForCommand.promise;
|
||||
assert.equal(executedCommand, TerminalCommands.TERMINAL_CLEAR.id);
|
||||
});
|
||||
|
||||
it('disabled keybinding should not override enabled', async () => {
|
||||
const id = '__test:keybindings.left';
|
||||
toTearDown.push(commands.registerCommand({ id }, {
|
||||
execute: () => { }
|
||||
}));
|
||||
toTearDown.push(keybindings.registerKeybinding({
|
||||
command: id,
|
||||
keybinding: 'left',
|
||||
when: 'false'
|
||||
}));
|
||||
|
||||
const editor = await editorManager.open(workspaceService.tryGetRoots()[0].resource.resolve('webpack.config.js'), {
|
||||
mode: 'activate',
|
||||
selection: {
|
||||
start: {
|
||||
line: 0,
|
||||
character: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
toTearDown.push(editor);
|
||||
const waitForCommand = new Deferred();
|
||||
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
|
||||
keybindings.dispatchKeyDown({
|
||||
code: Key.ARROW_LEFT.code
|
||||
}, editor.node);
|
||||
const executedCommand = await waitForCommand.promise;
|
||||
assert.notEqual(executedCommand, id);
|
||||
});
|
||||
|
||||
it('later registered keybinding should have higher priority', async () => {
|
||||
const id = '__test:keybindings.copy';
|
||||
toTearDown.push(commands.registerCommand({ id }, {
|
||||
execute: () => { }
|
||||
}));
|
||||
const keybinding = keybindings.getKeybindingsForCommand(CommonCommands.COPY.id)[0];
|
||||
toTearDown.push(keybindings.registerKeybinding({
|
||||
command: id,
|
||||
keybinding: keybinding.keybinding
|
||||
}));
|
||||
const waitForCommand = new Deferred();
|
||||
toTearDown.push(commands.onWillExecuteCommand(e => waitForCommand.resolve(e.commandId)));
|
||||
keybindings.dispatchKeyDown({
|
||||
code: Key.KEY_C.code,
|
||||
metaKey: isOSX,
|
||||
ctrlKey: !isOSX
|
||||
});
|
||||
const executedCommand = await waitForCommand.promise;
|
||||
assert.equal(executedCommand, id);
|
||||
});
|
||||
|
||||
});
|
||||
731
examples/api-tests/src/launch-preferences.spec.js
Normal file
731
examples/api-tests/src/launch-preferences.spec.js
Normal file
@@ -0,0 +1,731 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
/* @typescript-eslint/no-explicit-any */
|
||||
|
||||
/**
|
||||
* @typedef {'.vscode' | '.theia' | ['.theia', '.vscode']} ConfigMode
|
||||
*/
|
||||
|
||||
/**
|
||||
* Expectations should be tested and aligned against VS Code.
|
||||
* See https://github.com/akosyakov/vscode-launch/blob/master/src/test/extension.test.ts
|
||||
*/
|
||||
describe('Launch Preferences', function () {
|
||||
this.timeout(30_000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { PreferenceProvider } = require('@theia/core/lib/common');
|
||||
const { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
|
||||
const { PreferenceScope } = require('@theia/core/lib/common/preferences/preference-scope');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
||||
const { FileResourceResolver } = require('@theia/filesystem/lib/browser/file-resource');
|
||||
const { AbstractResourcePreferenceProvider } = require('@theia/preferences/lib/common/abstract-resource-preference-provider');
|
||||
const { waitForEvent } = require('@theia/core/lib/common/promise-util');
|
||||
|
||||
const container = window.theia.container;
|
||||
/** @type {import('@theia/core/lib/browser/preferences/preference-service').PreferenceService} */
|
||||
const preferences = container.get(PreferenceService);
|
||||
/** @type {import('@theia/preferences/lib/browser/user-configs-preference-provider').UserConfigsPreferenceProvider} */
|
||||
const userPreferences = container.getNamed(PreferenceProvider, PreferenceScope.User);
|
||||
/** @type {import('@theia/preferences/lib/browser/workspace-preference-provider').WorkspacePreferenceProvider} */
|
||||
const workspacePreferences = container.getNamed(PreferenceProvider, PreferenceScope.Workspace);
|
||||
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
|
||||
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const fileService = container.get(FileService);
|
||||
const fileResourceResolver = container.get(FileResourceResolver);
|
||||
|
||||
const defaultLaunch = {
|
||||
'configurations': [],
|
||||
'compounds': []
|
||||
};
|
||||
|
||||
const validConfiguration = {
|
||||
'name': 'Launch Program',
|
||||
'program': '${file}',
|
||||
'request': 'launch',
|
||||
'type': 'node',
|
||||
};
|
||||
|
||||
const validConfiguration2 = {
|
||||
'name': 'Launch Program 2',
|
||||
'program': '${file}',
|
||||
'request': 'launch',
|
||||
'type': 'node',
|
||||
};
|
||||
|
||||
const bogusConfiguration = {};
|
||||
|
||||
const validCompound = {
|
||||
'name': 'Compound',
|
||||
'configurations': [
|
||||
'Launch Program',
|
||||
'Launch Program 2'
|
||||
]
|
||||
};
|
||||
|
||||
const bogusCompound = {};
|
||||
|
||||
const bogusCompound2 = {
|
||||
'name': 'Compound 2',
|
||||
'configurations': [
|
||||
'Foo',
|
||||
'Launch Program 2'
|
||||
]
|
||||
};
|
||||
|
||||
const validLaunch = {
|
||||
configurations: [validConfiguration, validConfiguration2],
|
||||
compounds: [validCompound]
|
||||
};
|
||||
|
||||
testSuite({
|
||||
name: 'No Preferences',
|
||||
expectation: defaultLaunch
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Empty With Version',
|
||||
launch: {
|
||||
'version': '0.2.0'
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [],
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Empty With Version And Configurations',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [],
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [],
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Empty With Version And Compounds',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'compounds': []
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [],
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Valid Conf',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration]
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration],
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Bogus Conf',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, bogusConfiguration]
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, bogusConfiguration],
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Completely Bogus Conf',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': { 'valid': validConfiguration, 'bogus': bogusConfiguration }
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
|
||||
'compounds': []
|
||||
}
|
||||
});
|
||||
|
||||
const arrayBogusLaunch = [
|
||||
'version', '0.2.0',
|
||||
'configurations', { 'valid': validConfiguration, 'bogus': bogusConfiguration }
|
||||
];
|
||||
testSuite({
|
||||
name: 'Array Bogus Launch Configuration',
|
||||
launch: arrayBogusLaunch,
|
||||
expectation: {
|
||||
'0': 'version',
|
||||
'1': '0.2.0',
|
||||
'2': 'configurations',
|
||||
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
|
||||
'compounds': [],
|
||||
'configurations': []
|
||||
},
|
||||
inspectExpectation: {
|
||||
preferenceName: 'launch',
|
||||
defaultValue: defaultLaunch,
|
||||
workspaceValue: {
|
||||
'0': 'version',
|
||||
'1': '0.2.0',
|
||||
'2': 'configurations',
|
||||
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration }
|
||||
}
|
||||
}
|
||||
});
|
||||
testSuite({
|
||||
name: 'Array Bogus Settings Configuration',
|
||||
settings: {
|
||||
launch: arrayBogusLaunch
|
||||
},
|
||||
expectation: {
|
||||
'0': 'version',
|
||||
'1': '0.2.0',
|
||||
'2': 'configurations',
|
||||
'3': { 'valid': validConfiguration, 'bogus': bogusConfiguration },
|
||||
'compounds': [],
|
||||
'configurations': []
|
||||
},
|
||||
inspectExpectation: {
|
||||
preferenceName: 'launch',
|
||||
defaultValue: defaultLaunch,
|
||||
workspaceValue: arrayBogusLaunch
|
||||
}
|
||||
});
|
||||
|
||||
testSuite({
|
||||
name: 'Null Bogus Launch Configuration',
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
launch: null,
|
||||
expectation: {
|
||||
'compounds': [],
|
||||
'configurations': []
|
||||
}
|
||||
});
|
||||
testSuite({
|
||||
name: 'Null Bogus Settings Configuration',
|
||||
settings: {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
'launch': null
|
||||
},
|
||||
expectation: {}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Valid Compound',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, validConfiguration2],
|
||||
'compounds': [validCompound]
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, validConfiguration2],
|
||||
'compounds': [validCompound]
|
||||
}
|
||||
});
|
||||
|
||||
testLaunchAndSettingsSuite({
|
||||
name: 'Valid And Bogus',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, validConfiguration2, bogusConfiguration],
|
||||
'compounds': [validCompound, bogusCompound, bogusCompound2]
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, validConfiguration2, bogusConfiguration],
|
||||
'compounds': [validCompound, bogusCompound, bogusCompound2]
|
||||
}
|
||||
});
|
||||
|
||||
testSuite({
|
||||
name: 'Mixed',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration, bogusConfiguration],
|
||||
'compounds': [bogusCompound, bogusCompound2]
|
||||
},
|
||||
settings: {
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration2],
|
||||
'compounds': [validCompound]
|
||||
}
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration2, validConfiguration, bogusConfiguration],
|
||||
'compounds': [validCompound, bogusCompound, bogusCompound2]
|
||||
}
|
||||
});
|
||||
|
||||
testSuite({
|
||||
name: 'Mixed Launch Without Configurations',
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'compounds': [bogusCompound, bogusCompound2]
|
||||
},
|
||||
settings: {
|
||||
launch: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration2],
|
||||
'compounds': [validCompound]
|
||||
}
|
||||
},
|
||||
expectation: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration2],
|
||||
'compounds': [validCompound, bogusCompound, bogusCompound2]
|
||||
},
|
||||
inspectExpectation: {
|
||||
preferenceName: 'launch',
|
||||
defaultValue: defaultLaunch,
|
||||
workspaceValue: {
|
||||
'version': '0.2.0',
|
||||
'configurations': [validConfiguration2],
|
||||
'compounds': [validCompound, bogusCompound, bogusCompound2]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} LaunchAndSettingsSuiteOptions
|
||||
* @property {string} name
|
||||
* @property {any} expectation
|
||||
* @property {any} [launch]
|
||||
* @property {boolean} [only]
|
||||
* @property {ConfigMode} [configMode]
|
||||
*/
|
||||
/**
|
||||
* @type {(options: LaunchAndSettingsSuiteOptions) => void}
|
||||
*/
|
||||
function testLaunchAndSettingsSuite({
|
||||
name, expectation, launch, only, configMode
|
||||
}) {
|
||||
testSuite({
|
||||
name: name + ' Launch Configuration',
|
||||
launch,
|
||||
expectation,
|
||||
only,
|
||||
configMode
|
||||
});
|
||||
testSuite({
|
||||
name: name + ' Settings Configuration',
|
||||
settings: {
|
||||
'launch': launch
|
||||
},
|
||||
expectation,
|
||||
only,
|
||||
configMode
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Partial<import('@theia/core/src/browser/preferences/preference-service').PreferenceInspection<any>>} PreferenceInspection
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SuiteOptions
|
||||
* @property {string} name
|
||||
* @property {any} expectation
|
||||
* @property {PreferenceInspection} [inspectExpectation]
|
||||
* @property {any} [launch]
|
||||
* @property {any} [settings]
|
||||
* @property {boolean} [only]
|
||||
* @property {ConfigMode} [configMode]
|
||||
*/
|
||||
/**
|
||||
* @type {(options: SuiteOptions) => void}
|
||||
*/
|
||||
function testSuite(options) {
|
||||
describe(options.name, () => {
|
||||
|
||||
if (options.configMode) {
|
||||
testConfigSuite(options);
|
||||
} else {
|
||||
|
||||
testConfigSuite({
|
||||
...options,
|
||||
configMode: '.theia'
|
||||
});
|
||||
|
||||
if (options.settings || options.launch) {
|
||||
testConfigSuite({
|
||||
...options,
|
||||
configMode: '.vscode'
|
||||
});
|
||||
|
||||
testConfigSuite({
|
||||
...options,
|
||||
configMode: ['.theia', '.vscode']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
|
||||
/**
|
||||
* @param uri the URI of the file to modify
|
||||
* @returns {AbstractResourcePreferenceProvider | undefined} The preference provider matching the provided URI.
|
||||
*/
|
||||
function findProvider(uri) {
|
||||
/**
|
||||
* @param {PreferenceProvider} provider
|
||||
* @returns {boolean} whether the provider matches the desired URI.
|
||||
*/
|
||||
const isMatch = (provider) => {
|
||||
const configUri = provider.getConfigUri();
|
||||
return configUri && uri.isEqual(configUri);
|
||||
};
|
||||
for (const provider of userPreferences['providers'].values()) {
|
||||
if (isMatch(provider) && provider instanceof AbstractResourcePreferenceProvider) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
for (const provider of folderPreferences['providers'].values()) {
|
||||
if (isMatch(provider) && provider instanceof AbstractResourcePreferenceProvider) {
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
/** @type {PreferenceProvider} */
|
||||
const workspaceDelegate = workspacePreferences['delegate'];
|
||||
if (workspaceDelegate !== folderPreferences) {
|
||||
if (isMatch(workspaceDelegate) && workspaceDelegate instanceof AbstractResourcePreferenceProvider) {
|
||||
return workspaceDelegate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkspacePreferences() {
|
||||
const promises = [];
|
||||
for (const configPath of ['.theia', '.vscode']) {
|
||||
for (const name of ['settings', 'launch']) {
|
||||
promises.push((async () => {
|
||||
const uri = rootUri.resolve(configPath + '/' + name + '.json');
|
||||
const provider = findProvider(uri);
|
||||
try {
|
||||
if (provider) {
|
||||
if (provider.valid) {
|
||||
try {
|
||||
await waitForEvent(provider.onDidChangeValidity, 1000);
|
||||
} catch (e) {
|
||||
console.log('timed out waiting for validity change'); // sometimes, we seen to miss events: https://github.com/eclipse-theia/theia/issues/16088
|
||||
}
|
||||
}
|
||||
await provider['readPreferencesFromFile']();
|
||||
await provider['fireDidPreferencesChanged']();
|
||||
} else {
|
||||
console.log('Unable to find provider for', uri.path.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})());
|
||||
}
|
||||
}
|
||||
|
||||
await fileService.delete(rootUri.resolve('.theia'), { fromUserGesture: false, recursive: true }).catch(() => { });
|
||||
await fileService.delete(rootUri.resolve('.vscode'), { fromUserGesture: false, recursive: true }).catch(() => { });
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
function mergeLaunchConfigurations(config1, config2) {
|
||||
if (config1 === undefined && config2 === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (config2 === undefined) {
|
||||
return config1;
|
||||
}
|
||||
|
||||
let result;
|
||||
// skip invalid configs
|
||||
if (typeof config1 === 'object' && !Array.isArray(config1)) {
|
||||
result = { ...config1 };
|
||||
}
|
||||
if (typeof config2 === 'object' && !Array.isArray(config2)) {
|
||||
result = { ...(result ?? {}), ...config2 }
|
||||
}
|
||||
// merge configurations and compounds arrays
|
||||
const mergedConfigurations = mergeArrays(config1?.configurations, config2?.configurations);
|
||||
if (mergedConfigurations) {
|
||||
result.configurations = mergedConfigurations
|
||||
}
|
||||
const mergedCompounds = mergeArrays(config1?.compounds, config2?.compounds);
|
||||
if (mergedCompounds) {
|
||||
result.compounds = mergedCompounds;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function mergeArrays(array1, array2) {
|
||||
if (array1 === undefined && array2 === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (!Array.isArray(array1) && !Array.isArray(array2)) {
|
||||
return undefined;
|
||||
}
|
||||
let result = [];
|
||||
if (Array.isArray(array1)) {
|
||||
result = [...array1];
|
||||
}
|
||||
if (Array.isArray(array2)) {
|
||||
result = [...result, ...array2];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
const originalShouldOverwrite = fileResourceResolver['shouldOverwrite'];
|
||||
|
||||
before(async () => {
|
||||
// fail tests if out of async happens
|
||||
fileResourceResolver['shouldOverwrite'] = async () => (assert.fail('should be in sync'), false);
|
||||
await deleteWorkspacePreferences();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
fileResourceResolver['shouldOverwrite'] = originalShouldOverwrite;
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} ConfigSuiteOptions
|
||||
* @property {any} expectation
|
||||
* @property {any} [inspectExpectation]
|
||||
* @property {any} [launch]
|
||||
* @property {any} [settings]
|
||||
* @property {boolean} [only]
|
||||
* @property {ConfigMode} [configMode]
|
||||
*/
|
||||
/**
|
||||
* @type {(options: ConfigSuiteOptions) => void}
|
||||
*/
|
||||
function testConfigSuite({
|
||||
configMode, expectation, inspectExpectation, settings, launch, only
|
||||
}) {
|
||||
|
||||
describe(JSON.stringify(configMode, undefined, 2), () => {
|
||||
const configPaths = Array.isArray(configMode) ? configMode : [configMode];
|
||||
|
||||
/** @typedef {import('@theia/monaco-editor-core/esm/vs/base/common/lifecycle').IReference<import('@theia/monaco/lib/browser/monaco-editor-model').MonacoEditorModel>} ConfigModelReference */
|
||||
/** @type {ConfigModelReference[]} */
|
||||
beforeEach(async () => {
|
||||
/** @type {Promise<void>[]} */
|
||||
const promises = [];
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {Record<string, unknown>} value
|
||||
*/
|
||||
const ensureConfigModel = (name, value) => {
|
||||
for (const configPath of configPaths) {
|
||||
promises.push((async () => {
|
||||
try {
|
||||
const uri = rootUri.resolve(configPath + '/' + name + '.json');
|
||||
const provider = findProvider(uri);
|
||||
if (provider) {
|
||||
await provider['doSetPreference']('', [], value);
|
||||
} else {
|
||||
console.log('Unable to find provider for', uri.path.toString());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
})());
|
||||
}
|
||||
};
|
||||
if (settings) {
|
||||
ensureConfigModel('settings', settings);
|
||||
}
|
||||
if (launch) {
|
||||
ensureConfigModel('launch', launch);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
after(async () => await deleteWorkspacePreferences());
|
||||
|
||||
const testItOnly = !!only ? it.only : it;
|
||||
const testIt = testItOnly;
|
||||
|
||||
const settingsLaunch = settings ? settings['launch'] : undefined;
|
||||
|
||||
testIt('get from default', () => {
|
||||
const config = preferences.get('launch');
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
|
||||
});
|
||||
|
||||
testIt('get from undefined', () => {
|
||||
/** @type {any} */
|
||||
const config = preferences.get('launch', undefined, undefined);
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
|
||||
});
|
||||
|
||||
testIt('get from rootUri', () => {
|
||||
/** @type {any} */
|
||||
const config = preferences.get('launch', undefined, rootUri.toString());
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(config)), expectation);
|
||||
});
|
||||
|
||||
testIt('inspect in undefined', () => {
|
||||
const inspect = preferences.inspect('launch');
|
||||
/** @type {PreferenceInspection} */
|
||||
let expected = inspectExpectation;
|
||||
if (!expected) {
|
||||
expected = {
|
||||
preferenceName: 'launch',
|
||||
defaultValue: defaultLaunch
|
||||
};
|
||||
const workspaceValue = mergeLaunchConfigurations(settingsLaunch, launch);
|
||||
if (workspaceValue !== undefined && JSON.stringify(workspaceValue) !== '{}') {
|
||||
Object.assign(expected, { workspaceValue });
|
||||
}
|
||||
}
|
||||
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
|
||||
});
|
||||
|
||||
testIt('inspect in rootUri', () => {
|
||||
const inspect = preferences.inspect('launch', rootUri.toString());
|
||||
/** @type {PreferenceInspection} */
|
||||
const expected = {
|
||||
preferenceName: 'launch',
|
||||
defaultValue: defaultLaunch
|
||||
};
|
||||
if (inspectExpectation) {
|
||||
Object.assign(expected, {
|
||||
workspaceValue: inspectExpectation.workspaceValue,
|
||||
workspaceFolderValue: inspectExpectation.workspaceValue
|
||||
});
|
||||
} else {
|
||||
const value = mergeLaunchConfigurations(settingsLaunch, launch);
|
||||
if (value !== undefined && JSON.stringify(value) !== '{}') {
|
||||
Object.assign(expected, {
|
||||
workspaceValue: value,
|
||||
workspaceFolderValue: value
|
||||
});
|
||||
}
|
||||
}
|
||||
const expectedValue = expected.workspaceFolderValue || expected.workspaceValue || expected.globalValue || expected.defaultValue;
|
||||
assert.deepStrictEqual(JSON.parse(JSON.stringify(inspect)), { ...expected, value: expectedValue });
|
||||
});
|
||||
|
||||
testIt('update launch', async () => {
|
||||
await preferences.set('launch', validLaunch);
|
||||
|
||||
const inspect = preferences.inspect('launch');
|
||||
const actual = inspect && inspect.workspaceValue;
|
||||
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
testIt('update launch Workspace', async () => {
|
||||
await preferences.set('launch', validLaunch, PreferenceScope.Workspace);
|
||||
|
||||
const inspect = preferences.inspect('launch');
|
||||
const actual = inspect && inspect.workspaceValue;
|
||||
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
testIt('update launch WorkspaceFolder', async () => {
|
||||
try {
|
||||
await preferences.set('launch', validLaunch, PreferenceScope.Folder);
|
||||
assert.fail('should not be possible to update Workspace Folder Without resource');
|
||||
} catch (e) {
|
||||
assert.deepStrictEqual(e.message, 'Unable to write to Folder Settings because no resource is provided.');
|
||||
}
|
||||
});
|
||||
|
||||
testIt('update launch WorkspaceFolder with resource', async () => {
|
||||
await preferences.set('launch', validLaunch, PreferenceScope.Folder, rootUri.toString());
|
||||
|
||||
const inspect = preferences.inspect('launch');
|
||||
const actual = inspect && inspect.workspaceValue;
|
||||
const expected = mergeLaunchConfigurations(settingsLaunch, validLaunch);
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
});
|
||||
|
||||
if ((launch && !Array.isArray(launch)) || (settingsLaunch && !Array.isArray(settingsLaunch))) {
|
||||
testIt('update launch.configurations', async () => {
|
||||
await preferences.set('launch.configurations', [validConfiguration, validConfiguration2]);
|
||||
|
||||
const inspect = preferences.inspect('launch');
|
||||
const actual = inspect && inspect.workspaceValue && inspect.workspaceValue.configurations;
|
||||
let expect = [validConfiguration, validConfiguration2];
|
||||
if (Array.isArray(settingsLaunch?.configurations)) {
|
||||
expect = [...(settingsLaunch.configurations), ...expect]
|
||||
}
|
||||
assert.deepStrictEqual(actual, expect);
|
||||
});
|
||||
}
|
||||
|
||||
testIt('delete launch', async () => {
|
||||
await preferences.set('launch', undefined);
|
||||
const actual = preferences.inspect('launch');
|
||||
|
||||
let expected = undefined;
|
||||
if (configPaths[1]) {
|
||||
expected = launch;
|
||||
if (Array.isArray(expected)) {
|
||||
expected = { ...expected };
|
||||
}
|
||||
}
|
||||
expected = mergeLaunchConfigurations(settingsLaunch, expected);
|
||||
assert.deepStrictEqual(actual && actual.workspaceValue, expected);
|
||||
});
|
||||
|
||||
if ((launch && !Array.isArray(launch)) || (settingsLaunch && !Array.isArray(settingsLaunch))) {
|
||||
testIt('delete launch.configurations', async () => {
|
||||
await preferences.set('launch.configurations', undefined);
|
||||
|
||||
const actual = preferences.inspect('launch');
|
||||
const actualWorkspaceValue = actual && actual.workspaceValue;
|
||||
|
||||
let expected = { ...launch };
|
||||
if (launch) {
|
||||
delete expected['configurations'];
|
||||
}
|
||||
expected = mergeLaunchConfigurations(settingsLaunch, expected);
|
||||
assert.deepStrictEqual(actualWorkspaceValue, expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
179
examples/api-tests/src/menus.spec.js
Normal file
179
examples/api-tests/src/menus.spec.js
Normal file
@@ -0,0 +1,179 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Menus', function () {
|
||||
this.timeout(7500);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { BrowserMenuBarContribution } = require('@theia/core/lib/browser/menu/browser-menu-plugin');
|
||||
const { MenuModelRegistry } = require('@theia/core/lib/common/menu');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { ContextMenuRenderer } = require('@theia/core/lib/browser/context-menu-renderer');
|
||||
const { BrowserContextMenuAccess } = require('@theia/core/lib/browser/menu/browser-context-menu-renderer');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { ViewContainer } = require('@theia/core/lib/browser/view-container');
|
||||
const { waitForRevealed, waitForHidden } = require('@theia/core/lib/browser/widgets/widget');
|
||||
const { CallHierarchyContribution } = require('@theia/callhierarchy/lib/browser/callhierarchy-contribution');
|
||||
const { EXPLORER_VIEW_CONTAINER_ID } = require('@theia/navigator/lib/browser/navigator-widget-factory');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
|
||||
const { ScmHistoryContribution } = require('@theia/scm-extra/lib/browser/history/scm-history-contribution');
|
||||
const { OutlineViewContribution } = require('@theia/outline-view/lib/browser/outline-view-contribution');
|
||||
const { OutputContribution } = require('@theia/output/lib/browser/output-contribution');
|
||||
const { PluginFrontendViewContribution } = require('@theia/plugin-ext/lib/main/browser/plugin-frontend-view-contribution');
|
||||
const { ProblemContribution } = require('@theia/markers/lib/browser/problem/problem-contribution');
|
||||
const { PropertyViewContribution } = require('@theia/property-view/lib/browser/property-view-contribution');
|
||||
const { SearchInWorkspaceFrontendContribution } = require('@theia/search-in-workspace/lib/browser/search-in-workspace-frontend-contribution');
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
|
||||
const container = window.theia.container;
|
||||
const shell = container.get(ApplicationShell);
|
||||
/** @type {BrowserMenuBarContribution} */
|
||||
const menuBarContribution = container.get(BrowserMenuBarContribution);
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
const menus = container.get(MenuModelRegistry);
|
||||
const commands = container.get(CommandRegistry);
|
||||
const contextMenuService = container.get(ContextMenuRenderer);
|
||||
|
||||
before(async function () {
|
||||
await pluginService.didStart;
|
||||
await pluginService.activateByViewContainer('explorer');
|
||||
// Updating the menu interferes with our ability to programmatically test it
|
||||
// We simply disable the menu updating
|
||||
menus.isReady = false;
|
||||
});
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
afterEach(() => toTearDown.dispose());
|
||||
|
||||
for (const contribution of [
|
||||
container.get(CallHierarchyContribution),
|
||||
container.get(FileNavigatorContribution),
|
||||
container.get(ScmContribution),
|
||||
container.get(ScmHistoryContribution),
|
||||
container.get(OutlineViewContribution),
|
||||
container.get(OutputContribution),
|
||||
container.get(PluginFrontendViewContribution),
|
||||
container.get(ProblemContribution),
|
||||
container.get(PropertyViewContribution),
|
||||
container.get(SearchInWorkspaceFrontendContribution)
|
||||
]) {
|
||||
it(`should toggle '${contribution.viewLabel}' view`, async () => {
|
||||
await contribution.closeView();
|
||||
await menuBarContribution.menuBar.triggerMenuItem('View', contribution.viewLabel);
|
||||
await shell.waitForActivation(contribution.viewId);
|
||||
});
|
||||
}
|
||||
|
||||
it('reveal more context menu in the explorer view container toolbar', async function () {
|
||||
const viewContainer = await shell.revealWidget(EXPLORER_VIEW_CONTAINER_ID);
|
||||
if (!(viewContainer instanceof ViewContainer)) {
|
||||
assert.isTrue(viewContainer instanceof ViewContainer);
|
||||
return;
|
||||
}
|
||||
|
||||
const contribution = container.get(FileNavigatorContribution);
|
||||
const waitForParts = [];
|
||||
for (const part of viewContainer.getParts()) {
|
||||
if (part.wrapped.id !== contribution.viewId) {
|
||||
part.hide();
|
||||
waitForParts.push(waitForHidden(part.wrapped));
|
||||
} else {
|
||||
part.show();
|
||||
waitForParts.push(waitForRevealed(part.wrapped));
|
||||
}
|
||||
}
|
||||
await Promise.all(waitForParts);
|
||||
|
||||
const contextMenuAccess = shell.leftPanelHandler.toolBar.showMoreContextMenu({ x: 0, y: 0 });
|
||||
toTearDown.push(contextMenuAccess);
|
||||
if (!(contextMenuAccess instanceof BrowserContextMenuAccess)) {
|
||||
assert.isTrue(contextMenuAccess instanceof BrowserContextMenuAccess);
|
||||
return;
|
||||
}
|
||||
const contextMenu = contextMenuAccess.menu;
|
||||
|
||||
await waitForRevealed(contextMenu);
|
||||
assert.notEqual(contextMenu.items.length, 0);
|
||||
});
|
||||
|
||||
it('rendering a new context menu should close the current', async function () {
|
||||
const commandId = '__test_command_' + new Date();
|
||||
const contextMenuPath = ['__test_first_context_menu_' + new Date()];
|
||||
const contextMenuPath2 = ['__test_second_context_menu_' + new Date()];
|
||||
toTearDown.push(commands.registerCommand({
|
||||
id: commandId,
|
||||
label: commandId
|
||||
}, {
|
||||
execute: () => { }
|
||||
}));
|
||||
toTearDown.push(menus.registerMenuAction(contextMenuPath, { commandId }));
|
||||
toTearDown.push(menus.registerMenuAction(contextMenuPath2, { commandId }));
|
||||
|
||||
const access = contextMenuService.render({
|
||||
anchor: { x: 0, y: 0 },
|
||||
menuPath: contextMenuPath
|
||||
});
|
||||
toTearDown.push(access);
|
||||
if (!(access instanceof BrowserContextMenuAccess)) {
|
||||
assert.isTrue(access instanceof BrowserContextMenuAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.deepEqual(contextMenuService.current, access);
|
||||
assert.isFalse(access.disposed);
|
||||
|
||||
await waitForRevealed(access.menu);
|
||||
assert.notEqual(access.menu.items.length, 0);
|
||||
assert.deepEqual(contextMenuService.current, access);
|
||||
assert.isFalse(access.disposed);
|
||||
|
||||
const access2 = contextMenuService.render({
|
||||
anchor: { x: 0, y: 0 },
|
||||
menuPath: contextMenuPath2
|
||||
});
|
||||
toTearDown.push(access2);
|
||||
if (!(access2 instanceof BrowserContextMenuAccess)) {
|
||||
assert.isTrue(access2 instanceof BrowserContextMenuAccess);
|
||||
return;
|
||||
}
|
||||
|
||||
assert.deepEqual(contextMenuService.current, access2);
|
||||
assert.isFalse(access2.disposed);
|
||||
assert.isTrue(access.disposed);
|
||||
|
||||
await waitForRevealed(access2.menu);
|
||||
assert.deepEqual(contextMenuService.current, access2);
|
||||
assert.isFalse(access2.disposed);
|
||||
assert.isTrue(access.disposed);
|
||||
|
||||
access2.dispose();
|
||||
assert.deepEqual(contextMenuService.current, undefined);
|
||||
assert.isTrue(access2.disposed);
|
||||
|
||||
await waitForHidden(access2.menu);
|
||||
assert.deepEqual(contextMenuService.current, undefined);
|
||||
assert.isTrue(access2.disposed);
|
||||
});
|
||||
|
||||
it('should not fail to register a menu with an invalid command', () => {
|
||||
assert.doesNotThrow(() => menus.registerMenuAction(['test-menu-path'], { commandId: 'invalid-command', label: 'invalid command' }), 'should not throw.');
|
||||
});
|
||||
|
||||
});
|
||||
198
examples/api-tests/src/monaco-api.spec.js
Normal file
198
examples/api-tests/src/monaco-api.spec.js
Normal file
@@ -0,0 +1,198 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
const { timeout } = require('@theia/core/lib/common/promise-util');
|
||||
const { IOpenerService } = require('@theia/monaco-editor-core/esm/vs/platform/opener/common/opener');
|
||||
|
||||
// @ts-check
|
||||
describe('Monaco API', async function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const { MonacoResolvedKeybinding } = require('@theia/monaco/lib/browser/monaco-resolved-keybinding');
|
||||
const { MonacoTextmateService } = require('@theia/monaco/lib/browser/textmate/monaco-textmate-service');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { KeyCodeChord, ResolvedChord } = require('@theia/monaco-editor-core/esm/vs/base/common/keybindings');
|
||||
const { IKeybindingService } = require('@theia/monaco-editor-core/esm/vs/platform/keybinding/common/keybinding');
|
||||
const { StandaloneServices } = require('@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices');
|
||||
const { TokenizationRegistry } = require('@theia/monaco-editor-core/esm/vs/editor/common/languages');
|
||||
const { MonacoContextKeyService } = require('@theia/monaco/lib/browser/monaco-context-key-service');
|
||||
const { URI } = require('@theia/monaco-editor-core/esm/vs/base/common/uri');
|
||||
|
||||
const container = window.theia.container;
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const textmateService = container.get(MonacoTextmateService);
|
||||
/** @type {import('@theia/core/src/common/command').CommandRegistry} */
|
||||
const commands = container.get(CommandRegistry);
|
||||
/** @type {import('@theia/monaco/src/browser/monaco-context-key-service').MonacoContextKeyService} */
|
||||
const contextKeys = container.get(MonacoContextKeyService);
|
||||
|
||||
/** @type {MonacoEditor} */
|
||||
let monacoEditor;
|
||||
|
||||
before(async () => {
|
||||
const root = workspaceService.tryGetRoots()[0];
|
||||
const editor = await editorManager.open(root.resource.resolve('package.json'), {
|
||||
mode: 'reveal'
|
||||
});
|
||||
monacoEditor = /** @type {MonacoEditor} */ (MonacoEditor.get(editor));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
it('KeybindingService.resolveKeybinding', () => {
|
||||
const chord = new KeyCodeChord(true, true, true, true, 41 /* KeyCode.KeyK */);
|
||||
const chordKeybinding = chord.toKeybinding();
|
||||
assert.equal(chordKeybinding.chords.length, 1);
|
||||
assert.equal(chordKeybinding.chords[0], chord);
|
||||
|
||||
const resolvedKeybindings = StandaloneServices.get(IKeybindingService).resolveKeybinding(chordKeybinding);
|
||||
assert.equal(resolvedKeybindings.length, 1);
|
||||
|
||||
const resolvedKeybinding = resolvedKeybindings[0];
|
||||
if (resolvedKeybinding instanceof MonacoResolvedKeybinding) {
|
||||
const label = resolvedKeybinding.getLabel();
|
||||
const ariaLabel = resolvedKeybinding.getAriaLabel();
|
||||
const electronAccelerator = resolvedKeybinding.getElectronAccelerator();
|
||||
const userSettingsLabel = resolvedKeybinding.getUserSettingsLabel();
|
||||
const WYSIWYG = resolvedKeybinding.isWYSIWYG();
|
||||
const parts = resolvedKeybinding.getChords();
|
||||
const dispatchParts = resolvedKeybinding.getDispatchChords().map(str => str === null ? '' : str);
|
||||
|
||||
const platform = window.navigator.platform;
|
||||
let expected;
|
||||
if (platform.includes('Mac')) {
|
||||
// Mac os
|
||||
expected = {
|
||||
label: '⌃⇧⌥⌘K',
|
||||
ariaLabel: '⌃⇧⌥⌘K',
|
||||
electronAccelerator: 'Ctrl+Shift+Alt+Cmd+K',
|
||||
userSettingsLabel: 'ctrl+shift+alt+cmd+K',
|
||||
WYSIWYG: true,
|
||||
parts: [new ResolvedChord(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
'K',
|
||||
'K',
|
||||
)],
|
||||
dispatchParts: [
|
||||
'ctrl+shift+alt+meta+K'
|
||||
]
|
||||
};
|
||||
} else {
|
||||
expected = {
|
||||
label: 'Ctrl+Shift+Alt+K',
|
||||
ariaLabel: 'Ctrl+Shift+Alt+K',
|
||||
electronAccelerator: 'Ctrl+Shift+Alt+K',
|
||||
userSettingsLabel: 'ctrl+shift+alt+K',
|
||||
WYSIWYG: true,
|
||||
parts: [new ResolvedChord(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
'K',
|
||||
'K'
|
||||
)],
|
||||
dispatchParts: [
|
||||
'ctrl+shift+alt+K'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
assert.deepStrictEqual({
|
||||
label, ariaLabel, electronAccelerator, userSettingsLabel, WYSIWYG, parts, dispatchParts
|
||||
}, expected);
|
||||
} else {
|
||||
assert.fail(`resolvedKeybinding must be of ${MonacoResolvedKeybinding.name} type`);
|
||||
}
|
||||
});
|
||||
|
||||
it('TokenizationRegistry.getColorMap', async () => {
|
||||
if (textmateService['monacoThemeRegistry'].getThemeData().base !== 'vs') {
|
||||
const didChangeColorMap = new Promise(resolve => {
|
||||
const toDispose = TokenizationRegistry.onDidChange(() => {
|
||||
toDispose.dispose();
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
textmateService['themeService'].setCurrentTheme('light');
|
||||
await didChangeColorMap;
|
||||
}
|
||||
|
||||
const textMateColorMap = textmateService['grammarRegistry'].getColorMap();
|
||||
assert.notEqual(textMateColorMap.indexOf('#795E26'), -1, 'Expected custom toke colors for the light theme to be enabled.');
|
||||
|
||||
const monacoColorMap = (TokenizationRegistry.getColorMap() || []).
|
||||
splice(0, textMateColorMap.length).map(c => c.toString().toUpperCase());
|
||||
assert.deepStrictEqual(monacoColorMap, textMateColorMap, 'Expected textmate colors to have the same index in the monaco color map.');
|
||||
});
|
||||
|
||||
it('OpenerService.open', async () => {
|
||||
/** @type {import('@theia/monaco-editor-core/esm/vs/editor/browser/services/openerService').OpenerService} */
|
||||
const openerService = StandaloneServices.get(IOpenerService);
|
||||
|
||||
let opened = false;
|
||||
const id = '__test:OpenerService.open';
|
||||
const unregisterCommand = commands.registerCommand({ id }, {
|
||||
execute: arg => (console.log(arg), opened = arg === 'foo')
|
||||
});
|
||||
try {
|
||||
await openerService.open(URI.parse('command:' + id + '?"foo"'));
|
||||
assert.isTrue(opened);
|
||||
} finally {
|
||||
unregisterCommand.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it('Supports setting contexts using the command registry', async () => {
|
||||
const setContext = '_setContext';
|
||||
const key = 'monaco-api-test-context';
|
||||
const firstValue = 'first setting';
|
||||
const secondValue = 'second setting';
|
||||
assert.isFalse(contextKeys.match(`${key} == '${firstValue}'`));
|
||||
await commands.executeCommand(setContext, key, firstValue);
|
||||
assert.isTrue(contextKeys.match(`${key} == '${firstValue}'`));
|
||||
await commands.executeCommand(setContext, key, secondValue);
|
||||
assert.isTrue(contextKeys.match(`${key} == '${secondValue}'`));
|
||||
});
|
||||
|
||||
it('Supports context key: inQuickOpen', async () => {
|
||||
const inQuickOpenContextKey = 'inQuickOpen';
|
||||
const quickOpenCommands = ['file-search.openFile', 'workbench.action.showCommands'];
|
||||
const CommandThatChangesFocus = 'workbench.files.action.focusFilesExplorer';
|
||||
|
||||
for (const cmd of quickOpenCommands) {
|
||||
assert.isFalse(contextKeys.match(inQuickOpenContextKey));
|
||||
await commands.executeCommand(cmd);
|
||||
assert.isTrue(contextKeys.match(inQuickOpenContextKey));
|
||||
|
||||
await commands.executeCommand(CommandThatChangesFocus);
|
||||
await timeout(0);
|
||||
assert.isFalse(contextKeys.match(inQuickOpenContextKey));
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
92
examples/api-tests/src/navigator.spec.js
Normal file
92
examples/api-tests/src/navigator.spec.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Navigator', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
||||
const { DirNode, FileNode } = require('@theia/filesystem/lib/browser/file-tree/file-tree');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
const fileService = container.get(FileService);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const navigatorContribution = container.get(FileNavigatorContribution);
|
||||
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const fileUri = rootUri.resolve('.test/nested/source/text.txt');
|
||||
const targetUri = rootUri.resolve('.test/target');
|
||||
|
||||
beforeEach(async () => {
|
||||
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
|
||||
await fileService.createFolder(targetUri);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fileService.delete(targetUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
|
||||
});
|
||||
|
||||
/** @type {Array<['copy' | 'move', boolean]>} */
|
||||
const operations = [
|
||||
['copy', false],
|
||||
['move', false]
|
||||
];
|
||||
/** @type {Array<['file' | 'dir', boolean]>} */
|
||||
const fileTypes = [
|
||||
['file', false],
|
||||
['dir', false],
|
||||
];
|
||||
for (const [operation, onlyOperation] of operations) {
|
||||
for (const [fileType, onlyFileType] of fileTypes) {
|
||||
const ExpectedNodeType = fileType === 'file' ? FileNode : DirNode;
|
||||
(onlyOperation || onlyFileType ? it.only : it)(operation + ' ' + fileType, async function () {
|
||||
const navigator = await navigatorContribution.openView({ reveal: true });
|
||||
await navigator.model.refresh();
|
||||
|
||||
const sourceUri = fileType === 'file' ? fileUri : fileUri.parent;
|
||||
const sourceNode = await navigator.model.revealFile(sourceUri);
|
||||
if (!ExpectedNodeType.is(sourceNode)) {
|
||||
return assert.isTrue(ExpectedNodeType.is(sourceNode));
|
||||
}
|
||||
|
||||
const targetNode = await navigator.model.revealFile(targetUri);
|
||||
if (!DirNode.is(targetNode)) {
|
||||
return assert.isTrue(DirNode.is(targetNode));
|
||||
}
|
||||
|
||||
let actualUri;
|
||||
if (operation === 'copy') {
|
||||
actualUri = await navigator.model.copy(sourceUri, targetNode);
|
||||
} else {
|
||||
actualUri = await navigator.model.move(sourceNode, targetNode);
|
||||
}
|
||||
if (!actualUri) {
|
||||
return assert.isDefined(actualUri);
|
||||
}
|
||||
|
||||
await navigator.model.refresh(targetNode);
|
||||
const actualNode = await navigator.model.revealFile(actualUri);
|
||||
assert.isTrue(ExpectedNodeType.is(actualNode));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
209
examples/api-tests/src/preferences.spec.js
Normal file
209
examples/api-tests/src/preferences.spec.js
Normal file
@@ -0,0 +1,209 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
|
||||
describe('Preferences', function () {
|
||||
this.timeout(5_000);
|
||||
const { assert } = chai;
|
||||
const { PreferenceProvider } = require('@theia/core/lib/common/preferences/preference-provider');
|
||||
const { PreferenceService, PreferenceScope } = require('@theia/core/lib/common/preferences');
|
||||
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
||||
const { PreferenceLanguageOverrideService } = require('@theia/core/lib/common/preferences/preference-language-override-service');
|
||||
const { MonacoTextModelService } = require('@theia/monaco/lib/browser/monaco-text-model-service');
|
||||
const { PreferenceSchemaService } = require('@theia/core/lib/common/preferences')
|
||||
const { container } = window.theia;
|
||||
/** @type {import ('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
|
||||
const preferenceService = container.get(PreferenceService);
|
||||
/** @type {import ('@theia/core/lib/common/preferences/preference-language-override-service').PreferenceLanguageOverrideService} */
|
||||
const overrideService = container.get(PreferenceLanguageOverrideService);
|
||||
const fileService = container.get(FileService);
|
||||
/** @type {import ('@theia/core/lib/common/uri').default} */
|
||||
const uri = preferenceService.getConfigUri(PreferenceScope.Workspace);
|
||||
/** @type {import('@theia/preferences/lib/browser/folders-preferences-provider').FoldersPreferencesProvider} */
|
||||
const folderPreferences = container.getNamed(PreferenceProvider, PreferenceScope.Folder);
|
||||
/** @type PreferenceSchemaService */
|
||||
const schemaService = container.get(PreferenceSchemaService);
|
||||
const modelService = container.get(MonacoTextModelService);
|
||||
|
||||
|
||||
const overrideIdentifier = 'bargle-noddle-zaus'; // Probably not in our preference files...
|
||||
schemaService.registerOverrideIdentifier(overrideIdentifier);
|
||||
const tabSize = 'editor.tabSize';
|
||||
const fontSize = 'editor.fontSize';
|
||||
const override = overrideService.markLanguageOverride(overrideIdentifier);
|
||||
const overriddenTabSize = overrideService.overridePreferenceName({ overrideIdentifier, preferenceName: tabSize });
|
||||
const overriddenFontSize = overrideService.overridePreferenceName({ overrideIdentifier, preferenceName: fontSize });
|
||||
/**
|
||||
* @returns {Promise<Record<string, any>>}
|
||||
*/
|
||||
async function getPreferences() {
|
||||
try {
|
||||
const content = (await fileService.read(uri)).value;
|
||||
return JSON.parse(content);
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {unknown} value
|
||||
*/
|
||||
async function setPreference(key, value) {
|
||||
return preferenceService.set(key, value, PreferenceScope.Workspace);
|
||||
}
|
||||
|
||||
async function deleteAllValues() {
|
||||
return setValueTo(undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} value - A JSON value to write to the workspace preference file.
|
||||
*/
|
||||
async function setValueTo(value) {
|
||||
const reference = await modelService.createModelReference(uri);
|
||||
if (reference.object.dirty) {
|
||||
await reference.object.revert();
|
||||
}
|
||||
/** @type {import ('@theia/preferences/lib/browser/folder-preference-provider').FolderPreferenceProvider} */
|
||||
const provider = Array.from(folderPreferences['providers'].values()).find(candidate => candidate.getConfigUri().isEqual(uri));
|
||||
assert.isDefined(provider);
|
||||
await provider['doSetPreference']('', [], value);
|
||||
reference.dispose();
|
||||
}
|
||||
|
||||
let fileExistsBeforehand = false;
|
||||
let contentBeforehand = '';
|
||||
|
||||
before(async function () {
|
||||
assert.isDefined(uri, 'The workspace config URI should be defined!');
|
||||
fileExistsBeforehand = await fileService.exists(uri);
|
||||
contentBeforehand = await fileService.read(uri).then(({ value }) => value).catch(() => '');
|
||||
schemaService.registerOverrideIdentifier(overrideIdentifier);
|
||||
await deleteAllValues();
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
if (!fileExistsBeforehand) {
|
||||
await fileService.delete(uri, { fromUserGesture: false }).catch(() => { });
|
||||
} else {
|
||||
let content = '';
|
||||
try { content = JSON.parse(contentBeforehand); } catch { }
|
||||
// Use the preference service because its promise is guaranteed to resolve after the file change is complete.
|
||||
await setValueTo(content);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
const prefs = await getPreferences();
|
||||
for (const key of [tabSize, fontSize, override, overriddenTabSize, overriddenFontSize]) {
|
||||
shouldBeUndefined(prefs[key], key);
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(async function () {
|
||||
await deleteAllValues();
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @param {string} key
|
||||
*/
|
||||
function shouldBeUndefined(value, key) {
|
||||
assert.isUndefined(value, `There should be no ${key} object or value in the preferences.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{newTabSize: number, newFontSize: number, startingTabSize: number, startingFontSize: number}>}
|
||||
*/
|
||||
async function setUpOverride() {
|
||||
const startingTabSize = preferenceService.get(tabSize);
|
||||
const startingFontSize = preferenceService.get(fontSize);
|
||||
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize, 'The overridden value should equal the default.');
|
||||
assert.equal(preferenceService.get(overriddenFontSize), startingFontSize, 'The overridden value should equal the default.');
|
||||
const newTabSize = startingTabSize + 2;
|
||||
const newFontSize = startingFontSize + 2;
|
||||
await Promise.all([
|
||||
setPreference(overriddenTabSize, newTabSize),
|
||||
setPreference(overriddenFontSize, newFontSize),
|
||||
]);
|
||||
assert.equal(preferenceService.get(overriddenTabSize), newTabSize, 'After setting, the new value should be active for the override.');
|
||||
assert.equal(preferenceService.get(overriddenFontSize), newFontSize, 'After setting, the new value should be active for the override.');
|
||||
return { newTabSize, newFontSize, startingTabSize, startingFontSize };
|
||||
}
|
||||
|
||||
it('Sets language overrides as objects', async function () {
|
||||
const { newTabSize, newFontSize } = await setUpOverride();
|
||||
const prefs = await getPreferences();
|
||||
assert.isObject(prefs[override], 'The override should be a key in the preference object.');
|
||||
assert.equal(prefs[override][tabSize], newTabSize, 'editor.tabSize should be a key in the override object and have the correct value.');
|
||||
assert.equal(prefs[override][fontSize], newFontSize, 'editor.fontSize should be a key in the override object and should have the correct value.');
|
||||
shouldBeUndefined(prefs[overriddenTabSize], overriddenTabSize);
|
||||
shouldBeUndefined(prefs[overriddenFontSize], overriddenFontSize);
|
||||
});
|
||||
|
||||
it('Allows deletion of individual keys in the override object.', async function () {
|
||||
const { startingTabSize } = await setUpOverride();
|
||||
await setPreference(overriddenTabSize, undefined);
|
||||
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize);
|
||||
const prefs = await getPreferences();
|
||||
shouldBeUndefined(prefs[override][tabSize], tabSize);
|
||||
shouldBeUndefined(prefs[overriddenFontSize], overriddenFontSize);
|
||||
shouldBeUndefined(prefs[overriddenTabSize], overriddenTabSize);
|
||||
});
|
||||
|
||||
it('Allows deletion of the whole override object', async function () {
|
||||
const { startingFontSize, startingTabSize } = await setUpOverride();
|
||||
await setPreference(override, undefined);
|
||||
assert.equal(preferenceService.get(overriddenTabSize), startingTabSize, 'The overridden value should revert to the default.');
|
||||
assert.equal(preferenceService.get(overriddenFontSize), startingFontSize, 'The overridden value should revert to the default.');
|
||||
const prefs = await getPreferences();
|
||||
shouldBeUndefined(prefs[override], override);
|
||||
});
|
||||
|
||||
it('Handles many synchronous settings of preferences gracefully', async function () {
|
||||
let settings = 0;
|
||||
const promises = [];
|
||||
const searchPref = 'search.searchOnTypeDebouncePeriod'
|
||||
const channelPref = 'output.maxChannelHistory'
|
||||
const hoverPref = 'workbench.hover.delay';
|
||||
let searchDebounce;
|
||||
let channelHistory;
|
||||
let hoverDelay;
|
||||
/** @type import ('@theia/core/src/browser/preferences/preference-service').PreferenceChanges | undefined */
|
||||
let event;
|
||||
const toDispose = preferenceService.onPreferencesChanged(e => event = e);
|
||||
while (settings++ < 50) {
|
||||
searchDebounce = 100 + Math.floor(Math.random() * 500);
|
||||
channelHistory = 200 + Math.floor(Math.random() * 800);
|
||||
hoverDelay = 250 + Math.floor(Math.random() * 2_500);
|
||||
promises.push(
|
||||
preferenceService.set(searchPref, searchDebounce),
|
||||
preferenceService.set(channelPref, channelHistory),
|
||||
preferenceService.set(hoverPref, hoverDelay)
|
||||
);
|
||||
}
|
||||
const results = await Promise.allSettled(promises);
|
||||
const expectedValues = { [searchPref]: searchDebounce, [channelPref]: channelHistory, [hoverPref]: hoverDelay };
|
||||
const actualValues = { [searchPref]: preferenceService.get(searchPref), [channelPref]: preferenceService.get(channelPref), [hoverPref]: preferenceService.get(hoverPref), }
|
||||
const eventKeys = event && Object.keys(event).sort();
|
||||
toDispose.dispose();
|
||||
assert(results.every(setting => setting.status === 'fulfilled'), 'All promises should have resolved rather than rejected.');
|
||||
assert.deepEqual([channelPref, searchPref, hoverPref], eventKeys, 'The event should contain the changed preference names.');
|
||||
assert.deepEqual(expectedValues, actualValues, 'The service state should reflect the most recent setting');
|
||||
});
|
||||
});
|
||||
512
examples/api-tests/src/saveable.spec.js
Normal file
512
examples/api-tests/src/saveable.spec.js
Normal file
@@ -0,0 +1,512 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Saveable', function () {
|
||||
this.timeout(30000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
|
||||
const { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
|
||||
const { Saveable, SaveableWidget } = require('@theia/core/lib/browser/saveable');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
||||
const { FileResource } = require('@theia/filesystem/lib/browser/file-resource');
|
||||
const { ETAG_DISABLED } = require('@theia/filesystem/lib/common/files');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const { Deferred, timeout } = require('@theia/core/lib/common/promise-util');
|
||||
const { Disposable, DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
|
||||
|
||||
const container = window.theia.container;
|
||||
/** @type {EditorManager} */
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const fileService = container.get(FileService);
|
||||
/** @type {import('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
|
||||
const preferences = container.get(PreferenceService);
|
||||
|
||||
/** @type {EditorWidget & SaveableWidget} */
|
||||
let widget;
|
||||
/** @type {MonacoEditor} */
|
||||
let editor;
|
||||
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const fileUri = rootUri.resolve('.test/foo.txt');
|
||||
|
||||
const closeOnFileDelete = 'workbench.editor.closeOnFileDelete';
|
||||
|
||||
/**
|
||||
* @param {FileResource['shouldOverwrite']} shouldOverwrite
|
||||
* @returns {Disposable}
|
||||
*/
|
||||
function setShouldOverwrite(shouldOverwrite) {
|
||||
const resource = editor.document['resource'];
|
||||
assert.isTrue(resource instanceof FileResource);
|
||||
const fileResource = /** @type {FileResource} */ (resource);
|
||||
const originalShouldOverwrite = fileResource['shouldOverwrite'];
|
||||
fileResource['shouldOverwrite'] = shouldOverwrite;
|
||||
return Disposable.create(() => fileResource['shouldOverwrite'] = originalShouldOverwrite);
|
||||
}
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
|
||||
/** @type {string | undefined} */
|
||||
const autoSave = preferences.get('files.autoSave', undefined, rootUri.toString());
|
||||
|
||||
beforeEach(async () => {
|
||||
await preferences.set('files.autoSave', 'off', undefined, rootUri.toString());
|
||||
await preferences.set(closeOnFileDelete, true);
|
||||
await editorManager.closeAll({ save: false });
|
||||
const watcher = fileService.watch(fileUri); // create/delete events are sometimes coalesced on Mac
|
||||
const gotCreate = new Deferred();
|
||||
const listener = fileService.onDidFilesChange(e => {
|
||||
if (e.contains(fileUri, { type: 1 })) { // FileChangeType.ADDED
|
||||
gotCreate.resolve();
|
||||
}
|
||||
});
|
||||
await fileService.create(fileUri, 'foo', { fromUserGesture: false, overwrite: true });
|
||||
await Promise.race([await timeout(2000), gotCreate.promise]);
|
||||
watcher.dispose();
|
||||
listener.dispose();
|
||||
|
||||
widget = /** @type {EditorWidget & SaveableWidget} */ (await editorManager.open(fileUri, { mode: 'reveal' }));
|
||||
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
toTearDown.dispose();
|
||||
// @ts-ignore
|
||||
editor = undefined;
|
||||
// @ts-ignore
|
||||
widget = undefined;
|
||||
await editorManager.closeAll({ save: false });
|
||||
await fileService.delete(fileUri.parent, { fromUserGesture: false, useTrash: false, recursive: true });
|
||||
await preferences.set('files.autoSave', autoSave, undefined, rootUri.toString());
|
||||
});
|
||||
|
||||
it('normal save', async function () {
|
||||
for (const edit of ['bar', 'baz']) {
|
||||
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty before '${edit}' edit`);
|
||||
editor.getControl().setValue(edit);
|
||||
assert.isTrue(Saveable.isDirty(widget), `should be dirty before '${edit}' save`);
|
||||
await Saveable.save(widget);
|
||||
assert.isFalse(Saveable.isDirty(widget), `should NOT be dirty after '${edit}' save`);
|
||||
assert.equal(editor.getControl().getValue().trimRight(), edit, `model should be updated with '${edit}'`);
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), edit, `fs should be updated with '${edit}'`);
|
||||
}
|
||||
});
|
||||
|
||||
it('reject save with incremental update', async function () {
|
||||
let longContent = 'foobarbaz';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
longContent += longContent + longContent;
|
||||
}
|
||||
editor.getControl().setValue(longContent);
|
||||
await Saveable.save(widget);
|
||||
|
||||
// @ts-ignore
|
||||
editor.getControl().getModel().applyEdits([{
|
||||
range: Range.fromPositions({ lineNumber: 1, column: 1 }, { lineNumber: 1, column: 4 }),
|
||||
forceMoveMarkers: false,
|
||||
text: ''
|
||||
}]);
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
||||
|
||||
const resource = editor.document['resource'];
|
||||
const version = resource.version;
|
||||
// @ts-ignore
|
||||
await resource.saveContents('baz');
|
||||
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
||||
|
||||
let outOfSync = false;
|
||||
let outOfSyncCount = 0;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
outOfSyncCount++;
|
||||
return false;
|
||||
}));
|
||||
|
||||
let incrementalUpdate = false;
|
||||
const saveContentChanges = resource.saveContentChanges;
|
||||
resource.saveContentChanges = async (changes, options) => {
|
||||
incrementalUpdate = true;
|
||||
// @ts-ignore
|
||||
return saveContentChanges.bind(resource)(changes, options);
|
||||
};
|
||||
try {
|
||||
await Saveable.save(widget);
|
||||
} finally {
|
||||
resource.saveContentChanges = saveContentChanges;
|
||||
}
|
||||
|
||||
assert.isTrue(incrementalUpdate, 'should tried to update incrementaly');
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.equal(outOfSyncCount, 1, 'user should be prompted only once with out of sync dialog');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
||||
assert.equal(editor.getControl().getValue().trimRight(), longContent.substring(3), 'model should be updated');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value, 'baz', 'fs should NOT be updated');
|
||||
});
|
||||
|
||||
it('accept rejected save', async function () {
|
||||
let outOfSync = false;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
return false;
|
||||
}));
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
||||
|
||||
const resource = editor.document['resource'];
|
||||
const version = resource.version;
|
||||
// @ts-ignore
|
||||
await resource.saveContents('bazz');
|
||||
assert.notEqual(version, resource.version, 'latest version should be different after write');
|
||||
|
||||
await Saveable.save(widget);
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after rejected save');
|
||||
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
||||
let state = await fileService.read(fileUri);
|
||||
assert.equal(state.value, 'bazz', 'fs should NOT be updated');
|
||||
|
||||
outOfSync = false;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
return true;
|
||||
}));
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
||||
await Saveable.save(widget);
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
||||
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
||||
state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
||||
});
|
||||
|
||||
it('accept new save', async () => {
|
||||
let outOfSync = false;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
return true;
|
||||
}));
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
||||
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
||||
await Saveable.save(widget);
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
||||
assert.equal(editor.getControl().getValue().trimRight(), 'bar', 'model should be updated');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
||||
});
|
||||
|
||||
it('cancel save on close', async () => {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before close');
|
||||
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => undefined
|
||||
});
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be still dirty after canceled close');
|
||||
assert.isFalse(widget.isDisposed, 'should NOT be disposed after canceled close');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value, 'foo', 'fs should NOT be updated after canceled close');
|
||||
});
|
||||
|
||||
it('reject save on close', async () => {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejected close');
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => false
|
||||
});
|
||||
assert.isTrue(widget.isDisposed, 'should be disposed after rejected close');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value, 'foo', 'fs should NOT be updated after rejected close');
|
||||
});
|
||||
|
||||
it('accept save on close and reject it', async () => {
|
||||
let outOfSync = false;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
return false;
|
||||
}));
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before rejecting save on close');
|
||||
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => true
|
||||
});
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.isFalse(widget.isDisposed, 'model should not be disposed after close when we reject the save');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value, 'foo2', 'fs should NOT be updated');
|
||||
});
|
||||
|
||||
it('accept save on close and accept new save', async () => {
|
||||
let outOfSync = false;
|
||||
toTearDown.push(setShouldOverwrite(async () => {
|
||||
outOfSync = true;
|
||||
return true;
|
||||
}));
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before accepting save on close');
|
||||
await fileService.write(fileUri, 'foo2', { etag: ETAG_DISABLED });
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => true
|
||||
});
|
||||
assert.isTrue(outOfSync, 'file should be out of sync');
|
||||
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
||||
});
|
||||
|
||||
it('no save prompt when multiple editors open for same file', async () => {
|
||||
const secondWidget = await editorManager.openToSide(fileUri);
|
||||
editor.getControl().setValue('two widgets');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'the first widget should be dirty');
|
||||
assert.isTrue(Saveable.isDirty(secondWidget), 'the second widget should also be dirty');
|
||||
await Promise.resolve(secondWidget.close());
|
||||
assert.isTrue(secondWidget.isDisposed, 'the widget should have closed without requesting user action');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'the original widget should still be dirty.');
|
||||
assert.equal(editor.getControl().getValue(), 'two widgets', 'should still have the same value');
|
||||
});
|
||||
|
||||
it('normal close', async () => {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before before close');
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => true
|
||||
});
|
||||
assert.isTrue(widget.isDisposed, 'model should be disposed after close');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
||||
});
|
||||
|
||||
it('delete and add again file for dirty', async () => {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before delete');
|
||||
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
||||
let waitForDidChangeTitle = new Deferred();
|
||||
const listener = () => waitForDidChangeTitle.resolve();
|
||||
widget.title.changed.connect(listener);
|
||||
try {
|
||||
await fileService.delete(fileUri);
|
||||
await waitForDidChangeTitle.promise;
|
||||
assert.isTrue(widget.title.label.endsWith('(Deleted)'), 'should be marked as deleted');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after delete');
|
||||
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after delete');
|
||||
} finally {
|
||||
widget.title.changed.disconnect(listener);
|
||||
}
|
||||
|
||||
waitForDidChangeTitle = new Deferred();
|
||||
widget.title.changed.connect(listener);
|
||||
try {
|
||||
await fileService.create(fileUri, 'foo');
|
||||
await waitForDidChangeTitle.promise;
|
||||
assert.isFalse(widget.title.label.endsWith('(deleted)'), 'should NOT be marked as deleted');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty after added again');
|
||||
assert.isFalse(widget.isDisposed, 'model should NOT be disposed after added again');
|
||||
} finally {
|
||||
widget.title.changed.disconnect(listener);
|
||||
}
|
||||
});
|
||||
|
||||
it('save deleted file for dirty', async function () {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save deleted');
|
||||
|
||||
assert.isTrue(editor.document.valid, 'should be valid before delete');
|
||||
const waitForInvalid = new Deferred();
|
||||
const listener = editor.document.onDidChangeValid(() => waitForInvalid.resolve());
|
||||
try {
|
||||
await fileService.delete(fileUri);
|
||||
await waitForInvalid.promise;
|
||||
assert.isFalse(editor.document.valid, 'should be invalid after delete');
|
||||
} finally {
|
||||
listener.dispose();
|
||||
}
|
||||
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before save');
|
||||
await Saveable.save(widget);
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty after save');
|
||||
assert.isTrue(editor.document.valid, 'should be valid after save');
|
||||
const state = await fileService.read(fileUri);
|
||||
assert.equal(state.value.trimRight(), 'bar', 'fs should be updated');
|
||||
});
|
||||
|
||||
it('move file for saved', async function () {
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before move');
|
||||
|
||||
const targetUri = fileUri.parent.resolve('bar.txt');
|
||||
await fileService.move(fileUri, targetUri, { overwrite: true });
|
||||
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
||||
|
||||
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
||||
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
||||
assert.equal(renamed.editor.document.getText(), 'foo', 'new model should be created after move');
|
||||
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after move');
|
||||
});
|
||||
|
||||
it('move file for dirty', async function () {
|
||||
editor.getControl().setValue('bar');
|
||||
assert.isTrue(Saveable.isDirty(widget), 'should be dirty before move');
|
||||
|
||||
const targetUri = fileUri.parent.resolve('bar.txt');
|
||||
|
||||
await fileService.move(fileUri, targetUri, { overwrite: true });
|
||||
assert.isTrue(widget.isDisposed, 'old model should be disposed after move');
|
||||
|
||||
const renamed = /** @type {EditorWidget} */ (await editorManager.getByUri(targetUri));
|
||||
assert.equal(String(renamed.getResourceUri()), targetUri.toString(), 'new model should be created after move');
|
||||
assert.equal(renamed.editor.document.getText(), 'bar', 'new model should be created after move');
|
||||
assert.isTrue(Saveable.isDirty(renamed), 'new model should be dirty after move');
|
||||
|
||||
await Saveable.save(renamed);
|
||||
assert.isFalse(Saveable.isDirty(renamed), 'new model should NOT be dirty after save');
|
||||
});
|
||||
|
||||
it('fail to open invalid file', async function () {
|
||||
const invalidFile = fileUri.parent.resolve('invalid_file.txt');
|
||||
try {
|
||||
await editorManager.open(invalidFile, { mode: 'reveal' });
|
||||
assert.fail('should not be possible to open an editor for invalid file');
|
||||
} catch (e) {
|
||||
assert.equal(e.code, 'MODEL_IS_INVALID');
|
||||
}
|
||||
});
|
||||
|
||||
it('decode without save', async function () {
|
||||
assert.strictEqual('utf8', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText());
|
||||
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
||||
assert.strictEqual('utf16le', editor.document.getEncoding());
|
||||
assert.notEqual('foo', editor.document.getText().trimRight());
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
||||
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => undefined
|
||||
});
|
||||
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
||||
|
||||
widget = /** @type {EditorWidget & SaveableWidget} */
|
||||
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
||||
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
||||
|
||||
assert.strictEqual('utf8', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText().trimRight());
|
||||
});
|
||||
|
||||
it('decode with save', async function () {
|
||||
assert.strictEqual('utf8', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText());
|
||||
await editor.setEncoding('utf16le', 1 /* EncodingMode.Decode */);
|
||||
assert.strictEqual('utf16le', editor.document.getEncoding());
|
||||
assert.notEqual('foo', editor.document.getText().trimRight());
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after decode');
|
||||
|
||||
await Saveable.save(widget);
|
||||
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => undefined
|
||||
});
|
||||
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
||||
|
||||
widget = /** @type {EditorWidget & SaveableWidget} */
|
||||
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
||||
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
||||
|
||||
assert.strictEqual('utf16le', editor.document.getEncoding());
|
||||
assert.notEqual('foo', editor.document.getText().trimRight());
|
||||
});
|
||||
|
||||
it('encode', async function () {
|
||||
assert.strictEqual('utf8', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText());
|
||||
await editor.setEncoding('utf16le', 0 /* EncodingMode.Encode */);
|
||||
assert.strictEqual('utf16le', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText().trimRight());
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should not be dirty after encode');
|
||||
|
||||
await widget.closeWithSaving({
|
||||
shouldSave: () => undefined
|
||||
});
|
||||
assert.isTrue(widget.isDisposed, 'widget should be disposed after close');
|
||||
|
||||
widget = /** @type {EditorWidget & SaveableWidget} */
|
||||
(await editorManager.open(fileUri, { mode: 'reveal' }));
|
||||
editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
||||
|
||||
assert.strictEqual('utf16le', editor.document.getEncoding());
|
||||
assert.strictEqual('foo', editor.document.getText().trimRight());
|
||||
});
|
||||
|
||||
it('delete file for saved', async () => {
|
||||
assert.isFalse(Saveable.isDirty(widget), 'should NOT be dirty before delete');
|
||||
const waitForDisposed = new Deferred();
|
||||
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
||||
try {
|
||||
await fileService.delete(fileUri);
|
||||
await waitForDisposed.promise;
|
||||
assert.isTrue(widget.isDisposed, 'model should be disposed after delete');
|
||||
} finally {
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it(`'${closeOnFileDelete}' should keep the editor opened when set to 'false'`, async () => {
|
||||
|
||||
await preferences.set(closeOnFileDelete, false);
|
||||
assert.isFalse(preferences.get(closeOnFileDelete));
|
||||
assert.isFalse(Saveable.isDirty(widget));
|
||||
|
||||
const waitForDidChangeTitle = new Deferred();
|
||||
const listener = () => waitForDidChangeTitle.resolve();
|
||||
widget.title.changed.connect(listener);
|
||||
try {
|
||||
await fileService.delete(fileUri);
|
||||
await waitForDidChangeTitle.promise;
|
||||
assert.isTrue(widget.title.label.endsWith('(Deleted)'));
|
||||
assert.isFalse(widget.isDisposed);
|
||||
} finally {
|
||||
widget.title.changed.disconnect(listener);
|
||||
}
|
||||
});
|
||||
|
||||
it(`'${closeOnFileDelete}' should close the editor when set to 'true'`, async () => {
|
||||
|
||||
await preferences.set(closeOnFileDelete, true);
|
||||
assert.isTrue(preferences.get(closeOnFileDelete));
|
||||
assert.isFalse(Saveable.isDirty(widget));
|
||||
|
||||
const waitForDisposed = new Deferred();
|
||||
// Must pass in 5 seconds, so check state after 4.5.
|
||||
const listener = editor.onDispose(() => waitForDisposed.resolve());
|
||||
const fourSeconds = new Promise(resolve => setTimeout(resolve, 4500));
|
||||
try {
|
||||
const deleteThenDispose = fileService.delete(fileUri).then(() => waitForDisposed.promise);
|
||||
await Promise.race([deleteThenDispose, fourSeconds]);
|
||||
assert.isTrue(widget.isDisposed);
|
||||
} finally {
|
||||
listener.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
222
examples/api-tests/src/scm.spec.js
Normal file
222
examples/api-tests/src/scm.spec.js
Normal file
@@ -0,0 +1,222 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
const { timeout } = require('@theia/core/lib/common/promise-util');
|
||||
|
||||
// @ts-check
|
||||
describe('SCM', function () {
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
const Uri = require('@theia/core/lib/common/uri');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
|
||||
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
|
||||
const { ScmService } = require('@theia/scm/lib/browser/scm-service');
|
||||
const { ScmWidget } = require('@theia/scm/lib/browser/scm-widget');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common');
|
||||
const { PreferenceService } = require('@theia/core/lib/browser');
|
||||
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
const contextKeyService = container.get(ContextKeyService);
|
||||
const scmContribution = container.get(ScmContribution);
|
||||
const shell = container.get(ApplicationShell);
|
||||
const service = container.get(ScmService);
|
||||
const commandRegistry = container.get(CommandRegistry);
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
const preferences = container.get(PreferenceService);
|
||||
|
||||
/** @type {ScmWidget} */
|
||||
let scmWidget;
|
||||
|
||||
/** @type {ScmService} */
|
||||
let scmService;
|
||||
|
||||
const gitPluginId = 'vscode.git';
|
||||
|
||||
/**
|
||||
* @param {() => unknown} condition
|
||||
* @param {number | undefined} [timeout]
|
||||
* @param {string | undefined} [message]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function waitForAnimation(condition, maxWait, message) {
|
||||
if (maxWait === undefined) {
|
||||
maxWait = 100000;
|
||||
}
|
||||
const endTime = Date.now() + maxWait;
|
||||
do {
|
||||
await (timeout(100));
|
||||
if (condition()) {
|
||||
return true;
|
||||
}
|
||||
if (Date.now() > endTime) {
|
||||
throw new Error(message ?? 'Wait for animation timed out.');
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
|
||||
before(async () => {
|
||||
preferences.set('git.autoRepositoryDetection', true);
|
||||
preferences.set('git.openRepositoryInParentFolders', 'always');
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
if (!pluginService.getPlugin(gitPluginId)) {
|
||||
throw new Error(gitPluginId + ' should be started');
|
||||
}
|
||||
await pluginService.activatePlugin(gitPluginId);
|
||||
await shell.leftPanelHandler.collapse();
|
||||
scmWidget = await scmContribution.openView({ activate: true, reveal: true });
|
||||
scmService = service;
|
||||
await waitForAnimation(() => scmService.selectedRepository, 10000, 'selected repository is not defined');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// @ts-ignore
|
||||
scmWidget = undefined;
|
||||
// @ts-ignore
|
||||
scmService = undefined;
|
||||
});
|
||||
|
||||
describe('scm-view', () => {
|
||||
it('the view should open and activate successfully', () => {
|
||||
assert.notEqual(scmWidget, undefined);
|
||||
assert.strictEqual(scmWidget, shell.activeWidget);
|
||||
});
|
||||
|
||||
describe('\'ScmTreeWidget\'', () => {
|
||||
|
||||
it('the view should display the resource tree when a repository is present', () => {
|
||||
assert.isTrue(scmWidget.resourceWidget.isVisible);
|
||||
});
|
||||
|
||||
it('the view should not display the resource tree when no repository is present', () => {
|
||||
|
||||
// Store the current selected repository so it can be restored.
|
||||
const cachedSelectedRepository = scmService.selectedRepository;
|
||||
|
||||
scmService.selectedRepository = undefined;
|
||||
assert.isFalse(scmWidget.resourceWidget.isVisible);
|
||||
|
||||
// Restore the selected repository.
|
||||
scmService.selectedRepository = cachedSelectedRepository;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('\'ScmNoRepositoryWidget\'', () => {
|
||||
|
||||
it('should not be visible when a repository is present', () => {
|
||||
assert.isFalse(scmWidget.noRepositoryWidget.isVisible);
|
||||
});
|
||||
|
||||
it('should be visible when no repository is present', () => {
|
||||
|
||||
// Store the current selected repository so it can be restored.
|
||||
const cachedSelectedRepository = scmService.selectedRepository;
|
||||
|
||||
scmService.selectedRepository = undefined;
|
||||
assert.isTrue(scmWidget.noRepositoryWidget.isVisible);
|
||||
|
||||
// Restore the selected repository.
|
||||
scmService.selectedRepository = cachedSelectedRepository;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('scm-service', () => {
|
||||
|
||||
it('should successfully return the list of repositories', () => {
|
||||
const repositories = scmService.repositories;
|
||||
assert.isTrue(repositories.length > 0);
|
||||
});
|
||||
|
||||
it('should include the selected repository in the list of repositories', () => {
|
||||
const repositories = scmService.repositories;
|
||||
const selectedRepository = scmService.selectedRepository;
|
||||
assert.isTrue(repositories.length === 1);
|
||||
assert.strictEqual(repositories[0], selectedRepository);
|
||||
});
|
||||
|
||||
it('should successfully return the selected repository', () => {
|
||||
assert.notEqual(scmService.selectedRepository, undefined);
|
||||
});
|
||||
|
||||
it('should successfully find the repository', () => {
|
||||
const selectedRepository = scmService.selectedRepository;
|
||||
if (selectedRepository) {
|
||||
const rootUri = selectedRepository.provider.rootUri;
|
||||
const foundRepository = scmService.findRepository(new Uri.default(rootUri));
|
||||
assert.notEqual(foundRepository, undefined);
|
||||
}
|
||||
else {
|
||||
assert.fail('Selected repository is undefined');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not find a repository for an unknown uri', () => {
|
||||
const mockUri = new Uri.default('foobar/foo/bar');
|
||||
const repo = scmService.findRepository(mockUri);
|
||||
assert.strictEqual(repo, undefined);
|
||||
});
|
||||
|
||||
it('should successfully return the list of statusbar commands', () => {
|
||||
assert.isTrue(scmService.statusBarCommands.length > 0);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('scm-provider', () => {
|
||||
|
||||
it('should successfully return the last commit', async () => {
|
||||
const selectedRepository = scmService.selectedRepository;
|
||||
if (selectedRepository) {
|
||||
const amendSupport = selectedRepository.provider.amendSupport;
|
||||
if (amendSupport) {
|
||||
const commit = await amendSupport.getLastCommit();
|
||||
assert.notEqual(commit, undefined);
|
||||
}
|
||||
}
|
||||
else {
|
||||
assert.fail('Selected repository is undefined');
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('scm-contribution', () => {
|
||||
|
||||
describe('scmFocus context-key', () => {
|
||||
|
||||
it('should return \'true\' when the view is focused', () => {
|
||||
assert.isTrue(contextKeyService.match('scmFocus'));
|
||||
});
|
||||
|
||||
it('should return \'false\' when the view is not focused', async () => {
|
||||
await scmContribution.closeView();
|
||||
assert.isFalse(contextKeyService.match('scmFocus'));
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
41
examples/api-tests/src/shell.spec.js
Normal file
41
examples/api-tests/src/shell.spec.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('Shell', function () {
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { StatusBarImpl } = require('@theia/core/lib/browser/status-bar');
|
||||
|
||||
const container = window.theia.container;
|
||||
const shell = container.get(ApplicationShell);
|
||||
const statusBar = container.get(StatusBarImpl);
|
||||
|
||||
it('should be shown', () => {
|
||||
assert.isTrue(shell.isAttached && shell.isVisible);
|
||||
});
|
||||
|
||||
it('should show the main content panel', () => {
|
||||
assert.isTrue(shell.mainPanel.isAttached && shell.mainPanel.isVisible);
|
||||
});
|
||||
|
||||
it('should show the status bar', () => {
|
||||
assert.isTrue(statusBar.isAttached && statusBar.isVisible);
|
||||
});
|
||||
|
||||
});
|
||||
112
examples/api-tests/src/task-configurations.spec.js
Normal file
112
examples/api-tests/src/task-configurations.spec.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
|
||||
describe('The Task Configuration Manager', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { TaskScope, TaskConfigurationScope } = require('@theia/task/lib/common/task-protocol');
|
||||
const { TaskConfigurationManager } = require('@theia/task/lib/browser/task-configuration-manager');
|
||||
const container = window.theia.container;
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const taskConfigurationManager = container.get(TaskConfigurationManager);
|
||||
|
||||
const baseWorkspaceURI = workspaceService.tryGetRoots()[0].resource;
|
||||
const baseWorkspaceRoot = baseWorkspaceURI.toString();
|
||||
|
||||
const basicTaskConfig = {
|
||||
label: 'task',
|
||||
type: 'shell',
|
||||
command: 'top',
|
||||
};
|
||||
|
||||
/** @type {Set<TaskConfigurationScope>} */
|
||||
const scopesToClear = new Set();
|
||||
|
||||
describe('in a single-root workspace', () => {
|
||||
beforeEach(() => clearTasks());
|
||||
after(() => clearTasks());
|
||||
|
||||
setAndRetrieveTasks(() => TaskScope.Global, 'user');
|
||||
setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
|
||||
setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder');
|
||||
});
|
||||
|
||||
async function clearTasks() {
|
||||
await Promise.all(Array.from(scopesToClear, async scope => {
|
||||
if (!!scope || scope === 0) {
|
||||
await taskConfigurationManager.setTaskConfigurations(scope, []);
|
||||
}
|
||||
}));
|
||||
scopesToClear.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => TaskConfigurationScope} scopeGenerator a function to allow lazy evaluation of the second workspace root.
|
||||
* @param {string} scopeLabel
|
||||
* @param {boolean} only
|
||||
*/
|
||||
function setAndRetrieveTasks(scopeGenerator, scopeLabel, only = false) {
|
||||
const testFunction = only ? it.only : it;
|
||||
testFunction(`successfully handles ${scopeLabel} scope`, async () => {
|
||||
const scope = scopeGenerator();
|
||||
scopesToClear.add(scope);
|
||||
const initialTasks = taskConfigurationManager.getTasks(scope);
|
||||
assert.deepEqual(initialTasks, []);
|
||||
await taskConfigurationManager.setTaskConfigurations(scope, [basicTaskConfig]);
|
||||
const newTasks = taskConfigurationManager.getTasks(scope);
|
||||
assert.deepEqual(newTasks, [basicTaskConfig]);
|
||||
});
|
||||
}
|
||||
|
||||
/* UNCOMMENT TO RUN MULTI-ROOT TESTS */
|
||||
// const { FileService } = require('@theia/filesystem/lib/browser/file-service');
|
||||
// const { EnvVariablesServer } = require('@theia/core/lib/common/env-variables');
|
||||
// const URI = require('@theia/core/lib/common/uri').default;
|
||||
|
||||
// const fileService = container.get(FileService);
|
||||
// /** @type {EnvVariablesServer} */
|
||||
// const envVariables = container.get(EnvVariablesServer);
|
||||
|
||||
// describe('in a multi-root workspace', () => {
|
||||
// let secondWorkspaceRoot = '';
|
||||
// before(async () => {
|
||||
// const configLocation = await envVariables.getConfigDirUri();
|
||||
// const secondWorkspaceRootURI = new URI(configLocation).parent.resolve(`test-root-${Date.now()}`);
|
||||
// secondWorkspaceRoot = secondWorkspaceRootURI.toString();
|
||||
// await fileService.createFolder(secondWorkspaceRootURI);
|
||||
// /** @type {Promise<void>} */
|
||||
// const waitForEvent = new Promise(resolve => {
|
||||
// const listener = taskConfigurationManager.onDidChangeTaskConfig(() => {
|
||||
// listener.dispose();
|
||||
// resolve();
|
||||
// });
|
||||
// });
|
||||
// workspaceService.addRoot(secondWorkspaceRootURI);
|
||||
// return waitForEvent;
|
||||
// });
|
||||
// beforeEach(() => clearTasks());
|
||||
// after(() => clearTasks());
|
||||
// setAndRetrieveTasks(() => TaskScope.Global, 'user');
|
||||
// setAndRetrieveTasks(() => TaskScope.Workspace, 'workspace');
|
||||
// setAndRetrieveTasks(() => baseWorkspaceRoot, 'folder (1)');
|
||||
// setAndRetrieveTasks(() => secondWorkspaceRoot, 'folder (2)');
|
||||
// });
|
||||
});
|
||||
873
examples/api-tests/src/typescript.spec.js
Normal file
873
examples/api-tests/src/typescript.spec.js
Normal file
@@ -0,0 +1,873 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
// @ts-check
|
||||
describe('TypeScript', function () {
|
||||
this.timeout(360_000);
|
||||
|
||||
const { assert } = chai;
|
||||
const { timeout } = require('@theia/core/lib/common/promise-util');
|
||||
const { MenuModelRegistry } = require('@theia/core/lib/common/menu/menu-model-registry');
|
||||
|
||||
const Uri = require('@theia/core/lib/common/uri');
|
||||
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { BrowserMainMenuFactory } = require('@theia/core/lib/browser/menu/browser-menu-plugin');
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { EditorWidget } = require('@theia/editor/lib/browser/editor-widget');
|
||||
const { EDITOR_CONTEXT_MENU } = require('@theia/editor/lib/browser/editor-menu');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
const { ContextKeyService } = require('@theia/core/lib/browser/context-key-service');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
|
||||
const { OpenerService, open } = require('@theia/core/lib/browser/opener-service');
|
||||
const { PreferenceService } = require('@theia/core/lib/common/preferences/preference-service');
|
||||
const { PreferenceScope } = require('@theia/core/lib/common/preferences/preference-scope');
|
||||
const { ProgressStatusBarItem } = require('@theia/core/lib/browser/progress-status-bar-item');
|
||||
const { PluginViewRegistry } = require('@theia/plugin-ext/lib/main/browser/view/plugin-view-registry');
|
||||
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
|
||||
const { Selection } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/selection');
|
||||
|
||||
const container = window.theia.container;
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const menuFactory = container.get(BrowserMainMenuFactory);
|
||||
const menuRegistry = container.get(MenuModelRegistry);
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
const contextKeyService = container.get(ContextKeyService);
|
||||
const commands = container.get(CommandRegistry);
|
||||
const openerService = container.get(OpenerService);
|
||||
/** @type {KeybindingRegistry} */
|
||||
const keybindings = container.get(KeybindingRegistry);
|
||||
/** @type {import('@theia/core/lib/common/preferences/preference-service').PreferenceService} */
|
||||
const preferences = container.get(PreferenceService);
|
||||
const progressStatusBarItem = container.get(ProgressStatusBarItem);
|
||||
/** @type {PluginViewRegistry} */
|
||||
const pluginViewRegistry = container.get(PluginViewRegistry);
|
||||
|
||||
const typescriptPluginId = 'vscode.typescript-language-features';
|
||||
const referencesPluginId = 'vscode.references-view';
|
||||
/** @type Uri.URI */
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const demoFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-file.ts');
|
||||
const definitionFileUri = rootUri.resolveToAbsolute('../api-tests/test-ts-workspace/demo-definitions-file.ts');
|
||||
let originalAutoSaveValue = preferences.get('files.autoSave');
|
||||
|
||||
before(async function () {
|
||||
await pluginService.didStart;
|
||||
await Promise.all([typescriptPluginId, referencesPluginId].map(async pluginId => {
|
||||
if (!pluginService.getPlugin(pluginId)) {
|
||||
throw new Error(pluginId + ' should be started');
|
||||
}
|
||||
await pluginService.activatePlugin(pluginId);
|
||||
}));
|
||||
await preferences.set('files.autoSave', 'off');
|
||||
await preferences.set('files.refactoring.autoSave', 'off');
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await editorManager.closeAll({ save: false });
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
afterEach(async () => {
|
||||
toTearDown.dispose();
|
||||
await editorManager.closeAll({ save: false });
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await preferences.set('files.autoSave', originalAutoSaveValue);
|
||||
})
|
||||
|
||||
async function waitLanguageServerReady() {
|
||||
// quite a bit of jitter in the "Initializing LS" status bar entry,
|
||||
// so we want to read a few times in a row that it's done (undefined)
|
||||
const MAX_N = 5
|
||||
let n = MAX_N;
|
||||
while (n > 0) {
|
||||
await timeout(1000);
|
||||
if (progressStatusBarItem.currentProgress) {
|
||||
n = MAX_N;
|
||||
} else {
|
||||
n--;
|
||||
}
|
||||
if (n < 5) {
|
||||
console.debug('n = ' + n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uri.default} uri
|
||||
* @param {boolean} preview
|
||||
*/
|
||||
async function openEditor(uri, preview = false) {
|
||||
const widget = await open(openerService, uri, { mode: 'activate', preview });
|
||||
const editorWidget = widget instanceof EditorWidget ? widget : undefined;
|
||||
const editor = MonacoEditor.get(editorWidget);
|
||||
assert.isDefined(editor);
|
||||
// wait till tsserver is running, see:
|
||||
// https://github.com/microsoft/vscode/blob/93cbbc5cae50e9f5f5046343c751b6d010468200/extensions/typescript-language-features/src/extension.ts#L98-L103
|
||||
await waitForAnimation(() => contextKeyService.match('typescript.isManagedFile'), 1000000, 'waiting for "typescript.isManagedFile"');
|
||||
|
||||
waitLanguageServerReady();
|
||||
return /** @type {MonacoEditor} */ (editor);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {() => unknown} condition
|
||||
* @param {number | undefined} [maxWait]
|
||||
* @param {string | function | undefined} [message]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function waitForAnimation(condition, maxWait, message) {
|
||||
if (maxWait === undefined) {
|
||||
maxWait = 100000;
|
||||
}
|
||||
const endTime = Date.now() + maxWait;
|
||||
do {
|
||||
await (timeout(100));
|
||||
if (condition()) {
|
||||
return;
|
||||
}
|
||||
if (Date.now() > endTime) {
|
||||
throw new Error((typeof message === 'function' ? message() : message) ?? 'Wait for animation timed out.');
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
/**
|
||||
* We ignore attributes on purpose since they are not stable.
|
||||
* But structure is important for us to see whether the plain text is rendered or markdown.
|
||||
*
|
||||
* @param {Element} element
|
||||
* @returns {string}
|
||||
*/
|
||||
function nodeAsString(element, indentation = '') {
|
||||
if (!element) {
|
||||
return '';
|
||||
}
|
||||
const header = element.tagName;
|
||||
let body = '';
|
||||
const childIndentation = indentation + ' ';
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const childNode = element.childNodes.item(i);
|
||||
if (childNode.nodeType === childNode.TEXT_NODE) {
|
||||
body += childIndentation + `"${childNode.textContent}"` + '\n';
|
||||
} else if (childNode instanceof HTMLElement) {
|
||||
body += childIndentation + nodeAsString(childNode, childIndentation) + '\n';
|
||||
}
|
||||
}
|
||||
const result = header + (body ? ' {\n' + body + indentation + '}' : '');
|
||||
if (indentation) {
|
||||
return result;
|
||||
}
|
||||
return `\n${result}\n`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MonacoEditor} editor
|
||||
*/
|
||||
async function assertPeekOpened(editor) {
|
||||
/** @type any */
|
||||
const referencesController = editor.getControl().getContribution('editor.contrib.referencesController');
|
||||
await waitForAnimation(() => referencesController._widget && referencesController._widget._tree.getFocus().length);
|
||||
|
||||
assert.isFalse(contextKeyService.match('editorTextFocus'));
|
||||
assert.isTrue(contextKeyService.match('referenceSearchVisible'));
|
||||
assert.isTrue(contextKeyService.match('listFocus'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MonacoEditor} editor
|
||||
*/
|
||||
async function openPeek(editor) {
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('referenceSearchVisible'));
|
||||
assert.isFalse(contextKeyService.match('listFocus'));
|
||||
|
||||
await commands.executeCommand('editor.action.peekDefinition');
|
||||
await assertPeekOpened(editor);
|
||||
}
|
||||
|
||||
async function openReference() {
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
await waitForAnimation(() => contextKeyService.match('listFocus'));
|
||||
assert.isFalse(contextKeyService.match('editorTextFocus'));
|
||||
assert.isTrue(contextKeyService.match('referenceSearchVisible'));
|
||||
assert.isTrue(contextKeyService.match('listFocus'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MonacoEditor} editor
|
||||
*/
|
||||
async function closePeek(editor) {
|
||||
await assertPeekOpened(editor);
|
||||
|
||||
console.log('closePeek() - Attempt to close by sending "Escape"');
|
||||
await dismissWithEscape('listFocus');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('referenceSearchVisible'));
|
||||
assert.isFalse(contextKeyService.match('listFocus'));
|
||||
}
|
||||
|
||||
it('document formatting should be visible and enabled', async function () {
|
||||
await openEditor(demoFileUri);
|
||||
const menu = menuFactory.createContextMenu(EDITOR_CONTEXT_MENU, menuRegistry.getMenu(EDITOR_CONTEXT_MENU), contextKeyService);
|
||||
const item = menu.items.find(i => i.command === 'editor.action.formatDocument');
|
||||
if (item) {
|
||||
assert.isTrue(item.isVisible, 'item is visible');
|
||||
assert.isTrue(item.isEnabled, 'item is enabled');
|
||||
} else {
|
||||
assert.isDefined(item, 'item is defined');
|
||||
}
|
||||
});
|
||||
|
||||
describe('editor.action.revealDefinition', function () {
|
||||
for (const preview of [false, true]) {
|
||||
const from = 'an editor' + (preview ? ' preview' : '');
|
||||
it('within ' + from, async function () {
|
||||
const editor = await openEditor(demoFileUri, preview);
|
||||
// const demoInstance = new Demo|Class('demo');
|
||||
editor.getControl().setPosition({ lineNumber: 28, column: 5 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoVariable');
|
||||
|
||||
await commands.executeCommand('editor.action.revealDefinition');
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(editorManager.activeEditor.isPreview, preview);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// constructor(someString: string) {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 7 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'demoVariable');
|
||||
});
|
||||
|
||||
// Note: this test generate annoying but apparently harmless error traces, during cleanup:
|
||||
// [Error: Error: Cannot update an unmounted root.
|
||||
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
|
||||
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
|
||||
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
|
||||
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
|
||||
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
|
||||
it(`from ${from} to another editor`, async function () {
|
||||
await editorManager.open(definitionFileUri, { mode: 'open' });
|
||||
|
||||
const editor = await openEditor(demoFileUri, preview);
|
||||
// const bar: Defined|Interface = { coolField: [] };
|
||||
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
|
||||
|
||||
await commands.executeCommand('editor.action.revealDefinition');
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isFalse(editorManager.activeEditor.isPreview);
|
||||
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
|
||||
|
||||
// export interface |DefinedInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
|
||||
});
|
||||
|
||||
it(`from ${from} to an editor preview`, async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const bar: Defined|Interface = { coolField: [] };
|
||||
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
|
||||
|
||||
await commands.executeCommand('editor.action.revealDefinition');
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isTrue(editorManager.activeEditor.isPreview);
|
||||
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
|
||||
// export interface |DefinedInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('editor.action.peekDefinition', function () {
|
||||
|
||||
for (const preview of [false, true]) {
|
||||
const from = 'an editor' + (preview ? ' preview' : '');
|
||||
it('within ' + from, async function () {
|
||||
const editor = await openEditor(demoFileUri, preview);
|
||||
editor.getControl().revealLine(24);
|
||||
// const demoInstance = new Demo|Class('demo');
|
||||
editor.getControl().setPosition({ lineNumber: 24, column: 30 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
|
||||
|
||||
await openPeek(editor);
|
||||
await openReference();
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(editorManager.activeEditor.isPreview, preview);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// constructor(someString: string) {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 11, column: 5 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'constructor');
|
||||
|
||||
await closePeek(activeEditor);
|
||||
});
|
||||
|
||||
// Note: this test generate annoying but apparently harmless error traces, during cleanup:
|
||||
// [Error: Error: Cannot update an unmounted root.
|
||||
// at ReactDOMRoot.__webpack_modules__.../../node_modules/react-dom/cjs/react-dom.development.js.ReactDOMHydrationRoot.render.ReactDOMRoot.render (http://127.0.0.1:3000/bundle.js:92757:11)
|
||||
// at BreadcrumbsRenderer.render (http://127.0.0.1:3000/bundle.js:137316:23)
|
||||
// at BreadcrumbsRenderer.update (http://127.0.0.1:3000/bundle.js:108722:14)
|
||||
// at BreadcrumbsRenderer.refresh (http://127.0.0.1:3000/bundle.js:108719:14)
|
||||
// at async ToolbarAwareTabBar.updateBreadcrumbs (http://127.0.0.1:3000/bundle.js:128229:9)]
|
||||
it(`from ${from} to another editor`, async function () {
|
||||
await editorManager.open(definitionFileUri, { mode: 'open' });
|
||||
|
||||
const editor = await openEditor(demoFileUri, preview);
|
||||
editor.getControl().revealLine(32);
|
||||
// const bar: Defined|Interface = { coolField: [] };
|
||||
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
|
||||
|
||||
await openPeek(editor);
|
||||
await openReference();
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isFalse(editorManager.activeEditor.isPreview);
|
||||
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
|
||||
// export interface |DefinedInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
|
||||
|
||||
await closePeek(activeEditor);
|
||||
});
|
||||
|
||||
it(`from ${from} to an editor preview`, async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
editor.getControl().revealLine(32);
|
||||
// const bar: Defined|Interface = { coolField: [] };
|
||||
editor.getControl().setPosition({ lineNumber: 32, column: 19 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DefinedInterface');
|
||||
|
||||
await openPeek(editor);
|
||||
await openReference();
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.isTrue(editorManager.activeEditor.isPreview);
|
||||
assert.equal(activeEditor.uri.toString(), definitionFileUri.toString());
|
||||
// export interface |DefinedInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 2, column: 18 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DefinedInterface');
|
||||
|
||||
await closePeek(activeEditor);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('editor.action.triggerSuggest', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
editor.getControl().setPosition({ lineNumber: 26, column: 46 });
|
||||
editor.getControl().setSelection(new Selection(26, 46, 26, 35));
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'stringField');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
await commands.executeCommand('editor.action.triggerSuggest');
|
||||
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
|
||||
const suggestController = editor.getControl().getContribution('editor.contrib.suggestController');
|
||||
|
||||
waitForAnimation(() => {
|
||||
const content = suggestController ? nodeAsString(suggestController['_widget']?.['_value']?.['element']?.['domNode']) : '';
|
||||
return !content.includes('loading');
|
||||
});
|
||||
|
||||
// May need a couple extra "Enter" being sent for the suggest to be accepted
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
await waitForAnimation(() => {
|
||||
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible');
|
||||
if (!suggestWidgetDismissed) {
|
||||
console.log('Re-try accepting suggest using "Enter" key');
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, 20000, 'Suggest widget has not been dismissed despite attempts to accept suggestion');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// demoInstance.stringField;
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 46 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'doSomething');
|
||||
});
|
||||
|
||||
it('editor.action.triggerSuggest navigate', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// demoInstance.[|stringField];
|
||||
editor.getControl().setPosition({ lineNumber: 26, column: 46 });
|
||||
editor.getControl().setSelection(new Selection(26, 46, 26, 35));
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'stringField');
|
||||
|
||||
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/suggest/browser/suggestController').SuggestController} */
|
||||
const suggest = editor.getControl().getContribution('editor.contrib.suggestController');
|
||||
const getFocusedLabel = () => {
|
||||
const focusedItem = suggest.widget.value.getFocusedItem();
|
||||
return focusedItem && focusedItem.item.completion.label;
|
||||
};
|
||||
|
||||
assert.isUndefined(getFocusedLabel());
|
||||
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
await commands.executeCommand('editor.action.triggerSuggest');
|
||||
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'doSomething', 5000);
|
||||
|
||||
assert.equal(getFocusedLabel(), 'doSomething');
|
||||
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
keybindings.dispatchKeyDown('ArrowDown');
|
||||
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'numberField', 2000);
|
||||
|
||||
assert.equal(getFocusedLabel(), 'numberField');
|
||||
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
keybindings.dispatchKeyDown('ArrowUp');
|
||||
await waitForAnimation(() => contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === 'doSomething', 2000);
|
||||
|
||||
assert.equal(getFocusedLabel(), 'doSomething');
|
||||
assert.isTrue(contextKeyService.match('suggestWidgetVisible'));
|
||||
|
||||
keybindings.dispatchKeyDown('Escape');
|
||||
|
||||
// once in a while, a second "Escape" is needed to dismiss widget
|
||||
await waitForAnimation(() => {
|
||||
const suggestWidgetDismissed = !contextKeyService.match('suggestWidgetVisible') && getFocusedLabel() === undefined;
|
||||
if (!suggestWidgetDismissed) {
|
||||
console.log('Re-try to dismiss suggest using "Escape" key');
|
||||
keybindings.dispatchKeyDown('Escape');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, 5000, 'Suggest widget not dismissed');
|
||||
|
||||
assert.isUndefined(getFocusedLabel());
|
||||
assert.isFalse(contextKeyService.match('suggestWidgetVisible'));
|
||||
});
|
||||
|
||||
it('editor.action.rename', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const |demoVariable = demoInstance.stringField;
|
||||
editor.getControl().setPosition({ lineNumber: 26, column: 7 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoVariable');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('renameInputVisible'));
|
||||
|
||||
commands.executeCommand('editor.action.rename');
|
||||
await waitForAnimation(() => contextKeyService.match('renameInputVisible')
|
||||
&& document.activeElement instanceof HTMLInputElement
|
||||
&& document.activeElement.selectionEnd === 'demoVariable'.length);
|
||||
assert.isFalse(contextKeyService.match('editorTextFocus'));
|
||||
assert.isTrue(contextKeyService.match('renameInputVisible'));
|
||||
|
||||
const input = document.activeElement;
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
assert.fail('expected focused input, but: ' + input);
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = 'foo';
|
||||
keybindings.dispatchKeyDown('Enter', input);
|
||||
|
||||
// all rename edits should be grouped in one edit operation and applied in the same tick
|
||||
await new Promise(resolve => editor.getControl().onDidChangeModelContent(resolve));
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('renameInputVisible'));
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// const |foo = new Container();
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 26, column: 7 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber: 28, column: 1 }).word, 'foo');
|
||||
});
|
||||
|
||||
async function dismissWithEscape(contextKey) {
|
||||
keybindings.dispatchKeyDown('Escape');
|
||||
// once in a while, a second "Escape" is needed to dismiss widget
|
||||
return waitForAnimation(() => {
|
||||
const suggestWidgetDismissed = !contextKeyService.match(contextKey);
|
||||
if (!suggestWidgetDismissed) {
|
||||
console.log(`Re-try to dismiss ${contextKey} using "Escape" key`);
|
||||
keybindings.dispatchKeyDown('Escape');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, 5000, `${contextKey} widget not dismissed`);
|
||||
}
|
||||
|
||||
it('editor.action.triggerParameterHints', async function () {
|
||||
this.timeout(30000);
|
||||
console.log('start trigger parameter hint');
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const demoInstance = new DemoClass('|demo');
|
||||
editor.getControl().setPosition({ lineNumber: 24, column: 37 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, "demo");
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
|
||||
|
||||
await commands.executeCommand('editor.action.triggerParameterHints');
|
||||
console.log('trigger command');
|
||||
await waitForAnimation(() => contextKeyService.match('parameterHintsVisible'));
|
||||
console.log('context key matched');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isTrue(contextKeyService.match('parameterHintsVisible'));
|
||||
|
||||
await dismissWithEscape('parameterHintsVisible');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('parameterHintsVisible'));
|
||||
});
|
||||
|
||||
it('editor.action.showHover', async function () {
|
||||
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// class |DemoClass);
|
||||
editor.getControl().setPosition({ lineNumber: 8, column: 7 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
|
||||
|
||||
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/hover/browser/contentHoverController').ContentHoverController} */
|
||||
const hover = editor.getControl().getContribution('editor.contrib.contentHover');
|
||||
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(contextKeyService.match('editorHoverVisible'));
|
||||
await commands.executeCommand('editor.action.showHover');
|
||||
let doLog = true;
|
||||
await waitForAnimation(() => contextKeyService.match('editorHoverVisible'));
|
||||
assert.isTrue(contextKeyService.match('editorHoverVisible'));
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
|
||||
waitForAnimation(() => {
|
||||
const content = nodeAsString(hover['_contentWidget']?.['widget']?.['_hover']?.['contentsDomNode']);
|
||||
return !content.includes('loading');
|
||||
});
|
||||
|
||||
const content = nodeAsString(hover['_contentWidget']?.['widget']?.['_hover']?.['contentsDomNode']);
|
||||
|
||||
assert.isTrue(content.includes('class', 'did not include'));
|
||||
assert.isTrue(content.includes('DemoClass', 'did not include'));
|
||||
await dismissWithEscape('editorHoverVisible');
|
||||
assert.isTrue(contextKeyService.match('editorTextFocus'));
|
||||
assert.isFalse(Boolean(hover['_contentWidget']?.['_widget']?.['_visibleData']));
|
||||
});
|
||||
|
||||
it('highlight semantic (write) occurrences', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const |container = new Container();
|
||||
const lineNumber = 24;
|
||||
const column = 7;
|
||||
const endColumn = column + 'demoInstance'.length;
|
||||
|
||||
const hasWriteDecoration = () => {
|
||||
for (const decoration of editor.getControl().getModel().getLineDecorations(lineNumber)) {
|
||||
if (decoration.range.startColumn === column && decoration.range.endColumn === endColumn && decoration.options.className === 'wordHighlightStrong') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
assert.isFalse(hasWriteDecoration());
|
||||
|
||||
editor.getControl().setPosition({ lineNumber, column });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
|
||||
// highlight occurrences is not trigged on the explicit position change, so move a cursor as a user
|
||||
keybindings.dispatchKeyDown('ArrowRight');
|
||||
await waitForAnimation(() => hasWriteDecoration());
|
||||
|
||||
assert.isTrue(hasWriteDecoration());
|
||||
});
|
||||
|
||||
it('editor.action.goToImplementation', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const demoInstance = new Demo|Class('demo');
|
||||
editor.getControl().setPosition({ lineNumber: 24, column: 30 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'DemoClass');
|
||||
|
||||
await commands.executeCommand('editor.action.goToImplementation');
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// class |DemoClass implements DemoInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 8, column: 7 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DemoClass');
|
||||
});
|
||||
|
||||
it('editor.action.goToTypeDefinition', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
// const demoVariable = demo|Instance.stringField;
|
||||
editor.getControl().setPosition({ lineNumber: 26, column: 26 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
|
||||
|
||||
await commands.executeCommand('editor.action.goToTypeDefinition');
|
||||
|
||||
const activeEditor = /** @type {MonacoEditor} */ MonacoEditor.get(editorManager.activeEditor);
|
||||
assert.equal(activeEditor.uri.toString(), demoFileUri.toString());
|
||||
// class |DemoClass implements DemoInterface {
|
||||
const { lineNumber, column } = activeEditor.getControl().getPosition();
|
||||
assert.deepEqual({ lineNumber, column }, { lineNumber: 8, column: 7 });
|
||||
assert.equal(activeEditor.getControl().getModel().getWordAtPosition({ lineNumber, column }).word, 'DemoClass');
|
||||
});
|
||||
|
||||
it('run reference code lens', async function () {
|
||||
const preferenceName = 'typescript.referencesCodeLens.enabled';
|
||||
const globalValue = preferences.inspect(preferenceName).globalValue;
|
||||
toTearDown.push({ dispose: () => preferences.set(preferenceName, globalValue, PreferenceScope.User) });
|
||||
await preferences.set(preferenceName, false, PreferenceScope.User);
|
||||
|
||||
const editor = await openEditor(demoFileUri);
|
||||
|
||||
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codelens/browser/codelensController').CodeLensContribution} */
|
||||
const codeLens = editor.getControl().getContribution('css.editor.codeLens');
|
||||
const codeLensNode = () => codeLens['_lenses'][0]?.['_contentWidget']?.['_domNode'];
|
||||
const codeLensNodeVisible = () => {
|
||||
const n = codeLensNode();
|
||||
return !!n && n.style.visibility !== 'hidden';
|
||||
};
|
||||
|
||||
assert.isFalse(codeLensNodeVisible());
|
||||
|
||||
// |interface DemoInterface {
|
||||
const position = { lineNumber: 2, column: 1 };
|
||||
await preferences.set(preferenceName, true, PreferenceScope.User);
|
||||
|
||||
editor.getControl().revealPosition(position);
|
||||
await waitForAnimation(() => codeLensNodeVisible());
|
||||
|
||||
assert.isTrue(codeLensNodeVisible());
|
||||
const node = codeLensNode();
|
||||
assert.isDefined(node);
|
||||
assert.equal(nodeAsString(node), `
|
||||
SPAN {
|
||||
A {
|
||||
"1 reference"
|
||||
}
|
||||
}
|
||||
`);
|
||||
const link = node.getElementsByTagName('a').item(0);
|
||||
assert.isDefined(link);
|
||||
link.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
await assertPeekOpened(editor);
|
||||
await closePeek(editor);
|
||||
});
|
||||
|
||||
it('editor.action.quickFix', async function () {
|
||||
const column = 45;
|
||||
const lineNumber = 26;
|
||||
const editor = await openEditor(demoFileUri);
|
||||
const currentChar = () => editor.getControl().getModel().getLineContent(lineNumber).charAt(column - 1);
|
||||
|
||||
editor.getControl().getModel().applyEdits([{
|
||||
range: {
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
startColumn: 45,
|
||||
endColumn: 46
|
||||
},
|
||||
forceMoveMarkers: false,
|
||||
text: ''
|
||||
}]);
|
||||
editor.getControl().setPosition({ lineNumber, column });
|
||||
editor.getControl().revealPosition({ lineNumber, column });
|
||||
assert.equal(currentChar(), ';', 'Failed at assert 1');
|
||||
|
||||
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
|
||||
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
|
||||
const lightBulbNode = () => {
|
||||
const lightBulb = codeActionController['_lightBulbWidget'].rawValue;
|
||||
return lightBulb && lightBulb['_domNode'];
|
||||
};
|
||||
const lightBulbVisible = () => {
|
||||
const node = lightBulbNode();
|
||||
return !!node && node.style.visibility !== 'hidden';
|
||||
};
|
||||
|
||||
await timeout(1000); // quick fix is always available: need to wait for the error fix to become available.
|
||||
|
||||
await commands.executeCommand('editor.action.quickFix');
|
||||
const codeActionSelector = '.action-widget';
|
||||
assert.isFalse(!!document.querySelector(codeActionSelector), 'Failed at assert 3 - codeActionWidget should not be visible');
|
||||
|
||||
console.log('Waiting for Quick Fix widget to be visible');
|
||||
await waitForAnimation(() => {
|
||||
const quickFixWidgetVisible = !!document.querySelector(codeActionSelector);
|
||||
if (!quickFixWidgetVisible) {
|
||||
// console.log('...');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, 10000, 'Timed-out waiting for the QuickFix widget to appear');
|
||||
await timeout();
|
||||
|
||||
assert.isTrue(lightBulbVisible(), 'Failed at assert 4');
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
console.log('Waiting for confirmation that QuickFix has taken effect');
|
||||
|
||||
await waitForAnimation(() => currentChar() === 'd', 10000, 'Failed to detect expected selected char: "d"');
|
||||
assert.equal(currentChar(), 'd', 'Failed at assert 5');
|
||||
});
|
||||
|
||||
it('editor.action.formatDocument', async function () {
|
||||
const lineNumber = 5;
|
||||
const editor = await openEditor(demoFileUri);
|
||||
const originalLength = editor.getControl().getModel().getLineLength(lineNumber);
|
||||
|
||||
// doSomething(): number; --> doSomething() : number;
|
||||
editor.getControl().getModel().applyEdits([{
|
||||
range: Range.fromPositions({ lineNumber, column: 18 }, { lineNumber, column: 18 }),
|
||||
forceMoveMarkers: false,
|
||||
text: ' '
|
||||
}]);
|
||||
|
||||
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength + 1);
|
||||
|
||||
await commands.executeCommand('editor.action.formatDocument');
|
||||
|
||||
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength);
|
||||
});
|
||||
|
||||
it('editor.action.formatSelection', async function () {
|
||||
// doSomething(): number {
|
||||
const lineNumber = 15;
|
||||
const editor = await openEditor(demoFileUri);
|
||||
const originalLength /* 28 */ = editor.getControl().getModel().getLineLength(lineNumber);
|
||||
|
||||
// doSomething( ) : number {
|
||||
editor.getControl().getModel().applyEdits([{
|
||||
range: Range.fromPositions({ lineNumber, column: 17 }, { lineNumber, column: 18 }),
|
||||
forceMoveMarkers: false,
|
||||
text: ' ) '
|
||||
}]);
|
||||
|
||||
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength + 4);
|
||||
|
||||
// [const { Container }] = require('inversify');
|
||||
editor.getControl().setSelection({ startLineNumber: lineNumber, startColumn: 1, endLineNumber: lineNumber, endColumn: 32 });
|
||||
|
||||
await commands.executeCommand('editor.action.formatSelection');
|
||||
|
||||
// [const { Container }] = require('inversify');
|
||||
assert.equal(editor.getControl().getModel().getLineLength(lineNumber), originalLength);
|
||||
});
|
||||
|
||||
it('Can execute code actions', async function () {
|
||||
const editor = await openEditor(demoFileUri);
|
||||
/** @type {import('@theia/monaco-editor-core/src/vs/editor/contrib/codeAction/browser/codeActionController').CodeActionController} */
|
||||
const codeActionController = editor.getControl().getContribution('editor.contrib.codeActionController');
|
||||
const isActionAvailable = () => {
|
||||
const lightbulbVisibility = codeActionController['_lightBulbWidget'].rawValue?.['_domNode'].style.visibility;
|
||||
return lightbulbVisibility !== undefined && lightbulbVisibility !== 'hidden';
|
||||
}
|
||||
assert.strictEqual(editor.getControl().getModel().getLineContent(30), 'import { DefinedInterface } from "./demo-definitions-file";');
|
||||
editor.getControl().revealLine(30);
|
||||
editor.getControl().setSelection(new Selection(30, 1, 30, 60));
|
||||
await waitForAnimation(() => isActionAvailable(), 5000, 'No code action available. (1)');
|
||||
assert.isTrue(isActionAvailable());
|
||||
|
||||
await timeout(1000)
|
||||
|
||||
await commands.executeCommand('editor.action.quickFix');
|
||||
await waitForAnimation(() => {
|
||||
const elements = document.querySelector('.action-widget');
|
||||
return !!elements;
|
||||
}, 5000, 'No context menu appeared. (1)');
|
||||
|
||||
await timeout();
|
||||
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
|
||||
assert.isNotNull(editor.getControl());
|
||||
assert.isNotNull(editor.getControl().getModel());
|
||||
console.log(`content: ${editor.getControl().getModel().getLineContent(30)}`);
|
||||
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import * as demoDefinitionsFile from "./demo-definitions-file";', 5000, 'The namespace import did not take effect :' + editor.getControl().getModel().getLineContent(30));
|
||||
|
||||
// momentarily toggle selection, waiting for code action to become unavailable.
|
||||
// Without doing this, the call to the quickfix command would sometimes fail because of an
|
||||
// unexpected "no code action available" pop-up, which would trip the rest of the testcase
|
||||
editor.getControl().setSelection(new Selection(30, 1, 30, 1));
|
||||
console.log('waiting for code action to no longer be available');
|
||||
await waitForAnimation(() => {
|
||||
if (!isActionAvailable()) {
|
||||
return true;
|
||||
}
|
||||
editor.getControl().setSelection(new Selection(30, 1, 30, 1));
|
||||
console.log('...');
|
||||
return !isActionAvailable();
|
||||
}, 5000, 'Code action still available with no proper selection.');
|
||||
// re-establish selection
|
||||
editor.getControl().setSelection(new Selection(30, 1, 30, 64));
|
||||
console.log('waiting for code action to become available again');
|
||||
await waitForAnimation(() => {
|
||||
console.log('...');
|
||||
return isActionAvailable()
|
||||
}, 5000, 'No code action available. (2)');
|
||||
|
||||
// Change import back: https://github.com/eclipse-theia/theia/issues/11059
|
||||
await commands.executeCommand('editor.action.quickFix');
|
||||
await waitForAnimation(() => Boolean(document.querySelector('.context-view-pointerBlock')), 5000, 'No context menu appeared. (2)');
|
||||
await timeout();
|
||||
|
||||
keybindings.dispatchKeyDown('Enter');
|
||||
|
||||
assert.isNotNull(editor.getControl());
|
||||
assert.isNotNull(editor.getControl().getModel());
|
||||
await waitForAnimation(() => editor.getControl().getModel().getLineContent(30) === 'import { DefinedInterface } from "./demo-definitions-file";', 10000, () => 'The named import did not take effect.' + editor.getControl().getModel().getLineContent(30));
|
||||
});
|
||||
|
||||
for (const referenceViewCommand of ['references-view.find', 'references-view.findImplementations']) {
|
||||
it(referenceViewCommand, async function () {
|
||||
let steps = 0;
|
||||
const editor = await openEditor(demoFileUri);
|
||||
editor.getControl().setPosition({ lineNumber: 24, column: 11 });
|
||||
assert.equal(editor.getControl().getModel().getWordAtPosition(editor.getControl().getPosition()).word, 'demoInstance');
|
||||
await commands.executeCommand(referenceViewCommand);
|
||||
const view = await pluginViewRegistry.openView('references-view.tree', { reveal: true });
|
||||
const expectedMessage = referenceViewCommand === 'references-view.find' ? '2 results in 1 file' : '1 result in 1 file';
|
||||
const getResultText = () => view.node.getElementsByClassName('theia-TreeViewInfo').item(0)?.textContent;
|
||||
await waitForAnimation(() => getResultText() === expectedMessage, 5000);
|
||||
assert.equal(getResultText(), expectedMessage);
|
||||
});
|
||||
}
|
||||
});
|
||||
204
examples/api-tests/src/undo-redo-selectAll.spec.js
Normal file
204
examples/api-tests/src/undo-redo-selectAll.spec.js
Normal file
@@ -0,0 +1,204 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
|
||||
// @ts-check
|
||||
describe('Undo, Redo and Select All', function () {
|
||||
this.timeout(5000);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { timeout } = require('@theia/core/lib/common/promise-util');
|
||||
const { DisposableCollection } = require('@theia/core/lib/common/disposable');
|
||||
const { CommonCommands } = require('@theia/core/lib/browser/common-frontend-contribution');
|
||||
const { EditorManager } = require('@theia/editor/lib/browser/editor-manager');
|
||||
const { WorkspaceService } = require('@theia/workspace/lib/browser/workspace-service');
|
||||
const { CommandRegistry } = require('@theia/core/lib/common/command');
|
||||
const { KeybindingRegistry } = require('@theia/core/lib/browser/keybinding');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { MonacoEditor } = require('@theia/monaco/lib/browser/monaco-editor');
|
||||
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
|
||||
const { Range } = require('@theia/monaco-editor-core/esm/vs/editor/common/core/range');
|
||||
const { PreferenceService, PreferenceScope } = require('@theia/core/lib/browser');
|
||||
|
||||
const container = window.theia.container;
|
||||
const editorManager = container.get(EditorManager);
|
||||
const workspaceService = container.get(WorkspaceService);
|
||||
const commands = container.get(CommandRegistry);
|
||||
const keybindings = container.get(KeybindingRegistry);
|
||||
const navigatorContribution = container.get(FileNavigatorContribution);
|
||||
const shell = container.get(ApplicationShell);
|
||||
const scmContribution = container.get(ScmContribution);
|
||||
/** @type {PreferenceService} */
|
||||
const preferenceService = container.get(PreferenceService)
|
||||
|
||||
const rootUri = workspaceService.tryGetRoots()[0].resource;
|
||||
const fileUri = rootUri.resolve('webpack.config.js');
|
||||
|
||||
const toTearDown = new DisposableCollection();
|
||||
|
||||
/**
|
||||
* @param {() => unknown} condition
|
||||
* @param {number | undefined} [maxWait]
|
||||
* @param {string | undefined} [message]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function waitForAnimation(condition, maxWait, message) {
|
||||
if (maxWait === undefined) {
|
||||
maxWait = 100000;
|
||||
}
|
||||
const endTime = Date.now() + maxWait;
|
||||
do {
|
||||
await (timeout(100));
|
||||
if (condition()) {
|
||||
return true;
|
||||
}
|
||||
if (Date.now() > endTime) {
|
||||
throw new reject(new Error(message ?? 'Wait for animation timed out.'));
|
||||
}
|
||||
} while (true);
|
||||
}
|
||||
|
||||
const originalValue = preferenceService.get('files.autoSave', undefined, rootUri.toString());
|
||||
before(async () => {
|
||||
await preferenceService.set('files.autoSave', 'off', undefined, rootUri.toString());
|
||||
await preferenceService.set('git.autoRepositoryDetection', true);
|
||||
await preferenceService.set('git.openRepositoryInParentFolders', 'always');
|
||||
shell.leftPanelHandler.collapse();
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
await scmContribution.closeView();
|
||||
await navigatorContribution.closeView();
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
toTearDown.dispose();
|
||||
await scmContribution.closeView();
|
||||
await navigatorContribution.closeView();
|
||||
await editorManager.closeAll({ save: false });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await preferenceService.set('files.autoSave', originalValue, undefined, rootUri.toString());
|
||||
shell.leftPanelHandler.collapse();
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@theia/editor/lib/browser/editor-widget').EditorWidget} widget
|
||||
*/
|
||||
async function assertInEditor(widget) {
|
||||
const originalContent = widget.editor.document.getText();
|
||||
const editor = /** @type {MonacoEditor} */ (MonacoEditor.get(widget));
|
||||
editor.getControl().pushUndoStop();
|
||||
editor.getControl().executeEdits('test', [{
|
||||
range: new Range(1, 1, 1, 1),
|
||||
text: 'A'
|
||||
}]);
|
||||
editor.getControl().pushUndoStop();
|
||||
|
||||
const modifiedContent = widget.editor.document.getText();
|
||||
assert.notEqual(modifiedContent, originalContent);
|
||||
|
||||
keybindings.dispatchCommand(CommonCommands.UNDO.id);
|
||||
await waitForAnimation(() => widget.editor.document.getText() === originalContent);
|
||||
assert.equal(widget.editor.document.getText(), originalContent);
|
||||
|
||||
keybindings.dispatchCommand(CommonCommands.REDO.id);
|
||||
await waitForAnimation(() => widget.editor.document.getText() === modifiedContent);
|
||||
assert.equal(widget.editor.document.getText(), modifiedContent);
|
||||
|
||||
const originalSelection = widget.editor.selection;
|
||||
keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id);
|
||||
await waitForAnimation(() => widget.editor.selection.end.line !== originalSelection.end.line);
|
||||
assert.notDeepEqual(widget.editor.selection, originalSelection);
|
||||
}
|
||||
|
||||
it('in the active editor', async function () {
|
||||
await navigatorContribution.openView({ activate: true });
|
||||
|
||||
const widget = await editorManager.open(fileUri, { mode: 'activate' });
|
||||
await assertInEditor(widget);
|
||||
});
|
||||
|
||||
it('in the active explorer without the current editor', async function () {
|
||||
await navigatorContribution.openView({ activate: true });
|
||||
|
||||
// should not throw
|
||||
await commands.executeCommand(CommonCommands.UNDO.id);
|
||||
await commands.executeCommand(CommonCommands.REDO.id);
|
||||
await commands.executeCommand(CommonCommands.SELECT_ALL.id);
|
||||
});
|
||||
|
||||
it('in the active explorer with the current editor', async function () {
|
||||
const widget = await editorManager.open(fileUri, { mode: 'activate' });
|
||||
|
||||
await navigatorContribution.openView({ activate: true });
|
||||
|
||||
await assertInEditor(widget);
|
||||
});
|
||||
|
||||
async function assertInScm() {
|
||||
const scmInput = document.activeElement;
|
||||
if (!(scmInput instanceof HTMLTextAreaElement)) {
|
||||
assert.isTrue(scmInput instanceof HTMLTextAreaElement);
|
||||
return;
|
||||
}
|
||||
|
||||
const originalValue = scmInput.value;
|
||||
document.execCommand('insertText', false, 'A');
|
||||
await waitForAnimation(() => scmInput.value !== originalValue);
|
||||
const modifiedValue = scmInput.value;
|
||||
assert.notEqual(originalValue, modifiedValue);
|
||||
|
||||
keybindings.dispatchCommand(CommonCommands.UNDO.id);
|
||||
await waitForAnimation(() => scmInput.value === originalValue);
|
||||
assert.equal(scmInput.value, originalValue, 'value equal');
|
||||
|
||||
keybindings.dispatchCommand(CommonCommands.REDO.id);
|
||||
await waitForAnimation(() => scmInput.value === modifiedValue);
|
||||
assert.equal(scmInput.value, modifiedValue, 'value not equal');
|
||||
|
||||
const selection = document.getSelection();
|
||||
if (!selection) {
|
||||
assert.isDefined(selection, 'selection defined');
|
||||
return;
|
||||
}
|
||||
|
||||
selection.empty();
|
||||
assert.equal(selection.rangeCount, 0, 'rangeCount equal');
|
||||
|
||||
keybindings.dispatchCommand(CommonCommands.SELECT_ALL.id);
|
||||
await waitForAnimation(() => !!selection.rangeCount);
|
||||
assert.notEqual(selection.rangeCount, 0, 'rangeCount not equal');
|
||||
assert.isTrue(selection.containsNode(scmInput), 'selection contains');
|
||||
}
|
||||
|
||||
it('in the active scm in workspace without the current editor', async function () {
|
||||
await scmContribution.openView({ activate: true });
|
||||
await assertInScm();
|
||||
});
|
||||
|
||||
it('in the active scm in workspace with the current editor', async function () {
|
||||
await editorManager.open(fileUri, { mode: 'activate' });
|
||||
|
||||
await scmContribution.openView({ activate: true });
|
||||
await assertInScm();
|
||||
});
|
||||
|
||||
});
|
||||
80
examples/api-tests/src/views.spec.js
Normal file
80
examples/api-tests/src/views.spec.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
|
||||
// @ts-check
|
||||
describe('Views', function () {
|
||||
this.timeout(7500);
|
||||
|
||||
const { assert } = chai;
|
||||
|
||||
const { timeout } = require('@theia/core/lib/common/promise-util');
|
||||
const { ApplicationShell } = require('@theia/core/lib/browser/shell/application-shell');
|
||||
const { FileNavigatorContribution } = require('@theia/navigator/lib/browser/navigator-contribution');
|
||||
const { ScmContribution } = require('@theia/scm/lib/browser/scm-contribution');
|
||||
const { OutlineViewContribution } = require('@theia/outline-view/lib/browser/outline-view-contribution');
|
||||
const { ProblemContribution } = require('@theia/markers/lib/browser/problem/problem-contribution');
|
||||
const { PropertyViewContribution } = require('@theia/property-view/lib/browser/property-view-contribution');
|
||||
const { HostedPluginSupport } = require('@theia/plugin-ext/lib/hosted/browser/hosted-plugin');
|
||||
|
||||
/** @type {import('inversify').Container} */
|
||||
const container = window['theia'].container;
|
||||
const shell = container.get(ApplicationShell);
|
||||
const navigatorContribution = container.get(FileNavigatorContribution);
|
||||
const scmContribution = container.get(ScmContribution);
|
||||
const outlineContribution = container.get(OutlineViewContribution);
|
||||
const problemContribution = container.get(ProblemContribution);
|
||||
const propertyViewContribution = container.get(PropertyViewContribution);
|
||||
const pluginService = container.get(HostedPluginSupport);
|
||||
|
||||
before(() => Promise.all([
|
||||
shell.leftPanelHandler.collapse(),
|
||||
(async function () {
|
||||
await pluginService.didStart;
|
||||
await pluginService.activateByViewContainer('explorer');
|
||||
})()
|
||||
]));
|
||||
|
||||
for (const contribution of [navigatorContribution, scmContribution, outlineContribution, problemContribution, propertyViewContribution]) {
|
||||
it(`should toggle ${contribution.viewLabel}`, async function () {
|
||||
let view = await contribution.closeView();
|
||||
if (view) {
|
||||
assert.notEqual(shell.getAreaFor(view), contribution.defaultViewOptions.area);
|
||||
assert.isFalse(view.isVisible);
|
||||
assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
|
||||
}
|
||||
|
||||
view = await contribution.toggleView();
|
||||
// we can't use "equals" here because Mocha chokes on the diff for certain widgets
|
||||
assert.isTrue(view !== undefined, `${contribution.viewLabel} !== undefined`);
|
||||
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
|
||||
assert.isDefined(shell.getTabBarFor(view));
|
||||
// @ts-ignore
|
||||
assert.equal(shell.getAreaFor(shell.getTabBarFor(view)), contribution.defaultViewOptions.area);
|
||||
assert.isTrue(view.isVisible);
|
||||
assert.isTrue(view === shell.activeWidget, `${contribution.viewLabel} === shell.activeWidget`);
|
||||
|
||||
view = await contribution.toggleView();
|
||||
await timeout(0); // seems that the "await" is not enought to guarantee that the panel is hidden
|
||||
assert.notEqual(view, undefined);
|
||||
assert.equal(shell.getAreaFor(view), contribution.defaultViewOptions.area);
|
||||
assert.isDefined(shell.getTabBarFor(view));
|
||||
assert.isFalse(view.isVisible);
|
||||
assert.isTrue(view !== shell.activeWidget, `${contribution.viewLabel} !== shell.activeWidget`);
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
export interface DefinedInterface {
|
||||
coolField: number[];
|
||||
}
|
||||
32
examples/api-tests/test-ts-workspace/demo-file.ts
Normal file
32
examples/api-tests/test-ts-workspace/demo-file.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
interface DemoInterface {
|
||||
stringField: string;
|
||||
numberField: number;
|
||||
doSomething(): number;
|
||||
}
|
||||
|
||||
class DemoClass implements DemoInterface {
|
||||
stringField: string;
|
||||
numberField: number;
|
||||
constructor(someString: string) {
|
||||
this.stringField = someString;
|
||||
this.numberField = this.stringField.length;
|
||||
}
|
||||
doSomething(): number {
|
||||
let output = 0;
|
||||
for (let i = 0; i < this.stringField.length; i++) {
|
||||
output += this.stringField.charCodeAt(i);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
const demoInstance = new DemoClass('demo');
|
||||
|
||||
const demoVariable = demoInstance.stringField;
|
||||
|
||||
demoVariable.concat('-string');
|
||||
|
||||
import { DefinedInterface } from "./demo-definitions-file";
|
||||
|
||||
const bar: DefinedInterface = { coolField: [] };
|
||||
30
examples/api-tests/test-ts-workspace/tsconfig.json
Normal file
30
examples/api-tests/test-ts-workspace/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitOverride": true,
|
||||
"noEmitOnError": false,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"strictNullChecks": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": true,
|
||||
"downlevelIteration": true,
|
||||
"resolveJsonModule": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2023",
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"ES2023",
|
||||
"DOM",
|
||||
"DOM.AsyncIterable"
|
||||
],
|
||||
"sourceMap": true
|
||||
}
|
||||
}
|
||||
89
examples/browser-only/package.json
Normal file
89
examples/browser-only/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
174
examples/browser-only/tsconfig.json
Normal file
174
examples/browser-only/tsconfig.json
Normal 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
Reference in New Issue
Block a user