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

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

View File

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

View File

@@ -0,0 +1,56 @@
<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 - PLUGIN-EXT EXTENSION</h2>
<hr />
</div>
## Description
The `@theia/plugin-ext` extension contributes functionality for the `plugin` API.
## Implementation
The implementation is inspired from: <https://blog.mattbierner.com/vscode-webview-web-learnings/>.
## Environment Variables
- `THEIA_WEBVIEW_EXTERNAL_ENDPOINT`
A string pattern possibly containing `{{uuid}}` and `{{hostname}}` which will be replaced. This is the host for which the `webviews` will be served on.
It is a good practice to host the `webview` handlers on a sub-domain as it is more secure.
Defaults to `{{uuid}}.webview.{{hostname}}`.
## Security Warnings
- Potentially Insecure Host Pattern
When you change the host pattern via the `THEIA_WEBVIEW_EXTERNAL_ENDPOINT` environment variable warning will be emitted both from the frontend and from the backend.
You can disable those warnings by setting `warnOnPotentiallyInsecureHostPattern: false` in the appropriate application configurations in your application's `package.json`.
## Naming in this package
This package has a different folder structure than other Theia packages. Stuff in the "hosted" folder is meant to be scoped to a front end,
whereas "main" is global to a back end instance. Code in "plugin" runs inside the plugin host process. But be aware that this is not always the case,
for example the plugin manifest scanners (e.g. `scanner-theia.ts`) are in the `hosted` folder, even though they a global concern.
## Additional Information
- [API documentation for `@theia/plugin-ext`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_plugin-ext.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,290 @@
# How to add new custom plugin API
As a Theia developer, you might want to make your app extensible by plugins in ways that are unique to your application.
That will require API that goes beyond what's in the VS Code Extension API and the Theia plugin API.
You can do that by implementing a Theia extension that creates and exposes an API object within the plugin host.
The API object can be imported by your plugins and exposes one or more API namespaces.
Depending on the plugin host we can either provide a frontend or backend plugin API, or an API for headless plugins that extend or otherwise access backend services:
- In the backend plugin host that runs in the Node environment in a separate process, we adapt the module loading to return a custom API object instead of loading a module with a particular name.
There is a distinct plugin host for each connected Theia frontend.
- In the frontend plugin host that runs in the browser environment via a web worker, we import the API scripts and put it in the global context.
There is a distinct plugin host for each connected Theia frontend.
- In the headless plugin host that also runs in the Node environment in a separate process, we similarly adapt the module loading mechanism.
When the first headless plugin is deployed, whether at start-up or upon later installation during run-time, then the one and only headless plugin host process is started.
In this document we focus on the implementation of a custom backend plugin API.
Headless plugin APIs are similar, and the same API can be contributed to both backend and headless plugin hosts.
All three APIs — backend, frontend, and headless — can be provided by implementing and binding an `ExtPluginApiProvider` which should be packaged as a Theia extension.
## Declare your plugin API provider
The plugin API provider is executed on the respective plugin host to add your custom API object and namespaces.
Add `@theia/plugin-ext` as a dependency in your `package.json`.
If your plugin is contributing API to headless plugins, then you also need to add the `@theia/plugin-ext-headless` package as a dependency.
Example Foo Plugin API provider.
Here we see that it provides the same API initialized by the same script to both backend plugins that are frontend-connection-scoped and to headless plugins.
Any combination of these API initialization scripts may be provided, offering the same or differing capabilities in each respective plugin host, although of course it would be odd to provide API to none of them.
```typescript
@injectable()
export class FooExtPluginApiProvider implements ExtPluginApiProvider {
provideApi(): ExtPluginApi {
return {
frontendExtApi: {
initPath: '/path/to/foo/api/implementation.js',
initFunction: 'fooInitializationFunction',
initVariable: 'foo_global_variable'
},
backendInitPath: path.join(__dirname, 'foo-init'),
// Provide the same API to headless plugins, too (or a different/subset API)
headlessInitPath: path.join(__dirname, 'foo-init')
};
}
}
```
Register your Plugin API provider in a backend module:
```typescript
bind(FooExtPluginApiProvider).toSelf().inSingletonScope();
bind(Symbol.for(ExtPluginApiProvider)).toService(FooExtPluginApiProvider);
```
## Define your API
To ease the usage of your API, it should be developed as separate npm package that can be easily imported without any additional dependencies, cf, the VS Code API or the Theia Plugin API.
Example `foo.d.ts`:
```typescript
declare module '@bar/foo' {
export class Foo { }
export namespace fooBar {
export function getFoo(): Promise<Foo>;
}
}
```
## Implement your plugin API provider
In our example, we aim to provide a new API object for the backend.
Theia expects that the `backendInitPath` or `headlessInitPath` that we specified in our API provider exports an [InversifyJS](https://inversify.io) `ContainerModule` under the name `containerModule`.
This container-module configures the Inversify `Container` in the plugin host for creation of our API object.
It also implements for us the customization of Node's module loading system to hook our API factory into the import of the module name that we choose.
Example `node/foo-init.ts`:
```typescript
import { inject, injectable } 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 { PluginContainerModule } from '@theia/plugin-ext/lib/plugin/node/plugin-container-module';
import { FooExt } from '../common/foo-api-rpc';
import { FooExtImpl } from './foo-ext-impl';
import * as fooBarAPI from '@bar/foo';
type FooBarApi = typeof fooBarAPI;
type Foo = FooBarApi['Foo'];
const FooBarApiFactory = Symbol('FooBarApiFactory');
// 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 the implementations of our Ext API interfaces (here just one)
bind(FooExt).to(FooExtImpl).inSingletonScope();
// Bind our API factory to the module name by which plugins will import it
bindApiFactory('@bar/foo', FooBarApiFactory, FooBarApiFactoryImpl);
});
```
## Implement your API object
We create a dedicated API object for each individual plugin as part of the module loading process.
Each API object is returned as part of the module loading process if a script imports `@bar/foo` and should therefore match the API definition that we provided in the `*.d.ts` file.
Multiple imports will not lead to the creation of multiple API objects as the `PluginContainerModule` automatically caches the API implementation for us.
Example `node/foo-init.ts` (continued):
```typescript
// Creates the @foo/bar API object
@injectable()
class FooBarApiFactoryImpl {
@inject(RPCProtocol) protected readonly rpc: RPCProtocol;
@inject(FooExt) protected readonly fooExt: FooExt;
@postConstruct()
initialize(): void {
this.rpc.set(FOO_MAIN_RPC_CONTEXT.FOO_EXT, this.fooExt);
}
// The plugin host expects our API factory to export a `createApi()` method
createApi(plugin: Plugin): FooBarApi {
const self = this;
return {
fooBar: {
getFoo(): Promise<Foo> {
return self.fooExt.getFooImpl();
}
}
};
};
}
```
In the example above the API object creates a local object that will fulfill the API contract.
The implementation details are hidden by the object and it could be a local implementation that only lives inside the plugin host but it could also be an implementation that uses the `RPCProtocol` to communicate with the main application to trigger changes, register functionality or retrieve information.
### Implement Main-Ext communication
In this document, we will only highlight the individual parts needed to establish the communication between the main application and the external plugin host.
For a more elaborate example of an API that communicates with the main application, please have a look at the definition of the [Theia Plugin API](https://github.com/eclipse-theia/theia/blob/master/doc/Plugin-API.md).
First, we need to establish the communication on the RPC protocol by providing an implementation for our own side and generating a proxy for the opposite side.
Proxies are identified using dedicated identifiers so we set them up first, together with the expected interfaces.
`Ext` and `Main` interfaces contain the functions called over RCP and must start with `$`.
Due to the asynchronous nature of the communication over RPC, the result should always be a `Promise` or `PromiseLike`.
Example `common/foo-api-rpc.ts`:
```typescript
export const FooMain = Symbol('FooMain');
export interface FooMain {
$getFooImpl(): Promise<Foo>;
}
export const FooExt = Symbol('FooExt');
export interface FooExt {
// placeholder for callbacks for the main application to the extension
}
// Plugin host will obtain a proxy using these IDs, main application will register an implementation for it.
export const FOO_PLUGIN_RPC_CONTEXT = {
FOO_MAIN: createProxyIdentifier<FooMain>('FooMain')
};
// Main application will obtain a proxy using these IDs, plugin host will register an implementation for it.
export const FOO_MAIN_RPC_CONTEXT = {
FOO_EXT: createProxyIdentifier<FooExt>('FooExt')
};
```
On the plugin host side we can register our implementation and retrieve the proxy as part of our `createAPIFactory` implementation:
Example `plugin/foo-ext.ts`:
```typescript
import { inject, injectable } from '@theia/core/shared/inversify';
import { RPCProtocol } from '@theia/plugin-ext/lib/common/rpc-protocol';
import { FooExt, FooMain, FOO_PLUGIN_RPC_CONTEXT } from '../common/foo-api-rpc';
@injectable()
export class FooExtImpl implements FooExt {
// Main application RCP counterpart
private proxy: FooMain;
constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
// Retrieve a proxy for the main side
this.proxy = rpc.getProxy(FOO_PLUGIN_RPC_CONTEXT.FOO_MAIN);
}
getFooImpl(): Promise<Foo> {
return this.proxy.$getFooImpl();
}
}
```
On the main side we need to implement the counterpart of the ExtPluginApiProvider, the `MainPluginApiProvider`, and expose it in a browser frontend module.
> [!NOTE]
> If the same API is also published to headless plugins, then the Main side is actually in the Node backend, not the browser frontend, so the implementation might
then be in the `common/` tree and registered in both the frontend and backend container modules.
> Alternatively, if the API is _only_ published to headless plugins, then it can be implemented in the `node/` tree and can take advantage of capabilities only available in the Node backend.
Example `main/browser/foo-main.ts`:
```typescript
@injectable()
export class FooMainImpl implements FooMain {
@inject(MessageService) protected messageService: MessageService;
protected proxy: FooExt;
constructor(@inject(RPCProtocol) rpc: RPCProtocol) {
// We would use this if we had a need to call back into the plugin-host/plugin
this.proxy = rpc.getProxy(FOO_MAIN_RPC_CONTEXT.FOO_EXT);
}
async $getFooImpl(): Promise<Foo> {
this.messageService.info('We were called from the plugin-host at the behest of the plugin.');
return new Foo();
}
}
@injectable()
export class FooMainPluginApiProvider implements MainPluginApiProvider {
@inject(MessageService) protected messageService: MessageService;
@inject(FooMain) protected fooMain: FooMain;
initialize(rpc: RPCProtocol): void {
this.messageService.info('Initialize RPC communication for FooMain!');
rpc.set(FOO_PLUGIN_RPC_CONTEXT.FOO_MAIN, this.fooMain);
}
}
export default new ContainerModule(bind => {
bind(MainPluginApiProvider).to(FooMainPluginApiProvider).inSingletonScope();
bind(FooMain).to(FooMainImpl).inSingletonScope();
});
```
In this example, we can already see the big advantage of going to the main application side as we have full access to our Theia services.
## Usage in a plugin
When using the API in a plugin the user can simply use the API as follows:
```typescript
import * as foo from '@bar/foo';
foo.fooBar.getFoo();
```
## Adding custom plugin activation events
When creating a custom plugin API there may also arise a need to trigger the activation of your plugins at a certain point in time.
The events that trigger the activation of a plugin are simply called `activation events`.
By default Theia supports a set of built-in activation events that contains the [activation events from VS Code](https://code.visualstudio.com/api/references/activation-events) as well as some additional Theia-specific events.
Technically, an activation event is nothing more than a unique string fired at a specific point in time.
To add more flexibility to activations events, Theia allows you to provide additional custom activation events when initializing a plugin host.
These additional events can be specified by adopters through the `ADDITIONAL_ACTIVATION_EVENTS` environment variable.
To fire an activation event, you need to call the plugin hosts `$activateByEvent(eventName)` method.
## Packaging
When bundling our application with the generated `gen-webpack.node.config.js` we need to make sure that our initialization function is bundled as a `commonjs2` library so it can be dynamically loaded.
Adjust the `webpack.config.js` accordingly:
```typescript
const configs = require('./gen-webpack.config.js');
const nodeConfig = require('./gen-webpack.node.config.js');
if (nodeConfig.config.entry) {
/**
* Add our initialization function. If unsure, look at the already generated entries for
* the nodeConfig where an entry is added for the default 'backend-init-theia' initialization.
*/
nodeConfig.config.entry['foo-init'] = {
import: require.resolve('@namespace/package/lib/node/foo-init'),
library: { type: 'commonjs2' }
};
}
module.exports = [...configs, nodeConfig.config];
```

View File

@@ -0,0 +1,100 @@
{
"name": "@theia/plugin-ext",
"version": "1.68.0",
"description": "Theia - Plugin Extension",
"main": "lib/common/index.js",
"typings": "lib/common/index.d.ts",
"dependencies": {
"@theia/bulk-edit": "1.68.0",
"@theia/callhierarchy": "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/markers": "1.68.0",
"@theia/messages": "1.68.0",
"@theia/monaco": "1.68.0",
"@theia/monaco-editor-core": "1.96.302",
"@theia/navigator": "1.68.0",
"@theia/notebook": "1.68.0",
"@theia/output": "1.68.0",
"@theia/plugin": "1.68.0",
"@theia/preferences": "1.68.0",
"@theia/scm": "1.68.0",
"@theia/search-in-workspace": "1.68.0",
"@theia/task": "1.68.0",
"@theia/terminal": "1.68.0",
"@theia/test": "1.68.0",
"@theia/timeline": "1.68.0",
"@theia/typehierarchy": "1.68.0",
"@theia/variable-resolver": "1.68.0",
"@theia/workspace": "1.68.0",
"@types/mime": "^2.0.1",
"@vscode/debugprotocol": "^1.51.0",
"@vscode/proxy-agent": "^0.13.2",
"async-mutex": "^0.4.0",
"decompress": "^4.2.1",
"escape-html": "^1.0.3",
"filenamify": "^4.1.0",
"is-electron": "^2.2.0",
"jsonc-parser": "^2.2.0",
"lodash.clonedeep": "^4.5.0",
"macaddress": "^0.5.3",
"mime": "^2.4.4",
"node-pty": "1.1.0-beta27",
"semver": "^7.5.4",
"tslib": "^2.6.2",
"vhost": "^3.0.2",
"vscode-textmate": "^9.2.0"
},
"publishConfig": {
"access": "public"
},
"theiaExtensions": [
{
"backend": "lib/plugin-ext-backend-module",
"backendElectron": "lib/plugin-ext-backend-electron-module",
"frontend": "lib/plugin-ext-frontend-module"
},
{
"frontendElectron": "lib/plugin-ext-frontend-electron-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": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0",
"@types/decompress": "^4.2.2",
"@types/escape-html": "^0.0.20",
"@types/lodash.clonedeep": "^4.5.3"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,70 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/arrays.ts
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
/**
* @returns True if the provided object is an array and has at least one element.
*/
export function isNonEmptyArray<T>(obj: T[] | undefined | null): obj is T[];
export function isNonEmptyArray<T>(obj: readonly T[] | undefined | null): obj is readonly T[];
export function isNonEmptyArray<T>(obj: T[] | readonly T[] | undefined | null): obj is T[] | readonly T[] {
return Array.isArray(obj) && obj.length > 0;
}
export function flatten<T>(arr: T[][]): T[] {
return (<T[]>[]).concat(...arr);
}
export interface Splice<T> {
readonly start: number;
readonly deleteCount: number;
readonly toInsert: T[];
}
/**
* @returns 'true' if the 'arg' is a 'ReadonlyArray'.
*/
export function isReadonlyArray(arg: unknown): arg is readonly unknown[] {
// Since Typescript does not properly narrow down typings for 'ReadonlyArray' we need to help it.
return Array.isArray(arg);
}
// Copied from https://github.com/microsoft/vscode/blob/1.72.2/src/vs/base/common/arrays.ts
/**
* Returns the first mapped value of the array which is not undefined.
*/
export function mapFind<T, R>(array: Iterable<T>, mapFn: (value: T) => R | undefined): R | undefined {
for (const value of array) {
const mapped = mapFn(value);
if (mapped !== undefined) {
return mapped;
}
}
return undefined;
}

View File

@@ -0,0 +1,23 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function ok(val?: any, message?: string): void {
if (!val || val === null) {
throw new Error(message ? `Assertion failed (${message})` : 'Assertion failed');
}
}

View File

@@ -0,0 +1,51 @@
// *****************************************************************************
// 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
// *****************************************************************************
// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/common/cache.ts
export class Cache<T> {
private static readonly enableDebugLogging = false;
private readonly _data = new Map<number, readonly T[]>();
private _idPool = 1;
constructor(
private readonly id: string
) { }
add(item: readonly T[]): number {
const id = this._idPool++;
this._data.set(id, item);
this.logDebugInfo();
return id;
}
get(pid: number, id: number): T | undefined {
return this._data.has(pid) ? this._data.get(pid)![id] : undefined;
}
delete(id: number): void {
this._data.delete(id);
this.logDebugInfo();
}
private logDebugInfo(): void {
if (!Cache.enableDebugLogging) {
return;
}
console.log(`${this.id} cache size — ${this._data.size}`);
}
}

View File

@@ -0,0 +1,73 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/editor/common/core/characterClassifier.ts
import { toUint8 } from './uint';
/**
* A fast character classifier that uses a compact array for ASCII values.
*/
export class CharacterClassifier<T extends number> {
/**
* Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code).
*/
protected _asciiMap: Uint8Array;
/**
* The entire map (sparse array).
*/
protected _map: Map<number, number>;
protected _defaultValue: number;
constructor(_defaultValue: T) {
const defaultValue = toUint8(_defaultValue);
this._defaultValue = defaultValue;
this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue);
this._map = new Map<number, number>();
}
private static _createAsciiMap(defaultValue: number): Uint8Array {
const asciiMap: Uint8Array = new Uint8Array(256);
for (let i = 0; i < 256; i++) {
asciiMap[i] = defaultValue;
}
return asciiMap;
}
public set(charCode: number, _value: T): void {
const value = toUint8(_value);
if (charCode >= 0 && charCode < 256) {
this._asciiMap[charCode] = value;
} else {
this._map.set(charCode, value);
}
}
public get(charCode: number): T {
if (charCode >= 0 && charCode < 256) {
return <T>this._asciiMap[charCode];
} else {
return <T>(this._map.get(charCode) || this._defaultValue);
}
}
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2022 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.71.2/src/vs/base/common/collections.ts
export function diffSets<T>(before: Set<T>, after: Set<T>): { removed: T[]; added: T[] } {
const removed: T[] = [];
const added: T[] = [];
for (const element of before) {
if (!after.has(element)) {
removed.push(element);
}
}
for (const element of after) {
if (!before.has(element)) {
added.push(element);
}
}
return { removed, added };
}
export function diffMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[]; added: V[] } {
const removed: V[] = [];
const added: V[] = [];
for (const [index, value] of before) {
if (!after.has(index)) {
removed.push(value);
}
}
for (const [index, value] of after) {
if (!before.has(index)) {
added.push(value);
}
}
return { removed, added };
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2023 ST Microelectronics, Inc. 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
export interface ArgumentProcessor {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
processArgument(arg: any): any;
}

View File

@@ -0,0 +1,137 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DebugChannel } from '@theia/debug/lib/common/debug-service';
import { ConnectionExt, ConnectionMain } from './plugin-api-rpc';
import { Emitter } from '@theia/core/lib/common/event';
/**
* A channel communicating with a counterpart in a plugin host.
*/
export class PluginChannel implements DebugChannel {
private messageEmitter: Emitter<string> = new Emitter();
private errorEmitter: Emitter<unknown> = new Emitter();
private closedEmitter: Emitter<void> = new Emitter();
constructor(
protected readonly id: string,
protected readonly connection: ConnectionExt | ConnectionMain) { }
send(content: string): void {
this.connection.$sendMessage(this.id, content);
}
fireMessageReceived(msg: string): void {
this.messageEmitter.fire(msg);
}
fireError(error: unknown): void {
this.errorEmitter.fire(error);
}
fireClosed(): void {
this.closedEmitter.fire();
}
onMessage(cb: (message: string) => void): void {
this.messageEmitter.event(cb);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onError(cb: (reason: any) => void): void {
this.errorEmitter.event(cb);
}
onClose(cb: (code: number, reason: string) => void): void {
this.closedEmitter.event(() => cb(-1, 'closed'));
}
close(): void {
this.connection.$deleteConnection(this.id);
}
}
export class ConnectionImpl implements ConnectionMain, ConnectionExt {
private readonly proxy: ConnectionExt | ConnectionExt;
private readonly connections = new Map<string, PluginChannel>();
constructor(proxy: ConnectionMain | ConnectionExt) {
this.proxy = proxy;
}
/**
* Gets the connection between plugin by id and sends string message to it.
*
* @param id connection's id
* @param message incoming message
*/
async $sendMessage(id: string, message: string): Promise<void> {
if (this.connections.has(id)) {
this.connections.get(id)!.fireMessageReceived(message);
} else {
console.warn(`Received message for unknown connection: ${id}`);
}
}
/**
* Instantiates a new connection by the given id.
* @param id the connection id
*/
async $createConnection(id: string): Promise<void> {
console.debug(`Creating plugin connection: ${id}`);
await this.doEnsureConnection(id);
}
/**
* Deletes a connection.
* @param id the connection id
*/
async $deleteConnection(id: string): Promise<void> {
console.debug(`Deleting plugin connection: ${id}`);
const connection = this.connections.get(id);
if (connection) {
this.connections.delete(id);
connection.fireClosed();
}
}
/**
* Returns existed connection or creates a new one.
* @param id the connection id
*/
async ensureConnection(id: string): Promise<PluginChannel> {
console.debug(`Creating local connection: ${id}`);
const connection = await this.doEnsureConnection(id);
await this.proxy.$createConnection(id);
return connection;
}
/**
* Returns existed connection or creates a new one.
* @param id the connection id
*/
async doEnsureConnection(id: string): Promise<PluginChannel> {
const connection = this.connections.get(id) || await this.doCreateConnection(id);
this.connections.set(id, connection);
return connection;
}
protected async doCreateConnection(id: string): Promise<PluginChannel> {
const channel = new PluginChannel(id, this.proxy);
channel.onClose(() => this.connections.delete(id));
return channel;
}
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
export interface Disposable {
dispose(): void;
}
export function dispose<T extends Disposable>(disposable: T): T | undefined;
export function dispose<T extends Disposable>(...disposables: T[]): T[] | undefined;
export function dispose<T extends Disposable>(disposables: T[]): T[] | undefined;
export function dispose<T extends Disposable>(first: T | T[], ...rest: T[]): T | T[] | undefined {
if (Array.isArray(first)) {
first.forEach(d => d && d.dispose());
return [];
} else if (rest.length === 0) {
if (first) {
first.dispose();
return first;
}
return undefined;
} else {
dispose(first);
dispose(rest);
return [];
}
}

View File

@@ -0,0 +1,74 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// enum copied from monaco.d.ts
/**
* The style in which the editor's cursor should be rendered.
*/
export enum TextEditorCursorStyle {
/**
* As a vertical line
*/
Line = 1,
/**
* As a block
*/
Block = 2,
/**
* As a horizontal line, under character
*/
Underline = 3,
/**
* As a thin vertical line
*/
LineThin = 4,
/**
* As an outlined block, on top of a character
*/
BlockOutline = 5,
/**
* As a thin horizontal line, under a character
*/
UnderlineThin = 6
}
export function cursorStyleToString(cursorStyle: TextEditorCursorStyle): 'line' | 'block' | 'underline' | 'line-thin' | 'block-outline' | 'underline-thin' {
switch (cursorStyle) {
case TextEditorCursorStyle.Line:
return 'line';
case TextEditorCursorStyle.Block:
return 'block';
case TextEditorCursorStyle.Underline:
return 'underline';
case TextEditorCursorStyle.LineThin:
return 'line-thin';
case TextEditorCursorStyle.BlockOutline:
return 'block-outline';
case TextEditorCursorStyle.UnderlineThin:
return 'underline-thin';
default:
throw new Error('cursorStyleToString: Unknown cursorStyle');
}
}

View File

@@ -0,0 +1,19 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
export interface QueryParameters {
[key: string]: string | string[]
}

View File

@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { isObject } from '@theia/core/lib/common/types';
export function illegalArgument(message?: string): Error {
if (message) {
return new Error(`Illegal argument: ${message}`);
} else {
return new Error('Illegal argument');
}
}
export function readonly(name?: string): Error {
if (name) {
return new Error(`readonly property '${name} cannot be changed'`);
} else {
return new Error('readonly property cannot be changed');
}
}
export function disposed(what: string): Error {
const result = new Error(`${what} has been disposed`);
result.name = 'DISPOSED';
return result;
}
interface Errno {
readonly code: string;
readonly errno: number
}
const ENOENT = 'ENOENT' as const;
type ErrnoException = Error & Errno;
function isErrnoException(arg: unknown): arg is ErrnoException {
return arg instanceof Error
&& isObject<Partial<Errno>>(arg)
&& typeof arg.code === 'string'
&& typeof arg.errno === 'number';
}
/**
* _(No such file or directory)_: Commonly raised by `fs` operations to indicate that a component of the specified pathname does not exist — no entity (file or directory) could be
* found by the given path.
*/
export function isENOENT(
arg: unknown
): arg is ErrnoException & Readonly<{ code: typeof ENOENT }> {
return isErrnoException(arg) && arg.code === ENOENT;
}

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
export class IdGenerator {
private lastId: number;
constructor(private prefix: string) {
this.lastId = 0;
}
nextId(): string {
return this.prefix + (++this.lastId);
}
}

View File

@@ -0,0 +1,24 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// Here we expose types from @theia/plugin, so it becomes a direct dependency
export * from './plugin-protocol';
export * from './plugin-api-rpc';
export * from './plugin-ext-api-contribution';
import { registerMsgPackExtensions } from './rpc-protocol';
registerMsgPackExtensions();

View File

@@ -0,0 +1,34 @@
// *****************************************************************************
// Copyright (C) 2023 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
// *****************************************************************************
/**
* Starting with vscode 1.73.0, language pack bundles have changed their shape to accommodate the new `l10n` API.
* They are now a record of { [englishValue]: translation }
*/
export interface LanguagePackBundle {
contents: Record<string, string>
uri: string
}
export const languagePackServicePath = '/services/languagePackService';
export const LanguagePackService = Symbol('LanguagePackService');
export interface LanguagePackService {
storeBundle(pluginId: string, locale: string, bundle: LanguagePackBundle): void;
deleteBundle(pluginId: string, locale?: string): void;
getBundle(pluginId: string, locale: string): Promise<LanguagePackBundle | undefined>;
}

View File

@@ -0,0 +1,354 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/editor/common/modes/linkComputer.ts
/* eslint-disable max-len */
import { CharacterClassifier } from './character-classifier';
import { CharCode } from '@theia/core/lib/common/char-code';
import { DocumentLink as ILink } from './plugin-api-rpc-model';
export interface ILinkComputerTarget {
getLineCount(): number;
getLineContent(lineNumber: number): string;
}
export const enum State {
Invalid = 0,
Start = 1,
H = 2,
HT = 3,
HTT = 4,
HTTP = 5,
F = 6,
FI = 7,
FIL = 8,
BeforeColon = 9,
AfterColon = 10,
AlmostThere = 11,
End = 12,
Accept = 13,
LastKnownState = 14 // marker, custom states may follow
}
export type Edge = [State, number, State];
export class Uint8Matrix {
private readonly _data: Uint8Array;
public readonly rows: number;
public readonly cols: number;
constructor(rows: number, cols: number, defaultValue: number) {
const data = new Uint8Array(rows * cols);
for (let i = 0, len = rows * cols; i < len; i++) {
data[i] = defaultValue;
}
this._data = data;
this.rows = rows;
this.cols = cols;
}
public get(row: number, col: number): number {
return this._data[row * this.cols + col];
}
public set(row: number, col: number, value: number): void {
this._data[row * this.cols + col] = value;
}
}
export class StateMachine {
private readonly _states: Uint8Matrix;
private readonly _maxCharCode: number;
constructor(edges: Edge[]) {
let maxCharCode = 0;
let maxState = State.Invalid;
for (let i = 0, len = edges.length; i < len; i++) {
const [from, chCode, to] = edges[i];
if (chCode > maxCharCode) {
maxCharCode = chCode;
}
if (from > maxState) {
maxState = from;
}
if (to > maxState) {
maxState = to;
}
}
maxCharCode++;
maxState++;
const states = new Uint8Matrix(maxState, maxCharCode, State.Invalid);
for (let i = 0, len = edges.length; i < len; i++) {
const [from, chCode, to] = edges[i];
states.set(from, chCode, to);
}
this._states = states;
this._maxCharCode = maxCharCode;
}
public nextState(currentState: State, chCode: number): State {
if (chCode < 0 || chCode >= this._maxCharCode) {
return State.Invalid;
}
return this._states.get(currentState, chCode);
}
}
// State machine for http:// or https:// or file://
let _stateMachine: StateMachine | null = null;
function getStateMachine(): StateMachine {
if (_stateMachine === null) {
_stateMachine = new StateMachine([
[State.Start, CharCode.h, State.H],
[State.Start, CharCode.H, State.H],
[State.Start, CharCode.f, State.F],
[State.Start, CharCode.F, State.F],
[State.H, CharCode.t, State.HT],
[State.H, CharCode.T, State.HT],
[State.HT, CharCode.t, State.HTT],
[State.HT, CharCode.T, State.HTT],
[State.HTT, CharCode.p, State.HTTP],
[State.HTT, CharCode.P, State.HTTP],
[State.HTTP, CharCode.s, State.BeforeColon],
[State.HTTP, CharCode.S, State.BeforeColon],
[State.HTTP, CharCode.Colon, State.AfterColon],
[State.F, CharCode.i, State.FI],
[State.F, CharCode.I, State.FI],
[State.FI, CharCode.l, State.FIL],
[State.FI, CharCode.L, State.FIL],
[State.FIL, CharCode.e, State.BeforeColon],
[State.FIL, CharCode.E, State.BeforeColon],
[State.BeforeColon, CharCode.Colon, State.AfterColon],
[State.AfterColon, CharCode.Slash, State.AlmostThere],
[State.AlmostThere, CharCode.Slash, State.End],
]);
}
return _stateMachine;
}
const enum CharacterClass {
None = 0,
ForceTermination = 1,
CannotEndIn = 2
}
let _classifier: CharacterClassifier<CharacterClass> | null = null;
function getClassifier(): CharacterClassifier<CharacterClass> {
if (_classifier === null) {
_classifier = new CharacterClassifier<CharacterClass>(CharacterClass.None);
const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"、。。、,.:;?!@#$%&*‘“〈《「『【〔([{「」}])〕】』」》〉”’`~…';
for (let i = 0; i < FORCE_TERMINATION_CHARACTERS.length; i++) {
_classifier.set(FORCE_TERMINATION_CHARACTERS.charCodeAt(i), CharacterClass.ForceTermination);
}
const CANNOT_END_WITH_CHARACTERS = '.,;';
for (let i = 0; i < CANNOT_END_WITH_CHARACTERS.length; i++) {
_classifier.set(CANNOT_END_WITH_CHARACTERS.charCodeAt(i), CharacterClass.CannotEndIn);
}
}
return _classifier;
}
export class LinkComputer {
private static _createLink(classifier: CharacterClassifier<CharacterClass>, line: string, lineNumber: number, linkBeginIndex: number, linkEndIndex: number): ILink {
// Do not allow to end link in certain characters...
let lastIncludedCharIndex = linkEndIndex - 1;
do {
const chCode = line.charCodeAt(lastIncludedCharIndex);
const chClass = classifier.get(chCode);
if (chClass !== CharacterClass.CannotEndIn) {
break;
}
lastIncludedCharIndex--;
} while (lastIncludedCharIndex > linkBeginIndex);
// Handle links enclosed in parens, square and curly brackets.
if (linkBeginIndex > 0) {
const charCodeBeforeLink = line.charCodeAt(linkBeginIndex - 1);
const lastCharCodeInLink = line.charCodeAt(lastIncludedCharIndex);
if (
(charCodeBeforeLink === CharCode.OpenParen && lastCharCodeInLink === CharCode.CloseParen)
|| (charCodeBeforeLink === CharCode.OpenSquareBracket && lastCharCodeInLink === CharCode.CloseSquareBracket)
|| (charCodeBeforeLink === CharCode.OpenCurlyBrace && lastCharCodeInLink === CharCode.CloseCurlyBrace)
) {
// Do not end in ) if ( is before the link start
// Do not end in ] if [ is before the link start
// Do not end in } if { is before the link start
lastIncludedCharIndex--;
}
}
return {
range: {
startLineNumber: lineNumber,
startColumn: linkBeginIndex + 1,
endLineNumber: lineNumber,
endColumn: lastIncludedCharIndex + 2
},
url: line.substring(linkBeginIndex, lastIncludedCharIndex + 1)
};
}
public static computeLinks(model: ILinkComputerTarget, stateMachine: StateMachine = getStateMachine()): ILink[] {
const classifier = getClassifier();
const result: ILink[] = [];
for (let i = 1, lineCount = model.getLineCount(); i <= lineCount; i++) {
const line = model.getLineContent(i);
const len = line.length;
let j = 0;
let linkBeginIndex = 0;
let linkBeginChCode = 0;
let state = State.Start;
let hasOpenParens = false;
let hasOpenSquareBracket = false;
let inSquareBrackets = false;
let hasOpenCurlyBracket = false;
while (j < len) {
let resetStateMachine = false;
const chCode = line.charCodeAt(j);
if (state === State.Accept) {
let chClass: CharacterClass;
switch (chCode) {
case CharCode.OpenParen:
hasOpenParens = true;
chClass = CharacterClass.None;
break;
case CharCode.CloseParen:
chClass = (hasOpenParens ? CharacterClass.None : CharacterClass.ForceTermination);
break;
case CharCode.OpenSquareBracket:
inSquareBrackets = true;
hasOpenSquareBracket = true;
chClass = CharacterClass.None;
break;
case CharCode.CloseSquareBracket:
inSquareBrackets = false;
chClass = (hasOpenSquareBracket ? CharacterClass.None : CharacterClass.ForceTermination);
break;
case CharCode.OpenCurlyBrace:
hasOpenCurlyBracket = true;
chClass = CharacterClass.None;
break;
case CharCode.CloseCurlyBrace:
chClass = (hasOpenCurlyBracket ? CharacterClass.None : CharacterClass.ForceTermination);
break;
/* The following three rules make it that ' or " or ` are allowed inside links if the link began with a different one */
case CharCode.SingleQuote:
chClass = (linkBeginChCode === CharCode.DoubleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination;
break;
case CharCode.DoubleQuote:
chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.None : CharacterClass.ForceTermination;
break;
case CharCode.BackTick:
chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.DoubleQuote) ? CharacterClass.None : CharacterClass.ForceTermination;
break;
case CharCode.Asterisk:
// `*` terminates a link if the link began with `*`
chClass = (linkBeginChCode === CharCode.Asterisk) ? CharacterClass.ForceTermination : CharacterClass.None;
break;
case CharCode.Pipe:
// `|` terminates a link if the link began with `|`
chClass = (linkBeginChCode === CharCode.Pipe) ? CharacterClass.ForceTermination : CharacterClass.None;
break;
case CharCode.Space:
// ` ` allow space in between [ and ]
chClass = (inSquareBrackets ? CharacterClass.None : CharacterClass.ForceTermination);
break;
default:
chClass = classifier.get(chCode);
}
// Check if character terminates link
if (chClass === CharacterClass.ForceTermination) {
result.push(LinkComputer._createLink(classifier, line, i, linkBeginIndex, j));
resetStateMachine = true;
}
} else if (state === State.End) {
let chClass: CharacterClass;
if (chCode === CharCode.OpenSquareBracket) {
// Allow for the authority part to contain ipv6 addresses which contain [ and ]
hasOpenSquareBracket = true;
chClass = CharacterClass.None;
} else {
chClass = classifier.get(chCode);
}
// Check if character terminates link
if (chClass === CharacterClass.ForceTermination) {
resetStateMachine = true;
} else {
state = State.Accept;
}
} else {
state = stateMachine.nextState(state, chCode);
if (state === State.Invalid) {
resetStateMachine = true;
}
}
if (resetStateMachine) {
state = State.Start;
hasOpenParens = false;
hasOpenSquareBracket = false;
hasOpenCurlyBracket = false;
// Record where the link started
linkBeginIndex = j + 1;
linkBeginChCode = chCode;
}
j++;
}
if (state === State.Accept) {
result.push(LinkComputer._createLink(classifier, line, i, linkBeginIndex, len));
}
}
return result;
}
}

View File

@@ -0,0 +1,137 @@
// *****************************************************************************
// 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 { UriComponents } from './uri-components';
/**
* Protocol interfaces for MCP server definition providers.
*/
export interface McpStdioServerDefinitionDto {
/**
* The human-readable name of the server.
*/
readonly label: string;
/**
* The working directory used to start the server.
*/
cwd?: UriComponents;
/**
* The command used to start the server. Node.js-based servers may use
* `process.execPath` to use the editor's version of Node.js to run the script.
*/
command: string;
/**
* Additional command-line arguments passed to the server.
*/
args?: string[];
/**
* Optional additional environment information for the server. Variables
* in this environment will overwrite or remove (if null) the default
* environment variables of the editor's extension host.
*/
env?: Record<string, string | number | null>;
/**
* Optional version identification for the server. If this changes, the
* editor will indicate that tools have changed and prompt to refresh them.
*/
version?: string;
}
/**
* McpHttpServerDefinition represents an MCP server available using the
* Streamable HTTP transport.
*/
export interface McpHttpServerDefinitionDto {
/**
* The human-readable name of the server.
*/
readonly label: string;
/**
* The URI of the server. The editor will make a POST request to this URI
* to begin each session.
*/
uri: UriComponents;
/**
* Optional additional heads included with each request to the server.
*/
headers?: Record<string, string>;
/**
* Optional version identification for the server. If this changes, the
* editor will indicate that tools have changed and prompt to refresh them.
*/
version?: string;
}
/**
* Definitions that describe different types of Model Context Protocol servers,
* which can be returned from the {@link McpServerDefinitionProvider}.
*/
export type McpServerDefinitionDto = McpStdioServerDefinitionDto | McpHttpServerDefinitionDto;
export const isMcpHttpServerDefinitionDto = (definition: McpServerDefinitionDto): definition is McpHttpServerDefinitionDto => 'uri' in definition;
/**
* Main side of the MCP server definition registry.
*/
export interface McpServerDefinitionRegistryMain {
/**
* Register an MCP server definition provider.
*/
$registerMcpServerDefinitionProvider(handle: number, name: string): void;
/**
* Unregister an MCP server definition provider.
*/
$unregisterMcpServerDefinitionProvider(handle: number): void;
/**
* Notify that server definitions have changed.
*/
$onDidChangeMcpServerDefinitions(handle: number): void;
/**
* Get server definitions from a provider.
*/
$getServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]>;
/**
* Resolve a server definition.
*/
$resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined>;
}
/**
* Extension side of the MCP server definition registry.
*/
export interface McpServerDefinitionRegistryExt {
/**
* Request server definitions from a provider.
*/
$provideServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]>;
/**
* Resolve a server definition from a provider.
*/
$resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined>;
}

View File

@@ -0,0 +1,33 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
export interface ObjectIdentifier {
$ident: number;
}
export namespace ObjectIdentifier {
export const name = '$ident';
export function mixin<T>(obj: T, id: number): T & ObjectIdentifier {
Object.defineProperty(obj, name, { value: id, enumerable: true });
return <T & ObjectIdentifier>obj;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function of(obj: any): number {
return obj[name];
}
}

View File

@@ -0,0 +1,50 @@
/* eslint-disable */
// copied from https://github.com/microsoft/vscode/blob/1.37.0/src/vs/base/common/objects.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isUndefinedOrNull, isArray, isObject } from './types';
const _hasOwnProperty = Object.prototype.hasOwnProperty;
export function cloneAndChange(obj: any, changer: (orig: any) => any): any {
return _cloneAndChange(obj, changer, new Set());
}
function _cloneAndChange(obj: any, changer: (orig: any) => any, seen: Set<any>): any {
if (isUndefinedOrNull(obj)) {
return obj;
}
const changed = changer(obj);
if (typeof changed !== 'undefined') {
return changed;
}
if (isArray(obj)) {
const r1: any[] = [];
for (const e of obj) {
r1.push(_cloneAndChange(e, changer, seen));
}
return r1;
}
if (isObject(obj)) {
if (seen.has(obj)) {
throw new Error('Cannot clone recursive data-structure');
}
seen.add(obj);
const r2 = {};
for (let i2 in obj) {
if (_hasOwnProperty.call(obj, i2)) {
(r2 as any)[i2] = _cloneAndChange(obj[i2], changer, seen);
}
}
seen.delete(obj);
return r2;
}
return obj;
}

View File

@@ -0,0 +1,158 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// file copied from https://github.com/wjordan/browser-path/blob/master/src/node_path.ts
// Original license:
/*
====
Copyright (c) 2015 John Vilk and other contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
====
*/
import { sep } from '@theia/core/lib/common/paths';
const replaceRegex = new RegExp('//+', 'g');
export function resolve(...paths: string[]): string {
let processed: string[] = [];
for (const p of paths) {
if (typeof p !== 'string') {
throw new TypeError('Invalid argument type to path.join: ' + (typeof p));
} else if (p !== '') {
if (p.charAt(0) === sep) {
processed = [];
}
processed.push(p);
}
}
const resolved = normalize(processed.join(sep));
if (resolved.length > 1 && resolved.charAt(resolved.length - 1) === sep) {
return resolved.substring(0, resolved.length - 1);
}
return resolved;
}
export function relative(from: string, to: string): string {
let i: number;
from = resolve(from);
to = resolve(to);
const fromSegments = from.split(sep);
const toSegments = to.split(sep);
toSegments.shift();
fromSegments.shift();
let upCount = 0;
let downSegments: string[] = [];
for (i = 0; i < fromSegments.length; i++) {
const seg = fromSegments[i];
if (seg === toSegments[i]) {
continue;
}
upCount = fromSegments.length - i;
break;
}
downSegments = toSegments.slice(i);
if (fromSegments.length === 1 && fromSegments[0] === '') {
upCount = 0;
}
if (upCount > fromSegments.length) {
upCount = fromSegments.length;
}
let rv = '';
for (i = 0; i < upCount; i++) {
rv += '../';
}
rv += downSegments.join(sep);
if (rv.length > 1 && rv.charAt(rv.length - 1) === sep) {
rv = rv.substring(0, rv.length - 1);
}
return rv;
}
export function normalize(p: string): string {
if (p === '') {
p = '.';
}
const absolute = p.charAt(0) === sep;
p = removeDuplicateSeparators(p);
const components = p.split(sep);
const goodComponents: string[] = [];
for (const c of components) {
if (c === '.') {
continue;
} else if (c === '..' && (absolute || (!absolute && goodComponents.length > 0 && goodComponents[0] !== '..'))) {
goodComponents.pop();
} else {
goodComponents.push(c);
}
}
if (!absolute && goodComponents.length < 2) {
switch (goodComponents.length) {
case 1:
if (goodComponents[0] === '') {
goodComponents.unshift('.');
}
break;
default:
goodComponents.push('.');
}
}
p = goodComponents.join(sep);
if (absolute && p.charAt(0) !== sep) {
p = sep + p;
}
return p;
}
function removeDuplicateSeparators(p: string): string {
p = p.replace(replaceRegex, sep);
return p;
}

View File

@@ -0,0 +1,937 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 theia from '@theia/plugin';
import type * as monaco from '@theia/monaco-editor-core';
import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering';
import { UriComponents } from './uri-components';
import { CompletionItemTag, DocumentPasteEditKind, SnippetString } from '../plugin/types-impl';
import { Event as TheiaEvent } from '@theia/core/lib/common/event';
import { URI } from '@theia/core/shared/vscode-uri';
import { SerializedRegExp } from './plugin-api-rpc';
// Should contains internal Plugin API types
/**
* Represents options to configure the behavior of showing a document in an editor.
*/
export interface TextDocumentShowOptions {
/**
* An optional selection to apply for the document in the editor.
*/
selection?: Range;
/**
* An optional flag that when `true` will stop the editor from taking focus.
*/
preserveFocus?: boolean;
/**
* An optional flag that controls if an editor-tab will be replaced
* with the next editor or if it will be kept.
*/
preview?: boolean;
/**
* Denotes a location of an editor in the window. Editors can be arranged in a grid
* and each column represents one editor location in that grid by counting the editors
* in order of their appearance.
*/
viewColumn?: theia.ViewColumn;
}
export interface Range {
/**
* Line number on which the range starts (starts at 1).
*/
readonly startLineNumber: number;
/**
* Column on which the range starts in line `startLineNumber` (starts at 1).
*/
readonly startColumn: number;
/**
* Line number on which the range ends.
*/
readonly endLineNumber: number;
/**
* Column on which the range ends in line `endLineNumber`.
*/
readonly endColumn: number;
}
export interface Position {
/**
* line number (starts at 1)
*/
readonly lineNumber: number,
/**
* column (starts at 1)
*/
readonly column: number
}
export { MarkdownStringDTO as MarkdownString };
export interface SerializedDocumentFilter {
$serialized: true;
language?: string;
scheme?: string;
pattern?: theia.GlobPattern;
notebookType?: string;
}
export enum CompletionTriggerKind {
Invoke = 0,
TriggerCharacter = 1,
TriggerForIncompleteCompletions = 2
}
export interface CompletionContext {
triggerKind: CompletionTriggerKind;
triggerCharacter?: string;
}
export enum CompletionItemInsertTextRule {
KeepWhitespace = 1,
InsertAsSnippet = 4
}
export interface Completion {
label: string | theia.CompletionItemLabel;
label2?: string;
kind: CompletionItemKind;
detail?: string;
documentation?: string | MarkdownStringDTO;
sortText?: string;
filterText?: string;
preselect?: boolean;
insertText: string;
insertTextRules?: CompletionItemInsertTextRule;
range?: Range | {
insert: Range;
replace: Range;
};
commitCharacters?: string[];
additionalTextEdits?: SingleEditOperation[];
command?: Command;
tags?: CompletionItemTag[];
/** @deprecated use tags instead. */
deprecated?: boolean;
}
export interface SingleEditOperation {
range: Range;
text: string | null;
/**
* This indicates that this operation has "insert" semantics.
* i.e. forceMoveMarkers = true => if `range` is collapsed, all markers at the position will be moved.
*/
forceMoveMarkers?: boolean;
}
export interface Command {
id: string;
title: string;
tooltip?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
arguments?: any[];
}
export enum CompletionItemKind {
Method = 0,
Function = 1,
Constructor = 2,
Field = 3,
Variable = 4,
Class = 5,
Struct = 6,
Interface = 7,
Module = 8,
Property = 9,
Event = 10,
Operator = 11,
Unit = 12,
Value = 13,
Constant = 14,
Enum = 15,
EnumMember = 16,
Keyword = 17,
Text = 18,
Color = 19,
File = 20,
Reference = 21,
Customcolor = 22,
Folder = 23,
TypeParameter = 24,
User = 25,
Issue = 26,
Snippet = 27
}
export class IdObject {
id?: number;
}
export interface CompletionDto extends Completion {
id: number;
parentId: number;
}
export interface CompletionResultDto extends IdObject {
id: number;
defaultRange: {
insert: Range,
replace: Range
}
completions: CompletionDto[];
incomplete?: boolean;
}
export interface MarkerData {
code?: string;
severity: MarkerSeverity;
message: string;
source?: string;
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
relatedInformation?: RelatedInformation[];
tags?: MarkerTag[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}
export interface RelatedInformation {
resource: string;
message: string;
startLineNumber: number;
startColumn: number;
endLineNumber: number;
endColumn: number;
}
export enum MarkerSeverity {
Hint = 1,
Info = 2,
Warning = 4,
Error = 8,
}
export enum MarkerTag {
Unnecessary = 1,
Deprecated = 2,
}
export interface ParameterInformation {
label: string | [number, number];
documentation?: string | MarkdownStringDTO;
}
export interface SignatureInformation {
label: string;
documentation?: string | MarkdownStringDTO;
parameters: ParameterInformation[];
activeParameter?: number;
}
export interface SignatureHelp extends IdObject {
signatures: SignatureInformation[];
activeSignature: number;
activeParameter: number;
}
export interface SignatureHelpContext {
triggerKind: theia.SignatureHelpTriggerKind;
triggerCharacter?: string;
isRetrigger: boolean;
activeSignatureHelp?: SignatureHelp;
}
export interface Hover {
contents: MarkdownStringDTO[];
range?: Range;
canIncreaseVerbosity?: boolean;
canDecreaseVerbosity?: boolean;
}
export interface HoverProvider {
provideHover(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Hover | undefined | Thenable<Hover | undefined>;
}
export interface HoverContext<THover = Hover> {
verbosityRequest?: HoverVerbosityRequest<THover>;
}
export interface HoverVerbosityRequest<THover = Hover> {
verbosityDelta: number;
previousHover: THover;
}
export enum HoverVerbosityAction {
Increase,
Decrease
}
export interface EvaluatableExpression {
range: Range;
expression?: string;
}
export interface EvaluatableExpressionProvider {
provideEvaluatableExpression(model: monaco.editor.ITextModel, position: monaco.Position,
token: monaco.CancellationToken): EvaluatableExpression | undefined | Thenable<EvaluatableExpression | undefined>;
}
export interface InlineValueContext {
frameId: number;
stoppedLocation: Range;
}
export interface InlineValueText {
type: 'text';
range: Range;
text: string;
}
export interface InlineValueVariableLookup {
type: 'variable';
range: Range;
variableName?: string;
caseSensitiveLookup: boolean;
}
export interface InlineValueEvaluatableExpression {
type: 'expression';
range: Range;
expression?: string;
}
export type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression;
export interface InlineValuesProvider {
onDidChangeInlineValues?: TheiaEvent<void> | undefined;
provideInlineValues(model: monaco.editor.ITextModel, viewPort: Range, context: InlineValueContext, token: monaco.CancellationToken):
InlineValue[] | undefined | Thenable<InlineValue[] | undefined>;
}
export enum DocumentHighlightKind {
Text = 0,
Read = 1,
Write = 2
}
export interface DocumentHighlight {
range: Range;
kind?: DocumentHighlightKind;
}
export interface DocumentHighlightProvider {
provideDocumentHighlights(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): DocumentHighlight[] | undefined;
}
export interface FormattingOptions {
tabSize: number;
insertSpaces: boolean;
}
export interface TextEdit {
range: Range;
text: string;
eol?: monaco.editor.EndOfLineSequence;
}
export interface DocumentDropEdit {
insertText: string | SnippetString;
additionalEdit?: WorkspaceEdit;
}
export interface DocumentDropEditProviderMetadata {
readonly providedDropEditKinds?: readonly DocumentPasteEditKind[];
readonly dropMimeTypes: readonly string[];
}
export interface DataTransferFileDTO {
readonly id: string;
readonly name: string;
readonly uri?: UriComponents;
}
export interface DataTransferItemDTO {
readonly asString: string;
readonly fileData: DataTransferFileDTO | undefined;
readonly uriListData?: ReadonlyArray<string | UriComponents>;
}
export interface DataTransferDTO {
readonly items: Array<[/* type */string, DataTransferItemDTO]>;
}
export interface Location {
uri: UriComponents;
range: Range;
}
export type Definition = Location | Location[] | LocationLink[];
export interface LocationLink {
uri: UriComponents;
range: Range;
originSelectionRange?: Range;
targetSelectionRange?: Range;
}
export interface DefinitionProvider {
provideDefinition(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Definition | undefined;
}
export interface DeclarationProvider {
provideDeclaration(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Definition | undefined;
}
/**
* Value-object that contains additional information when
* requesting references.
*/
export interface ReferenceContext {
/**
* Include the declaration of the current symbol.
*/
includeDeclaration: boolean;
}
export type CacheId = number;
export type ChainedCacheId = [CacheId, CacheId];
export type CachedSessionItem<T> = T & { cacheId?: ChainedCacheId };
export type CachedSession<T> = T & { cacheId?: CacheId };
export interface DocumentLink {
cacheId?: ChainedCacheId,
range: Range;
url?: UriComponents | string;
tooltip?: string;
}
export interface DocumentLinkProvider {
provideLinks(model: monaco.editor.ITextModel, token: monaco.CancellationToken): DocumentLink[] | undefined | PromiseLike<DocumentLink[] | undefined>;
resolveLink?: (link: DocumentLink, token: monaco.CancellationToken) => DocumentLink | PromiseLike<DocumentLink[]>;
}
export interface CodeLensSymbol {
range: Range;
command?: Command;
}
export interface CodeAction {
cacheId: number;
title: string;
command?: Command;
edit?: WorkspaceEdit;
diagnostics?: MarkerData[];
kind?: string;
disabled?: { reason: string };
isPreferred?: boolean;
}
export enum CodeActionTriggerKind {
Invoke = 1,
Automatic = 2,
}
export interface CodeActionContext {
only?: string;
trigger: CodeActionTriggerKind
}
export type CodeActionProviderDocumentation = ReadonlyArray<{ command: Command, kind: string }>;
export interface CodeActionProvider {
provideCodeActions(
model: monaco.editor.ITextModel,
range: Range | Selection,
context: monaco.languages.CodeActionContext,
token: monaco.CancellationToken
): CodeAction[] | PromiseLike<CodeAction[]>;
providedCodeActionKinds?: string[];
}
// copied from https://github.com/microsoft/vscode/blob/b165e20587dd0797f37251515bc9e4dbe513ede8/src/vs/editor/common/modes.ts
export interface WorkspaceEditMetadata {
needsConfirmation: boolean;
label: string;
description?: string;
iconPath?: UriComponents | {
id: string;
} | {
light: UriComponents;
dark: UriComponents;
};
}
export interface WorkspaceFileEdit {
newResource?: UriComponents;
oldResource?: UriComponents;
options?: { overwrite?: boolean, ignoreIfNotExists?: boolean, ignoreIfExists?: boolean, recursive?: boolean };
metadata?: WorkspaceEditMetadata;
}
export interface WorkspaceTextEdit {
resource: UriComponents;
modelVersionId?: number;
textEdit: TextEdit;
metadata?: WorkspaceEditMetadata;
}
export interface WorkspaceEdit {
edits: Array<WorkspaceTextEdit | WorkspaceFileEdit>;
}
export enum SymbolKind {
File = 0,
Module = 1,
Namespace = 2,
Package = 3,
Class = 4,
Method = 5,
Property = 6,
Field = 7,
Constructor = 8,
Enum = 9,
Interface = 10,
Function = 11,
Variable = 12,
Constant = 13,
String = 14,
Number = 15,
Boolean = 16,
Array = 17,
Object = 18,
Key = 19,
Null = 20,
EnumMember = 21,
Struct = 22,
Event = 23,
Operator = 24,
TypeParameter = 25
}
export enum SymbolTag {
Deprecated = 1
}
export interface DocumentSymbol {
name: string;
detail: string;
kind: SymbolKind;
tags: ReadonlyArray<SymbolTag>;
containerName?: string;
range: Range;
selectionRange: Range;
children?: DocumentSymbol[];
}
export interface WorkspaceRootsChangeEvent {
roots: string[];
}
export interface WorkspaceFolder {
uri: UriComponents;
name: string;
index: number;
}
export interface Breakpoint {
readonly id: string;
readonly enabled: boolean;
readonly condition?: string;
readonly hitCondition?: string;
readonly logMessage?: string;
readonly location?: Location;
readonly functionName?: string;
}
export interface WorkspaceSymbolParams {
query: string
}
export interface FoldingContext {
}
export interface FoldingRange {
start: number;
end: number;
kind?: FoldingRangeKind;
}
export class FoldingRangeKind {
static readonly Comment = new FoldingRangeKind('comment');
static readonly Imports = new FoldingRangeKind('imports');
static readonly Region = new FoldingRangeKind('region');
public constructor(public value: string) { }
}
export interface SelectionRange {
range: Range;
}
export interface Color {
readonly red: number;
readonly green: number;
readonly blue: number;
readonly alpha: number;
}
export interface ColorPresentation {
label: string;
textEdit?: TextEdit;
additionalTextEdits?: TextEdit[];
}
export interface ColorInformation {
range: Range;
color: Color;
}
export interface DocumentColorProvider {
provideDocumentColors(model: monaco.editor.ITextModel): PromiseLike<ColorInformation[]>;
provideColorPresentations(model: monaco.editor.ITextModel, colorInfo: ColorInformation): PromiseLike<ColorPresentation[]>;
}
export interface Rejection {
rejectReason?: string;
}
export interface RenameLocation {
range: Range;
text: string;
}
export class HierarchyItem {
_sessionId?: string;
_itemId?: string;
kind: SymbolKind;
tags?: readonly SymbolTag[];
name: string;
detail?: string;
uri: UriComponents;
range: Range;
selectionRange: Range;
}
export class TypeHierarchyItem extends HierarchyItem { }
export interface CallHierarchyItem extends HierarchyItem {
data?: unknown;
}
export interface CallHierarchyIncomingCall {
from: CallHierarchyItem;
fromRanges: Range[];
}
export interface CallHierarchyOutgoingCall {
to: CallHierarchyItem;
fromRanges: Range[];
}
export interface LinkedEditingRanges {
ranges: Range[];
wordPattern?: SerializedRegExp;
}
export interface SearchInWorkspaceResult {
root: string;
fileUri: string;
matches: SearchMatch[];
}
export interface SearchMatch {
line: number;
character: number;
length: number;
lineText: string | LinePreview;
}
export interface LinePreview {
text: string;
character: number;
}
/**
* @deprecated Use {@link theia.AuthenticationSession} instead.
*/
export interface AuthenticationSession extends theia.AuthenticationSession {
}
/**
* @deprecated Use {@link theia.AuthenticationProviderAuthenticationSessionsChangeEvent} instead.
*/
export interface AuthenticationSessionsChangeEvent extends theia.AuthenticationProviderAuthenticationSessionsChangeEvent {
}
/**
* @deprecated Use {@link theia.AuthenticationProviderInformation} instead.
*/
export interface AuthenticationProviderInformation extends theia.AuthenticationProviderInformation {
}
export interface CommentOptions {
/**
* An optional string to show on the comment input box when it's collapsed.
*/
prompt?: string;
/**
* An optional string to show as placeholder in the comment input box when it's focused.
*/
placeHolder?: string;
}
export enum CommentMode {
Editing = 0,
Preview = 1
}
export interface Comment {
readonly uniqueIdInThread: number;
readonly body: MarkdownStringDTO;
readonly userName: string;
readonly userIconPath?: string;
readonly contextValue?: string;
readonly label?: string;
readonly mode?: CommentMode;
/** Timestamp serialized as ISO date string via Date.prototype.toISOString */
readonly timestamp?: string;
}
export enum CommentThreadState {
Unresolved = 0,
Resolved = 1
}
export enum CommentThreadCollapsibleState {
/**
* Determines an item is collapsed
*/
Collapsed = 0,
/**
* Determines an item is expanded
*/
Expanded = 1
}
export interface CommentInput {
value: string;
uri: URI;
}
export interface CommentThread {
commentThreadHandle: number;
controllerHandle: number;
extensionId?: string;
threadId: string;
resource: string | null;
range: Range | undefined;
label: string | undefined;
contextValue: string | undefined;
comments: Comment[] | undefined;
onDidChangeComments: TheiaEvent<Comment[] | undefined>;
collapsibleState?: CommentThreadCollapsibleState;
state?: CommentThreadState;
input?: CommentInput;
onDidChangeInput: TheiaEvent<CommentInput | undefined>;
onDidChangeRange: TheiaEvent<Range | undefined>;
onDidChangeLabel: TheiaEvent<string | undefined>;
onDidChangeState: TheiaEvent<CommentThreadState | undefined>;
onDidChangeCollapsibleState: TheiaEvent<CommentThreadCollapsibleState | undefined>;
isDisposed: boolean;
canReply: boolean | theia.CommentAuthorInformation;
onDidChangeCanReply: TheiaEvent<boolean | theia.CommentAuthorInformation>;
}
export interface CommentThreadChangedEventMain extends CommentThreadChangedEvent {
owner: string;
}
export interface CommentThreadChangedEvent {
/**
* Added comment threads.
*/
readonly added: CommentThread[];
/**
* Removed comment threads.
*/
readonly removed: CommentThread[];
/**
* Changed comment threads.
*/
readonly changed: CommentThread[];
}
export interface CommentingRanges {
readonly resource: URI;
ranges: Range[];
fileComments: boolean;
}
export interface CommentInfo {
extensionId?: string;
threads: CommentThread[];
commentingRanges: CommentingRanges;
}
export interface ProvidedTerminalLink extends theia.TerminalLink {
providerId: string
}
export interface InlayHintLabelPart {
label: string;
tooltip?: string | MarkdownStringDTO;
location?: Location;
command?: Command;
}
export interface InlayHint {
position: { lineNumber: number, column: number };
label: string | InlayHintLabelPart[];
tooltip?: string | MarkdownStringDTO | undefined;
kind?: InlayHintKind;
textEdits?: TextEdit[];
paddingLeft?: boolean;
paddingRight?: boolean;
}
export enum InlayHintKind {
Type = 1,
Parameter = 2,
}
export interface InlayHintsProvider {
onDidChangeInlayHints?: TheiaEvent<void> | undefined;
provideInlayHints(model: monaco.editor.ITextModel, range: Range, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable<InlayHint[] | undefined>;
resolveInlayHint?(hint: InlayHint, token: monaco.CancellationToken): InlayHint[] | undefined | Thenable<InlayHint[] | undefined>;
}
/**
* How an {@link InlineCompletionsProvider inline completion provider} was triggered.
*/
export enum InlineCompletionTriggerKind {
/**
* Completion was triggered automatically while editing.
* It is sufficient to return a single completion item in this case.
*/
Automatic = 0,
/**
* Completion was triggered explicitly by a user gesture.
* Return multiple completion items to enable cycling through them.
*/
Explicit = 1,
}
export interface InlineCompletionContext {
/**
* How the completion was triggered.
*/
readonly triggerKind: InlineCompletionTriggerKind;
readonly selectedSuggestionInfo: SelectedSuggestionInfo | undefined;
}
export interface SelectedSuggestionInfo {
range: Range;
text: string;
isSnippetText: boolean;
completionKind: CompletionItemKind;
}
export interface InlineCompletion {
/**
* The text to insert.
* If the text contains a line break, the range must end at the end of a line.
* If existing text should be replaced, the existing text must be a prefix of the text to insert.
*
* The text can also be a snippet. In that case, a preview with default parameters is shown.
* When accepting the suggestion, the full snippet is inserted.
*/
readonly insertText: string | { snippet: string };
/**
* A text that is used to decide if this inline completion should be shown.
* An inline completion is shown if the text to replace is a subword of the filter text.
*/
readonly filterText?: string;
/**
* An optional array of additional text edits that are applied when
* selecting this completion. Edits must not overlap with the main edit
* nor with themselves.
*/
readonly additionalTextEdits?: SingleEditOperation[];
/**
* The range to replace.
* Must begin and end on the same line.
*/
readonly range?: Range;
readonly command?: Command;
/**
* If set to `true`, unopened closing brackets are removed and unclosed opening brackets are closed.
* Defaults to `false`.
*/
readonly completeBracketPairs?: boolean;
}
export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> {
readonly items: readonly TItem[];
}
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(
model: monaco.editor.ITextModel,
position: monaco.Position,
context: InlineCompletionContext,
token: monaco.CancellationToken
): T[] | undefined | Thenable<T[] | undefined>;
/**
* Will be called when an item is shown.
*/
handleItemDidShow?(completions: T, item: T['items'][number]): void;
/**
* Will be called when a completions list is no longer in use and can be garbage-collected.
*/
freeInlineCompletions(completions: T): void;
}
export interface DebugStackFrameDTO {
readonly sessionId: string,
readonly frameId: number,
readonly threadId: number
}
export interface DebugThreadDTO {
readonly sessionId: string,
readonly threadId: number
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { RPCProtocol } from './rpc-protocol';
import { PluginManager, Plugin } from './plugin-api-rpc';
import { interfaces } from '@theia/core/shared/inversify';
export const ExtPluginApiProvider = 'extPluginApi';
/**
* Provider for extension API description.
*/
export interface ExtPluginApiProvider {
/**
* Provide API description.
*/
provideApi(): ExtPluginApi;
}
/**
* Provider for backend extension API description.
*/
export interface ExtPluginBackendApiProvider {
/**
* Provide API description.
*/
provideApi(): ExtPluginBackendApi;
}
/**
* Provider for frontend extension API description.
*/
export interface ExtPluginFrontendApiProvider {
/**
* Provide API description.
*/
provideApi(): ExtPluginFrontendApi;
}
/**
* Backend Plugin API extension description.
* This interface describes a script for the backend(NodeJs) runtime.
*/
export interface ExtPluginBackendApi {
/**
* Path to the script which should be loaded to provide api, module should export `provideApi` function with
* [ExtPluginApiBackendInitializationFn](#ExtPluginApiBackendInitializationFn) signature
*/
backendInitPath?: string;
}
/**
* Frontend Plugin API extension description.
* This interface describes a script for the frontend(WebWorker) runtime.
*/
export interface ExtPluginFrontendApi {
/**
* Initialization information for frontend part of Plugin API
*/
frontendExtApi?: FrontendExtPluginApi;
}
/**
* Plugin API extension description.
* This interface describes scripts for both plugin runtimes: frontend(WebWorker) and backend(NodeJs)
*/
export interface ExtPluginApi extends ExtPluginBackendApi, ExtPluginFrontendApi { }
export interface ExtPluginApiFrontendInitializationFn {
(rpc: RPCProtocol, plugins: Map<string, Plugin>): void;
}
export interface ExtPluginApiBackendInitializationFn {
(rpc: RPCProtocol, pluginManager: PluginManager): void;
}
/**
* Interface contains information for frontend(WebWorker) Plugin API extension initialization
*/
export interface FrontendExtPluginApi {
/**
* path to js file
*/
initPath: string;
/** global variable name */
initVariable: string;
/**
* init function name,
* function should have [ExtPluginApiFrontendInitializationFn](#ExtPluginApiFrontendInitializationFn)
*/
initFunction: string;
}
export const MainPluginApiProvider = Symbol('mainPluginApi');
/**
* Implementation should contains main(Theia) part of new namespace in Plugin API.
* [initialize](#initialize) will be called once per plugin runtime
*/
export interface MainPluginApiProvider {
initialize(rpc: RPCProtocol, container: interfaces.Container): void;
}

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// 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
// *****************************************************************************
export namespace PluginIdentifiers {
export interface Components {
publisher?: string;
name: string;
version: string;
}
export interface IdAndVersion {
id: UnversionedId;
version: string;
}
export type VersionedId = `${string}.${string}@${string}`;
export type UnversionedId = `${string}.${string}`;
/** Unpublished plugins (not from Open VSX or VSCode plugin store) may not have a `publisher` field. */
export const UNPUBLISHED = '<unpublished>';
/**
* @returns a string in the format `<publisher>.<name>`
*/
export function componentsToUnversionedId({ publisher = UNPUBLISHED, name }: Components): UnversionedId {
return `${publisher.toLowerCase()}.${name.toLowerCase()}`;
}
/**
* @returns a string in the format `<publisher>.<name>@<version>`.
*/
export function componentsToVersionedId({ publisher = UNPUBLISHED, name, version }: Components): VersionedId {
return `${publisher.toLowerCase()}.${name.toLowerCase()}@${version}`;
}
export function componentsToVersionWithId(components: Components): IdAndVersion {
return { id: componentsToUnversionedId(components), version: components.version };
}
/**
* @returns a string in the format `<id>@<version>`.
*/
export function idAndVersionToVersionedId({ id, version }: IdAndVersion): VersionedId {
return `${id}@${version}`;
}
/**
* @returns a string in the format `<publisher>.<name>`.
*/
export function unversionedFromVersioned(id: VersionedId): UnversionedId {
return toUnversioned(id);
}
/**
* @returns a string in the format `<publisher>.<name>`.
*
* If the supplied ID does not include `@`, it will be returned in whole.
*/
export function toUnversioned(id: VersionedId | UnversionedId): UnversionedId {
const endOfId = id.indexOf('@');
return endOfId === -1 ? id : id.slice(0, endOfId) as UnversionedId;
}
/**
* @returns `undefined` if it looks like the string passed in does not have the format of {@link VersionedId}.
*/
export function identifiersFromVersionedId(probablyId: string): Components | undefined {
const endOfPublisher = probablyId.indexOf('.');
const endOfName = probablyId.indexOf('@', endOfPublisher);
if (endOfPublisher === -1 || endOfName === -1) {
return undefined;
}
return { publisher: probablyId.slice(0, endOfPublisher), name: probablyId.slice(endOfPublisher + 1, endOfName), version: probablyId.slice(endOfName + 1) };
}
/**
* @returns `undefined` if it looks like the string passed in does not have the format of {@link VersionedId}.
*/
export function idAndVersionFromVersionedId(probablyId: string): IdAndVersion | undefined {
const endOfPublisher = probablyId.indexOf('.');
const endOfName = probablyId.indexOf('@', endOfPublisher);
if (endOfPublisher === -1 || endOfName === -1) {
return undefined;
}
return { id: probablyId.slice(0, endOfName) as UnversionedId, version: probablyId.slice(endOfName + 1) };
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
// *****************************************************************************
// 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
// *****************************************************************************
// copied from hhttps://github.com/microsoft/vscode/blob/6261075646f055b99068d3688932416f2346dd3b/src/vs/workbench/api/common/extHostLanguageFeatures.ts#L1291-L1310.
export class ReferenceMap<T> {
private readonly _references = new Map<number, T>();
private _idPool = 1;
createReferenceId(value: T): number {
const id = this._idPool++;
this._references.set(id, value);
return id;
}
disposeReferenceId(referenceId: number): T | undefined {
const value = this._references.get(referenceId);
this._references.delete(referenceId);
return value;
}
get(referenceId: number): T | undefined {
return this._references.get(referenceId);
}
}

View File

@@ -0,0 +1,315 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied from https://github.com/Microsoft/vscode/blob/master/src/vs/workbench/services/extensions/node/rpcProtocol.ts
// with small modifications
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Channel, Disposable, DisposableCollection, isObject, ReadBuffer, RpcProtocol, URI, WriteBuffer } from '@theia/core';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageProvider } from '@theia/core/lib/common/message-rpc/channel';
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer';
import { MsgPackExtensionManager } from '@theia/core/lib/common/message-rpc/msg-pack-extension-manager';
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { Range, Position } from '../plugin/types-impl';
export interface MessageConnection {
send(msg: string): void;
onMessage: Event<string>;
}
export const RPCProtocol = Symbol.for('RPCProtocol');
export interface RPCProtocol extends Disposable {
/**
* Returns a proxy to an object addressable/named in the plugin process or in the main process.
*/
getProxy<T>(proxyId: ProxyIdentifier<T>): T;
/**
* Register manually created instance.
*/
set<T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R;
}
export class ProxyIdentifier<T> {
public readonly id: string;
constructor(public readonly isMain: boolean, id: string | T) {
// TODO this is nasty, rewrite this
this.id = (id as any).toString();
}
}
export function createProxyIdentifier<T>(identifier: string): ProxyIdentifier<T> {
return new ProxyIdentifier(false, identifier);
}
export interface ConnectionClosedError extends Error {
code: 'RPC_PROTOCOL_CLOSED'
}
export namespace ConnectionClosedError {
const code: ConnectionClosedError['code'] = 'RPC_PROTOCOL_CLOSED';
export function create(message: string = 'connection is closed'): ConnectionClosedError {
return Object.assign(new Error(message), { code });
}
export function is(error: unknown): error is ConnectionClosedError {
return isObject(error) && 'code' in error && (error as ConnectionClosedError).code === code;
}
}
export class RPCProtocolImpl implements RPCProtocol {
private readonly locals = new Map<string, any>();
private readonly proxies = new Map<string, any>();
private readonly rpc: RpcProtocol;
private readonly toDispose = new DisposableCollection(
Disposable.create(() => { /* mark as no disposed */ })
);
constructor(channel: Channel) {
this.rpc = new RpcProtocol(new BatchingChannel(channel), (method, args) => this.handleRequest(method, args));
this.rpc.onNotification((evt: { method: string; args: any[]; }) => this.handleNotification(evt.method, evt.args));
this.toDispose.push(Disposable.create(() => this.proxies.clear()));
}
handleNotification(method: any, args: any[]): void {
const serviceId = args[0] as string;
const handler: any = this.locals.get(serviceId);
if (!handler) {
throw new Error(`no local service handler with id ${serviceId}`);
}
handler[method](...(args.slice(1)));
}
handleRequest(method: string, args: any[]): Promise<any> {
const serviceId = args[0] as string;
const handler: any = this.locals.get(serviceId);
if (!handler) {
throw new Error(`no local service handler with id ${serviceId}`);
}
return handler[method](...(args.slice(1)));
}
dispose(): void {
this.toDispose.dispose();
}
protected get isDisposed(): boolean {
return this.toDispose.disposed;
}
getProxy<T>(proxyId: ProxyIdentifier<T>): T {
if (this.isDisposed) {
throw ConnectionClosedError.create();
}
let proxy = this.proxies.get(proxyId.id);
if (!proxy) {
proxy = this.createProxy(proxyId.id);
this.proxies.set(proxyId.id, proxy);
}
return proxy;
}
protected createProxy<T>(proxyId: string): T {
const handler = {
get: (target: any, name: string, receiver: any): any => {
if (target[name] || name.charCodeAt(0) !== 36 /* CharCode.DollarSign */) {
// not a remote property
return target[name];
}
const isNotify = this.isNotification(name);
return async (...args: any[]) => {
const method = name.toString();
if (isNotify) {
this.rpc.sendNotification(method, [proxyId, ...args]);
} else {
return await this.rpc.sendRequest(method, [proxyId, ...args]) as Promise<any>;
}
};
}
};
return new Proxy(Object.create(null), handler);
}
/**
* Return whether the given property represents a notification. If true,
* the promise returned from the invocation will resolve immediately to `undefined`
*
* A property leads to a notification rather than a method call if its name
* begins with `notify` or `on`.
*
* @param p - The property being called on the proxy.
* @return Whether `p` represents a notification.
*/
protected isNotification(p: PropertyKey): boolean {
let propertyString = p.toString();
if (propertyString.charCodeAt(0) === 36/* CharCode.DollarSign */) {
propertyString = propertyString.substring(1);
}
return propertyString.startsWith('notify') || propertyString.startsWith('on');
}
set<T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R {
if (this.isDisposed) {
throw ConnectionClosedError.create();
}
if (!this.locals.has(identifier.id)) {
this.locals.set(identifier.id, instance);
if (Disposable.is(instance)) {
this.toDispose.push(instance);
}
this.toDispose.push(Disposable.create(() => this.locals.delete(identifier.id)));
}
return instance;
}
}
/**
* Wraps and underlying channel to send/receive multiple messages in one go:
* - multiple messages to be sent from one stack get sent in bulk at `process.nextTick`.
* - each incoming message is handled in a separate `process.nextTick`.
*/
export class BatchingChannel implements Channel {
protected messagesToSend: Uint8Array[] = [];
constructor(protected underlyingChannel: Channel) {
underlyingChannel.onMessage(msg => this.handleMessages(msg()));
}
protected onMessageEmitter: Emitter<MessageProvider> = new Emitter();
get onMessage(): Event<MessageProvider> {
return this.onMessageEmitter.event;
};
readonly onClose = this.underlyingChannel.onClose;
readonly onError = this.underlyingChannel.onError;
close(): void {
this.underlyingChannel.close();
this.onMessageEmitter.dispose();
this.messagesToSend = [];
}
getWriteBuffer(): WriteBuffer {
const writer = new Uint8ArrayWriteBuffer();
writer.onCommit(buffer => this.commitSingleMessage(buffer));
return writer;
}
protected commitSingleMessage(msg: Uint8Array): void {
if (this.messagesToSend.length === 0) {
if (typeof setImmediate !== 'undefined') {
setImmediate(() => this.sendAccumulated());
} else {
setTimeout(() => this.sendAccumulated(), 0);
}
}
this.messagesToSend.push(msg);
}
protected sendAccumulated(): void {
const cachedMessages = this.messagesToSend;
this.messagesToSend = [];
const writer = this.underlyingChannel.getWriteBuffer();
if (cachedMessages.length > 0) {
writer.writeLength(cachedMessages.length);
cachedMessages.forEach(msg => {
writer.writeBytes(msg);
});
}
writer.commit();
}
protected handleMessages(buffer: ReadBuffer): void {
// Read in the list of messages and dispatch each message individually
const length = buffer.readLength();
if (length > 0) {
for (let index = 0; index < length; index++) {
const message = buffer.readBytes();
this.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(message));
}
}
}
}
export const enum MsgPackExtensionTag {
Uri = 2,
// eslint-disable-next-line @typescript-eslint/no-shadow
Range = 3,
VsCodeUri = 4,
// eslint-disable-next-line @typescript-eslint/no-shadow
BinaryBuffer = 5,
}
export function registerMsgPackExtensions(): void {
MsgPackExtensionManager.getInstance().registerExtensions(
{
class: URI,
tag: MsgPackExtensionTag.Uri,
serialize: (instance: URI) => instance.toString(),
deserialize: data => new URI(data)
},
{
class: Range,
tag: MsgPackExtensionTag.Range,
serialize: (range: Range) => ({
start: {
line: range.start.line,
character: range.start.character
},
end: {
line: range.end.line,
character: range.end.character
}
}),
deserialize: data => {
const start = new Position(data.start.line, data.start.character);
const end = new Position(data.end.line, data.end.character);
return new Range(start, end);
}
},
{
class: VSCodeURI,
tag: MsgPackExtensionTag.VsCodeUri,
// eslint-disable-next-line arrow-body-style
serialize: (instance: URI) => {
return instance.toString();
},
deserialize: data => VSCodeURI.parse(data)
},
{
class: BinaryBuffer,
tag: MsgPackExtensionTag.BinaryBuffer,
// eslint-disable-next-line arrow-body-style
serialize: (instance: BinaryBuffer) => {
return instance.buffer;
},
// eslint-disable-next-line arrow-body-style
deserialize: buffer => {
return BinaryBuffer.wrap(buffer);
}
}
);
}

View File

@@ -0,0 +1,182 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/microsoft/vscode/blob/0eb3a02ca2bcfab5faa3dc6e52d7c079efafcab0/src/vs/workbench/api/common/shared/semanticTokensDto.ts
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
let _isLittleEndian = true;
let _isLittleEndianComputed = false;
function isLittleEndian(): boolean {
if (!_isLittleEndianComputed) {
_isLittleEndianComputed = true;
const test = new Uint8Array(2);
test[0] = 1;
test[1] = 2;
const view = new Uint16Array(test.buffer);
_isLittleEndian = (view[0] === (2 << 8) + 1);
}
return _isLittleEndian;
}
export interface IFullSemanticTokensDto {
id: number;
type: 'full';
data: Uint32Array;
}
export interface IDeltaSemanticTokensDto {
id: number;
type: 'delta';
deltas: { start: number; deleteCount: number; data?: Uint32Array; }[];
}
export type ISemanticTokensDto = IFullSemanticTokensDto | IDeltaSemanticTokensDto;
const enum EncodedSemanticTokensType {
Full = 1,
Delta = 2
}
function reverseEndianness(arr: Uint8Array): void {
for (let i = 0, len = arr.length; i < len; i += 4) {
// flip bytes 0<->3 and 1<->2
const b0 = arr[i + 0];
const b1 = arr[i + 1];
const b2 = arr[i + 2];
const b3 = arr[i + 3];
arr[i + 0] = b3;
arr[i + 1] = b2;
arr[i + 2] = b1;
arr[i + 3] = b0;
}
}
function toLittleEndianBuffer(arr: Uint32Array): BinaryBuffer {
const uint8Arr = new Uint8Array(arr.buffer, arr.byteOffset, arr.length * 4);
if (!isLittleEndian()) {
// the byte order must be changed
reverseEndianness(uint8Arr);
}
return BinaryBuffer.wrap(uint8Arr);
}
function fromLittleEndianBuffer(buff: BinaryBuffer): Uint32Array {
const uint8Arr = buff.buffer;
if (!isLittleEndian()) {
// the byte order must be changed
reverseEndianness(uint8Arr);
}
if (uint8Arr.byteOffset % 4 === 0) {
return new Uint32Array(uint8Arr.buffer, uint8Arr.byteOffset, uint8Arr.length / 4);
} else {
// unaligned memory access doesn't work on all platforms
const data = new Uint8Array(uint8Arr.byteLength);
data.set(uint8Arr);
return new Uint32Array(data.buffer, data.byteOffset, data.length / 4);
}
}
export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): BinaryBuffer {
const dest = new Uint32Array(encodeSemanticTokensDtoSize(semanticTokens));
let offset = 0;
dest[offset++] = semanticTokens.id;
if (semanticTokens.type === 'full') {
dest[offset++] = EncodedSemanticTokensType.Full;
dest[offset++] = semanticTokens.data.length;
dest.set(semanticTokens.data, offset); offset += semanticTokens.data.length;
} else {
dest[offset++] = EncodedSemanticTokensType.Delta;
dest[offset++] = semanticTokens.deltas.length;
for (const delta of semanticTokens.deltas) {
dest[offset++] = delta.start;
dest[offset++] = delta.deleteCount;
if (delta.data) {
dest[offset++] = delta.data.length;
dest.set(delta.data, offset); offset += delta.data.length;
} else {
dest[offset++] = 0;
}
}
}
return toLittleEndianBuffer(dest);
}
function encodeSemanticTokensDtoSize(semanticTokens: ISemanticTokensDto): number {
let result = 0;
result += (
+ 1 // id
+ 1 // type
);
if (semanticTokens.type === 'full') {
result += (
+ 1 // data length
+ semanticTokens.data.length
);
} else {
result += (
+ 1 // delta count
);
result += (
+ 1 // start
+ 1 // deleteCount
+ 1 // data length
) * semanticTokens.deltas.length;
for (const delta of semanticTokens.deltas) {
if (delta.data) {
result += delta.data.length;
}
}
}
return result;
}
export function decodeSemanticTokensDto(_buff: BinaryBuffer): ISemanticTokensDto {
const src = fromLittleEndianBuffer(_buff);
let offset = 0;
const id = src[offset++];
const type: EncodedSemanticTokensType = src[offset++];
if (type === EncodedSemanticTokensType.Full) {
const length = src[offset++];
const data = src.subarray(offset, offset + length); offset += length;
return {
id: id,
type: 'full',
data: data
};
}
const deltaCount = src[offset++];
const deltas: { start: number; deleteCount: number; data?: Uint32Array; }[] = [];
for (let i = 0; i < deltaCount; i++) {
const start = src[offset++];
const deleteCount = src[offset++];
const length = src[offset++];
let data: Uint32Array | undefined;
if (length > 0) {
data = src.subarray(offset, offset + length); offset += length;
}
deltas[i] = { start, deleteCount, data };
}
return {
id: id,
type: 'delta',
deltas: deltas
};
}

View File

@@ -0,0 +1,168 @@
// *****************************************************************************
// Copyright (C) 2023 Mathieu Bussieres 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation and others. All rights reserved.
* Licensed under the MIT License. See https://github.com/Microsoft/vscode/blob/master/LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
// Based on https://github.com/microsoft/vscode/blob/1.72.2/src/vs/workbench/contrib/testing/common/testTypes.ts
/* eslint-disable import/no-extraneous-dependencies */
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { UriComponents } from './uri-components';
import { Location, Range } from './plugin-api-rpc-model';
import { isObject } from '@theia/core';
import * as languageProtocol from '@theia/core/shared/vscode-languageserver-protocol';
export enum TestRunProfileKind {
Run = 1,
Debug = 2,
Coverage = 3
}
export interface TestRunProfileDTO {
readonly id: string;
readonly label: string;
readonly kind: TestRunProfileKind;
readonly isDefault: boolean;
readonly tag: string;
readonly canConfigure: boolean;
}
export interface TestRunDTO {
readonly id: string;
readonly name: string;
readonly isRunning: boolean;
}
export interface TestOutputDTO {
readonly output: string;
readonly location?: Location;
readonly itemPath?: string[];
}
export enum TestExecutionState {
Queued = 1,
Running = 2,
Passed = 3,
Failed = 4,
Skipped = 5,
Errored = 6
}
export interface TestStateChangeDTO {
readonly state: TestExecutionState;
readonly itemPath: string[];
}
export interface TestFailureDTO extends TestStateChangeDTO {
readonly state: TestExecutionState.Failed | TestExecutionState.Errored;
readonly messages: TestMessageDTO[];
readonly duration?: number;
}
export namespace TestFailureDTO {
export function is(ref: unknown): ref is TestFailureDTO {
return isObject<TestFailureDTO>(ref)
&& (ref.state === TestExecutionState.Failed || ref.state === TestExecutionState.Errored);
}
}
export interface TestSuccessDTO extends TestStateChangeDTO {
readonly state: TestExecutionState.Passed;
readonly duration?: number;
}
export interface TestMessageStackFrameDTO {
uri?: languageProtocol.DocumentUri;
position?: languageProtocol.Position;
label: string;
}
export interface TestMessageDTO {
readonly expected?: string;
readonly actual?: string;
readonly location?: languageProtocol.Location;
readonly message: string | MarkdownString;
readonly contextValue?: string;
readonly stackTrace?: TestMessageStackFrameDTO[];
}
export interface TestItemDTO {
readonly id: string;
readonly label: string;
readonly range?: Range;
readonly sortKey?: string;
readonly tags: string[];
readonly uri?: UriComponents;
readonly busy: boolean;
readonly canResolveChildren: boolean;
readonly description?: string;
readonly error?: string | MarkdownString
readonly children?: TestItemDTO[];
}
export interface TestRunRequestDTO {
controllerId: string;
profileId: string;
name: string;
includedTests: string[][]; // array of paths
excludedTests: string[][]; // array of paths
preserveFocus: boolean;
}
export interface TestItemReference {
typeTag: '$type_test_item_reference',
controllerId: string;
testPath: string[];
}
export namespace TestItemReference {
export function is(ref: unknown): ref is TestItemReference {
return isObject<TestItemReference>(ref)
&& ref.typeTag === '$type_test_item_reference'
&& typeof ref.controllerId === 'string'
&& Array.isArray(ref.testPath);
}
export function create(controllerId: string, testPath: string[]): TestItemReference {
return {
typeTag: '$type_test_item_reference',
controllerId,
testPath
};
}
}
export interface TestMessageArg {
testItemReference: TestItemReference | undefined,
testMessage: TestMessageDTO
}
export namespace TestMessageArg {
export function is(arg: unknown): arg is TestMessageArg {
return isObject<TestMessageArg>(arg)
&& isObject<TestMessageDTO>(arg.testMessage)
&& (MarkdownString.is(arg.testMessage.message) || typeof arg.testMessage.message === 'string');
}
export function create(testItemReference: TestItemReference | undefined, testMessageDTO: TestMessageDTO): TestMessageArg {
return {
testItemReference: testItemReference,
testMessage: testMessageDTO
};
}
}

View File

@@ -0,0 +1,129 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// copied from https://github.com/microsoft/vscode/blob/1.37.0/src/vs/base/common/types.ts
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isObject as isObject0 } from '@theia/core/lib/common';
/**
* Returns `true` if the parameter has type "object" and not null, an array, a regexp, a date.
*/
export function isObject(obj: unknown): boolean {
return isObject0(obj)
&& !Array.isArray(obj)
&& !(obj instanceof RegExp)
&& !(obj instanceof Date);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function mixin(destination: any, source: any, overwrite: boolean = true): any {
if (!isObject(destination)) {
return source;
}
if (isObject(source)) {
Object.keys(source).forEach(key => {
if (key in destination) {
if (overwrite) {
if (isObject(destination[key]) && isObject(source[key])) {
mixin(destination[key], source[key], overwrite);
} else {
destination[key] = source[key];
}
}
} else {
destination[key] = source[key];
}
});
}
return destination;
}
export enum LogType {
Info,
Error
}
export interface LogPart {
data: string;
type: LogType;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface KeysToAnyValues { [key: string]: any }
export interface KeysToKeysToAnyValue { [key: string]: KeysToAnyValues }
/* eslint-disable @typescript-eslint/no-explicit-any */
/** copied from https://github.com/TypeFox/vscode/blob/70b8db24a37fafc77247de7f7cb5bb0195120ed0/src/vs/workbench/api/common/extHostTypes.ts#L18-L27 */
export function es5ClassCompat<T extends Function>(target: T): T {
// @ts-ignore
function _(): any { return Reflect.construct(target, arguments, this.constructor); }
Object.defineProperty(_, 'name', Object.getOwnPropertyDescriptor(target, 'name')!);
Object.setPrototypeOf(_, target);
Object.setPrototypeOf(_.prototype, target.prototype);
return _ as unknown as T;
}
/* eslint-enable @typescript-eslint/no-explicit-any */
const _typeof = {
number: 'number',
string: 'string',
undefined: 'undefined',
object: 'object',
function: 'function'
};
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @returns whether the provided parameter is a JavaScript Array or not.
*/
export function isArray(array: any): array is any[] {
if (Array.isArray) {
return Array.isArray(array);
}
if (array && typeof (array.length) === _typeof.number && array.constructor === Array) {
return true;
}
return false;
}
/**
* @returns whether the provided parameter is undefined.
*/
export function isUndefined(obj: any): obj is undefined {
return typeof (obj) === _typeof.undefined;
}
/**
* @returns whether the provided parameter is undefined or null.
*/
export function isUndefinedOrNull(obj: any): obj is undefined | null {
return isUndefined(obj) || obj === null; // eslint-disable-line no-null/no-null
}
/**
* Asserts that the argument passed in is neither undefined nor null.
*/
export function assertIsDefined<T>(arg: T | null | undefined): T {
if (isUndefinedOrNull(arg)) {
throw new Error('Assertion Failed: argument is undefined or null');
}
return arg;
}

View File

@@ -0,0 +1,37 @@
// *****************************************************************************
// Copyright (C) 2020 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/uint.ts
export const enum Constants {
/**
* Max unsigned integer that fits on 8 bits.
*/
MAX_UINT_8 = 255, // 2^8 - 1
}
export function toUint8(v: number): number {
if (v < 0) {
return 0;
}
if (v > Constants.MAX_UINT_8) {
return Constants.MAX_UINT_8;
}
return v | 0;
}

View File

@@ -0,0 +1,84 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriComponents } from '@theia/core/lib/common/uri';
import { CellUri } from '@theia/notebook/lib/common';
export { UriComponents };
// some well known URI schemas
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/base/common/network.ts#L9-L79
// TODO move to network.ts file
export namespace Schemes {
/**
* A schema that is used for models that exist in memory
* only and that have no correspondence on a server or such.
*/
export const inMemory = 'inmemory';
/**
* A schema that is used for setting files
*/
export const vscode = 'vscode';
/**
* A schema that is used for internal private files
*/
export const internal = 'private';
/**
* A walk-through document.
*/
export const walkThrough = 'walkThrough';
/**
* An embedded code snippet.
*/
export const walkThroughSnippet = 'walkThroughSnippet';
export const http = 'http';
export const https = 'https';
export const file = 'file';
export const mailto = 'mailto';
export const untitled = 'untitled';
export const data = 'data';
export const command = 'command';
export const vscodeRemote = 'vscode-remote';
export const vscodeRemoteResource = 'vscode-remote-resource';
export const userData = 'vscode-userdata';
export const vscodeCustomEditor = 'vscode-custom-editor';
export const vscodeSettings = 'vscode-settings';
export const vscodeNotebookCell = CellUri.cellUriScheme;
export const webviewPanel = 'webview-panel';
}

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { Emitter, Event } from '@theia/core/lib/common/event';
import { HostedPluginClient } from '../../common/plugin-protocol';
import { LogPart } from '../../common/types';
@injectable()
export class HostedPluginWatcher {
private onPostMessage = new Emitter<{ pluginHostId: string, message: Uint8Array }>();
private onLogMessage = new Emitter<LogPart>();
private readonly onDidDeployEmitter = new Emitter<void>();
readonly onDidDeploy = this.onDidDeployEmitter.event;
getHostedPluginClient(): HostedPluginClient {
const messageEmitter = this.onPostMessage;
const logEmitter = this.onLogMessage;
return {
postMessage(pluginHostId, message: Uint8Array): Promise<void> {
messageEmitter.fire({ pluginHostId, message });
return Promise.resolve();
},
log(logPart: LogPart): Promise<void> {
logEmitter.fire(logPart);
return Promise.resolve();
},
onDidDeploy: () => this.onDidDeployEmitter.fire(undefined)
};
}
get onPostMessageEvent(): Event<{ pluginHostId: string, message: Uint8Array }> {
return this.onPostMessage.event;
}
get onLogMessageEvent(): Event<LogPart> {
return this.onLogMessage.event;
}
}

View File

@@ -0,0 +1,635 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import { generateUuid } from '@theia/core/lib/common/uuid';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { PluginWorker } from './plugin-worker';
import { getPluginId, DeployedPlugin, HostedPluginServer } from '../../common/plugin-protocol';
import { HostedPluginWatcher } from './hosted-plugin-watcher';
import { ExtensionKind, MAIN_RPC_CONTEXT, PluginManagerExt, UIKind } from '../../common/plugin-api-rpc';
import { setUpPluginApi } from '../../main/browser/main-context';
import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol';
import {
Disposable, DisposableCollection, isCancelled,
CommandRegistry, WillExecuteCommandEvent,
CancellationTokenSource, ProgressService, nls,
RpcProxy
} from '@theia/core';
import { PreferenceServiceImpl, PreferenceProviderProvider } from '@theia/core/lib/common/preferences';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginContributionHandler } from '../../main/browser/plugin-contribution-handler';
import { getQueryParameters } from '../../main/browser/env-main';
import { getPreferences } from '../../main/browser/preference-registry-main';
import { Deferred, waitForEvent } from '@theia/core/lib/common/promise-util';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { Event, WaitUntilEvent } from '@theia/core/lib/common/event';
import { FileSearchService } from '@theia/file-search/lib/common/file-search-service';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { PluginViewRegistry } from '../../main/browser/view/plugin-view-registry';
import { WillResolveTaskProvider, TaskProviderRegistry, TaskResolverRegistry } from '@theia/task/lib/browser/task-contribution';
import { TaskDefinitionRegistry } from '@theia/task/lib/browser/task-definition-registry';
import { WebviewEnvironment } from '../../main/browser/webview/webview-environment';
import { WebviewWidget } from '../../main/browser/webview/webview';
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import URI from '@theia/core/lib/common/uri';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store';
import { FileService, FileSystemProviderActivationEvent } from '@theia/filesystem/lib/browser/file-service';
import { PluginCustomEditorRegistry } from '../../main/browser/custom-editors/plugin-custom-editor-registry';
import { CustomEditorWidget } from '../../main/browser/custom-editors/custom-editor-widget';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/languages/language';
import { LanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageService';
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer';
import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel';
import { NotebookTypeRegistry, NotebookService, NotebookRendererMessagingService } from '@theia/notebook/lib/browser';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import {
AbstractHostedPluginSupport, PluginContributions, PluginHost,
ALL_ACTIVATION_EVENT, isConnectionScopedBackendPlugin
} from '../common/hosted-plugin';
import { isRemote } from '@theia/core/lib/browser/browser';
export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker' | 'onDebugDynamicConfigurations';
export const PluginProgressLocation = 'plugin';
@injectable()
export class HostedPluginSupport extends AbstractHostedPluginSupport<PluginManagerExt, RpcProxy<HostedPluginServer>> {
protected static ADDITIONAL_ACTIVATION_EVENTS_ENV = 'ADDITIONAL_ACTIVATION_EVENTS';
protected static BUILTIN_ACTIVATION_EVENTS = [
'*',
'onLanguage',
'onCommand',
'onDebug',
'onDebugInitialConfigurations',
'onDebugResolve',
'onDebugAdapterProtocolTracker',
'onDebugDynamicConfigurations',
'onTaskType',
'workspaceContains',
'onView',
'onUri',
'onTerminalProfile',
'onWebviewPanel',
'onFileSystem',
'onCustomEditor',
'onStartupFinished',
'onAuthenticationRequest',
'onNotebook',
'onNotebookSerializer'
];
@inject(HostedPluginWatcher)
protected readonly watcher: HostedPluginWatcher;
@inject(PluginContributionHandler)
protected readonly contributionHandler: PluginContributionHandler;
@inject(PreferenceProviderProvider)
protected readonly preferenceProviderProvider: PreferenceProviderProvider;
@inject(PreferenceServiceImpl)
protected readonly preferenceServiceImpl: PreferenceServiceImpl;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(NotebookService)
protected readonly notebookService: NotebookService;
@inject(NotebookRendererMessagingService)
protected readonly notebookRendererMessagingService: NotebookRendererMessagingService;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(DebugSessionManager)
protected readonly debugSessionManager: DebugSessionManager;
@inject(DebugConfigurationManager)
protected readonly debugConfigurationManager: DebugConfigurationManager;
@inject(FileService)
protected readonly fileService: FileService;
@inject(FileSearchService)
protected readonly fileSearchService: FileSearchService;
@inject(FrontendApplicationStateService)
protected readonly appState: FrontendApplicationStateService;
@inject(NotebookTypeRegistry)
protected readonly notebookTypeRegistry: NotebookTypeRegistry;
@inject(PluginViewRegistry)
protected readonly viewRegistry: PluginViewRegistry;
@inject(TaskProviderRegistry)
protected readonly taskProviderRegistry: TaskProviderRegistry;
@inject(TaskResolverRegistry)
protected readonly taskResolverRegistry: TaskResolverRegistry;
@inject(TaskDefinitionRegistry)
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
@inject(ProgressService)
protected readonly progressService: ProgressService;
@inject(WebviewEnvironment)
protected readonly webviewEnvironment: WebviewEnvironment;
@inject(WidgetManager)
protected readonly widgets: WidgetManager;
@inject(TerminalService)
protected readonly terminalService: TerminalService;
@inject(JsonSchemaStore)
protected readonly jsonSchemaStore: JsonSchemaStore;
@inject(PluginCustomEditorRegistry)
protected readonly customEditorRegistry: PluginCustomEditorRegistry;
@inject(ApplicationServer)
protected readonly applicationServer: ApplicationServer;
constructor() {
super(generateUuid());
}
@postConstruct()
protected override init(): void {
super.init();
this.workspaceService.onWorkspaceChanged(() => this.updateStoragePath());
const languageService = (StandaloneServices.get(ILanguageService) as LanguageService);
for (const language of languageService['_requestedBasicLanguages'] as Set<string>) {
this.activateByLanguage(language);
}
languageService.onDidRequestBasicLanguageFeatures(language => this.activateByLanguage(language));
this.commands.onWillExecuteCommand(event => this.ensureCommandHandlerRegistration(event));
this.debugSessionManager.onWillStartDebugSession(event => this.ensureDebugActivation(event));
this.debugSessionManager.onWillResolveDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugResolve', event.debugType));
this.debugConfigurationManager.onWillProvideDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugInitialConfigurations'));
// Activate all providers of dynamic configurations, i.e. Let the user pick a configuration from all the available ones.
this.debugConfigurationManager.onWillProvideDynamicDebugConfiguration(event => this.ensureDebugActivation(event, 'onDebugDynamicConfigurations', ALL_ACTIVATION_EVENT));
this.viewRegistry.onDidExpandView(id => this.activateByView(id));
this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event));
this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event));
this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event));
this.customEditorRegistry.onWillOpenCustomEditor(event => this.activateByCustomEditor(event));
this.notebookService.onWillOpenNotebook(async event => this.activateByNotebook(event));
this.notebookRendererMessagingService.onWillActivateRenderer(rendererId => this.activateByNotebookRenderer(rendererId));
this.widgets.onDidCreateWidget(({ factoryId, widget }) => {
// note: state restoration of custom editors is handled in `PluginCustomEditorRegistry.init`
if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) {
const storeState = widget.storeState.bind(widget);
const restoreState = widget.restoreState.bind(widget);
widget.storeState = () => {
if (this.webviewRevivers.has(widget.viewType)) {
return storeState();
}
return undefined;
};
widget.restoreState = state => {
if (state.viewType) {
restoreState(state);
this.preserveWebview(widget);
} else {
widget.dispose();
}
};
}
});
}
protected createTheiaReadyPromise(): Promise<unknown> {
return Promise.all([this.preferenceServiceImpl.ready, this.workspaceService.roots]);
}
protected override runOperation(operation: () => Promise<void>): Promise<void> {
return this.progressService.withProgress('', PluginProgressLocation, () => this.doLoad());
}
protected override afterStart(): void {
this.watcher.onDidDeploy(() => this.load());
this.server.onDidOpenConnection(() => this.load());
}
// Only load connection-scoped plugins
protected acceptPlugin(plugin: DeployedPlugin): boolean {
return isConnectionScopedBackendPlugin(plugin);
}
protected override async beforeSyncPlugins(toDisconnect: DisposableCollection): Promise<void> {
await super.beforeSyncPlugins(toDisconnect);
toDisconnect.push(Disposable.create(() => this.preserveWebviews()));
this.server.onDidCloseConnection(() => toDisconnect.dispose());
}
protected override async beforeLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
// make sure that the previous state, including plugin widgets, is restored
// and core layout is initialized, i.e. explorer, scm, debug views are already added to the shell
// but shell is not yet revealed
await this.appState.reachedState('initialized_layout');
}
protected override async afterLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
await this.viewRegistry.initWidgets();
// remove restored plugin widgets which were not registered by contributions
this.viewRegistry.removeStaleWidgets();
}
protected handleContributions(plugin: DeployedPlugin): Disposable {
return this.contributionHandler.handleContributions(this.clientId, plugin);
}
protected override handlePluginStarted(manager: PluginManagerExt, plugin: DeployedPlugin): void {
this.activateByWorkspaceContains(manager, plugin);
}
protected async obtainManager(host: string, hostContributions: PluginContributions[], toDisconnect: DisposableCollection): Promise<PluginManagerExt | undefined> {
let manager = this.managers.get(host);
if (!manager) {
const pluginId = getPluginId(hostContributions[0].plugin.metadata.model);
const rpc = this.initRpc(host, pluginId);
toDisconnect.push(rpc);
manager = rpc.getProxy(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT);
this.managers.set(host, manager);
toDisconnect.push(Disposable.create(() => this.managers.delete(host)));
const [extApi, globalState, workspaceState, webviewResourceRoot, webviewCspSource, defaultShell, jsonValidation] = await Promise.all([
this.server.getExtPluginAPI(),
this.pluginServer.getAllStorageValues(undefined),
this.pluginServer.getAllStorageValues({
workspace: this.workspaceService.workspace?.resource.toString(),
roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString())
}),
this.webviewEnvironment.resourceRoot(host),
this.webviewEnvironment.cspSource(),
this.terminalService.getDefaultShell(),
this.jsonSchemaStore.schemas
]);
if (toDisconnect.disposed) {
return undefined;
}
const isElectron = environment.electron.is();
const supportedActivationEvents = [...HostedPluginSupport.BUILTIN_ACTIVATION_EVENTS];
const [additionalActivationEvents, appRoot] = await Promise.all([
this.envServer.getValue(HostedPluginSupport.ADDITIONAL_ACTIVATION_EVENTS_ENV),
this.applicationServer.getApplicationRoot()
]);
if (additionalActivationEvents && additionalActivationEvents.value) {
additionalActivationEvents.value.split(',').forEach(event => supportedActivationEvents.push(event));
}
await manager.$init({
preferences: getPreferences(this.preferenceProviderProvider, this.workspaceService.tryGetRoots()),
globalState,
workspaceState,
env: {
queryParams: getQueryParameters(),
language: nls.locale || nls.defaultLocale,
shell: defaultShell,
uiKind: isElectron ? UIKind.Desktop : UIKind.Web,
appName: FrontendApplicationConfigProvider.get().applicationName,
appHost: isElectron ? 'desktop' : 'web', // TODO: 'web' could be the embedder's name, e.g. 'github.dev'
appRoot,
appUriScheme: FrontendApplicationConfigProvider.get().electron.uriScheme
},
extApi,
webview: {
webviewResourceRoot,
webviewCspSource
},
jsonValidation,
pluginKind: isRemote ? ExtensionKind.Workspace : ExtensionKind.UI,
supportedActivationEvents
});
if (toDisconnect.disposed) {
return undefined;
}
this.activationEvents.forEach(event => manager!.$activateByEvent(event));
}
return manager;
}
protected initRpc(host: PluginHost, pluginId: string): RPCProtocol {
const rpc = host === 'frontend' ? new PluginWorker().rpc : this.createServerRpc(host);
setUpPluginApi(rpc, this.container);
this.mainPluginApiProviders.getContributions().forEach(p => p.initialize(rpc, this.container));
return rpc;
}
protected createServerRpc(pluginHostId: string): RPCProtocol {
const channel = new BasicChannel(() => {
const writer = new Uint8ArrayWriteBuffer();
writer.onCommit(buffer => {
this.server.onMessage(pluginHostId, buffer);
});
return writer;
});
// Create RPC protocol before adding the listener to the watcher to receive the watcher's cached messages after the rpc protocol was created.
const rpc = new RPCProtocolImpl(channel);
this.watcher.onPostMessageEvent(received => {
if (pluginHostId === received.pluginHostId) {
channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(received.message));
}
});
return rpc;
}
protected async updateStoragePath(): Promise<void> {
const path = await this.getStoragePath();
for (const manager of this.managers.values()) {
manager.$updateStoragePath(path);
}
}
protected async getStoragePath(): Promise<string | undefined> {
const roots = await this.workspaceService.roots;
return this.pluginPathsService.getHostStoragePath(this.workspaceService.workspace?.resource.toString(), roots.map(root => root.resource.toString()));
}
protected async getHostGlobalStoragePath(): Promise<string> {
const configDirUri = await this.envServer.getConfigDirUri();
const globalStorageFolderUri = new URI(configDirUri).resolve('globalStorage');
// Make sure that folder by the path exists
if (!await this.fileService.exists(globalStorageFolderUri)) {
await this.fileService.createFolder(globalStorageFolderUri, { fromUserGesture: false });
}
const globalStorageFolderFsPath = await this.fileService.fsPath(globalStorageFolderUri);
if (!globalStorageFolderFsPath) {
throw new Error(`Could not resolve the FS path for URI: ${globalStorageFolderUri}`);
}
return globalStorageFolderFsPath;
}
async activateByViewContainer(viewContainerId: string): Promise<void> {
await Promise.all(this.viewRegistry.getContainerViews(viewContainerId).map(viewId => this.activateByView(viewId)));
}
async activateByView(viewId: string): Promise<void> {
await this.activateByEvent(`onView:${viewId}`);
}
async activateByLanguage(languageId: string): Promise<void> {
await this.activateByEvent('onLanguage');
await this.activateByEvent(`onLanguage:${languageId}`);
}
async activateByUri(scheme: string, authority: string): Promise<void> {
await this.activateByEvent(`onUri:${scheme}://${authority}`);
}
async activateByCommand(commandId: string): Promise<void> {
await this.activateByEvent(`onCommand:${commandId}`);
}
async activateByTaskType(taskType: string): Promise<void> {
await this.activateByEvent(`onTaskType:${taskType}`);
}
async activateByCustomEditor(viewType: string): Promise<void> {
await this.activateByEvent(`onCustomEditor:${viewType}`);
}
async activateByNotebook(viewType: string): Promise<void> {
await this.activateByEvent(`onNotebook:${viewType}`);
}
async activateByNotebookSerializer(viewType: string): Promise<void> {
await this.activateByEvent(`onNotebookSerializer:${viewType}`);
}
async activateByNotebookRenderer(rendererId: string): Promise<void> {
await this.activateByEvent(`onRenderer:${rendererId}`);
}
activateByFileSystem(event: FileSystemProviderActivationEvent): Promise<void> {
return this.activateByEvent(`onFileSystem:${event.scheme}`);
}
activateByTerminalProfile(profileId: string): Promise<void> {
return this.activateByEvent(`onTerminalProfile:${profileId}`);
}
protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void {
event.waitUntil(this.activateByFileSystem(event).then(() => {
if (!this.fileService.hasProvider(event.scheme)) {
return waitForEvent(Event.filter(this.fileService.onDidChangeFileSystemProviderRegistrations,
({ added, scheme }) => added && scheme === event.scheme), 3000);
}
}));
}
protected ensureCommandHandlerRegistration(event: WillExecuteCommandEvent): void {
const activation = this.activateByCommand(event.commandId);
if (this.commands.getCommand(event.commandId) &&
(!this.contributionHandler.hasCommand(event.commandId) ||
this.contributionHandler.hasCommandHandler(event.commandId))) {
return;
}
const waitForCommandHandler = new Deferred<void>();
const listener = this.contributionHandler.onDidRegisterCommandHandler(id => {
if (id === event.commandId) {
listener.dispose();
waitForCommandHandler.resolve();
}
});
const p = Promise.all([
activation,
waitForCommandHandler.promise
]);
p.then(() => listener.dispose(), () => listener.dispose());
event.waitUntil(p);
}
protected ensureTaskActivation(event: WillResolveTaskProvider): void {
const promises = [this.activateByCommand('workbench.action.tasks.runTask')];
const taskType = event.taskType;
if (taskType) {
if (taskType === ALL_ACTIVATION_EVENT) {
for (const taskDefinition of this.taskDefinitionRegistry.getAll()) {
promises.push(this.activateByTaskType(taskDefinition.taskType));
}
} else {
promises.push(this.activateByTaskType(taskType));
}
}
event.waitUntil(Promise.all(promises));
}
protected ensureDebugActivation(event: WaitUntilEvent, activationEvent?: DebugActivationEvent, debugType?: string): void {
event.waitUntil(this.activateByDebug(activationEvent, debugType));
}
async activateByDebug(activationEvent?: DebugActivationEvent, debugType?: string): Promise<void> {
const promises = [this.activateByEvent('onDebug')];
if (activationEvent) {
promises.push(this.activateByEvent(activationEvent));
if (debugType) {
promises.push(this.activateByEvent(activationEvent + ':' + debugType));
}
}
await Promise.all(promises);
}
protected async activateByWorkspaceContains(manager: PluginManagerExt, plugin: DeployedPlugin): Promise<void> {
const activationEvents = plugin.contributes && plugin.contributes.activationEvents;
if (!activationEvents) {
return;
}
const paths: string[] = [];
const includePatterns: string[] = [];
// should be aligned with https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts#L460-L469
for (const activationEvent of activationEvents) {
if (/^workspaceContains:/.test(activationEvent)) {
const fileNameOrGlob = activationEvent.substring('workspaceContains:'.length);
if (fileNameOrGlob.indexOf(ALL_ACTIVATION_EVENT) >= 0 || fileNameOrGlob.indexOf('?') >= 0) {
includePatterns.push(fileNameOrGlob);
} else {
paths.push(fileNameOrGlob);
}
}
}
const activatePlugin = () => manager.$activateByEvent(`onPlugin:${plugin.metadata.model.id}`);
const promises: Promise<boolean>[] = [];
if (paths.length) {
promises.push(this.workspaceService.containsSome(paths));
}
if (includePatterns.length) {
const tokenSource = new CancellationTokenSource();
const searchTimeout = setTimeout(() => {
tokenSource.cancel();
// activate eagerly if took to long to search
activatePlugin();
}, 7000);
promises.push((async () => {
try {
const result = await this.fileSearchService.find('', {
rootUris: this.workspaceService.tryGetRoots().map(r => r.resource.toString()),
includePatterns,
limit: 1
}, tokenSource.token);
return result.length > 0;
} catch (e) {
if (!isCancelled(e)) {
console.error(e);
}
return false;
} finally {
clearTimeout(searchTimeout);
}
})());
}
if (promises.length && await Promise.all(promises).then(exists => exists.some(v => v))) {
await activatePlugin();
}
}
protected readonly webviewsToRestore = new Map<string, WebviewWidget>();
protected readonly webviewRevivers = new Map<string, (webview: WebviewWidget) => Promise<void>>();
registerWebviewReviver(viewType: string, reviver: (webview: WebviewWidget) => Promise<void>): void {
if (this.webviewRevivers.has(viewType)) {
throw new Error(`Reviver for ${viewType} already registered`);
}
this.webviewRevivers.set(viewType, reviver);
if (this.webviewsToRestore.has(viewType)) {
this.restoreWebview(this.webviewsToRestore.get(viewType) as WebviewWidget);
}
}
unregisterWebviewReviver(viewType: string): void {
this.webviewRevivers.delete(viewType);
}
protected async preserveWebviews(): Promise<void> {
for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) {
this.preserveWebview(webview as WebviewWidget);
}
for (const webview of this.widgets.getWidgets(CustomEditorWidget.FACTORY_ID)) {
(webview as CustomEditorWidget).modelRef.dispose();
if ((webview as any)['closeWithoutSaving']) {
delete (webview as any)['closeWithoutSaving'];
}
this.customEditorRegistry.resolveWidget(webview as CustomEditorWidget);
}
}
protected preserveWebview(webview: WebviewWidget): void {
if (!this.webviewsToRestore.has(webview.viewType)) {
this.activateByEvent(`onWebviewPanel:${webview.viewType}`);
this.webviewsToRestore.set(webview.viewType, webview);
webview.disposed.connect(() => this.webviewsToRestore.delete(webview.viewType));
}
}
protected async restoreWebview(webview: WebviewWidget): Promise<void> {
const restore = this.webviewRevivers.get(webview.viewType);
if (restore) {
try {
await restore(webview);
} catch (e) {
webview.setHTML(this.getDeserializationFailedContents(`
An error occurred while restoring '${webview.viewType}' view. Please check logs.
`));
console.error('Failed to restore the webview', e);
}
}
}
protected getDeserializationFailedContents(message: string): string {
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
</head>
<body>${message}</body>
</html>`;
}
}

View File

@@ -0,0 +1,52 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { BasicChannel } from '@theia/core/lib/common/message-rpc/channel';
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer';
import { injectable } from '@theia/core/shared/inversify';
import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol';
@injectable()
export class PluginWorker {
private worker: Worker;
public readonly rpc: RPCProtocol;
constructor() {
this.worker = new Worker(new URL('./worker/worker-main',
// @ts-expect-error (TS1343)
// We compile to CommonJS but `import.meta` is still available in the browser
import.meta.url));
const channel = new BasicChannel(() => {
const writer = new Uint8ArrayWriteBuffer();
writer.onCommit(buffer => {
this.worker.postMessage(buffer);
});
return writer;
});
this.rpc = new RPCProtocolImpl(channel);
// eslint-disable-next-line arrow-body-style
this.worker.onmessage = buffer => channel.onMessageEmitter.fire(() => {
return new Uint8ArrayReadBuffer(buffer.data);
});
this.worker.onerror = e => channel.onErrorEmitter.fire(e);
}
}

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// eslint-disable-next-line @theia/runtime-import-check
import { interfaces } from '@theia/core/shared/inversify';
import { DebugExtImpl } from '../../../plugin/debug/debug-ext';
/* eslint-disable @typescript-eslint/no-explicit-any */
export function createDebugExtStub(container: interfaces.Container): DebugExtImpl {
const delegate = container.get(DebugExtImpl);
return new Proxy(delegate, {
apply: function (target, that, args): void {
console.error('Debug API works only in plugin container');
}
});
}

View File

@@ -0,0 +1,114 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { PluginIdentifiers, PluginModel, PluginPackage } from '../../../common/plugin-protocol';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import URI from '@theia/core/lib/common/uri';
const NLS_REGEX = /^%([\w\d.-]+)%$/i;
function getUri(pluginModel: PluginModel, relativePath: string): URI {
const ownURI = new Endpoint().getRestUrl();
return ownURI.parent.resolve(PluginPackage.toPluginUrl(pluginModel, relativePath));
}
function readPluginFile(pluginModel: PluginModel, relativePath: string): Promise<string> {
return readContents(getUri(pluginModel, relativePath).toString());
}
function readContents(uri: string): Promise<string> {
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.onreadystatechange = function (): void {
if (this.readyState === XMLHttpRequest.DONE) {
if (this.status === 200) {
resolve(this.response);
} else if (this.status === 404) {
reject('NotFound');
} else {
reject(new Error('Could not fetch plugin resource'));
}
}
};
request.open('GET', uri, true);
request.send();
});
}
async function readPluginJson(pluginModel: PluginModel, relativePath: string): Promise<any> {
const content = await readPluginFile(pluginModel, relativePath);
const json = JSON.parse(content) as PluginPackage;
json.publisher ??= PluginIdentifiers.UNPUBLISHED;
return json;
}
export async function loadManifest(pluginModel: PluginModel): Promise<any> {
const [manifest, translations] = await Promise.all([
readPluginJson(pluginModel, 'package.json'),
loadTranslations(pluginModel)
]);
// translate vscode builtins, as they are published with a prefix.
const built_prefix = '@theia/vscode-builtin-';
if (manifest && manifest.name && manifest.name.startsWith(built_prefix)) {
manifest.name = manifest.name.substring(built_prefix.length);
}
return manifest && translations && Object.keys(translations).length ?
localize(manifest, translations) :
manifest;
}
async function loadTranslations(pluginModel: PluginModel): Promise<any> {
try {
return await readPluginJson(pluginModel, 'package.nls.json');
} catch (e) {
if (e !== 'NotFound') {
throw e;
}
return {};
}
}
function localize(value: any, translations: {
[key: string]: string
}): any {
if (typeof value === 'string') {
const match = NLS_REGEX.exec(value);
return match && translations[match[1]] || value;
}
if (Array.isArray(value)) {
const result = [];
for (const item of value) {
result.push(localize(item, translations));
}
return result;
}
if (value === null) {
return value;
}
if (typeof value === 'object') {
const result: { [key: string]: any } = {};
// eslint-disable-next-line guard-for-in
for (const propertyName in value) {
result[propertyName] = localize(value[propertyName], translations);
}
return result;
}
return value;
}

View File

@@ -0,0 +1,40 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 { EnvExtImpl } from '../../../plugin/env';
/**
* Worker specific implementation not returning any FileSystem details
* Extending the common class
*/
@injectable()
export class WorkerEnvExtImpl extends EnvExtImpl {
constructor() {
super();
}
override get appRoot(): string {
// The documentation indicates that this should be an empty string
return '';
}
get isNewAppInstall(): boolean {
return false;
}
}

View File

@@ -0,0 +1,212 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
// eslint-disable-next-line import/no-extraneous-dependencies
import 'reflect-metadata';
import { Container } from '@theia/core/shared/inversify';
import * as theia from '@theia/plugin';
import { emptyPlugin, MAIN_RPC_CONTEXT, Plugin } from '../../../common/plugin-api-rpc';
import { ExtPluginApi } from '../../../common/plugin-ext-api-contribution';
import { getPluginId, PluginMetadata } from '../../../common/plugin-protocol';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { ClipboardExt } from '../../../plugin/clipboard-ext';
import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-documents';
import { MessageRegistryExt } from '../../../plugin/message-registry';
import { createAPIFactory } from '../../../plugin/plugin-context';
import { PluginManagerExtImpl } from '../../../plugin/plugin-manager';
import { KeyValueStorageProxy } from '../../../plugin/plugin-storage';
import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry';
import { WebviewsExtImpl } from '../../../plugin/webviews';
import { WorkspaceExtImpl } from '../../../plugin/workspace';
import { loadManifest } from './plugin-manifest-loader';
import { EnvExtImpl } from '../../../plugin/env';
import { DebugExtImpl } from '../../../plugin/debug/debug-ext';
import { LocalizationExtImpl } from '../../../plugin/localization-ext';
import pluginHostModule from './worker-plugin-module';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ctx = self as any;
const pluginsApiImpl = new Map<string, typeof theia>();
const pluginsModulesNames = new Map<string, Plugin>();
const scripts = new Set<string>();
function initialize(contextPath: string, pluginMetadata: PluginMetadata): void {
const path = './context/' + contextPath;
if (!scripts.has(path)) {
ctx.importScripts(path);
scripts.add(path);
}
}
const container = new Container();
container.load(pluginHostModule);
const rpc: RPCProtocol = container.get(RPCProtocol);
const pluginManager = container.get(PluginManagerExtImpl);
pluginManager.setPluginHost({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loadPlugin(plugin: Plugin): any {
if (plugin.pluginPath) {
if (isElectron()) {
ctx.importScripts(plugin.pluginPath);
} else {
if (plugin.lifecycle.frontendModuleName) {
// Set current module name being imported
ctx.frontendModuleName = plugin.lifecycle.frontendModuleName;
}
ctx.importScripts('./hostedPlugin/' + getPluginId(plugin.model) + '/' + plugin.pluginPath);
}
}
if (plugin.lifecycle.frontendModuleName) {
if (!ctx[plugin.lifecycle.frontendModuleName]) {
console.error(`WebWorker: Cannot start plugin "${plugin.model.name}". Frontend plugin not found: "${plugin.lifecycle.frontendModuleName}"`);
return;
}
return ctx[plugin.lifecycle.frontendModuleName];
}
},
async init(rawPluginData: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> {
const result: Plugin[] = [];
const foreign: Plugin[] = [];
// Process the plugins concurrently, making sure to keep the order.
const plugins = await Promise.all<{
/** Where to push the plugin: `result` or `foreign` */
target: Plugin[],
plugin: Plugin
}>(rawPluginData.map(async plg => {
const pluginModel = plg.model;
const pluginLifecycle = plg.lifecycle;
if (pluginModel.entryPoint!.frontend) {
let frontendInitPath = pluginLifecycle.frontendInitPath;
if (frontendInitPath) {
initialize(frontendInitPath, plg);
} else {
frontendInitPath = '';
}
const rawModel = await loadManifest(pluginModel);
const plugin: Plugin = {
pluginPath: pluginModel.entryPoint.frontend!,
pluginFolder: pluginModel.packagePath,
pluginUri: pluginModel.packageUri,
model: pluginModel,
lifecycle: pluginLifecycle,
rawModel,
isUnderDevelopment: !!plg.isUnderDevelopment
};
const apiImpl = apiFactory(plugin);
pluginsApiImpl.set(plugin.model.id, apiImpl);
pluginsModulesNames.set(plugin.lifecycle.frontendModuleName!, plugin);
return { target: result, plugin };
} else {
return {
target: foreign,
plugin: {
pluginPath: pluginModel.entryPoint.backend,
pluginFolder: pluginModel.packagePath,
pluginUri: pluginModel.packageUri,
model: pluginModel,
lifecycle: pluginLifecycle,
get rawModel(): never {
throw new Error('not supported');
},
isUnderDevelopment: !!plg.isUnderDevelopment
}
};
}
}));
// Collect the ordered plugins and insert them in the target array:
for (const { target, plugin } of plugins) {
target.push(plugin);
}
return [result, foreign];
},
initExtApi(extApi: ExtPluginApi[]): void {
for (const api of extApi) {
try {
if (api.frontendExtApi) {
ctx.importScripts(api.frontendExtApi.initPath);
ctx[api.frontendExtApi.initVariable][api.frontendExtApi.initFunction](rpc, pluginsModulesNames);
}
} catch (e) {
console.error(e);
}
}
}
});
const envExt = container.get(EnvExtImpl);
const debugExt = container.get(DebugExtImpl);
const preferenceRegistryExt = container.get(PreferenceRegistryExtImpl);
const editorsAndDocuments = container.get(EditorsAndDocumentsExtImpl);
const workspaceExt = container.get(WorkspaceExtImpl);
const messageRegistryExt = container.get(MessageRegistryExt);
const clipboardExt = container.get(ClipboardExt);
const webviewExt = container.get(WebviewsExtImpl);
const localizationExt = container.get(LocalizationExtImpl);
const storageProxy = container.get(KeyValueStorageProxy);
const apiFactory = createAPIFactory(
rpc,
pluginManager,
envExt,
debugExt,
preferenceRegistryExt,
editorsAndDocuments,
workspaceExt,
messageRegistryExt,
clipboardExt,
webviewExt,
localizationExt
);
let defaultApi: typeof theia;
const handler = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get: (target: any, name: string) => {
const plugin = pluginsModulesNames.get(name);
if (plugin) {
const apiImpl = pluginsApiImpl.get(plugin.model.id);
return apiImpl;
}
if (!defaultApi) {
defaultApi = apiFactory(emptyPlugin);
}
return defaultApi;
}
};
ctx['theia'] = new Proxy(Object.create(null), handler);
rpc.set(MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT, pluginManager);
rpc.set(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT, editorsAndDocuments);
rpc.set(MAIN_RPC_CONTEXT.WORKSPACE_EXT, workspaceExt);
rpc.set(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT, preferenceRegistryExt);
rpc.set(MAIN_RPC_CONTEXT.STORAGE_EXT, storageProxy);
rpc.set(MAIN_RPC_CONTEXT.WEBVIEWS_EXT, webviewExt);
function isElectron(): boolean {
if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
return true;
}
return false;
}

View File

@@ -0,0 +1,82 @@
// *****************************************************************************
// 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
// *****************************************************************************
// eslint-disable-next-line import/no-extraneous-dependencies
import 'reflect-metadata';
import { ContainerModule } from '@theia/core/shared/inversify';
import { BasicChannel } from '@theia/core/lib/common/message-rpc/channel';
import { Uint8ArrayReadBuffer, Uint8ArrayWriteBuffer } from '@theia/core/lib/common/message-rpc/uint8-array-message-buffer';
import { LocalizationExt } from '../../../common/plugin-api-rpc';
import { RPCProtocol, RPCProtocolImpl } from '../../../common/rpc-protocol';
import { ClipboardExt } from '../../../plugin/clipboard-ext';
import { EditorsAndDocumentsExtImpl } from '../../../plugin/editors-and-documents';
import { MessageRegistryExt } from '../../../plugin/message-registry';
import { MinimalTerminalServiceExt, PluginManagerExtImpl } from '../../../plugin/plugin-manager';
import { InternalStorageExt, KeyValueStorageProxy } from '../../../plugin/plugin-storage';
import { PreferenceRegistryExtImpl } from '../../../plugin/preference-registry';
import { InternalSecretsExt, SecretsExtImpl } from '../../../plugin/secrets-ext';
import { TerminalServiceExtImpl } from '../../../plugin/terminal-ext';
import { WebviewsExtImpl } from '../../../plugin/webviews';
import { WorkspaceExtImpl } from '../../../plugin/workspace';
import { createDebugExtStub } from './debug-stub';
import { EnvExtImpl } from '../../../plugin/env';
import { WorkerEnvExtImpl } from './worker-env-ext';
import { DebugExtImpl } from '../../../plugin/debug/debug-ext';
import { LocalizationExtImpl } from '../../../plugin/localization-ext';
import { EncodingService } from '@theia/core/lib/common/encoding-service';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ctx = self as any;
export default new ContainerModule(bind => {
const channel = new BasicChannel(() => {
const writeBuffer = new Uint8ArrayWriteBuffer();
writeBuffer.onCommit(buffer => {
ctx.postMessage(buffer);
});
return writeBuffer;
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
addEventListener('message', (message: any) => {
channel.onMessageEmitter.fire(() => new Uint8ArrayReadBuffer(message.data));
});
const rpc = new RPCProtocolImpl(channel);
bind(RPCProtocol).toConstantValue(rpc);
bind(PluginManagerExtImpl).toSelf().inSingletonScope();
bind(EnvExtImpl).to(WorkerEnvExtImpl).inSingletonScope();
bind(LocalizationExtImpl).toSelf().inSingletonScope();
bind(LocalizationExt).toService(LocalizationExtImpl);
bind(KeyValueStorageProxy).toSelf().inSingletonScope();
bind(InternalStorageExt).toService(KeyValueStorageProxy);
bind(SecretsExtImpl).toSelf().inSingletonScope();
bind(InternalSecretsExt).toService(SecretsExtImpl);
bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope();
bind(DebugExtImpl).toDynamicValue(({ container }) => {
const child = container.createChild();
child.bind(DebugExtImpl).toSelf();
return createDebugExtStub(child);
}).inSingletonScope();
bind(EncodingService).toSelf().inSingletonScope();
bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope();
bind(WorkspaceExtImpl).toSelf().inSingletonScope();
bind(MessageRegistryExt).toSelf().inSingletonScope();
bind(ClipboardExt).toSelf().inSingletonScope();
bind(WebviewsExtImpl).toSelf().inSingletonScope();
bind(TerminalServiceExtImpl).toSelf().inSingletonScope();
bind(MinimalTerminalServiceExt).toService(TerminalServiceExtImpl);
});

View File

@@ -0,0 +1,467 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/da5fb7d5b865aa522abc7e82c10b746834b98639/src/vs/workbench/api/node/extHostExtensionService.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import debounce = require('@theia/core/shared/lodash.debounce');
import { injectable, inject, interfaces, named, postConstruct, unmanaged } from '@theia/core/shared/inversify';
import { PluginMetadata, HostedPluginServer, DeployedPlugin, PluginServer, PluginIdentifiers } from '../../common/plugin-protocol';
import { AbstractPluginManagerExt, ConfigStorage } from '../../common/plugin-api-rpc';
import {
Disposable, DisposableCollection, Emitter,
ILogger, ContributionProvider,
RpcProxy
} from '@theia/core';
import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution';
import { PluginPathsService } from '../../main/common/plugin-paths-protocol';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { Measurement, Stopwatch } from '@theia/core/lib/common';
export type PluginHost = 'frontend' | string;
export const ALL_ACTIVATION_EVENT = '*';
export function isConnectionScopedBackendPlugin(plugin: DeployedPlugin): boolean {
const entryPoint = plugin.metadata.model.entryPoint;
// A plugin doesn't have to have any entry-point if it doesn't need the activation handler,
// in which case it's assumed to be a backend plugin.
return !entryPoint.headless || !!entryPoint.backend;
}
@injectable()
export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManagerExt<any>, HPS extends HostedPluginServer | RpcProxy<HostedPluginServer>> {
protected container: interfaces.Container;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(HostedPluginServer)
protected readonly server: HPS;
@inject(ContributionProvider)
@named(MainPluginApiProvider)
protected readonly mainPluginApiProviders: ContributionProvider<MainPluginApiProvider>;
@inject(PluginServer)
protected readonly pluginServer: PluginServer;
@inject(PluginPathsService)
protected readonly pluginPathsService: PluginPathsService;
@inject(EnvVariablesServer)
protected readonly envServer: EnvVariablesServer;
@inject(Stopwatch)
protected readonly stopwatch: Stopwatch;
protected theiaReadyPromise: Promise<unknown>;
protected readonly managers = new Map<string, PM>();
protected readonly contributions = new Map<PluginIdentifiers.UnversionedId, PluginContributions>();
protected readonly activationEvents = new Set<string>();
protected readonly onDidChangePluginsEmitter = new Emitter<void>();
readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event;
protected readonly deferredWillStart = new Deferred<void>();
/**
* Resolves when the initial plugins are loaded and about to be started.
*/
get willStart(): Promise<void> {
return this.deferredWillStart.promise;
}
protected readonly deferredDidStart = new Deferred<void>();
/**
* Resolves when the initial plugins are started.
*/
get didStart(): Promise<void> {
return this.deferredDidStart.promise;
}
constructor(@unmanaged() protected readonly clientId: string) { }
@postConstruct()
protected init(): void {
this.theiaReadyPromise = this.createTheiaReadyPromise();
}
protected abstract createTheiaReadyPromise(): Promise<unknown>;
get plugins(): PluginMetadata[] {
const plugins: PluginMetadata[] = [];
this.contributions.forEach(contributions => plugins.push(contributions.plugin.metadata));
return plugins;
}
getPlugin(id: PluginIdentifiers.UnversionedId): DeployedPlugin | undefined {
const contributions = this.contributions.get(id);
return contributions && contributions.plugin;
}
/** do not call it, except from the plugin frontend contribution */
onStart(container: interfaces.Container): void {
this.container = container;
this.load();
this.afterStart();
}
protected afterStart(): void {
// Nothing to do in the abstract
}
protected loadQueue: Promise<void> = Promise.resolve(undefined);
load = debounce(() => this.loadQueue = this.loadQueue.then(async () => {
try {
await this.runOperation(() => this.doLoad());
} catch (e) {
console.error('Failed to load plugins:', e);
}
}), 50, { leading: true });
protected runOperation(operation: () => Promise<void>): Promise<void> {
return operation();
}
protected async doLoad(): Promise<void> {
const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
await this.beforeSyncPlugins(toDisconnect);
// process empty plugins as well in order to properly remove stale plugin widgets
await this.syncPlugins();
// it has to be resolved before awaiting layout is initialized
// otherwise clients can hang forever in the initialization phase
this.deferredWillStart.resolve();
await this.beforeLoadContributions(toDisconnect);
if (toDisconnect.disposed) {
// if disconnected then don't try to load plugin contributions
return;
}
const contributionsByHost = this.loadContributions(toDisconnect);
await this.afterLoadContributions(toDisconnect);
await this.theiaReadyPromise;
if (toDisconnect.disposed) {
// if disconnected then don't try to init plugin code and dynamic contributions
return;
}
await this.startPlugins(contributionsByHost, toDisconnect);
this.deferredDidStart.resolve();
}
protected beforeSyncPlugins(toDisconnect: DisposableCollection): Promise<void> {
// Nothing to do in the abstract
return Promise.resolve();
}
protected beforeLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
// Nothing to do in the abstract
return Promise.resolve();
}
protected afterLoadContributions(toDisconnect: DisposableCollection): Promise<void> {
// Nothing to do in the abstract
return Promise.resolve();
}
/**
* Sync loaded and deployed plugins:
* - undeployed plugins are unloaded
* - newly deployed plugins are initialized
*/
protected async syncPlugins(): Promise<void> {
let initialized = 0;
const waitPluginsMeasurement = this.measure('waitForDeployment');
let syncPluginsMeasurement: Measurement | undefined;
const toUnload = new Set(this.contributions.keys());
let didChangeInstallationStatus = false;
try {
const newPluginIds: PluginIdentifiers.VersionedId[] = [];
const [deployedPluginIds, uninstalledPluginIds, disabledPlugins] = await Promise.all(
[this.server.getDeployedPluginIds(), this.server.getUninstalledPluginIds(), this.server.getDisabledPluginIds()]);
waitPluginsMeasurement.log('Waiting for backend deployment');
syncPluginsMeasurement = this.measure('syncPlugins');
for (const versionedId of deployedPluginIds) {
const unversionedId = PluginIdentifiers.unversionedFromVersioned(versionedId);
toUnload.delete(unversionedId);
if (!this.contributions.has(unversionedId)) {
newPluginIds.push(versionedId);
}
}
for (const pluginId of toUnload) {
this.contributions.get(pluginId)?.dispose();
}
for (const versionedId of uninstalledPluginIds) {
const plugin = this.getPlugin(PluginIdentifiers.unversionedFromVersioned(versionedId));
if (plugin && PluginIdentifiers.componentsToVersionedId(plugin.metadata.model) === versionedId && !plugin.metadata.outOfSync) {
plugin.metadata.outOfSync = didChangeInstallationStatus = true;
}
}
for (const unversionedId of disabledPlugins) {
const plugin = this.getPlugin(unversionedId);
if (plugin && PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model) === unversionedId && !plugin.metadata.outOfSync) {
plugin.metadata.outOfSync = didChangeInstallationStatus = true;
}
}
for (const contribution of this.contributions.values()) {
if (contribution.plugin.metadata.outOfSync && !(
uninstalledPluginIds.includes(PluginIdentifiers.componentsToVersionedId(contribution.plugin.metadata.model))
|| disabledPlugins.includes(PluginIdentifiers.componentsToUnversionedId(contribution.plugin.metadata.model))
)) {
contribution.plugin.metadata.outOfSync = false;
didChangeInstallationStatus = true;
}
}
if (newPluginIds.length) {
const deployedPlugins = await this.server.getDeployedPlugins(newPluginIds);
const plugins: DeployedPlugin[] = [];
for (const plugin of deployedPlugins) {
const accepted = this.acceptPlugin(plugin);
if (typeof accepted === 'object') {
plugins.push(accepted);
} else if (accepted) {
plugins.push(plugin);
}
}
for (const plugin of plugins) {
const pluginId = PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model);
const contributions = new PluginContributions(plugin);
this.contributions.set(pluginId, contributions);
contributions.push(Disposable.create(() => this.contributions.delete(pluginId)));
initialized++;
}
}
} finally {
if (initialized || toUnload.size || didChangeInstallationStatus) {
this.onDidChangePluginsEmitter.fire(undefined);
}
if (!syncPluginsMeasurement) {
// await didn't complete normally
waitPluginsMeasurement.error('Backend deployment failed.');
}
}
if (initialized > 0) {
// Only log sync measurement if there are were plugins to sync.
syncPluginsMeasurement?.log(`Sync of ${this.getPluginCount(initialized)}`);
} else {
syncPluginsMeasurement?.stop();
}
}
/**
* Accept a deployed plugin to load in this host, or reject it, or adapt it for loading.
* The result may be a boolean to accept (`true`) or reject (`false`) the plugin as is,
* or else an adaptation of the original `plugin` to load in its stead.
*/
protected abstract acceptPlugin(plugin: DeployedPlugin): boolean | DeployedPlugin;
/**
* Always synchronous in order to simplify handling disconnections.
* @throws never
*/
protected loadContributions(toDisconnect: DisposableCollection): Map<PluginHost, PluginContributions[]> {
let loaded = 0;
const loadPluginsMeasurement = this.measure('loadPlugins');
const hostContributions = new Map<PluginHost, PluginContributions[]>();
console.log(`[${this.clientId}] Loading plugin contributions`);
for (const contributions of this.contributions.values()) {
const plugin = contributions.plugin.metadata;
const pluginId = plugin.model.id;
if (contributions.state === PluginContributions.State.INITIALIZING) {
contributions.state = PluginContributions.State.LOADING;
contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`)));
contributions.push(this.handleContributions(contributions.plugin));
contributions.state = PluginContributions.State.LOADED;
console.debug(`[${this.clientId}][${pluginId}]: Loaded contributions.`);
loaded++;
}
if (contributions.state === PluginContributions.State.LOADED) {
contributions.state = PluginContributions.State.STARTING;
const host = plugin.model.entryPoint.frontend ? 'frontend' : plugin.host;
const dynamicContributions = hostContributions.get(host) || [];
dynamicContributions.push(contributions);
hostContributions.set(host, dynamicContributions);
toDisconnect.push(Disposable.create(() => {
contributions!.state = PluginContributions.State.LOADED;
console.debug(`[${this.clientId}][${pluginId}]: Disconnected.`);
}));
}
}
if (loaded > 0) {
// Only log load measurement if there are were plugins to load.
loadPluginsMeasurement?.log(`Load contributions of ${this.getPluginCount(loaded)}`);
} else {
loadPluginsMeasurement.stop();
}
return hostContributions;
}
protected abstract handleContributions(plugin: DeployedPlugin): Disposable;
protected async startPlugins(contributionsByHost: Map<PluginHost, PluginContributions[]>, toDisconnect: DisposableCollection): Promise<void> {
let started = 0;
const startPluginsMeasurement = this.measure('startPlugins');
const [hostLogPath, hostStoragePath, hostGlobalStoragePath] = await Promise.all([
this.pluginPathsService.getHostLogPath(),
this.getStoragePath(),
this.getHostGlobalStoragePath()
]);
if (toDisconnect.disposed) {
return;
}
const thenable: Promise<void>[] = [];
const configStorage: ConfigStorage = {
hostLogPath,
hostStoragePath,
hostGlobalStoragePath
};
for (const [host, hostContributions] of contributionsByHost) {
// do not start plugins for electron browser
if (host === 'frontend' && environment.electron.is()) {
continue;
}
const manager = await this.obtainManager(host, hostContributions, toDisconnect);
if (!manager) {
continue;
}
const plugins = hostContributions.map(contributions => contributions.plugin.metadata);
thenable.push((async () => {
try {
const activationEvents = [...this.activationEvents];
await manager.$start({ plugins, configStorage, activationEvents });
if (toDisconnect.disposed) {
return;
}
console.log(`[${this.clientId}] Starting plugins.`);
for (const contributions of hostContributions) {
started++;
const plugin = contributions.plugin;
const id = plugin.metadata.model.id;
contributions.state = PluginContributions.State.STARTED;
console.debug(`[${this.clientId}][${id}]: Started plugin.`);
toDisconnect.push(contributions.push(Disposable.create(() => {
console.debug(`[${this.clientId}][${id}]: Stopped plugin.`);
manager.$stop(id);
})));
this.handlePluginStarted(manager, plugin);
}
} catch (e) {
console.error(`Failed to start plugins for '${host}' host`, e);
}
})());
}
await Promise.all(thenable);
await this.activateByEvent('onStartupFinished');
if (toDisconnect.disposed) {
return;
}
if (started > 0) {
startPluginsMeasurement.log(`Start of ${this.getPluginCount(started)}`);
} else {
startPluginsMeasurement.stop();
}
}
protected abstract obtainManager(host: string, hostContributions: PluginContributions[],
toDisconnect: DisposableCollection): Promise<PM | undefined>;
protected abstract getStoragePath(): Promise<string | undefined>;
protected abstract getHostGlobalStoragePath(): Promise<string>;
async activateByEvent(activationEvent: string): Promise<void> {
if (this.activationEvents.has(activationEvent)) {
return;
}
this.activationEvents.add(activationEvent);
await Promise.all(Array.from(this.managers.values(), manager => manager.$activateByEvent(activationEvent)));
}
async activatePlugin(id: string): Promise<void> {
const activation = [];
for (const manager of this.managers.values()) {
activation.push(manager.$activatePlugin(id));
}
await Promise.all(activation);
}
protected handlePluginStarted(manager: PM, plugin: DeployedPlugin): void {
// Nothing to do in the abstract
}
protected measure(name: string): Measurement {
return this.stopwatch.start(name, { context: this.clientId });
}
protected getPluginCount(plugins: number): string {
return `${plugins} plugin${plugins === 1 ? '' : 's'}`;
}
}
export class PluginContributions extends DisposableCollection {
constructor(
readonly plugin: DeployedPlugin
) {
super();
}
state: PluginContributions.State = PluginContributions.State.INITIALIZING;
}
export namespace PluginContributions {
export enum State {
INITIALIZING = 0,
LOADING = 1,
LOADED = 2,
STARTING = 3,
STARTED = 4
}
}

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { bindCommonHostedBackend } from '../node/plugin-ext-hosted-backend-module';
import { PluginScanner } from '../../common/plugin-protocol';
import { TheiaPluginScannerElectron } from './scanner-theia-electron';
export function bindElectronBackend(bind: interfaces.Bind): void {
bindCommonHostedBackend(bind);
bind(PluginScanner).to(TheiaPluginScannerElectron).inSingletonScope();
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { TheiaPluginScanner } from '../node/scanners/scanner-theia';
import { injectable } from '@theia/core/shared/inversify';
import { PluginPackage, PluginModel } from '../../common/plugin-protocol';
@injectable()
export class TheiaPluginScannerElectron extends TheiaPluginScanner {
override getModel(plugin: PluginPackage): PluginModel {
const result = super.getModel(plugin);
if (result.entryPoint.frontend) {
result.entryPoint.frontend = path.resolve(plugin.packagePath, result.entryPoint.frontend);
}
return result;
}
}

View File

@@ -0,0 +1,75 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Argv, Arguments } from '@theia/core/shared/yargs';
import { CliContribution } from '@theia/core/lib/node';
let pluginHostTerminateTimeout = 10 * 1000;
if (process.env.PLUGIN_HOST_TERMINATE_TIMEOUT) {
pluginHostTerminateTimeout = Number.parseInt(process.env.PLUGIN_HOST_TERMINATE_TIMEOUT);
}
let pluginHostStopTimeout = 4 * 1000;
if (process.env.PLUGIN_HOST_STOP_TIMEOUT) {
pluginHostStopTimeout = Number.parseInt(process.env.PLUGIN_HOST_STOP_TIMEOUT);
}
@injectable()
export class HostedPluginCliContribution implements CliContribution {
static EXTENSION_TESTS_PATH = 'extensionTestsPath';
static PLUGIN_HOST_TERMINATE_TIMEOUT = 'pluginHostTerminateTimeout';
static PLUGIN_HOST_STOP_TIMEOUT = 'pluginHostStopTimeout';
protected _extensionTestsPath: string | undefined;
get extensionTestsPath(): string | undefined {
return this._extensionTestsPath;
}
protected _pluginHostTerminateTimeout = pluginHostTerminateTimeout;
get pluginHostTerminateTimeout(): number {
return this._pluginHostTerminateTimeout;
}
protected _pluginHostStopTimeout = pluginHostStopTimeout;
get pluginHostStopTimeout(): number {
return this._pluginHostStopTimeout;
}
configure(conf: Argv): void {
conf.option(HostedPluginCliContribution.EXTENSION_TESTS_PATH, {
type: 'string'
});
conf.option(HostedPluginCliContribution.PLUGIN_HOST_TERMINATE_TIMEOUT, {
type: 'number',
default: pluginHostTerminateTimeout,
description: 'Timeout in milliseconds to wait for the plugin host process to terminate before killing it. Use 0 for no timeout.'
});
conf.option(HostedPluginCliContribution.PLUGIN_HOST_STOP_TIMEOUT, {
type: 'number',
default: pluginHostStopTimeout,
description: 'Timeout in milliseconds to wait for the plugin host process to stop internal services. Use 0 for no timeout.'
});
}
setArguments(args: Arguments): void {
this._extensionTestsPath = args[HostedPluginCliContribution.EXTENSION_TESTS_PATH] as string;
this._pluginHostTerminateTimeout = args[HostedPluginCliContribution.PLUGIN_HOST_TERMINATE_TIMEOUT] as number;
this._pluginHostStopTimeout = args[HostedPluginCliContribution.PLUGIN_HOST_STOP_TIMEOUT] as number;
}
}

View File

@@ -0,0 +1,407 @@
// *****************************************************************************
// Copyright (C) 2021 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 * as path from 'path';
import * as fs from '@theia/core/shared/fs-extra';
import { LazyLocalization, LocalizationProvider } from '@theia/core/lib/node/i18n/localization-provider';
import { Localization } from '@theia/core/lib/common/i18n/localization';
import { inject, injectable } from '@theia/core/shared/inversify';
import { DeployedPlugin, Localization as PluginLocalization, PluginIdentifiers, Translation } from '../../common';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { BackendApplicationContribution } from '@theia/core/lib/node';
import { Disposable, DisposableCollection, isObject, MaybePromise, nls, Path, URI } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { LanguagePackBundle, LanguagePackService } from '../../common/language-pack-service';
export interface VSCodeNlsConfig {
locale: string
availableLanguages: Record<string, string>
_languagePackSupport?: boolean
_languagePackId?: string
_translationsConfigFile?: string
_cacheRoot?: string
_corruptedFile?: string
}
@injectable()
export class HostedPluginLocalizationService implements BackendApplicationContribution {
@inject(LocalizationProvider)
protected readonly localizationProvider: LocalizationProvider;
@inject(LanguagePackService)
protected readonly languagePackService: LanguagePackService;
@inject(EnvVariablesServer)
protected readonly envVariables: EnvVariablesServer;
protected localizationDisposeMap = new Map<string, Disposable>();
protected translationConfigFiles: Map<string, string> = new Map();
protected readonly _ready = new Deferred();
/**
* This promise resolves when the cache has been cleaned up after starting the backend server.
* Once resolved, the service allows to cache localization files for plugins.
*/
ready = this._ready.promise;
initialize(): MaybePromise<void> {
this.getLocalizationCacheDir()
.then(cacheDir => fs.emptyDir(cacheDir))
.then(() => this._ready.resolve());
}
async deployLocalizations(plugin: DeployedPlugin): Promise<void> {
const disposable = new DisposableCollection();
if (plugin.contributes?.localizations) {
// Indicator that this plugin is a vscode language pack
// Language packs translate Theia and some builtin vscode extensions
const localizations = buildLocalizations(plugin.metadata.model.packageUri, plugin.contributes.localizations);
disposable.push(this.localizationProvider.addLocalizations(...localizations));
}
if (plugin.metadata.model.l10n || plugin.contributes?.localizations) {
// Indicator that this plugin is a vscode language pack or has its own localization bundles
// These bundles are purely used for translating plugins
// The branch above builds localizations for Theia's own strings
disposable.push(await this.updateLanguagePackBundles(plugin));
}
if (!disposable.disposed) {
const versionedId = PluginIdentifiers.componentsToVersionedId(plugin.metadata.model);
disposable.push(Disposable.create(() => {
this.localizationDisposeMap.delete(versionedId);
}));
this.localizationDisposeMap.set(versionedId, disposable);
}
}
undeployLocalizations(plugin: PluginIdentifiers.VersionedId): void {
this.localizationDisposeMap.get(plugin)?.dispose();
}
protected async updateLanguagePackBundles(plugin: DeployedPlugin): Promise<Disposable> {
const disposable = new DisposableCollection();
const pluginId = plugin.metadata.model.id;
const packageUri = new URI(plugin.metadata.model.packageUri);
if (plugin.contributes?.localizations) {
const l10nPromises: Promise<void>[] = [];
for (const localization of plugin.contributes.localizations) {
for (const translation of localization.translations) {
l10nPromises.push(getL10nTranslation(plugin.metadata.model.packageUri, translation).then(l10n => {
if (l10n) {
const translatedPluginId = translation.id;
const translationUri = packageUri.resolve(translation.path);
const locale = localization.languageId;
// We store a bundle for another extension in here
// Hence we use `translatedPluginId` instead of `pluginId`
this.languagePackService.storeBundle(translatedPluginId, locale, {
contents: processL10nBundle(l10n),
uri: translationUri.toString()
});
disposable.push(Disposable.create(() => {
// Only dispose the deleted locale for the specific plugin
this.languagePackService.deleteBundle(translatedPluginId, locale);
}));
}
}));
}
}
await Promise.all(l10nPromises);
}
// The `l10n` field of the plugin model points to a relative directory path within the plugin
// It is supposed to contain localization bundles that contain translations of the plugin strings into different languages
if (plugin.metadata.model.l10n) {
const bundleDirectory = packageUri.resolve(plugin.metadata.model.l10n);
const bundles = await loadPluginBundles(bundleDirectory);
if (bundles) {
for (const [locale, bundle] of Object.entries(bundles)) {
this.languagePackService.storeBundle(pluginId, locale, bundle);
}
disposable.push(Disposable.create(() => {
// Dispose all bundles contributed by the deleted plugin
this.languagePackService.deleteBundle(pluginId);
}));
}
}
return disposable;
}
/**
* Performs localization of the plugin model. Translates entries such as command names, view names and other items.
*
* Translatable items are indicated with a `%id%` value.
* The `id` is the translation key that gets replaced with the localized value for the currently selected language.
*
* Returns a copy of the plugin argument and does not modify the argument.
* This is done to preserve the original `%id%` values for subsequent invocations of this method.
*/
async localizePlugin(plugin: DeployedPlugin): Promise<DeployedPlugin> {
const currentLanguage = this.localizationProvider.getCurrentLanguage();
const pluginPath = new URI(plugin.metadata.model.packageUri).path.fsPath();
const pluginId = plugin.metadata.model.id;
try {
const [localization, translations] = await Promise.all([
this.localizationProvider.loadLocalization(currentLanguage),
loadPackageTranslations(pluginPath, currentLanguage),
]);
plugin = localizePackage(plugin, translations, (key, original) => {
const fullKey = `${pluginId}/package/${key}`;
return Localization.localize(localization, fullKey, original);
}) as DeployedPlugin;
} catch (err) {
console.error(`Failed to localize plugin '${pluginId}'.`, err);
}
return plugin;
}
getNlsConfig(): VSCodeNlsConfig {
const locale = this.localizationProvider.getCurrentLanguage();
const configFile = this.translationConfigFiles.get(locale);
if (locale === nls.defaultLocale || !configFile) {
return { locale, availableLanguages: {} };
}
const cache = path.dirname(configFile);
return {
locale,
availableLanguages: { '*': locale },
_languagePackSupport: true,
_cacheRoot: cache,
_languagePackId: locale,
_translationsConfigFile: configFile
};
}
async buildTranslationConfig(plugins: DeployedPlugin[]): Promise<void> {
await this.ready;
const cacheDir = await this.getLocalizationCacheDir();
const configs = new Map<string, Record<string, string>>();
for (const plugin of plugins) {
if (plugin.contributes?.localizations) {
const pluginPath = new URI(plugin.metadata.model.packageUri).path.fsPath();
for (const localization of plugin.contributes.localizations) {
const config = configs.get(localization.languageId) || {};
for (const translation of localization.translations) {
const fullPath = path.join(pluginPath, translation.path);
config[translation.id] = fullPath;
}
configs.set(localization.languageId, config);
}
}
}
for (const [language, config] of configs.entries()) {
const languageConfigDir = path.join(cacheDir, language);
await fs.mkdirs(languageConfigDir);
const configFile = path.join(languageConfigDir, `nls.config.${language}.json`);
this.translationConfigFiles.set(language, configFile);
await fs.writeJson(configFile, config);
}
}
protected async getLocalizationCacheDir(): Promise<string> {
const configDir = new URI(await this.envVariables.getConfigDirUri()).path.fsPath();
const cacheDir = path.join(configDir, 'localization-cache');
return cacheDir;
}
}
// New plugin localization logic using vscode.l10n
async function getL10nTranslation(packageUri: string, translation: Translation): Promise<UnprocessedL10nBundle | undefined> {
// 'bundle' is a special key that contains all translations for the l10n vscode API
// If that doesn't exist, we can assume that the language pack is using the old vscode-nls API
if (translation.cachedContents) {
return translation.cachedContents.bundle;
} else {
const translationPath = new URI(packageUri).path.join(translation.path).fsPath();
try {
const translationJson = await fs.readJson(translationPath);
translation.cachedContents = translationJson?.contents;
return translationJson?.contents?.bundle;
} catch (err) {
console.error('Failed reading translation file from: ' + translationPath, err);
// Store an empty object, so we don't reattempt to load the file
translation.cachedContents = {};
return undefined;
}
}
}
async function loadPluginBundles(l10nUri: URI): Promise<Record<string, LanguagePackBundle> | undefined> {
try {
const directory = l10nUri.path.fsPath();
const files = await fs.readdir(directory);
const result: Record<string, LanguagePackBundle> = {};
await Promise.all(files.map(async fileName => {
const match = fileName.match(/^bundle\.l10n\.([\w\-]+)\.json$/);
if (match) {
const locale = match[1];
const contents = await fs.readJSON(path.join(directory, fileName));
result[locale] = {
contents,
uri: l10nUri.resolve(fileName).toString()
};
}
}));
return result;
} catch (err) {
// The directory either doesn't exist or its contents cannot be parsed
console.error(`Failed to load plugin localization bundles from ${l10nUri}.`, err);
// In any way we should just safely return undefined
return undefined;
}
}
type UnprocessedL10nBundle = Record<string, string | { message: string }>;
function processL10nBundle(bundle: UnprocessedL10nBundle): Record<string, string> {
const processedBundle: Record<string, string> = {};
for (const [name, value] of Object.entries(bundle)) {
const stringValue = typeof value === 'string' ? value : value.message;
processedBundle[name] = stringValue;
}
return processedBundle;
}
// Old plugin localization logic for vscode-nls
// vscode-nls was used until version 1.73 of VSCode to translate extensions
// This style of localization is still used by vscode language packs
function buildLocalizations(packageUri: string, localizations: PluginLocalization[]): LazyLocalization[] {
const theiaLocalizations: LazyLocalization[] = [];
const packagePath = new URI(packageUri).path;
for (const localization of localizations) {
let cachedLocalization: Promise<Record<string, string>> | undefined;
const theiaLocalization: LazyLocalization = {
languageId: localization.languageId,
languageName: localization.languageName,
localizedLanguageName: localization.localizedLanguageName,
languagePack: true,
async getTranslations(): Promise<Record<string, string>> {
cachedLocalization ??= loadTranslations(packagePath, localization.translations);
return cachedLocalization;
},
};
theiaLocalizations.push(theiaLocalization);
}
return theiaLocalizations;
}
async function loadTranslations(packagePath: Path, translations: Translation[]): Promise<Record<string, string>> {
const allTranslations = await Promise.all(translations.map(async translation => {
const values: Record<string, string> = {};
const translationPath = packagePath.join(translation.path).fsPath();
try {
const translationJson = await fs.readJson(translationPath);
const translationContents: Record<string, Record<string, string>> = translationJson?.contents;
for (const [scope, value] of Object.entries(translationContents ?? {})) {
for (const [key, item] of Object.entries(value)) {
const translationKey = buildTranslationKey(translation.id, scope, key);
values[translationKey] = item;
}
}
} catch (err) {
console.error('Failed to load translation from: ' + translationPath, err);
}
return values;
}));
return Object.assign({}, ...allTranslations);
}
function buildTranslationKey(pluginId: string, scope: string, key: string): string {
return `${pluginId}/${Localization.transformKey(scope)}/${key}`;
}
// Localization logic for `package.json` entries
// Extensions can use `package.nls.json` files to store translations for values in their package.json
// This logic has not changed with the introduction of the vscode.l10n API
interface PackageTranslation {
translation?: Record<string, string>
default?: Record<string, string>
}
async function loadPackageTranslations(pluginPath: string, locale: string): Promise<PackageTranslation> {
const localizedPluginPath = path.join(pluginPath, `package.nls.${locale}.json`);
try {
const defaultValue = coerceLocalizations(await fs.readJson(path.join(pluginPath, 'package.nls.json')));
if (await fs.pathExists(localizedPluginPath)) {
return {
translation: coerceLocalizations(await fs.readJson(localizedPluginPath)),
default: defaultValue
};
}
return {
default: defaultValue
};
} catch (e) {
if (e.code !== 'ENOENT') {
throw e;
}
return {};
}
}
interface LocalizeInfo {
message: string
comment?: string
}
function isLocalizeInfo(obj: unknown): obj is LocalizeInfo {
return isObject(obj) && 'message' in obj || false;
}
function coerceLocalizations(translations: Record<string, string | LocalizeInfo>): Record<string, string> {
for (const [key, value] of Object.entries(translations)) {
if (isLocalizeInfo(value)) {
translations[key] = value.message;
} else if (typeof value !== 'string') {
// Only strings or LocalizeInfo values are valid
translations[key] = 'INVALID TRANSLATION VALUE';
}
}
return translations as Record<string, string>;
}
function localizePackage(value: unknown, translations: PackageTranslation, callback: (key: string, defaultValue: string) => string): unknown {
if (typeof value === 'string') {
let result = value;
if (value.length > 2 && value.startsWith('%') && value.endsWith('%')) {
const key = value.slice(1, -1);
if (translations.translation && key in translations.translation) {
result = translations.translation[key];
} else if (translations.default && key in translations.default) {
result = callback(key, translations.default[key]);
}
}
return result;
}
if (Array.isArray(value)) {
const result = [];
for (const item of value) {
result.push(localizePackage(item, translations, callback));
}
return result;
}
if (isObject(value)) {
const result: Record<string, unknown> = {};
for (const [name, item] of Object.entries(value)) {
result[name] = localizePackage(item, translations, callback);
}
return result;
}
return value;
}

View File

@@ -0,0 +1,231 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { ConnectionErrorHandler, ContributionProvider, ILogger, MessageService } from '@theia/core/lib/common';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { BinaryMessagePipe } from '@theia/core/lib/node/messaging/binary-message-pipe';
import { createIpcEnv } from '@theia/core/lib/node/messaging/ipc-protocol';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import * as cp from 'child_process';
import { Duplex } from 'stream';
import { HostedPluginClient, PLUGIN_HOST_BACKEND, PluginHostEnvironmentVariable, ServerPluginRunner } from '../../common/plugin-protocol';
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
import { ProcessTerminateMessage, ProcessTerminatedMessage } from './hosted-plugin-protocol';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
export interface IPCConnectionOptions {
readonly serverName: string;
readonly logger: ILogger;
readonly args: string[];
readonly errorHandler?: ConnectionErrorHandler;
}
export const HostedPluginProcessConfiguration = Symbol('HostedPluginProcessConfiguration');
export interface HostedPluginProcessConfiguration {
readonly path: string
}
@injectable()
export class HostedPluginProcess implements ServerPluginRunner {
@inject(HostedPluginProcessConfiguration)
protected configuration: HostedPluginProcessConfiguration;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(HostedPluginCliContribution)
protected readonly cli: HostedPluginCliContribution;
@inject(ContributionProvider)
@named(PluginHostEnvironmentVariable)
protected readonly pluginHostEnvironmentVariables: ContributionProvider<PluginHostEnvironmentVariable>;
@inject(MessageService)
protected readonly messageService: MessageService;
@inject(HostedPluginLocalizationService)
protected readonly localizationService: HostedPluginLocalizationService;
@inject(ProcessUtils)
protected readonly processUtils: ProcessUtils;
private childProcess: cp.ChildProcess | undefined;
private messagePipe?: BinaryMessagePipe;
private client: HostedPluginClient;
private terminatingPluginServer = false;
public setClient(client: HostedPluginClient): void {
if (this.client) {
if (this.childProcess) {
this.runPluginServer();
}
}
this.client = client;
}
public clientClosed(): void {
}
public setDefault(defaultRunner: ServerPluginRunner): void {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public acceptMessage(pluginHostId: string, message: Uint8Array): boolean {
return pluginHostId === 'main';
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public onMessage(pluginHostId: string, message: Uint8Array): void {
if (this.messagePipe) {
this.messagePipe.send(message);
}
}
async terminatePluginServer(): Promise<void> {
if (this.childProcess === undefined) {
return;
}
this.terminatingPluginServer = true;
// eslint-disable-next-line @typescript-eslint/no-shadow
const cp = this.childProcess;
this.childProcess = undefined;
const waitForTerminated = new Deferred<void>();
cp.on('message', message => {
const msg = JSON.parse(message as string);
if (ProcessTerminatedMessage.is(msg)) {
waitForTerminated.resolve();
}
});
const stopTimeout = this.cli.pluginHostStopTimeout;
cp.send(JSON.stringify({ type: ProcessTerminateMessage.TYPE, stopTimeout }));
const terminateTimeout = this.cli.pluginHostTerminateTimeout;
if (terminateTimeout) {
await Promise.race([
waitForTerminated.promise,
new Promise(resolve => setTimeout(resolve, terminateTimeout))
]);
} else {
await waitForTerminated.promise;
}
this.killProcessTree(cp.pid!);
}
killProcessTree(parentPid: number): void {
this.processUtils.terminateProcessTree(parentPid);
}
protected killProcess(pid: number): void {
try {
process.kill(pid);
} catch (e) {
if (e && 'code' in e && e.code === 'ESRCH') {
return;
}
this.logger.error(`[${pid}] failed to kill`, e);
}
}
public runPluginServer(serverName?: string): void {
if (this.childProcess) {
this.terminatePluginServer();
}
this.terminatingPluginServer = false;
this.childProcess = this.fork({
serverName: serverName ?? 'hosted-plugin',
logger: this.logger,
args: []
});
this.messagePipe = new BinaryMessagePipe(this.childProcess.stdio[4] as Duplex);
this.messagePipe.onMessage(buffer => {
if (this.client) {
this.client.postMessage(PLUGIN_HOST_BACKEND, buffer);
}
});
}
readonly HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION = new RegExp('HOSTED_PLUGIN*');
private fork(options: IPCConnectionOptions): cp.ChildProcess {
// create env and add PATH to it so any executable from root process is available
const env = createIpcEnv({ env: process.env });
for (const key of Object.keys(env)) {
if (this.HOSTED_PLUGIN_ENV_REGEXP_EXCLUSION.test(key)) {
delete env[key];
}
}
env['VSCODE_NLS_CONFIG'] = JSON.stringify(this.localizationService.getNlsConfig());
// apply external env variables
this.pluginHostEnvironmentVariables.getContributions().forEach(envVar => envVar.process(env));
if (this.cli.extensionTestsPath) {
env.extensionTestsPath = this.cli.extensionTestsPath;
}
const forkOptions: cp.ForkOptions = {
silent: true,
env: env,
execArgv: [],
// 5th element MUST be 'overlapped' for it to work properly on Windows.
// 'overlapped' works just like 'pipe' on non-Windows platforms.
// See: https://nodejs.org/docs/latest-v14.x/api/child_process.html#child_process_options_stdio
stdio: ['pipe', 'pipe', 'pipe', 'ipc', 'overlapped']
};
const inspectArgPrefix = `--${options.serverName}-inspect`;
const inspectArg = process.argv.find(v => v.startsWith(inspectArgPrefix));
if (inspectArg !== undefined) {
forkOptions.execArgv = ['--nolazy', `--inspect${inspectArg.substring(inspectArgPrefix.length)}`];
}
const childProcess = cp.fork(this.configuration.path, options.args, forkOptions);
childProcess.stdout!.on('data', data => this.logger.info(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
childProcess.stderr!.on('data', data => this.logger.error(`[${options.serverName}: ${childProcess.pid}] ${data.toString().trim()}`));
this.logger.debug(`[${options.serverName}: ${childProcess.pid}] IPC started`);
childProcess.once('exit', (code: number, signal: string) => this.onChildProcessExit(options.serverName, childProcess.pid!, code, signal));
childProcess.on('error', err => this.onChildProcessError(err));
return childProcess;
}
private onChildProcessExit(serverName: string, pid: number, code: number, signal: string): void {
if (this.terminatingPluginServer) {
return;
}
this.logger.error(`[${serverName}: ${pid}] IPC exited, with signal: ${signal}, and exit code: ${code}`);
const message = 'Plugin runtime crashed unexpectedly, all plugins are not working, please reload the page.';
let hintMessage: string = 'If it doesn\'t help, please check Theia server logs.';
if (signal && signal.toUpperCase() === 'SIGKILL') {
// May happen in case of OOM or manual force stop.
hintMessage = 'Probably there is not enough memory for the plugins. ' + hintMessage;
}
this.messageService.error(message + ' ' + hintMessage, { timeout: 15 * 60 * 1000 });
}
private onChildProcessError(err: Error): void {
this.logger.error(`Error from plugin host: ${err.message}`);
}
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// 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
// *****************************************************************************
// Custom message protocol between `HostedPluginProcess` and its `PluginHost` child process.
/**
* Sent to initiate termination of the counterpart process.
*/
export interface ProcessTerminateMessage {
type: typeof ProcessTerminateMessage.TYPE,
stopTimeout?: number
}
export namespace ProcessTerminateMessage {
export const TYPE = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function is(object: any): object is ProcessTerminateMessage {
return typeof object === 'object' && object.type === TYPE;
}
}
/**
* Sent to inform the counter part process that the process termination has been completed.
*/
export interface ProcessTerminatedMessage {
type: typeof ProcessTerminateMessage.TYPE,
}
export namespace ProcessTerminatedMessage {
export const TYPE = 1;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function is(object: any): object is ProcessTerminateMessage {
return typeof object === 'object' && object.type === TYPE;
}
}

View File

@@ -0,0 +1,102 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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, multiInject, postConstruct, optional } from '@theia/core/shared/inversify';
import { ILogger, ConnectionErrorHandler } from '@theia/core/lib/common';
import { HostedPluginClient, PluginModel, ServerPluginRunner } from '../../common/plugin-protocol';
import { LogPart } from '../../common/types';
import { HostedPluginProcess } from './hosted-plugin-process';
export interface IPCConnectionOptions {
readonly serverName: string;
readonly logger: ILogger;
readonly args: string[];
readonly errorHandler?: ConnectionErrorHandler;
}
@injectable()
export class HostedPluginSupport {
private isPluginProcessRunning = false;
private client: HostedPluginClient;
@inject(ILogger)
protected readonly logger: ILogger;
@inject(HostedPluginProcess)
protected readonly hostedPluginProcess: HostedPluginProcess;
/**
* Optional runners to delegate some work
*/
@optional()
@multiInject(ServerPluginRunner)
private readonly pluginRunners: ServerPluginRunner[];
@postConstruct()
protected init(): void {
this.pluginRunners.forEach(runner => {
runner.setDefault(this.hostedPluginProcess);
});
}
setClient(client: HostedPluginClient): void {
this.client = client;
this.hostedPluginProcess.setClient(client);
this.pluginRunners.forEach(runner => runner.setClient(client));
}
clientClosed(): void {
this.isPluginProcessRunning = false;
this.terminatePluginServer();
this.isPluginProcessRunning = false;
this.pluginRunners.forEach(runner => runner.clientClosed());
}
runPlugin(plugin: PluginModel): void {
if (!plugin.entryPoint.frontend) {
this.runPluginServer();
}
}
onMessage(pluginHostId: string, message: Uint8Array): void {
// need to perform routing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (this.pluginRunners.length > 0) {
this.pluginRunners.forEach(runner => {
if (runner.acceptMessage(pluginHostId, message)) {
runner.onMessage(pluginHostId, message);
}
});
} else {
this.hostedPluginProcess.onMessage(pluginHostId, message);
}
}
runPluginServer(serverName?: string): void {
if (!this.isPluginProcessRunning) {
this.hostedPluginProcess.runPluginServer(serverName);
this.isPluginProcessRunning = true;
}
}
sendLog(logPart: LogPart): void {
this.client.log(logPart);
}
private terminatePluginServer(): void {
this.hostedPluginProcess.terminatePluginServer();
}
}

View File

@@ -0,0 +1,65 @@
// *****************************************************************************
// Copyright (C) 2015-2018 Red Hat, Inc.
//
// 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, multiInject } from '@theia/core/shared/inversify';
import { PluginPackage, PluginScanner, PluginMetadata, PLUGIN_HOST_BACKEND, PluginIdentifiers } from '../../common/plugin-protocol';
import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager';
@injectable()
export class MetadataScanner {
private scanners: Map<string, PluginScanner> = new Map();
@inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager;
constructor(@multiInject(PluginScanner) scanners: PluginScanner[]) {
scanners.forEach((scanner: PluginScanner) => {
this.scanners.set(scanner.apiType, scanner);
});
}
async getPluginMetadata(plugin: PluginPackage): Promise<PluginMetadata> {
const scanner = this.getScanner(plugin);
const id = PluginIdentifiers.componentsToVersionedId(plugin);
return {
host: PLUGIN_HOST_BACKEND,
model: scanner.getModel(plugin),
lifecycle: scanner.getLifecycle(plugin),
outOfSync: this.uninstallationManager.isUninstalled(id) || await this.uninstallationManager.isDisabled(PluginIdentifiers.toUnversioned(id)),
};
}
/**
* Returns the first suitable scanner.
*
* Throws if no scanner was found.
*
* @param {PluginPackage} plugin
* @returns {PluginScanner}
*/
getScanner(plugin: PluginPackage): PluginScanner {
let scanner: PluginScanner | undefined;
if (plugin && plugin.engines) {
const scanners = Object.keys(plugin.engines)
.filter(engineName => this.scanners.has(engineName))
.map(engineName => this.scanners.get(engineName)!);
// get the first suitable scanner from the list
scanner = scanners[0];
}
if (!scanner) {
throw new Error('There is no suitable scanner found for ' + plugin.name);
}
return scanner;
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2023 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 { flatten } from '../../common/arrays';
import { isStringArray, isObject } from '@theia/core/lib/common/types';
import {
PluginPackage,
PluginPackageAuthenticationProvider,
PluginPackageCommand,
PluginPackageContribution,
PluginPackageCustomEditor,
PluginPackageLanguageContribution,
PluginPackageNotebook,
PluginPackageView
} from '../../common/plugin-protocol';
/**
* Most activation events can be automatically deduced from the package manifest.
* This function will update the manifest based on the plugin contributions.
*/
export function updateActivationEvents(manifest: PluginPackage): void {
if (!isObject<PluginPackage>(manifest) || !isObject<PluginPackageContribution>(manifest.contributes) || !manifest.contributes) {
return;
}
const activationEvents = new Set(isStringArray(manifest.activationEvents) ? manifest.activationEvents : []);
if (manifest.contributes.commands) {
const value = manifest.contributes.commands;
const commands = Array.isArray(value) ? value : [value];
updateCommandsContributions(commands, activationEvents);
}
if (isObject(manifest.contributes.views)) {
const views = flatten(Object.values(manifest.contributes.views));
updateViewsContribution(views, activationEvents);
}
if (Array.isArray(manifest.contributes.customEditors)) {
updateCustomEditorsContribution(manifest.contributes.customEditors, activationEvents);
}
if (Array.isArray(manifest.contributes.authentication)) {
updateAuthenticationProviderContributions(manifest.contributes.authentication, activationEvents);
}
if (Array.isArray(manifest.contributes.languages)) {
updateLanguageContributions(manifest.contributes.languages, activationEvents);
}
if (Array.isArray(manifest.contributes.notebooks)) {
updateNotebookContributions(manifest.contributes.notebooks, activationEvents);
}
manifest.activationEvents = Array.from(activationEvents);
}
function updateViewsContribution(views: PluginPackageView[], activationEvents: Set<string>): void {
for (const view of views) {
if (isObject<PluginPackageView>(view) && typeof view.id === 'string') {
activationEvents.add(`onView:${view.id}`);
}
}
}
function updateCustomEditorsContribution(customEditors: PluginPackageCustomEditor[], activationEvents: Set<string>): void {
for (const customEditor of customEditors) {
if (isObject<PluginPackageCustomEditor>(customEditor) && typeof customEditor.viewType === 'string') {
activationEvents.add(`onCustomEditor:${customEditor.viewType}`);
}
}
}
function updateCommandsContributions(commands: PluginPackageCommand[], activationEvents: Set<string>): void {
for (const command of commands) {
if (isObject<PluginPackageCommand>(command) && typeof command.command === 'string') {
activationEvents.add(`onCommand:${command.command}`);
}
}
}
function updateAuthenticationProviderContributions(authProviders: PluginPackageAuthenticationProvider[], activationEvents: Set<string>): void {
for (const authProvider of authProviders) {
if (isObject<PluginPackageAuthenticationProvider>(authProvider) && typeof authProvider.id === 'string') {
activationEvents.add(`onAuthenticationRequest:${authProvider.id}`);
}
}
}
function updateLanguageContributions(languages: PluginPackageLanguageContribution[], activationEvents: Set<string>): void {
for (const language of languages) {
if (isObject<PluginPackageLanguageContribution>(language) && typeof language.id === 'string') {
activationEvents.add(`onLanguage:${language.id}`);
}
}
}
function updateNotebookContributions(notebooks: PluginPackageNotebook[], activationEvents: Set<string>): void {
for (const notebook of notebooks) {
if (isObject<PluginPackageNotebook>(notebook) && typeof notebook.type === 'string') {
activationEvents.add(`onNotebookSerializer:${notebook.type}`);
}
}
}

View File

@@ -0,0 +1,285 @@
// *****************************************************************************
// Copyright (C) 2019 RedHat 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 fs from '@theia/core/shared/fs-extra';
import { injectable, inject } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core';
import {
PluginDeployerHandler, PluginDeployerEntry, PluginEntryPoint, DeployedPlugin,
PluginDependencies, PluginType, PluginIdentifiers
} from '../../common/plugin-protocol';
import { HostedPluginReader } from './plugin-reader';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
import { Stopwatch } from '@theia/core/lib/common';
import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager';
@injectable()
export class PluginDeployerHandlerImpl implements PluginDeployerHandler {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(HostedPluginReader)
private readonly reader: HostedPluginReader;
@inject(HostedPluginLocalizationService)
private readonly localizationService: HostedPluginLocalizationService;
@inject(Stopwatch)
protected readonly stopwatch: Stopwatch;
@inject(PluginUninstallationManager)
protected readonly uninstallationManager: PluginUninstallationManager;
private readonly deployedLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();
protected readonly sourceLocations = new Map<PluginIdentifiers.VersionedId, Set<string>>();
/**
* Managed plugin metadata backend entries.
*/
private readonly deployedBackendPlugins = new Map<PluginIdentifiers.VersionedId, DeployedPlugin>();
/**
* Managed plugin metadata frontend entries.
*/
private readonly deployedFrontendPlugins = new Map<PluginIdentifiers.VersionedId, DeployedPlugin>();
private backendPluginsMetadataDeferred = new Deferred<void>();
private frontendPluginsMetadataDeferred = new Deferred<void>();
async getDeployedFrontendPluginIds(): Promise<PluginIdentifiers.VersionedId[]> {
// await first deploy
await this.frontendPluginsMetadataDeferred.promise;
// fetch the last deployed state
return Array.from(this.deployedFrontendPlugins.keys());
}
async getDeployedBackendPluginIds(): Promise<PluginIdentifiers.VersionedId[]> {
// await first deploy
await this.backendPluginsMetadataDeferred.promise;
// fetch the last deployed state
return Array.from(this.deployedBackendPlugins.keys());
}
async getDeployedBackendPlugins(): Promise<DeployedPlugin[]> {
// await first deploy
await this.backendPluginsMetadataDeferred.promise;
// fetch the last deployed state
return Array.from(this.deployedBackendPlugins.values());
}
async getDeployedPluginIds(): Promise<readonly PluginIdentifiers.VersionedId[]> {
return [... await this.getDeployedBackendPluginIds(), ... await this.getDeployedFrontendPluginIds()];
}
async getDeployedPlugins(): Promise<DeployedPlugin[]> {
await this.frontendPluginsMetadataDeferred.promise;
await this.backendPluginsMetadataDeferred.promise;
return [...this.deployedFrontendPlugins.values(), ...this.deployedBackendPlugins.values()];
}
getDeployedPluginsById(pluginId: string): DeployedPlugin[] {
const matches: DeployedPlugin[] = [];
const handle = (plugins: Iterable<DeployedPlugin>): void => {
for (const plugin of plugins) {
if (PluginIdentifiers.componentsToVersionWithId(plugin.metadata.model).id === pluginId) {
matches.push(plugin);
}
}
};
handle(this.deployedFrontendPlugins.values());
handle(this.deployedBackendPlugins.values());
return matches;
}
getDeployedPlugin(pluginId: PluginIdentifiers.VersionedId): DeployedPlugin | undefined {
return this.deployedBackendPlugins.get(pluginId) ?? this.deployedFrontendPlugins.get(pluginId);
}
/**
* @throws never! in order to isolate plugin deployment
*/
async getPluginDependencies(entry: PluginDeployerEntry): Promise<PluginDependencies | undefined> {
const pluginPath = entry.path();
try {
const manifest = await this.reader.readPackage(pluginPath);
if (!manifest) {
return undefined;
}
const metadata = await this.reader.readMetadata(manifest);
const dependencies: PluginDependencies = { metadata };
// Do not resolve system (aka builtin) plugins because it should be done statically at build time.
if (entry.type !== PluginType.System) {
dependencies.mapping = this.reader.readDependencies(manifest);
}
return dependencies;
} catch (e) {
console.error(`Failed to load plugin dependencies from '${pluginPath}' path`, e);
return undefined;
}
}
async deployFrontendPlugins(frontendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of frontendPlugins) {
if (await this.deployPlugin(plugin, 'frontend')) { successes++; }
}
// resolve on first deploy
this.frontendPluginsMetadataDeferred.resolve(undefined);
return successes;
}
async deployBackendPlugins(backendPlugins: PluginDeployerEntry[]): Promise<number> {
let successes = 0;
for (const plugin of backendPlugins) {
if (await this.deployPlugin(plugin, 'backend')) { successes++; }
}
// rebuild translation config after deployment
await this.localizationService.buildTranslationConfig([...this.deployedBackendPlugins.values()]);
// resolve on first deploy
this.backendPluginsMetadataDeferred.resolve(undefined);
return successes;
}
/**
* @throws never! in order to isolate plugin deployment.
* @returns whether the plugin is deployed after running this function. If the plugin was already installed, will still return `true`.
*/
protected async deployPlugin(entry: PluginDeployerEntry, entryPoint: keyof PluginEntryPoint): Promise<boolean> {
const pluginPath = entry.path();
const deployPlugin = this.stopwatch.start('deployPlugin');
let id;
let success = true;
try {
const manifest = await this.reader.readPackage(pluginPath);
if (!manifest) {
deployPlugin.error(`Failed to read ${entryPoint} plugin manifest from '${pluginPath}''`);
return success = false;
}
const metadata = await this.reader.readMetadata(manifest);
metadata.isUnderDevelopment = entry.getValue('isUnderDevelopment') ?? false;
id = PluginIdentifiers.componentsToVersionedId(metadata.model);
const deployedLocations = this.deployedLocations.get(id) ?? new Set<string>();
deployedLocations.add(entry.rootPath);
this.deployedLocations.set(id, deployedLocations);
this.setSourceLocationsForPlugin(id, entry);
const deployedPlugins = entryPoint === 'backend' ? this.deployedBackendPlugins : this.deployedFrontendPlugins;
if (deployedPlugins.has(id)) {
deployPlugin.debug(`Skipped ${entryPoint} plugin ${metadata.model.name} already deployed`);
return true;
}
const { type } = entry;
const deployed: DeployedPlugin = { metadata, type };
deployed.contributes = await this.reader.readContribution(manifest);
await this.localizationService.deployLocalizations(deployed);
deployedPlugins.set(id, deployed);
deployPlugin.debug(`Deployed ${entryPoint} plugin "${id}" from "${metadata.model.entryPoint[entryPoint] || pluginPath}"`);
} catch (e) {
deployPlugin.error(`Failed to deploy ${entryPoint} plugin from '${pluginPath}' path`, e);
return success = false;
} finally {
if (success && id) {
this.markAsInstalled(id);
}
}
return success;
}
async uninstallPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
try {
const sourceLocations = this.sourceLocations.get(pluginId);
if (!sourceLocations) {
return false;
}
await Promise.all(Array.from(sourceLocations,
location => fs.remove(location).catch(err => console.error(`Failed to remove source for ${pluginId} at ${location}`, err))));
this.sourceLocations.delete(pluginId);
this.localizationService.undeployLocalizations(pluginId);
this.uninstallationManager.markAsUninstalled(pluginId);
return true;
} catch (e) {
console.error('Error uninstalling plugin', e);
return false;
}
}
protected markAsInstalled(id: PluginIdentifiers.VersionedId): void {
const metadata = PluginIdentifiers.idAndVersionFromVersionedId(id);
if (metadata) {
const toMarkAsUninstalled: PluginIdentifiers.VersionedId[] = [];
const checkForDifferentVersions = (others: Iterable<PluginIdentifiers.VersionedId>) => {
for (const other of others) {
const otherMetadata = PluginIdentifiers.idAndVersionFromVersionedId(other);
if (metadata.id === otherMetadata?.id && metadata.version !== otherMetadata.version) {
toMarkAsUninstalled.push(other);
}
}
};
checkForDifferentVersions(this.deployedFrontendPlugins.keys());
checkForDifferentVersions(this.deployedBackendPlugins.keys());
this.uninstallationManager.markAsUninstalled(...toMarkAsUninstalled);
this.uninstallationManager.markAsInstalled(id);
toMarkAsUninstalled.forEach(pluginToUninstall => this.uninstallPlugin(pluginToUninstall));
}
}
async undeployPlugin(pluginId: PluginIdentifiers.VersionedId): Promise<boolean> {
this.deployedBackendPlugins.delete(pluginId);
this.deployedFrontendPlugins.delete(pluginId);
const deployedLocations = this.deployedLocations.get(pluginId);
if (!deployedLocations) {
return false;
}
const undeployPlugin = this.stopwatch.start('undeployPlugin');
this.deployedLocations.delete(pluginId);
for (const location of deployedLocations) {
try {
await fs.remove(location);
undeployPlugin.log(`[${pluginId}]: undeployed from "${location}"`);
} catch (e) {
undeployPlugin.error(`[${pluginId}]: failed to undeploy from location "${location}". reason:`, e);
}
}
return true;
}
protected setSourceLocationsForPlugin(id: PluginIdentifiers.VersionedId, entry: PluginDeployerEntry): void {
const knownLocations = this.sourceLocations.get(id) ?? new Set();
const maybeStoredLocations = entry.getValue('sourceLocations');
const storedLocations = Array.isArray(maybeStoredLocations) && maybeStoredLocations.every(location => typeof location === 'string')
? maybeStoredLocations.concat(entry.rootPath)
: [entry.rootPath];
storedLocations.forEach(location => knownLocations.add(location));
this.sourceLocations.set(id, knownLocations);
}
async enablePlugin(pluginId: PluginIdentifiers.UnversionedId): Promise<boolean> {
return this.uninstallationManager.markAsEnabled(pluginId);
}
async disablePlugin(pluginId: PluginIdentifiers.UnversionedId): Promise<boolean> {
return this.uninstallationManager.markAsDisabled(pluginId);
}
}

View File

@@ -0,0 +1,94 @@
// *****************************************************************************
// Copyright (C) 2018-2021 Red Hat, Inc. 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 { interfaces } from '@theia/core/shared/inversify';
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { CliContribution } from '@theia/core/lib/node/cli';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { MetadataScanner } from './metadata-scanner';
import { BackendPluginHostableFilter, HostedPluginServerImpl } from './plugin-service';
import { HostedPluginReader } from './plugin-reader';
import { HostedPluginSupport } from './hosted-plugin';
import { TheiaPluginScanner } from './scanners/scanner-theia';
import { HostedPluginServer, PluginScanner, HostedPluginClient, hostedServicePath, PluginDeployerHandler, PluginHostEnvironmentVariable } from '../../common/plugin-protocol';
import { GrammarsReader } from './scanners/grammars-reader';
import { HostedPluginProcess, HostedPluginProcessConfiguration } from './hosted-plugin-process';
import { ExtPluginApiProvider } from '../../common/plugin-ext-api-contribution';
import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
import { PluginDeployerHandlerImpl } from './plugin-deployer-handler-impl';
import { PluginUriFactory } from './scanners/plugin-uri-factory';
import { FilePluginUriFactory } from './scanners/file-plugin-uri-factory';
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
import { LanguagePackService, languagePackServicePath } from '../../common/language-pack-service';
import { PluginLanguagePackService } from './plugin-language-pack-service';
import { RpcConnectionHandler } from '@theia/core/lib/common/messaging/proxy-factory';
import { ConnectionHandler } from '@theia/core/lib/common/messaging/handler';
import { isConnectionScopedBackendPlugin } from '../common/hosted-plugin';
const commonHostedConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
bind(HostedPluginProcess).toSelf().inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
bindContributionProvider(bind, Symbol.for(ExtPluginApiProvider));
bindContributionProvider(bind, PluginHostEnvironmentVariable);
bind(HostedPluginServerImpl).toSelf().inSingletonScope();
bind(HostedPluginServer).toService(HostedPluginServerImpl);
bind(BackendPluginHostableFilter).toConstantValue(isConnectionScopedBackendPlugin);
bindBackendService<HostedPluginServer, HostedPluginClient>(hostedServicePath, HostedPluginServer, (server, client) => {
server.setClient(client);
client.onDidCloseConnection(() => server.dispose());
return server;
});
});
export function bindCommonHostedBackend(bind: interfaces.Bind): void {
bind(HostedPluginCliContribution).toSelf().inSingletonScope();
bind(CliContribution).toService(HostedPluginCliContribution);
bind(MetadataScanner).toSelf().inSingletonScope();
bind(HostedPluginReader).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(HostedPluginReader);
bind(HostedPluginLocalizationService).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(HostedPluginLocalizationService);
bind(PluginDeployerHandlerImpl).toSelf().inSingletonScope();
bind(PluginDeployerHandler).toService(PluginDeployerHandlerImpl);
bind(PluginLanguagePackService).toSelf().inSingletonScope();
bind(LanguagePackService).toService(PluginLanguagePackService);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(languagePackServicePath, () =>
ctx.container.get(LanguagePackService)
)
).inSingletonScope();
bind(GrammarsReader).toSelf().inSingletonScope();
bind(HostedPluginProcessConfiguration).toConstantValue({
path: path.join(__dirname, 'plugin-host'),
});
bind(ConnectionContainerModule).toConstantValue(commonHostedConnectionModule);
bind(PluginUriFactory).to(FilePluginUriFactory).inSingletonScope();
}
export function bindHostedBackend(bind: interfaces.Bind): void {
bindCommonHostedBackend(bind);
bind(PluginScanner).to(TheiaPluginScanner).inSingletonScope();
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// 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 { LogLevel } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { PluginLogger } from '../../plugin/logger';
import { format } from 'util';
export function setupPluginHostLogger(rpc: RPCProtocol): void {
const logger = new PluginLogger(rpc, 'plugin-host');
function createLog(level: LogLevel): typeof console.log {
return (message, ...params) => {
// Format the messages beforehand
// This ensures that we don't accidentally send objects that are not serializable
const formatted = format(message, ...params);
logger.log(level, formatted);
};
}
console.log = console.info = createLog(LogLevel.Info);
console.debug = createLog(LogLevel.Debug);
console.warn = createLog(LogLevel.Warn);
console.error = createLog(LogLevel.Error);
console.trace = createLog(LogLevel.Trace);
}

View File

@@ -0,0 +1,77 @@
// *****************************************************************************
// 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 '@theia/core/shared/reflect-metadata';
import { ContainerModule } from '@theia/core/shared/inversify';
import { RPCProtocol, RPCProtocolImpl } from '../../common/rpc-protocol';
import { AbstractPluginHostRPC, PluginHostRPC, PluginContainerModuleLoader } from './plugin-host-rpc';
import { AbstractPluginManagerExtImpl, MinimalTerminalServiceExt, PluginManagerExtImpl } from '../../plugin/plugin-manager';
import { IPCChannel } from '@theia/core/lib/node';
import { InternalPluginContainerModule } from '../../plugin/node/plugin-container-module';
import { LocalizationExt } from '../../common/plugin-api-rpc';
import { EnvExtImpl } from '../../plugin/env';
import { EnvNodeExtImpl } from '../../plugin/node/env-node-ext';
import { LocalizationExtImpl } from '../../plugin/localization-ext';
import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry';
import { DebugExtImpl } from '../../plugin/debug/debug-ext';
import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents';
import { WorkspaceExtImpl } from '../../plugin/workspace';
import { MessageRegistryExt } from '../../plugin/message-registry';
import { ClipboardExt } from '../../plugin/clipboard-ext';
import { KeyValueStorageProxy, InternalStorageExt } from '../../plugin/plugin-storage';
import { WebviewsExtImpl } from '../../plugin/webviews';
import { TerminalServiceExtImpl } from '../../plugin/terminal-ext';
import { InternalSecretsExt, SecretsExtImpl } from '../../plugin/secrets-ext';
import { setupPluginHostLogger } from './plugin-host-logger';
import { LmExtImpl } from '../../plugin/lm-ext';
import { EncodingService } from '@theia/core/lib/common/encoding-service';
export default new ContainerModule(bind => {
const channel = new IPCChannel();
const rpc = new RPCProtocolImpl(channel);
setupPluginHostLogger(rpc);
bind(RPCProtocol).toConstantValue(rpc);
bind(PluginContainerModuleLoader).toDynamicValue(({ container }) =>
(module: ContainerModule) => {
container.load(module);
const internalModule = module as InternalPluginContainerModule;
const pluginApiCache = internalModule.initializeApi?.(container);
return pluginApiCache;
}).inSingletonScope();
bind(AbstractPluginHostRPC).toService(PluginHostRPC);
bind(AbstractPluginManagerExtImpl).toService(PluginManagerExtImpl);
bind(PluginManagerExtImpl).toSelf().inSingletonScope();
bind(PluginHostRPC).toSelf().inSingletonScope();
bind(EnvExtImpl).to(EnvNodeExtImpl).inSingletonScope();
bind(LocalizationExt).to(LocalizationExtImpl).inSingletonScope();
bind(InternalStorageExt).toService(KeyValueStorageProxy);
bind(KeyValueStorageProxy).toSelf().inSingletonScope();
bind(InternalSecretsExt).toService(SecretsExtImpl);
bind(SecretsExtImpl).toSelf().inSingletonScope();
bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope();
bind(DebugExtImpl).toSelf().inSingletonScope();
bind(LmExtImpl).toSelf().inSingletonScope();
bind(EncodingService).toSelf().inSingletonScope();
bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope();
bind(WorkspaceExtImpl).toSelf().inSingletonScope();
bind(MessageRegistryExt).toSelf().inSingletonScope();
bind(ClipboardExt).toSelf().inSingletonScope();
bind(WebviewsExtImpl).toSelf().inSingletonScope();
bind(MinimalTerminalServiceExt).toService(TerminalServiceExtImpl);
bind(TerminalServiceExtImpl).toSelf().inSingletonScope();
});

View File

@@ -0,0 +1,82 @@
/********************************************************************************
* Copyright (C) 2022 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 * as http from 'http';
import * as https from 'https';
import * as tls from 'tls';
import { createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting } from '@vscode/proxy-agent';
import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry';
import { WorkspaceExtImpl } from '../../plugin/workspace';
export function connectProxyResolver(workspaceExt: WorkspaceExtImpl, configProvider: PreferenceRegistryExtImpl): void {
const resolveProxy = createProxyResolver({
resolveProxy: async url => workspaceExt.resolveProxy(url),
getHttpProxySetting: () => configProvider.getConfiguration('http').get('proxy'),
log: () => { },
getLogLevel: () => 0,
proxyResolveTelemetry: () => { },
useHostProxy: true,
env: process.env,
});
const lookup = createPatchedModules(configProvider, resolveProxy);
configureModuleLoading(lookup);
}
interface PatchedModules {
http: typeof http;
https: typeof https;
tls: typeof tls;
}
function createPatchedModules(configProvider: PreferenceRegistryExtImpl, resolveProxy: ReturnType<typeof createProxyResolver>): PatchedModules {
const defaultConfig = 'override' as ProxySupportSetting;
const proxySetting = {
config: defaultConfig
};
const certSetting = {
config: false
};
configProvider.onDidChangeConfiguration(() => {
const httpConfig = configProvider.getConfiguration('http');
proxySetting.config = httpConfig?.get<ProxySupportSetting>('proxySupport') || defaultConfig;
certSetting.config = !!httpConfig?.get<boolean>('systemCertificates');
});
return {
http: Object.assign(http, createHttpPatch(http, resolveProxy, proxySetting, certSetting, true)),
https: Object.assign(https, createHttpPatch(https, resolveProxy, proxySetting, certSetting, true)),
tls: Object.assign(tls, createTlsPatch(tls))
};
}
function configureModuleLoading(lookup: PatchedModules): void {
const node_module = require('module');
const original = node_module._load;
node_module._load = function (request: string): typeof tls | typeof http | typeof https {
if (request === 'tls') {
return lookup.tls;
}
if (request !== 'http' && request !== 'https') {
return original.apply(this, arguments);
}
// Create a shallow copy of the http(s) module to work around extensions that apply changes to the modules
// See for more info: https://github.com/microsoft/vscode/issues/93167
return { ...lookup[request] };
};
}

View File

@@ -0,0 +1,380 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { dynamicRequire, removeFromCache } from '@theia/core/lib/node/dynamic-require';
import { ContainerModule, inject, injectable, postConstruct, unmanaged } from '@theia/core/shared/inversify';
import { AbstractPluginManagerExtImpl, PluginHost, PluginManagerExtImpl } from '../../plugin/plugin-manager';
import {
MAIN_RPC_CONTEXT, Plugin, PluginAPIFactory, PluginManager,
LocalizationExt
} from '../../common/plugin-api-rpc';
import { PluginMetadata, PluginModel } from '../../common/plugin-protocol';
import { createAPIFactory } from '../../plugin/plugin-context';
import { EnvExtImpl } from '../../plugin/env';
import { PreferenceRegistryExtImpl } from '../../plugin/preference-registry';
import { ExtPluginApi, ExtPluginApiBackendInitializationFn } from '../../common/plugin-ext-api-contribution';
import { DebugExtImpl } from '../../plugin/debug/debug-ext';
import { EditorsAndDocumentsExtImpl } from '../../plugin/editors-and-documents';
import { WorkspaceExtImpl } from '../../plugin/workspace';
import { MessageRegistryExt } from '../../plugin/message-registry';
import { ClipboardExt } from '../../plugin/clipboard-ext';
import { loadManifest } from './plugin-manifest-loader';
import { KeyValueStorageProxy } from '../../plugin/plugin-storage';
import { WebviewsExtImpl } from '../../plugin/webviews';
import { TerminalServiceExtImpl } from '../../plugin/terminal-ext';
import { SecretsExtImpl } from '../../plugin/secrets-ext';
import { connectProxyResolver } from './plugin-host-proxy';
import { LocalizationExtImpl } from '../../plugin/localization-ext';
import { RPCProtocol, ProxyIdentifier } from '../../common/rpc-protocol';
import { PluginApiCache } from '../../plugin/node/plugin-container-module';
import { overridePluginDependencies } from './plugin-require-override';
/**
* The full set of all possible `Ext` interfaces that a plugin manager can support.
*/
export interface ExtInterfaces {
envExt: EnvExtImpl,
storageExt: KeyValueStorageProxy,
debugExt: DebugExtImpl,
editorsAndDocumentsExt: EditorsAndDocumentsExtImpl,
messageRegistryExt: MessageRegistryExt,
workspaceExt: WorkspaceExtImpl,
preferenceRegistryExt: PreferenceRegistryExtImpl,
clipboardExt: ClipboardExt,
webviewExt: WebviewsExtImpl,
terminalServiceExt: TerminalServiceExtImpl,
secretsExt: SecretsExtImpl,
localizationExt: LocalizationExtImpl
}
/**
* The RPC proxy identifier keys to set in the RPC object to register our `Ext` interface implementations.
*/
export type RpcKeys<EXT extends Partial<ExtInterfaces>> = Partial<Record<keyof EXT, ProxyIdentifier<any>>> & {
$pluginManager: ProxyIdentifier<any>;
};
export const PluginContainerModuleLoader = Symbol('PluginContainerModuleLoader');
/**
* A function that loads a `PluginContainerModule` exported by a plugin's entry-point
* script, returning the per-`Container` cache of its exported API instances if the
* module has an API factory registered.
*/
export type PluginContainerModuleLoader = (module: ContainerModule) => PluginApiCache<object> | undefined;
/**
* Handle the RPC calls.
*
* @template PM is the plugin manager (ext) type
* @template PAF is the plugin API factory type
* @template EXT is the type identifying the `Ext` interfaces supported by the plugin manager
*/
@injectable()
export abstract class AbstractPluginHostRPC<PM extends AbstractPluginManagerExtImpl<any>, PAF, EXT extends Partial<ExtInterfaces>> {
@inject(RPCProtocol)
protected readonly rpc: any;
@inject(PluginContainerModuleLoader)
protected readonly loadContainerModule: PluginContainerModuleLoader;
@inject(AbstractPluginManagerExtImpl)
protected readonly pluginManager: PM;
protected readonly banner: string;
protected apiFactory: PAF;
constructor(
@unmanaged() name: string,
@unmanaged() private readonly backendInitPath: string | undefined,
@unmanaged() private readonly extRpc: RpcKeys<EXT>) {
this.banner = `${name}(${process.pid}):`;
}
@postConstruct()
initialize(): void {
overridePluginDependencies();
this.pluginManager.setPluginHost(this.createPluginHost());
const extInterfaces = this.createExtInterfaces();
this.registerExtInterfaces(extInterfaces);
this.apiFactory = this.createAPIFactory(extInterfaces);
this.loadContainerModule(new ContainerModule(bind => bind(PluginManager).toConstantValue(this.pluginManager)));
}
async terminate(): Promise<void> {
await this.pluginManager.terminate();
}
protected abstract createAPIFactory(extInterfaces: EXT): PAF;
protected abstract createExtInterfaces(): EXT;
protected registerExtInterfaces(extInterfaces: EXT): void {
for (const _key in this.extRpc) {
if (Object.hasOwnProperty.call(this.extRpc, _key)) {
const key = _key as keyof ExtInterfaces;
// In case of present undefineds
if (extInterfaces[key]) {
this.rpc.set(this.extRpc[key], extInterfaces[key]);
}
}
}
this.rpc.set(this.extRpc.$pluginManager, this.pluginManager);
}
initContext(contextPath: string, plugin: Plugin): void {
const { name, version } = plugin.rawModel;
console.debug(this.banner, 'initializing(' + name + '@' + version + ' with ' + contextPath + ')');
try {
type BackendInitFn = (pluginApiFactory: PAF, plugin: Plugin) => void;
const backendInit = dynamicRequire<{ doInitialization: BackendInitFn }>(contextPath);
backendInit.doInitialization(this.apiFactory, plugin);
} catch (e) {
console.error(e);
}
}
protected getBackendPluginPath(pluginModel: PluginModel): string | undefined {
return pluginModel.entryPoint.backend;
}
/**
* Create the {@link PluginHost} that is required by my plugin manager ext interface to delegate
* critical behaviour such as loading and initializing plugins to me.
*/
createPluginHost(): PluginHost {
const { extensionTestsPath } = process.env;
const self = this;
return {
loadPlugin(plugin: Plugin): any {
console.debug(self.banner, 'PluginManagerExtImpl/loadPlugin(' + plugin.pluginPath + ')');
// cleaning the cache for all files of that plug-in.
// this prevents a memory leak on plugin host restart. See for reference:
// https://github.com/eclipse-theia/theia/pull/4931
// https://github.com/nodejs/node/issues/8443
removeFromCache(mod => mod.id.startsWith(plugin.pluginFolder));
if (plugin.pluginPath) {
return dynamicRequire(plugin.pluginPath);
}
},
async init(raw: PluginMetadata[]): Promise<[Plugin[], Plugin[]]> {
console.log(self.banner, 'PluginManagerExtImpl/init()');
const result: Plugin[] = [];
const foreign: Plugin[] = [];
for (const plg of raw) {
try {
const pluginModel = plg.model;
const pluginLifecycle = plg.lifecycle;
const rawModel = await loadManifest(pluginModel.packagePath);
rawModel.packagePath = pluginModel.packagePath;
if (pluginModel.entryPoint!.frontend) {
foreign.push({
pluginPath: pluginModel.entryPoint.frontend!,
pluginFolder: pluginModel.packagePath,
pluginUri: pluginModel.packageUri,
model: pluginModel,
lifecycle: pluginLifecycle,
rawModel,
isUnderDevelopment: !!plg.isUnderDevelopment
});
} else {
// Headless and backend plugins are, for now, very similar
let backendInitPath = pluginLifecycle.backendInitPath;
// if no init path, try to init as regular Theia plugin
if (!backendInitPath && self.backendInitPath) {
backendInitPath = __dirname + self.backendInitPath;
}
const pluginPath = self.getBackendPluginPath(pluginModel);
const plugin: Plugin = {
pluginPath,
pluginFolder: pluginModel.packagePath,
pluginUri: pluginModel.packageUri,
model: pluginModel,
lifecycle: pluginLifecycle,
rawModel,
isUnderDevelopment: !!plg.isUnderDevelopment
};
if (backendInitPath) {
self.initContext(backendInitPath, plugin);
} else {
const { name, version } = plugin.rawModel;
console.debug(self.banner, 'initializing(' + name + '@' + version + ' without any default API)');
}
result.push(plugin);
}
} catch (e) {
console.error(self.banner, `Failed to initialize ${plg.model.id} plugin.`, e);
}
}
return [result, foreign];
},
initExtApi(extApi: ExtPluginApi[]): void {
for (const api of extApi) {
try {
self.initExtApi(api);
} catch (e) {
console.error(e);
}
}
},
loadTests: extensionTestsPath ? async () => {
// Require the test runner via node require from the provided path
let testRunner: any;
let requireError: Error | undefined;
try {
testRunner = dynamicRequire(extensionTestsPath);
} catch (error) {
requireError = error;
}
// Execute the runner if it follows our spec
if (testRunner && typeof testRunner.run === 'function') {
return new Promise<void>((resolve, reject) => {
testRunner.run(extensionTestsPath, (error: any) => {
if (error) {
reject(error.toString());
} else {
resolve(undefined);
}
});
});
}
throw new Error(requireError ?
requireError.toString() :
`Path ${extensionTestsPath} does not point to a valid extension test runner.`
);
} : undefined
};
}
/**
* Initialize the end of the given provided extension API applicable to the current plugin host.
* Errors should be propagated to the caller.
*
* @param extApi the extension API to initialize, if appropriate
* @throws if any error occurs in initializing the extension API
*/
protected abstract initExtApi(extApi: ExtPluginApi): void;
}
/**
* The RPC handler for frontend-connection-scoped plugins (Theia and VSCode plugins).
*/
@injectable()
export class PluginHostRPC extends AbstractPluginHostRPC<PluginManagerExtImpl, PluginAPIFactory, ExtInterfaces> {
@inject(EnvExtImpl)
protected readonly envExt: EnvExtImpl;
@inject(LocalizationExt)
protected readonly localizationExt: LocalizationExtImpl;
@inject(KeyValueStorageProxy)
protected readonly keyValueStorageProxy: KeyValueStorageProxy;
@inject(DebugExtImpl)
protected readonly debugExt: DebugExtImpl;
@inject(EditorsAndDocumentsExtImpl)
protected readonly editorsAndDocumentsExt: EditorsAndDocumentsExtImpl;
@inject(MessageRegistryExt)
protected readonly messageRegistryExt: MessageRegistryExt;
@inject(WorkspaceExtImpl)
protected readonly workspaceExt: WorkspaceExtImpl;
@inject(PreferenceRegistryExtImpl)
protected readonly preferenceRegistryExt: PreferenceRegistryExtImpl;
@inject(ClipboardExt)
protected readonly clipboardExt: ClipboardExt;
@inject(WebviewsExtImpl)
protected readonly webviewExt: WebviewsExtImpl;
@inject(TerminalServiceExtImpl)
protected readonly terminalServiceExt: TerminalServiceExtImpl;
@inject(SecretsExtImpl)
protected readonly secretsExt: SecretsExtImpl;
constructor() {
super('PLUGIN_HOST', '/scanners/backend-init-theia.js',
{
$pluginManager: MAIN_RPC_CONTEXT.HOSTED_PLUGIN_MANAGER_EXT,
editorsAndDocumentsExt: MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT,
workspaceExt: MAIN_RPC_CONTEXT.WORKSPACE_EXT,
preferenceRegistryExt: MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT,
storageExt: MAIN_RPC_CONTEXT.STORAGE_EXT,
webviewExt: MAIN_RPC_CONTEXT.WEBVIEWS_EXT,
secretsExt: MAIN_RPC_CONTEXT.SECRETS_EXT
}
);
}
protected createExtInterfaces(): ExtInterfaces {
connectProxyResolver(this.workspaceExt, this.preferenceRegistryExt);
return {
envExt: this.envExt,
storageExt: this.keyValueStorageProxy,
debugExt: this.debugExt,
editorsAndDocumentsExt: this.editorsAndDocumentsExt,
messageRegistryExt: this.messageRegistryExt,
workspaceExt: this.workspaceExt,
preferenceRegistryExt: this.preferenceRegistryExt,
clipboardExt: this.clipboardExt,
webviewExt: this.webviewExt,
terminalServiceExt: this.terminalServiceExt,
secretsExt: this.secretsExt,
localizationExt: this.localizationExt
};
}
protected createAPIFactory(extInterfaces: ExtInterfaces): PluginAPIFactory {
const {
envExt, debugExt, preferenceRegistryExt, editorsAndDocumentsExt, workspaceExt,
messageRegistryExt, clipboardExt, webviewExt, localizationExt
} = extInterfaces;
return createAPIFactory(this.rpc, this.pluginManager, envExt, debugExt, preferenceRegistryExt,
editorsAndDocumentsExt, workspaceExt, messageRegistryExt, clipboardExt, webviewExt,
localizationExt);
}
protected initExtApi(extApi: ExtPluginApi): void {
interface PluginExports {
containerModule?: ContainerModule;
provideApi?: ExtPluginApiBackendInitializationFn;
}
if (extApi.backendInitPath) {
const { containerModule, provideApi } = dynamicRequire<PluginExports>(extApi.backendInitPath);
if (containerModule) {
this.loadContainerModule(containerModule);
}
if (provideApi) {
provideApi(this.rpc, this.pluginManager);
}
}
}
}

View File

@@ -0,0 +1,122 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 '@theia/core/shared/reflect-metadata';
import { Container } from '@theia/core/shared/inversify';
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
import { MsgPackExtensionManager } from '@theia/core/lib/common/message-rpc/msg-pack-extension-manager';
import { ConnectionClosedError, MsgPackExtensionTag, RPCProtocol } from '../../common/rpc-protocol';
import { ProcessTerminatedMessage, ProcessTerminateMessage } from './hosted-plugin-protocol';
import { PluginHostRPC } from './plugin-host-rpc';
import pluginHostModule from './plugin-host-module';
import { URI } from '../../plugin/types-impl';
console.log('PLUGIN_HOST(' + process.pid + ') starting instance');
// override exit() function, to do not allow plugin kill this node
process.exit = function (code?: number): void {
const err = new Error('An plugin call process.exit() and it was prevented.');
console.warn(err.stack);
} as (code?: number) => never;
// same for 'crash'(works only in electron)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const proc = process as any;
if (proc.crash) {
proc.crash = function (): void {
const err = new Error('An plugin call process.crash() and it was prevented.');
console.warn(err.stack);
};
}
process.on('uncaughtException', (err: Error) => {
console.error(err);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unhandledPromises: Promise<any>[] = [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.on('unhandledRejection', (reason: any, promise: Promise<any>) => {
unhandledPromises.push(promise);
setTimeout(() => {
const index = unhandledPromises.indexOf(promise);
if (index >= 0) {
promise.catch(err => {
unhandledPromises.splice(index, 1);
if (terminating && (ConnectionClosedError.is(err) || ConnectionClosedError.is(reason))) {
// during termination it is expected that pending rpc request are rejected
return;
}
console.error(`Promise rejection not handled in one second: ${err} , reason: ${reason}`);
if (err && err.stack) {
console.error(`With stack trace: ${err.stack}`);
}
});
}
}, 1000);
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
process.on('rejectionHandled', (promise: Promise<any>) => {
const index = unhandledPromises.indexOf(promise);
if (index >= 0) {
unhandledPromises.splice(index, 1);
}
});
// Our own vscode.Uri class with a custom reviver had been introduced in #9422;
// the custom reviver was then removed in #11261 without any replacement, which
// caused `uri instanceof vscode.Uri` checks to no longer succeed for deserialized URIs
// in plugins. This code reestablishes the custom deserialization for URIs.
const vsCodeUriMsgPackExtension = MsgPackExtensionManager.getInstance().getExtension(MsgPackExtensionTag.VsCodeUri);
if (vsCodeUriMsgPackExtension?.class === VSCodeURI) { // double-check the extension class
vsCodeUriMsgPackExtension.deserialize = data => URI.parse(data); // create an instance of our local plugin API URI class
}
let terminating = false;
const container = new Container();
container.load(pluginHostModule);
const rpc: RPCProtocol = container.get(RPCProtocol);
const pluginHostRPC = container.get(PluginHostRPC);
process.on('message', async (message: string) => {
if (terminating) {
return;
}
try {
const msg = JSON.parse(message);
if (ProcessTerminateMessage.is(msg)) {
terminating = true;
if (msg.stopTimeout) {
await Promise.race([
pluginHostRPC.terminate(),
new Promise(resolve => setTimeout(resolve, msg.stopTimeout))
]);
} else {
await pluginHostRPC.terminate();
}
rpc.dispose();
if (process.send) {
process.send(JSON.stringify({ type: ProcessTerminatedMessage.TYPE }));
}
}
} catch (e) {
console.error(e);
}
});

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// Copyright (C) 2023 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 { LanguagePackBundle, LanguagePackService } from '../../common/language-pack-service';
@injectable()
export class PluginLanguagePackService implements LanguagePackService {
protected readonly storage = new Map<string, Map<string, LanguagePackBundle>>();
storeBundle(pluginId: string, locale: string, bundle: LanguagePackBundle): void {
if (!this.storage.has(pluginId)) {
this.storage.set(pluginId, new Map());
}
this.storage.get(pluginId)!.set(locale, bundle);
}
deleteBundle(pluginId: string, locale?: string): void {
if (locale) {
this.storage.get(pluginId)?.delete(locale);
} else {
this.storage.delete(pluginId);
}
}
async getBundle(pluginId: string, locale: string): Promise<LanguagePackBundle | undefined> {
return this.storage.get(pluginId)?.get(locale);
}
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 * as fs from '@theia/core/shared/fs-extra';
import { PluginIdentifiers, PluginPackage } from '../../common';
import { updateActivationEvents } from './plugin-activation-events';
export async function loadManifest(pluginPath: string): Promise<PluginPackage> {
const manifest = await fs.readJson(path.join(pluginPath, 'package.json'));
// translate vscode builtins, as they are published with a prefix. See https://github.com/theia-ide/vscode-builtin-extensions/blob/master/src/republish.js#L50
const built_prefix = '@theia/vscode-builtin-';
if (manifest && manifest.name && manifest.name.startsWith(built_prefix)) {
manifest.name = manifest.name.substring(built_prefix.length);
}
manifest.publisher ??= PluginIdentifiers.UNPUBLISHED;
updateActivationEvents(manifest);
return manifest;
}

View File

@@ -0,0 +1,172 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 * as express from '@theia/core/shared/express';
import * as escape_html from 'escape-html';
import { realpath, stat } from 'fs/promises';
import { ILogger } from '@theia/core';
import { inject, injectable, optional, multiInject } from '@theia/core/shared/inversify';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { PluginMetadata, getPluginId, MetadataProcessor, PluginPackage, PluginContribution } from '../../common/plugin-protocol';
import { MetadataScanner } from './metadata-scanner';
import { loadManifest } from './plugin-manifest-loader';
@injectable()
export class HostedPluginReader implements BackendApplicationContribution {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(MetadataScanner)
protected readonly scanner: MetadataScanner;
@optional()
@multiInject(MetadataProcessor) private readonly metadataProcessors: MetadataProcessor[];
/**
* Map between a plugin id and its local storage
*/
protected pluginsIdsFiles: Map<string, string> = new Map();
configure(app: express.Application): void {
app.get('/hostedPlugin/:pluginId/:path(*)', async (req, res) => {
const pluginId = req.params.pluginId;
const filePath = req.params.path;
const localPath = this.pluginsIdsFiles.get(pluginId);
if (localPath) {
const absolutePath = path.resolve(localPath, filePath);
const resolvedFile = await this.resolveFile(absolutePath);
if (!resolvedFile) {
res.status(404).send(`No such file found in '${escape_html(pluginId)}' plugin.`);
return;
}
res.sendFile(resolvedFile, e => {
if (!e) {
// the file was found and successfully transferred
return;
}
console.error(`Could not transfer '${filePath}' file from '${pluginId}'`, e);
if (res.headersSent) {
// the request was already closed
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((e as any)['code'] === 'ENOENT') {
res.status(404).send(`No such file found in '${escape_html(pluginId)}' plugin.`);
} else {
res.status(500).send(`Failed to transfer a file from '${escape_html(pluginId)}' plugin.`);
}
});
} else {
await this.handleMissingResource(req, res);
}
});
}
/**
* Resolves a plugin file path with fallback to .js and .cjs extensions.
*
* This handles cases where plugins reference modules without extensions,
* which is common in Node.js/CommonJS environments.
*
*/
protected async resolveFile(absolutePath: string): Promise<string | undefined> {
const candidates = [absolutePath];
const pathExtension = path.extname(absolutePath).toLowerCase();
if (!pathExtension) {
candidates.push(absolutePath + '.js');
candidates.push(absolutePath + '.cjs');
}
for (const candidate of candidates) {
try {
const stats = await stat(candidate);
if (stats.isFile()) {
return candidate;
}
} catch {
// File doesn't exist or is inaccessible - try next candidate
// Actual 404 errors are handled by the caller
}
}
return undefined;
}
protected async handleMissingResource(req: express.Request, res: express.Response): Promise<void> {
const pluginId = req.params.pluginId;
res.status(404).send(`The plugin with id '${escape_html(pluginId)}' does not exist.`);
}
/**
* @throws never
*/
async getPluginMetadata(pluginPath: string | undefined): Promise<PluginMetadata | undefined> {
try {
const manifest = await this.readPackage(pluginPath);
return manifest && this.readMetadata(manifest);
} catch (e) {
this.logger.error(`Failed to load plugin metadata from "${pluginPath}"`, e);
return undefined;
}
}
async readPackage(pluginPath: string | undefined): Promise<PluginPackage | undefined> {
if (!pluginPath) {
return undefined;
}
const resolvedPluginPath = await realpath(pluginPath);
const manifest = await loadManifest(resolvedPluginPath);
if (!manifest) {
return undefined;
}
manifest.packagePath = resolvedPluginPath;
return manifest;
}
async readMetadata(plugin: PluginPackage): Promise<PluginMetadata> {
const pluginMetadata = await this.scanner.getPluginMetadata(plugin);
if (pluginMetadata.model.entryPoint.backend) {
pluginMetadata.model.entryPoint.backend = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.backend);
}
if (pluginMetadata.model.entryPoint.headless) {
pluginMetadata.model.entryPoint.headless = path.resolve(plugin.packagePath, pluginMetadata.model.entryPoint.headless);
}
if (pluginMetadata) {
// Add post processor
if (this.metadataProcessors) {
this.metadataProcessors.forEach(metadataProcessor => {
metadataProcessor.process(pluginMetadata);
});
}
this.pluginsIdsFiles.set(getPluginId(pluginMetadata.model), plugin.packagePath);
}
return pluginMetadata;
}
async readContribution(plugin: PluginPackage): Promise<PluginContribution | undefined> {
const scanner = this.scanner.getScanner(plugin);
return scanner.getContribution(plugin);
}
readDependencies(plugin: PluginPackage): Map<string, string> | undefined {
const scanner = this.scanner.getScanner(plugin);
return scanner.getDependencies(plugin);
}
}

View File

@@ -0,0 +1,53 @@
/********************************************************************************
* 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 * as nodePty from 'node-pty';
const overrides = [
{
package: 'node-pty',
module: nodePty
}
];
/**
* Some plugins attempt to require some packages from VSCode's node_modules.
* Since we don't have node_modules usually, we need to override the require function to return the expected package.
*
* See also:
* https://github.com/eclipse-theia/theia/issues/14714
* https://github.com/eclipse-theia/theia/issues/13779
*/
export function overridePluginDependencies(): void {
const node_module = require('module');
const original = node_module._load;
node_module._load = function (request: string): unknown {
try {
// Attempt to load the original module
// In some cases VS Code extensions will come with their own `node_modules` folder
return original.apply(this, arguments);
} catch (e) {
// If the `require` call failed, attempt to load the module from the overrides
for (const filter of overrides) {
if (request === filter.package || request.endsWith(`node_modules/${filter.package}`) || request.endsWith(`node_modules\\${filter.package}`)) {
return filter.module;
}
}
// If no override was found, rethrow the error
throw e;
}
};
}

View File

@@ -0,0 +1,211 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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, named, optional, postConstruct } from '@theia/core/shared/inversify';
import { HostedPluginServer, HostedPluginClient, PluginDeployer, DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol';
import { HostedPluginSupport } from './hosted-plugin';
import { ILogger, Disposable, ContributionProvider, DisposableCollection } from '@theia/core';
import { ExtPluginApiProvider, ExtPluginApi } from '../../common/plugin-ext-api-contribution';
import { PluginDeployerHandlerImpl } from './plugin-deployer-handler-impl';
import { PluginDeployerImpl } from '../../main/node/plugin-deployer-impl';
import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
import { PluginUninstallationManager } from '../../main/node/plugin-uninstallation-manager';
import { Deferred } from '@theia/core/lib/common/promise-util';
export const BackendPluginHostableFilter = Symbol('BackendPluginHostableFilter');
/**
* A filter matching backend plugins that are hostable in my plugin host process.
* Only if at least one backend plugin is deployed that matches my filter will I
* start the host process.
*/
export type BackendPluginHostableFilter = (plugin: DeployedPlugin) => boolean;
/**
* This class implements the per-front-end services for plugin management and communication
*/
@injectable()
export class HostedPluginServerImpl implements HostedPluginServer {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(PluginDeployerHandlerImpl)
protected readonly deployerHandler: PluginDeployerHandlerImpl;
@inject(PluginDeployer)
protected readonly pluginDeployer: PluginDeployerImpl;
@inject(HostedPluginLocalizationService)
protected readonly localizationService: HostedPluginLocalizationService;
@inject(ContributionProvider)
@named(Symbol.for(ExtPluginApiProvider))
protected readonly extPluginAPIContributions: ContributionProvider<ExtPluginApiProvider>;
@inject(PluginUninstallationManager) protected readonly uninstallationManager: PluginUninstallationManager;
@inject(BackendPluginHostableFilter)
@optional()
protected backendPluginHostableFilter: BackendPluginHostableFilter;
protected client: HostedPluginClient | undefined;
protected toDispose = new DisposableCollection();
protected uninstalledPlugins: Set<PluginIdentifiers.VersionedId>;
protected disabledPlugins: Set<PluginIdentifiers.UnversionedId>;
protected readonly pluginVersions = new Map<PluginIdentifiers.UnversionedId, string>();
protected readonly initialized = new Deferred<void>();
constructor(
@inject(HostedPluginSupport) private readonly hostedPlugin: HostedPluginSupport) {
}
@postConstruct()
protected init(): void {
if (!this.backendPluginHostableFilter) {
this.backendPluginHostableFilter = () => true;
}
this.uninstalledPlugins = new Set(this.uninstallationManager.getUninstalledPluginIds());
const asyncInit = async () => {
this.disabledPlugins = new Set(await this.uninstallationManager.getDisabledPluginIds());
this.toDispose.pushAll([
this.pluginDeployer.onDidDeploy(() => this.client?.onDidDeploy()),
this.uninstallationManager.onDidChangeUninstalledPlugins(currentUninstalled => {
if (this.uninstalledPlugins) {
const uninstalled = new Set(currentUninstalled);
for (const previouslyUninstalled of this.uninstalledPlugins) {
if (!uninstalled.has(previouslyUninstalled)) {
this.uninstalledPlugins.delete(previouslyUninstalled);
}
}
}
this.client?.onDidDeploy();
}),
this.uninstallationManager.onDidChangeDisabledPlugins(currentlyDisabled => {
if (this.disabledPlugins) {
const disabled = new Set(currentlyDisabled);
for (const previouslyUninstalled of this.disabledPlugins) {
if (!disabled.has(previouslyUninstalled)) {
this.disabledPlugins.delete(previouslyUninstalled);
}
}
}
this.client?.onDidDeploy();
}),
Disposable.create(() => this.hostedPlugin.clientClosed()),
]);
this.initialized.resolve();
};
asyncInit();
}
protected getServerName(): string {
return 'hosted-plugin';
}
dispose(): void {
this.toDispose.dispose();
}
setClient(client: HostedPluginClient): void {
this.client = client;
this.hostedPlugin.setClient(client);
}
async getDeployedPluginIds(): Promise<PluginIdentifiers.VersionedId[]> {
return this.getInstalledPluginIds()
.then(ids => ids.filter(candidate => this.isInstalledPlugin(candidate) && !this.disabledPlugins.has(PluginIdentifiers.toUnversioned(candidate))));
}
async getInstalledPluginIds(): Promise<PluginIdentifiers.VersionedId[]> {
await this.initialized.promise;
const backendPlugins = (await this.deployerHandler.getDeployedBackendPlugins())
.filter(this.backendPluginHostableFilter);
if (backendPlugins.length > 0) {
this.hostedPlugin.runPluginServer(this.getServerName());
}
const plugins = new Set<PluginIdentifiers.VersionedId>();
const addIds = (identifiers: Promise<PluginIdentifiers.VersionedId[]>): Promise<void> => identifiers
.then(ids => ids.forEach(id => this.isInstalledPlugin(id) && plugins.add(id)));
await Promise.all([
addIds(this.deployerHandler.getDeployedFrontendPluginIds()),
addIds(this.deployerHandler.getDeployedBackendPluginIds()),
]);
return Array.from(plugins);
}
/**
* Ensures that the plugin was not uninstalled when this session was started
* and that it matches the first version of the given plugin seen by this session.
*
* The deployment system may have multiple versions of the same plugin available, but
* a single session should only ever activate one of them.
*/
protected isInstalledPlugin(identifier: PluginIdentifiers.VersionedId): boolean {
const versionAndId = PluginIdentifiers.idAndVersionFromVersionedId(identifier);
if (!versionAndId) {
return false;
}
const knownVersion = this.pluginVersions.get(versionAndId.id);
if (knownVersion !== undefined && knownVersion !== versionAndId.version) {
return false;
}
if (this.uninstalledPlugins.has(identifier)) {
return false;
}
if (knownVersion === undefined) {
this.pluginVersions.set(versionAndId.id, versionAndId.version);
}
return true;
}
getUninstalledPluginIds(): Promise<readonly PluginIdentifiers.VersionedId[]> {
return Promise.resolve(this.uninstallationManager.getUninstalledPluginIds());
}
getDisabledPluginIds(): Promise<readonly PluginIdentifiers.UnversionedId[]> {
return Promise.resolve(this.uninstallationManager.getDisabledPluginIds());
}
async getDeployedPlugins(pluginIds: PluginIdentifiers.VersionedId[]): Promise<DeployedPlugin[]> {
if (!pluginIds.length) {
return [];
}
const plugins: DeployedPlugin[] = [];
for (const versionedId of pluginIds) {
const plugin = this.deployerHandler.getDeployedPlugin(versionedId);
if (plugin) {
plugins.push(plugin);
}
}
return Promise.all(plugins.map(plugin => this.localizationService.localizePlugin(plugin)));
}
onMessage(pluginHostId: string, message: Uint8Array): Promise<void> {
this.hostedPlugin.onMessage(pluginHostId, message);
return Promise.resolve();
}
getExtPluginAPI(): Promise<ExtPluginApi[]> {
return Promise.resolve(this.extPluginAPIContributions.getContributions().map(p => p.provideApi()));
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2015-2018 Red Hat, Inc.
//
// 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 theia from '@theia/plugin';
import { BackendInitializationFn } from '../../../common/plugin-protocol';
import { PluginAPIFactory, Plugin, emptyPlugin } from '../../../common/plugin-api-rpc';
const pluginsApiImpl = new Map<string, typeof theia>();
const plugins = new Array<Plugin>();
let defaultApi: typeof theia;
let isLoadOverride = false;
let pluginApiFactory: PluginAPIFactory;
export const doInitialization: BackendInitializationFn = (apiFactory: PluginAPIFactory, plugin: Plugin) => {
const apiImpl = apiFactory(plugin);
pluginsApiImpl.set(plugin.model.id, apiImpl);
plugins.push(plugin);
pluginApiFactory = apiFactory;
if (!isLoadOverride) {
overrideInternalLoad();
isLoadOverride = true;
}
};
function overrideInternalLoad(): void {
const module = require('module');
// save original load method
const internalLoad = module._load;
// if we try to resolve theia module, return the filename entry to use cache.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
module._load = function (request: string, parent: any, isMain: {}): any {
if (request !== '@theia/plugin') {
return internalLoad.apply(this, arguments);
}
const plugin = findPlugin(parent.filename);
if (plugin) {
const apiImpl = pluginsApiImpl.get(plugin.model.id);
return apiImpl;
}
if (!defaultApi) {
console.warn(`Could not identify plugin for 'Theia' require call from ${parent.filename}`);
defaultApi = pluginApiFactory(emptyPlugin);
}
return defaultApi;
};
}
function findPlugin(filePath: string): Plugin | undefined {
return plugins.find(plugin => filePath.startsWith(plugin.pluginFolder));
}

View File

@@ -0,0 +1,32 @@
// *****************************************************************************
// Copyright (C) 2021 Red Hat, Inc.
//
// 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 * as path from 'path';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { PluginPackage } from '../../../common';
import { PluginUriFactory } from './plugin-uri-factory';
/**
* The default implementation of PluginUriFactory simply returns a File URI from the concatenated
* package path and relative path.
*/
@injectable()
export class FilePluginUriFactory implements PluginUriFactory {
createUri(pkg: PluginPackage, pkgRelativePath?: string): URI {
return FileUri.create(pkgRelativePath ? path.join(pkg.packagePath, pkgRelativePath) : pkg.packagePath);
}
}

View File

@@ -0,0 +1,57 @@
// *****************************************************************************
// Copyright (C) 2015-2018 Red Hat, Inc.
//
// 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 { PluginPackageGrammarsContribution, GrammarsContribution } from '../../../common';
import * as path from 'path';
import * as fs from '@theia/core/shared/fs-extra';
@injectable()
export class GrammarsReader {
async readGrammars(rawGrammars: PluginPackageGrammarsContribution[], pluginPath: string): Promise<GrammarsContribution[]> {
const result = new Array<GrammarsContribution>();
for (const rawGrammar of rawGrammars) {
const grammar = await this.readGrammar(rawGrammar, pluginPath);
if (grammar) {
result.push(grammar);
}
}
return result;
}
private async readGrammar(rawGrammar: PluginPackageGrammarsContribution, pluginPath: string): Promise<GrammarsContribution | undefined> {
// TODO: validate inputs
let grammar: string | object;
if (rawGrammar.path.endsWith('json')) {
grammar = await fs.readJSON(path.resolve(pluginPath, rawGrammar.path));
} else {
grammar = await fs.readFile(path.resolve(pluginPath, rawGrammar.path), 'utf8');
}
return {
language: rawGrammar.language,
scope: rawGrammar.scopeName,
format: rawGrammar.path.endsWith('json') ? 'json' : 'plist',
grammar: grammar,
grammarLocation: rawGrammar.path,
injectTo: rawGrammar.injectTo,
embeddedLanguages: rawGrammar.embeddedLanguages,
tokenTypes: rawGrammar.tokenTypes
};
}
}

View File

@@ -0,0 +1,33 @@
// *****************************************************************************
// Copyright (C) 2021 Red Hat, Inc.
//
// 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/lib/common/uri';
import { PluginPackage } from '../../../common';
export const PluginUriFactory = Symbol('PluginUriFactory');
/**
* Creates URIs for resources used in plugin contributions. Projects where plugin host is not located on the back-end
* machine and therefor resources cannot be loaded from the local file system in the back end can override the factory.
*/
export interface PluginUriFactory {
/**
* Returns a URI that allows a file to be loaded given a plugin package and a path relative to the plugin's package path
*
* @param pkg the package this the file is contained in
* @param pkgRelativePath the path of the file relative to the package path, e.g. 'resources/snippets.json'
*/
createUri(pkg: PluginPackage, pkgRelativePath?: string): URI;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,403 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// code copied and modified from https://github.com/microsoft/vscode/blob/1.47.3/src/vs/workbench/api/browser/mainThreadAuthentication.ts
import { interfaces } from '@theia/core/shared/inversify';
import { AuthenticationExt, AuthenticationMain, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { MessageService } from '@theia/core/lib/common/message-service';
import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
import {
AuthenticationProvider,
AuthenticationProviderSessionOptions,
AuthenticationService,
AuthenticationSession,
AuthenticationSessionAccountInformation,
readAllowedExtensions
} from '@theia/core/lib/browser/authentication-service';
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
import * as theia from '@theia/plugin';
import { QuickPickValue } from '@theia/core/lib/browser/quick-input/quick-input-service';
import { nls } from '@theia/core/lib/common/nls';
import { isObject } from '@theia/core';
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
export class AuthenticationMainImpl implements AuthenticationMain {
private readonly proxy: AuthenticationExt;
private readonly messageService: MessageService;
private readonly storageService: StorageService;
private readonly authenticationService: AuthenticationService;
private readonly quickPickService: QuickPickService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.AUTHENTICATION_EXT);
this.messageService = container.get(MessageService);
this.storageService = container.get(StorageService);
this.authenticationService = container.get(AuthenticationService);
this.quickPickService = container.get(QuickPickService);
this.authenticationService.onDidChangeSessions(e => {
this.proxy.$onDidChangeAuthenticationSessions({ id: e.providerId, label: e.label });
});
}
async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise<void> {
const provider = new AuthenticationProviderImpl(this.proxy, id, label, supportsMultipleAccounts, this.storageService, this.messageService);
this.authenticationService.registerAuthenticationProvider(id, provider);
}
async $unregisterAuthenticationProvider(id: string): Promise<void> {
this.authenticationService.unregisterAuthenticationProvider(id);
}
async $updateSessions(id: string, event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void> {
this.authenticationService.updateSessions(id, event);
}
$logout(providerId: string, sessionId: string): Promise<void> {
return this.authenticationService.logout(providerId, sessionId);
}
$getAccounts(providerId: string): Thenable<readonly theia.AuthenticationSessionAccountInformation[]> {
return this.authenticationService.getSessions(providerId).then(sessions => sessions.map(session => session.account));
}
async $getSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | theia.AuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string,
options: theia.AuthenticationGetSessionOptions): Promise<theia.AuthenticationSession | undefined> {
const sessions = await this.authenticationService.getSessions(providerId, scopeListOrRequest, options?.account);
// Error cases
if (options.forceNewSession && options.createIfNone) {
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, createIfNone');
}
if (options.forceNewSession && options.silent) {
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, silent');
}
if (options.createIfNone && options.silent) {
throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent');
}
const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId);
// Check if the sessions we have are valid
if (!options.forceNewSession && sessions.length) {
if (supportsMultipleAccounts) {
if (options.clearSessionPreference) {
await this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, undefined);
} else {
const existingSessionPreference = await this.storageService.getData(`authentication-session-${extensionName}-${providerId}`);
if (existingSessionPreference) {
const matchingSession = sessions.find(session => session.id === existingSessionPreference);
if (matchingSession && await this.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) {
return matchingSession;
}
}
}
} else if (await this.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {
return sessions[0];
}
}
// We may need to prompt because we don't have a valid session modal flows
if (options.createIfNone || options.forceNewSession) {
const providerName = this.authenticationService.getLabel(providerId);
let detail: string | undefined;
if (isAuthenticationGetSessionPresentationOptions(options.forceNewSession)) {
detail = options.forceNewSession.detail;
} else if (isAuthenticationGetSessionPresentationOptions(options.createIfNone)) {
detail = options.createIfNone.detail;
}
const shouldForceNewSession = !!options.forceNewSession;
const recreatingSession = shouldForceNewSession && !sessions.length;
const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail);
if (!isAllowed) {
throw new Error('User did not consent to login.');
}
const session = sessions?.length && !shouldForceNewSession && supportsMultipleAccounts
? await this.selectSession(providerId, providerName, extensionId, extensionName, sessions, scopeListOrRequest, !!options.clearSessionPreference)
: await this.authenticationService.login(providerId, scopeListOrRequest);
await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
return session;
}
// passive flows (silent or default)
const validSession = sessions.find(s => this.isAccessAllowed(providerId, s.account.label, extensionId));
if (!options.silent && !validSession) {
this.authenticationService.requestNewSession(providerId, scopeListOrRequest, extensionId, extensionName);
}
return validSession;
}
protected async selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string,
potentialSessions: Readonly<AuthenticationSession[]>, scopeListOrRequest: ReadonlyArray<string> | theia.AuthenticationWwwAuthenticateRequest,
clearSessionPreference: boolean): Promise<theia.AuthenticationSession> {
if (!potentialSessions.length) {
throw new Error('No potential sessions found');
}
return new Promise(async (resolve, reject) => {
const items: QuickPickValue<{ session?: AuthenticationSession, account?: AuthenticationSessionAccountInformation }>[] = potentialSessions.map(session => ({
label: session.account.label,
value: { session }
}));
items.push({
label: nls.localizeByDefault('Sign in to another account'),
value: {}
});
// VS Code has code here that pushes accounts that have no active sessions. However, since we do not store
// any accounts that don't have sessions, we dont' do this.
const selected = await this.quickPickService.show(items,
{
title: nls.localizeByDefault("The extension '{0}' wants to access a {1} account", extensionName, providerName),
ignoreFocusOut: true
});
if (selected) {
// if we ever have accounts without sessions, pass the account to the login call
const session = selected.value?.session ?? await this.authenticationService.login(providerId, scopeListOrRequest);
const accountName = session.account.label;
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
if (!allowList.find(allowed => allowed.id === extensionId)) {
allowList.push({ id: extensionId, name: extensionName });
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
}
this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, session.id);
resolve(session);
} else {
reject('User did not consent to account access');
}
});
}
protected async getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
const extensionData = allowList.find(extension => extension.id === extensionId);
if (extensionData) {
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
return true;
}
const choice = await this.messageService.info(`The extension '${extensionName}' wants to access the ${providerName} account '${accountName}'.`, 'Allow', 'Cancel');
const allow = choice === 'Allow';
if (allow) {
await addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
allowList.push({ id: extensionId, name: extensionName });
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
}
return allow;
}
protected async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise<boolean> {
const msg = document.createElement('span');
msg.textContent = recreatingSession
? nls.localizeByDefault("The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName)
: nls.localizeByDefault("The extension '{0}' wants to sign in using {1}.", extensionName, providerName);
if (detail) {
const detailElement = document.createElement('p');
detailElement.textContent = detail;
msg.appendChild(detailElement);
}
return !!await new ConfirmDialog({
title: nls.localize('theia/plugin-ext/authentication-main/loginTitle', 'Login'),
msg,
ok: nls.localizeByDefault('Allow'),
cancel: Dialog.CANCEL
}).open();
}
protected async isAccessAllowed(providerId: string, accountName: string, extensionId: string): Promise<boolean> {
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
return !!allowList.find(allowed => allowed.id === extensionId);
}
protected async setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise<void> {
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
if (!allowList.find(allowed => allowed.id === extensionId)) {
allowList.push({ id: extensionId, name: extensionName });
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
}
this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, sessionId);
}
$onDidChangeSessions(providerId: string, event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): void {
this.authenticationService.updateSessions(providerId, event);
}
}
function isAuthenticationGetSessionPresentationOptions(arg: unknown): arg is theia.AuthenticationGetSessionPresentationOptions {
return isObject<theia.AuthenticationGetSessionPresentationOptions>(arg) && typeof arg.detail === 'string';
}
async function addAccountUsage(storageService: StorageService, providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<void> {
const accountKey = `authentication-${providerId}-${accountName}-usages`;
const usages = await readAccountUsages(storageService, providerId, accountName);
const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId);
if (existingUsageIndex > -1) {
usages.splice(existingUsageIndex, 1, {
extensionId,
extensionName,
lastUsed: Date.now()
});
} else {
usages.push({
extensionId,
extensionName,
lastUsed: Date.now()
});
}
await storageService.setData(accountKey, JSON.stringify(usages));
}
interface AccountUsage {
extensionId: string;
extensionName: string;
lastUsed: number;
}
export class AuthenticationProviderImpl implements AuthenticationProvider {
/** map from account name to session ids */
private accounts = new Map<string, string[]>();
/** map from session id to account name */
private sessions = new Map<string, string>();
readonly onDidChangeSessions: theia.Event<theia.AuthenticationProviderAuthenticationSessionsChangeEvent>;
constructor(
private readonly proxy: AuthenticationExt,
public readonly id: string,
public readonly label: string,
public readonly supportsMultipleAccounts: boolean,
private readonly storageService: StorageService,
private readonly messageService: MessageService
) { }
public hasSessions(): boolean {
return !!this.sessions.size;
}
private registerSession(session: theia.AuthenticationSession): void {
this.sessions.set(session.id, session.account.label);
const existingSessionsForAccount = this.accounts.get(session.account.label);
if (existingSessionsForAccount) {
this.accounts.set(session.account.label, existingSessionsForAccount.concat(session.id));
return;
} else {
this.accounts.set(session.account.label, [session.id]);
}
}
async signOut(accountName: string): Promise<void> {
const accountUsages = await readAccountUsages(this.storageService, this.id, accountName);
const sessionsForAccount = this.accounts.get(accountName);
const result = await this.messageService.info(accountUsages.length
? nls.localizeByDefault("The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName,
accountUsages.map(usage => usage.extensionName).join(', '))
: nls.localizeByDefault("Sign out of '{0}'?", accountName),
nls.localizeByDefault('Sign Out'),
Dialog.CANCEL);
if (result && result === nls.localizeByDefault('Sign Out') && sessionsForAccount) {
sessionsForAccount.forEach(sessionId => this.removeSession(sessionId));
removeAccountUsage(this.storageService, this.id, accountName);
}
}
async getSessions(scopes?: string[], account?: AuthenticationSessionAccountInformation): Promise<ReadonlyArray<theia.AuthenticationSession>> {
return this.proxy.$getSessions(this.id, scopes, { account: account });
}
async updateSessionItems(event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void> {
const { added, removed } = event;
const session = await this.proxy.$getSessions(this.id, undefined, {});
const addedSessions = added ? session.filter(s => added.some(addedSession => addedSession.id === s.id)) : [];
removed?.forEach(removedSession => {
const sessionId = removedSession.id;
if (sessionId) {
const accountName = this.sessions.get(sessionId);
if (accountName) {
this.sessions.delete(sessionId);
const sessionsForAccount = this.accounts.get(accountName) || [];
const sessionIndex = sessionsForAccount.indexOf(sessionId);
sessionsForAccount.splice(sessionIndex);
if (!sessionsForAccount.length) {
this.accounts.delete(accountName);
}
}
}
});
addedSessions.forEach(s => this.registerSession(s));
}
async login(scopes: string[], options: AuthenticationProviderSessionOptions): Promise<theia.AuthenticationSession> {
return this.createSession(scopes, options);
}
async logout(sessionId: string): Promise<void> {
return this.removeSession(sessionId);
}
createSession(scopes: string[], options: AuthenticationProviderSessionOptions): Thenable<theia.AuthenticationSession> {
return this.proxy.$createSession(this.id, scopes, options);
}
removeSession(sessionId: string): Thenable<void> {
return this.proxy.$removeSession(this.id, sessionId)
.then(() => {
this.messageService.info(nls.localize('theia/plugin-ext/authentication-main/signedOut', 'Successfully signed out.'));
});
}
}
async function readAccountUsages(storageService: StorageService, providerId: string, accountName: string): Promise<AccountUsage[]> {
const accountKey = `authentication-${providerId}-${accountName}-usages`;
const storedUsages: string | undefined = await storageService.getData(accountKey);
let usages: AccountUsage[] = [];
if (storedUsages) {
try {
usages = JSON.parse(storedUsages);
} catch (e) {
console.log(e);
}
}
return usages;
}
function removeAccountUsage(storageService: StorageService, providerId: string, accountName: string): void {
const accountKey = `authentication-${providerId}-${accountName}-usages`;
storageService.setData(accountKey, undefined);
}

View File

@@ -0,0 +1,38 @@
// *****************************************************************************
// Copyright (C) 2019 RedHat 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 { ClipboardMain } from '../../common';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
export class ClipboardMainImpl implements ClipboardMain {
protected readonly clipboardService: ClipboardService;
constructor(container: interfaces.Container) {
this.clipboardService = container.get(ClipboardService);
}
async $readText(): Promise<string> {
const result = await this.clipboardService.readText();
return result;
}
async $writeText(value: string): Promise<void> {
await this.clipboardService.writeText(value);
}
}

View File

@@ -0,0 +1,130 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { CommandRegistry } from '@theia/core/lib/common/command';
import * as theia from '@theia/plugin';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { CommandRegistryMain, CommandRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { PluginContributionHandler } from './plugin-contribution-handler';
import { ArgumentProcessor } from '../../common/commands';
import { ContributionProvider } from '@theia/core';
export const ArgumentProcessorContribution = Symbol('ArgumentProcessorContribution');
export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable {
private readonly proxy: CommandRegistryExt;
private readonly commands = new Map<string, Disposable>();
private readonly handlers = new Map<string, Disposable>();
private readonly delegate: CommandRegistry;
private readonly keyBinding: KeybindingRegistry;
private readonly contributions: PluginContributionHandler;
private readonly argumentProcessors: ArgumentProcessor[] = [];
protected readonly toDispose = new DisposableCollection();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT);
this.delegate = container.get(CommandRegistry);
this.keyBinding = container.get(KeybindingRegistry);
this.contributions = container.get(PluginContributionHandler);
container.getNamed<ContributionProvider<ArgumentProcessor>>(ContributionProvider, ArgumentProcessorContribution).getContributions().forEach(processor => {
this.registerArgumentProcessor(processor);
});
}
dispose(): void {
this.toDispose.dispose();
}
registerArgumentProcessor(processor: ArgumentProcessor): Disposable {
this.argumentProcessors.push(processor);
return Disposable.create(() => {
const index = this.argumentProcessors.lastIndexOf(processor);
if (index >= 0) {
this.argumentProcessors.splice(index, 1);
}
});
}
$registerCommand(command: theia.CommandDescription): void {
const id = command.id;
this.commands.set(id, this.contributions.registerCommand(command));
this.toDispose.push(Disposable.create(() => this.$unregisterCommand(id)));
}
$unregisterCommand(id: string): void {
const command = this.commands.get(id);
if (command) {
command.dispose();
this.commands.delete(id);
}
}
$registerHandler(id: string): void {
this.handlers.set(id, this.contributions.registerCommandHandler(id, (...args) =>
this.proxy.$executeCommand(id, ...args.map(arg => this.argumentProcessors.reduce((currentValue, processor) => processor.processArgument(currentValue), arg)))
));
this.toDispose.push(Disposable.create(() => this.$unregisterHandler(id)));
}
$unregisterHandler(id: string): void {
const handler = this.handlers.get(id);
if (handler) {
handler.dispose();
this.handlers.delete(id);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async $executeCommand<T>(id: string, ...args: any[]): Promise<T | undefined> {
if (!this.delegate.getCommand(id)) {
throw new Error(`Command with id '${id}' is not registered.`);
}
try {
return await this.delegate.executeCommand<T>(id, ...args);
} catch (e) {
// Command handler may be not active at the moment so the error must be caught. See https://github.com/eclipse-theia/theia/pull/6687#discussion_r354810079
if ('code' in e && e['code'] === 'NO_ACTIVE_HANDLER') {
return;
} else {
throw e;
}
}
}
$getKeyBinding(commandId: string): PromiseLike<theia.CommandKeyBinding[] | undefined> {
try {
const keyBindings = this.keyBinding.getKeybindingsForCommand(commandId);
if (keyBindings) {
// transform inner type to CommandKeyBinding
return Promise.resolve(keyBindings.map(keyBinding => ({ id: commandId, value: keyBinding.keybinding })));
} else {
return Promise.resolve(undefined);
}
} catch (e) {
return Promise.reject(e);
}
}
$getCommands(): PromiseLike<string[]> {
return Promise.resolve(this.delegate.commandIds);
}
}

View File

@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Command, CommandService } from '@theia/core/lib/common/command';
import { AbstractDialog } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class OpenUriCommandHandler {
public static readonly COMMAND_METADATA: Command = {
id: 'theia.open'
};
private openNewTabDialog: OpenNewTabDialog;
constructor(
@inject(WindowService)
protected readonly windowService: WindowService,
@inject(CommandService)
protected readonly commandService: CommandService
) {
this.openNewTabDialog = new OpenNewTabDialog(windowService);
}
public execute(resource: URI | string | undefined): void {
if (!resource) {
return;
}
const uriString = resource.toString();
if (uriString.startsWith('http://') || uriString.startsWith('https://')) {
this.openWebUri(uriString);
} else {
this.commandService.executeCommand('editor.action.openLink', uriString);
}
}
private openWebUri(uri: string): void {
try {
this.windowService.openNewWindow(uri);
} catch (err) {
// browser has blocked opening of a new tab
this.openNewTabDialog.showOpenNewTabDialog(uri);
}
}
}
class OpenNewTabDialog extends AbstractDialog<string> {
protected readonly windowService: WindowService;
protected readonly openButton: HTMLButtonElement;
protected readonly messageNode: HTMLDivElement;
protected readonly linkNode: HTMLAnchorElement;
value: string;
constructor(windowService: WindowService) {
super({
title: nls.localize('theia/plugin/blockNewTab', 'Your browser prevented opening of a new tab')
});
this.windowService = windowService;
this.linkNode = document.createElement('a');
this.linkNode.target = '_blank';
this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
this.contentNode.appendChild(this.linkNode);
const messageNode = document.createElement('div');
messageNode.innerText = 'You are going to open: ';
messageNode.appendChild(this.linkNode);
this.contentNode.appendChild(messageNode);
this.appendCloseButton();
this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
}
showOpenNewTabDialog(uri: string): void {
this.value = uri;
this.linkNode.innerHTML = DOMPurify.sanitize(uri);
this.linkNode.href = uri;
this.openButton.onclick = () => {
this.windowService.openNewWindow(uri);
};
// show dialog window to user
this.open();
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 { Disposable } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts
export class CommentGlyphWidget implements Disposable {
private lineNumber!: number;
private editor: monaco.editor.ICodeEditor;
private commentsDecorations: string[] = [];
readonly commentsOptions: monaco.editor.IModelDecorationOptions;
constructor(editor: monaco.editor.ICodeEditor) {
this.commentsOptions = {
isWholeLine: true,
linesDecorationsClassName: 'comment-range-glyph comment-thread'
};
this.editor = editor;
}
getPosition(): number {
const model = this.editor.getModel();
const range = model && this.commentsDecorations && this.commentsDecorations.length
? model.getDecorationRange(this.commentsDecorations[0])
: null;
return range ? range.startLineNumber : this.lineNumber;
}
setLineNumber(lineNumber: number): void {
this.lineNumber = lineNumber;
const commentsDecorations = [{
range: {
startLineNumber: lineNumber, startColumn: 1,
endLineNumber: lineNumber, endColumn: 1
},
options: this.commentsOptions
}];
this.commentsDecorations = this.editor.deltaDecorations(this.commentsDecorations, commentsDecorations);
}
dispose(): void {
if (this.commentsDecorations) {
this.editor.deltaDecorations(this.commentsDecorations, []);
}
}
}

View File

@@ -0,0 +1,791 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
import {
Comment,
CommentMode,
CommentThread,
CommentThreadState,
CommentThreadCollapsibleState
} from '../../../common/plugin-api-rpc-model';
import { CommentGlyphWidget } from './comment-glyph-widget';
import { BaseWidget, DISABLED_CLASS } from '@theia/core/lib/browser';
import * as React from '@theia/core/shared/react';
import { MouseTargetType } from '@theia/editor/lib/browser';
import { CommentsService } from './comments-service';
import {
CommandMenu,
CommandRegistry,
CompoundMenuNode,
isObject,
DisposableCollection,
MenuModelRegistry,
MenuPath
} from '@theia/core/lib/common';
import { CommentsContext } from './comments-context';
import { RefObject } from '@theia/core/shared/react';
import * as monaco from '@theia/monaco-editor-core';
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
import { CommentAuthorInformation } from '@theia/plugin';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts
export const COMMENT_THREAD_CONTEXT: MenuPath = ['comment_thread-context-menu'];
export const COMMENT_CONTEXT: MenuPath = ['comment-context-menu'];
export const COMMENT_TITLE: MenuPath = ['comment-title-menu'];
export class CommentThreadWidget extends BaseWidget {
protected readonly zoneWidget: MonacoEditorZoneWidget;
protected readonly containerNodeRoot: Root;
protected readonly commentGlyphWidget: CommentGlyphWidget;
protected readonly commentFormRef: RefObject<CommentForm> = React.createRef<CommentForm>();
protected isExpanded?: boolean;
constructor(
editor: monaco.editor.IStandaloneCodeEditor,
private _owner: string,
private _commentThread: CommentThread,
private commentService: CommentsService,
protected readonly menus: MenuModelRegistry,
protected readonly commentsContext: CommentsContext,
protected readonly contextKeyService: ContextKeyService,
protected readonly commands: CommandRegistry
) {
super();
this.toDispose.push(this.zoneWidget = new MonacoEditorZoneWidget(editor));
this.containerNodeRoot = createRoot(this.zoneWidget.containerNode);
this.toDispose.push(this.commentGlyphWidget = new CommentGlyphWidget(editor));
this.toDispose.push(this._commentThread.onDidChangeCollapsibleState(state => {
if (state === CommentThreadCollapsibleState.Expanded && !this.isExpanded) {
const lineNumber = this._commentThread.range?.startLineNumber ?? 0;
this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2 });
return;
}
if (state === CommentThreadCollapsibleState.Collapsed && this.isExpanded) {
this.hide();
return;
}
}));
this.commentsContext.commentIsEmpty.set(true);
this.toDispose.push(this.zoneWidget.editor.onMouseDown(e => this.onEditorMouseDown(e)));
this.toDispose.push(this._commentThread.onDidChangeCanReply(_canReply => {
const commentForm = this.commentFormRef.current;
if (commentForm) {
commentForm.update();
}
}));
this.toDispose.push(this._commentThread.onDidChangeState(_state => {
this.update();
}));
const contextMenu = this.menus.getMenu(COMMENT_THREAD_CONTEXT);
contextMenu?.children.forEach(node => {
if (node.onDidChange) {
this.toDispose.push(node.onDidChange(() => {
const commentForm = this.commentFormRef.current;
if (commentForm) {
commentForm.update();
}
}));
}
});
}
public getGlyphPosition(): number {
return this.commentGlyphWidget.getPosition();
}
public collapse(): void {
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed;
if (this._commentThread.comments && this._commentThread.comments.length === 0) {
this.deleteCommentThread();
}
this.hide();
}
private deleteCommentThread(): void {
this.dispose();
this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId);
}
override dispose(): void {
super.dispose();
if (this.commentGlyphWidget) {
this.commentGlyphWidget.dispose();
}
}
toggleExpand(lineNumber: number): void {
if (this.isExpanded) {
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed;
this.hide();
if (!this._commentThread.comments || !this._commentThread.comments.length) {
this.deleteCommentThread();
}
} else {
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded;
this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2 });
}
}
override hide(): void {
this.zoneWidget.hide();
this.isExpanded = false;
super.hide();
}
display(options: MonacoEditorZoneWidget.Options): void {
this.isExpanded = true;
if (this._commentThread.collapsibleState && this._commentThread.collapsibleState !== CommentThreadCollapsibleState.Expanded) {
return;
}
this.commentGlyphWidget.setLineNumber(options.afterLineNumber);
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded;
this.zoneWidget.show(options);
this.update();
}
private onEditorMouseDown(e: monaco.editor.IEditorMouseEvent): void {
const range = e.target.range;
if (!range) {
return;
}
if (!e.event.leftButton) {
return;
}
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
return;
}
const data = e.target.detail;
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
// don't collide with folding and git decorations
if (gutterOffsetX > 14) {
return;
}
const mouseDownInfo = { lineNumber: range.startLineNumber };
const { lineNumber } = mouseDownInfo;
if (!range || range.startLineNumber !== lineNumber) {
return;
}
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
return;
}
if (!e.target.element) {
return;
}
if (this.commentGlyphWidget && this.commentGlyphWidget.getPosition() !== lineNumber) {
return;
}
if (e.target.element.className.indexOf('comment-thread') >= 0) {
this.toggleExpand(lineNumber);
return;
}
if (this._commentThread.collapsibleState === CommentThreadCollapsibleState.Collapsed) {
this.display({ afterLineNumber: mouseDownInfo.lineNumber, heightInLines: 2 });
} else {
this.hide();
}
}
public get owner(): string {
return this._owner;
}
public get commentThread(): CommentThread {
return this._commentThread;
}
private getThreadLabel(): string {
let label: string | undefined;
label = this._commentThread.label;
if (label === undefined) {
if (this._commentThread.comments && this._commentThread.comments.length) {
const onlyUnique = (value: Comment, index: number, self: Comment[]) => self.indexOf(value) === index;
const participantsList = this._commentThread.comments.filter(onlyUnique).map(comment => `@${comment.userName}`).join(', ');
const resolutionState = this._commentThread.state === CommentThreadState.Resolved ? '(Resolved)' : '(Unresolved)';
label = `Participants: ${participantsList} ${resolutionState}`;
} else {
label = 'Start discussion';
}
}
return label;
}
override update(): void {
if (!this.isExpanded) {
return;
}
this.render();
const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2);
const lineHeight = this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight);
const arrowHeight = Math.round(lineHeight / 3);
const frameThickness = Math.round(lineHeight / 9) * 2;
const body = this.zoneWidget.containerNode.getElementsByClassName('body')[0];
const computedLinesNumber = Math.ceil((headHeight + (body?.clientHeight ?? 0) + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */)
/ lineHeight);
this.zoneWidget.show({ afterLineNumber: this._commentThread.range?.startLineNumber ?? 0, heightInLines: computedLinesNumber });
}
protected render(): void {
const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2);
this.containerNodeRoot.render(<div className={'review-widget'}>
<div className={'head'} style={{ height: headHeight, lineHeight: `${headHeight}px` }}>
<div className={'review-title'}>
<span className={'filename'}>{this.getThreadLabel()}</span>
</div>
<div className={'review-actions'}>
<div className={'monaco-action-bar animated'}>
<ul className={'actions-container'} role={'toolbar'}>
<li className={'action-item'} role={'presentation'}>
<a className={'action-label codicon expand-review-action codicon-chevron-up'}
role={'button'}
tabIndex={0}
title={'Collapse'}
onClick={() => this.collapse()}
/>
</li>
</ul>
</div>
</div>
</div>
<div className={'body'}>
<div className={'comments-container'} role={'presentation'} tabIndex={0}>
{this._commentThread.comments?.map((comment, index) => <ReviewComment
key={index}
contextKeyService={this.contextKeyService}
commentsContext={this.commentsContext}
menus={this.menus}
comment={comment}
commentForm={this.commentFormRef}
commands={this.commands}
commentThread={this._commentThread}
/>)}
</div>
<CommentForm contextKeyService={this.contextKeyService}
commentsContext={this.commentsContext}
commands={this.commands}
commentThread={this._commentThread}
menus={this.menus}
widget={this}
ref={this.commentFormRef}
/>
</div>
</div>);
}
}
namespace CommentForm {
export interface Props {
menus: MenuModelRegistry,
commentThread: CommentThread;
commands: CommandRegistry;
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
widget: CommentThreadWidget;
}
export interface State {
expanded: boolean
}
}
export class CommentForm<P extends CommentForm.Props = CommentForm.Props> extends React.Component<P, CommentForm.State> {
private inputRef: RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>();
private inputValue: string = '';
private readonly getInput = () => this.inputValue;
private toDisposeOnUnmount = new DisposableCollection();
private readonly clearInput: () => void = () => {
const input = this.inputRef.current;
if (input) {
this.inputValue = '';
input.value = this.inputValue;
this.props.commentsContext.commentIsEmpty.set(true);
}
};
update(): void {
this.setState(this.state);
}
protected expand = () => {
this.setState({ expanded: true });
// Wait for the widget to be rendered.
setTimeout(() => {
// Update the widget's height.
this.props.widget.update();
this.inputRef.current?.focus();
}, 100);
};
protected collapse = () => {
this.setState({ expanded: false });
// Wait for the widget to be rendered.
setTimeout(() => {
// Update the widget's height.
this.props.widget.update();
}, 100);
};
override componentDidMount(): void {
// Wait for the widget to be rendered.
setTimeout(() => {
this.inputRef.current?.focus();
}, 100);
}
override componentWillUnmount(): void {
this.toDisposeOnUnmount.dispose();
}
private readonly onInput: (event: React.FormEvent) => void = (event: React.FormEvent) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const value = (event.target as any).value;
if (this.inputValue.length === 0 || value.length === 0) {
this.props.commentsContext.commentIsEmpty.set(value.length === 0);
}
this.inputValue = value;
};
constructor(props: P) {
super(props);
this.state = {
expanded: false
};
const setState = this.setState.bind(this);
this.setState = newState => {
setState(newState);
};
}
/**
* Renders the comment form with textarea, actions, and reply button.
*
* @returns The rendered comment form
*/
protected renderCommentForm(): React.ReactNode {
const { commentThread, commentsContext, contextKeyService, menus } = this.props;
const hasExistingComments = commentThread.comments && commentThread.comments.length > 0;
// Determine when to show the expanded form:
// - When state.expanded is true (user clicked the reply button)
// - When there are no existing comments (new thread)
const shouldShowExpanded = this.state.expanded || (commentThread.comments && commentThread.comments.length === 0);
return commentThread.canReply ? (
<div className={`comment-form${shouldShowExpanded ? ' expand' : ''}`}>
<div className={'theia-comments-input-message-container'}>
<textarea className={'theia-comments-input-message theia-input'}
spellCheck={false}
placeholder={hasExistingComments ? 'Reply...' : 'Type a new comment'}
onInput={this.onInput}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onBlur={(event: any) => {
if (event.target.value.length > 0) {
return;
}
if (event.relatedTarget && event.relatedTarget.className === 'comments-button comments-text-button theia-button') {
this.state = { expanded: false };
return;
}
this.collapse();
}}
ref={this.inputRef}>
</textarea>
</div>
<CommentActions menu={menus.getMenu(COMMENT_THREAD_CONTEXT)}
menuPath={[]}
contextKeyService={contextKeyService}
commentsContext={commentsContext}
commentThread={commentThread}
getInput={this.getInput}
clearInput={this.clearInput}
/>
<button className={'review-thread-reply-button'} title={'Reply...'} onClick={this.expand}>Reply...</button>
</div>
) : null;
}
/**
* Renders the author information section.
*
* @param authorInfo The author information to display
* @returns The rendered author information section
*/
protected renderAuthorInfo(authorInfo: CommentAuthorInformation): React.ReactNode {
return (
<div className={'avatar-container'}>
{authorInfo.iconPath && (
<img className={'avatar'} src={authorInfo.iconPath.toString()} />
)}
</div>
);
}
override render(): React.ReactNode {
const { commentThread } = this.props;
if (!commentThread.canReply) {
return null;
}
// If there's author info, wrap in a container with author info on the left
if (isCommentAuthorInformation(commentThread.canReply)) {
return (
<div className={'review-comment'}>
{this.renderAuthorInfo(commentThread.canReply)}
<div className={'review-comment-contents'}>
<div className={'comment-title monaco-mouse-cursor-text'}>
<strong className={'author'}>{commentThread.canReply.name}</strong>
</div>
{this.renderCommentForm()}
</div>
</div>
);
}
// Otherwise, just return the comment form
return (
<div className={'review-comment'}>
<div className={'review-comment-contents'}>
{this.renderCommentForm()}
</div>
</div>);
}
}
function isCommentAuthorInformation(item: unknown): item is CommentAuthorInformation {
return isObject(item) && 'name' in item;
}
namespace ReviewComment {
export interface Props {
menus: MenuModelRegistry,
comment: Comment;
commentThread: CommentThread;
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
commands: CommandRegistry;
commentForm: RefObject<CommentForm>;
}
export interface State {
hover: boolean
}
}
export class ReviewComment<P extends ReviewComment.Props = ReviewComment.Props> extends React.Component<P, ReviewComment.State> {
constructor(props: P) {
super(props);
this.state = {
hover: false
};
const setState = this.setState.bind(this);
this.setState = newState => {
setState(newState);
};
}
protected detectHover = (element: HTMLElement | null) => {
if (element) {
window.requestAnimationFrame(() => {
const hover = element.matches(':hover');
this.setState({ hover });
});
}
};
protected showHover = () => this.setState({ hover: true });
protected hideHover = () => this.setState({ hover: false });
override render(): React.ReactNode {
const { comment, commentForm, contextKeyService, commentsContext, menus, commands, commentThread } = this.props;
const commentUniqueId = comment.uniqueIdInThread;
const { hover } = this.state;
commentsContext.comment.set(comment.contextValue);
return <div className={'review-comment'}
tabIndex={-1}
aria-label={`${comment.userName}, ${comment.body.value}`}
ref={this.detectHover}
onMouseEnter={this.showHover}
onMouseLeave={this.hideHover}>
<div className={'avatar-container'}>
<img className={'avatar'} src={comment.userIconPath} />
</div>
<div className={'review-comment-contents'}>
<div className={'comment-title monaco-mouse-cursor-text'}>
<strong className={'author'}>{comment.userName}</strong>
<small className={'timestamp'}>{this.localeDate(comment.timestamp)}</small>
<span className={'isPending'}>{comment.label}</span>
<div className={'theia-comments-inline-actions-container'}>
<div className={'theia-comments-inline-actions'} role={'toolbar'}>
{hover && menus.getMenuNode(COMMENT_TITLE) && menus.getMenu(COMMENT_TITLE)?.children.map((node, index): React.ReactNode => CommandMenu.is(node) &&
<CommentsInlineAction key={index} {...{
node, nodePath: [...COMMENT_TITLE, node.id], commands, commentThread, commentUniqueId,
contextKeyService, commentsContext
}} />)}
</div>
</div>
</div>
<CommentBody value={comment.body.value}
isVisible={comment.mode === undefined || comment.mode === CommentMode.Preview} />
<CommentEditContainer contextKeyService={contextKeyService}
commentsContext={commentsContext}
menus={menus}
comment={comment}
commentThread={commentThread}
commentForm={commentForm}
commands={commands} />
</div>
</div>;
}
protected localeDate(timestamp: string | undefined): string {
if (timestamp === undefined) {
return '';
}
const date = new Date(timestamp);
if (!isNaN(date.getTime())) {
return date.toLocaleString();
}
return '';
}
}
namespace CommentBody {
export interface Props {
value: string
isVisible: boolean
}
}
export class CommentBody extends React.Component<CommentBody.Props> {
override render(): React.ReactNode {
const { value, isVisible } = this.props;
if (!isVisible) {
return false;
}
return <div className={'comment-body monaco-mouse-cursor-text'}>
<div>
<p>{value}</p>
</div>
</div>;
}
}
namespace CommentEditContainer {
export interface Props {
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
menus: MenuModelRegistry,
comment: Comment;
commentThread: CommentThread;
commentForm: RefObject<CommentForm>;
commands: CommandRegistry;
}
}
export class CommentEditContainer extends React.Component<CommentEditContainer.Props> {
private readonly inputRef: RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>();
private dirtyCommentMode: CommentMode | undefined;
private dirtyCommentFormState: boolean | undefined;
override componentDidUpdate(prevProps: Readonly<CommentEditContainer.Props>, prevState: Readonly<{}>): void {
const commentFormState = this.props.commentForm.current?.state;
const mode = this.props.comment.mode;
if (this.dirtyCommentMode !== mode || (this.dirtyCommentFormState !== commentFormState?.expanded && !commentFormState?.expanded)) {
const currentInput = this.inputRef.current;
if (currentInput) {
// Wait for the widget to be rendered.
setTimeout(() => {
currentInput.focus();
currentInput.setSelectionRange(currentInput.value.length, currentInput.value.length);
}, 50);
}
}
this.dirtyCommentMode = mode;
this.dirtyCommentFormState = commentFormState?.expanded;
}
override render(): React.ReactNode {
const { menus, comment, commands, commentThread, contextKeyService, commentsContext } = this.props;
if (!(comment.mode === CommentMode.Editing)) {
return false;
}
return <div className={'edit-container'}>
<div className={'edit-textarea'}>
<div className={'theia-comments-input-message-container'}>
<textarea className={'theia-comments-input-message theia-input'}
spellCheck={false}
defaultValue={comment.body.value}
ref={this.inputRef} />
</div>
</div>
<div className={'form-actions'}>
{menus.getMenu(COMMENT_CONTEXT)?.children.map((node, index): React.ReactNode => {
const onClick = () => {
commands.executeCommand(node.id, {
commentControlHandle: commentThread.controllerHandle,
commentThreadHandle: commentThread.commentThreadHandle,
commentUniqueId: comment.uniqueIdInThread,
text: this.inputRef.current ? this.inputRef.current.value : ''
});
};
return CommandMenu.is(node) &&
<CommentAction key={index} {...{
node, nodePath: [...COMMENT_CONTEXT, node.id], comment,
commands, onClick, contextKeyService, commentsContext, commentThread
}} />;
}
)}
</div>
</div>;
}
}
namespace CommentsInlineAction {
export interface Props {
nodePath: MenuPath,
node: CommandMenu;
commentThread: CommentThread;
commentUniqueId: number;
commands: CommandRegistry;
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
}
}
export class CommentsInlineAction extends React.Component<CommentsInlineAction.Props> {
override render(): React.ReactNode {
const { node, nodePath, commands, contextKeyService, commentThread, commentUniqueId } = this.props;
if (node.isVisible(nodePath, contextKeyService, undefined, {
thread: commentThread,
commentUniqueId
})) {
return false;
}
return <div className='theia-comments-inline-action'>
<a className={node.icon}
title={node.label}
onClick={() => {
commands.executeCommand(node.id, {
thread: commentThread,
commentUniqueId: commentUniqueId
});
}} />
</div>;
}
}
namespace CommentActions {
export interface Props {
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
menuPath: MenuPath,
menu: CompoundMenuNode | undefined;
commentThread: CommentThread;
getInput: () => string;
clearInput: () => void;
}
}
export class CommentActions extends React.Component<CommentActions.Props> {
override render(): React.ReactNode {
const { contextKeyService, commentsContext, menuPath, menu, commentThread, getInput, clearInput } = this.props;
return <div className={'form-actions'}>
{menu?.children.map((node, index) => CommandMenu.is(node) &&
<CommentAction key={index}
nodePath={menuPath}
node={node}
onClick={() => {
node.run(
[...menuPath, menu.id], {
thread: commentThread,
text: getInput()
});
clearInput();
}}
commentThread={commentThread}
contextKeyService={contextKeyService}
commentsContext={commentsContext}
/>)}
</div>;
}
}
namespace CommentAction {
export interface Props {
commentThread: CommentThread;
contextKeyService: ContextKeyService;
commentsContext: CommentsContext;
nodePath: MenuPath,
node: CommandMenu;
onClick: () => void;
}
}
export class CommentAction extends React.Component<CommentAction.Props> {
override render(): React.ReactNode {
const classNames = ['comments-button', 'comments-text-button', 'theia-button'];
const { node, nodePath, contextKeyService, onClick, commentThread } = this.props;
if (!node.isVisible(nodePath, contextKeyService, undefined, {
thread: commentThread
})) {
return false;
}
const isEnabled = node.isEnabled(nodePath, {
thread: commentThread
});
if (!isEnabled) {
classNames.push(DISABLED_CLASS);
}
return <button
className={classNames.join(' ')}
tabIndex={0}
role={'button'}
onClick={() => {
if (isEnabled) {
onClick();
}
}}>{node.label}
</button>;
}
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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, postConstruct } from '@theia/core/shared/inversify';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class CommentsContext {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected readonly contextKeys: Set<string> = new Set();
protected _commentIsEmpty: ContextKey<boolean>;
protected _commentController: ContextKey<string | undefined>;
protected _comment: ContextKey<string | undefined>;
get commentController(): ContextKey<string | undefined> {
return this._commentController;
}
get comment(): ContextKey<string | undefined> {
return this._comment;
}
get commentIsEmpty(): ContextKey<boolean> {
return this._commentIsEmpty;
}
@postConstruct()
protected init(): void {
this.contextKeys.add('commentIsEmpty');
this._commentController = this.contextKeyService.createKey<string | undefined>('commentController', undefined);
this._comment = this.contextKeyService.createKey<string | undefined>('comment', undefined);
this._commentIsEmpty = this.contextKeyService.createKey<boolean>('commentIsEmpty', true);
}
}

View File

@@ -0,0 +1,268 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 * as monaco from '@theia/monaco-editor-core';
import { CommentingRangeDecorator } from './comments-decorator';
import { EditorManager, EditorMouseEvent, EditorWidget } from '@theia/editor/lib/browser';
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
import { CommentThreadWidget } from './comment-thread-widget';
import { CommentsService, CommentInfoMain } from './comments-service';
import { CommentThread } from '../../../common/plugin-api-rpc-model';
import { CommandRegistry, DisposableCollection, MenuModelRegistry } from '@theia/core/lib/common';
import { URI } from '@theia/core/shared/vscode-uri';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { Uri } from '@theia/plugin';
import { CommentsContext } from './comments-context';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/comments.contribution.ts
@injectable()
export class CommentsContribution {
private addInProgress!: boolean;
private commentWidgets: CommentThreadWidget[];
private commentInfos: CommentInfoMain[];
private emptyThreadsToAddQueue: [number, EditorMouseEvent | undefined][] = [];
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
@inject(CommentsContext) protected readonly commentsContext: CommentsContext;
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
constructor(@inject(CommentingRangeDecorator) protected readonly rangeDecorator: CommentingRangeDecorator,
@inject(CommentsService) protected readonly commentService: CommentsService,
@inject(EditorManager) protected readonly editorManager: EditorManager) {
this.commentWidgets = [];
this.commentInfos = [];
this.commentService.onDidSetResourceCommentInfos(e => {
const editor = this.getCurrentEditor();
const editorURI = editor && editor.editor instanceof MonacoDiffEditor && editor.editor.diffEditor.getModifiedEditor().getModel();
if (editorURI && editorURI.toString() === e.resource.toString()) {
this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));
}
});
this.editorManager.onCreated(async widget => {
const disposables = new DisposableCollection();
const editor = widget.editor;
if (editor instanceof MonacoDiffEditor) {
const originalEditorModel = editor.diffEditor.getOriginalEditor().getModel();
if (originalEditorModel) {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
const originalComments = await this.commentService.getComments(originalEditorModel.uri as Uri);
if (originalComments) {
this.rangeDecorator.update(editor.diffEditor.getOriginalEditor(), <CommentInfoMain[]>originalComments.filter(c => !!c));
}
}
const modifiedEditorModel = editor.diffEditor.getModifiedEditor().getModel();
if (modifiedEditorModel) {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
const modifiedComments = await this.commentService.getComments(modifiedEditorModel.uri as Uri);
if (modifiedComments) {
this.rangeDecorator.update(editor.diffEditor.getModifiedEditor(), <CommentInfoMain[]>modifiedComments.filter(c => !!c));
}
}
disposables.push(editor.onMouseDown(e => this.onEditorMouseDown(e)));
disposables.push(this.commentService.onDidUpdateCommentThreads(async e => {
const editorURI = editor.document.uri;
const commentInfo = this.commentInfos.filter(info => info.owner === e.owner);
if (!commentInfo || !commentInfo.length) {
return;
}
const added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
const removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
const changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
removed.forEach(thread => {
const matchedZones = this.commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner
&& zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== '');
if (matchedZones.length) {
const matchedZone = matchedZones[0];
const index = this.commentWidgets.indexOf(matchedZone);
this.commentWidgets.splice(index, 1);
matchedZone.dispose();
}
});
changed.forEach(thread => {
const matchedZones = this.commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner
&& zoneWidget.commentThread.threadId === thread.threadId);
if (matchedZones.length) {
const matchedZone = matchedZones[0];
matchedZone.update();
}
});
added.forEach(thread => {
this.displayCommentThread(e.owner, thread);
this.commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
});
})
);
editor.onDispose(() => {
disposables.dispose();
});
this.beginCompute();
}
});
}
private onEditorMouseDown(e: EditorMouseEvent): void {
let mouseDownInfo = null;
const range = e.target.range;
if (!range) {
return;
}
if (e.target.type !== monaco.editor.MouseTargetType.GUTTER_LINE_DECORATIONS) {
return;
}
const data = e.target.detail;
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
// don't collide with folding and git decorations
if (gutterOffsetX > 14) {
return;
}
mouseDownInfo = { lineNumber: range.start };
const { lineNumber } = mouseDownInfo;
mouseDownInfo = null;
if (!range || range.start !== lineNumber) {
return;
}
if (!e.target.element) {
return;
}
if (e.target.element.className.indexOf('comment-diff-added') >= 0) {
this.addOrToggleCommentAtLine(e.target.position!.line + 1, e);
}
}
private async beginCompute(): Promise<void> {
const editorModel = this.editor && this.editor.getModel();
const editorURI = this.editor && editorModel && editorModel.uri;
if (editorURI) {
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
const comments = await this.commentService.getComments(editorURI as Uri);
this.setComments(<CommentInfoMain[]>comments.filter(c => !!c));
}
}
private setComments(commentInfos: CommentInfoMain[]): void {
if (!this.editor) {
return;
}
this.commentInfos = commentInfos;
}
get editor(): monaco.editor.IStandaloneCodeEditor | undefined {
const editor = this.getCurrentEditor();
if (editor && editor.editor instanceof MonacoDiffEditor) {
return editor.editor.diffEditor.getModifiedEditor();
}
}
private displayCommentThread(owner: string, thread: CommentThread): void {
const editor = this.editor;
if (editor) {
const provider = this.commentService.getCommentController(owner);
if (provider) {
this.commentsContext.commentController.set(provider.id);
}
const zoneWidget = new CommentThreadWidget(editor, owner, thread, this.commentService, this.menus, this.commentsContext, this.contextKeyService, this.commands);
zoneWidget.display({ afterLineNumber: thread.range?.startLineNumber || 0, heightInLines: 5 });
const currentEditor = this.getCurrentEditor();
if (currentEditor) {
currentEditor.onDispose(() => zoneWidget.dispose());
}
this.commentWidgets.push(zoneWidget);
}
}
public async addOrToggleCommentAtLine(lineNumber: number, e: EditorMouseEvent | undefined): Promise<void> {
// If an add is already in progress, queue the next add and process it after the current one finishes to
// prevent empty comment threads from being added to the same line.
if (!this.addInProgress) {
this.addInProgress = true;
// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead
const existingCommentsAtLine = this.commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber);
if (existingCommentsAtLine.length) {
existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber));
this.processNextThreadToAdd();
return;
} else {
this.addCommentAtLine(lineNumber, e);
}
} else {
this.emptyThreadsToAddQueue.push([lineNumber, e]);
}
}
private processNextThreadToAdd(): void {
this.addInProgress = false;
const info = this.emptyThreadsToAddQueue.shift();
if (info) {
this.addOrToggleCommentAtLine(info[0], info[1]);
}
}
private getCurrentEditor(): EditorWidget | undefined {
return this.editorManager.currentEditor;
}
public addCommentAtLine(lineNumber: number, e: EditorMouseEvent | undefined): Promise<void> {
const newCommentInfos = this.rangeDecorator.getMatchedCommentAction(lineNumber);
const editor = this.getCurrentEditor();
if (!editor) {
return Promise.resolve();
}
if (!newCommentInfos.length) {
return Promise.resolve();
}
const { ownerId } = newCommentInfos[0]!;
this.addCommentAtLine2(lineNumber, ownerId);
return Promise.resolve();
}
public addCommentAtLine2(lineNumber: number, ownerId: string): void {
const editorModel = this.editor && this.editor.getModel();
const editorURI = this.editor && editorModel && editorModel.uri;
if (editorURI) {
this.commentService.createCommentThreadTemplate(ownerId, URI.parse(editorURI.toString()), {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn: 1,
endColumn: 1
});
this.processNextThreadToAdd();
}
}
}

View File

@@ -0,0 +1,110 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 { CommentInfoMain } from './comments-service';
import { CommentingRanges, Range } from '../../../common/plugin-api-rpc-model';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class CommentingRangeDecorator {
private decorationOptions: monaco.editor.IModelDecorationOptions;
private commentingRangeDecorations: CommentingRangeDecoration[] = [];
constructor() {
this.decorationOptions = {
isWholeLine: true,
linesDecorationsClassName: 'comment-range-glyph comment-diff-added'
};
}
public update(editor: monaco.editor.ICodeEditor, commentInfos: CommentInfoMain[]): void {
const model = editor.getModel();
if (!model) {
return;
}
const commentingRangeDecorations: CommentingRangeDecoration[] = [];
for (const info of commentInfos) {
info.commentingRanges.ranges.forEach(range => {
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label,
range, this.decorationOptions, info.commentingRanges));
});
}
const oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id);
editor.deltaDecorations(oldDecorations, []);
this.commentingRangeDecorations = commentingRangeDecorations;
}
public getMatchedCommentAction(line: number): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges }[] {
const result = [];
for (const decoration of this.commentingRangeDecorations) {
const range = decoration.getActiveRange();
if (range && range.startLineNumber <= line && line <= range.endLineNumber) {
result.push(decoration.getCommentAction());
}
}
return result;
}
}
class CommentingRangeDecoration {
private decorationId: string;
public get id(): string {
return this.decorationId;
}
constructor(private _editor: monaco.editor.ICodeEditor, private _ownerId: string, private _extensionId: string | undefined,
private _label: string | undefined, private _range: Range, commentingOptions: monaco.editor.IModelDecorationOptions,
private commentingRangesInfo: CommentingRanges) {
const startLineNumber = _range.startLineNumber;
const endLineNumber = _range.endLineNumber;
const commentingRangeDecorations = [{
range: {
startLineNumber: startLineNumber, startColumn: 1,
endLineNumber: endLineNumber, endColumn: 1
},
options: commentingOptions
}];
this.decorationId = this._editor.deltaDecorations([], commentingRangeDecorations)[0];
}
public getCommentAction(): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges } {
return {
extensionId: this._extensionId,
label: this._label,
ownerId: this._ownerId,
commentingRangesInfo: this.commentingRangesInfo
};
}
public getOriginalRange(): Range {
return this._range;
}
public getActiveRange(): Range | undefined {
const range = this._editor.getModel()!.getDecorationRange(this.decorationId);
if (range) {
return range;
}
}
}

View File

@@ -0,0 +1,484 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 {
Range,
Comment,
CommentInput,
CommentOptions,
CommentThread,
CommentThreadChangedEvent
} from '../../../common/plugin-api-rpc-model';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { CommentThreadCollapsibleState, CommentThreadState } from '../../../plugin/types-impl';
import {
CommentProviderFeatures,
CommentsExt,
CommentsMain,
CommentThreadChanges,
MAIN_RPC_CONTEXT
} from '../../../common/plugin-api-rpc';
import { Disposable } from '@theia/core/lib/common/disposable';
import { CommentsService, CommentInfoMain } from './comments-service';
import { UriComponents } from '../../../common/uri-components';
import { URI } from '@theia/core/shared/vscode-uri';
import { CancellationToken } from '@theia/core/lib/common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { CommentsContribution } from './comments-contribution';
import { CommentAuthorInformation } from '@theia/plugin';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/api/browser/mainThreadComments.ts
export class CommentThreadImpl implements CommentThread, Disposable {
private _input?: CommentInput;
get input(): CommentInput | undefined {
return this._input;
}
set input(value: CommentInput | undefined) {
this._input = value;
this.onDidChangeInputEmitter.fire(value);
}
private readonly onDidChangeInputEmitter = new Emitter<CommentInput | undefined>();
get onDidChangeInput(): Event<CommentInput | undefined> { return this.onDidChangeInputEmitter.event; }
private _label: string | undefined;
get label(): string | undefined {
return this._label;
}
set label(label: string | undefined) {
this._label = label;
this.onDidChangeLabelEmitter.fire(this._label);
}
private readonly onDidChangeLabelEmitter = new Emitter<string | undefined>();
readonly onDidChangeLabel: Event<string | undefined> = this.onDidChangeLabelEmitter.event;
private _contextValue: string | undefined;
get contextValue(): string | undefined {
return this._contextValue;
}
set contextValue(context: string | undefined) {
this._contextValue = context;
}
private _comments: Comment[] | undefined;
public get comments(): Comment[] | undefined {
return this._comments;
}
public set comments(newComments: Comment[] | undefined) {
this._comments = newComments;
this.onDidChangeCommentsEmitter.fire(this._comments);
}
private readonly onDidChangeCommentsEmitter = new Emitter<Comment[] | undefined>();
get onDidChangeComments(): Event<Comment[] | undefined> { return this.onDidChangeCommentsEmitter.event; }
set range(range: Range | undefined) {
this._range = range;
this.onDidChangeRangeEmitter.fire(this._range);
}
get range(): Range | undefined {
return this._range;
}
private readonly onDidChangeRangeEmitter = new Emitter<Range | undefined>();
public onDidChangeRange = this.onDidChangeRangeEmitter.event;
private _collapsibleState: CommentThreadCollapsibleState | undefined;
get collapsibleState(): CommentThreadCollapsibleState | undefined {
return this._collapsibleState;
}
set collapsibleState(newState: CommentThreadCollapsibleState | undefined) {
this._collapsibleState = newState;
this.onDidChangeCollapsibleStateEmitter.fire(this._collapsibleState);
}
private readonly onDidChangeCollapsibleStateEmitter = new Emitter<CommentThreadCollapsibleState | undefined>();
readonly onDidChangeCollapsibleState = this.onDidChangeCollapsibleStateEmitter.event;
private _state: CommentThreadState | undefined;
get state(): CommentThreadState | undefined {
return this._state;
}
set state(newState: CommentThreadState | undefined) {
if (this._state !== newState) {
this._state = newState;
this.onDidChangeStateEmitter.fire(this._state);
}
}
private readonly onDidChangeStateEmitter = new Emitter<CommentThreadState | undefined>();
readonly onDidChangeState = this.onDidChangeStateEmitter.event;
private readonly onDidChangeCanReplyEmitter = new Emitter<boolean | CommentAuthorInformation>();
readonly onDidChangeCanReply = this.onDidChangeCanReplyEmitter.event;
private _isDisposed: boolean;
get isDisposed(): boolean {
return this._isDisposed;
}
private _canReply: boolean | CommentAuthorInformation = true;
get canReply(): boolean | CommentAuthorInformation {
return this._canReply;
}
set canReply(canReply: boolean | CommentAuthorInformation) {
this._canReply = canReply;
this.onDidChangeCanReplyEmitter.fire(this._canReply);
}
constructor(
public commentThreadHandle: number,
public controllerHandle: number,
public extensionId: string,
public threadId: string,
public resource: string,
private _range: Range | undefined
) {
this._isDisposed = false;
}
batchUpdate(changes: CommentThreadChanges): void {
const modified = (value: keyof CommentThreadChanges): boolean =>
Object.prototype.hasOwnProperty.call(changes, value);
if (modified('range')) { this._range = changes.range; }
if (modified('label')) { this._label = changes.label; }
if (modified('contextValue')) { this._contextValue = changes.contextValue; }
if (modified('comments')) { this._comments = changes.comments; }
if (modified('collapseState')) { this._collapsibleState = changes.collapseState; }
if (modified('state')) { this._state = changes.state; }
if (modified('canReply')) { this._canReply = changes.canReply!; }
}
dispose(): void {
this._isDisposed = true;
this.onDidChangeCollapsibleStateEmitter.dispose();
this.onDidChangeStateEmitter.dispose();
this.onDidChangeCommentsEmitter.dispose();
this.onDidChangeInputEmitter.dispose();
this.onDidChangeLabelEmitter.dispose();
this.onDidChangeRangeEmitter.dispose();
this.onDidChangeCanReplyEmitter.dispose();
}
}
export class CommentController {
get handle(): number {
return this._handle;
}
get id(): string {
return this._id;
}
get contextValue(): string {
return this._id;
}
get proxy(): CommentsExt {
return this._proxy;
}
get label(): string {
return this._label;
}
get options(): CommentOptions | undefined {
return this._features.options;
}
private readonly threads: Map<number, CommentThreadImpl> = new Map<number, CommentThreadImpl>();
public activeCommentThread?: CommentThread;
get features(): CommentProviderFeatures {
return this._features;
}
constructor(
private readonly _proxy: CommentsExt,
private readonly _commentService: CommentsService,
private readonly _handle: number,
private readonly _uniqueId: string,
private readonly _id: string,
private readonly _label: string,
private _features: CommentProviderFeatures
) { }
updateFeatures(features: CommentProviderFeatures): void {
this._features = features;
}
createCommentThread(extensionId: string,
commentThreadHandle: number,
threadId: string,
resource: UriComponents,
range: Range | undefined,
): CommentThread {
const thread = new CommentThreadImpl(
commentThreadHandle,
this.handle,
extensionId,
threadId,
URI.revive(resource).toString(),
range
);
this.threads.set(commentThreadHandle, thread);
this._commentService.updateComments(this._uniqueId, {
added: [thread],
removed: [],
changed: []
});
return thread;
}
updateCommentThread(commentThreadHandle: number,
threadId: string,
resource: UriComponents,
changes: CommentThreadChanges): void {
const thread = this.getKnownThread(commentThreadHandle);
thread.batchUpdate(changes);
this._commentService.updateComments(this._uniqueId, {
added: [],
removed: [],
changed: [thread]
});
}
deleteCommentThread(commentThreadHandle: number): void {
const thread = this.getKnownThread(commentThreadHandle);
this.threads.delete(commentThreadHandle);
this._commentService.updateComments(this._uniqueId, {
added: [],
removed: [thread],
changed: []
});
thread.dispose();
}
deleteCommentThreadMain(commentThreadId: string): void {
this.threads.forEach(thread => {
if (thread.threadId === commentThreadId) {
this._proxy.$deleteCommentThread(this._handle, thread.commentThreadHandle);
}
});
}
updateInput(input: string): void {
const thread = this.activeCommentThread;
if (thread && thread.input) {
const commentInput = thread.input;
commentInput.value = input;
thread.input = commentInput;
}
}
private getKnownThread(commentThreadHandle: number): CommentThreadImpl {
const thread = this.threads.get(commentThreadHandle);
if (!thread) {
throw new Error('unknown thread');
}
return thread;
}
async getDocumentComments(resource: URI, token: CancellationToken): Promise<CommentInfoMain> {
const ret: CommentThread[] = [];
for (const thread of [...this.threads.keys()]) {
const commentThread = this.threads.get(thread)!;
if (commentThread.resource === resource.toString()) {
ret.push(commentThread);
}
}
const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
return <CommentInfoMain>{
owner: this._uniqueId,
label: this.label,
threads: ret,
commentingRanges: {
resource: resource,
ranges: commentingRanges?.ranges || [],
fileComments: !!commentingRanges?.fileComments
}
};
}
async getCommentingRanges(resource: URI, token: CancellationToken): Promise<{ ranges: Range[]; fileComments: boolean } | undefined> {
const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
return commentingRanges;
}
getAllComments(): CommentThread[] {
const ret: CommentThread[] = [];
for (const thread of [...this.threads.keys()]) {
ret.push(this.threads.get(thread)!);
}
return ret;
}
createCommentThreadTemplate(resource: UriComponents, range: Range): void {
this._proxy.$createCommentThreadTemplate(this.handle, resource, range);
}
async updateCommentThreadTemplate(threadHandle: number, range: Range): Promise<void> {
await this._proxy.$updateCommentThreadTemplate(this.handle, threadHandle, range);
}
}
export class CommentsMainImp implements CommentsMain {
private readonly proxy: CommentsExt;
private documentProviders = new Map<number, Disposable>();
private workspaceProviders = new Map<number, Disposable>();
private handlers = new Map<number, string>();
private commentControllers = new Map<number, CommentController>();
private activeCommentThread?: CommentThread;
private readonly commentService: CommentsService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMENTS_EXT);
container.get(CommentsContribution);
this.commentService = container.get(CommentsService);
this.commentService.onDidChangeActiveCommentThread(async thread => {
const handle = (thread as CommentThread).controllerHandle;
const controller = this.commentControllers.get(handle);
if (!controller) {
return;
}
this.activeCommentThread = thread as CommentThread;
controller.activeCommentThread = this.activeCommentThread;
});
}
$registerCommentController(handle: number, id: string, label: string): void {
const providerId = generateUuid();
this.handlers.set(handle, providerId);
const provider = new CommentController(this.proxy, this.commentService, handle, providerId, id, label, {});
this.commentService.registerCommentController(providerId, provider);
this.commentControllers.set(handle, provider);
this.commentService.setWorkspaceComments(String(handle), []);
}
$unregisterCommentController(handle: number): void {
const providerId = this.handlers.get(handle);
if (typeof providerId !== 'string') {
throw new Error('unknown handler');
}
this.commentService.unregisterCommentController(providerId);
this.handlers.delete(handle);
this.commentControllers.delete(handle);
}
$updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void {
const provider = this.commentControllers.get(handle);
if (!provider) {
return undefined;
}
provider.updateFeatures(features);
}
$createCommentThread(handle: number,
commentThreadHandle: number,
threadId: string,
resource: UriComponents,
range: Range | undefined,
extensionId: string
): CommentThread | undefined {
const provider = this.commentControllers.get(handle);
if (!provider) {
return undefined;
}
return provider.createCommentThread(extensionId, commentThreadHandle, threadId, resource, range);
}
$updateCommentThread(handle: number,
commentThreadHandle: number,
threadId: string,
resource: UriComponents,
changes: CommentThreadChanges): void {
const provider = this.commentControllers.get(handle);
if (!provider) {
return undefined;
}
return provider.updateCommentThread(commentThreadHandle, threadId, resource, changes);
}
$deleteCommentThread(handle: number, commentThreadHandle: number): void {
const provider = this.commentControllers.get(handle);
if (!provider) {
return;
}
return provider.deleteCommentThread(commentThreadHandle);
}
private getHandler(handle: number): string {
if (!this.handlers.has(handle)) {
throw new Error('Unknown handler');
}
return this.handlers.get(handle)!;
}
$onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void {
const providerId = this.getHandler(handle);
this.commentService.updateComments(providerId, event);
}
dispose(): void {
this.workspaceProviders.forEach(value => value.dispose());
this.workspaceProviders.clear();
this.documentProviders.forEach(value => value.dispose());
this.documentProviders.clear();
}
}

View File

@@ -0,0 +1,207 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. 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 { URI } from '@theia/core/shared/vscode-uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import {
Range,
CommentInfo,
CommentingRanges,
CommentThread,
CommentThreadChangedEvent,
CommentThreadChangedEventMain
} from '../../../common/plugin-api-rpc-model';
import { CommentController } from './comments-main';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentService.ts
export interface ResourceCommentThreadEvent {
resource: URI;
commentInfos: CommentInfoMain[];
}
export interface CommentInfoMain extends CommentInfo {
owner: string;
label?: string;
}
export interface WorkspaceCommentThreadsEventMain {
ownerId: string;
commentThreads: CommentThread[];
}
export const CommentsService = Symbol('CommentsService');
export interface CommentsService {
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent>;
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain>;
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain>;
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
readonly onDidSetDataProvider: Event<void>;
readonly onDidDeleteDataProvider: Event<string>;
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void;
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
removeWorkspaceComments(owner: string): void;
registerCommentController(owner: string, commentControl: CommentController): void;
unregisterCommentController(owner: string): void;
getCommentController(owner: string): CommentController | undefined;
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void;
updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void>;
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
disposeCommentThread(ownerId: string, threadId: string): void;
getComments(resource: URI): Promise<(CommentInfoMain | null)[]>;
getCommentingRanges(resource: URI): Promise<Range[]>;
setActiveCommentThread(commentThread: CommentThread | null): void;
}
@injectable()
export class PluginCommentService implements CommentsService {
private readonly onDidSetDataProviderEmitter: Emitter<void> = new Emitter<void>();
readonly onDidSetDataProvider: Event<void> = this.onDidSetDataProviderEmitter.event;
private readonly onDidDeleteDataProviderEmitter: Emitter<string> = new Emitter<string>();
readonly onDidDeleteDataProvider: Event<string> = this.onDidDeleteDataProviderEmitter.event;
private readonly onDidSetResourceCommentInfosEmitter: Emitter<ResourceCommentThreadEvent> = new Emitter<ResourceCommentThreadEvent>();
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent> = this.onDidSetResourceCommentInfosEmitter.event;
private readonly onDidSetAllCommentThreadsEmitter: Emitter<WorkspaceCommentThreadsEventMain> = new Emitter<WorkspaceCommentThreadsEventMain>();
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain> = this.onDidSetAllCommentThreadsEmitter.event;
private readonly onDidUpdateCommentThreadsEmitter: Emitter<CommentThreadChangedEventMain> = new Emitter<CommentThreadChangedEventMain>();
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain> = this.onDidUpdateCommentThreadsEmitter.event;
private readonly onDidChangeActiveCommentThreadEmitter = new Emitter<CommentThread | null>();
readonly onDidChangeActiveCommentThread = this.onDidChangeActiveCommentThreadEmitter.event;
private readonly onDidChangeActiveCommentingRangeEmitter = new Emitter<{ range: Range, commentingRangesInfo: CommentingRanges }>();
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }> = this.onDidChangeActiveCommentingRangeEmitter.event;
private commentControls = new Map<string, CommentController>();
setActiveCommentThread(commentThread: CommentThread | null): void {
this.onDidChangeActiveCommentThreadEmitter.fire(commentThread);
}
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void {
this.onDidSetResourceCommentInfosEmitter.fire({ resource, commentInfos });
}
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: commentsByResource });
}
removeWorkspaceComments(owner: string): void {
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: [] });
}
registerCommentController(owner: string, commentControl: CommentController): void {
this.commentControls.set(owner, commentControl);
this.onDidSetDataProviderEmitter.fire();
}
unregisterCommentController(owner: string): void {
this.commentControls.delete(owner);
this.onDidDeleteDataProviderEmitter.fire(owner);
}
getCommentController(owner: string): CommentController | undefined {
return this.commentControls.get(owner);
}
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void {
const commentController = this.commentControls.get(owner);
if (!commentController) {
return;
}
commentController.createCommentThreadTemplate(resource, range);
}
async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void> {
const commentController = this.commentControls.get(owner);
if (!commentController) {
return;
}
await commentController.updateCommentThreadTemplate(threadHandle, range);
}
disposeCommentThread(owner: string, threadId: string): void {
const controller = this.getCommentController(owner);
if (controller) {
controller.deleteCommentThreadMain(threadId);
}
}
updateComments(ownerId: string, event: CommentThreadChangedEvent): void {
const evt: CommentThreadChangedEventMain = Object.assign({}, event, { owner: ownerId });
this.onDidUpdateCommentThreadsEmitter.fire(evt);
}
async getComments(resource: URI): Promise<(CommentInfoMain | null)[]> {
const commentControlResult: Promise<CommentInfoMain | null>[] = [];
this.commentControls.forEach(control => {
commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None)
.catch(e => {
console.log(e);
return null;
}));
});
return Promise.all(commentControlResult);
}
async getCommentingRanges(resource: URI): Promise<Range[]> {
const commentControlResult: Promise<{ ranges: Range[]; fileComments: boolean } | undefined>[] = [];
this.commentControls.forEach(control => {
commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None));
});
const ret = await Promise.all(commentControlResult);
return ret.reduce<Range[]>((prev, curr) => {
if (curr) {
prev.push(...curr.ranges);
}
return prev;
}, []);
}
}

View File

@@ -0,0 +1,209 @@
// *****************************************************************************
// Copyright (C) 2021 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 URI from '@theia/core/lib/common/uri';
import {
ApplicationShell, DiffUris, OpenHandler, OpenerOptions, SplitWidget, Widget, WidgetManager, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority
} from '@theia/core/lib/browser';
import { CustomEditor, CustomEditorPriority, CustomEditorSelector } from '../../../common';
import { CustomEditorWidget } from './custom-editor-widget';
import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { DisposableCollection, Emitter, PreferenceService } from '@theia/core';
import { match } from '@theia/core/lib/common/glob';
export class CustomEditorOpener implements OpenHandler {
readonly id: string;
readonly label: string;
private readonly onDidOpenCustomEditorEmitter = new Emitter<[CustomEditorWidget, WidgetOpenerOptions?]>();
readonly onDidOpenCustomEditor = this.onDidOpenCustomEditorEmitter.event;
constructor(
private readonly editor: CustomEditor,
protected readonly shell: ApplicationShell,
protected readonly widgetManager: WidgetManager,
protected readonly editorRegistry: PluginCustomEditorRegistry,
protected readonly preferenceService: PreferenceService
) {
this.id = CustomEditorOpener.toCustomEditorId(this.editor.viewType);
this.label = this.editor.displayName;
}
static toCustomEditorId(editorViewType: string): string {
return `custom-editor-${editorViewType}`;
}
canHandle(uri: URI, options?: OpenerOptions): number {
let priority = 0;
const { selector } = this.editor;
if (DiffUris.isDiffUri(uri)) {
const [left, right] = DiffUris.decode(uri);
if (this.matches(selector, right) && this.matches(selector, left)) {
if (getDefaultHandler(right, this.preferenceService) === this.editor.viewType) {
priority = defaultHandlerPriority;
} else {
priority = this.getPriority();
}
}
} else if (this.matches(selector, uri)) {
if (getDefaultHandler(uri, this.preferenceService) === this.editor.viewType) {
priority = defaultHandlerPriority;
} else {
priority = this.getPriority();
}
}
return priority;
}
canOpenWith(uri: URI): number {
if (this.matches(this.editor.selector, uri)) {
return this.getPriority();
}
return 0;
}
getPriority(): number {
switch (this.editor.priority) {
case CustomEditorPriority.default: return 500;
case CustomEditorPriority.builtin: return 400;
/** `option` should not open the custom-editor by default. */
case CustomEditorPriority.option: return 1;
default: return 200;
}
}
protected readonly pendingWidgetPromises = new Map<string, Promise<CustomEditorWidget>>();
protected async openCustomEditor(uri: URI, options?: WidgetOpenerOptions): Promise<CustomEditorWidget> {
let widget: CustomEditorWidget | undefined;
let isNewWidget = false;
const uriString = uri.toString();
let widgetPromise = this.pendingWidgetPromises.get(uriString);
if (widgetPromise) {
widget = await widgetPromise;
} else {
const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[];
widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uriString);
if (!widget) {
isNewWidget = true;
const id = generateUuid();
widgetPromise = this.widgetManager.getOrCreateWidget<CustomEditorWidget>(CustomEditorWidget.FACTORY_ID, { id }).then(async w => {
try {
w.viewType = this.editor.viewType;
w.resource = uri;
await this.editorRegistry.resolveWidget(w);
if (options?.widgetOptions) {
await this.shell.addWidget(w, options.widgetOptions);
}
return w;
} catch (e) {
w.dispose();
throw e;
}
}).finally(() => this.pendingWidgetPromises.delete(uriString));
this.pendingWidgetPromises.set(uriString, widgetPromise);
widget = await widgetPromise;
}
}
if (options?.mode === 'activate') {
await this.shell.activateWidget(widget.id);
} else if (options?.mode === 'reveal') {
await this.shell.revealWidget(widget.id);
}
if (isNewWidget) {
this.onDidOpenCustomEditorEmitter.fire([widget, options]);
}
return widget;
}
protected async openSideBySide(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
const [leftUri, rightUri] = DiffUris.decode(uri);
const widget = await this.widgetManager.getOrCreateWidget<SplitWidget>(
CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID, { uri: uri.toString(), viewType: this.editor.viewType });
if (!widget.panes.length) { // a new widget
const trackedDisposables = new DisposableCollection(widget);
try {
const createPane = async (paneUri: URI) => {
let pane = await this.openCustomEditor(paneUri);
if (pane.isAttached) {
await this.shell.closeWidget(pane.id);
if (!pane.isDisposed) { // user canceled
return undefined;
}
pane = await this.openCustomEditor(paneUri);
}
return pane;
};
const rightPane = await createPane(rightUri);
if (!rightPane) {
trackedDisposables.dispose();
return undefined;
}
trackedDisposables.push(rightPane);
const leftPane = await createPane(leftUri);
if (!leftPane) {
trackedDisposables.dispose();
return undefined;
}
trackedDisposables.push(leftPane);
widget.addPane(leftPane);
widget.addPane(rightPane);
// dispose the widget if either of its panes gets externally disposed
leftPane.disposed.connect(() => widget.dispose());
rightPane.disposed.connect(() => widget.dispose());
if (options?.widgetOptions) {
await this.shell.addWidget(widget, options.widgetOptions);
}
} catch (e) {
trackedDisposables.dispose();
console.error(e);
throw e;
}
}
if (options?.mode === 'activate') {
await this.shell.activateWidget(widget.id);
} else if (options?.mode === 'reveal') {
await this.shell.revealWidget(widget.id);
}
return widget;
}
async open(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
options = { ...options };
options.mode ??= 'activate';
options.widgetOptions ??= { area: 'main' };
return DiffUris.isDiffUri(uri) ? this.openSideBySide(uri, options) : this.openCustomEditor(uri, options);
}
matches(selectors: CustomEditorSelector[], resource: URI): boolean {
return selectors.some(selector => this.selectorMatches(selector, resource));
}
selectorMatches(selector: CustomEditorSelector, resource: URI): boolean {
if (selector.filenamePattern) {
if (match(selector.filenamePattern.toLowerCase(), resource.path.name.toLowerCase() + resource.path.ext.toLowerCase())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,108 @@
// *****************************************************************************
// Copyright (C) 2021 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/contrib/customEditor/browser/customEditors.ts
import { injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Reference } from '@theia/core/lib/common/reference';
import { CustomEditorModel } from './custom-editors-main';
@injectable()
export class CustomEditorService {
protected _models = new CustomEditorModelManager();
get models(): CustomEditorModelManager { return this._models; }
}
export class CustomEditorModelManager {
private readonly references = new Map<string, {
readonly viewType: string,
readonly model: Promise<CustomEditorModel>,
counter: number
}>();
add(resource: URI, viewType: string, model: Promise<CustomEditorModel>): Promise<Reference<CustomEditorModel>> {
const key = this.key(resource, viewType);
const existing = this.references.get(key);
if (existing) {
throw new Error('Model already exists');
}
this.references.set(key, { viewType, model, counter: 0 });
return this.tryRetain(resource, viewType)!;
}
async get(resource: URI, viewType: string): Promise<CustomEditorModel | undefined> {
const key = this.key(resource, viewType);
const entry = this.references.get(key);
return entry?.model;
}
tryRetain(resource: URI, viewType: string): Promise<Reference<CustomEditorModel>> | undefined {
const key = this.key(resource, viewType);
const entry = this.references.get(key);
if (!entry) {
return undefined;
}
entry.counter++;
return entry.model.then(model => ({
object: model,
dispose: once(() => {
if (--entry!.counter <= 0) {
entry.model.then(x => x.dispose());
this.references.delete(key);
}
}),
}));
}
disposeAllModelsForView(viewType: string): void {
for (const [key, value] of this.references) {
if (value.viewType === viewType) {
value.model.then(x => x.dispose());
this.references.delete(key);
}
}
}
private key(resource: URI, viewType: string): string {
return `${resource.toString()}@@@${viewType}`;
}
}
export function once<T extends Function>(this: unknown, fn: T): T {
const _this = this;
let didCall = false;
let result: unknown;
return function (): unknown {
if (didCall) {
return result;
}
didCall = true;
result = fn.apply(_this, arguments);
return result;
} as unknown as T;
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// 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 { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser';
import { CustomEditorWidget } from './custom-editor-widget';
@injectable()
export class CustomEditorUndoRedoHandler implements UndoRedoHandler<CustomEditorWidget> {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
priority = 190;
select(): CustomEditorWidget | undefined {
const current = this.applicationShell.currentWidget;
if (current instanceof CustomEditorWidget) {
return current;
}
return undefined;
}
undo(item: CustomEditorWidget): void {
item.undo();
}
redo(item: CustomEditorWidget): void {
item.redo();
}
}

View File

@@ -0,0 +1,44 @@
// *****************************************************************************
// Copyright (C) 2021 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 { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
import { interfaces } from '@theia/core/shared/inversify';
import { WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from '../webview/webview';
import { WebviewEnvironment } from '../webview/webview-environment';
export class CustomEditorWidgetFactory {
readonly id = CustomEditorWidget.FACTORY_ID;
protected readonly container: interfaces.Container;
constructor(container: interfaces.Container) {
this.container = container;
}
async createWidget(identifier: WebviewWidgetIdentifier): Promise<CustomEditorWidget> {
const externalEndpoint = await this.container.get(WebviewEnvironment).externalEndpoint();
let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id);
if (endpoint[endpoint.length - 1] === '/') {
endpoint = endpoint.slice(0, endpoint.length - 1);
}
const child = this.container.createChild();
child.bind(WebviewWidgetIdentifier).toConstantValue(identifier);
child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint);
return child.get(CustomEditorWidget);
}
}

View File

@@ -0,0 +1,114 @@
// *****************************************************************************
// Copyright (C) 2021 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { FileOperation } from '@theia/filesystem/lib/common/files';
import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
import { Reference } from '@theia/core/lib/common/reference';
import { WebviewWidget } from '../webview/webview';
import { CustomEditorModel } from './custom-editors-main';
import { CustomEditorWidget as CustomEditorWidgetShape } from '@theia/editor/lib/browser';
@injectable()
export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget {
static override FACTORY_ID = 'plugin-custom-editor';
static readonly SIDE_BY_SIDE_FACTORY_ID = CustomEditorWidget.FACTORY_ID + '.side-by-side';
resource: URI;
protected _modelRef: Reference<CustomEditorModel | undefined> = { object: undefined, dispose: () => { } };
get modelRef(): Reference<CustomEditorModel | undefined> {
return this._modelRef;
}
set modelRef(modelRef: Reference<CustomEditorModel>) {
this._modelRef.dispose();
this._modelRef = modelRef;
this.delegatingSaveable.delegate = modelRef.object;
this.doUpdateContent();
}
// ensures that saveable is available even if modelRef.object is undefined
protected readonly delegatingSaveable = new DelegatingSaveable();
get saveable(): Saveable {
return this.delegatingSaveable;
}
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(SaveableService)
protected readonly saveService: SaveableService;
@postConstruct()
protected override init(): void {
super.init();
this.id = CustomEditorWidget.FACTORY_ID + ':' + this.identifier.id;
this.toDispose.push(this.fileService.onDidRunOperation(e => {
if (e.isOperation(FileOperation.MOVE)) {
this.doMove(e.target.resource);
}
}));
}
undo(): void {
this._modelRef.object?.undo();
}
redo(): void {
this._modelRef.object?.redo();
}
getResourceUri(): URI | undefined {
return this.resource;
}
createMoveToUri(resourceUri: URI): URI | undefined {
return this.resource.withPath(resourceUri.path);
}
override storeState(): CustomEditorWidget.State {
return {
...super.storeState(),
strResource: this.resource.toString(),
};
}
override restoreState(oldState: CustomEditorWidget.State): void {
const { strResource } = oldState;
this.resource = new URI(strResource);
super.restoreState(oldState);
}
onMove(handler: (newResource: URI) => Promise<void>): void {
this._moveHandler = handler;
}
private _moveHandler?: (newResource: URI) => void;
private doMove(target: URI): void {
if (this._moveHandler) {
this._moveHandler(target);
}
}
}
export namespace CustomEditorWidget {
export interface State extends WebviewWidget.State {
strResource: string
}
}

View File

@@ -0,0 +1,528 @@
// *****************************************************************************
// Copyright (C) 2021 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/browser/mainThreadCustomEditors.ts
import { interfaces } from '@theia/core/shared/inversify';
import { MAIN_RPC_CONTEXT, CustomEditorsMain, CustomEditorsExt, CustomTextEditorCapabilities } from '../../../common/plugin-api-rpc';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
import { Emitter } from '@theia/core';
import { UriComponents } from '../../../common/uri-components';
import { URI } from '@theia/core/shared/vscode-uri';
import TheiaURI from '@theia/core/lib/common/uri';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Reference } from '@theia/core/lib/common/reference';
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { EditorModelService } from '../text-editor-model-service';
import { CustomEditorService } from './custom-editor-service';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
import { WebviewsMainImpl } from '../webviews-main';
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
import { ApplicationShell, LabelProvider, Saveable, SaveAsOptions, SaveOptions } from '@theia/core/lib/browser';
import { WebviewPanelOptions } from '@theia/plugin';
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
const enum CustomEditorModelType {
Custom,
Text,
}
export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable {
protected readonly pluginService: HostedPluginSupport;
protected readonly shell: ApplicationShell;
protected readonly textModelService: EditorModelService;
protected readonly fileService: FileService;
protected readonly customEditorService: CustomEditorService;
protected readonly undoRedoService: UndoRedoService;
protected readonly customEditorRegistry: PluginCustomEditorRegistry;
protected readonly labelProvider: LabelProvider;
protected readonly widgetManager: WidgetManager;
protected readonly editorPreferences: EditorPreferences;
private readonly proxy: CustomEditorsExt;
private readonly editorProviders = new Map<string, Disposable>();
constructor(rpc: RPCProtocol,
container: interfaces.Container,
readonly webviewsMain: WebviewsMainImpl,
) {
this.pluginService = container.get(HostedPluginSupport);
this.shell = container.get(ApplicationShell);
this.textModelService = container.get(EditorModelService);
this.fileService = container.get(FileService);
this.customEditorService = container.get(CustomEditorService);
this.undoRedoService = container.get(UndoRedoService);
this.customEditorRegistry = container.get(PluginCustomEditorRegistry);
this.labelProvider = container.get(LabelProvider);
this.editorPreferences = container.get(EditorPreferences);
this.widgetManager = container.get(WidgetManager);
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT);
}
dispose(): void {
for (const disposable of this.editorProviders.values()) {
disposable.dispose();
}
this.editorProviders.clear();
}
$registerTextEditorProvider(
viewType: string, options: WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void {
this.registerEditorProvider(CustomEditorModelType.Text, viewType, options, capabilities, true);
}
$registerCustomEditorProvider(viewType: string, options: WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void {
this.registerEditorProvider(CustomEditorModelType.Custom, viewType, options, {}, supportsMultipleEditorsPerDocument);
}
protected async registerEditorProvider(
modelType: CustomEditorModelType,
viewType: string,
options: WebviewPanelOptions,
capabilities: CustomTextEditorCapabilities,
supportsMultipleEditorsPerDocument: boolean,
): Promise<void> {
if (this.editorProviders.has(viewType)) {
throw new Error(`Provider for ${viewType} already registered`);
}
const disposables = new DisposableCollection();
disposables.push(
this.customEditorRegistry.registerResolver(viewType, async widget => {
const { resource, identifier } = widget;
widget.options = options;
const cancellationSource = new CancellationTokenSource();
let modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, cancellationSource.token);
widget.modelRef = modelRef;
widget.onDidDispose(() => {
// If the model is still dirty, make sure we have time to save it
if (modelRef.object.dirty) {
const sub = modelRef.object.onDirtyChanged(() => {
if (!modelRef.object.dirty) {
sub.dispose();
modelRef.dispose();
}
});
return;
}
modelRef.dispose();
});
if (capabilities.supportsMove) {
const onMoveCancelTokenSource = new CancellationTokenSource();
widget.onMove(async (newResource: TheiaURI) => {
const oldModel = modelRef;
modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, onMoveCancelTokenSource.token);
this.proxy.$onMoveCustomEditor(identifier.id, newResource.toComponents(), viewType);
oldModel.dispose();
});
}
this.webviewsMain.hookWebview(widget);
widget.title.label = this.labelProvider.getName(resource);
const _cancellationSource = new CancellationTokenSource();
await this.proxy.$resolveWebviewEditor(
resource.toComponents(),
identifier.id,
viewType,
widget.title.label,
widget.viewState.position,
options,
_cancellationSource.token
);
})
);
this.editorProviders.set(viewType, disposables);
}
$unregisterEditorProvider(viewType: string): void {
const provider = this.editorProviders.get(viewType);
if (!provider) {
throw new Error(`No provider for ${viewType} registered`);
}
provider.dispose();
this.editorProviders.delete(viewType);
this.customEditorService.models.disposeAllModelsForView(viewType);
}
protected async getOrCreateCustomEditorModel(
modelType: CustomEditorModelType,
resource: TheiaURI,
viewType: string,
cancellationToken: CancellationToken,
): Promise<Reference<CustomEditorModel>> {
const existingModel = this.customEditorService.models.tryRetain(resource, viewType);
if (existingModel) {
return existingModel;
}
switch (modelType) {
case CustomEditorModelType.Text: {
const model = CustomTextEditorModel.create(viewType, resource, this.textModelService);
return this.customEditorService.models.add(resource, viewType, model);
}
case CustomEditorModelType.Custom: {
const model = MainCustomEditorModel.create(this.proxy, viewType, resource, this.undoRedoService, this.fileService, cancellationToken);
return this.customEditorService.models.add(resource, viewType, model);
}
}
}
protected async getCustomEditorModel(resourceComponents: UriComponents, viewType: string): Promise<MainCustomEditorModel> {
const resource = URI.revive(resourceComponents);
const model = await this.customEditorService.models.get(new TheiaURI(resource), viewType);
if (!model || !(model instanceof MainCustomEditorModel)) {
throw new Error('Could not find model for custom editor');
}
return model;
}
async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {
const model = await this.getCustomEditorModel(resourceComponents, viewType);
model.pushEdit(editId, label);
}
async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {
const model = await this.getCustomEditorModel(resourceComponents, viewType);
model.changeContent();
}
}
export interface CustomEditorModel extends Saveable, Disposable {
readonly viewType: string;
readonly resource: URI;
readonly readonly: boolean;
readonly dirty: boolean;
revert(options?: Saveable.RevertOptions): Promise<void>;
saveCustomEditor(options?: SaveOptions): Promise<void>;
saveCustomEditorAs?(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise<void>;
undo(): void;
redo(): void;
}
export class MainCustomEditorModel implements CustomEditorModel {
private currentEditIndex: number = -1;
private savePoint: number = -1;
private isDirtyFromContentChange = false;
private ongoingSave?: CancellationTokenSource;
private readonly edits: Array<number> = [];
private readonly toDispose = new DisposableCollection();
private readonly onDirtyChangedEmitter = new Emitter<void>();
readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
private readonly onContentChangedEmitter = new Emitter<void>();
readonly onContentChanged = this.onContentChangedEmitter.event;
static async create(
proxy: CustomEditorsExt,
viewType: string,
resource: TheiaURI,
undoRedoService: UndoRedoService,
fileService: FileService,
cancellation: CancellationToken,
): Promise<MainCustomEditorModel> {
const { editable } = await proxy.$createCustomDocument(resource.toComponents(), viewType, {}, cancellation);
return new MainCustomEditorModel(proxy, viewType, resource, editable, undoRedoService, fileService);
}
constructor(
private proxy: CustomEditorsExt,
readonly viewType: string,
private readonly editorResource: TheiaURI,
private readonly editable: boolean,
private readonly undoRedoService: UndoRedoService,
private readonly fileService: FileService
) {
this.toDispose.push(this.onDirtyChangedEmitter);
}
get resource(): URI {
return URI.from(this.editorResource.toComponents());
}
get dirty(): boolean {
if (this.isDirtyFromContentChange) {
return true;
}
if (this.edits.length > 0) {
return this.savePoint !== this.currentEditIndex;
}
return false;
}
get readonly(): boolean {
return !this.editable;
}
setProxy(proxy: CustomEditorsExt): void {
this.proxy = proxy;
}
dispose(): void {
if (this.editable) {
this.undoRedoService.removeElements(this.editorResource);
}
this.proxy.$disposeCustomDocument(this.resource, this.viewType);
}
changeContent(): void {
this.change(() => {
this.isDirtyFromContentChange = true;
});
}
pushEdit(editId: number, label: string | undefined): void {
if (!this.editable) {
throw new Error('Document is not editable');
}
this.change(() => {
this.spliceEdits(editId);
this.currentEditIndex = this.edits.length - 1;
});
this.undoRedoService.pushElement(
this.editorResource,
() => this.undo(),
() => this.redo(),
);
}
async revert(options?: Saveable.RevertOptions): Promise<void> {
if (!this.editable) {
return;
}
if (this.currentEditIndex === this.savePoint && !this.isDirtyFromContentChange) {
return;
}
const cancellationSource = new CancellationTokenSource();
await this.proxy.$revert(this.resource, this.viewType, cancellationSource.token);
this.change(() => {
this.isDirtyFromContentChange = false;
this.currentEditIndex = this.savePoint;
this.spliceEdits();
});
}
async save(options?: SaveOptions): Promise<void> {
await this.saveCustomEditor(options);
}
async saveCustomEditor(options?: SaveOptions): Promise<void> {
if (!this.editable) {
return;
}
const cancelable = new CancellationTokenSource();
const savePromise = this.proxy.$save(this.resource, this.viewType, cancelable.token);
this.ongoingSave?.cancel();
this.ongoingSave = cancelable;
try {
await savePromise;
if (this.ongoingSave === cancelable) { // Make sure we are still doing the same save
this.change(() => {
this.isDirtyFromContentChange = false;
this.savePoint = this.currentEditIndex;
});
}
} finally {
if (this.ongoingSave === cancelable) { // Make sure we are still doing the same save
this.ongoingSave = undefined;
}
}
}
async saveAs(options: SaveAsOptions): Promise<void> {
await this.saveCustomEditorAs(new TheiaURI(this.resource), options.target, options);
}
async saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise<void> {
if (this.editable) {
const source = new CancellationTokenSource();
await this.proxy.$saveAs(this.resource, this.viewType, targetResource.toComponents(), source.token);
this.change(() => {
this.savePoint = this.currentEditIndex;
});
} else {
// Since the editor is readonly, just copy the file over
await this.fileService.copy(resource, targetResource, { overwrite: false });
}
}
async undo(): Promise<void> {
if (!this.editable) {
return;
}
if (this.currentEditIndex < 0) {
// nothing to undo
return;
}
const undoneEdit = this.edits[this.currentEditIndex];
this.change(() => {
--this.currentEditIndex;
});
await this.proxy.$undo(this.resource, this.viewType, undoneEdit, this.dirty);
}
async redo(): Promise<void> {
if (!this.editable) {
return;
}
if (this.currentEditIndex >= this.edits.length - 1) {
// nothing to redo
return;
}
const redoneEdit = this.edits[this.currentEditIndex + 1];
this.change(() => {
++this.currentEditIndex;
});
await this.proxy.$redo(this.resource, this.viewType, redoneEdit, this.dirty);
}
private spliceEdits(editToInsert?: number): void {
const start = this.currentEditIndex + 1;
const toRemove = this.edits.length - this.currentEditIndex;
const removedEdits = typeof editToInsert === 'number'
? this.edits.splice(start, toRemove, editToInsert)
: this.edits.splice(start, toRemove);
if (removedEdits.length) {
this.proxy.$disposeEdits(this.resource, this.viewType, removedEdits);
}
}
private change(makeEdit: () => void): void {
const wasDirty = this.dirty;
makeEdit();
if (this.dirty !== wasDirty) {
this.onDirtyChangedEmitter.fire();
}
this.onContentChangedEmitter.fire();
}
}
// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts
export class CustomTextEditorModel implements CustomEditorModel {
private readonly toDispose = new DisposableCollection();
private readonly onDirtyChangedEmitter = new Emitter<void>();
readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
private readonly onContentChangedEmitter = new Emitter<void>();
readonly onContentChanged = this.onContentChangedEmitter.event;
static async create(
viewType: string,
resource: TheiaURI,
editorModelService: EditorModelService
): Promise<CustomTextEditorModel> {
const model = await editorModelService.createModelReference(resource);
model.object.suppressOpenEditorWhenDirty = true;
return new CustomTextEditorModel(viewType, resource, model);
}
constructor(
readonly viewType: string,
readonly editorResource: TheiaURI,
private readonly model: Reference<MonacoEditorModel>
) {
this.toDispose.push(
this.editorTextModel.onDirtyChanged(e => {
this.onDirtyChangedEmitter.fire();
})
);
this.toDispose.push(
this.editorTextModel.onContentChanged(e => {
this.onContentChangedEmitter.fire();
})
);
this.toDispose.push(this.onDirtyChangedEmitter);
this.toDispose.push(this.onContentChangedEmitter);
}
dispose(): void {
this.toDispose.dispose();
this.model.dispose();
}
get resource(): URI {
return URI.from(this.editorResource.toComponents());
}
get dirty(): boolean {
return this.editorTextModel.dirty;
};
get readonly(): boolean {
return Boolean(this.editorTextModel.readOnly);
}
get editorTextModel(): MonacoEditorModel {
return this.model.object;
}
revert(options?: Saveable.RevertOptions): Promise<void> {
return this.editorTextModel.revert(options);
}
save(options?: SaveOptions): Promise<void> {
return this.saveCustomEditor(options);
}
serialize(): Promise<BinaryBuffer> {
return this.editorTextModel.serialize();
}
saveCustomEditor(options?: SaveOptions): Promise<void> {
return this.editorTextModel.save(options);
}
undo(): void {
this.editorTextModel.undo();
}
redo(): void {
this.editorTextModel.redo();
}
}

View File

@@ -0,0 +1,126 @@
// *****************************************************************************
// Copyright (C) 2021 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 { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { CustomEditor, DeployedPlugin } from '../../../common';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { CustomEditorOpener } from './custom-editor-opener';
import { Emitter, PreferenceService } from '@theia/core';
import { ApplicationShell, DefaultOpenerService, OpenWithService, WidgetManager } from '@theia/core/lib/browser';
import { CustomEditorWidget } from './custom-editor-widget';
@injectable()
export class PluginCustomEditorRegistry {
private readonly editors = new Map<string, CustomEditor>();
private readonly pendingEditors = new Map<CustomEditorWidget, { deferred: Deferred<void>, disposable: Disposable }>();
private readonly resolvers = new Map<string, (widget: CustomEditorWidget) => Promise<void>>();
private readonly onWillOpenCustomEditorEmitter = new Emitter<string>();
readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event;
@inject(DefaultOpenerService)
protected readonly defaultOpenerService: DefaultOpenerService;
@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(OpenWithService)
protected readonly openWithService: OpenWithService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@postConstruct()
protected init(): void {
this.widgetManager.onDidCreateWidget(({ factoryId, widget }) => {
if (factoryId === CustomEditorWidget.FACTORY_ID && widget instanceof CustomEditorWidget) {
const restoreState = widget.restoreState.bind(widget);
widget.restoreState = state => {
if (state.viewType && state.strResource) {
restoreState(state);
this.resolveWidget(widget);
} else {
widget.dispose();
}
};
}
});
}
registerCustomEditor(editor: CustomEditor, plugin: DeployedPlugin): Disposable {
if (this.editors.has(editor.viewType)) {
console.warn('editor with such id already registered: ', JSON.stringify(editor));
return Disposable.NULL;
}
this.editors.set(editor.viewType, editor);
const toDispose = new DisposableCollection();
toDispose.push(Disposable.create(() => this.editors.delete(editor.viewType)));
const editorOpenHandler = new CustomEditorOpener(
editor,
this.shell,
this.widgetManager,
this,
this.preferenceService
);
toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler));
toDispose.push(
this.openWithService.registerHandler({
id: editor.viewType,
label: editorOpenHandler.label,
providerName: plugin.metadata.model.displayName,
canHandle: uri => editorOpenHandler.canOpenWith(uri),
open: uri => editorOpenHandler.open(uri)
})
);
return toDispose;
}
async resolveWidget(widget: CustomEditorWidget): Promise<void> {
const resolver = this.resolvers.get(widget.viewType);
if (resolver) {
await resolver(widget);
} else {
const deferred = new Deferred<void>();
const disposable = widget.onDidDispose(() => this.pendingEditors.delete(widget));
this.pendingEditors.set(widget, { deferred, disposable });
this.onWillOpenCustomEditorEmitter.fire(widget.viewType);
return deferred.promise;
}
};
registerResolver(viewType: string, resolver: (widget: CustomEditorWidget) => Promise<void>): Disposable {
if (this.resolvers.has(viewType)) {
throw new Error(`Resolver for ${viewType} already registered`);
}
for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) {
if (editorWidget.viewType === viewType) {
resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose());
this.pendingEditors.delete(editorWidget);
}
}
this.resolvers.set(viewType, resolver);
return Disposable.create(() => this.resolvers.delete(viewType));
}
}

View File

@@ -0,0 +1,68 @@
// *****************************************************************************
// Copyright (C) 2023 Red Hat, Inc. 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 { IDataTransferItem, IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer';
import { DataTransferDTO, DataTransferItemDTO } from '../../../common/plugin-api-rpc-model';
import { URI } from '../../../plugin/types-impl';
export namespace DataTransferItem {
export async function from(mime: string, item: IDataTransferItem): Promise<DataTransferItemDTO> {
const stringValue = await item.asString();
if (mime === 'text/uri-list') {
return {
asString: '',
fileData: undefined,
uriListData: serializeUriList(stringValue),
};
}
const fileValue = item.asFile();
return {
asString: stringValue,
fileData: fileValue ? { id: fileValue.id, name: fileValue.name, uri: fileValue.uri } : undefined,
};
}
function serializeUriList(stringValue: string): ReadonlyArray<string | URI> {
return stringValue.split('\r\n').map(part => {
if (part.startsWith('#')) {
return part;
}
try {
return URI.parse(part);
} catch {
// noop
}
return part;
});
}
}
export namespace DataTransfer {
export async function toDataTransferDTO(value: IReadonlyVSDataTransfer): Promise<DataTransferDTO> {
return {
items: await Promise.all(
Array.from(value)
.map(
async ([mime, item]) => [mime, await DataTransferItem.from(mime, item)]
)
)
};
}
}

View File

@@ -0,0 +1,400 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../../common/rpc-protocol';
import {
DebugConfigurationProviderDescriptor,
DebugMain,
DebugExt,
MAIN_RPC_CONTEXT
} from '../../../common/plugin-api-rpc';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { Breakpoint, DebugStackFrameDTO, DebugThreadDTO, WorkspaceFolder } from '../../../common/plugin-api-rpc-model';
import { LabelProvider } from '@theia/core/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { BreakpointManager, BreakpointsChangeEvent } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint';
import { URI as Uri } from '@theia/core/shared/vscode-uri';
import { SourceBreakpoint, FunctionBreakpoint } from '@theia/debug/lib/browser/breakpoint/breakpoint-marker';
import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { DebugProtocol } from '@vscode/debugprotocol';
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { DebugPreferences } from '@theia/debug/lib/common/debug-preferences';
import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution';
import { PluginDebugConfigurationProvider } from './plugin-debug-configuration-provider';
import { PluginDebugSessionContributionRegistrator, PluginDebugSessionContributionRegistry } from './plugin-debug-session-contribution-registry';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { PluginDebugSessionFactory } from './plugin-debug-session-factory';
import { PluginDebugService } from './plugin-debug-service';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { ConsoleSessionManager } from '@theia/console/lib/browser/console-session-manager';
import { DebugConsoleSession } from '@theia/debug/lib/browser/console/debug-console-session';
import { CommandService, ContributionProvider } from '@theia/core/lib/common';
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
import { ConnectionImpl } from '../../../common/connection';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { DebugSessionOptions as TheiaDebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
import { DebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
import { DebugThread } from '@theia/debug/lib/browser/model/debug-thread';
import { TestService } from '@theia/test/lib/browser/test-service';
export class DebugMainImpl implements DebugMain, Disposable {
private readonly debugExt: DebugExt;
private readonly sessionManager: DebugSessionManager;
private readonly labelProvider: LabelProvider;
private readonly editorManager: EditorManager;
private readonly breakpointsManager: BreakpointManager;
private readonly consoleSessionManager: ConsoleSessionManager;
private readonly configurationManager: DebugConfigurationManager;
private readonly terminalService: TerminalService;
private readonly messages: MessageClient;
private readonly outputChannelManager: OutputChannelManager;
private readonly debugPreferences: DebugPreferences;
private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator;
private readonly pluginDebugService: PluginDebugService;
private readonly fileService: FileService;
private readonly pluginService: HostedPluginSupport;
private readonly debugContributionProvider: ContributionProvider<DebugContribution>;
private readonly testService: TestService;
private readonly workspaceService: WorkspaceService;
private readonly commandService: CommandService;
private readonly debuggerContributions = new Map<string, DisposableCollection>();
private readonly configurationProviders = new Map<number, DisposableCollection>();
private readonly toDispose = new DisposableCollection();
constructor(rpc: RPCProtocol, readonly connectionMain: ConnectionImpl, container: interfaces.Container) {
this.debugExt = rpc.getProxy(MAIN_RPC_CONTEXT.DEBUG_EXT);
this.sessionManager = container.get(DebugSessionManager);
this.labelProvider = container.get(LabelProvider);
this.editorManager = container.get(EditorManager);
this.breakpointsManager = container.get(BreakpointManager);
this.consoleSessionManager = container.get(ConsoleSessionManager);
this.configurationManager = container.get(DebugConfigurationManager);
this.terminalService = container.get(TerminalService);
this.messages = container.get(MessageClient);
this.outputChannelManager = container.get(OutputChannelManager);
this.debugPreferences = container.get(DebugPreferences);
this.pluginDebugService = container.get(PluginDebugService);
this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry);
this.debugContributionProvider = container.getNamed(ContributionProvider, DebugContribution);
this.fileService = container.get(FileService);
this.pluginService = container.get(HostedPluginSupport);
this.testService = container.get(TestService);
this.workspaceService = container.get(WorkspaceService);
this.commandService = container.get(CommandService);
const fireDidChangeBreakpoints = ({ added, removed, changed }: BreakpointsChangeEvent<SourceBreakpoint | FunctionBreakpoint>) => {
this.debugExt.$breakpointsDidChange(
this.toTheiaPluginApiBreakpoints(added),
removed.map(b => b.id),
this.toTheiaPluginApiBreakpoints(changed)
);
};
this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getBreakpoints()), [], []);
this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getFunctionBreakpoints()), [], []);
this.toDispose.pushAll([
this.breakpointsManager.onDidChangeBreakpoints(fireDidChangeBreakpoints),
this.breakpointsManager.onDidChangeFunctionBreakpoints(fireDidChangeBreakpoints),
this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)),
this.sessionManager.onDidStartDebugSession(debugSession => this.debugExt.$sessionDidStart(debugSession.id)),
this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)),
this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)),
this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)),
this.sessionManager.onDidFocusStackFrame(stackFrame => this.debugExt.$onDidChangeActiveFrame(this.toDebugStackFrameDTO(stackFrame))),
this.sessionManager.onDidFocusThread(debugThread => this.debugExt.$onDidChangeActiveThread(this.toDebugThreadDTO(debugThread))),
]);
}
dispose(): void {
this.toDispose.dispose();
}
async $appendToDebugConsole(value: string): Promise<void> {
const session = this.consoleSessionManager.selectedSession;
if (session instanceof DebugConsoleSession) {
session.append(value);
}
}
async $appendLineToDebugConsole(value: string): Promise<void> {
const session = this.consoleSessionManager.selectedSession;
if (session instanceof DebugConsoleSession) {
session.appendLine(value);
}
}
async $registerDebuggerContribution(description: DebuggerDescription): Promise<void> {
const debugType = description.type;
const terminalOptionsExt = await this.debugExt.$getTerminalCreationOptions(debugType);
if (this.toDispose.disposed) {
return;
}
const debugSessionFactory = new PluginDebugSessionFactory(
this.terminalService,
this.editorManager,
this.breakpointsManager,
this.labelProvider,
this.messages,
this.outputChannelManager,
this.debugPreferences,
async (sessionId: string) => {
const connection = await this.connectionMain.ensureConnection(sessionId);
return connection;
},
this.fileService,
terminalOptionsExt,
this.debugContributionProvider,
this.testService,
this.workspaceService,
this.commandService,
);
const toDispose = new DisposableCollection(
Disposable.create(() => this.debuggerContributions.delete(debugType))
);
this.debuggerContributions.set(debugType, toDispose);
toDispose.pushAll([
this.pluginDebugService.registerDebugAdapterContribution(
new PluginDebugAdapterContribution(description, this.debugExt, this.pluginService)
),
this.sessionContributionRegistrator.registerDebugSessionContribution({
debugType: description.type,
debugSessionFactory: () => debugSessionFactory
})
]);
this.toDispose.push(Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType)));
}
async $unregisterDebuggerConfiguration(debugType: string): Promise<void> {
const disposable = this.debuggerContributions.get(debugType);
if (disposable) {
disposable.dispose();
}
}
$registerDebugConfigurationProvider(description: DebugConfigurationProviderDescriptor): void {
const handle = description.handle;
const toDispose = new DisposableCollection(
Disposable.create(() => this.configurationProviders.delete(handle))
);
this.configurationProviders.set(handle, toDispose);
toDispose.push(
this.pluginDebugService.registerDebugConfigurationProvider(new PluginDebugConfigurationProvider(description, this.debugExt))
);
this.toDispose.push(Disposable.create(() => this.$unregisterDebugConfigurationProvider(handle)));
}
async $unregisterDebugConfigurationProvider(handle: number): Promise<void> {
const disposable = this.configurationProviders.get(handle);
if (disposable) {
disposable.dispose();
}
}
async $addBreakpoints(breakpoints: Breakpoint[]): Promise<void> {
const newBreakpoints = new Map<string, Breakpoint>();
breakpoints.forEach(b => newBreakpoints.set(b.id, b));
this.breakpointsManager.findMarkers({
dataFilter: data => {
// install only new breakpoints
if (newBreakpoints.has(data.id)) {
newBreakpoints.delete(data.id);
}
return false;
}
});
let addedFunctionBreakpoints = false;
const functionBreakpoints = this.breakpointsManager.getFunctionBreakpoints();
for (const breakpoint of functionBreakpoints) {
// install only new breakpoints
if (newBreakpoints.has(breakpoint.id)) {
newBreakpoints.delete(breakpoint.id);
}
}
for (const breakpoint of newBreakpoints.values()) {
if (breakpoint.location) {
const location = breakpoint.location;
const column = breakpoint.location.range.startColumn;
this.breakpointsManager.addBreakpoint({
id: breakpoint.id,
uri: Uri.revive(location.uri).toString(),
enabled: breakpoint.enabled,
raw: {
line: breakpoint.location.range.startLineNumber + 1,
column: column > 0 ? column + 1 : undefined,
condition: breakpoint.condition,
hitCondition: breakpoint.hitCondition,
logMessage: breakpoint.logMessage
}
});
} else if (breakpoint.functionName) {
addedFunctionBreakpoints = true;
functionBreakpoints.push({
id: breakpoint.id,
enabled: breakpoint.enabled,
raw: {
name: breakpoint.functionName
}
});
}
}
if (addedFunctionBreakpoints) {
this.breakpointsManager.setFunctionBreakpoints(functionBreakpoints);
}
}
async $getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise<DebugProtocol.Breakpoint | undefined> {
const session = this.sessionManager.getSession(sessionId);
if (session) {
return session.getBreakpoint(breakpointId)?.raw;
} else {
throw new Error(`Debug session '${sessionId}' not found`);
}
}
async $removeBreakpoints(breakpoints: string[]): Promise<void> {
const { labelProvider, breakpointsManager, editorManager } = this;
const session = this.sessionManager.currentSession;
const ids = new Set<string>(breakpoints);
for (const origin of this.breakpointsManager.findMarkers({ dataFilter: data => ids.has(data.id) })) {
const breakpoint = new DebugSourceBreakpoint(origin.data, { labelProvider, breakpoints: breakpointsManager, editorManager, session }, this.commandService);
breakpoint.remove();
}
for (const origin of this.breakpointsManager.getFunctionBreakpoints()) {
if (ids.has(origin.id)) {
const breakpoint = new DebugFunctionBreakpoint(origin, { labelProvider, breakpoints: breakpointsManager, editorManager, session });
breakpoint.remove();
}
}
}
async $customRequest(sessionId: string, command: string, args?: any): Promise<DebugProtocol.Response> {
const session = this.sessionManager.getSession(sessionId);
if (session) {
return session.sendCustomRequest(command, args);
}
throw new Error(`Debug session '${sessionId}' not found`);
}
async $startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, options: DebugSessionOptions): Promise<boolean> {
// search for matching options
let sessionOptions: TheiaDebugSessionOptions | undefined;
if (typeof nameOrConfiguration === 'string') {
for (const configOptions of this.configurationManager.all) {
if (configOptions.name === nameOrConfiguration) {
sessionOptions = configOptions;
}
}
} else {
sessionOptions = {
name: nameOrConfiguration.name,
configuration: nameOrConfiguration
};
}
if (!sessionOptions) {
console.error(`There is no debug configuration for ${nameOrConfiguration}`);
return false;
}
// translate given extra data
const workspaceFolderUri = folder && Uri.revive(folder.uri).toString();
if (TheiaDebugSessionOptions.isConfiguration(sessionOptions)) {
sessionOptions = { ...sessionOptions, configuration: { ...sessionOptions.configuration, ...options }, workspaceFolderUri };
} else {
sessionOptions = { ...sessionOptions, ...options, workspaceFolderUri };
}
sessionOptions.testRun = options.testRun;
// start options
const session = await this.sessionManager.start(sessionOptions);
return !!session;
}
async $stopDebugging(sessionId?: string): Promise<void> {
if (sessionId) {
const session = this.sessionManager.getSession(sessionId);
return this.sessionManager.terminateSession(session);
}
// Terminate all sessions if no session is provided.
for (const session of this.sessionManager.sessions) {
this.sessionManager.terminateSession(session);
}
}
private toDebugStackFrameDTO(stackFrame: DebugStackFrame | undefined): DebugStackFrameDTO | undefined {
return stackFrame ? {
sessionId: stackFrame.session.id,
frameId: stackFrame.frameId,
threadId: stackFrame.thread.threadId
} : undefined;
}
private toDebugThreadDTO(debugThread: DebugThread | undefined): DebugThreadDTO | undefined {
return debugThread ? {
sessionId: debugThread.session.id,
threadId: debugThread.threadId
} : undefined;
}
private toTheiaPluginApiBreakpoints(breakpoints: (SourceBreakpoint | FunctionBreakpoint)[]): Breakpoint[] {
return breakpoints.map(b => this.toTheiaPluginApiBreakpoint(b));
}
private toTheiaPluginApiBreakpoint(breakpoint: SourceBreakpoint | FunctionBreakpoint): Breakpoint {
if ('uri' in breakpoint) {
const raw = breakpoint.raw;
return {
id: breakpoint.id,
enabled: breakpoint.enabled,
condition: breakpoint.raw.condition,
hitCondition: breakpoint.raw.hitCondition,
logMessage: raw.logMessage,
location: {
uri: Uri.parse(breakpoint.uri),
range: {
startLineNumber: raw.line - 1,
startColumn: (raw.column || 1) - 1,
endLineNumber: raw.line - 1,
endColumn: (raw.column || 1) - 1
}
}
};
}
return {
id: breakpoint.id,
enabled: breakpoint.enabled,
functionName: breakpoint.raw.name
};
}
}

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DebugExt } from '../../../common/plugin-api-rpc';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { MaybePromise } from '@theia/core/lib/common/types';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
/**
* Plugin [DebugAdapterContribution](#DebugAdapterContribution).
*/
export class PluginDebugAdapterContribution {
constructor(
protected readonly description: DebuggerDescription,
protected readonly debugExt: DebugExt,
protected readonly pluginService: HostedPluginSupport) { }
get type(): string {
return this.description.type;
}
get label(): MaybePromise<string | undefined> {
return this.description.label;
}
async createDebugSession(config: DebugConfiguration, workspaceFolder: string | undefined): Promise<string> {
await this.pluginService.activateByDebug('onDebugAdapterProtocolTracker', config.type);
return this.debugExt.$createDebugSession(config, workspaceFolder);
}
async terminateDebugSession(sessionId: string): Promise<void> {
this.debugExt.$terminateDebugSession(sessionId);
}
}

View File

@@ -0,0 +1,69 @@
// *****************************************************************************
// 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 {
DebugConfigurationProvider,
DebugConfigurationProviderDescriptor,
DebugConfigurationProviderTriggerKind,
DebugExt
} from '../../../common/plugin-api-rpc';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
export class PluginDebugConfigurationProvider implements DebugConfigurationProvider {
/**
* After https://github.com/eclipse-theia/theia/pull/13196, the debug config handles might change.
* Store the original handle to be able to call the extension host when getting by handle.
*/
protected readonly originalHandle: number;
public handle: number;
public type: string;
public triggerKind: DebugConfigurationProviderTriggerKind;
provideDebugConfigurations: (folder: string | undefined) => Promise<DebugConfiguration[]>;
resolveDebugConfiguration: (
folder: string | undefined,
debugConfiguration: DebugConfiguration
) => Promise<DebugConfiguration | undefined | null>;
resolveDebugConfigurationWithSubstitutedVariables: (
folder: string | undefined,
debugConfiguration: DebugConfiguration
) => Promise<DebugConfiguration | undefined | null>;
constructor(
description: DebugConfigurationProviderDescriptor,
protected readonly debugExt: DebugExt
) {
this.handle = description.handle;
this.originalHandle = this.handle;
this.type = description.type;
this.triggerKind = description.trigger;
if (description.provideDebugConfiguration) {
this.provideDebugConfigurations = async (folder: string | undefined) => this.debugExt.$provideDebugConfigurationsByHandle(this.originalHandle, folder);
}
if (description.resolveDebugConfigurations) {
this.resolveDebugConfiguration =
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
this.debugExt.$resolveDebugConfigurationByHandle(this.originalHandle, folder, debugConfiguration);
}
if (description.resolveDebugConfigurationWithSubstitutedVariables) {
this.resolveDebugConfigurationWithSubstitutedVariables =
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
this.debugExt.$resolveDebugConfigurationWithSubstitutedVariablesByHandle(this.originalHandle, folder, debugConfiguration);
}
}
}

View File

@@ -0,0 +1,432 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DebuggerDescription, DebugPath, DebugService } from '@theia/debug/lib/common/debug-service';
import debounce = require('@theia/core/shared/lodash.debounce');
import { deepClone, Emitter, Event, nls } from '@theia/core';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution';
import { PluginDebugConfigurationProvider } from './plugin-debug-configuration-provider';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types';
import { DebugConfigurationProviderTriggerKind } from '../../../common/plugin-api-rpc';
import { DebuggerContribution } from '../../../common/plugin-protocol';
import { DebugRequestTypes } from '@theia/debug/lib/browser/debug-session-connection';
import * as theia from '@theia/plugin';
/**
* Debug service to work with plugin and extension contributions.
*/
@injectable()
export class PluginDebugService implements DebugService {
protected readonly onDidChangeDebuggersEmitter = new Emitter<void>();
get onDidChangeDebuggers(): Event<void> {
return this.onDidChangeDebuggersEmitter.event;
}
protected readonly debuggers: DebuggerContribution[] = [];
protected readonly contributors = new Map<string, PluginDebugAdapterContribution>();
protected readonly configurationProviders = new Map<number, PluginDebugConfigurationProvider>();
protected readonly toDispose = new DisposableCollection(this.onDidChangeDebuggersEmitter);
protected readonly onDidChangeDebugConfigurationProvidersEmitter = new Emitter<void>();
get onDidChangeDebugConfigurationProviders(): Event<void> {
return this.onDidChangeDebugConfigurationProvidersEmitter.event;
}
// maps session and contribution
protected readonly sessionId2contrib = new Map<string, PluginDebugAdapterContribution>();
protected delegated: DebugService;
@inject(WebSocketConnectionProvider)
protected readonly connectionProvider: WebSocketConnectionProvider;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@postConstruct()
protected init(): void {
this.delegated = this.connectionProvider.createProxy<DebugService>(DebugPath);
this.toDispose.pushAll([
Disposable.create(() => this.delegated.dispose()),
Disposable.create(() => {
for (const sessionId of this.sessionId2contrib.keys()) {
const contrib = this.sessionId2contrib.get(sessionId)!;
contrib.terminateDebugSession(sessionId);
}
this.sessionId2contrib.clear();
})]);
}
registerDebugAdapterContribution(contrib: PluginDebugAdapterContribution): Disposable {
const { type } = contrib;
if (this.contributors.has(type)) {
console.warn(`Debugger with type '${type}' already registered.`);
return Disposable.NULL;
}
this.contributors.set(type, contrib);
return Disposable.create(() => this.unregisterDebugAdapterContribution(type));
}
unregisterDebugAdapterContribution(debugType: string): void {
this.contributors.delete(debugType);
}
// debouncing to send a single notification for multiple registrations at initialization time
fireOnDidConfigurationProvidersChanged = debounce(() => {
this.onDidChangeDebugConfigurationProvidersEmitter.fire();
}, 100);
registerDebugConfigurationProvider(provider: PluginDebugConfigurationProvider): Disposable {
if (this.configurationProviders.has(provider.handle)) {
const configuration = this.configurationProviders.get(provider.handle);
if (configuration && configuration.type !== provider.type) {
console.warn(`Different debug configuration provider with type '${configuration.type}' already registered.`);
provider.handle = this.configurationProviders.size;
}
}
const handle = provider.handle;
this.configurationProviders.set(handle, provider);
this.fireOnDidConfigurationProvidersChanged();
return Disposable.create(() => this.unregisterDebugConfigurationProvider(handle));
}
unregisterDebugConfigurationProvider(handle: number): void {
this.configurationProviders.delete(handle);
this.fireOnDidConfigurationProvidersChanged();
}
async debugTypes(): Promise<string[]> {
const debugTypes = new Set(await this.delegated.debugTypes());
for (const contribution of this.debuggers) {
debugTypes.add(contribution.type);
}
for (const debugType of this.contributors.keys()) {
debugTypes.add(debugType);
}
return [...debugTypes];
}
async provideDebugConfigurations(debugType: keyof DebugRequestTypes, workspaceFolderUri: string | undefined): Promise<theia.DebugConfiguration[]> {
const pluginProviders =
Array.from(this.configurationProviders.values()).filter(p => (
p.triggerKind === DebugConfigurationProviderTriggerKind.Initial &&
(p.type === debugType || p.type === '*') &&
p.provideDebugConfigurations
));
if (pluginProviders.length === 0) {
return this.delegated.provideDebugConfigurations(debugType, workspaceFolderUri);
}
const results: DebugConfiguration[] = [];
await Promise.all(pluginProviders.map(async p => {
const result = await p.provideDebugConfigurations(workspaceFolderUri);
if (result) {
results.push(...result);
}
}));
return results;
}
async fetchDynamicDebugConfiguration(name: string, providerType: string, folder?: string): Promise<DebugConfiguration | undefined> {
const pluginProviders =
Array.from(this.configurationProviders.values()).filter(p => (
p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic &&
p.type === providerType &&
p.provideDebugConfigurations
));
for (const provider of pluginProviders) {
const configurations = await provider.provideDebugConfigurations(folder);
for (const configuration of configurations) {
if (configuration.name === name) {
return configuration;
}
}
}
}
async provideDynamicDebugConfigurations(folder?: string): Promise<Record<string, DebugConfiguration[]>> {
const pluginProviders =
Array.from(this.configurationProviders.values()).filter(p => (
p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic &&
p.provideDebugConfigurations
));
const configurationsRecord: Record<string, DebugConfiguration[]> = {};
await Promise.all(pluginProviders.map(async provider => {
const configurations = await provider.provideDebugConfigurations(folder);
let configurationsPerType = configurationsRecord[provider.type];
configurationsPerType = configurationsPerType ? configurationsPerType.concat(configurations) : configurations;
if (configurationsPerType.length > 0) {
configurationsRecord[provider.type] = configurationsPerType;
}
}));
return configurationsRecord;
}
async resolveDebugConfiguration(
config: DebugConfiguration,
workspaceFolderUri: string | undefined
): Promise<DebugConfiguration | undefined | null> {
const allProviders = Array.from(this.configurationProviders.values());
const resolvers = allProviders
.filter(p => p.type === config.type && !!p.resolveDebugConfiguration)
.map(p => p.resolveDebugConfiguration);
// Append debug type '*' at the end
resolvers.push(
...allProviders
.filter(p => p.type === '*' && !!p.resolveDebugConfiguration)
.map(p => p.resolveDebugConfiguration)
);
const resolved = await this.resolveDebugConfigurationByResolversChain(config, workspaceFolderUri, resolvers);
return resolved ? this.delegated.resolveDebugConfiguration(resolved, workspaceFolderUri) : resolved;
}
async resolveDebugConfigurationWithSubstitutedVariables(
config: DebugConfiguration,
workspaceFolderUri: string | undefined
): Promise<DebugConfiguration | undefined | null> {
const allProviders = Array.from(this.configurationProviders.values());
const resolvers = allProviders
.filter(p => p.type === config.type && !!p.resolveDebugConfigurationWithSubstitutedVariables)
.map(p => p.resolveDebugConfigurationWithSubstitutedVariables);
// Append debug type '*' at the end
resolvers.push(
...allProviders
.filter(p => p.type === '*' && !!p.resolveDebugConfigurationWithSubstitutedVariables)
.map(p => p.resolveDebugConfigurationWithSubstitutedVariables)
);
const resolved = await this.resolveDebugConfigurationByResolversChain(config, workspaceFolderUri, resolvers);
return resolved
? this.delegated.resolveDebugConfigurationWithSubstitutedVariables(resolved, workspaceFolderUri)
: resolved;
}
protected async resolveDebugConfigurationByResolversChain(
config: DebugConfiguration,
workspaceFolderUri: string | undefined,
resolvers: ((
folder: string | undefined,
debugConfiguration: DebugConfiguration
) => Promise<DebugConfiguration | null | undefined>)[]
): Promise<DebugConfiguration | undefined | null> {
let resolved: DebugConfiguration | undefined | null = config;
for (const resolver of resolvers) {
try {
if (!resolved) {
// A provider has indicated to stop and process undefined or null as per specified in the vscode API
// https://code.visualstudio.com/api/references/vscode-api#DebugConfigurationProvider
break;
}
resolved = await resolver(workspaceFolderUri, resolved);
} catch (e) {
console.error(e);
}
}
return resolved;
}
registerDebugger(contribution: DebuggerContribution): Disposable {
this.debuggers.push(contribution);
return Disposable.create(() => {
const index = this.debuggers.indexOf(contribution);
if (index !== -1) {
this.debuggers.splice(index, 1);
}
});
}
async provideDebuggerVariables(debugType: string): Promise<CommandIdVariables> {
for (const contribution of this.debuggers) {
if (contribution.type === debugType) {
const variables = contribution.variables;
if (variables && Object.keys(variables).length > 0) {
return variables;
}
}
}
return {};
}
async getDebuggersForLanguage(language: string): Promise<DebuggerDescription[]> {
const debuggers = await this.delegated.getDebuggersForLanguage(language);
for (const contributor of this.debuggers) {
const languages = contributor.languages;
if (languages && languages.indexOf(language) !== -1) {
const { label, type } = contributor;
debuggers.push({ type, label: label || type });
}
}
return debuggers;
}
async getSchemaAttributes(debugType: string): Promise<IJSONSchema[]> {
let schemas = await this.delegated.getSchemaAttributes(debugType);
for (const contribution of this.debuggers) {
if (contribution.configurationAttributes &&
(contribution.type === debugType || contribution.type === '*' || debugType === '*')) {
schemas = schemas.concat(this.resolveSchemaAttributes(contribution.type, contribution.configurationAttributes));
}
}
return schemas;
}
protected resolveSchemaAttributes(type: string, configurationAttributes: { [request: string]: IJSONSchema }): IJSONSchema[] {
const taskSchema = {};
return Object.keys(configurationAttributes).map(request => {
const attributes: IJSONSchema = deepClone(configurationAttributes[request]);
const defaultRequired = ['name', 'type', 'request'];
attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired;
attributes.additionalProperties = false;
attributes.type = 'object';
if (!attributes.properties) {
attributes.properties = {};
}
const properties = attributes.properties;
properties['type'] = {
enum: [type],
description: nls.localizeByDefault('Type of configuration.'),
pattern: '^(?!node2)',
errorMessage: nls.localizeByDefault('The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled.'),
patternErrorMessage: nls.localizeByDefault('"node2" is no longer supported, use "node" instead and set the "protocol" attribute to "inspector".')
};
properties['name'] = {
type: 'string',
description: nls.localizeByDefault('Name of configuration; appears in the launch configuration dropdown menu.'),
default: 'Launch'
};
properties['request'] = {
enum: [request],
description: nls.localizeByDefault('Request type of configuration. Can be "launch" or "attach".'),
};
properties['debugServer'] = {
type: 'number',
description: nls.localizeByDefault(
'For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode'
),
default: 4711
};
properties['preLaunchTask'] = {
anyOf: [taskSchema, {
type: ['string'],
}],
default: '',
description: nls.localizeByDefault('Task to run before debug session starts.')
};
properties['postDebugTask'] = {
anyOf: [taskSchema, {
type: ['string'],
}],
default: '',
description: nls.localizeByDefault('Task to run after debug session ends.')
};
properties['internalConsoleOptions'] = {
enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'],
default: 'openOnFirstSessionStart',
description: nls.localizeByDefault('Controls when the internal Debug Console should open.')
};
properties['suppressMultipleSessionWarning'] = {
type: 'boolean',
description: nls.localizeByDefault('Disable the warning when trying to start the same debug configuration more than once.'),
default: true
};
const osProperties = Object.assign({}, properties);
properties['windows'] = {
type: 'object',
description: nls.localizeByDefault('Windows specific launch configuration attributes.'),
properties: osProperties
};
properties['osx'] = {
type: 'object',
description: nls.localizeByDefault('OS X specific launch configuration attributes.'),
properties: osProperties
};
properties['linux'] = {
type: 'object',
description: nls.localizeByDefault('Linux specific launch configuration attributes.'),
properties: osProperties
};
Object.keys(attributes.properties).forEach(name => {
// Use schema allOf property to get independent error reporting #21113
attributes!.properties![name].pattern = attributes!.properties![name].pattern || '^(?!.*\\$\\{(env|config|command)\\.)';
attributes!.properties![name].patternErrorMessage = attributes!.properties![name].patternErrorMessage ||
nls.localizeByDefault("'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead.");
});
return attributes;
});
}
async getConfigurationSnippets(): Promise<IJSONSchemaSnippet[]> {
let snippets = await this.delegated.getConfigurationSnippets();
for (const contribution of this.debuggers) {
if (contribution.configurationSnippets) {
snippets = snippets.concat(contribution.configurationSnippets);
}
}
return snippets;
}
async createDebugSession(config: DebugConfiguration, workspaceFolder: string | undefined): Promise<string> {
const contributor = this.contributors.get(config.type);
if (contributor) {
const sessionId = await contributor.createDebugSession(config, workspaceFolder);
this.sessionId2contrib.set(sessionId, contributor);
return sessionId;
} else {
return this.delegated.createDebugSession(config, workspaceFolder);
}
}
async terminateDebugSession(sessionId: string): Promise<void> {
const contributor = this.sessionId2contrib.get(sessionId);
if (contributor) {
this.sessionId2contrib.delete(sessionId);
return contributor.terminateDebugSession(sessionId);
} else {
return this.delegated.terminateDebugSession(sessionId);
}
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,76 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DebugSessionContributionRegistry, DebugSessionContribution } from '@theia/debug/lib/browser/debug-session-contribution';
import { injectable, inject, named, postConstruct } from '@theia/core/shared/inversify';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { Disposable } from '@theia/core/lib/common/disposable';
/**
* Debug session contribution registrator.
*/
export interface PluginDebugSessionContributionRegistrator {
/**
* Registers [DebugSessionContribution](#DebugSessionContribution).
* @param contrib contribution
*/
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable;
/**
* Unregisters [DebugSessionContribution](#DebugSessionContribution).
* @param debugType the debug type
*/
unregisterDebugSessionContribution(debugType: string): void;
}
/**
* Plugin debug session contribution registry implementation with functionality
* to register / unregister plugin contributions.
*/
@injectable()
export class PluginDebugSessionContributionRegistry implements DebugSessionContributionRegistry, PluginDebugSessionContributionRegistrator {
protected readonly contribs = new Map<string, DebugSessionContribution>();
@inject(ContributionProvider) @named(DebugSessionContribution)
protected readonly contributions: ContributionProvider<DebugSessionContribution>;
@postConstruct()
protected init(): void {
for (const contrib of this.contributions.getContributions()) {
this.contribs.set(contrib.debugType, contrib);
}
}
get(debugType: string): DebugSessionContribution | undefined {
return this.contribs.get(debugType);
}
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable {
const { debugType } = contrib;
if (this.contribs.has(debugType)) {
console.warn(`Debug session contribution already registered for ${debugType}`);
return Disposable.NULL;
}
this.contribs.set(debugType, contrib);
return Disposable.create(() => this.unregisterDebugSessionContribution(debugType));
}
unregisterDebugSessionContribution(debugType: string): void {
this.contribs.delete(debugType);
}
}

View File

@@ -0,0 +1,121 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { DebugPreferences } from '@theia/debug/lib/common/debug-preferences';
import { DebugConfigurationSessionOptions, TestRunReference } from '@theia/debug/lib/browser/debug-session-options';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { TerminalOptionsExt } from '../../../common/plugin-api-rpc';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginChannel } from '../../../common/connection';
import { TestService } from '@theia/test/lib/browser/test-service';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { CommandService } from '@theia/core';
export class PluginDebugSession extends DebugSession {
constructor(
override readonly id: string,
override readonly options: DebugConfigurationSessionOptions,
override readonly parentSession: DebugSession | undefined,
testService: TestService,
testRun: TestRunReference | undefined,
sessionManager: DebugSessionManager,
protected override readonly connection: DebugSessionConnection,
protected override readonly terminalServer: TerminalService,
protected override readonly editorManager: EditorManager,
protected override readonly breakpoints: BreakpointManager,
protected override readonly labelProvider: LabelProvider,
protected override readonly messages: MessageClient,
protected override readonly fileService: FileService,
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
protected override readonly workspaceService: WorkspaceService,
debugPreferences: DebugPreferences,
protected override readonly commandService: CommandService) {
super(id, options, parentSession, testService, testRun, sessionManager, connection, terminalServer, editorManager, breakpoints,
labelProvider, messages, fileService, debugContributionProvider,
workspaceService, debugPreferences, commandService);
}
protected override async doCreateTerminal(terminalWidgetOptions: TerminalWidgetOptions): Promise<TerminalWidget> {
terminalWidgetOptions = Object.assign({}, terminalWidgetOptions, this.terminalOptionsExt);
return super.doCreateTerminal(terminalWidgetOptions);
}
}
/**
* Session factory for a client debug session that communicates with debug adapter contributed as plugin.
* The main difference is to use a connection factory that creates [Channel](#Channel) over Rpc channel.
*/
export class PluginDebugSessionFactory extends DefaultDebugSessionFactory {
constructor(
protected override readonly terminalService: TerminalService,
protected override readonly editorManager: EditorManager,
protected override readonly breakpoints: BreakpointManager,
protected override readonly labelProvider: LabelProvider,
protected override readonly messages: MessageClient,
protected override readonly outputChannelManager: OutputChannelManager,
protected override readonly debugPreferences: DebugPreferences,
protected readonly connectionFactory: (sessionId: string) => Promise<PluginChannel>,
protected override readonly fileService: FileService,
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
protected override readonly testService: TestService,
protected override readonly workspaceService: WorkspaceService,
protected override readonly commandService: CommandService,
) {
super();
}
override get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession {
const connection = new DebugSessionConnection(
sessionId,
this.connectionFactory,
this.getTraceOutputChannel());
return new PluginDebugSession(
sessionId,
options,
parentSession,
this.testService,
options.testRun,
manager,
connection,
this.terminalService,
this.editorManager,
this.breakpoints,
this.labelProvider,
this.messages,
this.fileService,
this.terminalOptionsExt,
this.debugContributionProvider,
this.workspaceService,
this.debugPreferences,
this.commandService
);
}
}

View File

@@ -0,0 +1,146 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. 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 {
DecorationData,
DecorationRequest,
DecorationsExt,
DecorationsMain,
MAIN_RPC_CONTEXT
} from '../../../common/plugin-api-rpc';
import { interfaces } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { UriComponents } from '../../../common/uri-components';
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import URI from '@theia/core/lib/common/uri';
import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/api/browser/mainThreadDecorations.ts#L85
class DecorationRequestsQueue {
private idPool = 0;
private requests = new Map<number, DecorationRequest>();
private resolver = new Map<number, (data: DecorationData) => void>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private timer: any;
constructor(
private readonly proxy: DecorationsExt,
private readonly handle: number
) {
}
enqueue(uri: URI, token: CancellationToken): Promise<DecorationData> {
const id = ++this.idPool;
const result = new Promise<DecorationData>(resolve => {
this.requests.set(id, { id, uri: VSCodeURI.parse(uri.toString()) });
this.resolver.set(id, resolve);
this.processQueue();
});
token.onCancellationRequested(() => {
this.requests.delete(id);
this.resolver.delete(id);
});
return result;
}
private processQueue(): void {
if (typeof this.timer === 'number') {
// already queued
return;
}
this.timer = setTimeout(() => {
// make request
const requests = this.requests;
const resolver = this.resolver;
this.proxy.$provideDecorations(this.handle, [...requests.values()], CancellationToken.None).then(data => {
for (const [id, resolve] of resolver) {
resolve(data[id]);
}
});
// reset
this.requests = new Map();
this.resolver = new Map();
this.timer = undefined;
}, 0);
}
}
export class DecorationsMainImpl implements DecorationsMain, Disposable {
private readonly proxy: DecorationsExt;
private readonly providers = new Map<number, [Emitter<URI[]>, Disposable]>();
private readonly decorationsService: DecorationsService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT);
this.decorationsService = container.get(DecorationsService);
}
dispose(): void {
this.providers.forEach(value => value.forEach(v => v.dispose()));
this.providers.clear();
}
async $registerDecorationProvider(handle: number): Promise<void> {
const emitter = new Emitter<URI[]>();
const queue = new DecorationRequestsQueue(this.proxy, handle);
const registration = this.decorationsService.registerDecorationsProvider({
onDidChange: emitter.event,
provideDecorations: async (uri, token) => {
const data = await queue.enqueue(uri, token);
if (!data) {
return undefined;
}
const [bubble, tooltip, letter, themeColor] = data;
return <Decoration>{
weight: 10,
bubble: bubble ?? false,
colorId: themeColor?.id,
tooltip,
letter
};
}
});
this.providers.set(handle, [emitter, registration]);
}
$onDidChange(handle: number, resources: UriComponents[]): void {
const providerSet = this.providers.get(handle);
if (providerSet) {
const [emitter] = providerSet;
emitter.fire(resources && resources.map(r => new URI(VSCodeURI.revive(r).toString())));
}
}
$unregisterDecorationProvider(handle: number): void {
const provider = this.providers.get(handle);
if (provider) {
provider.forEach(p => p.dispose());
this.providers.delete(handle);
}
}
}

View File

@@ -0,0 +1,185 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { RPCProtocol } from '../../common/rpc-protocol';
import { OpenDialogOptionsMain, SaveDialogOptionsMain, DialogsMain, UploadDialogOptionsMain } from '../../common/plugin-api-rpc';
import { OpenFileDialogProps, SaveFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { FileUploadService } from '@theia/filesystem/lib/common/upload/file-upload';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { nls } from '@theia/core';
export class DialogsMainImpl implements DialogsMain {
private workspaceService: WorkspaceService;
private fileService: FileService;
private environments: EnvVariablesServer;
private fileDialogService: FileDialogService;
private uploadService: FileUploadService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.workspaceService = container.get(WorkspaceService);
this.fileService = container.get(FileService);
this.environments = container.get(EnvVariablesServer);
this.fileDialogService = container.get(FileDialogService);
this.uploadService = container.get(FileUploadService);
}
protected async getRootStat(defaultUri: string | undefined): Promise<FileStat | undefined> {
let rootStat: FileStat | undefined;
// Try to use default URI as root
if (defaultUri) {
try {
rootStat = await this.fileService.resolve(new URI(defaultUri));
} catch {
rootStat = undefined;
}
// Try to use as root the parent folder of existing file URI/non existing URI
if (rootStat && !rootStat.isDirectory || !rootStat) {
try {
rootStat = await this.fileService.resolve(new URI(defaultUri).parent);
} catch {
rootStat = undefined;
}
}
}
// Try to use workspace service root if there is no pre-configured URI
if (!rootStat) {
rootStat = (await this.workspaceService.roots)[0];
}
// Try to use current user home if root folder is still not taken
if (!rootStat) {
const homeDirUri = await this.environments.getHomeDirUri();
try {
rootStat = await this.fileService.resolve(new URI(homeDirUri));
} catch { }
}
return rootStat;
}
async $showOpenDialog(options: OpenDialogOptionsMain): Promise<string[] | undefined> {
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
if (!rootStat) {
throw new Error('Unable to find the rootStat');
}
try {
const canSelectFiles = typeof options.canSelectFiles === 'boolean' ? options.canSelectFiles : true;
const canSelectFolders = typeof options.canSelectFolders === 'boolean' ? options.canSelectFolders : true;
let title = options.title;
if (!title) {
if (canSelectFiles && canSelectFolders) {
title = 'Open';
} else {
if (canSelectFiles) {
title = 'Open File';
} else {
title = 'Open Folder';
}
if (options.canSelectMany) {
title += '(s)';
}
}
}
// Create open file dialog props
const dialogProps = {
title: title,
openLabel: options.openLabel,
canSelectFiles: options.canSelectFiles,
canSelectFolders: options.canSelectFolders,
canSelectMany: options.canSelectMany,
filters: options.filters
} as OpenFileDialogProps;
const result = await this.fileDialogService.showOpenDialog(dialogProps, rootStat);
if (Array.isArray(result)) {
return result.map(uri => uri.path.toString());
} else {
return result ? [result].map(uri => uri.path.toString()) : undefined;
}
} catch (error) {
console.error(error);
}
return undefined;
}
async $showSaveDialog(options: SaveDialogOptionsMain): Promise<string | undefined> {
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
// File name field should be empty unless the URI is a file
let fileNameValue = '';
if (options.defaultUri) {
let defaultURIStat: FileStat | undefined;
try {
defaultURIStat = await this.fileService.resolve(new URI(options.defaultUri));
} catch { }
if (defaultURIStat && !defaultURIStat.isDirectory || !defaultURIStat) {
fileNameValue = new URI(options.defaultUri).path.base;
}
}
try {
// Create save file dialog props
const dialogProps = {
title: options.title ?? nls.localizeByDefault('Save'),
saveLabel: options.saveLabel,
filters: options.filters,
inputValue: fileNameValue
} as SaveFileDialogProps;
const result = await this.fileDialogService.showSaveDialog(dialogProps, rootStat);
if (result) {
return result.path.toString();
}
return undefined;
} catch (error) {
console.error(error);
}
return undefined;
}
async $showUploadDialog(options: UploadDialogOptionsMain): Promise<string[] | undefined> {
const rootStat = await this.getRootStat(options.defaultUri);
// Fail if root not fount
if (!rootStat) {
throw new Error('Failed to resolve base directory where files should be uploaded');
}
const uploadResult = await this.uploadService.upload(rootStat.resource.toString());
if (uploadResult) {
return uploadResult.uploaded;
}
return undefined;
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { Message } from '@theia/core/shared/@lumino/messaging';
import { codiconArray, Key } from '@theia/core/lib/browser';
import { AbstractDialog } from '@theia/core/lib/browser/dialogs';
import '../../../../src/main/browser/dialogs/style/modal-notification.css';
import { MainMessageItem, MainMessageOptions } from '../../../common/plugin-api-rpc';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { nls } from '@theia/core/lib/common/nls';
export enum MessageType {
Error = 'error',
Warning = 'warning',
Info = 'info'
}
const NOTIFICATION = 'modal-Notification';
const ICON = 'icon';
const TEXT = 'text';
const DETAIL = 'detail';
@injectable()
export class ModalNotification extends AbstractDialog<string | undefined> {
protected actionTitle: string | undefined;
constructor() {
super({ title: FrontendApplicationConfigProvider.get().applicationName });
}
protected override onCloseRequest(msg: Message): void {
this.actionTitle = undefined;
this.accept();
}
get value(): string | undefined {
return this.actionTitle;
}
showDialog(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise<string | undefined> {
this.contentNode.appendChild(this.createMessageNode(messageType, text, options, actions));
return this.open();
}
protected createMessageNode(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): HTMLElement {
const messageNode = document.createElement('div');
messageNode.classList.add(NOTIFICATION);
const iconContainer = messageNode.appendChild(document.createElement('div'));
iconContainer.classList.add(ICON);
const iconElement = iconContainer.appendChild(document.createElement('i'));
iconElement.classList.add(...this.toIconClass(messageType), messageType.toString());
const textContainer = messageNode.appendChild(document.createElement('div'));
textContainer.classList.add(TEXT);
const textElement = textContainer.appendChild(document.createElement('p'));
textElement.textContent = text;
if (options.detail) {
const detailContainer = textContainer.appendChild(document.createElement('div'));
detailContainer.classList.add(DETAIL);
const detailElement = detailContainer.appendChild(document.createElement('p'));
detailElement.textContent = options.detail;
}
actions.forEach((action: MainMessageItem, index: number) => {
const button = index === 0
? this.appendAcceptButton(action.title)
: this.createButton(action.title);
button.classList.add('main');
this.controlPanel.appendChild(button);
this.addKeyListener(button,
Key.ENTER,
() => {
this.actionTitle = action.title;
this.accept();
},
'click');
});
if (actions.length <= 0) {
this.appendAcceptButton();
} else if (!actions.some(action => action.isCloseAffordance === true)) {
this.appendCloseButton(nls.localizeByDefault('Close'));
}
return messageNode;
}
protected toIconClass(icon: MessageType): string[] {
if (icon === MessageType.Error) {
return codiconArray('error');
}
if (icon === MessageType.Warning) {
return codiconArray('warning');
}
return codiconArray('info');
}
}

View File

@@ -0,0 +1,123 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. 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
********************************************************************************/
.modal-Notification {
pointer-events: all;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: flex;
flex-direction: row;
-webkit-justify-content: center;
justify-content: center;
clear: both;
box-sizing: border-box;
position: relative;
min-width: 200px;
max-width: min(66vw, 800px);
background-color: var(--theia-editorWidget-background);
min-height: 35px;
margin-bottom: 1px;
color: var(--theia-editorWidget-foreground);
}
.modal-Notification .icon {
display: inline-block;
font-size: 20px;
padding: 5px 0;
width: 35px;
order: 1;
}
.modal-Notification .icon .codicon {
line-height: inherit;
vertical-align: middle;
font-size: calc(var(--theia-ui-padding) * 5);
color: var(--theia-editorInfo-foreground);
}
.modal-Notification .icon .error {
color: var(--theia-editorError-foreground);
}
.modal-Notification .icon .warning {
color: var(--theia-editorWarning-foreground);
}
.modal-Notification .text {
order: 2;
display: inline-block;
max-height: min(66vh, 600px);
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
align-self: center;
flex: 1 100%;
padding: 10px;
overflow: auto;
white-space: pre-wrap;
}
.modal-Notification .text > p {
margin: 0;
font-size: var(--theia-ui-font-size1);
font-family: var(--theia-ui-font-family);
vertical-align: middle;
}
.modal-Notification .buttons {
display: flex;
flex-direction: row;
order: 3;
white-space: nowrap;
align-self: flex-end;
height: 40px;
}
.modal-Notification .buttons > button {
background-color: var(--theia-button-background);
color: var(--theia-button-foreground);
border: none;
border-radius: 0;
text-align: center;
text-decoration: none;
display: inline-block;
padding: 0 10px;
margin: 0;
font-size: var(--theia-ui-font-size1);
outline: none;
cursor: pointer;
}
.modal-Notification .buttons > button:hover {
background-color: var(--theia-button-hoverBackground);
}
.modal-Notification .detail {
align-self: center;
order: 3;
flex: 1 100%;
color: var(--theia-descriptionForeground);
}
.modal-Notification .detail > p {
margin: calc(var(--theia-ui-padding) * 2) 0px 0px 0px;
}
.modal-Notification .text {
padding: calc(var(--theia-ui-padding) * 1.5);
}

View File

@@ -0,0 +1,294 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. 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 { DocumentsMain, MAIN_RPC_CONTEXT, DocumentsExt } from '../../common/plugin-api-rpc';
import { UriComponents } from '../../common/uri-components';
import { EditorsAndDocumentsMain } from './editors-and-documents-main';
import { DisposableCollection, Disposable, UntitledResourceResolver } from '@theia/core';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { RPCProtocol } from '../../common/rpc-protocol';
import { EditorModelService } from './text-editor-model-service';
import { EditorOpenerOptions, EncodingMode } from '@theia/editor/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { URI as CodeURI } from '@theia/core/shared/vscode-uri';
import { ApplicationShell, SaveReason } from '@theia/core/lib/browser';
import { TextDocumentShowOptions } from '../../common/plugin-api-rpc-model';
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { OpenerService } from '@theia/core/lib/browser/opener-service';
import { Reference } from '@theia/core/lib/common/reference';
import { dispose } from '../../common/disposable-util';
import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
import * as monaco from '@theia/monaco-editor-core';
import { TextDocumentChangeReason } from '../../plugin/types-impl';
import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class ModelReferenceCollection {
private data = new Array<{ length: number, dispose(): void }>();
private length = 0;
constructor(
private readonly maxAge: number = 1000 * 60 * 3,
private readonly maxLength: number = 1024 * 1024 * 80
) { }
dispose(): void {
this.data = dispose(this.data) || [];
}
add(ref: Reference<MonacoEditorModel>): void {
const length = ref.object.textEditorModel.getValueLength();
const handle = setTimeout(_dispose, this.maxAge);
const entry = { length, dispose: _dispose };
const self = this;
function _dispose(): void {
const idx = self.data.indexOf(entry);
if (idx >= 0) {
self.length -= length;
ref.dispose();
clearTimeout(handle);
self.data.splice(idx, 1);
}
};
this.data.push(entry);
this.length += length;
this.cleanup();
}
private cleanup(): void {
while (this.length > this.maxLength) {
this.data[0].dispose();
}
}
}
export class DocumentsMainImpl implements DocumentsMain, Disposable {
private readonly proxy: DocumentsExt;
private readonly syncedModels = new Map<string, Disposable>();
private readonly modelReferenceCache = new ModelReferenceCollection();
protected saveTimeout = 1750;
private readonly toDispose = new DisposableCollection(this.modelReferenceCache);
constructor(
editorsAndDocuments: EditorsAndDocumentsMain,
notebookDocuments: NotebookDocumentsMainImpl,
private readonly modelService: EditorModelService,
rpc: RPCProtocol,
private openerService: OpenerService,
private shell: ApplicationShell,
private untitledResourceResolver: UntitledResourceResolver,
private languageService: MonacoLanguages
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT);
this.toDispose.push(editorsAndDocuments);
this.toDispose.push(editorsAndDocuments.onDocumentAdd(documents => documents.forEach(this.onModelAdded, this)));
this.toDispose.push(editorsAndDocuments.onDocumentRemove(documents => documents.forEach(this.onModelRemoved, this)));
this.toDispose.push(modelService.onModelModeChanged(this.onModelChanged, this));
this.toDispose.push(notebookDocuments.onDidAddNotebookCellModel(this.onModelAdded, this));
this.toDispose.push(modelService.onModelSaved(m => {
this.proxy.$acceptModelSaved(m.textEditorModel.uri);
}));
this.toDispose.push(modelService.onModelWillSave(async e => {
const saveReason = e.options?.saveReason ?? SaveReason.Manual;
const edits = await this.proxy.$acceptModelWillSave(new URI(e.model.uri).toComponents(), saveReason.valueOf(), this.saveTimeout);
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
for (const edit of edits) {
const { range, text } = edit;
if (!range && !text) {
continue;
}
if (range && range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn && !edit.text) {
continue;
}
editOperations.push({
range: range ? monaco.Range.lift(range) : e.model.textEditorModel.getFullModelRange(),
/* eslint-disable-next-line no-null/no-null */
text: text || null,
forceMoveMarkers: edit.forceMoveMarkers
});
}
e.model.textEditorModel.applyEdits(editOperations);
}));
this.toDispose.push(modelService.onModelDirtyChanged(m => {
this.proxy.$acceptDirtyStateChanged(m.textEditorModel.uri, m.dirty);
}));
this.toDispose.push(modelService.onModelEncodingChanged(e => {
this.proxy.$acceptEncodingChanged(e.model.textEditorModel.uri, e.encoding);
}));
}
dispose(): void {
this.toDispose.dispose();
}
private onModelChanged(event: { model: MonacoEditorModel, oldModeId: string }): void {
const modelUrl = event.model.textEditorModel.uri;
if (this.syncedModels.has(modelUrl.toString())) {
this.proxy.$acceptModelModeChanged(modelUrl, event.oldModeId, event.model.languageId);
}
}
private onModelAdded(model: MonacoEditorModel): void {
const modelUri = model.textEditorModel.uri;
const key = modelUri.toString();
const toDispose = new DisposableCollection(
model.textEditorModel.onDidChangeContent(e =>
this.proxy.$acceptModelChanged(modelUri, {
eol: e.eol,
versionId: e.versionId,
reason: e.isRedoing ? TextDocumentChangeReason.Redo : e.isUndoing ? TextDocumentChangeReason.Undo : undefined,
changes: e.changes.map(c =>
({
text: c.text,
range: c.range,
rangeLength: c.rangeLength,
rangeOffset: c.rangeOffset
}))
}, model.dirty)
),
Disposable.create(() => this.syncedModels.delete(key))
);
this.syncedModels.set(key, toDispose);
this.toDispose.push(toDispose);
}
private onModelRemoved(url: monaco.Uri): void {
const model = this.syncedModels.get(url.toString());
if (model) {
model.dispose();
}
}
async $tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<UriComponents> {
const language = options?.language && this.languageService.getExtension(options.language);
const content = options?.content;
const encoding = options?.encoding;
const resource = await this.untitledResourceResolver.createUntitledResource(content, language, undefined, encoding);
return monaco.Uri.parse(resource.uri.toString());
}
async $tryShowDocument(uri: UriComponents, options?: TextDocumentShowOptions): Promise<void> {
// Removing try-catch block here makes it not possible to handle errors.
// Following message is appeared in browser console
// - Uncaught (in promise) Error: Cannot read property 'message' of undefined.
try {
const editorOptions = DocumentsMainImpl.toEditorOpenerOptions(this.shell, options);
const uriArg = new URI(CodeURI.revive(uri));
const opener = await this.openerService.getOpener(uriArg, editorOptions);
await opener.open(uriArg, editorOptions);
} catch (err) {
throw new Error(err);
}
}
async $trySaveDocument(uri: UriComponents): Promise<boolean> {
return this.modelService.save(new URI(CodeURI.revive(uri)));
}
async $tryOpenDocument(uri: UriComponents, encoding?: string): Promise<boolean> {
// Convert URI to Theia URI
const theiaUri = new URI(CodeURI.revive(uri));
// Create model reference
const ref = await this.modelService.createModelReference(theiaUri);
if (ref.object) {
// If we have encoding option, make sure to apply it
if (encoding && ref.object.setEncoding) {
try {
await ref.object.setEncoding(encoding, EncodingMode.Decode);
} catch (e) {
// If encoding fails, log error but continue
console.error(`Failed to set encoding ${encoding} for ${theiaUri.toString()}`, e);
}
}
this.modelReferenceCache.add(ref);
return true;
} else {
ref.dispose();
return false;
}
}
static toEditorOpenerOptions(shell: ApplicationShell, options?: TextDocumentShowOptions): EditorOpenerOptions | undefined {
if (!options) {
return undefined;
}
let range: Range | undefined;
if (options.selection) {
const selection = options.selection;
range = {
start: { line: selection.startLineNumber - 1, character: selection.startColumn - 1 },
end: { line: selection.endLineNumber - 1, character: selection.endColumn - 1 }
};
}
/* fall back to side group -> split relative to the active widget */
let widgetOptions: ApplicationShell.WidgetOptions | undefined = { mode: 'split-right' };
let viewColumn = options.viewColumn;
if (viewColumn === -2) {
/* show besides -> compute current column and adjust viewColumn accordingly */
const tabBars = shell.mainAreaTabBars;
const currentTabBar = shell.currentTabBar;
if (currentTabBar) {
const currentColumn = tabBars.indexOf(currentTabBar);
if (currentColumn > -1) {
// +2 because conversion from 0-based to 1-based index and increase of 1
viewColumn = currentColumn + 2;
}
}
}
if (viewColumn === undefined || viewColumn === -1) {
/* active group -> skip (default behaviour) */
widgetOptions = undefined;
} else if (viewColumn > 0 && shell.mainAreaTabBars.length > 0) {
const tabBars = shell.mainAreaTabBars;
if (viewColumn <= tabBars.length) {
// convert to zero-based index
const tabBar = tabBars[viewColumn - 1];
if (tabBar?.currentTitle) {
widgetOptions = { ref: tabBar.currentTitle.owner };
}
} else {
const tabBar = tabBars[tabBars.length - 1];
if (tabBar?.currentTitle) {
widgetOptions!.ref = tabBar.currentTitle.owner;
}
}
}
return {
selection: range,
mode: options.preserveFocus ? 'reveal' : 'activate',
preview: options.preview,
widgetOptions
};
}
}

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