deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
70
packages/plugin-ext/src/common/arrays.ts
Normal file
70
packages/plugin-ext/src/common/arrays.ts
Normal 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;
|
||||
}
|
||||
23
packages/plugin-ext/src/common/assert.ts
Normal file
23
packages/plugin-ext/src/common/assert.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
51
packages/plugin-ext/src/common/cache.ts
Normal file
51
packages/plugin-ext/src/common/cache.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
73
packages/plugin-ext/src/common/character-classifier.ts
Normal file
73
packages/plugin-ext/src/common/character-classifier.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
54
packages/plugin-ext/src/common/collections.ts
Normal file
54
packages/plugin-ext/src/common/collections.ts
Normal 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 };
|
||||
}
|
||||
|
||||
19
packages/plugin-ext/src/common/commands.ts
Normal file
19
packages/plugin-ext/src/common/commands.ts
Normal 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;
|
||||
}
|
||||
137
packages/plugin-ext/src/common/connection.ts
Normal file
137
packages/plugin-ext/src/common/connection.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
39
packages/plugin-ext/src/common/disposable-util.ts
Normal file
39
packages/plugin-ext/src/common/disposable-util.ts
Normal 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 [];
|
||||
}
|
||||
}
|
||||
74
packages/plugin-ext/src/common/editor-options.ts
Normal file
74
packages/plugin-ext/src/common/editor-options.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
19
packages/plugin-ext/src/common/env.ts
Normal file
19
packages/plugin-ext/src/common/env.ts
Normal 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[]
|
||||
}
|
||||
63
packages/plugin-ext/src/common/errors.ts
Normal file
63
packages/plugin-ext/src/common/errors.ts
Normal 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;
|
||||
}
|
||||
26
packages/plugin-ext/src/common/id-generator.ts
Normal file
26
packages/plugin-ext/src/common/id-generator.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
24
packages/plugin-ext/src/common/index.ts
Normal file
24
packages/plugin-ext/src/common/index.ts
Normal 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();
|
||||
34
packages/plugin-ext/src/common/language-pack-service.ts
Normal file
34
packages/plugin-ext/src/common/language-pack-service.ts
Normal 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>;
|
||||
}
|
||||
354
packages/plugin-ext/src/common/link-computer.ts
Normal file
354
packages/plugin-ext/src/common/link-computer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
137
packages/plugin-ext/src/common/lm-protocol.ts
Normal file
137
packages/plugin-ext/src/common/lm-protocol.ts
Normal 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>;
|
||||
}
|
||||
33
packages/plugin-ext/src/common/object-identifier.ts
Normal file
33
packages/plugin-ext/src/common/object-identifier.ts
Normal 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];
|
||||
}
|
||||
}
|
||||
50
packages/plugin-ext/src/common/objects.ts
Normal file
50
packages/plugin-ext/src/common/objects.ts
Normal 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;
|
||||
}
|
||||
158
packages/plugin-ext/src/common/paths-util.ts
Normal file
158
packages/plugin-ext/src/common/paths-util.ts
Normal 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;
|
||||
}
|
||||
937
packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Normal file
937
packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Normal 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
|
||||
}
|
||||
2813
packages/plugin-ext/src/common/plugin-api-rpc.ts
Normal file
2813
packages/plugin-ext/src/common/plugin-api-rpc.ts
Normal file
File diff suppressed because it is too large
Load Diff
115
packages/plugin-ext/src/common/plugin-ext-api-contribution.ts
Normal file
115
packages/plugin-ext/src/common/plugin-ext-api-contribution.ts
Normal 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;
|
||||
}
|
||||
92
packages/plugin-ext/src/common/plugin-identifiers.ts
Normal file
92
packages/plugin-ext/src/common/plugin-identifiers.ts
Normal 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) };
|
||||
}
|
||||
}
|
||||
1113
packages/plugin-ext/src/common/plugin-protocol.ts
Normal file
1113
packages/plugin-ext/src/common/plugin-protocol.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
packages/plugin-ext/src/common/reference-map.ts
Normal file
38
packages/plugin-ext/src/common/reference-map.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
315
packages/plugin-ext/src/common/rpc-protocol.ts
Normal file
315
packages/plugin-ext/src/common/rpc-protocol.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
182
packages/plugin-ext/src/common/semantic-tokens-dto.ts
Normal file
182
packages/plugin-ext/src/common/semantic-tokens-dto.ts
Normal 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
|
||||
};
|
||||
}
|
||||
168
packages/plugin-ext/src/common/test-types.ts
Normal file
168
packages/plugin-ext/src/common/test-types.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
129
packages/plugin-ext/src/common/types.ts
Normal file
129
packages/plugin-ext/src/common/types.ts
Normal 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;
|
||||
}
|
||||
37
packages/plugin-ext/src/common/uint.ts
Normal file
37
packages/plugin-ext/src/common/uint.ts
Normal 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;
|
||||
}
|
||||
84
packages/plugin-ext/src/common/uri-components.ts
Normal file
84
packages/plugin-ext/src/common/uri-components.ts
Normal 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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
635
packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Normal file
635
packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Normal 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>`;
|
||||
}
|
||||
|
||||
}
|
||||
52
packages/plugin-ext/src/hosted/browser/plugin-worker.ts
Normal file
52
packages/plugin-ext/src/hosted/browser/plugin-worker.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
29
packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts
Normal file
29
packages/plugin-ext/src/hosted/browser/worker/debug-stub.ts
Normal 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
212
packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
Normal file
212
packages/plugin-ext/src/hosted/browser/worker/worker-main.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
467
packages/plugin-ext/src/hosted/common/hosted-plugin.ts
Normal file
467
packages/plugin-ext/src/hosted/common/hosted-plugin.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
231
packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts
Normal file
231
packages/plugin-ext/src/hosted/node/hosted-plugin-process.ts
Normal 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}`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
102
packages/plugin-ext/src/hosted/node/hosted-plugin.ts
Normal file
102
packages/plugin-ext/src/hosted/node/hosted-plugin.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
65
packages/plugin-ext/src/hosted/node/metadata-scanner.ts
Normal file
65
packages/plugin-ext/src/hosted/node/metadata-scanner.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
112
packages/plugin-ext/src/hosted/node/plugin-activation-events.ts
Normal file
112
packages/plugin-ext/src/hosted/node/plugin-activation-events.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
39
packages/plugin-ext/src/hosted/node/plugin-host-logger.ts
Normal file
39
packages/plugin-ext/src/hosted/node/plugin-host-logger.ts
Normal 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);
|
||||
}
|
||||
77
packages/plugin-ext/src/hosted/node/plugin-host-module.ts
Normal file
77
packages/plugin-ext/src/hosted/node/plugin-host-module.ts
Normal 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();
|
||||
});
|
||||
82
packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts
Normal file
82
packages/plugin-ext/src/hosted/node/plugin-host-proxy.ts
Normal 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] };
|
||||
};
|
||||
}
|
||||
380
packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
Normal file
380
packages/plugin-ext/src/hosted/node/plugin-host-rpc.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
122
packages/plugin-ext/src/hosted/node/plugin-host.ts
Normal file
122
packages/plugin-ext/src/hosted/node/plugin-host.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
172
packages/plugin-ext/src/hosted/node/plugin-reader.ts
Normal file
172
packages/plugin-ext/src/hosted/node/plugin-reader.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
211
packages/plugin-ext/src/hosted/node/plugin-service.ts
Normal file
211
packages/plugin-ext/src/hosted/node/plugin-service.ts
Normal 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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
1073
packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Normal file
1073
packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Normal file
File diff suppressed because it is too large
Load Diff
403
packages/plugin-ext/src/main/browser/authentication-main.ts
Normal file
403
packages/plugin-ext/src/main/browser/authentication-main.ts
Normal 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);
|
||||
}
|
||||
38
packages/plugin-ext/src/main/browser/clipboard-main.ts
Normal file
38
packages/plugin-ext/src/main/browser/clipboard-main.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
130
packages/plugin-ext/src/main/browser/command-registry-main.ts
Normal file
130
packages/plugin-ext/src/main/browser/command-registry-main.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
104
packages/plugin-ext/src/main/browser/commands.ts
Normal file
104
packages/plugin-ext/src/main/browser/commands.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
484
packages/plugin-ext/src/main/browser/comments/comments-main.ts
Normal file
484
packages/plugin-ext/src/main/browser/comments/comments-main.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
400
packages/plugin-ext/src/main/browser/debug/debug-main.ts
Normal file
400
packages/plugin-ext/src/main/browser/debug/debug-main.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
packages/plugin-ext/src/main/browser/dialogs-main.ts
Normal file
185
packages/plugin-ext/src/main/browser/dialogs-main.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
294
packages/plugin-ext/src/main/browser/documents-main.ts
Normal file
294
packages/plugin-ext/src/main/browser/documents-main.ts
Normal 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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// *****************************************************************************
|
||||
// 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 * as monaco from '@theia/monaco-editor-core';
|
||||
import { type ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import {
|
||||
MAIN_RPC_CONTEXT,
|
||||
EditorsAndDocumentsExt,
|
||||
EditorsAndDocumentsDelta,
|
||||
ModelAddedData,
|
||||
TextEditorAddData,
|
||||
EditorPosition
|
||||
} from '../../common/plugin-api-rpc';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { EditorModelService } from './text-editor-model-service';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { TextEditorMain } from './text-editor-main';
|
||||
import { DisposableCollection, Emitter, URI } from '@theia/core';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
|
||||
import { TabsMainImpl } from './tabs/tabs-main';
|
||||
import { NotebookCellEditorService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser';
|
||||
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
||||
import { EncodingRegistry } from '@theia/core/lib/browser/encoding-registry';
|
||||
|
||||
export class EditorsAndDocumentsMain implements Disposable {
|
||||
|
||||
private readonly proxy: EditorsAndDocumentsExt;
|
||||
|
||||
private readonly stateComputer: EditorAndDocumentStateComputer;
|
||||
private readonly textEditors = new Map<string, TextEditorMain>();
|
||||
|
||||
private readonly modelService: EditorModelService;
|
||||
private readonly editorManager: EditorManager;
|
||||
private readonly saveResourceService: SaveableService;
|
||||
private readonly encodingRegistry: EncodingRegistry;
|
||||
|
||||
private readonly onTextEditorAddEmitter = new Emitter<TextEditorMain[]>();
|
||||
private readonly onTextEditorRemoveEmitter = new Emitter<string[]>();
|
||||
private readonly onDocumentAddEmitter = new Emitter<MonacoEditorModel[]>();
|
||||
private readonly onDocumentRemoveEmitter = new Emitter<monaco.Uri[]>();
|
||||
|
||||
readonly onTextEditorAdd = this.onTextEditorAddEmitter.event;
|
||||
readonly onTextEditorRemove = this.onTextEditorRemoveEmitter.event;
|
||||
readonly onDocumentAdd = this.onDocumentAddEmitter.event;
|
||||
readonly onDocumentRemove = this.onDocumentRemoveEmitter.event;
|
||||
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.textEditors.clear())
|
||||
);
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container, tabsMain: TabsMainImpl) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT);
|
||||
|
||||
this.editorManager = container.get(EditorManager);
|
||||
this.modelService = container.get(EditorModelService);
|
||||
this.saveResourceService = container.get(SaveableService);
|
||||
this.encodingRegistry = container.get(EncodingRegistry);
|
||||
|
||||
this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d),
|
||||
this.editorManager,
|
||||
container.get(NotebookCellEditorService),
|
||||
container.get(NotebookEditorWidgetService),
|
||||
this.modelService, tabsMain);
|
||||
this.toDispose.push(this.stateComputer);
|
||||
this.toDispose.push(this.onTextEditorAddEmitter);
|
||||
this.toDispose.push(this.onTextEditorRemoveEmitter);
|
||||
this.toDispose.push(this.onDocumentAddEmitter);
|
||||
this.toDispose.push(this.onDocumentRemoveEmitter);
|
||||
}
|
||||
|
||||
listen(): void {
|
||||
this.stateComputer.listen();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private onDelta(delta: EditorAndDocumentStateDelta): void {
|
||||
const removedEditors = new Array<string>();
|
||||
const addedEditors = new Array<TextEditorMain>();
|
||||
|
||||
const removedDocuments = delta.removedDocuments.map(d => d.textEditorModel.uri);
|
||||
|
||||
for (const editor of delta.addedEditors) {
|
||||
const textEditorMain = new TextEditorMain(editor.id, editor.editor.getControl().getModel()!, editor.editor);
|
||||
this.textEditors.set(editor.id, textEditorMain);
|
||||
this.toDispose.push(textEditorMain);
|
||||
addedEditors.push(textEditorMain);
|
||||
}
|
||||
|
||||
for (const { id } of delta.removedEditors) {
|
||||
const textEditorMain = this.textEditors.get(id);
|
||||
if (textEditorMain) {
|
||||
textEditorMain.dispose();
|
||||
this.textEditors.delete(id);
|
||||
removedEditors.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const deltaExt: EditorsAndDocumentsDelta = {};
|
||||
let empty = true;
|
||||
|
||||
if (delta.newActiveEditor !== undefined) {
|
||||
empty = false;
|
||||
deltaExt.newActiveEditor = delta.newActiveEditor;
|
||||
}
|
||||
if (removedDocuments.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.removedDocuments = removedDocuments;
|
||||
}
|
||||
if (removedEditors.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.removedEditors = removedEditors;
|
||||
}
|
||||
if (delta.addedDocuments.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.addedDocuments = delta.addedDocuments.map(d => this.toModelAddData(d));
|
||||
}
|
||||
if (delta.addedEditors.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.addedEditors = addedEditors.map(e => this.toTextEditorAddData(e));
|
||||
}
|
||||
|
||||
if (!empty) {
|
||||
this.proxy.$acceptEditorsAndDocumentsDelta(deltaExt);
|
||||
this.onDocumentRemoveEmitter.fire(removedDocuments);
|
||||
this.onDocumentAddEmitter.fire(delta.addedDocuments);
|
||||
this.onTextEditorRemoveEmitter.fire(removedEditors);
|
||||
this.onTextEditorAddEmitter.fire(addedEditors);
|
||||
}
|
||||
}
|
||||
|
||||
private toModelAddData(model: MonacoEditorModel): ModelAddedData {
|
||||
return {
|
||||
uri: model.textEditorModel.uri,
|
||||
versionId: model.textEditorModel.getVersionId(),
|
||||
lines: model.textEditorModel.getLinesContent(),
|
||||
languageId: model.getLanguageId(),
|
||||
EOL: model.textEditorModel.getEOL(),
|
||||
modeId: model.languageId,
|
||||
isDirty: model.dirty,
|
||||
encoding: this.encodingRegistry.getEncodingForResource(URI.fromComponents(model.textEditorModel.uri), model.getEncoding())
|
||||
};
|
||||
}
|
||||
|
||||
private toTextEditorAddData(textEditor: TextEditorMain): TextEditorAddData {
|
||||
const properties = textEditor.getProperties();
|
||||
return {
|
||||
id: textEditor.getId(),
|
||||
documentUri: textEditor.getModel().uri,
|
||||
options: properties!.options,
|
||||
selections: properties!.selections,
|
||||
visibleRanges: properties!.visibleRanges,
|
||||
editorPosition: this.findEditorPosition(textEditor)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private findEditorPosition(editor: TextEditorMain): EditorPosition | undefined {
|
||||
return EditorPosition.ONE; // TODO: fix this when Theia has support splitting editors
|
||||
}
|
||||
|
||||
getEditor(id: string): TextEditorMain | undefined {
|
||||
return this.textEditors.get(id);
|
||||
}
|
||||
|
||||
async save(uri: URI): Promise<URI | undefined> {
|
||||
const editor = await this.editorManager.getByUri(uri);
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
return this.saveResourceService.save(editor);
|
||||
}
|
||||
|
||||
async saveAs(uri: URI): Promise<URI | undefined> {
|
||||
const editor = await this.editorManager.getByUri(uri);
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.saveResourceService.canSaveAs(editor)) {
|
||||
return undefined;
|
||||
}
|
||||
return this.saveResourceService.saveAs(editor);
|
||||
}
|
||||
|
||||
saveAll(includeUntitled?: boolean): Promise<boolean> {
|
||||
return this.modelService.saveAll(includeUntitled);
|
||||
}
|
||||
|
||||
hideEditor(id: string): Promise<void> {
|
||||
for (const editorWidget of this.editorManager.all) {
|
||||
const monacoEditor = MonacoEditor.get(editorWidget);
|
||||
if (monacoEditor) {
|
||||
if (id === new EditorSnapshot(monacoEditor).id) {
|
||||
editorWidget.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getDiffInformation(id: string): ILineChange[] {
|
||||
const editor = this.getEditor(id);
|
||||
return editor?.diffInformation || [];
|
||||
}
|
||||
}
|
||||
|
||||
class EditorAndDocumentStateComputer implements Disposable {
|
||||
private currentState: EditorAndDocumentState | undefined;
|
||||
private readonly editors = new Map<string, DisposableCollection>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.currentState = undefined)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private callback: (delta: EditorAndDocumentStateDelta) => void,
|
||||
private readonly editorService: EditorManager,
|
||||
private readonly cellEditorService: NotebookCellEditorService,
|
||||
private readonly notebookWidgetService: NotebookEditorWidgetService,
|
||||
private readonly modelService: EditorModelService,
|
||||
private readonly tabsMain: TabsMainImpl
|
||||
) { }
|
||||
|
||||
listen(): void {
|
||||
if (this.toDispose.disposed) {
|
||||
return;
|
||||
}
|
||||
this.toDispose.push(this.editorService.onCreated(async widget => {
|
||||
await this.tabsMain.waitForWidget(widget);
|
||||
this.onTextEditorAdd(widget);
|
||||
this.update();
|
||||
}));
|
||||
this.toDispose.push(this.editorService.onCurrentEditorChanged(async widget => {
|
||||
if (widget) {
|
||||
await this.tabsMain.waitForWidget(widget);
|
||||
}
|
||||
this.update();
|
||||
}));
|
||||
this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this));
|
||||
this.toDispose.push(this.modelService.onModelRemoved(() => this.update()));
|
||||
|
||||
this.toDispose.push(this.cellEditorService.onDidChangeCellEditors(() => this.update()));
|
||||
|
||||
this.toDispose.push(this.notebookWidgetService.onDidChangeCurrentEditor(() => {
|
||||
this.currentState = this.currentState && new EditorAndDocumentState(
|
||||
this.currentState.documents,
|
||||
this.currentState.editors,
|
||||
undefined
|
||||
);
|
||||
}));
|
||||
|
||||
for (const widget of this.editorService.all) {
|
||||
this.onTextEditorAdd(widget);
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private onModelAdded(model: MonacoEditorModel): void {
|
||||
if (!this.currentState) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
this.currentState = new EditorAndDocumentState(
|
||||
this.currentState.documents.add(model),
|
||||
this.currentState.editors,
|
||||
this.currentState.activeEditor);
|
||||
|
||||
this.callback(new EditorAndDocumentStateDelta(
|
||||
[],
|
||||
[model],
|
||||
[],
|
||||
[],
|
||||
undefined,
|
||||
undefined
|
||||
));
|
||||
}
|
||||
|
||||
private onTextEditorAdd(widget: EditorWidget): void {
|
||||
if (widget.isDisposed) {
|
||||
return;
|
||||
}
|
||||
const editor = MonacoEditor.get(widget);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const id = editor.getControl().getId();
|
||||
const toDispose = new DisposableCollection(
|
||||
editor.onDispose(() => this.onTextEditorRemove(editor)),
|
||||
Disposable.create(() => this.editors.delete(id))
|
||||
);
|
||||
this.editors.set(id, toDispose);
|
||||
this.toDispose.push(toDispose);
|
||||
}
|
||||
|
||||
private onTextEditorRemove(e: MonacoEditor): void {
|
||||
const toDispose = this.editors.get(e.getControl().getId());
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
const models = new Set<MonacoEditorModel>();
|
||||
for (const model of this.modelService.getModels()) {
|
||||
models.add(model);
|
||||
}
|
||||
|
||||
let activeId: string | null = null;
|
||||
const activeEditor = MonacoEditor.getCurrent(this.editorService) ?? this.cellEditorService.getActiveCell();
|
||||
|
||||
const editors = new Map<string, EditorSnapshot>();
|
||||
for (const widget of this.editorService.all) {
|
||||
const editor = MonacoEditor.get(widget);
|
||||
// VS Code tracks only visible widgets
|
||||
if (!editor || !widget.isVisible) {
|
||||
continue;
|
||||
}
|
||||
const model = editor.getControl().getModel();
|
||||
if (model && !model.isDisposed()) {
|
||||
const editorSnapshot = new EditorSnapshot(editor);
|
||||
editors.set(editorSnapshot.id, editorSnapshot);
|
||||
if (activeEditor === editor) {
|
||||
activeId = editorSnapshot.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const editor of this.cellEditorService.allCellEditors) {
|
||||
if (editor.getControl()?.getModel()) {
|
||||
const editorSnapshot = new EditorSnapshot(editor);
|
||||
editors.set(editorSnapshot.id, editorSnapshot);
|
||||
if (activeEditor === editor) {
|
||||
activeId = editorSnapshot.id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = new EditorAndDocumentState(models, editors, activeId);
|
||||
const delta = EditorAndDocumentState.compute(this.currentState, newState);
|
||||
if (!delta.isEmpty) {
|
||||
this.currentState = newState;
|
||||
this.callback(delta);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EditorAndDocumentStateDelta {
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
constructor(
|
||||
readonly removedDocuments: MonacoEditorModel[],
|
||||
readonly addedDocuments: MonacoEditorModel[],
|
||||
readonly removedEditors: EditorSnapshot[],
|
||||
readonly addedEditors: EditorSnapshot[],
|
||||
readonly oldActiveEditor: string | null | undefined,
|
||||
readonly newActiveEditor: string | null | undefined
|
||||
) {
|
||||
this.isEmpty = this.removedDocuments.length === 0
|
||||
&& this.addedDocuments.length === 0
|
||||
&& this.addedEditors.length === 0
|
||||
&& this.removedEditors.length === 0
|
||||
&& this.newActiveEditor === this.oldActiveEditor;
|
||||
}
|
||||
}
|
||||
|
||||
class EditorAndDocumentState {
|
||||
|
||||
constructor(
|
||||
readonly documents: Set<MonacoEditorModel>,
|
||||
readonly editors: Map<string, EditorSnapshot>,
|
||||
readonly activeEditor: string | null | undefined) {
|
||||
}
|
||||
|
||||
static compute(before: EditorAndDocumentState | undefined, after: EditorAndDocumentState): EditorAndDocumentStateDelta {
|
||||
if (!before) {
|
||||
return new EditorAndDocumentStateDelta(
|
||||
[],
|
||||
Array.from(after.documents),
|
||||
[],
|
||||
Array.from(after.editors.values()),
|
||||
undefined,
|
||||
after.activeEditor
|
||||
);
|
||||
}
|
||||
|
||||
const documentDelta = Delta.ofSets(before.documents, after.documents);
|
||||
const editorDelta = Delta.ofMaps(before.editors, after.editors);
|
||||
const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
|
||||
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
|
||||
return new EditorAndDocumentStateDelta(
|
||||
documentDelta.removed,
|
||||
documentDelta.added,
|
||||
editorDelta.removed,
|
||||
editorDelta.added,
|
||||
oldActiveEditor,
|
||||
newActiveEditor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorSnapshot {
|
||||
readonly id: string;
|
||||
constructor(readonly editor: MonacoEditor | SimpleMonacoEditor) {
|
||||
this.id = `${editor.getControl().getId()},${editor.getControl().getModel()!.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Delta {
|
||||
|
||||
export function ofSets<T>(before: Set<T>, after: Set<T>): { removed: T[], added: T[] } {
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
before.forEach(element => {
|
||||
if (!after.has(element)) {
|
||||
removed.push(element);
|
||||
}
|
||||
});
|
||||
after.forEach(element => {
|
||||
if (!before.has(element)) {
|
||||
added.push(element);
|
||||
}
|
||||
});
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
export function ofMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[], added: V[] } {
|
||||
const removed: V[] = [];
|
||||
const added: V[] = [];
|
||||
before.forEach((value, index) => {
|
||||
if (!after.has(index)) {
|
||||
removed.push(value);
|
||||
}
|
||||
});
|
||||
after.forEach((value, index) => {
|
||||
if (!before.has(index)) {
|
||||
added.push(value);
|
||||
}
|
||||
});
|
||||
return { removed, added };
|
||||
}
|
||||
}
|
||||
60
packages/plugin-ext/src/main/browser/env-main.ts
Normal file
60
packages/plugin-ext/src/main/browser/env-main.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// *****************************************************************************
|
||||
// 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 { QueryParameters } from '../../common/env';
|
||||
export { EnvMainImpl } from '../common/env-main';
|
||||
|
||||
/**
|
||||
* Returns query parameters from current page.
|
||||
*/
|
||||
export function getQueryParameters(): QueryParameters {
|
||||
const queryParameters: QueryParameters = {};
|
||||
if (window.location.search !== '') {
|
||||
const queryParametersString = window.location.search.substring(1); // remove question mark
|
||||
const params = queryParametersString.split('&');
|
||||
for (const pair of params) {
|
||||
if (pair === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keyValue = pair.split('=');
|
||||
let key: string = keyValue[0];
|
||||
let value: string = keyValue[1] ? keyValue[1] : '';
|
||||
try {
|
||||
key = decodeURIComponent(key);
|
||||
if (value !== '') {
|
||||
value = decodeURIComponent(value);
|
||||
}
|
||||
} catch (error) {
|
||||
// skip malformed URI sequence
|
||||
continue;
|
||||
}
|
||||
|
||||
const existedValue = queryParameters[key];
|
||||
if (existedValue) {
|
||||
if (existedValue instanceof Array) {
|
||||
existedValue.push(value);
|
||||
} else {
|
||||
// existed value is string
|
||||
queryParameters[key] = [existedValue, value];
|
||||
}
|
||||
} else {
|
||||
queryParameters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return queryParameters;
|
||||
}
|
||||
267
packages/plugin-ext/src/main/browser/file-system-main-impl.ts
Normal file
267
packages/plugin-ext/src/main/browser/file-system-main-impl.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
// *****************************************************************************
|
||||
// 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/workbench/api/browser/mainThreadFileSystem.ts
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable @typescript-eslint/tslint/config */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import CoreURI from '@theia/core/lib/common/uri';
|
||||
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt, IFileChangeDto } from '../../common/plugin-api-rpc';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { UriComponents } from '../../common/uri-components';
|
||||
import {
|
||||
FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions,
|
||||
FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability,
|
||||
FileStat, FileChange, FileOperationError, FileOperationResult, ReadOnlyMessageFileSystemProvider
|
||||
} from '@theia/filesystem/lib/common/files';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { MarkdownString } from '../../common/plugin-api-rpc-model';
|
||||
|
||||
type IDisposable = Disposable;
|
||||
|
||||
export class FileSystemMainImpl implements FileSystemMain, Disposable {
|
||||
|
||||
private readonly _proxy: FileSystemExt;
|
||||
private readonly _fileProvider = new Map<number, RemoteFileSystemProvider>();
|
||||
private readonly _fileService: FileService;
|
||||
private readonly _disposables = new DisposableCollection();
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this._proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT);
|
||||
this._fileService = container.get(FileService);
|
||||
|
||||
for (const { scheme, capabilities } of this._fileService.listCapabilities()) {
|
||||
this._proxy.$acceptProviderInfos(scheme, capabilities);
|
||||
}
|
||||
|
||||
this._disposables.push(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider?.capabilities)));
|
||||
this._disposables.push(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider.capabilities)));
|
||||
this._disposables.push(Disposable.create(() => this._fileProvider.forEach(value => value.dispose())));
|
||||
this._disposables.push(Disposable.create(() => this._fileProvider.clear()));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
$registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void {
|
||||
this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy, readonlyMessage));
|
||||
}
|
||||
|
||||
$unregisterProvider(handle: number): void {
|
||||
const provider = this._fileProvider.get(handle);
|
||||
if (provider) {
|
||||
provider.dispose();
|
||||
this._fileProvider.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
$onFileSystemChange(handle: number, changes: IFileChangeDto[]): void {
|
||||
const fileProvider = this._fileProvider.get(handle);
|
||||
if (!fileProvider) {
|
||||
throw new Error('Unknown file provider');
|
||||
}
|
||||
fileProvider.$onFileSystemChange(changes);
|
||||
}
|
||||
|
||||
// --- consumer fs, vscode.workspace.fs
|
||||
|
||||
$stat(uri: UriComponents): Promise<Stat> {
|
||||
return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: true }).then(stat => ({
|
||||
ctime: stat.ctime,
|
||||
mtime: stat.mtime,
|
||||
size: stat.size,
|
||||
type: FileStat.asFileType(stat)
|
||||
})).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$readdir(uri: UriComponents): Promise<[string, FileType][]> {
|
||||
return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: false }).then(stat => {
|
||||
if (!stat.isDirectory) {
|
||||
const err = new Error(stat.name);
|
||||
err.name = FileSystemProviderErrorCode.FileNotADirectory;
|
||||
throw err;
|
||||
}
|
||||
return !stat.children ? [] : stat.children.map(child => [child.name, FileStat.asFileType(child)] as [string, FileType]);
|
||||
}).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$readFile(uri: UriComponents): Promise<BinaryBuffer> {
|
||||
return this._fileService.readFile(new CoreURI(URI.revive(uri))).then(file => file.value).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$writeFile(uri: UriComponents, content: BinaryBuffer): Promise<void> {
|
||||
return this._fileService.writeFile(new CoreURI(URI.revive(uri)), content)
|
||||
.then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.move(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), {
|
||||
...opts,
|
||||
fromUserGesture: false
|
||||
}).then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.copy(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), {
|
||||
...opts,
|
||||
fromUserGesture: false
|
||||
}).then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$mkdir(uri: UriComponents): Promise<void> {
|
||||
return this._fileService.createFolder(new CoreURI(URI.revive(uri)), { fromUserGesture: false })
|
||||
.then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$delete(uri: UriComponents, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._fileService.delete(new CoreURI(URI.revive(uri)), opts).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
private static _handleError(err: any): never {
|
||||
if (err instanceof FileOperationError) {
|
||||
switch (err.fileOperationResult) {
|
||||
case FileOperationResult.FILE_NOT_FOUND:
|
||||
err.name = FileSystemProviderErrorCode.FileNotFound;
|
||||
break;
|
||||
case FileOperationResult.FILE_IS_DIRECTORY:
|
||||
err.name = FileSystemProviderErrorCode.FileIsADirectory;
|
||||
break;
|
||||
case FileOperationResult.FILE_PERMISSION_DENIED:
|
||||
err.name = FileSystemProviderErrorCode.NoPermissions;
|
||||
break;
|
||||
case FileOperationResult.FILE_MOVE_CONFLICT:
|
||||
err.name = FileSystemProviderErrorCode.FileExists;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, ReadOnlyMessageFileSystemProvider {
|
||||
|
||||
private readonly _onDidChange = new Emitter<readonly FileChange[]>();
|
||||
private readonly _registration: IDisposable;
|
||||
|
||||
readonly onDidChangeFile: Event<readonly FileChange[]> = this._onDidChange.event;
|
||||
readonly onFileWatchError: Event<void> = new Emitter<void>().event; // dummy, never fired
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
readonly onDidChangeReadOnlyMessage: Event<MarkdownString | undefined> = Event.None;
|
||||
|
||||
constructor(
|
||||
fileService: FileService,
|
||||
scheme: string,
|
||||
capabilities: FileSystemProviderCapabilities,
|
||||
private readonly _handle: number,
|
||||
private readonly _proxy: FileSystemExt,
|
||||
public readonly readOnlyMessage: MarkdownString | undefined = undefined
|
||||
) {
|
||||
this.capabilities = capabilities;
|
||||
this._registration = fileService.registerProvider(scheme, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._registration.dispose();
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
watch(resource: CoreURI, opts: WatchOptions) {
|
||||
const session = Math.random();
|
||||
this._proxy.$watch(this._handle, session, resource['codeUri'], opts);
|
||||
return Disposable.create(() => {
|
||||
this._proxy.$unwatch(this._handle, session);
|
||||
});
|
||||
}
|
||||
|
||||
$onFileSystemChange(changes: IFileChangeDto[]): void {
|
||||
this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange));
|
||||
}
|
||||
|
||||
private static _createFileChange(dto: IFileChangeDto): FileChange {
|
||||
return { resource: new CoreURI(URI.revive(dto.resource)), type: dto.type };
|
||||
}
|
||||
|
||||
// --- forwarding calls
|
||||
|
||||
stat(resource: CoreURI): Promise<Stat> {
|
||||
return this._proxy.$stat(this._handle, resource['codeUri']).then(undefined, err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
readFile(resource: CoreURI): Promise<Uint8Array> {
|
||||
return this._proxy.$readFile(this._handle, resource['codeUri']).then(buffer => buffer.buffer);
|
||||
}
|
||||
|
||||
writeFile(resource: CoreURI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
return this._proxy.$writeFile(this._handle, resource['codeUri'], BinaryBuffer.wrap(content), opts);
|
||||
}
|
||||
|
||||
delete(resource: CoreURI, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._proxy.$delete(this._handle, resource['codeUri'], opts);
|
||||
}
|
||||
|
||||
mkdir(resource: CoreURI): Promise<void> {
|
||||
return this._proxy.$mkdir(this._handle, resource['codeUri']);
|
||||
}
|
||||
|
||||
readdir(resource: CoreURI): Promise<[string, FileType][]> {
|
||||
return this._proxy.$readdir(this._handle, resource['codeUri']);
|
||||
}
|
||||
|
||||
rename(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$rename(this._handle, resource['codeUri'], target['codeUri'], opts);
|
||||
}
|
||||
|
||||
copy(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$copy(this._handle, resource['codeUri'], target['codeUri'], opts);
|
||||
}
|
||||
|
||||
open(resource: CoreURI, opts: FileOpenOptions): Promise<number> {
|
||||
return this._proxy.$open(this._handle, resource['codeUri'], opts);
|
||||
}
|
||||
|
||||
close(fd: number): Promise<void> {
|
||||
return this._proxy.$close(this._handle, fd);
|
||||
}
|
||||
|
||||
read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this._proxy.$read(this._handle, fd, pos, length).then(readData => {
|
||||
data.set(readData.buffer, offset);
|
||||
return readData.byteLength;
|
||||
});
|
||||
}
|
||||
|
||||
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this._proxy.$write(this._handle, fd, pos, BinaryBuffer.wrap(data).slice(offset, offset + length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// *****************************************************************************
|
||||
// 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 { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall } from '@theia/callhierarchy/lib/browser';
|
||||
import * as languageProtocol from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { TypeHierarchyItem } from '@theia/typehierarchy/lib/browser';
|
||||
import * as rpc from '../../../common/plugin-api-rpc';
|
||||
import * as model from '../../../common/plugin-api-rpc-model';
|
||||
import { UriComponents } from '../../../common/uri-components';
|
||||
|
||||
export function toUriComponents(uri: string): UriComponents {
|
||||
return URI.parse(uri);
|
||||
}
|
||||
|
||||
export function fromUriComponents(uri: UriComponents): string {
|
||||
return URI.revive(uri).toString();
|
||||
}
|
||||
|
||||
export function fromLocation(location: languageProtocol.Location): model.Location {
|
||||
return <model.Location>{
|
||||
uri: URI.parse(location.uri),
|
||||
range: fromRange(location.range)
|
||||
};
|
||||
}
|
||||
|
||||
export function toLocation(uri: UriComponents, range: model.Range): languageProtocol.Location {
|
||||
return {
|
||||
uri: URI.revive(uri).toString(),
|
||||
range: toRange(range)
|
||||
};
|
||||
}
|
||||
|
||||
export function fromPosition(position: languageProtocol.Position): rpc.Position {
|
||||
return <rpc.Position>{
|
||||
lineNumber: position.line,
|
||||
column: position.character
|
||||
};
|
||||
}
|
||||
|
||||
export function fromRange(range: languageProtocol.Range): model.Range {
|
||||
const { start, end } = range;
|
||||
return {
|
||||
startLineNumber: start.line + 1,
|
||||
startColumn: start.character + 1,
|
||||
endLineNumber: end.line + 1,
|
||||
endColumn: end.character + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function toRange(range: model.Range): languageProtocol.Range {
|
||||
return languageProtocol.Range.create(
|
||||
range.startLineNumber - 1,
|
||||
range.startColumn - 1,
|
||||
range.endLineNumber - 1,
|
||||
range.endColumn - 1,
|
||||
);
|
||||
}
|
||||
|
||||
export namespace SymbolKindConverter {
|
||||
export function fromSymbolKind(kind: languageProtocol.SymbolKind): model.SymbolKind {
|
||||
switch (kind) {
|
||||
case languageProtocol.SymbolKind.File: return model.SymbolKind.File;
|
||||
case languageProtocol.SymbolKind.Module: return model.SymbolKind.Module;
|
||||
case languageProtocol.SymbolKind.Namespace: return model.SymbolKind.Namespace;
|
||||
case languageProtocol.SymbolKind.Package: return model.SymbolKind.Package;
|
||||
case languageProtocol.SymbolKind.Class: return model.SymbolKind.Class;
|
||||
case languageProtocol.SymbolKind.Method: return model.SymbolKind.Method;
|
||||
case languageProtocol.SymbolKind.Property: return model.SymbolKind.Property;
|
||||
case languageProtocol.SymbolKind.Field: return model.SymbolKind.Field;
|
||||
case languageProtocol.SymbolKind.Constructor: return model.SymbolKind.Constructor;
|
||||
case languageProtocol.SymbolKind.Enum: return model.SymbolKind.Enum;
|
||||
case languageProtocol.SymbolKind.Interface: return model.SymbolKind.Interface;
|
||||
case languageProtocol.SymbolKind.Function: return model.SymbolKind.Function;
|
||||
case languageProtocol.SymbolKind.Variable: return model.SymbolKind.Variable;
|
||||
case languageProtocol.SymbolKind.Constant: return model.SymbolKind.Constant;
|
||||
case languageProtocol.SymbolKind.String: return model.SymbolKind.String;
|
||||
case languageProtocol.SymbolKind.Number: return model.SymbolKind.Number;
|
||||
case languageProtocol.SymbolKind.Boolean: return model.SymbolKind.Boolean;
|
||||
case languageProtocol.SymbolKind.Array: return model.SymbolKind.Array;
|
||||
case languageProtocol.SymbolKind.Object: return model.SymbolKind.Object;
|
||||
case languageProtocol.SymbolKind.Key: return model.SymbolKind.Key;
|
||||
case languageProtocol.SymbolKind.Null: return model.SymbolKind.Null;
|
||||
case languageProtocol.SymbolKind.EnumMember: return model.SymbolKind.EnumMember;
|
||||
case languageProtocol.SymbolKind.Struct: return model.SymbolKind.Struct;
|
||||
case languageProtocol.SymbolKind.Event: return model.SymbolKind.Event;
|
||||
case languageProtocol.SymbolKind.Operator: return model.SymbolKind.Operator;
|
||||
case languageProtocol.SymbolKind.TypeParameter: return model.SymbolKind.TypeParameter;
|
||||
default: return model.SymbolKind.Property;
|
||||
}
|
||||
}
|
||||
export function toSymbolKind(kind: model.SymbolKind): languageProtocol.SymbolKind {
|
||||
switch (kind) {
|
||||
case model.SymbolKind.File: return languageProtocol.SymbolKind.File;
|
||||
case model.SymbolKind.Module: return languageProtocol.SymbolKind.Module;
|
||||
case model.SymbolKind.Namespace: return languageProtocol.SymbolKind.Namespace;
|
||||
case model.SymbolKind.Package: return languageProtocol.SymbolKind.Package;
|
||||
case model.SymbolKind.Class: return languageProtocol.SymbolKind.Class;
|
||||
case model.SymbolKind.Method: return languageProtocol.SymbolKind.Method;
|
||||
case model.SymbolKind.Property: return languageProtocol.SymbolKind.Property;
|
||||
case model.SymbolKind.Field: return languageProtocol.SymbolKind.Field;
|
||||
case model.SymbolKind.Constructor: return languageProtocol.SymbolKind.Constructor;
|
||||
case model.SymbolKind.Enum: return languageProtocol.SymbolKind.Enum;
|
||||
case model.SymbolKind.Interface: return languageProtocol.SymbolKind.Interface;
|
||||
case model.SymbolKind.Function: return languageProtocol.SymbolKind.Function;
|
||||
case model.SymbolKind.Variable: return languageProtocol.SymbolKind.Variable;
|
||||
case model.SymbolKind.Constant: return languageProtocol.SymbolKind.Constant;
|
||||
case model.SymbolKind.String: return languageProtocol.SymbolKind.String;
|
||||
case model.SymbolKind.Number: return languageProtocol.SymbolKind.Number;
|
||||
case model.SymbolKind.Boolean: return languageProtocol.SymbolKind.Boolean;
|
||||
case model.SymbolKind.Array: return languageProtocol.SymbolKind.Array;
|
||||
case model.SymbolKind.Object: return languageProtocol.SymbolKind.Object;
|
||||
case model.SymbolKind.Key: return languageProtocol.SymbolKind.Key;
|
||||
case model.SymbolKind.Null: return languageProtocol.SymbolKind.Null;
|
||||
case model.SymbolKind.EnumMember: return languageProtocol.SymbolKind.EnumMember;
|
||||
case model.SymbolKind.Struct: return languageProtocol.SymbolKind.Struct;
|
||||
case model.SymbolKind.Event: return languageProtocol.SymbolKind.Event;
|
||||
case model.SymbolKind.Operator: return languageProtocol.SymbolKind.Operator;
|
||||
case model.SymbolKind.TypeParameter: return languageProtocol.SymbolKind.TypeParameter;
|
||||
default: return languageProtocol.SymbolKind.Property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toItemHierarchyDefinition(modelItem: model.HierarchyItem): TypeHierarchyItem | CallHierarchyItem {
|
||||
return {
|
||||
...modelItem,
|
||||
kind: SymbolKindConverter.toSymbolKind(modelItem.kind),
|
||||
range: toRange(modelItem.range),
|
||||
selectionRange: toRange(modelItem.selectionRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromItemHierarchyDefinition(definition: TypeHierarchyItem | CallHierarchyItem): model.HierarchyItem {
|
||||
return {
|
||||
...definition,
|
||||
kind: SymbolKindConverter.fromSymbolKind(definition.kind),
|
||||
range: fromRange(definition.range),
|
||||
selectionRange: fromRange(definition.range),
|
||||
};
|
||||
}
|
||||
|
||||
export function toCaller(caller: model.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: toItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(toRange)
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCaller(caller: CallHierarchyIncomingCall): model.CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: fromItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(fromRange)
|
||||
};
|
||||
}
|
||||
|
||||
export function toCallee(callee: model.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
|
||||
return {
|
||||
to: toItemHierarchyDefinition(callee.to),
|
||||
fromRanges: callee.fromRanges.map(toRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCallHierarchyCallerToModelCallHierarchyIncomingCall(caller: CallHierarchyIncomingCall): model.CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: fromItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(fromRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCallHierarchyCalleeToModelCallHierarchyOutgoingCall(callee: CallHierarchyOutgoingCall): model.CallHierarchyOutgoingCall {
|
||||
return {
|
||||
to: fromItemHierarchyDefinition(callee.to),
|
||||
fromRanges: callee.fromRanges.map(fromRange),
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user