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

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

View File

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

View File

@@ -0,0 +1,38 @@
// *****************************************************************************
// Copyright (C) 2019 RedHat and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { ClipboardMain } from '../../common';
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
export class ClipboardMainImpl implements ClipboardMain {
protected readonly clipboardService: ClipboardService;
constructor(container: interfaces.Container) {
this.clipboardService = container.get(ClipboardService);
}
async $readText(): Promise<string> {
const result = await this.clipboardService.readText();
return result;
}
async $writeText(value: string): Promise<void> {
await this.clipboardService.writeText(value);
}
}

View File

@@ -0,0 +1,130 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { CommandRegistry } from '@theia/core/lib/common/command';
import * as theia from '@theia/plugin';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { CommandRegistryMain, CommandRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { KeybindingRegistry } from '@theia/core/lib/browser';
import { PluginContributionHandler } from './plugin-contribution-handler';
import { ArgumentProcessor } from '../../common/commands';
import { ContributionProvider } from '@theia/core';
export const ArgumentProcessorContribution = Symbol('ArgumentProcessorContribution');
export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable {
private readonly proxy: CommandRegistryExt;
private readonly commands = new Map<string, Disposable>();
private readonly handlers = new Map<string, Disposable>();
private readonly delegate: CommandRegistry;
private readonly keyBinding: KeybindingRegistry;
private readonly contributions: PluginContributionHandler;
private readonly argumentProcessors: ArgumentProcessor[] = [];
protected readonly toDispose = new DisposableCollection();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT);
this.delegate = container.get(CommandRegistry);
this.keyBinding = container.get(KeybindingRegistry);
this.contributions = container.get(PluginContributionHandler);
container.getNamed<ContributionProvider<ArgumentProcessor>>(ContributionProvider, ArgumentProcessorContribution).getContributions().forEach(processor => {
this.registerArgumentProcessor(processor);
});
}
dispose(): void {
this.toDispose.dispose();
}
registerArgumentProcessor(processor: ArgumentProcessor): Disposable {
this.argumentProcessors.push(processor);
return Disposable.create(() => {
const index = this.argumentProcessors.lastIndexOf(processor);
if (index >= 0) {
this.argumentProcessors.splice(index, 1);
}
});
}
$registerCommand(command: theia.CommandDescription): void {
const id = command.id;
this.commands.set(id, this.contributions.registerCommand(command));
this.toDispose.push(Disposable.create(() => this.$unregisterCommand(id)));
}
$unregisterCommand(id: string): void {
const command = this.commands.get(id);
if (command) {
command.dispose();
this.commands.delete(id);
}
}
$registerHandler(id: string): void {
this.handlers.set(id, this.contributions.registerCommandHandler(id, (...args) =>
this.proxy.$executeCommand(id, ...args.map(arg => this.argumentProcessors.reduce((currentValue, processor) => processor.processArgument(currentValue), arg)))
));
this.toDispose.push(Disposable.create(() => this.$unregisterHandler(id)));
}
$unregisterHandler(id: string): void {
const handler = this.handlers.get(id);
if (handler) {
handler.dispose();
this.handlers.delete(id);
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async $executeCommand<T>(id: string, ...args: any[]): Promise<T | undefined> {
if (!this.delegate.getCommand(id)) {
throw new Error(`Command with id '${id}' is not registered.`);
}
try {
return await this.delegate.executeCommand<T>(id, ...args);
} catch (e) {
// Command handler may be not active at the moment so the error must be caught. See https://github.com/eclipse-theia/theia/pull/6687#discussion_r354810079
if ('code' in e && e['code'] === 'NO_ACTIVE_HANDLER') {
return;
} else {
throw e;
}
}
}
$getKeyBinding(commandId: string): PromiseLike<theia.CommandKeyBinding[] | undefined> {
try {
const keyBindings = this.keyBinding.getKeybindingsForCommand(commandId);
if (keyBindings) {
// transform inner type to CommandKeyBinding
return Promise.resolve(keyBindings.map(keyBinding => ({ id: commandId, value: keyBinding.keybinding })));
} else {
return Promise.resolve(undefined);
}
} catch (e) {
return Promise.reject(e);
}
}
$getCommands(): PromiseLike<string[]> {
return Promise.resolve(this.delegate.commandIds);
}
}

View File

@@ -0,0 +1,104 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Command, CommandService } from '@theia/core/lib/common/command';
import { AbstractDialog } from '@theia/core/lib/browser';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import * as DOMPurify from '@theia/core/shared/dompurify';
import { nls } from '@theia/core/lib/common/nls';
@injectable()
export class OpenUriCommandHandler {
public static readonly COMMAND_METADATA: Command = {
id: 'theia.open'
};
private openNewTabDialog: OpenNewTabDialog;
constructor(
@inject(WindowService)
protected readonly windowService: WindowService,
@inject(CommandService)
protected readonly commandService: CommandService
) {
this.openNewTabDialog = new OpenNewTabDialog(windowService);
}
public execute(resource: URI | string | undefined): void {
if (!resource) {
return;
}
const uriString = resource.toString();
if (uriString.startsWith('http://') || uriString.startsWith('https://')) {
this.openWebUri(uriString);
} else {
this.commandService.executeCommand('editor.action.openLink', uriString);
}
}
private openWebUri(uri: string): void {
try {
this.windowService.openNewWindow(uri);
} catch (err) {
// browser has blocked opening of a new tab
this.openNewTabDialog.showOpenNewTabDialog(uri);
}
}
}
class OpenNewTabDialog extends AbstractDialog<string> {
protected readonly windowService: WindowService;
protected readonly openButton: HTMLButtonElement;
protected readonly messageNode: HTMLDivElement;
protected readonly linkNode: HTMLAnchorElement;
value: string;
constructor(windowService: WindowService) {
super({
title: nls.localize('theia/plugin/blockNewTab', 'Your browser prevented opening of a new tab')
});
this.windowService = windowService;
this.linkNode = document.createElement('a');
this.linkNode.target = '_blank';
this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
this.contentNode.appendChild(this.linkNode);
const messageNode = document.createElement('div');
messageNode.innerText = 'You are going to open: ';
messageNode.appendChild(this.linkNode);
this.contentNode.appendChild(messageNode);
this.appendCloseButton();
this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
}
showOpenNewTabDialog(uri: string): void {
this.value = uri;
this.linkNode.innerHTML = DOMPurify.sanitize(uri);
this.linkNode.href = uri;
this.openButton.onclick = () => {
this.windowService.openNewWindow(uri);
};
// show dialog window to user
this.open();
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Disposable } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts
export class CommentGlyphWidget implements Disposable {
private lineNumber!: number;
private editor: monaco.editor.ICodeEditor;
private commentsDecorations: string[] = [];
readonly commentsOptions: monaco.editor.IModelDecorationOptions;
constructor(editor: monaco.editor.ICodeEditor) {
this.commentsOptions = {
isWholeLine: true,
linesDecorationsClassName: 'comment-range-glyph comment-thread'
};
this.editor = editor;
}
getPosition(): number {
const model = this.editor.getModel();
const range = model && this.commentsDecorations && this.commentsDecorations.length
? model.getDecorationRange(this.commentsDecorations[0])
: null;
return range ? range.startLineNumber : this.lineNumber;
}
setLineNumber(lineNumber: number): void {
this.lineNumber = lineNumber;
const commentsDecorations = [{
range: {
startLineNumber: lineNumber, startColumn: 1,
endLineNumber: lineNumber, endColumn: 1
},
options: this.commentsOptions
}];
this.commentsDecorations = this.editor.deltaDecorations(this.commentsDecorations, commentsDecorations);
}
dispose(): void {
if (this.commentsDecorations) {
this.editor.deltaDecorations(this.commentsDecorations, []);
}
}
}

View File

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

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class CommentsContext {
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected readonly contextKeys: Set<string> = new Set();
protected _commentIsEmpty: ContextKey<boolean>;
protected _commentController: ContextKey<string | undefined>;
protected _comment: ContextKey<string | undefined>;
get commentController(): ContextKey<string | undefined> {
return this._commentController;
}
get comment(): ContextKey<string | undefined> {
return this._comment;
}
get commentIsEmpty(): ContextKey<boolean> {
return this._commentIsEmpty;
}
@postConstruct()
protected init(): void {
this.contextKeys.add('commentIsEmpty');
this._commentController = this.contextKeyService.createKey<string | undefined>('commentController', undefined);
this._comment = this.contextKeyService.createKey<string | undefined>('comment', undefined);
this._commentIsEmpty = this.contextKeyService.createKey<boolean>('commentIsEmpty', true);
}
}

View File

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

View File

@@ -0,0 +1,110 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { CommentInfoMain } from './comments-service';
import { CommentingRanges, Range } from '../../../common/plugin-api-rpc-model';
import * as monaco from '@theia/monaco-editor-core';
@injectable()
export class CommentingRangeDecorator {
private decorationOptions: monaco.editor.IModelDecorationOptions;
private commentingRangeDecorations: CommentingRangeDecoration[] = [];
constructor() {
this.decorationOptions = {
isWholeLine: true,
linesDecorationsClassName: 'comment-range-glyph comment-diff-added'
};
}
public update(editor: monaco.editor.ICodeEditor, commentInfos: CommentInfoMain[]): void {
const model = editor.getModel();
if (!model) {
return;
}
const commentingRangeDecorations: CommentingRangeDecoration[] = [];
for (const info of commentInfos) {
info.commentingRanges.ranges.forEach(range => {
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label,
range, this.decorationOptions, info.commentingRanges));
});
}
const oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id);
editor.deltaDecorations(oldDecorations, []);
this.commentingRangeDecorations = commentingRangeDecorations;
}
public getMatchedCommentAction(line: number): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges }[] {
const result = [];
for (const decoration of this.commentingRangeDecorations) {
const range = decoration.getActiveRange();
if (range && range.startLineNumber <= line && line <= range.endLineNumber) {
result.push(decoration.getCommentAction());
}
}
return result;
}
}
class CommentingRangeDecoration {
private decorationId: string;
public get id(): string {
return this.decorationId;
}
constructor(private _editor: monaco.editor.ICodeEditor, private _ownerId: string, private _extensionId: string | undefined,
private _label: string | undefined, private _range: Range, commentingOptions: monaco.editor.IModelDecorationOptions,
private commentingRangesInfo: CommentingRanges) {
const startLineNumber = _range.startLineNumber;
const endLineNumber = _range.endLineNumber;
const commentingRangeDecorations = [{
range: {
startLineNumber: startLineNumber, startColumn: 1,
endLineNumber: endLineNumber, endColumn: 1
},
options: commentingOptions
}];
this.decorationId = this._editor.deltaDecorations([], commentingRangeDecorations)[0];
}
public getCommentAction(): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges } {
return {
extensionId: this._extensionId,
label: this._label,
ownerId: this._ownerId,
commentingRangesInfo: this.commentingRangesInfo
};
}
public getOriginalRange(): Range {
return this._range;
}
public getActiveRange(): Range | undefined {
const range = this._editor.getModel()!.getDecorationRange(this.decorationId);
if (range) {
return range;
}
}
}

View File

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

View File

@@ -0,0 +1,207 @@
// *****************************************************************************
// Copyright (C) 2020 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { URI } from '@theia/core/shared/vscode-uri';
import { Event, Emitter } from '@theia/core/lib/common/event';
import {
Range,
CommentInfo,
CommentingRanges,
CommentThread,
CommentThreadChangedEvent,
CommentThreadChangedEventMain
} from '../../../common/plugin-api-rpc-model';
import { CommentController } from './comments-main';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentService.ts
export interface ResourceCommentThreadEvent {
resource: URI;
commentInfos: CommentInfoMain[];
}
export interface CommentInfoMain extends CommentInfo {
owner: string;
label?: string;
}
export interface WorkspaceCommentThreadsEventMain {
ownerId: string;
commentThreads: CommentThread[];
}
export const CommentsService = Symbol('CommentsService');
export interface CommentsService {
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent>;
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain>;
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain>;
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
readonly onDidSetDataProvider: Event<void>;
readonly onDidDeleteDataProvider: Event<string>;
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void;
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
removeWorkspaceComments(owner: string): void;
registerCommentController(owner: string, commentControl: CommentController): void;
unregisterCommentController(owner: string): void;
getCommentController(owner: string): CommentController | undefined;
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void;
updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void>;
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
disposeCommentThread(ownerId: string, threadId: string): void;
getComments(resource: URI): Promise<(CommentInfoMain | null)[]>;
getCommentingRanges(resource: URI): Promise<Range[]>;
setActiveCommentThread(commentThread: CommentThread | null): void;
}
@injectable()
export class PluginCommentService implements CommentsService {
private readonly onDidSetDataProviderEmitter: Emitter<void> = new Emitter<void>();
readonly onDidSetDataProvider: Event<void> = this.onDidSetDataProviderEmitter.event;
private readonly onDidDeleteDataProviderEmitter: Emitter<string> = new Emitter<string>();
readonly onDidDeleteDataProvider: Event<string> = this.onDidDeleteDataProviderEmitter.event;
private readonly onDidSetResourceCommentInfosEmitter: Emitter<ResourceCommentThreadEvent> = new Emitter<ResourceCommentThreadEvent>();
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent> = this.onDidSetResourceCommentInfosEmitter.event;
private readonly onDidSetAllCommentThreadsEmitter: Emitter<WorkspaceCommentThreadsEventMain> = new Emitter<WorkspaceCommentThreadsEventMain>();
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain> = this.onDidSetAllCommentThreadsEmitter.event;
private readonly onDidUpdateCommentThreadsEmitter: Emitter<CommentThreadChangedEventMain> = new Emitter<CommentThreadChangedEventMain>();
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain> = this.onDidUpdateCommentThreadsEmitter.event;
private readonly onDidChangeActiveCommentThreadEmitter = new Emitter<CommentThread | null>();
readonly onDidChangeActiveCommentThread = this.onDidChangeActiveCommentThreadEmitter.event;
private readonly onDidChangeActiveCommentingRangeEmitter = new Emitter<{ range: Range, commentingRangesInfo: CommentingRanges }>();
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }> = this.onDidChangeActiveCommentingRangeEmitter.event;
private commentControls = new Map<string, CommentController>();
setActiveCommentThread(commentThread: CommentThread | null): void {
this.onDidChangeActiveCommentThreadEmitter.fire(commentThread);
}
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void {
this.onDidSetResourceCommentInfosEmitter.fire({ resource, commentInfos });
}
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: commentsByResource });
}
removeWorkspaceComments(owner: string): void {
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: [] });
}
registerCommentController(owner: string, commentControl: CommentController): void {
this.commentControls.set(owner, commentControl);
this.onDidSetDataProviderEmitter.fire();
}
unregisterCommentController(owner: string): void {
this.commentControls.delete(owner);
this.onDidDeleteDataProviderEmitter.fire(owner);
}
getCommentController(owner: string): CommentController | undefined {
return this.commentControls.get(owner);
}
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void {
const commentController = this.commentControls.get(owner);
if (!commentController) {
return;
}
commentController.createCommentThreadTemplate(resource, range);
}
async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void> {
const commentController = this.commentControls.get(owner);
if (!commentController) {
return;
}
await commentController.updateCommentThreadTemplate(threadHandle, range);
}
disposeCommentThread(owner: string, threadId: string): void {
const controller = this.getCommentController(owner);
if (controller) {
controller.deleteCommentThreadMain(threadId);
}
}
updateComments(ownerId: string, event: CommentThreadChangedEvent): void {
const evt: CommentThreadChangedEventMain = Object.assign({}, event, { owner: ownerId });
this.onDidUpdateCommentThreadsEmitter.fire(evt);
}
async getComments(resource: URI): Promise<(CommentInfoMain | null)[]> {
const commentControlResult: Promise<CommentInfoMain | null>[] = [];
this.commentControls.forEach(control => {
commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None)
.catch(e => {
console.log(e);
return null;
}));
});
return Promise.all(commentControlResult);
}
async getCommentingRanges(resource: URI): Promise<Range[]> {
const commentControlResult: Promise<{ ranges: Range[]; fileComments: boolean } | undefined>[] = [];
this.commentControls.forEach(control => {
commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None));
});
const ret = await Promise.all(commentControlResult);
return ret.reduce<Range[]>((prev, curr) => {
if (curr) {
prev.push(...curr.ranges);
}
return prev;
}, []);
}
}

View File

@@ -0,0 +1,209 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import URI from '@theia/core/lib/common/uri';
import {
ApplicationShell, DiffUris, OpenHandler, OpenerOptions, SplitWidget, Widget, WidgetManager, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority
} from '@theia/core/lib/browser';
import { CustomEditor, CustomEditorPriority, CustomEditorSelector } from '../../../common';
import { CustomEditorWidget } from './custom-editor-widget';
import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { DisposableCollection, Emitter, PreferenceService } from '@theia/core';
import { match } from '@theia/core/lib/common/glob';
export class CustomEditorOpener implements OpenHandler {
readonly id: string;
readonly label: string;
private readonly onDidOpenCustomEditorEmitter = new Emitter<[CustomEditorWidget, WidgetOpenerOptions?]>();
readonly onDidOpenCustomEditor = this.onDidOpenCustomEditorEmitter.event;
constructor(
private readonly editor: CustomEditor,
protected readonly shell: ApplicationShell,
protected readonly widgetManager: WidgetManager,
protected readonly editorRegistry: PluginCustomEditorRegistry,
protected readonly preferenceService: PreferenceService
) {
this.id = CustomEditorOpener.toCustomEditorId(this.editor.viewType);
this.label = this.editor.displayName;
}
static toCustomEditorId(editorViewType: string): string {
return `custom-editor-${editorViewType}`;
}
canHandle(uri: URI, options?: OpenerOptions): number {
let priority = 0;
const { selector } = this.editor;
if (DiffUris.isDiffUri(uri)) {
const [left, right] = DiffUris.decode(uri);
if (this.matches(selector, right) && this.matches(selector, left)) {
if (getDefaultHandler(right, this.preferenceService) === this.editor.viewType) {
priority = defaultHandlerPriority;
} else {
priority = this.getPriority();
}
}
} else if (this.matches(selector, uri)) {
if (getDefaultHandler(uri, this.preferenceService) === this.editor.viewType) {
priority = defaultHandlerPriority;
} else {
priority = this.getPriority();
}
}
return priority;
}
canOpenWith(uri: URI): number {
if (this.matches(this.editor.selector, uri)) {
return this.getPriority();
}
return 0;
}
getPriority(): number {
switch (this.editor.priority) {
case CustomEditorPriority.default: return 500;
case CustomEditorPriority.builtin: return 400;
/** `option` should not open the custom-editor by default. */
case CustomEditorPriority.option: return 1;
default: return 200;
}
}
protected readonly pendingWidgetPromises = new Map<string, Promise<CustomEditorWidget>>();
protected async openCustomEditor(uri: URI, options?: WidgetOpenerOptions): Promise<CustomEditorWidget> {
let widget: CustomEditorWidget | undefined;
let isNewWidget = false;
const uriString = uri.toString();
let widgetPromise = this.pendingWidgetPromises.get(uriString);
if (widgetPromise) {
widget = await widgetPromise;
} else {
const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[];
widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uriString);
if (!widget) {
isNewWidget = true;
const id = generateUuid();
widgetPromise = this.widgetManager.getOrCreateWidget<CustomEditorWidget>(CustomEditorWidget.FACTORY_ID, { id }).then(async w => {
try {
w.viewType = this.editor.viewType;
w.resource = uri;
await this.editorRegistry.resolveWidget(w);
if (options?.widgetOptions) {
await this.shell.addWidget(w, options.widgetOptions);
}
return w;
} catch (e) {
w.dispose();
throw e;
}
}).finally(() => this.pendingWidgetPromises.delete(uriString));
this.pendingWidgetPromises.set(uriString, widgetPromise);
widget = await widgetPromise;
}
}
if (options?.mode === 'activate') {
await this.shell.activateWidget(widget.id);
} else if (options?.mode === 'reveal') {
await this.shell.revealWidget(widget.id);
}
if (isNewWidget) {
this.onDidOpenCustomEditorEmitter.fire([widget, options]);
}
return widget;
}
protected async openSideBySide(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
const [leftUri, rightUri] = DiffUris.decode(uri);
const widget = await this.widgetManager.getOrCreateWidget<SplitWidget>(
CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID, { uri: uri.toString(), viewType: this.editor.viewType });
if (!widget.panes.length) { // a new widget
const trackedDisposables = new DisposableCollection(widget);
try {
const createPane = async (paneUri: URI) => {
let pane = await this.openCustomEditor(paneUri);
if (pane.isAttached) {
await this.shell.closeWidget(pane.id);
if (!pane.isDisposed) { // user canceled
return undefined;
}
pane = await this.openCustomEditor(paneUri);
}
return pane;
};
const rightPane = await createPane(rightUri);
if (!rightPane) {
trackedDisposables.dispose();
return undefined;
}
trackedDisposables.push(rightPane);
const leftPane = await createPane(leftUri);
if (!leftPane) {
trackedDisposables.dispose();
return undefined;
}
trackedDisposables.push(leftPane);
widget.addPane(leftPane);
widget.addPane(rightPane);
// dispose the widget if either of its panes gets externally disposed
leftPane.disposed.connect(() => widget.dispose());
rightPane.disposed.connect(() => widget.dispose());
if (options?.widgetOptions) {
await this.shell.addWidget(widget, options.widgetOptions);
}
} catch (e) {
trackedDisposables.dispose();
console.error(e);
throw e;
}
}
if (options?.mode === 'activate') {
await this.shell.activateWidget(widget.id);
} else if (options?.mode === 'reveal') {
await this.shell.revealWidget(widget.id);
}
return widget;
}
async open(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
options = { ...options };
options.mode ??= 'activate';
options.widgetOptions ??= { area: 'main' };
return DiffUris.isDiffUri(uri) ? this.openSideBySide(uri, options) : this.openCustomEditor(uri, options);
}
matches(selectors: CustomEditorSelector[], resource: URI): boolean {
return selectors.some(selector => this.selectorMatches(selector, resource));
}
selectorMatches(selector: CustomEditorSelector, resource: URI): boolean {
if (selector.filenamePattern) {
if (match(selector.filenamePattern.toLowerCase(), resource.path.name.toLowerCase() + resource.path.ext.toLowerCase())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,108 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/contrib/customEditor/browser/customEditors.ts
import { injectable } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { Reference } from '@theia/core/lib/common/reference';
import { CustomEditorModel } from './custom-editors-main';
@injectable()
export class CustomEditorService {
protected _models = new CustomEditorModelManager();
get models(): CustomEditorModelManager { return this._models; }
}
export class CustomEditorModelManager {
private readonly references = new Map<string, {
readonly viewType: string,
readonly model: Promise<CustomEditorModel>,
counter: number
}>();
add(resource: URI, viewType: string, model: Promise<CustomEditorModel>): Promise<Reference<CustomEditorModel>> {
const key = this.key(resource, viewType);
const existing = this.references.get(key);
if (existing) {
throw new Error('Model already exists');
}
this.references.set(key, { viewType, model, counter: 0 });
return this.tryRetain(resource, viewType)!;
}
async get(resource: URI, viewType: string): Promise<CustomEditorModel | undefined> {
const key = this.key(resource, viewType);
const entry = this.references.get(key);
return entry?.model;
}
tryRetain(resource: URI, viewType: string): Promise<Reference<CustomEditorModel>> | undefined {
const key = this.key(resource, viewType);
const entry = this.references.get(key);
if (!entry) {
return undefined;
}
entry.counter++;
return entry.model.then(model => ({
object: model,
dispose: once(() => {
if (--entry!.counter <= 0) {
entry.model.then(x => x.dispose());
this.references.delete(key);
}
}),
}));
}
disposeAllModelsForView(viewType: string): void {
for (const [key, value] of this.references) {
if (value.viewType === viewType) {
value.model.then(x => x.dispose());
this.references.delete(key);
}
}
}
private key(resource: URI, viewType: string): string {
return `${resource.toString()}@@@${viewType}`;
}
}
export function once<T extends Function>(this: unknown, fn: T): T {
const _this = this;
let didCall = false;
let result: unknown;
return function (): unknown {
if (didCall) {
return result;
}
didCall = true;
result = fn.apply(_this, arguments);
return result;
} as unknown as T;
}

View File

@@ -0,0 +1,41 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser';
import { CustomEditorWidget } from './custom-editor-widget';
@injectable()
export class CustomEditorUndoRedoHandler implements UndoRedoHandler<CustomEditorWidget> {
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
priority = 190;
select(): CustomEditorWidget | undefined {
const current = this.applicationShell.currentWidget;
if (current instanceof CustomEditorWidget) {
return current;
}
return undefined;
}
undo(item: CustomEditorWidget): void {
item.undo();
}
redo(item: CustomEditorWidget): void {
item.redo();
}
}

View File

@@ -0,0 +1,44 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
import { interfaces } from '@theia/core/shared/inversify';
import { WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from '../webview/webview';
import { WebviewEnvironment } from '../webview/webview-environment';
export class CustomEditorWidgetFactory {
readonly id = CustomEditorWidget.FACTORY_ID;
protected readonly container: interfaces.Container;
constructor(container: interfaces.Container) {
this.container = container;
}
async createWidget(identifier: WebviewWidgetIdentifier): Promise<CustomEditorWidget> {
const externalEndpoint = await this.container.get(WebviewEnvironment).externalEndpoint();
let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id);
if (endpoint[endpoint.length - 1] === '/') {
endpoint = endpoint.slice(0, endpoint.length - 1);
}
const child = this.container.createChild();
child.bind(WebviewWidgetIdentifier).toConstantValue(identifier);
child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint);
return child.get(CustomEditorWidget);
}
}

View File

@@ -0,0 +1,114 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { FileOperation } from '@theia/filesystem/lib/common/files';
import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
import { Reference } from '@theia/core/lib/common/reference';
import { WebviewWidget } from '../webview/webview';
import { CustomEditorModel } from './custom-editors-main';
import { CustomEditorWidget as CustomEditorWidgetShape } from '@theia/editor/lib/browser';
@injectable()
export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget {
static override FACTORY_ID = 'plugin-custom-editor';
static readonly SIDE_BY_SIDE_FACTORY_ID = CustomEditorWidget.FACTORY_ID + '.side-by-side';
resource: URI;
protected _modelRef: Reference<CustomEditorModel | undefined> = { object: undefined, dispose: () => { } };
get modelRef(): Reference<CustomEditorModel | undefined> {
return this._modelRef;
}
set modelRef(modelRef: Reference<CustomEditorModel>) {
this._modelRef.dispose();
this._modelRef = modelRef;
this.delegatingSaveable.delegate = modelRef.object;
this.doUpdateContent();
}
// ensures that saveable is available even if modelRef.object is undefined
protected readonly delegatingSaveable = new DelegatingSaveable();
get saveable(): Saveable {
return this.delegatingSaveable;
}
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(SaveableService)
protected readonly saveService: SaveableService;
@postConstruct()
protected override init(): void {
super.init();
this.id = CustomEditorWidget.FACTORY_ID + ':' + this.identifier.id;
this.toDispose.push(this.fileService.onDidRunOperation(e => {
if (e.isOperation(FileOperation.MOVE)) {
this.doMove(e.target.resource);
}
}));
}
undo(): void {
this._modelRef.object?.undo();
}
redo(): void {
this._modelRef.object?.redo();
}
getResourceUri(): URI | undefined {
return this.resource;
}
createMoveToUri(resourceUri: URI): URI | undefined {
return this.resource.withPath(resourceUri.path);
}
override storeState(): CustomEditorWidget.State {
return {
...super.storeState(),
strResource: this.resource.toString(),
};
}
override restoreState(oldState: CustomEditorWidget.State): void {
const { strResource } = oldState;
this.resource = new URI(strResource);
super.restoreState(oldState);
}
onMove(handler: (newResource: URI) => Promise<void>): void {
this._moveHandler = handler;
}
private _moveHandler?: (newResource: URI) => void;
private doMove(target: URI): void {
if (this._moveHandler) {
this._moveHandler(target);
}
}
}
export namespace CustomEditorWidget {
export interface State extends WebviewWidget.State {
strResource: string
}
}

View File

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

View File

@@ -0,0 +1,126 @@
// *****************************************************************************
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { CustomEditor, DeployedPlugin } from '../../../common';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { CustomEditorOpener } from './custom-editor-opener';
import { Emitter, PreferenceService } from '@theia/core';
import { ApplicationShell, DefaultOpenerService, OpenWithService, WidgetManager } from '@theia/core/lib/browser';
import { CustomEditorWidget } from './custom-editor-widget';
@injectable()
export class PluginCustomEditorRegistry {
private readonly editors = new Map<string, CustomEditor>();
private readonly pendingEditors = new Map<CustomEditorWidget, { deferred: Deferred<void>, disposable: Disposable }>();
private readonly resolvers = new Map<string, (widget: CustomEditorWidget) => Promise<void>>();
private readonly onWillOpenCustomEditorEmitter = new Emitter<string>();
readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event;
@inject(DefaultOpenerService)
protected readonly defaultOpenerService: DefaultOpenerService;
@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(OpenWithService)
protected readonly openWithService: OpenWithService;
@inject(PreferenceService)
protected readonly preferenceService: PreferenceService;
@postConstruct()
protected init(): void {
this.widgetManager.onDidCreateWidget(({ factoryId, widget }) => {
if (factoryId === CustomEditorWidget.FACTORY_ID && widget instanceof CustomEditorWidget) {
const restoreState = widget.restoreState.bind(widget);
widget.restoreState = state => {
if (state.viewType && state.strResource) {
restoreState(state);
this.resolveWidget(widget);
} else {
widget.dispose();
}
};
}
});
}
registerCustomEditor(editor: CustomEditor, plugin: DeployedPlugin): Disposable {
if (this.editors.has(editor.viewType)) {
console.warn('editor with such id already registered: ', JSON.stringify(editor));
return Disposable.NULL;
}
this.editors.set(editor.viewType, editor);
const toDispose = new DisposableCollection();
toDispose.push(Disposable.create(() => this.editors.delete(editor.viewType)));
const editorOpenHandler = new CustomEditorOpener(
editor,
this.shell,
this.widgetManager,
this,
this.preferenceService
);
toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler));
toDispose.push(
this.openWithService.registerHandler({
id: editor.viewType,
label: editorOpenHandler.label,
providerName: plugin.metadata.model.displayName,
canHandle: uri => editorOpenHandler.canOpenWith(uri),
open: uri => editorOpenHandler.open(uri)
})
);
return toDispose;
}
async resolveWidget(widget: CustomEditorWidget): Promise<void> {
const resolver = this.resolvers.get(widget.viewType);
if (resolver) {
await resolver(widget);
} else {
const deferred = new Deferred<void>();
const disposable = widget.onDidDispose(() => this.pendingEditors.delete(widget));
this.pendingEditors.set(widget, { deferred, disposable });
this.onWillOpenCustomEditorEmitter.fire(widget.viewType);
return deferred.promise;
}
};
registerResolver(viewType: string, resolver: (widget: CustomEditorWidget) => Promise<void>): Disposable {
if (this.resolvers.has(viewType)) {
throw new Error(`Resolver for ${viewType} already registered`);
}
for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) {
if (editorWidget.viewType === viewType) {
resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose());
this.pendingEditors.delete(editorWidget);
}
}
this.resolvers.set(viewType, resolver);
return Disposable.create(() => this.resolvers.delete(viewType));
}
}

View File

@@ -0,0 +1,68 @@
// *****************************************************************************
// Copyright (C) 2023 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { IDataTransferItem, IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer';
import { DataTransferDTO, DataTransferItemDTO } from '../../../common/plugin-api-rpc-model';
import { URI } from '../../../plugin/types-impl';
export namespace DataTransferItem {
export async function from(mime: string, item: IDataTransferItem): Promise<DataTransferItemDTO> {
const stringValue = await item.asString();
if (mime === 'text/uri-list') {
return {
asString: '',
fileData: undefined,
uriListData: serializeUriList(stringValue),
};
}
const fileValue = item.asFile();
return {
asString: stringValue,
fileData: fileValue ? { id: fileValue.id, name: fileValue.name, uri: fileValue.uri } : undefined,
};
}
function serializeUriList(stringValue: string): ReadonlyArray<string | URI> {
return stringValue.split('\r\n').map(part => {
if (part.startsWith('#')) {
return part;
}
try {
return URI.parse(part);
} catch {
// noop
}
return part;
});
}
}
export namespace DataTransfer {
export async function toDataTransferDTO(value: IReadonlyVSDataTransfer): Promise<DataTransferDTO> {
return {
items: await Promise.all(
Array.from(value)
.map(
async ([mime, item]) => [mime, await DataTransferItem.from(mime, item)]
)
)
};
}
}

View File

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

View File

@@ -0,0 +1,48 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { DebugExt } from '../../../common/plugin-api-rpc';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
import { MaybePromise } from '@theia/core/lib/common/types';
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
/**
* Plugin [DebugAdapterContribution](#DebugAdapterContribution).
*/
export class PluginDebugAdapterContribution {
constructor(
protected readonly description: DebuggerDescription,
protected readonly debugExt: DebugExt,
protected readonly pluginService: HostedPluginSupport) { }
get type(): string {
return this.description.type;
}
get label(): MaybePromise<string | undefined> {
return this.description.label;
}
async createDebugSession(config: DebugConfiguration, workspaceFolder: string | undefined): Promise<string> {
await this.pluginService.activateByDebug('onDebugAdapterProtocolTracker', config.type);
return this.debugExt.$createDebugSession(config, workspaceFolder);
}
async terminateDebugSession(sessionId: string): Promise<void> {
this.debugExt.$terminateDebugSession(sessionId);
}
}

View File

@@ -0,0 +1,69 @@
// *****************************************************************************
// Copyright (C) 2022 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
DebugConfigurationProvider,
DebugConfigurationProviderDescriptor,
DebugConfigurationProviderTriggerKind,
DebugExt
} from '../../../common/plugin-api-rpc';
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
export class PluginDebugConfigurationProvider implements DebugConfigurationProvider {
/**
* After https://github.com/eclipse-theia/theia/pull/13196, the debug config handles might change.
* Store the original handle to be able to call the extension host when getting by handle.
*/
protected readonly originalHandle: number;
public handle: number;
public type: string;
public triggerKind: DebugConfigurationProviderTriggerKind;
provideDebugConfigurations: (folder: string | undefined) => Promise<DebugConfiguration[]>;
resolveDebugConfiguration: (
folder: string | undefined,
debugConfiguration: DebugConfiguration
) => Promise<DebugConfiguration | undefined | null>;
resolveDebugConfigurationWithSubstitutedVariables: (
folder: string | undefined,
debugConfiguration: DebugConfiguration
) => Promise<DebugConfiguration | undefined | null>;
constructor(
description: DebugConfigurationProviderDescriptor,
protected readonly debugExt: DebugExt
) {
this.handle = description.handle;
this.originalHandle = this.handle;
this.type = description.type;
this.triggerKind = description.trigger;
if (description.provideDebugConfiguration) {
this.provideDebugConfigurations = async (folder: string | undefined) => this.debugExt.$provideDebugConfigurationsByHandle(this.originalHandle, folder);
}
if (description.resolveDebugConfigurations) {
this.resolveDebugConfiguration =
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
this.debugExt.$resolveDebugConfigurationByHandle(this.originalHandle, folder, debugConfiguration);
}
if (description.resolveDebugConfigurationWithSubstitutedVariables) {
this.resolveDebugConfigurationWithSubstitutedVariables =
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
this.debugExt.$resolveDebugConfigurationWithSubstitutedVariablesByHandle(this.originalHandle, folder, debugConfiguration);
}
}
}

View File

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

View File

@@ -0,0 +1,76 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { DebugSessionContributionRegistry, DebugSessionContribution } from '@theia/debug/lib/browser/debug-session-contribution';
import { injectable, inject, named, postConstruct } from '@theia/core/shared/inversify';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { Disposable } from '@theia/core/lib/common/disposable';
/**
* Debug session contribution registrator.
*/
export interface PluginDebugSessionContributionRegistrator {
/**
* Registers [DebugSessionContribution](#DebugSessionContribution).
* @param contrib contribution
*/
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable;
/**
* Unregisters [DebugSessionContribution](#DebugSessionContribution).
* @param debugType the debug type
*/
unregisterDebugSessionContribution(debugType: string): void;
}
/**
* Plugin debug session contribution registry implementation with functionality
* to register / unregister plugin contributions.
*/
@injectable()
export class PluginDebugSessionContributionRegistry implements DebugSessionContributionRegistry, PluginDebugSessionContributionRegistrator {
protected readonly contribs = new Map<string, DebugSessionContribution>();
@inject(ContributionProvider) @named(DebugSessionContribution)
protected readonly contributions: ContributionProvider<DebugSessionContribution>;
@postConstruct()
protected init(): void {
for (const contrib of this.contributions.getContributions()) {
this.contribs.set(contrib.debugType, contrib);
}
}
get(debugType: string): DebugSessionContribution | undefined {
return this.contribs.get(debugType);
}
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable {
const { debugType } = contrib;
if (this.contribs.has(debugType)) {
console.warn(`Debug session contribution already registered for ${debugType}`);
return Disposable.NULL;
}
this.contribs.set(debugType, contrib);
return Disposable.create(() => this.unregisterDebugSessionContribution(debugType));
}
unregisterDebugSessionContribution(debugType: string): void {
this.contribs.delete(debugType);
}
}

View File

@@ -0,0 +1,121 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { DefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
import { DebugPreferences } from '@theia/debug/lib/common/debug-preferences';
import { DebugConfigurationSessionOptions, TestRunReference } from '@theia/debug/lib/browser/debug-session-options';
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { TerminalOptionsExt } from '../../../common/plugin-api-rpc';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { PluginChannel } from '../../../common/connection';
import { TestService } from '@theia/test/lib/browser/test-service';
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
import { CommandService } from '@theia/core';
export class PluginDebugSession extends DebugSession {
constructor(
override readonly id: string,
override readonly options: DebugConfigurationSessionOptions,
override readonly parentSession: DebugSession | undefined,
testService: TestService,
testRun: TestRunReference | undefined,
sessionManager: DebugSessionManager,
protected override readonly connection: DebugSessionConnection,
protected override readonly terminalServer: TerminalService,
protected override readonly editorManager: EditorManager,
protected override readonly breakpoints: BreakpointManager,
protected override readonly labelProvider: LabelProvider,
protected override readonly messages: MessageClient,
protected override readonly fileService: FileService,
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
protected override readonly workspaceService: WorkspaceService,
debugPreferences: DebugPreferences,
protected override readonly commandService: CommandService) {
super(id, options, parentSession, testService, testRun, sessionManager, connection, terminalServer, editorManager, breakpoints,
labelProvider, messages, fileService, debugContributionProvider,
workspaceService, debugPreferences, commandService);
}
protected override async doCreateTerminal(terminalWidgetOptions: TerminalWidgetOptions): Promise<TerminalWidget> {
terminalWidgetOptions = Object.assign({}, terminalWidgetOptions, this.terminalOptionsExt);
return super.doCreateTerminal(terminalWidgetOptions);
}
}
/**
* Session factory for a client debug session that communicates with debug adapter contributed as plugin.
* The main difference is to use a connection factory that creates [Channel](#Channel) over Rpc channel.
*/
export class PluginDebugSessionFactory extends DefaultDebugSessionFactory {
constructor(
protected override readonly terminalService: TerminalService,
protected override readonly editorManager: EditorManager,
protected override readonly breakpoints: BreakpointManager,
protected override readonly labelProvider: LabelProvider,
protected override readonly messages: MessageClient,
protected override readonly outputChannelManager: OutputChannelManager,
protected override readonly debugPreferences: DebugPreferences,
protected readonly connectionFactory: (sessionId: string) => Promise<PluginChannel>,
protected override readonly fileService: FileService,
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
protected override readonly testService: TestService,
protected override readonly workspaceService: WorkspaceService,
protected override readonly commandService: CommandService,
) {
super();
}
override get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession {
const connection = new DebugSessionConnection(
sessionId,
this.connectionFactory,
this.getTraceOutputChannel());
return new PluginDebugSession(
sessionId,
options,
parentSession,
this.testService,
options.testRun,
manager,
connection,
this.terminalService,
this.editorManager,
this.breakpoints,
this.labelProvider,
this.messages,
this.fileService,
this.terminalOptionsExt,
this.debugContributionProvider,
this.workspaceService,
this.debugPreferences,
this.commandService
);
}
}

View File

@@ -0,0 +1,146 @@
// *****************************************************************************
// Copyright (C) 2019 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import {
DecorationData,
DecorationRequest,
DecorationsExt,
DecorationsMain,
MAIN_RPC_CONTEXT
} from '../../../common/plugin-api-rpc';
import { interfaces } from '@theia/core/shared/inversify';
import { Emitter } from '@theia/core/lib/common/event';
import { Disposable } from '@theia/core/lib/common/disposable';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { UriComponents } from '../../../common/uri-components';
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import URI from '@theia/core/lib/common/uri';
import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service';
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/api/browser/mainThreadDecorations.ts#L85
class DecorationRequestsQueue {
private idPool = 0;
private requests = new Map<number, DecorationRequest>();
private resolver = new Map<number, (data: DecorationData) => void>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private timer: any;
constructor(
private readonly proxy: DecorationsExt,
private readonly handle: number
) {
}
enqueue(uri: URI, token: CancellationToken): Promise<DecorationData> {
const id = ++this.idPool;
const result = new Promise<DecorationData>(resolve => {
this.requests.set(id, { id, uri: VSCodeURI.parse(uri.toString()) });
this.resolver.set(id, resolve);
this.processQueue();
});
token.onCancellationRequested(() => {
this.requests.delete(id);
this.resolver.delete(id);
});
return result;
}
private processQueue(): void {
if (typeof this.timer === 'number') {
// already queued
return;
}
this.timer = setTimeout(() => {
// make request
const requests = this.requests;
const resolver = this.resolver;
this.proxy.$provideDecorations(this.handle, [...requests.values()], CancellationToken.None).then(data => {
for (const [id, resolve] of resolver) {
resolve(data[id]);
}
});
// reset
this.requests = new Map();
this.resolver = new Map();
this.timer = undefined;
}, 0);
}
}
export class DecorationsMainImpl implements DecorationsMain, Disposable {
private readonly proxy: DecorationsExt;
private readonly providers = new Map<number, [Emitter<URI[]>, Disposable]>();
private readonly decorationsService: DecorationsService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT);
this.decorationsService = container.get(DecorationsService);
}
dispose(): void {
this.providers.forEach(value => value.forEach(v => v.dispose()));
this.providers.clear();
}
async $registerDecorationProvider(handle: number): Promise<void> {
const emitter = new Emitter<URI[]>();
const queue = new DecorationRequestsQueue(this.proxy, handle);
const registration = this.decorationsService.registerDecorationsProvider({
onDidChange: emitter.event,
provideDecorations: async (uri, token) => {
const data = await queue.enqueue(uri, token);
if (!data) {
return undefined;
}
const [bubble, tooltip, letter, themeColor] = data;
return <Decoration>{
weight: 10,
bubble: bubble ?? false,
colorId: themeColor?.id,
tooltip,
letter
};
}
});
this.providers.set(handle, [emitter, registration]);
}
$onDidChange(handle: number, resources: UriComponents[]): void {
const providerSet = this.providers.get(handle);
if (providerSet) {
const [emitter] = providerSet;
emitter.fire(resources && resources.map(r => new URI(VSCodeURI.revive(r).toString())));
}
}
$unregisterDecorationProvider(handle: number): void {
const provider = this.providers.get(handle);
if (provider) {
provider.forEach(p => p.dispose());
this.providers.delete(handle);
}
}
}

View File

@@ -0,0 +1,185 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import { OpenDialogOptionsMain, SaveDialogOptionsMain, DialogsMain, UploadDialogOptionsMain } from '../../common/plugin-api-rpc';
import { OpenFileDialogProps, SaveFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { FileUploadService } from '@theia/filesystem/lib/common/upload/file-upload';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { nls } from '@theia/core';
export class DialogsMainImpl implements DialogsMain {
private workspaceService: WorkspaceService;
private fileService: FileService;
private environments: EnvVariablesServer;
private fileDialogService: FileDialogService;
private uploadService: FileUploadService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.workspaceService = container.get(WorkspaceService);
this.fileService = container.get(FileService);
this.environments = container.get(EnvVariablesServer);
this.fileDialogService = container.get(FileDialogService);
this.uploadService = container.get(FileUploadService);
}
protected async getRootStat(defaultUri: string | undefined): Promise<FileStat | undefined> {
let rootStat: FileStat | undefined;
// Try to use default URI as root
if (defaultUri) {
try {
rootStat = await this.fileService.resolve(new URI(defaultUri));
} catch {
rootStat = undefined;
}
// Try to use as root the parent folder of existing file URI/non existing URI
if (rootStat && !rootStat.isDirectory || !rootStat) {
try {
rootStat = await this.fileService.resolve(new URI(defaultUri).parent);
} catch {
rootStat = undefined;
}
}
}
// Try to use workspace service root if there is no pre-configured URI
if (!rootStat) {
rootStat = (await this.workspaceService.roots)[0];
}
// Try to use current user home if root folder is still not taken
if (!rootStat) {
const homeDirUri = await this.environments.getHomeDirUri();
try {
rootStat = await this.fileService.resolve(new URI(homeDirUri));
} catch { }
}
return rootStat;
}
async $showOpenDialog(options: OpenDialogOptionsMain): Promise<string[] | undefined> {
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
if (!rootStat) {
throw new Error('Unable to find the rootStat');
}
try {
const canSelectFiles = typeof options.canSelectFiles === 'boolean' ? options.canSelectFiles : true;
const canSelectFolders = typeof options.canSelectFolders === 'boolean' ? options.canSelectFolders : true;
let title = options.title;
if (!title) {
if (canSelectFiles && canSelectFolders) {
title = 'Open';
} else {
if (canSelectFiles) {
title = 'Open File';
} else {
title = 'Open Folder';
}
if (options.canSelectMany) {
title += '(s)';
}
}
}
// Create open file dialog props
const dialogProps = {
title: title,
openLabel: options.openLabel,
canSelectFiles: options.canSelectFiles,
canSelectFolders: options.canSelectFolders,
canSelectMany: options.canSelectMany,
filters: options.filters
} as OpenFileDialogProps;
const result = await this.fileDialogService.showOpenDialog(dialogProps, rootStat);
if (Array.isArray(result)) {
return result.map(uri => uri.path.toString());
} else {
return result ? [result].map(uri => uri.path.toString()) : undefined;
}
} catch (error) {
console.error(error);
}
return undefined;
}
async $showSaveDialog(options: SaveDialogOptionsMain): Promise<string | undefined> {
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
// File name field should be empty unless the URI is a file
let fileNameValue = '';
if (options.defaultUri) {
let defaultURIStat: FileStat | undefined;
try {
defaultURIStat = await this.fileService.resolve(new URI(options.defaultUri));
} catch { }
if (defaultURIStat && !defaultURIStat.isDirectory || !defaultURIStat) {
fileNameValue = new URI(options.defaultUri).path.base;
}
}
try {
// Create save file dialog props
const dialogProps = {
title: options.title ?? nls.localizeByDefault('Save'),
saveLabel: options.saveLabel,
filters: options.filters,
inputValue: fileNameValue
} as SaveFileDialogProps;
const result = await this.fileDialogService.showSaveDialog(dialogProps, rootStat);
if (result) {
return result.path.toString();
}
return undefined;
} catch (error) {
console.error(error);
}
return undefined;
}
async $showUploadDialog(options: UploadDialogOptionsMain): Promise<string[] | undefined> {
const rootStat = await this.getRootStat(options.defaultUri);
// Fail if root not fount
if (!rootStat) {
throw new Error('Failed to resolve base directory where files should be uploaded');
}
const uploadResult = await this.uploadService.upload(rootStat.resource.toString());
if (uploadResult) {
return uploadResult.uploaded;
}
return undefined;
}
}

View File

@@ -0,0 +1,112 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@lumino/messaging';
import { codiconArray, Key } from '@theia/core/lib/browser';
import { AbstractDialog } from '@theia/core/lib/browser/dialogs';
import '../../../../src/main/browser/dialogs/style/modal-notification.css';
import { MainMessageItem, MainMessageOptions } from '../../../common/plugin-api-rpc';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { nls } from '@theia/core/lib/common/nls';
export enum MessageType {
Error = 'error',
Warning = 'warning',
Info = 'info'
}
const NOTIFICATION = 'modal-Notification';
const ICON = 'icon';
const TEXT = 'text';
const DETAIL = 'detail';
@injectable()
export class ModalNotification extends AbstractDialog<string | undefined> {
protected actionTitle: string | undefined;
constructor() {
super({ title: FrontendApplicationConfigProvider.get().applicationName });
}
protected override onCloseRequest(msg: Message): void {
this.actionTitle = undefined;
this.accept();
}
get value(): string | undefined {
return this.actionTitle;
}
showDialog(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise<string | undefined> {
this.contentNode.appendChild(this.createMessageNode(messageType, text, options, actions));
return this.open();
}
protected createMessageNode(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): HTMLElement {
const messageNode = document.createElement('div');
messageNode.classList.add(NOTIFICATION);
const iconContainer = messageNode.appendChild(document.createElement('div'));
iconContainer.classList.add(ICON);
const iconElement = iconContainer.appendChild(document.createElement('i'));
iconElement.classList.add(...this.toIconClass(messageType), messageType.toString());
const textContainer = messageNode.appendChild(document.createElement('div'));
textContainer.classList.add(TEXT);
const textElement = textContainer.appendChild(document.createElement('p'));
textElement.textContent = text;
if (options.detail) {
const detailContainer = textContainer.appendChild(document.createElement('div'));
detailContainer.classList.add(DETAIL);
const detailElement = detailContainer.appendChild(document.createElement('p'));
detailElement.textContent = options.detail;
}
actions.forEach((action: MainMessageItem, index: number) => {
const button = index === 0
? this.appendAcceptButton(action.title)
: this.createButton(action.title);
button.classList.add('main');
this.controlPanel.appendChild(button);
this.addKeyListener(button,
Key.ENTER,
() => {
this.actionTitle = action.title;
this.accept();
},
'click');
});
if (actions.length <= 0) {
this.appendAcceptButton();
} else if (!actions.some(action => action.isCloseAffordance === true)) {
this.appendCloseButton(nls.localizeByDefault('Close'));
}
return messageNode;
}
protected toIconClass(icon: MessageType): string[] {
if (icon === MessageType.Error) {
return codiconArray('error');
}
if (icon === MessageType.Warning) {
return codiconArray('warning');
}
return codiconArray('info');
}
}

View File

@@ -0,0 +1,123 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.modal-Notification {
pointer-events: all;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: flex;
flex-direction: row;
-webkit-justify-content: center;
justify-content: center;
clear: both;
box-sizing: border-box;
position: relative;
min-width: 200px;
max-width: min(66vw, 800px);
background-color: var(--theia-editorWidget-background);
min-height: 35px;
margin-bottom: 1px;
color: var(--theia-editorWidget-foreground);
}
.modal-Notification .icon {
display: inline-block;
font-size: 20px;
padding: 5px 0;
width: 35px;
order: 1;
}
.modal-Notification .icon .codicon {
line-height: inherit;
vertical-align: middle;
font-size: calc(var(--theia-ui-padding) * 5);
color: var(--theia-editorInfo-foreground);
}
.modal-Notification .icon .error {
color: var(--theia-editorError-foreground);
}
.modal-Notification .icon .warning {
color: var(--theia-editorWarning-foreground);
}
.modal-Notification .text {
order: 2;
display: inline-block;
max-height: min(66vh, 600px);
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
align-self: center;
flex: 1 100%;
padding: 10px;
overflow: auto;
white-space: pre-wrap;
}
.modal-Notification .text > p {
margin: 0;
font-size: var(--theia-ui-font-size1);
font-family: var(--theia-ui-font-family);
vertical-align: middle;
}
.modal-Notification .buttons {
display: flex;
flex-direction: row;
order: 3;
white-space: nowrap;
align-self: flex-end;
height: 40px;
}
.modal-Notification .buttons > button {
background-color: var(--theia-button-background);
color: var(--theia-button-foreground);
border: none;
border-radius: 0;
text-align: center;
text-decoration: none;
display: inline-block;
padding: 0 10px;
margin: 0;
font-size: var(--theia-ui-font-size1);
outline: none;
cursor: pointer;
}
.modal-Notification .buttons > button:hover {
background-color: var(--theia-button-hoverBackground);
}
.modal-Notification .detail {
align-self: center;
order: 3;
flex: 1 100%;
color: var(--theia-descriptionForeground);
}
.modal-Notification .detail > p {
margin: calc(var(--theia-ui-padding) * 2) 0px 0px 0px;
}
.modal-Notification .text {
padding: calc(var(--theia-ui-padding) * 1.5);
}

View File

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

View File

@@ -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 };
}
}

View 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;
}

View 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));
}
}

View File

@@ -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),
};
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// 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 { PluginContribution, Keybinding as PluginKeybinding } from '../../../common';
import { Keybinding } from '@theia/core/lib/common/keybinding';
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
import { OS } from '@theia/core/lib/common/os';
import { Disposable } from '@theia/core/lib/common/disposable';
import { DisposableCollection } from '@theia/core';
@injectable()
export class KeybindingsContributionPointHandler {
@inject(KeybindingRegistry)
private readonly keybindingRegistry: KeybindingRegistry;
handle(contributions: PluginContribution): Disposable {
if (!contributions || !contributions.keybindings) {
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
for (const raw of contributions.keybindings) {
const keybinding = this.toKeybinding(raw);
if (keybinding) {
toDispose.push(this.keybindingRegistry.registerKeybinding(keybinding));
}
}
return toDispose;
}
protected toKeybinding(pluginKeybinding: PluginKeybinding): Keybinding | undefined {
const keybinding = this.toOSKeybinding(pluginKeybinding);
if (!keybinding) {
return undefined;
}
const { command, when, args } = pluginKeybinding;
return { keybinding, command, when, args };
}
protected toOSKeybinding(pluginKeybinding: PluginKeybinding): string | undefined {
let keybinding: string | undefined;
const os = OS.type();
if (os === OS.Type.Windows) {
keybinding = pluginKeybinding.win;
} else if (os === OS.Type.OSX) {
keybinding = pluginKeybinding.mac;
} else {
keybinding = pluginKeybinding.linux;
}
return keybinding || pluginKeybinding.keybinding;
}
}

View File

@@ -0,0 +1,51 @@
// *****************************************************************************
// 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 { LabelServiceMain } from '../../common/plugin-api-rpc';
import { interfaces } from '@theia/core/shared/inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { DefaultUriLabelProviderContribution, LabelProviderContribution } from '@theia/core/lib/browser';
import { ContributionProvider } from '@theia/core/lib/common';
import { ResourceLabelFormatter } from '@theia/core/lib/common/label-protocol';
export class LabelServiceMainImpl implements LabelServiceMain {
private readonly resourceLabelFormatters = new Map<number, Disposable>();
private readonly contributionProvider: ContributionProvider<LabelProviderContribution>;
constructor(container: interfaces.Container) {
this.contributionProvider = container.getNamed(ContributionProvider, LabelProviderContribution);
}
$registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void {
// Dynamically registered formatters should have priority over those contributed via package.json
formatter.priority = true;
const disposables: DisposableCollection = new DisposableCollection();
for (const contribution of this.contributionProvider.getContributions()) {
if (contribution instanceof DefaultUriLabelProviderContribution) {
disposables.push(contribution.registerFormatter(formatter));
}
}
this.resourceLabelFormatters.set(handle, disposables);
}
$unregisterResourceLabelFormatter(handle: number): void {
const toDispose = this.resourceLabelFormatters.get(handle);
if (toDispose) {
toDispose.dispose();
}
this.resourceLabelFormatters.delete(handle);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,176 @@
// *****************************************************************************
// 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 { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import {
McpServerDefinitionRegistryMain,
McpServerDefinitionRegistryExt,
McpServerDefinitionDto,
isMcpHttpServerDefinitionDto,
} from '../../common/lm-protocol';
import { MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
// import { MCPServerManager, MCPServerDescription, RemoteMCPServerDescription } from '@theia/ai-mcp/lib/common';
// ai-mcp package is disabled, so types are replaced with 'any' throughout this file
import { URI } from '@theia/core';
export class McpServerDefinitionRegistryMainImpl implements McpServerDefinitionRegistryMain {
private readonly proxy: McpServerDefinitionRegistryExt;
private readonly providers = new Map<number, string>();
private readonly mcpServerManager: any | undefined;
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_EXT);
try {
// MCPServerManager is from disabled ai-mcp package, so this will always fail gracefully
this.mcpServerManager = (container as any).get(Symbol.for('MCPServerManager'));
} catch {
// MCP Server Manager is optional
this.mcpServerManager = undefined;
}
}
$registerMcpServerDefinitionProvider(handle: number, name: string): void {
this.providers.set(handle, name);
this.loadServerDefinitions(handle);
}
async $unregisterMcpServerDefinitionProvider(handle: number): Promise<void> {
if (!this.mcpServerManager) {
console.warn('MCP Server Manager not available - MCP server definitions will not be loaded');
return;
}
const provider = this.providers.get(handle);
if (!provider) {
console.warn(`No MCP Server provider found for handle '${handle}' - MCP server definitions will not be loaded`);
return;
}
// Get all servers provided by this provider and remove them server by server
try {
const definitions = await this.$getServerDefinitions(handle);
for (const definition of definitions) {
this.mcpServerManager.removeServer(definition.label);
}
} catch (error) {
console.error('Error getting server definitions for removal:', error);
}
this.providers.delete(handle);
}
$onDidChangeMcpServerDefinitions(handle: number): void {
// Reload server definitions when provider reports changes
this.loadServerDefinitions(handle);
}
async $getServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]> {
try {
return await this.proxy.$provideServerDefinitions(handle);
} catch (error) {
console.error('Error getting MCP server definitions:', error);
return [];
}
}
async $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined> {
try {
return await this.proxy.$resolveServerDefinition(handle, server);
} catch (error) {
console.error('Error resolving MCP server definition:', error);
return server;
}
}
private async loadServerDefinitions(handle: number): Promise<void> {
if (!this.mcpServerManager) {
console.warn('MCP Server Manager not available - MCP server definitions will not be loaded');
return;
}
try {
const definitions = await this.$getServerDefinitions(handle);
for (const definition of definitions) {
const mcpServerDescription = this.convertToMcpServerDescription(handle, definition);
this.mcpServerManager.addOrUpdateServer(mcpServerDescription);
}
} catch (error) {
console.error('Error loading MCP server definitions:', error);
}
}
private convertToMcpServerDescription(handle: number, definition: McpServerDefinitionDto): any {
const self = this;
if (isMcpHttpServerDefinitionDto(definition)) {
// Convert headers values to strings, filtering out null values
let convertedHeaders: Record<string, string> | undefined;
if (definition.headers) {
convertedHeaders = {};
for (const [key, value] of Object.entries(definition.headers)) {
if (value !== null) {
convertedHeaders[key] = String(value);
}
}
}
const serverDescription: any = {
name: definition.label,
serverUrl: URI.fromComponents(definition.uri).toString(),
headers: convertedHeaders,
autostart: false,
async resolve(description: any): Promise<any> {
const resolved = await self.$resolveServerDefinition(handle, definition);
if (resolved) {
return self.convertToMcpServerDescription(handle, resolved);
}
return description;
}
};
return serverDescription;
}
// Convert env values to strings, filtering out null values
let convertedEnv: Record<string, string> | undefined;
if (definition.env) {
convertedEnv = {};
for (const [key, value] of Object.entries(definition.env)) {
if (value !== null) {
convertedEnv[key] = String(value);
}
}
}
return {
name: definition.label,
command: definition.command!,
args: definition.args,
env: convertedEnv,
autostart: false, // Extensions should manage their own server lifecycle
async resolve(serverDescription: any): Promise<any> {
const resolved = await self.$resolveServerDefinition(handle, definition);
if (resolved) {
return self.convertToMcpServerDescription(handle, resolved);
}
return serverDescription;
}
};
}
}

View File

@@ -0,0 +1,34 @@
// *****************************************************************************
// Copyright (C) 2023 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { nls } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
import { LocalizationMain } from '../../common/plugin-api-rpc';
import { LanguagePackBundle, LanguagePackService } from '../../common/language-pack-service';
export class LocalizationMainImpl implements LocalizationMain {
private readonly languagePackService: LanguagePackService;
constructor(container: interfaces.Container) {
this.languagePackService = container.get(LanguagePackService);
}
async $fetchBundle(id: string): Promise<LanguagePackBundle | undefined> {
const bundle = await this.languagePackService.getBundle(id, nls.locale ?? nls.defaultLocale);
return bundle;
}
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// 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
// *****************************************************************************
/* eslint-disable @typescript-eslint/no-explicit-any */
import { interfaces } from '@theia/core/shared/inversify';
import { LoggerMain, LogLevel } from '../../common';
import { ILogger } from '@theia/core';
export class LoggerMainImpl implements LoggerMain {
constructor(private readonly container: interfaces.Container) {
}
$log(level: LogLevel, name: string | undefined, message: string, params: any[]): void {
let logger: ILogger;
if (name) {
logger = this.container.getNamed<ILogger>(ILogger, name);
} else {
logger = this.container.get<ILogger>(ILogger);
}
switch (level) {
case LogLevel.Trace:
logger.trace(message, ...params);
break;
case LogLevel.Debug:
logger.debug(message, ...params);
break;
case LogLevel.Info:
logger.info(message, ...params);
break;
case LogLevel.Warn:
logger.warn(message, ...params);
break;
case LogLevel.Error:
logger.error(message, ...params);
break;
}
}
}

View File

@@ -0,0 +1,218 @@
// *****************************************************************************
// 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 { CommandRegistryMainImpl } from './command-registry-main';
import { PreferenceRegistryMainImpl } from './preference-registry-main';
import { QuickOpenMainImpl } from './quick-open-main';
import { RPCProtocol } from '../../common/rpc-protocol';
import { PLUGIN_RPC_CONTEXT, LanguagesMainFactory, OutputChannelRegistryFactory, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { MessageRegistryMainImpl } from './message-registry-main';
import { WindowStateMain } from './window-state-main';
import { WorkspaceMainImpl } from './workspace-main';
import { StatusBarMessageRegistryMainImpl } from './status-bar-message-registry-main';
import { EnvMainImpl } from './env-main';
import { EditorsAndDocumentsMain } from './editors-and-documents-main';
import { TerminalServiceMainImpl } from './terminal-main';
import { DialogsMainImpl } from './dialogs-main';
import { TreeViewsMainImpl } from './view/tree-views-main';
import { NotificationMainImpl } from './notification-main';
import { ConnectionImpl } from '../../common/connection';
import { WebviewsMainImpl } from './webviews-main';
import { TasksMainImpl } from './tasks-main';
import { StorageMainImpl } from './plugin-storage';
import { DebugMainImpl } from './debug/debug-main';
import { FileSystemMainImpl } from './file-system-main-impl';
import { ScmMainImpl } from './scm-main';
import { DecorationsMainImpl } from './decorations/decorations-main';
import { ClipboardMainImpl } from './clipboard-main';
import { DocumentsMainImpl } from './documents-main';
import { TextEditorsMainImpl } from './text-editors-main';
import { EditorModelService } from './text-editor-model-service';
import { OpenerService } from '@theia/core/lib/browser/opener-service';
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
import { MainFileSystemEventService } from './main-file-system-event-service';
import { LabelServiceMainImpl } from './label-service-main';
import { TimelineMainImpl } from './timeline-main';
import { AuthenticationMainImpl } from './authentication-main';
import { ThemingMainImpl } from './theming-main';
import { CommentsMainImp } from './comments/comments-main';
import { CustomEditorsMainImpl } from './custom-editors/custom-editors-main';
import { SecretsMainImpl } from './secrets-main';
import { WebviewViewsMainImpl } from './webview-views/webview-views-main';
import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
import { UntitledResourceResolver } from '@theia/core/lib/common/resource';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { TabsMainImpl } from './tabs/tabs-main';
import { NotebooksMainImpl } from './notebooks/notebooks-main';
import { LocalizationMainImpl } from './localization-main';
import { NotebookRenderersMainImpl } from './notebooks/notebook-renderers-main';
import { NotebookEditorsMainImpl } from './notebooks/notebook-editors-main';
import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main';
import { NotebookKernelsMainImpl } from './notebooks/notebook-kernels-main';
import { NotebooksAndEditorsMain } from './notebooks/notebook-documents-and-editors-main';
import { TestingMainImpl } from './test-main';
import { UriMainImpl } from './uri-main';
import { LoggerMainImpl } from './logger-main';
import { McpServerDefinitionRegistryMainImpl } from './lm-main';
export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
const loggerMain = new LoggerMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.LOGGER_MAIN, loggerMain);
const authenticationMain = new AuthenticationMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.AUTHENTICATION_MAIN, authenticationMain);
const commandRegistryMain = new CommandRegistryMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.COMMAND_REGISTRY_MAIN, commandRegistryMain);
const quickOpenMain = new QuickOpenMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.QUICK_OPEN_MAIN, quickOpenMain);
const workspaceMain = new WorkspaceMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.WORKSPACE_MAIN, workspaceMain);
const dialogsMain = new DialogsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.DIALOGS_MAIN, dialogsMain);
const messageRegistryMain = new MessageRegistryMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.MESSAGE_REGISTRY_MAIN, messageRegistryMain);
const preferenceRegistryMain = new PreferenceRegistryMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.PREFERENCE_REGISTRY_MAIN, preferenceRegistryMain);
const tabsMain = new TabsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TABS_MAIN, tabsMain);
const editorsAndDocuments = new EditorsAndDocumentsMain(rpc, container, tabsMain);
const notebookDocumentsMain = new NotebookDocumentsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_MAIN, notebookDocumentsMain);
const modelService = container.get(EditorModelService);
const openerService = container.get<OpenerService>(OpenerService);
const shell = container.get(ApplicationShell);
const untitledResourceResolver = container.get(UntitledResourceResolver);
const languageService = container.get(MonacoLanguages);
const documentsMain = new DocumentsMainImpl(editorsAndDocuments, notebookDocumentsMain, modelService, rpc,
openerService, shell, untitledResourceResolver, languageService);
rpc.set(PLUGIN_RPC_CONTEXT.DOCUMENTS_MAIN, documentsMain);
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOKS_MAIN, new NotebooksMainImpl(rpc, container, commandRegistryMain));
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_MAIN, new NotebookRenderersMainImpl(rpc, container));
const notebookEditorsMain = new NotebookEditorsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_EDITORS_MAIN, notebookEditorsMain);
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_AND_EDITORS_MAIN, new NotebooksAndEditorsMain(rpc, container, tabsMain, notebookDocumentsMain, notebookEditorsMain));
rpc.set(PLUGIN_RPC_CONTEXT.NOTEBOOK_KERNELS_MAIN, new NotebookKernelsMainImpl(rpc, container));
const editorsMain = new TextEditorsMainImpl(editorsAndDocuments, documentsMain, rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TEXT_EDITORS_MAIN, editorsMain);
// start listening only after all clients are subscribed to events
editorsAndDocuments.listen();
const statusBarMessageRegistryMain = new StatusBarMessageRegistryMainImpl(container, rpc);
rpc.set(PLUGIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_MAIN, statusBarMessageRegistryMain);
const envMain = new EnvMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.ENV_MAIN, envMain);
const notificationMain = new NotificationMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.NOTIFICATION_MAIN, notificationMain);
const testingMain = new TestingMainImpl(rpc, container, commandRegistryMain);
rpc.set(PLUGIN_RPC_CONTEXT.TESTING_MAIN, testingMain);
const terminalMain = new TerminalServiceMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TERMINAL_MAIN, terminalMain);
const treeViewsMain = new TreeViewsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TREE_VIEWS_MAIN, treeViewsMain);
const outputChannelRegistryFactory: OutputChannelRegistryFactory = container.get(OutputChannelRegistryFactory);
const outputChannelRegistryMain = outputChannelRegistryFactory();
rpc.set(PLUGIN_RPC_CONTEXT.OUTPUT_CHANNEL_REGISTRY_MAIN, outputChannelRegistryMain);
const languagesMainFactory: LanguagesMainFactory = container.get(LanguagesMainFactory);
const languagesMain = languagesMainFactory(rpc);
rpc.set(PLUGIN_RPC_CONTEXT.LANGUAGES_MAIN, languagesMain);
const webviewsMain = new WebviewsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.WEBVIEWS_MAIN, webviewsMain);
const customEditorsMain = new CustomEditorsMainImpl(rpc, container, webviewsMain);
rpc.set(PLUGIN_RPC_CONTEXT.CUSTOM_EDITORS_MAIN, customEditorsMain);
const webviewViewsMain = new WebviewViewsMainImpl(rpc, container, webviewsMain);
rpc.set(PLUGIN_RPC_CONTEXT.WEBVIEW_VIEWS_MAIN, webviewViewsMain);
const storageMain = new StorageMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.STORAGE_MAIN, storageMain);
const connectionMain = new ConnectionImpl(rpc.getProxy(MAIN_RPC_CONTEXT.CONNECTION_EXT));
rpc.set(PLUGIN_RPC_CONTEXT.CONNECTION_MAIN, connectionMain);
const tasksMain = new TasksMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TASKS_MAIN, tasksMain);
const debugMain = new DebugMainImpl(rpc, connectionMain, container);
rpc.set(PLUGIN_RPC_CONTEXT.DEBUG_MAIN, debugMain);
const fs = new FileSystemMainImpl(rpc, container);
const fsEventService = new MainFileSystemEventService(rpc, container);
const disposeFS = fs.dispose.bind(fs);
fs.dispose = () => {
fsEventService.dispose();
disposeFS();
};
rpc.set(PLUGIN_RPC_CONTEXT.FILE_SYSTEM_MAIN, fs);
const scmMain = new ScmMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.SCM_MAIN, scmMain);
const secretsMain = new SecretsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.SECRETS_MAIN, secretsMain);
const decorationsMain = new DecorationsMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.DECORATIONS_MAIN, decorationsMain);
const windowMain = new WindowStateMain(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.WINDOW_MAIN, windowMain);
const clipboardMain = new ClipboardMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.CLIPBOARD_MAIN, clipboardMain);
const labelServiceMain = new LabelServiceMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.LABEL_SERVICE_MAIN, labelServiceMain);
const timelineMain = new TimelineMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.TIMELINE_MAIN, timelineMain);
const themingMain = new ThemingMainImpl(rpc, container.get(ThemeService));
rpc.set(PLUGIN_RPC_CONTEXT.THEMING_MAIN, themingMain);
const commentsMain = new CommentsMainImp(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.COMMENTS_MAIN, commentsMain);
const localizationMain = new LocalizationMainImpl(container);
rpc.set(PLUGIN_RPC_CONTEXT.LOCALIZATION_MAIN, localizationMain);
const uriMain = new UriMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.URI_MAIN, uriMain);
const mcpServerDefinitionRegistryMain = new McpServerDefinitionRegistryMainImpl(rpc, container);
rpc.set(PLUGIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_MAIN, mcpServerDefinitionRegistryMain);
}

View File

@@ -0,0 +1,76 @@
// *****************************************************************************
// 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/mainThreadFileSystemEventService.ts
import { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import { MAIN_RPC_CONTEXT, FileSystemEvents } from '../../common/plugin-api-rpc';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileChangeType } from '@theia/filesystem/lib/common/files';
export class MainFileSystemEventService {
private readonly toDispose = new DisposableCollection();
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
const proxy = rpc.getProxy(MAIN_RPC_CONTEXT.ExtHostFileSystemEventService);
const fileService = container.get(FileService);
this.toDispose.push(fileService.onDidFilesChange(event => {
// file system events - (changes the editor and others make)
const events: FileSystemEvents = {
created: [],
changed: [],
deleted: []
};
for (const change of event.changes) {
switch (change.type) {
case FileChangeType.ADDED:
events.created.push(change.resource['codeUri']);
break;
case FileChangeType.UPDATED:
events.changed.push(change.resource['codeUri']);
break;
case FileChangeType.DELETED:
events.deleted.push(change.resource['codeUri']);
break;
}
}
proxy.$onFileEvent(events);
}));
// BEFORE file operation
fileService.addFileOperationParticipant({
participate: (target, source, operation, timeout, token) => proxy.$onWillRunFileOperation(operation, target['codeUri'], source?.['codeUri'], timeout, token)
});
// AFTER file operation
this.toDispose.push(fileService.onDidRunUserOperation(e => proxy.$onDidRunFileOperation(e.operation, e.target['codeUri'], e.source?.['codeUri'])));
}
dispose(): void {
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,192 @@
// *****************************************************************************
// 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 { inject, injectable, optional } from '@theia/core/shared/inversify';
import { MenuPath, CommandRegistry, Disposable, DisposableCollection, nls, CommandMenu, AcceleratorSource, ContextExpressionMatcher } from '@theia/core';
import { MenuModelRegistry } from '@theia/core/lib/common';
import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { DeployedPlugin, IconUrl, Menu } from '../../../common';
import { ScmWidget } from '@theia/scm/lib/browser/scm-widget';
import { KeybindingRegistry, QuickCommandService, codicon } from '@theia/core/lib/browser';
import {
CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint,
PLUGIN_EDITOR_TITLE_MENU, PLUGIN_EDITOR_TITLE_RUN_MENU, PLUGIN_SCM_TITLE_MENU, PLUGIN_VIEW_TITLE_MENU
} from './vscode-theia-menu-mappings';
import { PluginMenuCommandAdapter } from './plugin-menu-command-adapter';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { PluginSharedStyle } from '../plugin-shared-style';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
@injectable()
export class MenusContributionPointHandler {
@inject(MenuModelRegistry) private readonly menuRegistry: MenuModelRegistry;
@inject(CommandRegistry) private readonly commandRegistry: CommandRegistry;
@inject(TabBarToolbarRegistry) private readonly tabBarToolbar: TabBarToolbarRegistry;
@inject(PluginMenuCommandAdapter) pluginMenuCommandAdapter: PluginMenuCommandAdapter;
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
@inject(PluginSharedStyle) protected readonly style: PluginSharedStyle;
@inject(KeybindingRegistry) keybindingRegistry: KeybindingRegistry;
@inject(QuickCommandService) @optional()
private readonly quickCommandService: QuickCommandService;
private initialized = false;
private initialize(): void {
this.initialized = true;
this.tabBarToolbar.registerMenuDelegate(PLUGIN_EDITOR_TITLE_MENU, widget => CodeEditorWidgetUtil.is(widget));
this.menuRegistry.registerSubmenu(PLUGIN_EDITOR_TITLE_RUN_MENU, 'EditorTitleRunMenu');
this.tabBarToolbar.registerItem({
id: this.tabBarToolbar.toElementId(PLUGIN_EDITOR_TITLE_RUN_MENU),
menuPath: PLUGIN_EDITOR_TITLE_RUN_MENU,
icon: codicon('debug-alt'),
text: nls.localizeByDefault('Run or Debug...'),
command: '',
group: 'navigation',
isVisible: widget => CodeEditorWidgetUtil.is(widget)
});
this.tabBarToolbar.registerMenuDelegate(PLUGIN_SCM_TITLE_MENU, widget => widget instanceof ScmWidget);
this.tabBarToolbar.registerMenuDelegate(PLUGIN_VIEW_TITLE_MENU, widget => !CodeEditorWidgetUtil.is(widget));
}
private getMatchingTheiaMenuPaths(contributionPoint: string): MenuPath[] | undefined {
return codeToTheiaMappings.get(contributionPoint);
}
handle(plugin: DeployedPlugin): Disposable {
const allMenus = plugin.contributes?.menus;
if (!allMenus) {
return Disposable.NULL;
}
if (!this.initialized) {
this.initialize();
}
const toDispose = new DisposableCollection();
const submenus = plugin.contributes?.submenus ?? [];
for (const submenu of submenus) {
const iconClass = submenu.icon && this.toIconClass(submenu.icon, toDispose);
this.menuRegistry.registerSubmenu([submenu.id], submenu.label, { icon: iconClass });
}
for (const [contributionPoint, items] of Object.entries(allMenus)) {
for (const item of items) {
try {
if (contributionPoint === 'commandPalette') {
toDispose.push(this.registerCommandPaletteAction(item));
} else {
let targets = this.getMatchingTheiaMenuPaths(contributionPoint as ContributionPoint);
if (!targets) {
targets = [[contributionPoint]];
}
const { group, order } = this.parseGroup(item.group);
const { submenu, command } = item;
if (submenu && command) {
console.warn(
`Menu item ${command} from plugin ${plugin.metadata.model.id} contributed both submenu and command. Only command will be registered.`
);
}
if (command) {
targets.forEach(target => {
const menuPath = group ? [...target, group] : target;
const cmd = this.commandRegistry.getCommand(command);
if (!cmd) {
console.debug(`No label for action menu node: No command "${command}" exists.`);
return;
}
const label = cmd.label || cmd.id;
const icon = cmd.iconClass;
const action: CommandMenu & AcceleratorSource = {
id: command,
sortString: order || '',
isVisible: <T>(effectiveMenuPath: MenuPath, contextMatcher: ContextExpressionMatcher<T>, context: T | undefined, ...args: any[]): boolean => {
if (item.when && !contextMatcher.match(item.when, context)) {
return false;
}
return this.commandRegistry.isVisible(command, ...this.pluginMenuCommandAdapter.getArgumentAdapter(effectiveMenuPath)(...args));
},
icon: icon,
label: label,
isEnabled: (effeciveMenuPath: MenuPath, ...args: any[]): boolean =>
this.commandRegistry.isEnabled(command, ...this.pluginMenuCommandAdapter.getArgumentAdapter(effeciveMenuPath)(...args)),
run: (effeciveMenuPath: MenuPath, ...args: any[]): Promise<void> =>
this.commandRegistry.executeCommand(command, ...this.pluginMenuCommandAdapter.getArgumentAdapter(effeciveMenuPath)(...args)),
isToggled: (effectiveMenuPath: MenuPath) => false,
getAccelerator: (context: HTMLElement | undefined): string[] => {
const bindings = this.keybindingRegistry.getKeybindingsForCommand(command);
// Only consider the first active keybinding.
if (bindings.length) {
const binding = bindings.find(b => this.keybindingRegistry.isEnabledInScope(b, context));
if (binding) {
return this.keybindingRegistry.acceleratorFor(binding, '+', true);
}
}
return [];
}
};
toDispose.push(this.menuRegistry.registerCommandMenu(menuPath, action));
});
} else if (submenu) {
targets.forEach(target => toDispose.push(this.menuRegistry.linkCompoundMenuNode({
newParentPath: group ? [...target, group] : target,
submenuPath: [submenu!],
order: order,
when: item.when
})));
}
}
} catch (error) {
console.warn(`Failed to register a menu item for plugin ${plugin.metadata.model.id} contributed to ${contributionPoint}`, item);
console.debug(error);
}
}
}
return toDispose;
}
private parseGroup(rawGroup?: string): { group?: string, order?: string } {
if (!rawGroup) { return {}; }
const separatorIndex = rawGroup.lastIndexOf('@');
if (separatorIndex > -1) {
return { group: rawGroup.substring(0, separatorIndex), order: rawGroup.substring(separatorIndex + 1) || undefined };
}
return { group: rawGroup };
}
private registerCommandPaletteAction(menu: Menu): Disposable {
if (menu.command && menu.when) {
return this.quickCommandService.pushCommandContext(menu.command, menu.when);
}
return Disposable.NULL;
}
protected toIconClass(url: IconUrl, toDispose: DisposableCollection): string | undefined {
if (typeof url === 'string') {
const asThemeIcon = ThemeIcon.fromString(url);
if (asThemeIcon) {
return ThemeIcon.asClassName(asThemeIcon);
}
}
const reference = this.style.toIconClass(url);
toDispose.push(reference);
return reference.object.iconClass;
}
}

View File

@@ -0,0 +1,271 @@
// *****************************************************************************
// 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 { MenuPath, SelectionService, UriSelection } from '@theia/core';
import { ResourceContextKey } from '@theia/core/lib/browser/resource-context-key';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
import { TreeWidgetSelection } from '@theia/core/lib/browser/tree/tree-widget-selection';
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { DirtyDiffWidget } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
import { Change, LineRange } from '@theia/scm/lib/browser/dirty-diff/diff-computer';
import { IChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
import { TimelineItem } from '@theia/timeline/lib/common/timeline-model';
import { ScmCommandArg, TimelineCommandArg, TreeViewItemReference } from '../../../common';
import { TestItemReference, TestMessageArg } from '../../../common/test-types';
import { PluginScmProvider, PluginScmResource, PluginScmResourceGroup } from '../scm-main';
import { TreeViewWidget } from '../view/tree-view-widget';
import { CodeEditorWidgetUtil, codeToTheiaMappings, ContributionPoint } from './vscode-theia-menu-mappings';
import { TestItem, TestMessage } from '@theia/test/lib/browser/test-service';
export type ArgumentAdapter = (...args: unknown[]) => unknown[];
function identity(...args: unknown[]): unknown[] {
return args;
}
@injectable()
export class PluginMenuCommandAdapter {
@inject(ScmService) private readonly scmService: ScmService;
@inject(SelectionService) private readonly selectionService: SelectionService;
@inject(ResourceContextKey) private readonly resourceContextKey: ResourceContextKey;
protected readonly argumentAdapters = new Map<string, ArgumentAdapter>();
@postConstruct()
protected init(): void {
const toCommentArgs: ArgumentAdapter = (...args) => this.toCommentArgs(...args);
const toTestMessageArgs: ArgumentAdapter = (...args) => this.toTestMessageArgs(...args);
const firstArgOnly: ArgumentAdapter = (...args) => [args[0]];
const noArgs: ArgumentAdapter = () => [];
const toScmArgs: ArgumentAdapter = (...args) => this.toScmArgs(...args);
const selectedResource = () => this.getSelectedResources();
const widgetURI: ArgumentAdapter = widget => CodeEditorWidgetUtil.is(widget) ? [CodeEditorWidgetUtil.getResourceUri(widget)] : [];
(<Array<[ContributionPoint, ArgumentAdapter]>>[
['comments/comment/context', toCommentArgs],
['comments/comment/title', toCommentArgs],
['comments/commentThread/context', toCommentArgs],
['debug/callstack/context', firstArgOnly],
['debug/variables/context', firstArgOnly],
['debug/toolBar', noArgs],
['editor/context', selectedResource],
['editor/content', widgetURI],
['editor/title', widgetURI],
['editor/title/context', selectedResource],
['editor/title/run', widgetURI],
['explorer/context', selectedResource],
['scm/resourceFolder/context', toScmArgs],
['scm/resourceGroup/context', toScmArgs],
['scm/resourceState/context', toScmArgs],
['scm/title', () => [this.toScmArg(this.scmService.selectedRepository)]],
['testing/message/context', toTestMessageArgs],
['testing/profiles/context', noArgs],
['scm/change/title', (...args) => this.toScmChangeArgs(...args)],
['timeline/item/context', (...args) => this.toTimelineArgs(...args)],
['view/item/context', (...args) => this.toTreeArgs(...args)],
['view/title', noArgs],
['webview/context', firstArgOnly],
['extension/context', noArgs],
['terminal/context', noArgs],
['terminal/title/context', noArgs],
]).forEach(([contributionPoint, adapter]) => {
this.argumentAdapters.set(contributionPoint, adapter);
});
}
getArgumentAdapter(menuPath: MenuPath): ArgumentAdapter {
for (const [contributionPoint, menuPaths] of codeToTheiaMappings) {
for (const theiaPath of menuPaths) {
if (this.isPrefixOf(theiaPath, menuPath)) {
return this.argumentAdapters.get(contributionPoint) || identity;
}
}
}
return identity;
}
private isPrefixOf(candidate: string[], menuPath: MenuPath): boolean {
if (candidate.length > menuPath.length) {
return false;
}
for (let i = 0; i < candidate.length; i++) {
if (candidate[i] !== menuPath[i]) {
return false;
}
}
return true;
}
/* eslint-disable @typescript-eslint/no-explicit-any */
protected toCommentArgs(...args: any[]): any[] {
const arg = args[0];
if ('text' in arg) {
if ('commentUniqueId' in arg) {
return [{
commentControlHandle: arg.thread.controllerHandle,
commentThreadHandle: arg.thread.commentThreadHandle,
text: arg.text,
commentUniqueId: arg.commentUniqueId
}];
}
return [{
commentControlHandle: arg.thread.controllerHandle,
commentThreadHandle: arg.thread.commentThreadHandle,
text: arg.text
}];
}
return [{
commentControlHandle: arg.thread.controllerHandle,
commentThreadHandle: arg.thread.commentThreadHandle,
commentUniqueId: arg.commentUniqueId
}];
}
protected toScmArgs(...args: any[]): any[] {
const scmArgs: any[] = [];
for (const arg of args) {
const scmArg = this.toScmArg(arg);
if (scmArg) {
scmArgs.push(scmArg);
}
}
return scmArgs;
}
protected toScmArg(arg: any): ScmCommandArg | undefined {
if (arg instanceof ScmRepository && arg.provider instanceof PluginScmProvider) {
return {
sourceControlHandle: arg.provider.handle
};
}
if (arg instanceof PluginScmResourceGroup) {
return {
sourceControlHandle: arg.provider.handle,
resourceGroupHandle: arg.handle
};
}
if (arg instanceof PluginScmResource) {
return {
sourceControlHandle: arg.group.provider.handle,
resourceGroupHandle: arg.group.handle,
resourceStateHandle: arg.handle
};
}
}
protected toScmChangeArgs(...args: any[]): any[] {
const arg = args[0];
if (arg instanceof DirtyDiffWidget) {
const toIChange = (change: Change): IChange => {
const convert = (range: LineRange): [number, number] => {
let startLineNumber;
let endLineNumber;
if (!LineRange.isEmpty(range)) {
startLineNumber = range.start + 1;
endLineNumber = range.end;
} else {
startLineNumber = range.start;
endLineNumber = 0;
}
return [startLineNumber, endLineNumber];
};
const { previousRange, currentRange } = change;
const [originalStartLineNumber, originalEndLineNumber] = convert(previousRange);
const [modifiedStartLineNumber, modifiedEndLineNumber] = convert(currentRange);
return {
originalStartLineNumber,
originalEndLineNumber,
modifiedStartLineNumber,
modifiedEndLineNumber
};
};
return [
arg.uri['codeUri'],
arg.changes.map(toIChange),
arg.currentChangeIndex
];
}
return [];
}
protected toTimelineArgs(...args: any[]): any[] {
const timelineArgs: any[] = [];
const arg = args[0];
timelineArgs.push(this.toTimelineArg(arg));
timelineArgs.push(CodeUri.parse(arg.uri));
timelineArgs.push(arg.source ?? '');
return timelineArgs;
}
protected toTestMessageArgs(...args: any[]): any[] {
let testItem: TestItem | undefined;
let testMessage: TestMessage | undefined;
for (const arg of args) {
if (TestItem.is(arg)) {
testItem = arg;
} else if (Array.isArray(arg) && TestMessage.is(arg[0])) {
testMessage = arg[0];
}
}
if (testMessage) {
const testItemReference = (testItem && testItem.controller) ? TestItemReference.create(testItem.controller.id, testItem.path) : undefined;
const testMessageDTO = {
message: testMessage.message,
actual: testMessage.actual,
expected: testMessage.expected,
contextValue: testMessage.contextValue,
location: testMessage.location,
stackTrace: testMessage.stackTrace
};
return [TestMessageArg.create(testItemReference, testMessageDTO)];
}
return [];
}
protected toTimelineArg(arg: TimelineItem): TimelineCommandArg {
return {
timelineHandle: arg.handle,
source: arg.source,
uri: arg.uri
};
}
protected toTreeArgs(...args: any[]): any[] {
const treeArgs: any[] = [];
for (const arg of args) {
if (TreeViewItemReference.is(arg)) {
treeArgs.push(arg);
} else if (Array.isArray(arg)) {
treeArgs.push(arg.filter(TreeViewItemReference.is));
}
}
return treeArgs;
}
protected getSelectedResources(): [CodeUri | TreeViewItemReference | undefined, CodeUri[] | undefined] {
const selection = this.selectionService.selection;
const resourceKey = this.resourceContextKey.get();
const resourceUri = resourceKey ? CodeUri.parse(resourceKey) : undefined;
const firstMember = TreeWidgetSelection.is(selection) && selection.source instanceof TreeViewWidget && selection[0]
? selection.source.toTreeViewItemReference(selection[0])
: UriSelection.getUri(selection)?.['codeUri'] ?? resourceUri;
const secondMember = TreeWidgetSelection.is(selection)
? UriSelection.getUris(selection).map(uri => uri['codeUri'])
: undefined;
return [firstMember, secondMember];
}
/* eslint-enable @typescript-eslint/no-explicit-any */
}

View File

@@ -0,0 +1,119 @@
// *****************************************************************************
// 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 { MenuPath } from '@theia/core';
import { CommonMenus, SHELL_TABBAR_CONTEXT_MENU } from '@theia/core/lib/browser';
import { Navigatable } from '@theia/core/lib/browser/navigatable';
import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
import { DebugStackFramesWidget } from '@theia/debug/lib/browser/view/debug-stack-frames-widget';
import { DebugThreadsWidget } from '@theia/debug/lib/browser/view/debug-threads-widget';
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
import { DebugVariablesWidget } from '@theia/debug/lib/browser/view/debug-variables-widget';
import { EditorWidget, EDITOR_CONTEXT_MENU, EDITOR_CONTENT_MENU } from '@theia/editor/lib/browser';
import { NAVIGATOR_CONTEXT_MENU } from '@theia/navigator/lib/browser/navigator-contribution';
import { ScmTreeWidget } from '@theia/scm/lib/browser/scm-tree-widget';
import { PLUGIN_SCM_CHANGE_TITLE_MENU } from '@theia/scm/lib/browser/dirty-diff/dirty-diff-widget';
import { TIMELINE_ITEM_CONTEXT_MENU } from '@theia/timeline/lib/browser/timeline-tree-widget';
import { COMMENT_CONTEXT, COMMENT_THREAD_CONTEXT, COMMENT_TITLE } from '../comments/comment-thread-widget';
import { VIEW_ITEM_CONTEXT_MENU } from '../view/tree-view-widget';
import { WEBVIEW_CONTEXT_MENU, WebviewWidget } from '../webview/webview';
import { EDITOR_LINENUMBER_CONTEXT_MENU } from '@theia/editor/lib/browser/editor-linenumber-contribution';
import { PLUGIN_TEST_VIEW_TITLE_MENU, TEST_VIEW_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-view-contribution';
import { TEST_RUNS_CONTEXT_MENU } from '@theia/test/lib/browser/view/test-run-view-contribution';
import { TerminalMenus } from '@theia/terminal/lib/browser/terminal-frontend-contribution';
export const PLUGIN_EDITOR_TITLE_MENU = ['plugin_editor/title'];
export const PLUGIN_EDITOR_TITLE_RUN_MENU = ['plugin_editor/title/run'];
export const PLUGIN_SCM_TITLE_MENU = ['plugin_scm/title'];
export const PLUGIN_VIEW_TITLE_MENU = ['plugin_view/title'];
export const implementedVSCodeContributionPoints = [
'comments/comment/context',
'comments/comment/title',
'comments/commentThread/context',
'debug/callstack/context',
'debug/variables/context',
'debug/toolBar',
'editor/context',
'editor/content',
'editor/title',
'editor/title/context',
'editor/title/run',
'editor/lineNumber/context',
'explorer/context',
'scm/change/title',
'scm/resourceFolder/context',
'scm/resourceGroup/context',
'scm/resourceState/context',
'scm/title',
'timeline/item/context',
'testing/item/context',
'testing/message/context',
'testing/profiles/context',
'view/item/context',
'view/title',
'webview/context',
'extension/context',
'terminal/context',
'terminal/title/context'
] as const;
export type ContributionPoint = (typeof implementedVSCodeContributionPoints)[number];
/** The values are menu paths to which the VSCode contribution points correspond */
export const codeToTheiaMappings = new Map<string, MenuPath[]>([
['comments/comment/context', [COMMENT_CONTEXT]],
['comments/comment/title', [COMMENT_TITLE]],
['comments/commentThread/context', [COMMENT_THREAD_CONTEXT]],
['debug/callstack/context', [DebugStackFramesWidget.CONTEXT_MENU, DebugThreadsWidget.CONTEXT_MENU]],
['debug/variables/context', [DebugVariablesWidget.CONTEXT_MENU]],
['debug/toolBar', [DebugToolBar.MENU]],
['editor/context', [EDITOR_CONTEXT_MENU]],
['editor/content', [EDITOR_CONTENT_MENU]],
['editor/title', [PLUGIN_EDITOR_TITLE_MENU]],
['editor/title/context', [SHELL_TABBAR_CONTEXT_MENU]],
['editor/title/run', [PLUGIN_EDITOR_TITLE_RUN_MENU]],
['editor/lineNumber/context', [EDITOR_LINENUMBER_CONTEXT_MENU]],
['explorer/context', [NAVIGATOR_CONTEXT_MENU]],
['file/newFile', [CommonMenus.FILE_NEW_CONTRIBUTIONS]],
['scm/change/title', [PLUGIN_SCM_CHANGE_TITLE_MENU]],
['scm/resourceFolder/context', [ScmTreeWidget.RESOURCE_FOLDER_CONTEXT_MENU]],
['scm/resourceGroup/context', [ScmTreeWidget.RESOURCE_GROUP_CONTEXT_MENU]],
['scm/resourceState/context', [ScmTreeWidget.RESOURCE_CONTEXT_MENU]],
['scm/title', [PLUGIN_SCM_TITLE_MENU]],
['testing/item/context', [TEST_VIEW_CONTEXT_MENU]],
['testing/message/context', [TEST_RUNS_CONTEXT_MENU]],
['testing/profiles/context', [PLUGIN_TEST_VIEW_TITLE_MENU]],
['timeline/item/context', [TIMELINE_ITEM_CONTEXT_MENU]],
['view/item/context', [VIEW_ITEM_CONTEXT_MENU]],
['view/title', [PLUGIN_VIEW_TITLE_MENU]],
['webview/context', [WEBVIEW_CONTEXT_MENU]],
['extension/context', [['extensions_context_menu', '3_contribution']]],
['terminal/context', [TerminalMenus.TERMINAL_CONTRIBUTIONS]],
['terminal/title/context', [TerminalMenus.TERMINAL_TITLE_CONTRIBUTIONS]]
]);
type CodeEditorWidget = EditorWidget | WebviewWidget;
export namespace CodeEditorWidgetUtil {
export function is(arg: unknown): arg is CodeEditorWidget {
return arg instanceof EditorWidget || arg instanceof WebviewWidget;
}
export function getResourceUri(editor: CodeEditorWidget): CodeUri | undefined {
const resourceUri = Navigatable.is(editor) && editor.getResourceUri();
return resourceUri ? resourceUri['codeUri'] : undefined;
}
}

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// 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 { MainMessageType, MainMessageOptions, MainMessageItem } from '../../common/plugin-api-rpc';
import { ModalNotification, MessageType } from './dialogs/modal-notification';
import { BasicMessageRegistryMainImpl } from '../common/basic-message-registry-main';
/**
* Message registry implementation that adds support for the model option via dialog in the browser.
*/
export class MessageRegistryMainImpl extends BasicMessageRegistryMainImpl {
constructor(container: interfaces.Container) {
super(container);
}
protected override async doShowMessage(type: MainMessageType, message: string,
options: MainMessageOptions, actions: MainMessageItem[]): Promise<string | undefined> {
if (options.modal) {
const messageType = type === MainMessageType.Error ? MessageType.Error :
type === MainMessageType.Warning ? MessageType.Warning :
MessageType.Info;
const modalNotification = new ModalNotification();
return modalNotification.showDialog(messageType, message, options, actions);
}
return super.doShowMessage(type, message, options, actions);
}
}

View File

@@ -0,0 +1,260 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableCollection } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
import { UriComponents } from '@theia/core/lib/common/uri';
import { NotebookEditorWidget, NotebookService, NotebookEditorWidgetService, NotebookCellEditorService } from '@theia/notebook/lib/browser';
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
import { MAIN_RPC_CONTEXT, NotebookDocumentsAndEditorsDelta, NotebookDocumentsAndEditorsMain, NotebookEditorAddData, NotebookModelAddedData, NotebooksExt } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { NotebookDto } from './notebook-dto';
import { WidgetManager } from '@theia/core/lib/browser';
import { NotebookEditorsMainImpl } from './notebook-editors-main';
import { NotebookDocumentsMainImpl } from './notebook-documents-main';
import { diffMaps, diffSets } from '../../../common/collections';
import { Mutex } from 'async-mutex';
import { TabsMainImpl } from '../tabs/tabs-main';
interface NotebookAndEditorDelta {
removedDocuments: UriComponents[];
addedDocuments: NotebookModel[];
removedEditors: string[];
addedEditors: NotebookEditorWidget[];
newActiveEditor?: string | null;
visibleEditors?: string[];
}
class NotebookAndEditorState {
static computeDelta(before: NotebookAndEditorState | undefined, after: NotebookAndEditorState): NotebookAndEditorDelta {
if (!before) {
return {
addedDocuments: [...after.documents],
removedDocuments: [],
addedEditors: [...after.textEditors.values()],
removedEditors: [],
visibleEditors: [...after.visibleEditors].map(editor => editor[0])
};
}
const documentDelta = diffSets(before.documents, after.documents);
const editorDelta = diffMaps(before.textEditors, after.textEditors);
const visibleEditorDelta = diffMaps(before.visibleEditors, after.visibleEditors);
return {
addedDocuments: documentDelta.added,
removedDocuments: documentDelta.removed.map(e => e.uri.toComponents()),
addedEditors: editorDelta.added,
removedEditors: editorDelta.removed.map(removed => removed.id),
newActiveEditor: after.activeEditor,
visibleEditors: visibleEditorDelta.added.length === 0 && visibleEditorDelta.removed.length === 0
? undefined
: [...after.visibleEditors].map(editor => editor[0])
};
}
constructor(
readonly documents: Set<NotebookModel>,
readonly textEditors: Map<string, NotebookEditorWidget>,
readonly activeEditor: string | null | undefined,
readonly visibleEditors: Map<string, NotebookEditorWidget>
) {
//
}
}
export class NotebooksAndEditorsMain implements NotebookDocumentsAndEditorsMain {
protected readonly proxy: NotebooksExt;
protected readonly disposables = new DisposableCollection();
protected readonly editorListeners = new Map<string, Disposable[]>();
protected currentState?: NotebookAndEditorState;
protected readonly updateMutex = new Mutex();
protected readonly notebookService: NotebookService;
protected readonly notebookEditorService: NotebookEditorWidgetService;
protected readonly WidgetManager: WidgetManager;
constructor(
rpc: RPCProtocol,
container: interfaces.Container,
tabsMain: TabsMainImpl,
protected readonly notebookDocumentsMain: NotebookDocumentsMainImpl,
protected readonly notebookEditorsMain: NotebookEditorsMainImpl
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT);
this.notebookService = container.get(NotebookService);
this.notebookEditorService = container.get(NotebookEditorWidgetService);
this.WidgetManager = container.get(WidgetManager);
const notebookCellEditorService = container.get(NotebookCellEditorService);
notebookCellEditorService.onDidChangeFocusedCellEditor(editor => this.proxy.$acceptActiveCellEditorChange(editor?.uri.toString() ?? null), this, this.disposables);
this.notebookService.onDidAddNotebookDocument(async () => this.updateState(), this, this.disposables);
this.notebookService.onDidRemoveNotebookDocument(async () => this.updateState(), this, this.disposables);
// this.WidgetManager.onActiveEditorChanged(() => this.updateState(), this, this.disposables);
this.notebookEditorService.onDidAddNotebookEditor(async editor => this.handleEditorAdd(editor), this, this.disposables);
this.notebookEditorService.onDidRemoveNotebookEditor(async editor => this.handleEditorRemove(editor), this, this.disposables);
this.notebookEditorService.onDidChangeCurrentEditor(async editor => {
if (editor) {
await tabsMain.waitForWidget(editor);
}
this.updateState(editor);
}, this, this.disposables);
}
dispose(): void {
this.notebookDocumentsMain.dispose();
this.notebookEditorsMain.dispose();
this.disposables.dispose();
this.editorListeners.forEach(listeners => listeners.forEach(listener => listener.dispose()));
}
private async handleEditorAdd(editor: NotebookEditorWidget): Promise<void> {
const listeners = this.editorListeners.get(editor.id);
const disposable = editor.onDidChangeModel(() => this.updateState());
if (listeners) {
listeners.push(disposable);
} else {
this.editorListeners.set(editor.id, [disposable]);
}
await this.updateState();
}
private handleEditorRemove(editor: NotebookEditorWidget): void {
const listeners = this.editorListeners.get(editor.id);
listeners?.forEach(listener => listener.dispose());
this.editorListeners.delete(editor.id);
this.updateState();
}
private async updateState(focusedEditor?: NotebookEditorWidget): Promise<void> {
await this.updateMutex.runExclusive(async () => this.doUpdateState(focusedEditor));
}
private async doUpdateState(focusedEditor?: NotebookEditorWidget): Promise<void> {
const editors = new Map<string, NotebookEditorWidget>();
const visibleEditorsMap = new Map<string, NotebookEditorWidget>();
for (const editor of this.notebookEditorService.getNotebookEditors()) {
editors.set(editor.id, editor);
}
const activeNotebookEditor = this.notebookEditorService.focusedEditor;
let activeEditor: string | null = null;
if (activeNotebookEditor) {
activeEditor = activeNotebookEditor.id;
} else if (focusedEditor?.model) {
activeEditor = focusedEditor.id;
}
if (activeEditor && !editors.has(activeEditor)) {
activeEditor = null;
}
const notebookEditors = this.WidgetManager.getWidgets(NotebookEditorWidget.ID) as NotebookEditorWidget[];
for (const notebookEditor of notebookEditors) {
if (editors.has(notebookEditor.id) && notebookEditor.isVisible) {
visibleEditorsMap.set(notebookEditor.id, notebookEditor);
}
}
const newState = new NotebookAndEditorState(
new Set(this.notebookService.listNotebookDocuments()),
editors,
activeEditor, visibleEditorsMap);
await this.onDelta(NotebookAndEditorState.computeDelta(this.currentState, newState));
this.currentState = newState;
}
private async onDelta(delta: NotebookAndEditorDelta): Promise<void> {
if (NotebooksAndEditorsMain.isDeltaEmpty(delta)) {
return;
}
const dto: NotebookDocumentsAndEditorsDelta = {
removedDocuments: delta.removedDocuments,
removedEditors: delta.removedEditors,
newActiveEditor: delta.newActiveEditor,
visibleEditors: delta.visibleEditors,
addedDocuments: delta.addedDocuments.map(NotebooksAndEditorsMain.asModelAddData),
addedEditors: delta.addedEditors.map(NotebooksAndEditorsMain.asEditorAddData),
};
// Handle internally first
// In case the plugin wants to perform documents edits immediately
// we want to make sure that all events have already been setup
this.notebookEditorsMain.handleEditorsRemoved(delta.removedEditors);
this.notebookDocumentsMain.handleNotebooksRemoved(delta.removedDocuments);
this.notebookDocumentsMain.handleNotebooksAdded(delta.addedDocuments);
this.notebookEditorsMain.handleEditorsAdded(delta.addedEditors);
// Send to plugin last
await this.proxy.$acceptDocumentsAndEditorsDelta(dto);
}
private static isDeltaEmpty(delta: NotebookAndEditorDelta): boolean {
if (delta.addedDocuments?.length) {
return false;
}
if (delta.removedDocuments?.length) {
return false;
}
if (delta.addedEditors?.length) {
return false;
}
if (delta.removedEditors?.length) {
return false;
}
if (delta.visibleEditors?.length) {
return false;
}
if (delta.newActiveEditor !== undefined) {
return false;
}
return true;
}
private static asModelAddData(e: NotebookModel): NotebookModelAddedData {
return {
viewType: e.viewType,
uri: e.uri.toComponents(),
metadata: e.metadata,
versionId: 1, // TODO implement versionID support
cells: e.cells.map(NotebookDto.toNotebookCellDto)
};
}
private static asEditorAddData(notebookEditor: NotebookEditorWidget): NotebookEditorAddData {
const uri = notebookEditor.getResourceUri();
if (!uri) {
throw new Error('Notebook editor without resource URI');
}
return {
id: notebookEditor.id,
documentUri: uri.toComponents(),
selections: [{ start: 0, end: 0 }],
visibleRanges: []
};
}
}

View File

@@ -0,0 +1,202 @@
// *****************************************************************************
// 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 { DisposableCollection, Event } from '@theia/core';
import { URI, UriComponents } from '@theia/core/lib/common/uri';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookModelResolverService } from '@theia/notebook/lib/browser';
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
import { NotebookCellsChangeType } from '@theia/notebook/lib/common';
import { NotebookMonacoTextModelService } from '@theia/notebook/lib/browser/service/notebook-monaco-text-model-service';
import { MAIN_RPC_CONTEXT, NotebookCellsChangedEventDto, NotebookDataDto, NotebookDocumentsExt, NotebookDocumentsMain, NotebookRawContentEventDto } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { NotebookDto } from './notebook-dto';
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
import { NotebookOpenHandler } from '@theia/notebook/lib/browser/notebook-open-handler';
export class NotebookDocumentsMainImpl implements NotebookDocumentsMain {
protected readonly disposables = new DisposableCollection();
protected readonly proxy: NotebookDocumentsExt;
protected readonly documentEventListenersMapping = new Map<string, DisposableCollection>();
protected readonly notebookModelResolverService: NotebookModelResolverService;
protected readonly notebookMonacoTextModelService: NotebookMonacoTextModelService;
protected readonly notebookOpenHandler: NotebookOpenHandler;
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_DOCUMENTS_EXT);
this.notebookModelResolverService = container.get(NotebookModelResolverService);
this.notebookOpenHandler = container.get(NotebookOpenHandler);
// forward dirty and save events
this.disposables.push(this.notebookModelResolverService.onDidChangeDirty(model => this.proxy.$acceptDirtyStateChanged(model.uri.toComponents(), model.isDirty())));
this.disposables.push(this.notebookModelResolverService.onDidSaveNotebook(e => this.proxy.$acceptModelSaved(e)));
this.notebookMonacoTextModelService = container.get(NotebookMonacoTextModelService) as NotebookMonacoTextModelService;
}
get onDidAddNotebookCellModel(): Event<MonacoEditorModel> {
return this.notebookMonacoTextModelService.onDidCreateNotebookCellModel;
}
dispose(): void {
this.disposables.dispose();
// this.modelReferenceCollection.dispose();
this.documentEventListenersMapping.forEach(value => value.dispose());
}
handleNotebooksAdded(notebooks: readonly NotebookModel[]): void {
for (const notebook of notebooks) {
const listener = notebook.onDidChangeContent(events => {
const eventDto: NotebookCellsChangedEventDto = {
versionId: 1, // TODO implement version ID support
rawEvents: []
};
for (const e of events) {
switch (e.kind) {
case NotebookCellsChangeType.ModelChange:
eventDto.rawEvents.push({
kind: e.kind,
changes: e.changes.map(diff =>
({ ...diff, newItems: diff.newItems.map(NotebookDto.toNotebookCellDto) }))
});
break;
case NotebookCellsChangeType.Move:
eventDto.rawEvents.push({
kind: e.kind,
index: e.index,
length: e.length,
newIdx: e.newIdx,
});
break;
case NotebookCellsChangeType.Output:
eventDto.rawEvents.push({
kind: e.kind,
index: e.index,
outputs: e.outputs.map(NotebookDto.toNotebookOutputDto)
});
break;
case NotebookCellsChangeType.OutputItem:
eventDto.rawEvents.push({
kind: e.kind,
index: e.index,
outputId: e.outputId,
outputItems: e.outputItems.map(NotebookDto.toNotebookOutputItemDto),
append: e.append
});
break;
case NotebookCellsChangeType.ChangeCellLanguage:
case NotebookCellsChangeType.ChangeCellContent:
case NotebookCellsChangeType.ChangeCellMetadata:
case NotebookCellsChangeType.ChangeCellInternalMetadata:
eventDto.rawEvents.push(e);
break;
case NotebookCellsChangeType.ChangeDocumentMetadata:
eventDto.rawEvents.push({
kind: e.kind,
metadata: e.metadata
});
break;
}
}
const hasDocumentMetadataChangeEvent = events.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata);
// using the model resolver service to know if the model is dirty or not.
// assuming this is the first listener it can mean that at first the model
// is marked as dirty and that another event is fired
this.proxy.$acceptModelChanged(
notebook.uri.toComponents(),
eventDto,
notebook.isDirty(),
hasDocumentMetadataChangeEvent ? notebook.metadata : undefined
);
});
this.documentEventListenersMapping.set(notebook.uri.toString(), new DisposableCollection(listener));
}
}
handleNotebooksRemoved(uris: UriComponents[]): void {
for (const uri of uris) {
this.documentEventListenersMapping.get(uri.toString())?.dispose();
this.documentEventListenersMapping.delete(uri.toString());
}
}
async $tryCreateNotebook(options: { viewType: string; content?: NotebookDataDto }): Promise<UriComponents> {
const ref = await this.notebookModelResolverService.resolveUntitledResource({ untitledResource: undefined }, options.viewType);
// untitled notebooks are disposed when they get saved. we should not hold a reference
// to such a disposed notebook and therefore dispose the reference as well
// ref.onWillDispose(() => {
// ref.dispose();
// });
const uriComponents = ref.uri.toComponents();
// untitled notebooks are dirty by default
this.proxy.$acceptDirtyStateChanged(uriComponents, true);
// apply content changes...
if (options.content) {
const data = NotebookDto.fromNotebookDataDto(options.content);
ref.setData(data);
// Create and send a change events
const rawEvents: NotebookRawContentEventDto[] = [];
if (options.content.cells && options.content.cells.length > 0) {
rawEvents.push({
kind: NotebookCellsChangeType.ModelChange,
changes: [{ start: 0, startHandle: 0, deleteCount: 0, newItems: ref.cells.map(NotebookDto.toNotebookCellDto) }]
});
}
if (options.content.metadata) {
rawEvents.push({
kind: NotebookCellsChangeType.ChangeDocumentMetadata,
metadata: options.content.metadata
});
}
if (rawEvents.length > 0) {
this.proxy.$acceptModelChanged(uriComponents, { versionId: 1, rawEvents }, true);
}
}
return uriComponents;
}
async $tryOpenNotebook(uriComponents: UriComponents): Promise<UriComponents> {
const uri = URI.fromComponents(uriComponents);
await this.notebookModelResolverService.resolve(uri);
return uri.toComponents();
}
async $trySaveNotebook(uriComponents: UriComponents): Promise<boolean> {
const uri = URI.fromComponents(uriComponents);
const ref = await this.notebookModelResolverService.resolve(uri);
await ref.save();
ref.dispose();
return true;
}
}

View File

@@ -0,0 +1,131 @@
// *****************************************************************************
// 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 { OS } from '@theia/core';
import * as notebookCommon from '@theia/notebook/lib/common';
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
import * as rpc from '../../../common';
import { CellExecutionUpdateType } from '@theia/notebook/lib/common';
import { CellExecuteUpdate, CellExecutionComplete } from '@theia/notebook/lib/browser';
export namespace NotebookDto {
export function toNotebookOutputItemDto(item: notebookCommon.CellOutputItem): rpc.NotebookOutputItemDto {
return {
mime: item.mime,
valueBytes: item.data
};
}
export function toNotebookOutputDto(output: notebookCommon.CellOutput): rpc.NotebookOutputDto {
return {
outputId: output.outputId,
metadata: output.metadata,
items: output.outputs.map(toNotebookOutputItemDto)
};
}
export function toNotebookCellDataDto(cell: notebookCommon.CellData): rpc.NotebookCellDataDto {
return {
cellKind: cell.cellKind,
language: cell.language,
source: cell.source,
internalMetadata: cell.internalMetadata,
metadata: cell.metadata,
outputs: cell.outputs.map(toNotebookOutputDto)
};
}
export function toNotebookDataDto(data: notebookCommon.NotebookData): rpc.NotebookDataDto {
return {
metadata: data.metadata,
cells: data.cells.map(toNotebookCellDataDto)
};
}
export function fromNotebookOutputItemDto(item: rpc.NotebookOutputItemDto): notebookCommon.CellOutputItem {
return {
mime: item.mime,
data: item.valueBytes
};
}
export function fromNotebookOutputDto(output: rpc.NotebookOutputDto): notebookCommon.CellOutput {
return {
outputId: output.outputId,
metadata: output.metadata,
outputs: output.items.map(fromNotebookOutputItemDto)
};
}
export function fromNotebookCellDataDto(cell: rpc.NotebookCellDataDto): notebookCommon.CellData {
return {
cellKind: cell.cellKind,
language: cell.language,
source: cell.source,
outputs: cell.outputs.map(fromNotebookOutputDto),
metadata: cell.metadata,
internalMetadata: cell.internalMetadata
};
}
export function fromNotebookDataDto(data: rpc.NotebookDataDto): notebookCommon.NotebookData {
return {
metadata: data.metadata,
cells: data.cells.map(fromNotebookCellDataDto)
};
}
export function toNotebookCellDto(cell: NotebookCellModel): rpc.NotebookCellDto {
const eol = OS.backend.EOL;
return {
handle: cell.handle,
uri: cell.uri.toComponents(),
source: cell.text.split(/\r?\n/g),
eol,
language: cell.language,
cellKind: cell.cellKind,
outputs: cell.outputs.map(toNotebookOutputDto),
metadata: cell.metadata,
internalMetadata: cell.internalMetadata,
};
}
export function fromCellExecuteUpdateDto(data: rpc.CellExecuteUpdateDto): CellExecuteUpdate {
if (data.editType === CellExecutionUpdateType.Output) {
return {
editType: data.editType,
cellHandle: data.cellHandle,
append: data.append,
outputs: data.outputs.map(fromNotebookOutputDto)
};
} else if (data.editType === CellExecutionUpdateType.OutputItems) {
return {
editType: data.editType,
outputId: data.outputId,
append: data.append,
items: data.items.map(fromNotebookOutputItemDto)
};
} else {
return data;
}
}
export function fromCellExecuteCompleteDto(data: rpc.CellExecutionCompleteDto): CellExecutionComplete {
return data;
}
}

View File

@@ -0,0 +1,88 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriComponents, URI } from '@theia/core/lib/common/uri';
import { CellRange } from '@theia/notebook/lib/common';
import { NotebookEditorWidget, NotebookService } from '@theia/notebook/lib/browser';
import { MAIN_RPC_CONTEXT, NotebookDocumentShowOptions, NotebookEditorRevealType, NotebookEditorsExt, NotebookEditorsMain } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookOpenHandler } from '@theia/notebook/lib/browser/notebook-open-handler';
export class NotebookEditorsMainImpl implements NotebookEditorsMain {
protected readonly proxy: NotebookEditorsExt;
protected readonly notebookService: NotebookService;
protected readonly notebookOpenHandler: NotebookOpenHandler;
protected readonly mainThreadEditors = new Map<string, NotebookEditorWidget>();
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_EDITORS_EXT);
this.notebookService = container.get(NotebookService);
this.notebookOpenHandler = container.get(NotebookOpenHandler);
}
async $tryShowNotebookDocument(uriComponents: UriComponents, viewType: string, options: NotebookDocumentShowOptions): Promise<string> {
const editor = await this.notebookOpenHandler.open(URI.fromComponents(uriComponents), {
notebookType: viewType
});
await editor.ready;
return editor.id;
}
$tryRevealRange(id: string, range: CellRange, revealType: NotebookEditorRevealType): Promise<void> {
throw new Error('Method not implemented.');
}
$trySetSelections(id: string, range: CellRange[]): void {
if (!this.mainThreadEditors.has(id)) {
throw new Error('Editor not found');
}
const editor = this.mainThreadEditors.get(id);
editor?.viewModel.setSelectedCell(editor.model!.cells[range[0].start]);
}
async handleEditorsAdded(editors: readonly NotebookEditorWidget[]): Promise<void> {
for (const editor of editors) {
this.mainThreadEditors.set(editor.id, editor);
const model = await editor.ready;
editor.viewModel.onDidChangeSelectedCell(e => {
const newCellIndex = e.cell ? model.cells.indexOf(e.cell) : -1;
this.proxy.$acceptEditorPropertiesChanged(editor.id, {
selections: {
selections: newCellIndex >= 0 ? [{ start: newCellIndex, end: newCellIndex }] : []
}
});
});
}
}
handleEditorsRemoved(editorIds: readonly string[]): void {
for (const id of editorIds) {
this.mainThreadEditors.get(id)?.dispose();
this.mainThreadEditors.delete(id);
}
}
dispose(): void {
}
}

View File

@@ -0,0 +1,339 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken, Disposable, Emitter, Event, URI } from '@theia/core';
import { UriComponents } from '@theia/core/lib/common/uri';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { CellExecuteUpdateDto, CellExecutionCompleteDto, MAIN_RPC_CONTEXT, NotebookKernelDto, NotebookKernelsExt, NotebookKernelsMain } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import {
CellExecution, NotebookEditorWidgetService, NotebookExecutionStateService,
NotebookKernelChangeEvent, NotebookKernelService, NotebookService, NotebookKernel as NotebookKernelServiceKernel
} from '@theia/notebook/lib/browser';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookKernelSourceAction } from '@theia/notebook/lib/common';
import { NotebookDto } from './notebook-dto';
abstract class NotebookKernel implements NotebookKernelServiceKernel {
private readonly onDidChangeEmitter = new Emitter<NotebookKernelChangeEvent>();
private readonly preloads: { uri: URI; provides: readonly string[] }[];
readonly onDidChange: Event<NotebookKernelChangeEvent> = this.onDidChangeEmitter.event;
readonly id: string;
readonly viewType: string;
readonly extensionId: string;
implementsInterrupt: boolean;
label: string;
description?: string;
detail?: string;
supportedLanguages: string[];
implementsExecutionOrder: boolean;
localResourceRoot: URI;
public get preloadUris(): URI[] {
return this.preloads.map(p => p.uri);
}
public get preloadProvides(): string[] {
return this.preloads.map(p => p.provides).flat();
}
constructor(public readonly handle: number, data: NotebookKernelDto, private languageService: LanguageService) {
this.id = data.id;
this.viewType = data.notebookType;
this.extensionId = data.extensionId;
this.implementsInterrupt = data.supportsInterrupt ?? false;
this.label = data.label;
this.description = data.description;
this.detail = data.detail;
this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ? data.supportedLanguages : languageService.languages.map(lang => lang.id);
this.implementsExecutionOrder = data.supportsExecutionOrder ?? false;
this.localResourceRoot = URI.fromComponents(data.extensionLocation);
this.preloads = data.preloads?.map(u => ({ uri: URI.fromComponents(u.uri), provides: u.provides })) ?? [];
}
update(data: Partial<NotebookKernelDto>): void {
const event: NotebookKernelChangeEvent = Object.create(null);
if (data.label !== undefined) {
this.label = data.label;
event.label = true;
}
if (data.description !== undefined) {
this.description = data.description;
event.description = true;
}
if (data.detail !== undefined) {
this.detail = data.detail;
event.detail = true;
}
if (data.supportedLanguages !== undefined) {
this.supportedLanguages = (data.supportedLanguages && data.supportedLanguages.length > 0) ?
data.supportedLanguages :
this.languageService.languages.map(lang => lang.id);
event.supportedLanguages = true;
}
if (data.supportsExecutionOrder !== undefined) {
this.implementsExecutionOrder = data.supportsExecutionOrder;
event.hasExecutionOrder = true;
}
if (data.supportsInterrupt !== undefined) {
this.implementsInterrupt = data.supportsInterrupt;
event.hasInterruptHandler = true;
}
this.onDidChangeEmitter.fire(event);
}
abstract executeNotebookCellsRequest(uri: URI, cellHandles: number[]): Promise<void>;
abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise<void>;
}
export interface KernelSourceActionProvider {
readonly viewType: string;
onDidChangeSourceActions?: Event<void>;
provideKernelSourceActions(): Promise<NotebookKernelSourceAction[]>;
}
export class NotebookKernelsMainImpl implements NotebookKernelsMain {
private readonly proxy: NotebookKernelsExt;
private readonly kernels = new Map<number, [kernel: NotebookKernel, registration: Disposable]>();
private readonly kernelDetectionTasks = new Map<number, [task: string, registration: Disposable]>();
private readonly kernelSourceActionProviders = new Map<number, [provider: KernelSourceActionProvider, registration: Disposable]>();
private readonly kernelSourceActionProvidersEventRegistrations = new Map<number, Disposable>();
private notebookKernelService: NotebookKernelService;
private notebookService: NotebookService;
private languageService: LanguageService;
private notebookExecutionStateService: NotebookExecutionStateService;
private notebookEditorWidgetService: NotebookEditorWidgetService;
private readonly executions = new Map<number, CellExecution>();
constructor(
rpc: RPCProtocol,
container: interfaces.Container,
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_KERNELS_EXT);
this.notebookKernelService = container.get(NotebookKernelService);
this.notebookExecutionStateService = container.get(NotebookExecutionStateService);
this.notebookService = container.get(NotebookService);
this.languageService = container.get(LanguageService);
this.notebookEditorWidgetService = container.get(NotebookEditorWidgetService);
this.notebookEditorWidgetService.onDidAddNotebookEditor(editor => {
editor.onDidReceiveKernelMessage(async message => {
const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(editor.model!);
if (kernel) {
this.proxy.$acceptKernelMessageFromRenderer(kernel.handle, editor.id, message);
}
});
});
this.notebookKernelService.onDidChangeSelectedKernel(e => {
if (e.newKernel) {
const newKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.newKernel)?.[0];
if (newKernelHandle !== undefined) {
this.proxy.$acceptNotebookAssociation(newKernelHandle, e.notebook.toComponents(), true);
}
} else {
const oldKernelHandle = Array.from(this.kernels.entries()).find(([_, [kernel]]) => kernel.id === e.oldKernel)?.[0];
if (oldKernelHandle !== undefined) {
this.proxy.$acceptNotebookAssociation(oldKernelHandle, e.notebook.toComponents(), false);
}
}
});
}
async $postMessage(handle: number, editorId: string | undefined, message: unknown): Promise<boolean> {
const tuple = this.kernels.get(handle);
if (!tuple) {
throw new Error('kernel already disposed');
}
const [kernel] = tuple;
let didSend = false;
for (const editor of this.notebookEditorWidgetService.getNotebookEditors()) {
if (!editor.model) {
continue;
}
if (this.notebookKernelService.getMatchingKernel(editor.model).selected !== kernel) {
// different kernel
continue;
}
if (editorId === undefined) {
// all editors
editor.postKernelMessage(message);
didSend = true;
} else if (editor.id === editorId) {
// selected editors
editor.postKernelMessage(message);
didSend = true;
break;
}
}
return didSend;
}
async $addKernel(handle: number, data: NotebookKernelDto): Promise<void> {
const that = this;
const kernel = new class extends NotebookKernel {
async executeNotebookCellsRequest(uri: URI, handles: number[]): Promise<void> {
await that.proxy.$executeCells(handle, uri.toComponents(), handles);
}
async cancelNotebookCellExecution(uri: URI, handles: number[]): Promise<void> {
await that.proxy.$cancelCells(handle, uri.toComponents(), handles);
}
}(handle, data, this.languageService);
// this is for when a kernel is bound to a notebook while being registered
const autobindListener = this.notebookKernelService.onDidChangeSelectedKernel(e => {
if (e.newKernel === kernel.id) {
this.proxy.$acceptNotebookAssociation(handle, e.notebook.toComponents(), true);
}
});
const registration = this.notebookKernelService.registerKernel(kernel);
this.kernels.set(handle, [kernel, registration]);
autobindListener.dispose();
}
$updateKernel(handle: number, data: Partial<NotebookKernelDto>): void {
const tuple = this.kernels.get(handle);
if (tuple) {
tuple[0].update(data);
}
}
$removeKernel(handle: number): void {
const tuple = this.kernels.get(handle);
if (tuple) {
tuple[1].dispose();
this.kernels.delete(handle);
}
}
$updateNotebookPriority(handle: number, uri: UriComponents, value: number | undefined): void {
throw new Error('Method not implemented.');
}
$createExecution(handle: number, controllerId: string, uriComponents: UriComponents, cellHandle: number): void {
const uri = URI.fromComponents(uriComponents);
const notebook = this.notebookService.getNotebookEditorModel(uri);
if (!notebook) {
throw new Error(`Notebook not found: ${uri.toString()}`);
}
const kernel = this.notebookKernelService.getMatchingKernel(notebook);
if (!kernel.selected || kernel.selected.id !== controllerId) {
throw new Error(`Kernel is not selected: ${kernel.selected?.id} !== ${controllerId}`);
}
const execution = this.notebookExecutionStateService.getOrCreateCellExecution(uri, cellHandle);
execution.confirm();
this.executions.set(handle, execution);
}
$updateExecution(handle: number, updates: CellExecuteUpdateDto[]): void {
const execution = this.executions.get(handle);
execution?.update(updates.map(NotebookDto.fromCellExecuteUpdateDto));
}
$completeExecution(handle: number, data: CellExecutionCompleteDto): void {
try {
const execution = this.executions.get(handle);
execution?.complete(NotebookDto.fromCellExecuteCompleteDto(data));
} finally {
this.executions.delete(handle);
}
}
// TODO implement notebook execution (special api for executing full notebook instead of just cells)
$createNotebookExecution(handle: number, controllerId: string, uri: UriComponents): void {
throw new Error('Method not implemented.');
}
$beginNotebookExecution(handle: number): void {
throw new Error('Method not implemented.');
}
$completeNotebookExecution(handle: number): void {
throw new Error('Method not implemented.');
}
async $addKernelDetectionTask(handle: number, notebookType: string): Promise<void> {
const registration = this.notebookKernelService.registerNotebookKernelDetectionTask(notebookType);
this.kernelDetectionTasks.set(handle, [notebookType, registration]);
}
$removeKernelDetectionTask(handle: number): void {
const tuple = this.kernelDetectionTasks.get(handle);
if (tuple) {
tuple[1].dispose();
this.kernelDetectionTasks.delete(handle);
}
}
async $addKernelSourceActionProvider(handle: number, eventHandle: number, notebookType: string): Promise<void> {
const kernelSourceActionProvider: KernelSourceActionProvider = {
viewType: notebookType,
provideKernelSourceActions: async () => {
const actions = await this.proxy.$provideKernelSourceActions(handle, CancellationToken.None);
return actions.map(action => ({
label: action.label,
command: action.command,
description: action.description,
detail: action.detail,
documentation: action.documentation,
}));
}
};
if (typeof eventHandle === 'number') {
const emitter = new Emitter<void>();
this.kernelSourceActionProvidersEventRegistrations.set(eventHandle, emitter);
kernelSourceActionProvider.onDidChangeSourceActions = emitter.event;
}
const registration = this.notebookKernelService.registerKernelSourceActionProvider(notebookType, kernelSourceActionProvider);
this.kernelSourceActionProviders.set(handle, [kernelSourceActionProvider, registration]);
}
$removeKernelSourceActionProvider(handle: number, eventHandle: number): void {
const tuple = this.kernelSourceActionProviders.get(handle);
if (tuple) {
tuple[1].dispose();
this.kernelSourceActionProviders.delete(handle);
}
if (typeof eventHandle === 'number') {
this.kernelSourceActionProvidersEventRegistrations.delete(eventHandle);
}
}
$emitNotebookKernelSourceActionsChangeEvent(eventHandle: number): void {
}
dispose(): void {
this.kernels.forEach(kernel => kernel[1].dispose());
}
}

View File

@@ -0,0 +1,47 @@
// *****************************************************************************
// 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 { DisposableCollection } from '@theia/core';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookRendererMessagingService } from '@theia/notebook/lib/browser';
import { MAIN_RPC_CONTEXT, NotebookRenderersExt, NotebookRenderersMain } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
export class NotebookRenderersMainImpl implements NotebookRenderersMain {
private readonly proxy: NotebookRenderersExt;
private readonly rendererMessagingService: NotebookRendererMessagingService;
private readonly disposables = new DisposableCollection();
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOK_RENDERERS_EXT);
this.rendererMessagingService = container.get(NotebookRendererMessagingService);
this.rendererMessagingService.onPostMessage(e => {
this.proxy.$postRendererMessage(e.editorId, e.rendererId, e.message);
});
}
$postMessage(editorId: string | undefined, rendererId: string, message: unknown): Promise<boolean> {
return this.rendererMessagingService.receiveMessage(editorId, rendererId, message);
}
dispose(): void {
this.disposables.dispose();
}
}

View File

@@ -0,0 +1,159 @@
// *****************************************************************************
// 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 { CancellationToken, DisposableCollection, Emitter, URI } from '@theia/core';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { CellEditType, NotebookCellModelResource, NotebookData, NotebookModelResource, TransientOptions } from '@theia/notebook/lib/common';
import { NotebookService, NotebookWorkspaceEdit } from '@theia/notebook/lib/browser';
import { Disposable } from '@theia/plugin';
import { CommandRegistryMain, MAIN_RPC_CONTEXT, NotebooksExt, NotebooksMain, WorkspaceEditDto, WorkspaceNotebookCellEditDto } from '../../../common';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { NotebookDto } from './notebook-dto';
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
import { interfaces } from '@theia/core/shared/inversify';
import {
NotebookCellStatusBarItemProvider,
NotebookCellStatusBarItemList,
NotebookCellStatusBarService
} from '@theia/notebook/lib/browser/service/notebook-cell-status-bar-service';
export class NotebooksMainImpl implements NotebooksMain {
protected readonly disposables = new DisposableCollection();
protected notebookService: NotebookService;
protected cellStatusBarService: NotebookCellStatusBarService;
protected readonly proxy: NotebooksExt;
protected readonly notebookSerializer = new Map<number, Disposable>();
protected readonly notebookCellStatusBarRegistrations = new Map<number, Disposable>();
constructor(
rpc: RPCProtocol,
container: interfaces.Container,
commands: CommandRegistryMain
) {
this.notebookService = container.get(NotebookService);
this.cellStatusBarService = container.get(NotebookCellStatusBarService);
const plugins = container.get(HostedPluginSupport);
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.NOTEBOOKS_EXT);
this.notebookService.onWillUseNotebookSerializer(event => plugins.activateByNotebookSerializer(event));
this.notebookService.markReady();
commands.registerArgumentProcessor({
processArgument: arg => {
if (arg instanceof NotebookModel) {
return NotebookModelResource.create(arg.uri);
} else if (arg instanceof NotebookCellModel) {
return NotebookCellModelResource.create(arg.uri);
}
return arg;
}
});
}
dispose(): void {
this.disposables.dispose();
for (const disposable of this.notebookSerializer.values()) {
disposable.dispose();
}
}
$registerNotebookSerializer(handle: number, viewType: string, options: TransientOptions): void {
const disposables = new DisposableCollection();
disposables.push(this.notebookService.registerNotebookSerializer(viewType, {
options,
toNotebook: async (data: BinaryBuffer): Promise<NotebookData> => {
const dto = await this.proxy.$dataToNotebook(handle, data, CancellationToken.None);
return NotebookDto.fromNotebookDataDto(dto);
},
fromNotebook: (data: NotebookData): Promise<BinaryBuffer> =>
this.proxy.$notebookToData(handle, NotebookDto.toNotebookDataDto(data), CancellationToken.None)
}));
this.notebookSerializer.set(handle, disposables);
}
$unregisterNotebookSerializer(handle: number): void {
this.notebookSerializer.get(handle)?.dispose();
this.notebookSerializer.delete(handle);
}
$emitCellStatusBarEvent(eventHandle: number): void {
const emitter = this.notebookCellStatusBarRegistrations.get(eventHandle);
if (emitter instanceof Emitter) {
emitter.fire(undefined);
}
}
async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, viewType: string): Promise<void> {
const that = this;
const provider: NotebookCellStatusBarItemProvider = {
async provideCellStatusBarItems(notebookUri: URI, index: number, token: CancellationToken): Promise<NotebookCellStatusBarItemList | undefined> {
const result = await that.proxy.$provideNotebookCellStatusBarItems(handle, notebookUri.toComponents(), index, token);
return {
items: result?.items ?? [],
dispose(): void {
if (result) {
that.proxy.$releaseNotebookCellStatusBarItems(result.cacheId);
}
}
};
},
viewType
};
if (typeof eventHandle === 'number') {
const emitter = new Emitter<void>();
this.notebookCellStatusBarRegistrations.set(eventHandle, emitter);
provider.onDidChangeStatusBarItems = emitter.event;
}
const disposable = this.cellStatusBarService.registerCellStatusBarItemProvider(provider);
this.notebookCellStatusBarRegistrations.set(handle, disposable);
}
async $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-shadow
const unregisterThing = (statusBarHandle: number) => {
const entry = this.notebookCellStatusBarRegistrations.get(statusBarHandle);
if (entry) {
this.notebookCellStatusBarRegistrations.get(statusBarHandle)?.dispose();
this.notebookCellStatusBarRegistrations.delete(statusBarHandle);
}
};
unregisterThing(handle);
if (typeof eventHandle === 'number') {
unregisterThing(eventHandle);
}
}
}
export function toNotebookWorspaceEdit(dto: WorkspaceEditDto): NotebookWorkspaceEdit {
return {
edits: dto.edits.map((edit: WorkspaceNotebookCellEditDto) => ({
resource: URI.fromComponents(edit.resource),
edit: edit.cellEdit.editType === CellEditType.Replace ? {
...edit.cellEdit,
cells: edit.cellEdit.cells.map(cell => NotebookDto.fromNotebookCellDataDto(cell))
} : edit.cellEdit
}))
};
}

View File

@@ -0,0 +1,620 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as React from '@theia/core/shared/react';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { generateUuid } from '@theia/core/lib/common/uuid';
import {
NotebookRendererMessagingService, CellOutputWebview, NotebookRendererRegistry,
NotebookEditorWidgetService, NotebookKernelService, NotebookEditorWidget,
OutputRenderEvent,
NotebookCellOutputsSplice,
NotebookContentChangedEvent
} from '@theia/notebook/lib/browser';
import { WebviewWidget } from '../../webview/webview';
import { Message, WidgetManager } from '@theia/core/lib/browser';
import { outputWebviewPreload, PreloadContext } from './output-webview-internal';
import { WorkspaceTrustService } from '@theia/workspace/lib/browser';
import {
CellOutputChange, CellsChangedMessage, CellsMoved, CellsSpliced,
ChangePreferredMimetypeMessage, FromWebviewMessage, Output, OutputChangedMessage
} from './webview-communication';
import { Disposable, DisposableCollection, Emitter, QuickPickService, nls } from '@theia/core';
import { NotebookModel } from '@theia/notebook/lib/browser/view-model/notebook-model';
import { NotebookOptionsService, NotebookOutputOptions } from '@theia/notebook/lib/browser/service/notebook-options';
import { NotebookCellModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-model';
import { CellOutput, NotebookCellsChangeType } from '@theia/notebook/lib/common';
import { NotebookCellOutputModel } from '@theia/notebook/lib/browser/view-model/notebook-cell-output-model';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { NOTEBOOK_OUTPUT_FOCUSED } from '@theia/notebook/lib/browser/contributions/notebook-context-keys';
export const AdditionalNotebookCellOutputCss = Symbol('AdditionalNotebookCellOutputCss');
export function createCellOutputWebviewContainer(ctx: interfaces.Container): interfaces.Container {
const child = ctx.createChild();
child.bind(AdditionalNotebookCellOutputCss).toConstantValue(DEFAULT_NOTEBOOK_OUTPUT_CSS);
child.bind(CellOutputWebviewImpl).toSelf();
return child;
}
// Should be kept up-to-date with:
// https://github.com/microsoft/vscode/blob/main/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewThemeMapping.ts
const mapping: ReadonlyMap<string, string> = new Map([
['theme-font-family', 'vscode-font-family'],
['theme-font-weight', 'vscode-font-weight'],
['theme-font-size', 'vscode-font-size'],
['theme-code-font-family', 'vscode-editor-font-family'],
['theme-code-font-weight', 'vscode-editor-font-weight'],
['theme-code-font-size', 'vscode-editor-font-size'],
['theme-scrollbar-background', 'vscode-scrollbarSlider-background'],
['theme-scrollbar-hover-background', 'vscode-scrollbarSlider-hoverBackground'],
['theme-scrollbar-active-background', 'vscode-scrollbarSlider-activeBackground'],
['theme-quote-background', 'vscode-textBlockQuote-background'],
['theme-quote-border', 'vscode-textBlockQuote-border'],
['theme-code-foreground', 'vscode-textPreformat-foreground'],
// Editor
['theme-background', 'vscode-editor-background'],
['theme-foreground', 'vscode-editor-foreground'],
['theme-ui-foreground', 'vscode-foreground'],
['theme-link', 'vscode-textLink-foreground'],
['theme-link-active', 'vscode-textLink-activeForeground'],
// Buttons
['theme-button-background', 'vscode-button-background'],
['theme-button-hover-background', 'vscode-button-hoverBackground'],
['theme-button-foreground', 'vscode-button-foreground'],
['theme-button-secondary-background', 'vscode-button-secondaryBackground'],
['theme-button-secondary-hover-background', 'vscode-button-secondaryHoverBackground'],
['theme-button-secondary-foreground', 'vscode-button-secondaryForeground'],
['theme-button-hover-foreground', 'vscode-button-foreground'],
['theme-button-focus-foreground', 'vscode-button-foreground'],
['theme-button-secondary-hover-foreground', 'vscode-button-secondaryForeground'],
['theme-button-secondary-focus-foreground', 'vscode-button-secondaryForeground'],
// Inputs
['theme-input-background', 'vscode-input-background'],
['theme-input-foreground', 'vscode-input-foreground'],
['theme-input-placeholder-foreground', 'vscode-input-placeholderForeground'],
['theme-input-focus-border-color', 'vscode-focusBorder'],
// Menus
['theme-menu-background', 'vscode-menu-background'],
['theme-menu-foreground', 'vscode-menu-foreground'],
['theme-menu-hover-background', 'vscode-menu-selectionBackground'],
['theme-menu-focus-background', 'vscode-menu-selectionBackground'],
['theme-menu-hover-foreground', 'vscode-menu-selectionForeground'],
['theme-menu-focus-foreground', 'vscode-menu-selectionForeground'],
// Errors
['theme-error-background', 'vscode-inputValidation-errorBackground'],
['theme-error-foreground', 'vscode-foreground'],
['theme-warning-background', 'vscode-inputValidation-warningBackground'],
['theme-warning-foreground', 'vscode-foreground'],
['theme-info-background', 'vscode-inputValidation-infoBackground'],
['theme-info-foreground', 'vscode-foreground'],
// Notebook:
['theme-notebook-output-background', 'vscode-notebook-outputContainerBackgroundColor'],
['theme-notebook-output-border', 'vscode-notebook-outputContainerBorderColor'],
['theme-notebook-cell-selected-background', 'vscode-notebook-selectedCellBackground'],
['theme-notebook-symbol-highlight-background', 'vscode-notebook-symbolHighlightBackground'],
['theme-notebook-diff-removed-background', 'vscode-diffEditor-removedTextBackground'],
['theme-notebook-diff-inserted-background', 'vscode-diffEditor-insertedTextBackground'],
]);
const constants: Record<string, string> = {
'theme-input-border-width': '1px',
'theme-button-primary-hover-shadow': 'none',
'theme-button-secondary-hover-shadow': 'none',
'theme-input-border-color': 'transparent',
};
export const DEFAULT_NOTEBOOK_OUTPUT_CSS = `
:root {
${Array.from(mapping.entries()).map(([key, value]) => `--${key}: var(--${value});`).join('\n')}
${Object.entries(constants).map(([key, value]) => `--${key}: ${value};`).join('\n')}
}
body {
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
table th,
table td {
border: 1px solid;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
table,
thead,
tr,
th,
td,
tbody {
border: none !important;
border-color: transparent;
border-spacing: 0;
border-collapse: collapse;
}
table,
th,
tr {
vertical-align: middle;
text-align: right;
}
thead {
font-weight: bold;
background-color: rgba(130, 130, 130, 0.16);
}
th,
td {
padding: 4px 8px;
}
tr:nth-child(even) {
background-color: rgba(130, 130, 130, 0.08);
}
tbody th {
font-weight: normal;
}
`;
interface CellOutputUpdate extends NotebookCellOutputsSplice {
cellHandle: number
}
@injectable()
export class CellOutputWebviewImpl implements CellOutputWebview, Disposable {
@inject(NotebookRendererMessagingService)
protected readonly messagingService: NotebookRendererMessagingService;
@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
@inject(WorkspaceTrustService)
protected readonly workspaceTrustService: WorkspaceTrustService;
@inject(NotebookRendererRegistry)
protected readonly notebookRendererRegistry: NotebookRendererRegistry;
@inject(NotebookEditorWidgetService)
protected readonly notebookEditorWidgetService: NotebookEditorWidgetService;
@inject(NotebookKernelService)
protected readonly notebookKernelService: NotebookKernelService;
@inject(QuickPickService)
protected readonly quickPickService: QuickPickService;
@inject(AdditionalNotebookCellOutputCss)
protected readonly additionalOutputCss: string;
@inject(NotebookOptionsService)
protected readonly notebookOptionsService: NotebookOptionsService;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
// returns the output Height
protected readonly onDidRenderOutputEmitter = new Emitter<OutputRenderEvent>();
readonly onDidRenderOutput = this.onDidRenderOutputEmitter.event;
protected notebook: NotebookModel;
protected options: NotebookOutputOptions;
readonly id = generateUuid();
protected editor: NotebookEditorWidget | undefined;
protected element?: HTMLDivElement; // React.createRef<HTMLDivElement>();
protected webviewWidget: WebviewWidget;
protected webviewWidgetInitialized = new Deferred();
protected toDispose = new DisposableCollection();
protected isDisposed = false;
async init(notebook: NotebookModel, editor: NotebookEditorWidget): Promise<void> {
this.notebook = notebook;
this.editor = editor;
this.options = this.notebookOptionsService.computeOutputOptions();
this.toDispose.push(this.notebookOptionsService.onDidChangeOutputOptions(options => {
this.options = options;
this.updateStyles();
}));
this.webviewWidget = await this.widgetManager.getOrCreateWidget(WebviewWidget.FACTORY_ID, { id: this.id });
this.webviewWidgetInitialized.resolve();
// this.webviewWidget.parent = this.editor ?? null;
this.webviewWidget.setContentOptions({
allowScripts: true,
// eslint-disable-next-line max-len
// list taken from https://github.com/microsoft/vscode/blob/a27099233b956dddc2536d4a0d714ab36266d897/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts#L762-L774
enableCommandUris: [
'github-issues.authNow',
'workbench.extensions.search',
'workbench.action.openSettings',
'_notebook.selectKernel',
'jupyter.viewOutput',
'workbench.action.openLargeOutput',
'cellOutput.enableScrolling',
],
});
this.webviewWidget.setHTML(await this.createWebviewContent());
this.notebook.onDidAddOrRemoveCell(e => {
if (e.newCellIds) {
const newCells = e.newCellIds.map(id => this.notebook.cells.find(cell => cell.handle === id)).filter(cell => !!cell) as NotebookCellModel[];
newCells.forEach(cell => this.attachCellAndOutputListeners(cell));
}
});
this.notebook.cells.forEach(cell => this.attachCellAndOutputListeners(cell));
if (this.editor) {
this.toDispose.push(this.editor.onDidPostKernelMessage(message => {
this.webviewWidget.sendMessage({
type: 'customKernelMessage',
message
});
}));
this.toDispose.push(this.editor.onPostRendererMessage(messageObj => {
this.webviewWidget.sendMessage({
type: 'customRendererMessage',
...messageObj
});
}));
}
this.webviewWidget.onMessage((message: FromWebviewMessage) => {
this.handleWebviewMessage(message);
});
}
attachCellAndOutputListeners(cell: NotebookCellModel): void {
this.toDispose.push(cell.onDidChangeOutputs(outputChange => this.updateOutputs([{
newOutputs: outputChange.newOutputs,
start: outputChange.start,
deleteCount: outputChange.deleteCount,
cellHandle: cell.handle
}])));
this.toDispose.push(cell.onDidChangeOutputItems(output => {
const oldOutputIndex = cell.outputs.findIndex(o => o.outputId === output.outputId);
this.updateOutputs([{
cellHandle: cell.handle,
newOutputs: [output],
start: oldOutputIndex,
deleteCount: 1
}]);
}));
this.toDispose.push(cell.onDidCellHeightChange(height => this.setCellHeight(cell, height)));
this.toDispose.push(cell.onDidChangeOutputVisibility(visible => {
this.webviewWidget.sendMessage({
type: 'outputVisibilityChanged',
cellHandle: cell.handle,
visible
});
}));
}
render(): React.JSX.Element {
return <div className='theia-notebook-cell-output-webview' ref={async element => {
if (element) {
this.element = element;
await this.webviewWidgetInitialized.promise;
this.attachWebview();
}
}}></div>;
}
protected attachWebview(): void {
if (this.element) {
this.webviewWidget.processMessage(new Message('before-attach'));
this.element.appendChild(this.webviewWidget.node);
this.webviewWidget.processMessage(new Message('after-attach'));
this.webviewWidget.setIframeHeight(0);
}
}
isAttached(): boolean {
return this.element?.contains(this.webviewWidget.node) ?? false;
}
updateOutputs(updates: CellOutputUpdate[]): void {
if (this.webviewWidget.isHidden) {
this.webviewWidget.show();
}
const visibleCells = this.notebook.getVisibleCells();
const visibleCellHandleLookup = new Set(visibleCells.map(cell => cell.handle));
const updateOutputMessage: OutputChangedMessage = {
type: 'outputChanged',
changes: updates
.filter(update => visibleCellHandleLookup.has(update.cellHandle))
.map(update => ({
cellHandle: update.cellHandle,
newOutputs: this.mapCellOutputsToWebviewOutput(update.newOutputs),
start: update.start,
deleteCount: update.deleteCount
}))
};
if (updateOutputMessage.changes.length > 0) {
this.webviewWidget.sendMessage(updateOutputMessage);
}
}
cellsChanged(cellEvents: NotebookContentChangedEvent[]): void {
const changes: Array<CellsMoved | CellsSpliced> = [];
const outputChanges: CellOutputChange[] = [];
const visibleCellLookup = new Set(this.notebook.getVisibleCells());
for (const event of cellEvents) {
if (event.kind === NotebookCellsChangeType.Move) {
changes.push(...event.cells.map((cell, i) => {
const cellMoved: CellsMoved = {
type: 'cellMoved',
cellHandle: event.cells[0].handle, // TODO check this, ask Jonah
toIndex: event.newIdx,
};
return cellMoved;
}));
} else if (event.kind === NotebookCellsChangeType.ModelChange) {
changes.push(...event.changes.map(change => {
const cellSpliced: CellsSpliced = {
type: 'cellsSpliced',
startCellHandle: change.startHandle,
deleteCount: change.deleteCount,
newCells: change.newItems.filter(cell => visibleCellLookup.has(cell as NotebookCellModel)).map(cell => cell.handle)
};
return cellSpliced;
}
));
outputChanges.push(...event.changes
.flatMap(change => change.newItems)
.filter(cell => visibleCellLookup.has(cell as NotebookCellModel) && cell.outputs.length)
.map(newCell => ({
start: 0,
deleteCount: 0,
cellHandle: newCell.handle,
newOutputs: this.mapCellOutputsToWebviewOutput(newCell.outputs)
})));
}
}
this.webviewWidget.sendMessage({
type: 'cellsChanged',
changes: changes.filter(e => e)
} as CellsChangedMessage);
if (outputChanges.length > 0) {
this.webviewWidget.sendMessage({
type: 'outputChanged',
changes: outputChanges
});
}
}
protected mapCellOutputsToWebviewOutput(outputs: CellOutput[]): Output[] {
return outputs.map(output => ({
id: output.outputId,
items: output.outputs.map(item => ({ mime: item.mime, data: item.data.buffer })),
metadata: output.metadata
}));
}
/**
* Currently not used, but could be useful in a subclasses
*
* @param index cell index
* @param cellHandle cell handle
* @param visibleCells visible cells
* @returns visible cell index or -1 if not found
*/
protected toVisibleCellIndex(index: number, cellHandle: number, visibleCells: Array<NotebookCellModel>): number {
const cell = this.notebook.cells[index];
if (cell.handle === cellHandle) {
return visibleCells.indexOf(cell);
}
// in case of deletion index points to a non-existing cell
return -1;
}
setCellHeight(cell: NotebookCellModel, height: number): void {
if (!this.isDisposed) {
this.webviewWidget.sendMessage({
type: 'cellHeightUpdate',
cellHandle: cell.handle,
cellKind: cell.cellKind,
height
});
}
}
async requestOutputPresentationUpdate(cellHandle: number, output: NotebookCellOutputModel): Promise<void> {
const selectedMime = await this.quickPickService.show(
output.outputs.map(item => ({ label: item.mime })),
{ description: nls.localizeByDefault('Select mimetype to render for current output') });
if (selectedMime) {
this.webviewWidget.sendMessage({
type: 'changePreferredMimetype',
cellHandle,
outputId: output.outputId,
mimeType: selectedMime.label
} as ChangePreferredMimetypeMessage);
}
}
protected handleWebviewMessage(message: FromWebviewMessage): void {
if (!this.editor) {
throw new Error('No editor found for cell output webview');
}
switch (message.type) {
case 'initialized':
this.updateOutputs(this.notebook.getVisibleCells().map(cell => ({
cellHandle: cell.handle,
newOutputs: cell.outputs,
start: 0,
deleteCount: 0
})));
this.updateStyles();
break;
case 'customRendererMessage':
this.messagingService.getScoped(this.editor.id).postMessage(message.rendererId, message.message);
break;
case 'didRenderOutput':
this.webviewWidget.setIframeHeight(message.bodyHeight);
this.onDidRenderOutputEmitter.fire({
cellHandle: message.cellHandle,
outputId: message.outputId,
outputHeight: message.outputHeight
});
break;
case 'did-scroll-wheel':
this.editor.node.getElementsByClassName('theia-notebook-viewport')[0].children[0].scrollBy(message.deltaX, message.deltaY);
break;
case 'customKernelMessage':
this.editor.recieveKernelMessage(message.message);
break;
case 'inputFocusChanged':
this.editor?.outputInputFocusChanged(message.focused);
break;
case 'cellFocusChanged':
const selectedCell = this.notebook.getCellByHandle(message.cellHandle);
if (selectedCell) {
this.editor.viewModel.setSelectedCell(selectedCell);
}
break;
case 'webviewFocusChanged':
if (message.focused) {
window.getSelection()?.empty();
}
this.contextKeyService.setContext(NOTEBOOK_OUTPUT_FOCUSED, message.focused);
break;
case 'cellHeightRequest':
const cellHeight = this.notebook.getCellByHandle(message.cellHandle)?.cellHeight ?? 0;
this.webviewWidget.sendMessage({
type: 'cellHeightUpdate',
cellHandle: message.cellHandle,
height: cellHeight
});
break;
case 'bodyHeightChange':
this.webviewWidget.setIframeHeight(message.height);
break;
}
}
getPreloads(): string[] {
const kernel = this.notebookKernelService.getSelectedOrSuggestedKernel(this.notebook);
const kernelPreloads = kernel?.preloadUris.map(uri => uri.toString()) ?? [];
const staticPreloads = this.notebookRendererRegistry.staticNotebookPreloads
.filter(preload => preload.type === this.notebook.viewType)
.map(preload => preload.entrypoint);
return kernelPreloads.concat(staticPreloads);
}
protected updateStyles(): void {
this.webviewWidget.sendMessage({
type: 'notebookStyles',
styles: this.generateStyles()
});
}
protected generateStyles(): { [key: string]: string } {
return {
'notebook-output-node-left-padding': `${this.options.outputNodeLeftPadding}px`,
'notebook-cell-output-font-size': `${this.options.outputFontSize || this.options.fontSize}px`,
'notebook-cell-output-line-height': `${this.options.outputLineHeight}px`,
'notebook-cell-output-max-height': `${this.options.outputLineHeight * this.options.outputLineLimit}px`,
'notebook-cell-output-font-family': this.options.outputFontFamily || this.options.fontFamily,
};
}
private async createWebviewContent(): Promise<string> {
const isWorkspaceTrusted = await this.workspaceTrustService.getWorkspaceTrust();
const preloads = this.preloadsScriptString(isWorkspaceTrusted);
const content = `
<html>
<head>
<meta charset="UTF-8">
<style>
${this.additionalOutputCss}
</style>
</head>
<body>
<script type="module">${preloads}</script>
</body>
</html>
`;
return content;
}
private preloadsScriptString(isWorkspaceTrusted: boolean): string {
const ctx: PreloadContext = {
isWorkspaceTrusted,
rendererData: this.notebookRendererRegistry.notebookRenderers,
renderOptions: {
lineLimit: this.options.outputLineLimit,
outputScrolling: this.options.outputScrolling,
outputWordWrap: this.options.outputWordWrap,
},
staticPreloadsData: this.getPreloads()
};
// TS will try compiling `import()` in webviewPreloads, so use a helper function instead
// of using `import(...)` directly
return `
const __import = (x) => import(x);
(${outputWebviewPreload})(JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(ctx))}")))`;
}
dispose(): void {
this.isDisposed = true;
this.toDispose.dispose();
}
}

View File

@@ -0,0 +1,829 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// only type imports are allowed here since this runs in an iframe. All other code is not accessible
import type * as webviewCommunication from './webview-communication';
import type * as rendererApi from 'vscode-notebook-renderer';
import type { Disposable, Event } from '@theia/core';
declare const acquireVsCodeApi: () => ({
getState(): { [key: string]: unknown };
setState(data: { [key: string]: unknown }): void;
postMessage: (msg: unknown) => void;
});
declare function __import(path: string): Promise<unknown>;
interface Listener<T> { fn: (evt: T) => void; thisArg: unknown };
interface EmitterLike<T> {
fire(data: T): void;
event: Event<T>;
}
interface RendererContext extends rendererApi.RendererContext<unknown> {
readonly onDidChangeSettings: Event<RenderOptions>;
readonly settings: RenderOptions;
}
interface NotebookRendererEntrypoint {
readonly path: string;
readonly extends?: string
};
export interface RenderOptions {
readonly lineLimit: number;
readonly outputScrolling: boolean;
readonly outputWordWrap: boolean;
}
export interface PreloadContext {
readonly isWorkspaceTrusted: boolean;
readonly rendererData: readonly webviewCommunication.RendererMetadata[];
readonly renderOptions: RenderOptions;
readonly staticPreloadsData: readonly string[];
}
interface KernelPreloadContext {
readonly onDidReceiveKernelMessage: Event<unknown>;
postKernelMessage(data: unknown): void;
}
interface KernelPreloadModule {
activate(ctx: KernelPreloadContext): Promise<void> | void;
}
export async function outputWebviewPreload(ctx: PreloadContext): Promise<void> {
// workaround to allow rendering of links in outputs for non chromium browsers
// see https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API#cross-browser_support_for_trusted_types
if (!window.trustedTypes) {
window.trustedTypes = {
createPolicy: (name: string, rules: unknown) => rules
} as unknown as typeof window.trustedTypes;
}
const theia = acquireVsCodeApi();
const renderFallbackErrorName = 'vscode.fallbackToNextRenderer';
document.body.style.overflow = 'hidden';
const container = document.createElement('div');
container.id = 'container';
container.classList.add('widgetarea');
document.body.appendChild(container);
function createEmitter<T>(listenerChange: (listeners: Set<Listener<T>>) => void = () => undefined): EmitterLike<T> {
const listeners = new Set<Listener<T>>();
return {
fire(data: T): void {
for (const listener of [...listeners]) {
listener.fn.call(listener.thisArg, data);
}
},
event(fn, thisArg, disposables): Disposable {
const listenerObj = { fn, thisArg };
const disposable: Disposable = {
dispose: () => {
listeners.delete(listenerObj);
listenerChange(listeners);
},
};
listeners.add(listenerObj);
listenerChange(listeners);
if (disposables) {
if ('push' in disposables) {
disposables.push(disposable);
} else {
disposables.add(disposable);
}
}
return disposable;
}
};
};
const settingChange: EmitterLike<RenderOptions> = createEmitter<RenderOptions>();
const onDidReceiveKernelMessage = createEmitter<unknown>();
function createKernelContext(): KernelPreloadContext {
return Object.freeze({
onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
postKernelMessage: (data: unknown) => {
theia.postMessage({ type: 'customKernelMessage', message: data });
}
});
}
async function runKernelPreload(url: string): Promise<void> {
try {
return activateModuleKernelPreload(url);
} catch (e) {
console.error(e);
throw e;
}
}
async function activateModuleKernelPreload(url: string): Promise<void> {
const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, '');
const module: KernelPreloadModule = (await __import(`${baseUri}/${url}`)) as KernelPreloadModule;
if (!module.activate) {
console.error(`Notebook preload '${url}' was expected to be a module but it does not export an 'activate' function`);
return;
}
return module.activate(createKernelContext());
}
class OutputCell {
readonly element: HTMLElement;
readonly outputElements: OutputContainer[] = [];
private cellHeight: number = 0;
constructor(public cellHandle: number, cellIndex?: number) {
this.element = document.createElement('div');
this.element.style.outline = '0';
this.element.id = `cellHandle${cellHandle}`;
this.element.classList.add('cell_container');
this.element.addEventListener('focusin', e => {
theia.postMessage({ type: 'cellFocusChanged', cellHandle: cellHandle });
});
if (cellIndex !== undefined && cellIndex < container.children.length) {
container.insertBefore(this.element, container.children[cellIndex]);
} else {
container.appendChild(this.element);
}
this.element = this.element;
theia.postMessage({ type: 'cellHeightRequest', cellHandle: cellHandle });
}
public dispose(): void {
this.element.remove();
}
calcTotalOutputHeight(): number {
return this.outputElements.reduce((acc, output) => acc + output.element.getBoundingClientRect().height, 0) + 5;
}
createOutputElement(index: number, output: webviewCommunication.Output, items: rendererApi.OutputItem[]): OutputContainer {
let outputContainer = this.outputElements.find(o => o.outputId === output.id);
if (!outputContainer) {
outputContainer = new OutputContainer(output, items, this);
this.element.appendChild(outputContainer.containerElement);
this.outputElements.splice(index, 0, outputContainer);
this.updateCellHeight(this.cellHeight);
}
return outputContainer;
}
public clearOutputs(start: number, deleteCount: number): void {
for (const output of this.outputElements.splice(start, deleteCount)) {
output?.clear();
output.containerElement.remove();
}
}
public show(outputId: string, top: number): void {
const outputContainer = this.outputElements.find(o => o.outputId === outputId);
if (!outputContainer) {
return;
}
}
public hide(): void {
this.element.style.visibility = 'hidden';
}
public updateCellHeight(height: number): void {
this.cellHeight = height;
let additionalHeight = 54.5;
additionalHeight -= cells[0] === this ? 2.5 : 0; // first cell
additionalHeight -= this.outputElements.length ? 0 : 5.5; // no outputs
this.element.style.paddingTop = `${height + additionalHeight}px`;
}
public outputVisibilityChanged(visible: boolean): void {
this.outputElements.forEach(output => {
output.element.style.display = visible ? 'initial' : 'none';
output.containerElement.style.minHeight = visible ? '20px' : '0px';
});
if (visible) {
this.element.getElementsByClassName('output-hidden')?.[0].remove();
window.requestAnimationFrame(() => this.outputElements.forEach(output => sendDidRenderMessage(this, output)));
} else {
const outputHiddenElement = document.createElement('div');
outputHiddenElement.classList.add('output-hidden');
outputHiddenElement.style.height = '16px';
this.element.appendChild(outputHiddenElement);
}
}
// public updateScroll(request: webviewCommunication.IContentWidgetTopRequest): void {
// this.element.style.top = `${request.cellTop}px`;
// const outputElement = this.outputElements.get(request.outputId);
// if (outputElement) {
// outputElement.updateScroll(request.outputOffset);
// if (request.forceDisplay && outputElement.element) {
// // TODO @rebornix @mjbvz, there is a misalignment here.
// // We set output visibility on cell container, other than output container or output node itself.
// outputElement.element.style.visibility = '';
// }
// }
// if (request.forceDisplay) {
// this.element.style.visibility = '';
// }
}
const cells: OutputCell[] = [];
class OutputContainer {
readonly outputId: string;
readonly cellId: string;
renderedItem?: rendererApi.OutputItem;
allItems: rendererApi.OutputItem[];
renderer: Renderer;
element: HTMLElement;
containerElement: HTMLElement;
constructor(output: webviewCommunication.Output, items: rendererApi.OutputItem[], private cell: OutputCell) {
this.outputId = output.id;
this.createHtmlElement();
this.allItems = items;
}
findItemToRender(preferredMimetype?: string): rendererApi.OutputItem {
if (preferredMimetype) {
const itemToRender = this.allItems.find(item => item.mime === preferredMimetype);
if (itemToRender) {
return itemToRender;
}
}
return this.renderedItem ?? this.allItems[0];
}
clear(): void {
this.renderer?.disposeOutputItem?.(this.renderedItem?.id);
this.element.innerHTML = '';
}
preferredMimeTypeChange(mimeType: string): void {
this.containerElement.remove();
this.createHtmlElement();
this.cell.element.appendChild(this.containerElement);
renderers.render(this.cell, this, mimeType, undefined, new AbortController().signal);
}
private createHtmlElement(): void {
this.containerElement = document.createElement('div');
this.containerElement.classList.add('output-container');
this.containerElement.style.minHeight = '20px';
this.element = document.createElement('div');
this.element.id = this.outputId;
this.element.classList.add('output');
this.containerElement.appendChild(this.element);
}
}
class Renderer {
entrypoint: NotebookRendererEntrypoint;
private rendererApi?: rendererApi.RendererApi;
private onMessageEvent: EmitterLike<unknown> = createEmitter();
constructor(
public readonly data: webviewCommunication.RendererMetadata
) { }
public receiveMessage(message: unknown): void {
this.onMessageEvent.fire(message);
}
public disposeOutputItem(id?: string): void {
this.rendererApi?.disposeOutputItem?.(id);
}
async getOrLoad(): Promise<rendererApi.RendererApi | undefined> {
if (this.rendererApi) {
return this.rendererApi;
}
// Preloads need to be loaded before loading renderers.
await kernelPreloads.waitForAllCurrent();
const baseUri = window.location.href.replace(/\/webview\/index\.html.*/, '');
const rendererModule = await __import(`${baseUri}/${this.data.entrypoint.uri}`) as { activate: rendererApi.ActivationFunction };
this.rendererApi = await rendererModule.activate(this.createRendererContext());
return this.rendererApi;
}
protected createRendererContext(): RendererContext {
const context: RendererContext = {
setState: newState => theia.setState({ ...theia.getState(), [this.data.id]: newState }),
getState: <T>() => {
const state = theia.getState();
return typeof state === 'object' && state ? state[this.data.id] as T : undefined;
},
getRenderer: async (id: string) => {
const renderer = renderers.getRenderer(id);
if (!renderer) {
return undefined;
}
if (renderer.rendererApi) {
return renderer.rendererApi;
}
return renderer.getOrLoad();
},
workspace: {
get isTrusted(): boolean { return true; } // TODO use Workspace trust service
},
settings: {
get lineLimit(): number { return ctx.renderOptions.lineLimit; },
get outputScrolling(): boolean { return ctx.renderOptions.outputScrolling; },
get outputWordWrap(): boolean { return ctx.renderOptions.outputWordWrap; },
},
get onDidChangeSettings(): Event<RenderOptions> { return settingChange.event; },
};
if (this.data.requiresMessaging) {
context.onDidReceiveMessage = this.onMessageEvent.event;
context.postMessage = message => {
theia.postMessage({ type: 'customRendererMessage', rendererId: this.data.id, message });
};
}
return Object.freeze(context);
}
}
const renderers = new class {
private readonly renderers = new Map</* id */ string, Renderer>();
constructor() {
for (const renderer of ctx.rendererData) {
this.addRenderer(renderer);
}
}
public getRenderer(id: string): Renderer | undefined {
return this.renderers.get(id);
}
private rendererEqual(a: webviewCommunication.RendererMetadata, b: webviewCommunication.RendererMetadata): boolean {
if (a.id !== b.id || a.entrypoint.uri !== b.entrypoint.uri || a.entrypoint.extends !== b.entrypoint.extends || a.requiresMessaging !== b.requiresMessaging) {
return false;
}
if (a.mimeTypes.length !== b.mimeTypes.length) {
return false;
}
for (let i = 0; i < a.mimeTypes.length; i++) {
if (a.mimeTypes[i] !== b.mimeTypes[i]) {
return false;
}
}
return true;
}
public updateRendererData(rendererData: readonly webviewCommunication.RendererMetadata[]): void {
const oldKeys = new Set(this.renderers.keys());
const newKeys = new Set(rendererData.map(d => d.id));
for (const renderer of rendererData) {
const existing = this.renderers.get(renderer.id);
if (existing && this.rendererEqual(existing.data, renderer)) {
continue;
}
this.addRenderer(renderer);
}
for (const key of oldKeys) {
if (!newKeys.has(key)) {
this.renderers.delete(key);
}
}
}
private addRenderer(renderer: webviewCommunication.RendererMetadata): void {
this.renderers.set(renderer.id, new Renderer(renderer));
}
public clearAll(): void {
for (const renderer of this.renderers.values()) {
renderer.disposeOutputItem();
}
}
public clearOutput(rendererId: string, outputId: string): void {
// outputRunner.cancelOutput(outputId);
this.renderers.get(rendererId)?.disposeOutputItem(outputId);
}
public async render(cell: OutputCell, output: OutputContainer, preferredMimeType: string | undefined,
preferredRendererId: string | undefined, signal: AbortSignal): Promise<void> {
const item = output.findItemToRender(preferredMimeType);
const primaryRenderer = this.findRenderer(preferredRendererId, item);
if (!primaryRenderer) {
this.showRenderError(item, output.element, 'No renderer found for output type.');
return;
}
// Try primary renderer first
if (!(await this.doRender(item, output.element, primaryRenderer, signal)).continue) {
output.renderer = primaryRenderer;
this.onRenderCompleted(cell, output);
return;
}
// Primary renderer failed in an expected way. Fallback to render the next mime types
for (const additionalItem of output.allItems) {
if (additionalItem.mime === item.mime) {
continue;
}
if (signal.aborted) {
return;
}
if (additionalItem) {
const renderer = this.findRenderer(undefined, additionalItem);
if (renderer) {
if (!(await this.doRender(additionalItem, output.element, renderer, signal)).continue) {
output.renderer = renderer;
this.onRenderCompleted(cell, output);
return; // We rendered successfully
}
}
}
}
// All renderers have failed and there is nothing left to fallback to
this.showRenderError(item, output.element, 'No fallback renderers found or all fallback renderers failed.');
}
private onRenderCompleted(cell: OutputCell, output: OutputContainer): void {
// we need to check for all images are loaded. Otherwise we can't determine the correct height of the output
const images = Array.from(document.images);
if (images.length > 0) {
Promise.all(images
.filter(img => !img.complete && !img.dataset.waiting)
.map(img => {
img.dataset.waiting = 'true'; // mark to avoid overriding onload a second time
return new Promise(resolve => { img.onload = img.onerror = resolve; });
})).then(() => {
sendDidRenderMessage(cell, output);
new ResizeObserver(() => sendDidRenderMessage(cell, output)).observe(cell.element);
});
} else {
sendDidRenderMessage(cell, output);
new ResizeObserver(() => sendDidRenderMessage(cell, output)).observe(cell.element);
}
}
private async doRender(item: rendererApi.OutputItem, element: HTMLElement, renderer: Renderer, signal: AbortSignal): Promise<{ continue: boolean }> {
try {
await (await renderer.getOrLoad())?.renderOutputItem(item, element, signal);
return { continue: false }; // We rendered successfully
} catch (e) {
if (signal.aborted) {
return { continue: false };
}
if (e instanceof Error && e.name === renderFallbackErrorName) {
return { continue: true };
} else {
throw e; // Bail and let callers handle unknown errors
}
}
}
private findRenderer(preferredRendererId: string | undefined, info: rendererApi.OutputItem): Renderer | undefined {
let foundRenderer: Renderer | undefined;
if (typeof preferredRendererId === 'string') {
foundRenderer = Array.from(this.renderers.values())
.find(renderer => renderer.data.id === preferredRendererId);
} else {
const rendererList = Array.from(this.renderers.values())
.filter(renderer => renderer.data.mimeTypes.includes(info.mime) && !renderer.data.entrypoint.extends);
if (rendererList.length) {
// De-prioritize built-in renderers
// rendererList.sort((a, b) => +a.data.isBuiltin - +b.data.isBuiltin);
// Use first renderer we find in sorted list
foundRenderer = rendererList[0];
}
}
return foundRenderer;
}
private showRenderError(info: rendererApi.OutputItem, element: HTMLElement, errorMessage: string): void {
const errorContainer = document.createElement('div');
const error = document.createElement('div');
error.className = 'no-renderer-error';
error.innerText = errorMessage;
const cellText = document.createElement('div');
cellText.innerText = info.text();
errorContainer.appendChild(error);
errorContainer.appendChild(cellText);
element.innerText = '';
element.appendChild(errorContainer);
}
}();
function sendDidRenderMessage(cell: OutputCell, output: OutputContainer): void {
theia.postMessage(<webviewCommunication.OnDidRenderOutput>{
type: 'didRenderOutput',
cellHandle: cell.cellHandle,
outputId: output.outputId,
outputHeight: cell.calcTotalOutputHeight(),
bodyHeight: document.body.clientHeight
});
}
const kernelPreloads = new class {
private readonly preloads = new Map<string /* uri */, Promise<unknown>>();
/**
* Returns a promise that resolves when the given preload is activated.
*/
public waitFor(uri: string): Promise<unknown> {
return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`));
}
/**
* Loads a preload.
* @param uri URI to load from
* @param originalUri URI to show in an error message if the preload is invalid.
*/
public load(uri: string): Promise<unknown> {
const promise = Promise.all([
runKernelPreload(uri),
this.waitForAllCurrent(),
]);
this.preloads.set(uri, promise);
return promise;
}
/**
* Returns a promise that waits for all currently-registered preloads to
* activate before resolving.
*/
public waitForAllCurrent(): Promise<unknown[]> {
return Promise.all([...this.preloads.values()].map(p => p.catch(err => err)));
}
};
await Promise.all(ctx.staticPreloadsData.map(preload => kernelPreloads.load(preload)));
async function outputsChanged(changedEvent: webviewCommunication.OutputChangedMessage): Promise<void> {
for (const cellChange of changedEvent.changes) {
let cell = cells.find(c => c.cellHandle === cellChange.cellHandle);
if (!cell) {
cell = new OutputCell(cellChange.cellHandle);
cells.push(cell);
}
cell.clearOutputs(cellChange.start, cellChange.deleteCount);
for (const outputData of cellChange.newOutputs ?? []) {
const apiItems: rendererApi.OutputItem[] = outputData.items.map((item, index) => ({
id: `${outputData.id}-${index}`,
mime: item.mime,
metadata: outputData.metadata,
data(): Uint8Array {
return item.data;
},
text(): string {
return new TextDecoder().decode(this.data());
},
json(): unknown {
return JSON.parse(this.text());
},
blob(): Blob {
return new Blob([new Uint8Array(this.data())], { type: this.mime });
},
}));
const output = cell.createOutputElement(cellChange.start, outputData, apiItems);
await renderers.render(cell, output, undefined, undefined, new AbortController().signal);
theia.postMessage(<webviewCommunication.OnDidRenderOutput>{
type: 'didRenderOutput',
cellHandle: cell.cellHandle,
outputId: outputData.id,
outputHeight: document.getElementById(output.outputId)?.clientHeight ?? 0,
bodyHeight: document.body.clientHeight
});
}
}
}
function cellsChanged(changes: (webviewCommunication.CellsMoved | webviewCommunication.CellsSpliced)[]): void {
for (const change of changes) {
if (change.type === 'cellMoved') {
const currentIndex = cells.findIndex(c => c.cellHandle === change.cellHandle);
const cell = cells[currentIndex];
cells.splice(change.toIndex, 0, cells.splice(currentIndex, 1)[0]);
if (change.toIndex < cells.length - 1) {
container.insertBefore(cell.element, container.children[change.toIndex + (change.toIndex > currentIndex ? 1 : 0)]);
} else {
container.appendChild(cell.element);
}
} else if (change.type === 'cellsSpliced') {
// if startCellHandle is negative, it means we should add a trailing new cell
const startCellIndex = change.startCellHandle < 0 ? cells.length : cells.findIndex(c => c.cellHandle === change.startCellHandle);
if (startCellIndex === -1) {
console.error(`Can't find cell output to splice. Cells: ${cells.length}, startCellHandle: ${change.startCellHandle}`);
} else {
const deletedCells = cells.splice(
startCellIndex,
change.deleteCount,
...change.newCells.map((cellHandle, i) => new OutputCell(cellHandle, startCellIndex + i))
);
deletedCells.forEach(cell => cell.dispose());
}
}
}
}
function shouldHandleScroll(event: WheelEvent): boolean {
for (let node = event.target as Node | null; node; node = node.parentNode) {
if (!(node instanceof Element)) {
return false;
}
// scroll up
if (event.deltaY < 0 && node.scrollTop > 0) {
// there is still some content to scroll
return true;
}
// scroll down
if (event.deltaY > 0 && node.scrollTop + node.clientHeight < node.scrollHeight) {
// per https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
// scrollTop is not rounded but scrollHeight and clientHeight are
// so we need to check if the difference is less than some threshold
if (node.scrollHeight - node.scrollTop - node.clientHeight < 2) {
continue;
}
// if the node is not scrollable, we can continue. We don't check the computed style always as it's expensive
if (window.getComputedStyle(node).overflowY === 'hidden' || window.getComputedStyle(node).overflowY === 'visible') {
continue;
}
return true;
}
}
return false;
}
const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => {
if (event.defaultPrevented || shouldHandleScroll(event)) {
return;
}
theia.postMessage({
type: 'did-scroll-wheel',
deltaY: event.deltaY,
deltaX: event.deltaX,
});
};
window.addEventListener('message', async rawEvent => {
const event = rawEvent as ({ data: webviewCommunication.ToWebviewMessage });
let cellHandle: number | undefined;
switch (event.data.type) {
case 'updateRenderers':
renderers.updateRendererData(event.data.rendererData);
break;
case 'outputChanged':
outputsChanged(event.data);
break;
case 'cellsChanged':
cellsChanged(event.data.changes);
break;
case 'customRendererMessage':
renderers.getRenderer(event.data.rendererId)?.receiveMessage(event.data.message);
break;
case 'changePreferredMimetype':
cellHandle = event.data.cellHandle;
const mimeType = event.data.mimeType;
cells.find(c => c.cellHandle === cellHandle)
?.outputElements.forEach(o => o.preferredMimeTypeChange(mimeType));
break;
case 'customKernelMessage':
onDidReceiveKernelMessage.fire(event.data.message);
break;
case 'preload':
const resources = event.data.resources;
for (const uri of resources) {
kernelPreloads.load(uri);
}
break;
case 'notebookStyles':
const documentStyle = window.document.documentElement.style;
for (let i = documentStyle.length - 1; i >= 0; i--) {
const property = documentStyle[i];
// Don't remove properties that the webview might have added separately
if (property && property.startsWith('--notebook-')) {
documentStyle.removeProperty(property);
}
}
// Re-add new properties
for (const [name, value] of Object.entries(event.data.styles)) {
documentStyle.setProperty(`--${name}`, value);
}
break;
case 'cellHeightUpdate':
cellHandle = event.data.cellHandle;
const cell = cells.find(c => c.cellHandle === cellHandle);
if (cell) {
cell.updateCellHeight(event.data.height);
}
break;
case 'outputVisibilityChanged':
cellHandle = event.data.cellHandle;
cells.find(c => c.cellHandle === cellHandle)?.outputVisibilityChanged(event.data.visible);
break;
}
});
window.addEventListener('wheel', handleWheel);
(document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).originalAppendChild = document.head.appendChild;
(document.head as HTMLHeadElement & { originalAppendChild: typeof document.head.appendChild }).appendChild = function appendChild<T extends Node>(node: T): T {
if (node instanceof HTMLScriptElement && node.src.includes('webviewuuid')) {
node.src = node.src.replace('webviewuuid', location.hostname.split('.')[0]);
}
return this.originalAppendChild(node);
};
const focusChange = (event: FocusEvent, focus: boolean) => {
if (event.target instanceof HTMLInputElement) {
theia.postMessage({ type: 'inputFocusChanged', focused: focus } as webviewCommunication.InputFocusChange);
}
};
window.addEventListener('focusin', (event: FocusEvent) => focusChange(event, true));
window.addEventListener('focusout', (event: FocusEvent) => focusChange(event, false));
const webviewFocuseChange = (focus: boolean) => {
theia.postMessage({ type: 'webviewFocusChanged', focused: focus } as webviewCommunication.WebviewFocusChange);
};
window.addEventListener('focus', () => webviewFocuseChange(true));
window.addEventListener('blur', () => webviewFocuseChange(false));
new ResizeObserver(() => {
theia.postMessage({
type: 'bodyHeightChange',
height: document.body.clientHeight
} as webviewCommunication.BodyHeightChange);
}).observe(document.body);
theia.postMessage(<webviewCommunication.WebviewInitialized>{ type: 'initialized' });
}

View File

@@ -0,0 +1,188 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export interface RendererMetadata {
readonly id: string;
readonly entrypoint: { readonly uri: string, readonly extends?: string; };
readonly mimeTypes: readonly string[];
readonly requiresMessaging: boolean;
}
export interface CustomRendererMessage {
readonly type: 'customRendererMessage';
readonly rendererId: string;
readonly message: unknown;
}
export interface UpdateRenderersMessage {
readonly type: 'updateRenderers';
readonly rendererData: readonly RendererMetadata[];
}
export interface CellOutputChange {
readonly cellHandle: number;
readonly newOutputs?: Output[];
readonly start: number;
readonly deleteCount: number;
}
export interface OutputChangedMessage {
readonly type: 'outputChanged';
changes: CellOutputChange[];
}
export interface ChangePreferredMimetypeMessage {
readonly type: 'changePreferredMimetype';
readonly cellHandle: number;
readonly outputId: string;
readonly mimeType: string;
}
export interface KernelMessage {
readonly type: 'customKernelMessage';
readonly message: unknown;
}
export interface PreloadMessage {
readonly type: 'preload';
readonly resources: string[];
}
export interface notebookStylesMessage {
readonly type: 'notebookStyles';
styles: Record<string, string>;
}
export interface CellHeigthsMessage {
type: 'cellHeigths';
cellHeigths: Record<number, number>;
}
export interface CellsMoved {
type: 'cellMoved';
cellHandle: number;
toIndex: number;
}
export interface CellsSpliced {
type: 'cellsSpliced';
/**
* Cell handle for the start cell.
* -1 in case of new Cells are added at the end.
*/
startCellHandle: number;
deleteCount: number;
newCells: number[];
}
export interface CellsChangedMessage {
type: 'cellsChanged';
changes: Array<CellsMoved | CellsSpliced>;
}
export interface CellHeightUpdateMessage {
type: 'cellHeightUpdate';
cellKind: number;
cellHandle: number;
height: number;
}
export interface OutputVisibilityChangedMessage {
type: 'outputVisibilityChanged';
cellHandle: number;
visible: boolean;
}
export type ToWebviewMessage = UpdateRenderersMessage
| OutputChangedMessage
| ChangePreferredMimetypeMessage
| CustomRendererMessage
| KernelMessage
| PreloadMessage
| notebookStylesMessage
| CellHeigthsMessage
| CellHeightUpdateMessage
| CellsChangedMessage
| OutputVisibilityChangedMessage;
export interface WebviewInitialized {
readonly type: 'initialized';
}
export interface OnDidRenderOutput {
readonly type: 'didRenderOutput';
cellHandle: number;
outputId: string;
outputHeight: number;
bodyHeight: number;
}
export interface WheelMessage {
readonly type: 'did-scroll-wheel';
readonly deltaY: number;
readonly deltaX: number;
}
export interface InputFocusChange {
readonly type: 'inputFocusChanged';
readonly focused: boolean;
}
export interface CellOuputFocus {
readonly type: 'cellFocusChanged';
readonly cellHandle: number;
}
export interface WebviewFocusChange {
readonly type: 'webviewFocusChanged';
readonly focused: boolean;
}
export interface CellHeightRequest {
readonly type: 'cellHeightRequest';
readonly cellHandle: number;
}
export interface BodyHeightChange {
readonly type: 'bodyHeightChange';
readonly height: number;
}
export type FromWebviewMessage = WebviewInitialized
| OnDidRenderOutput
| WheelMessage
| CustomRendererMessage
| KernelMessage
| InputFocusChange
| CellOuputFocus
| WebviewFocusChange
| CellHeightRequest
| BodyHeightChange;
export interface Output {
id: string
metadata?: Record<string, unknown>;
items: OutputItem[];
}
export interface OutputItem {
readonly mime: string;
readonly data: Uint8Array;
}

View File

@@ -0,0 +1,26 @@
// *****************************************************************************
// Copyright (C) 2018 Red Hat, Inc. and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MAIN_RPC_CONTEXT } from '../../common';
import { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import { BasicNotificationMainImpl } from '../common/basic-notification-main';
export class NotificationMainImpl extends BasicNotificationMainImpl {
constructor(rpc: RPCProtocol, container: interfaces.Container) {
super(rpc, container, MAIN_RPC_CONTEXT.NOTIFICATION_EXT);
}
}

View File

@@ -0,0 +1,53 @@
// *****************************************************************************
// 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 { CommandService } from '@theia/core/lib/common/command';
import { OutputCommands } from '@theia/output/lib/browser/output-commands';
import { OutputChannelRegistryMain, PluginInfo } from '../../common/plugin-api-rpc';
@injectable()
export class OutputChannelRegistryMainImpl implements OutputChannelRegistryMain {
@inject(CommandService)
protected readonly commandService: CommandService;
$append(name: string, text: string, pluginInfo: PluginInfo): PromiseLike<void> {
this.commandService.executeCommand(OutputCommands.APPEND.id, { name, text });
return Promise.resolve();
}
$clear(name: string): PromiseLike<void> {
this.commandService.executeCommand(OutputCommands.CLEAR.id, { name });
return Promise.resolve();
}
$dispose(name: string): PromiseLike<void> {
this.commandService.executeCommand(OutputCommands.DISPOSE.id, { name });
return Promise.resolve();
}
async $reveal(name: string, preserveFocus: boolean): Promise<void> {
const options = { preserveFocus };
this.commandService.executeCommand(OutputCommands.SHOW.id, { name, options });
}
$close(name: string): PromiseLike<void> {
this.commandService.executeCommand(OutputCommands.HIDE.id, { name });
return Promise.resolve();
}
}

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2022 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 { AuthenticationProvider, AuthenticationService, AuthenticationServiceImpl, AuthenticationSession } from '@theia/core/lib/browser/authentication-service';
import { inject } from '@theia/core/shared/inversify';
import { Deferred, timeoutReject } from '@theia/core/lib/common/promise-util';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
/**
* Plugin authentication service that aims to activate additional plugins if sessions are created or queried.
*/
export class PluginAuthenticationServiceImpl extends AuthenticationServiceImpl implements AuthenticationService {
@inject(HostedPluginSupport) protected readonly pluginService: HostedPluginSupport;
override async getSessions(id: string, scopes?: string[]): Promise<ReadonlyArray<AuthenticationSession>> {
await this.tryActivateProvider(id);
return super.getSessions(id, scopes);
}
override async login(id: string, scopes: string[]): Promise<AuthenticationSession> {
await this.tryActivateProvider(id);
return super.login(id, scopes);
}
protected async tryActivateProvider(providerId: string): Promise<AuthenticationProvider> {
this.pluginService.activateByEvent(getAuthenticationProviderActivationEvent(providerId));
const provider = this.authenticationProviders.get(providerId);
if (provider) {
return provider;
}
// When activate has completed, the extension has made the call to `registerAuthenticationProvider`.
// However, activate cannot block on this, so the renderer may not have gotten the event yet.
return Promise.race([
this.waitForProviderRegistration(providerId),
timeoutReject<AuthenticationProvider>(5000, 'Timed out waiting for authentication provider to register')
]);
}
protected async waitForProviderRegistration(providerId: string): Promise<AuthenticationProvider> {
const waitForRegistration = new Deferred<AuthenticationProvider>();
const registration = this.onDidRegisterAuthenticationProvider(info => {
if (info.id === providerId) {
registration.dispose();
const provider = this.authenticationProviders.get(providerId);
if (provider) {
waitForRegistration.resolve(provider);
} else {
waitForRegistration.reject(new Error(`No authentication provider '${providerId}' is currently registered.`));
}
}
});
return waitForRegistration.promise;
}
}

View File

@@ -0,0 +1,689 @@
// *****************************************************************************
// 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 } from '@theia/core/shared/inversify';
import { ITokenTypeMap, IEmbeddedLanguagesMap } from 'vscode-textmate';
import { TextmateRegistry, getEncodedLanguageId, MonacoTextmateService, GrammarDefinition } from '@theia/monaco/lib/browser/textmate';
import { MenusContributionPointHandler } from './menus/menus-contribution-handler';
import { PluginViewRegistry } from './view/plugin-view-registry';
import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry';
import {
PluginContribution, IndentationRules, FoldingRules, ScopeMap, DeployedPlugin,
GrammarsContribution, EnterAction, OnEnterRule, RegExpOptions, IconContribution, PluginPackage
} from '../../common';
import {
DefaultUriLabelProviderContribution,
LabelProviderContribution,
} from '@theia/core/lib/browser';
import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler';
import { MonacoSnippetSuggestProvider } from '@theia/monaco/lib/browser/monaco-snippet-suggest-provider';
import { PluginSharedStyle } from './plugin-shared-style';
import { CommandRegistry, Command, CommandHandler } from '@theia/core/lib/common/command';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter } from '@theia/core/lib/common/event';
import { TaskDefinitionRegistry, ProblemMatcherRegistry, ProblemPatternRegistry } from '@theia/task/lib/browser';
import { NotebookRendererRegistry, NotebookTypeRegistry } from '@theia/notebook/lib/browser';
import { PluginDebugService } from './debug/plugin-debug-service';
import { DebugSchemaUpdater } from '@theia/debug/lib/browser/debug-schema-updater';
import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { PluginIconService } from './plugin-icon-service';
import { PluginIconThemeService } from './plugin-icon-theme-service';
import { ContributionProvider, isObject, OVERRIDE_PROPERTY_PATTERN, PreferenceSchemaService } from '@theia/core/lib/common';
import * as monaco from '@theia/monaco-editor-core';
import { ContributedTerminalProfileStore, TerminalProfileStore } from '@theia/terminal/lib/browser/terminal-profile-service';
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { PluginTerminalRegistry } from './plugin-terminal-registry';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { LanguageService } from '@theia/core/lib/browser/language-service';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
import { JSONObject, JSONValue } from '@theia/core/shared/@lumino/coreutils';
// The enum export is missing from `vscode-textmate@9.2.0`
const enum StandardTokenType {
Other = 0,
Comment = 1,
String = 2,
RegEx = 3
}
@injectable()
export class PluginContributionHandler {
private injections = new Map<string, string[]>();
@inject(TextmateRegistry)
private readonly grammarsRegistry: TextmateRegistry;
@inject(PluginViewRegistry)
private readonly viewRegistry: PluginViewRegistry;
@inject(PluginCustomEditorRegistry)
private readonly customEditorRegistry: PluginCustomEditorRegistry;
@inject(MenusContributionPointHandler)
private readonly menusContributionHandler: MenusContributionPointHandler;
@inject(PreferenceSchemaService)
private readonly preferenceSchemaProvider: PreferenceSchemaService;
@inject(MonacoTextmateService)
private readonly monacoTextmateService: MonacoTextmateService;
@inject(KeybindingsContributionPointHandler)
private readonly keybindingsContributionHandler: KeybindingsContributionPointHandler;
@inject(MonacoSnippetSuggestProvider)
protected readonly snippetSuggestProvider: MonacoSnippetSuggestProvider;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(LanguageService)
protected readonly languageService: LanguageService;
@inject(PluginSharedStyle)
protected readonly style: PluginSharedStyle;
@inject(TaskDefinitionRegistry)
protected readonly taskDefinitionRegistry: TaskDefinitionRegistry;
@inject(ProblemMatcherRegistry)
protected readonly problemMatcherRegistry: ProblemMatcherRegistry;
@inject(ProblemPatternRegistry)
protected readonly problemPatternRegistry: ProblemPatternRegistry;
@inject(PluginDebugService)
protected readonly debugService: PluginDebugService;
@inject(DebugSchemaUpdater)
protected readonly debugSchema: DebugSchemaUpdater;
@inject(MonacoThemingService)
protected readonly monacoThemingService: MonacoThemingService;
@inject(ColorRegistry)
protected readonly colors: ColorRegistry;
@inject(PluginIconService)
protected readonly iconService: PluginIconService;
@inject(PluginIconThemeService)
protected readonly iconThemeService: PluginIconThemeService;
@inject(TerminalService)
protected readonly terminalService: TerminalService;
@inject(PluginTerminalRegistry)
protected readonly pluginTerminalRegistry: PluginTerminalRegistry;
@inject(ContributedTerminalProfileStore)
protected readonly contributedProfileStore: TerminalProfileStore;
@inject(NotebookTypeRegistry)
protected readonly notebookTypeRegistry: NotebookTypeRegistry;
@inject(NotebookRendererRegistry)
protected readonly notebookRendererRegistry: NotebookRendererRegistry;
@inject(ContributionProvider) @named(LabelProviderContribution)
protected readonly contributionProvider: ContributionProvider<LabelProviderContribution>;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
protected readonly commandHandlers = new Map<string, CommandHandler['execute'] | undefined>();
protected readonly onDidRegisterCommandHandlerEmitter = new Emitter<string>();
readonly onDidRegisterCommandHandler = this.onDidRegisterCommandHandlerEmitter.event;
/**
* Always synchronous in order to simplify handling disconnections.
* @throws never, loading of each contribution should handle errors
* in order to avoid preventing loading of other contributions or extensions
*/
handleContributions(clientId: string, plugin: DeployedPlugin): Disposable {
const contributions = plugin.contributes;
if (!contributions) {
return Disposable.NULL;
}
const toDispose = new DisposableCollection(Disposable.create(() => { /* mark as not disposed */ }));
/* eslint-disable @typescript-eslint/no-explicit-any */
const logError = (message: string, ...args: any[]) => console.error(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
const logWarning = (message: string, ...args: any[]) => console.warn(`[${clientId}][${plugin.metadata.model.id}]: ${message}`, ...args);
const pushContribution = (id: string, contribute: () => Disposable) => {
if (toDispose.disposed) {
return;
}
try {
toDispose.push(contribute());
} catch (e) {
logError(`Failed to load '${id}' contribution.`, e);
}
};
const configuration = contributions.configuration;
if (configuration) {
for (const config of configuration) {
pushContribution('configuration', () => this.preferenceSchemaProvider.addSchema(config));
}
}
const configurationDefaults = contributions.configurationDefaults;
if (configurationDefaults) {
pushContribution('configurationDefaults', () => this.updateDefaultOverridesSchema(configurationDefaults));
}
const languages = contributions.languages;
if (languages && languages.length) {
for (const lang of languages) {
// it is not possible to unregister a language
monaco.languages.register({
id: lang.id,
aliases: lang.aliases,
extensions: lang.extensions,
filenamePatterns: lang.filenamePatterns,
filenames: lang.filenames,
firstLine: lang.firstLine,
mimetypes: lang.mimetypes
});
if (lang.icon) {
const languageIcon = this.style.toFileIconClass(lang.icon);
pushContribution(`language.${lang.id}.icon`, () => languageIcon);
pushContribution(`language.${lang.id}.iconRegistration`, () => this.languageService.registerIcon(lang.id, languageIcon.object.iconClass));
}
const langConfiguration = lang.configuration;
if (langConfiguration) {
pushContribution(`language.${lang.id}.configuration`, () => monaco.languages.setLanguageConfiguration(lang.id, {
wordPattern: this.createRegex(langConfiguration.wordPattern),
autoClosingPairs: langConfiguration.autoClosingPairs,
brackets: langConfiguration.brackets,
comments: langConfiguration.comments,
folding: this.convertFolding(langConfiguration.folding),
surroundingPairs: langConfiguration.surroundingPairs,
indentationRules: this.convertIndentationRules(langConfiguration.indentationRules),
onEnterRules: this.convertOnEnterRules(langConfiguration.onEnterRules),
}));
}
}
}
const grammars = contributions.grammars;
if (grammars && grammars.length) {
const grammarsWithLanguage: GrammarsContribution[] = [];
for (const grammar of grammars) {
if (grammar.injectTo) {
for (const injectScope of grammar.injectTo) {
pushContribution(`grammar.injectTo.${injectScope}`, () => {
const injections = this.injections.get(injectScope) || [];
injections.push(grammar.scope);
this.injections.set(injectScope, injections);
return Disposable.create(() => {
const index = injections.indexOf(grammar.scope);
if (index !== -1) {
injections.splice(index, 1);
}
});
});
}
}
if (grammar.language) {
// processing is deferred.
grammarsWithLanguage.push(grammar);
}
pushContribution(`grammar.textmate.scope.${grammar.scope}`, () => this.grammarsRegistry.registerTextmateGrammarScope(grammar.scope, {
async getGrammarDefinition(): Promise<GrammarDefinition> {
return {
format: grammar.format,
content: grammar.grammar || '',
location: grammar.grammarLocation
};
},
getInjections: (scopeName: string) =>
this.injections.get(scopeName)!
}));
}
// load grammars on next tick to await registration of languages from all plugins in current tick
// see https://github.com/eclipse-theia/theia/issues/6907#issuecomment-578600243
setTimeout(() => {
for (const grammar of grammarsWithLanguage) {
const language = grammar.language!;
pushContribution(`grammar.language.${language}.scope`, () => this.grammarsRegistry.mapLanguageIdToTextmateGrammar(language, grammar.scope));
pushContribution(`grammar.language.${language}.configuration`, () => this.grammarsRegistry.registerGrammarConfiguration(language, {
embeddedLanguages: this.convertEmbeddedLanguages(grammar.embeddedLanguages, logWarning),
tokenTypes: this.convertTokenTypes(grammar.tokenTypes),
balancedBracketSelectors: grammar.balancedBracketScopes ?? ['*'],
unbalancedBracketSelectors: grammar.balancedBracketScopes,
}));
}
// activate grammars only once everything else is loaded.
// see https://github.com/eclipse-theia/theia-cpp-extensions/issues/100#issuecomment-610643866
setTimeout(() => {
for (const grammar of grammarsWithLanguage) {
const language = grammar.language!;
pushContribution(`grammar.language.${language}.activation`,
() => this.monacoTextmateService.activateLanguage(language)
);
}
});
});
}
pushContribution('commands', () => this.registerCommands(contributions));
pushContribution('menus', () => this.menusContributionHandler.handle(plugin));
pushContribution('keybindings', () => this.keybindingsContributionHandler.handle(contributions));
if (contributions.customEditors) {
for (const customEditor of contributions.customEditors) {
pushContribution(`customEditors.${customEditor.viewType}`,
() => this.customEditorRegistry.registerCustomEditor(customEditor, plugin)
);
}
}
if (contributions.viewsContainers) {
for (const location in contributions.viewsContainers) {
if (contributions.viewsContainers!.hasOwnProperty(location)) {
for (const viewContainer of contributions.viewsContainers[location]) {
pushContribution(`viewContainers.${viewContainer.id}`,
() => this.viewRegistry.registerViewContainer(location, viewContainer)
);
}
}
}
}
if (contributions.views) {
// eslint-disable-next-line guard-for-in
for (const location in contributions.views) {
for (const view of contributions.views[location]) {
pushContribution(`views.${view.id}`,
() => this.viewRegistry.registerView(location, view)
);
}
}
}
if (contributions.viewsWelcome) {
for (const [index, viewWelcome] of contributions.viewsWelcome.entries()) {
pushContribution(`viewsWelcome.${viewWelcome.view}.${index}`,
() => this.viewRegistry.registerViewWelcome(viewWelcome)
);
}
}
if (contributions.snippets) {
for (const snippet of contributions.snippets) {
pushContribution(`snippets.${snippet.uri}`, () => this.snippetSuggestProvider.fromURI(snippet.uri, {
language: snippet.language,
source: snippet.source
}));
}
}
if (contributions.themes && contributions.themes.length) {
const pending = {};
for (const theme of contributions.themes) {
pushContribution(`themes.${theme.uri}`, () => this.monacoThemingService.register(theme, pending));
}
}
if (contributions.iconThemes && contributions.iconThemes.length) {
for (const iconTheme of contributions.iconThemes) {
pushContribution(`iconThemes.${iconTheme.uri}`, () => this.iconThemeService.register(iconTheme, plugin));
}
}
if (contributions.icons && contributions.icons.length) {
for (const icon of contributions.icons) {
const defaultIcon = icon.defaults;
let key: string;
if (IconContribution.isIconDefinition(defaultIcon)) {
key = defaultIcon.location;
} else {
key = defaultIcon.id;
}
pushContribution(`icons.${key}`, () => this.iconService.register(icon, plugin));
}
}
const colors = contributions.colors;
if (colors) {
pushContribution('colors', () => this.colors.register(...colors));
}
if (contributions.taskDefinitions) {
for (const taskDefinition of contributions.taskDefinitions) {
pushContribution(`taskDefinitions.${taskDefinition.taskType}`,
() => this.taskDefinitionRegistry.register(taskDefinition)
);
}
}
if (contributions.problemPatterns) {
for (const problemPattern of contributions.problemPatterns) {
pushContribution(`problemPatterns.${problemPattern.name || problemPattern.regexp}`,
() => this.problemPatternRegistry.register(problemPattern)
);
}
}
if (contributions.problemMatchers) {
for (const problemMatcher of contributions.problemMatchers) {
pushContribution(`problemMatchers.${problemMatcher.label}`,
() => this.problemMatcherRegistry.register(problemMatcher)
);
}
}
if (contributions.debuggers && contributions.debuggers.length) {
toDispose.push(Disposable.create(() => this.debugSchema.update()));
for (const contribution of contributions.debuggers) {
pushContribution(`debuggers.${contribution.type}`,
() => this.debugService.registerDebugger(contribution)
);
}
this.debugSchema.update();
}
if (contributions.resourceLabelFormatters) {
for (const formatter of contributions.resourceLabelFormatters) {
for (const contribution of this.contributionProvider.getContributions()) {
if (contribution instanceof DefaultUriLabelProviderContribution) {
pushContribution(`resourceLabelFormatters.${formatter.scheme}`,
() => contribution.registerFormatter(formatter)
);
}
}
}
}
const self = this;
if (contributions.terminalProfiles) {
for (const profile of contributions.terminalProfiles) {
pushContribution(`terminalProfiles.${profile.id}`, () => {
this.contributedProfileStore.registerTerminalProfile(profile.title, {
async start(): Promise<TerminalWidget> {
const terminalId = await self.pluginTerminalRegistry.start(profile.id);
const result = self.terminalService.getById(terminalId);
if (!result) {
throw new Error(`Error starting terminal from profile ${profile.id}`);
}
return result;
}
});
return Disposable.create(() => {
this.contributedProfileStore.unregisterTerminalProfile(profile.id);
});
});
}
}
if (contributions.notebooks) {
for (const notebook of contributions.notebooks) {
pushContribution(`notebook.${notebook.type}`,
() => this.notebookTypeRegistry.registerNotebookType(notebook, plugin.metadata.model.displayName)
);
}
}
if (contributions.notebookRenderer) {
for (const renderer of contributions.notebookRenderer) {
pushContribution(`notebookRenderer.${renderer.id}`,
() => this.notebookRendererRegistry.registerNotebookRenderer(renderer, PluginPackage.toPluginUrl(plugin.metadata.model, ''))
);
}
}
if (contributions.notebookPreload) {
for (const preload of contributions.notebookPreload) {
pushContribution(`notebookPreloads.${preload.type}:${preload.entrypoint}`,
() => this.notebookRendererRegistry.registerStaticNotebookPreload(preload.type, preload.entrypoint, PluginPackage.toPluginUrl(plugin.metadata.model, ''))
);
}
}
return toDispose;
}
protected registerCommands(contribution: PluginContribution): Disposable {
if (!contribution.commands) {
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
for (const { iconUrl, themeIcon, command, category, shortTitle, title, originalTitle, enablement } of contribution.commands) {
const reference = iconUrl && this.style.toIconClass(iconUrl);
const icon = themeIcon && ThemeIcon.fromString(themeIcon);
let iconClass;
if (reference) {
toDispose.push(reference);
iconClass = reference.object.iconClass;
} else if (icon) {
iconClass = ThemeIcon.asClassName(icon);
}
toDispose.push(this.registerCommand({ id: command, category, shortTitle, label: title, originalLabel: originalTitle, iconClass }, enablement));
}
return toDispose;
}
registerCommand(command: Command, enablement?: string): Disposable {
if (this.hasCommand(command.id)) {
console.warn(`command '${command.id}' already registered`);
return Disposable.NULL;
}
const commandHandler: CommandHandler = {
execute: async (...args) => {
const handler = this.commandHandlers.get(command.id);
if (!handler) {
throw new Error(`command '${command.id}' not found`);
}
return handler(...args);
},
// Always enabled - a command can be executed programmatically or via the commands palette.
isEnabled: () => {
if (enablement) {
return this.contextKeyService.match(enablement);
}
return true;
},
// Visibility rules are defined via the `menus` contribution point.
isVisible(): boolean { return true; }
};
if (enablement) {
const contextKeys = this.contextKeyService.parseKeys(enablement);
if (contextKeys && contextKeys.size > 0) {
commandHandler.onDidChangeEnabled = (listener: () => void) => this.contextKeyService.onDidChange(e => {
if (e.affects(contextKeys)) {
listener();
}
});
}
}
const toDispose = new DisposableCollection();
if (this.commands.getCommand(command.id)) {
// overriding built-in command, i.e. `type` by the VSCodeVim extension
toDispose.push(this.commands.registerHandler(command.id, commandHandler));
} else {
toDispose.push(this.commands.registerCommand(command, commandHandler));
}
this.commandHandlers.set(command.id, undefined);
toDispose.push(Disposable.create(() => this.commandHandlers.delete(command.id)));
return toDispose;
}
registerCommandHandler(id: string, execute: CommandHandler['execute']): Disposable {
if (this.hasCommandHandler(id)) {
console.warn(`command handler '${id}' already registered`);
return Disposable.NULL;
}
this.commandHandlers.set(id, execute);
this.onDidRegisterCommandHandlerEmitter.fire(id);
return Disposable.create(() => this.commandHandlers.set(id, undefined));
}
hasCommand(id: string): boolean {
return this.commandHandlers.has(id);
}
hasCommandHandler(id: string): boolean {
return !!this.commandHandlers.get(id);
}
protected updateDefaultOverridesSchema(configurationDefaults: JSONObject): Disposable {
const disposables = new DisposableCollection();
// eslint-disable-next-line guard-for-in
for (const key in configurationDefaults) {
const defaultValue = configurationDefaults[key];
const match = key.match(OVERRIDE_PROPERTY_PATTERN);
if (match && isObject(defaultValue)) {
for (const [propertyName, value] of Object.entries(defaultValue)) {
disposables.push(this.preferenceSchemaProvider.registerOverride(propertyName, match[1], value as JSONValue));
}
} else {
// regular configuration override
disposables.push(this.preferenceSchemaProvider.registerOverride(key, undefined, defaultValue));
}
}
return disposables;
}
private createRegex(value: string | RegExpOptions | undefined): RegExp | undefined {
if (typeof value === 'string') {
return new RegExp(value, '');
}
if (typeof value == 'undefined') {
return undefined;
}
return new RegExp(value.pattern, value.flags);
}
private convertIndentationRules(rules?: IndentationRules): monaco.languages.IndentationRule | undefined {
if (!rules) {
return undefined;
}
return {
decreaseIndentPattern: this.createRegex(rules.decreaseIndentPattern)!,
increaseIndentPattern: this.createRegex(rules.increaseIndentPattern)!,
indentNextLinePattern: this.createRegex(rules.indentNextLinePattern),
unIndentedLinePattern: this.createRegex(rules.unIndentedLinePattern)
};
}
private convertFolding(folding?: FoldingRules): monaco.languages.FoldingRules | undefined {
if (!folding) {
return undefined;
}
const result: monaco.languages.FoldingRules = {
offSide: folding.offSide
};
if (folding.markers) {
result.markers = {
end: this.createRegex(folding.markers.end)!,
start: this.createRegex(folding.markers.start)!
};
}
return result;
}
private convertTokenTypes(tokenTypes?: ScopeMap): ITokenTypeMap | undefined {
if (typeof tokenTypes === 'undefined' || tokenTypes === null) {
return undefined;
}
const result = Object.create(null);
const scopes = Object.keys(tokenTypes);
const len = scopes.length;
for (let i = 0; i < len; i++) {
const scope = scopes[i];
const tokenType = tokenTypes[scope];
switch (tokenType) {
case 'string':
result[scope] = StandardTokenType.String;
break;
case 'other':
result[scope] = StandardTokenType.Other;
break;
case 'comment':
result[scope] = StandardTokenType.Comment;
break;
}
}
return result;
}
private convertEmbeddedLanguages(languages: ScopeMap | undefined, logWarning: (warning: string) => void): IEmbeddedLanguagesMap | undefined {
if (typeof languages === 'undefined' || languages === null) {
return undefined;
}
const result = Object.create(null);
const scopes = Object.keys(languages);
const len = scopes.length;
for (let i = 0; i < len; i++) {
const scope = scopes[i];
const langId = languages[scope];
result[scope] = getEncodedLanguageId(langId);
if (!result[scope]) {
logWarning(`Language for '${scope}' not found.`);
}
}
return result;
}
private convertOnEnterRules(onEnterRules?: OnEnterRule[]): monaco.languages.OnEnterRule[] | undefined {
if (!onEnterRules) {
return undefined;
}
const result: monaco.languages.OnEnterRule[] = [];
for (const onEnterRule of onEnterRules) {
const rule: monaco.languages.OnEnterRule = {
beforeText: this.createRegex(onEnterRule.beforeText)!,
afterText: this.createRegex(onEnterRule.afterText),
previousLineText: this.createRegex(onEnterRule.previousLineText),
action: this.createEnterAction(onEnterRule.action),
};
result.push(rule);
}
return result;
}
private createEnterAction(action: EnterAction): monaco.languages.EnterAction {
let indentAction: monaco.languages.IndentAction;
switch (action.indent) {
case 'indent':
indentAction = monaco.languages.IndentAction.Indent;
break;
case 'indentOutdent':
indentAction = monaco.languages.IndentAction.IndentOutdent;
break;
case 'outdent':
indentAction = monaco.languages.IndentAction.Outdent;
break;
default:
indentAction = monaco.languages.IndentAction.None;
break;
}
return { indentAction, appendText: action.appendText, removeText: action.removeText };
}
}

View File

@@ -0,0 +1,39 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Widget } from '@theia/core/lib/browser';
import { injectable } from '@theia/core/shared/inversify';
import { ArgumentProcessor } from '../../common/commands';
/**
* This processor handles arguments passed to commands that are contributed by plugins and available as toolbar items.
*
* When a toolbar item executes a command, it often passes the active widget as an argument. This can lead to
* serialization problems. To solve this issue, this processor checks if an argument is a Widget instance and if so, it extracts
* and returns only the widget's ID, which can be safely serialized and used to identify the widget in the plugin host.
*/
@injectable()
export class PluginExtToolbarItemArgumentProcessor implements ArgumentProcessor {
processArgument(arg: unknown): unknown {
if (arg instanceof Widget) {
return arg.id;
}
return arg;
}
}

View File

@@ -0,0 +1,297 @@
// *****************************************************************************
// 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 '../../../src/main/style/status-bar.css';
import '../../../src/main/browser/style/index.css';
import '../../../src/main/browser/style/comments.css';
import { ContainerModule } from '@theia/core/shared/inversify';
import {
FrontendApplicationContribution, WidgetFactory, bindViewContribution,
ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeWidget, LabelProviderContribution, LabelProvider,
UndoRedoHandler, DiffUris, Navigatable, SplitWidget,
noopWidgetStatusBarContribution,
WidgetStatusBarContribution
} from '@theia/core/lib/browser';
import { MaybePromise, CommandContribution, ResourceResolver, bindContributionProvider, URI, generateUuid, PreferenceContribution } from '@theia/core/lib/common';
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
import { HostedPluginWatcher } from '../../hosted/browser/hosted-plugin-watcher';
import { OpenUriCommandHandler } from './commands';
import { PluginApiFrontendContribution } from './plugin-frontend-contribution';
import { HostedPluginServer, hostedServicePath, PluginServer, pluginServerJsonRpcPath } from '../../common/plugin-protocol';
import { ModalNotification } from './dialogs/modal-notification';
import { PluginWidget } from './plugin-ext-widget';
import { PluginFrontendViewContribution } from './plugin-frontend-view-contribution';
import { EditorModelService } from './text-editor-model-service';
import { MenusContributionPointHandler } from './menus/menus-contribution-handler';
import { PluginContributionHandler } from './plugin-contribution-handler';
import { PluginViewRegistry, PLUGIN_VIEW_CONTAINER_FACTORY_ID, PLUGIN_VIEW_FACTORY_ID, PLUGIN_VIEW_DATA_FACTORY_ID } from './view/plugin-view-registry';
import { TextContentResourceResolver } from './workspace-main';
import { MainPluginApiProvider } from '../../common/plugin-ext-api-contribution';
import { PluginPathsService, pluginPathsServicePath } from '../common/plugin-paths-protocol';
import { KeybindingsContributionPointHandler } from './keybindings/keybindings-contribution-handler';
import { DebugSessionContributionRegistry } from '@theia/debug/lib/browser/debug-session-contribution';
import { PluginDebugSessionContributionRegistry } from './debug/plugin-debug-session-contribution-registry';
import { PluginDebugService } from './debug/plugin-debug-service';
import { DebugService } from '@theia/debug/lib/common/debug-service';
import { PluginSharedStyle } from './plugin-shared-style';
import { SelectionProviderCommandContribution } from './selection-provider-command';
import { ViewContextKeyService } from './view/view-context-key-service';
import { PluginViewWidget, PluginViewWidgetIdentifier } from './view/plugin-view-widget';
import { TreeViewWidgetOptions, VIEW_ITEM_CONTEXT_MENU, PluginTree, TreeViewWidget, PluginTreeModel } from './view/tree-view-widget';
import { RPCProtocol } from '../../common/rpc-protocol';
import { LanguagesMainFactory, OutputChannelRegistryFactory } from '../../common';
import { LanguagesMainImpl } from './languages-main';
import { OutputChannelRegistryMainImpl } from './output-channel-registry-main';
import { WebviewWidget } from './webview/webview';
import { WebviewEnvironment } from './webview/webview-environment';
import { WebviewThemeDataProvider } from './webview/webview-theme-data-provider';
import { WebviewResourceCache } from './webview/webview-resource-cache';
import { PluginIconThemeService, PluginIconThemeFactory, PluginIconThemeDefinition, PluginIconTheme } from './plugin-icon-theme-service';
import { PluginTreeViewNodeLabelProvider } from './view/plugin-tree-view-node-label-provider';
import { WebviewWidgetFactory } from './webview/webview-widget-factory';
import { CommentsService, PluginCommentService } from './comments/comments-service';
import { CommentingRangeDecorator } from './comments/comments-decorator';
import { CommentsContribution } from './comments/comments-contribution';
import { CommentsContext } from './comments/comments-context';
import { PluginCustomEditorRegistry } from './custom-editors/plugin-custom-editor-registry';
import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-editor-widget-factory';
import { CustomEditorWidget } from './custom-editors/custom-editor-widget';
import { CustomEditorService } from './custom-editors/custom-editor-service';
import { WebviewFrontendSecurityWarnings } from './webview/webview-frontend-security-warnings';
import { PluginAuthenticationServiceImpl } from './plugin-authentication-service';
import { AuthenticationService } from '@theia/core/lib/browser/authentication-service';
import { bindTreeViewDecoratorUtilities, TreeViewDecoratorService } from './view/tree-view-decorator-service';
import { PluginMenuCommandAdapter } from './menus/plugin-menu-command-adapter';
import './theme-icon-override';
import { PluginIconService } from './plugin-icon-service';
import { PluginTerminalRegistry } from './plugin-terminal-registry';
import { DnDFileContentStore } from './view/dnd-file-content-store';
import { WebviewContextKeys } from './webview/webview-context-keys';
import { LanguagePackService, languagePackServicePath } from '../../common/language-pack-service';
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { CellOutputWebviewFactory } from '@theia/notebook/lib/browser';
import { CellOutputWebviewImpl, createCellOutputWebviewContainer } from './notebooks/renderers/cell-output-webview';
import { ArgumentProcessorContribution } from './command-registry-main';
import { WebviewSecondaryWindowSupport } from './webview/webview-secondary-window-support';
import { CustomEditorUndoRedoHandler } from './custom-editors/custom-editor-undo-redo-handler';
import { bindWebviewPreferences } from '../common/webview-preferences';
import { WebviewFrontendPreferenceContribution } from './webview/webview-frontend-preference-contribution';
import { PluginExtToolbarItemArgumentProcessor } from './plugin-ext-argument-processor';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(LanguagesMainImpl).toSelf().inTransientScope();
bind(LanguagesMainFactory).toFactory(context => (rpc: RPCProtocol) => {
const child = context.container.createChild();
child.bind(RPCProtocol).toConstantValue(rpc);
return child.get(LanguagesMainImpl);
});
bind(OutputChannelRegistryMainImpl).toSelf().inTransientScope();
bind(OutputChannelRegistryFactory).toFactory(context => () => {
const child = context.container.createChild();
return child.get(OutputChannelRegistryMainImpl);
});
bind(ModalNotification).toSelf().inSingletonScope();
bind(HostedPluginSupport).toSelf().inSingletonScope();
bind(HostedPluginWatcher).toSelf().inSingletonScope();
bind(SelectionProviderCommandContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(SelectionProviderCommandContribution);
bind(OpenUriCommandHandler).toSelf().inSingletonScope();
bind(PluginApiFrontendContribution).toSelf().inSingletonScope();
bind(CommandContribution).toService(PluginApiFrontendContribution);
bind(TabBarToolbarContribution).toService(PluginApiFrontendContribution);
bind(EditorModelService).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toDynamicValue(ctx => ({
onStart(): MaybePromise<void> {
ctx.container.get(HostedPluginSupport).onStart(ctx.container);
}
}));
bind(HostedPluginServer).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
const hostedWatcher = ctx.container.get(HostedPluginWatcher);
return connection.createProxy<HostedPluginServer>(hostedServicePath, hostedWatcher.getHostedPluginClient());
}).inSingletonScope();
bind(PluginPathsService).toDynamicValue(ctx => {
const connection = ctx.container.get(WebSocketConnectionProvider);
return connection.createProxy<PluginPathsService>(pluginPathsServicePath);
}).inSingletonScope();
bindViewContribution(bind, PluginFrontendViewContribution);
bind(PluginWidget).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: PluginFrontendViewContribution.PLUGINS_WIDGET_FACTORY_ID,
createWidget: () => ctx.container.get(PluginWidget)
}));
bind(PluginServer).toDynamicValue(ctx => {
const provider = ctx.container.get(WebSocketConnectionProvider);
return provider.createProxy<PluginServer>(pluginServerJsonRpcPath);
}).inSingletonScope();
bind(ViewContextKeyService).toSelf().inSingletonScope();
bindTreeViewDecoratorUtilities(bind);
bind(PluginTreeViewNodeLabelProvider).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(PluginTreeViewNodeLabelProvider);
bind(DnDFileContentStore).toSelf().inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_DATA_FACTORY_ID,
createWidget: (options: TreeViewWidgetOptions) => {
const props = {
contextMenuPath: VIEW_ITEM_CONTEXT_MENU,
expandOnlyOnExpansionToggleClick: true,
expansionTogglePadding: 22,
globalSelection: true,
leftPadding: 8,
search: true,
multiSelect: options.multiSelect
};
const child = createTreeContainer(container, {
props,
tree: PluginTree,
model: PluginTreeModel,
widget: TreeViewWidget,
decoratorService: TreeViewDecoratorService
});
child.bind(TreeViewWidgetOptions).toConstantValue(options);
return child.get(TreeWidget);
}
})).inSingletonScope();
bindWebviewPreferences(bind);
bind(WebviewFrontendPreferenceContribution).toSelf().inSingletonScope();
bind(PreferenceContribution).toService(WebviewFrontendPreferenceContribution);
bind(WebviewEnvironment).toSelf().inSingletonScope();
bind(WebviewThemeDataProvider).toSelf().inSingletonScope();
bind(WebviewResourceCache).toSelf().inSingletonScope();
bind(WebviewWidget).toSelf();
bind(WebviewWidgetFactory).toDynamicValue(ctx => new WebviewWidgetFactory(ctx.container)).inSingletonScope();
bind(WidgetFactory).toService(WebviewWidgetFactory);
bind(WebviewContextKeys).toSelf().inSingletonScope();
bind(WebviewSecondaryWindowSupport).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(WebviewSecondaryWindowSupport);
bind(FrontendApplicationContribution).toService(WebviewContextKeys);
bind(WidgetStatusBarContribution).toConstantValue(noopWidgetStatusBarContribution(WebviewWidget));
bind(PluginCustomEditorRegistry).toSelf().inSingletonScope();
bind(CustomEditorService).toSelf().inSingletonScope();
bind(CustomEditorWidget).toSelf();
bind(CustomEditorWidgetFactory).toDynamicValue(ctx => new CustomEditorWidgetFactory(ctx.container)).inSingletonScope();
bind(WidgetFactory).toService(CustomEditorWidgetFactory);
bind(CustomEditorUndoRedoHandler).toSelf().inSingletonScope();
bind(UndoRedoHandler).toService(CustomEditorUndoRedoHandler);
bind(WidgetFactory).toDynamicValue(ctx => ({
id: CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID,
createWidget: (arg: { uri: string, viewType: string }) => {
const uri = new URI(arg.uri);
const [leftUri, rightUri] = DiffUris.decode(uri);
const navigatable: Navigatable = {
getResourceUri: () => rightUri,
createMoveToUri: resourceUri => DiffUris.encode(leftUri, rightUri.withPath(resourceUri.path))
};
const widget = new SplitWidget({ navigatable });
widget.id = arg.viewType + '.side-by-side:' + generateUuid();
const labelProvider = ctx.container.get(LabelProvider);
widget.title.label = labelProvider.getName(uri);
widget.title.iconClass = labelProvider.getIcon(uri);
widget.title.closable = true;
return widget;
}
})).inSingletonScope();
bind(PluginViewWidget).toSelf();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_FACTORY_ID,
createWidget: (identifier: PluginViewWidgetIdentifier) => {
const child = container.createChild();
child.bind(PluginViewWidgetIdentifier).toConstantValue(identifier);
return child.get(PluginViewWidget);
}
})).inSingletonScope();
bind(WidgetFactory).toDynamicValue(({ container }) => ({
id: PLUGIN_VIEW_CONTAINER_FACTORY_ID,
createWidget: (identifier: ViewContainerIdentifier) =>
container.get<ViewContainer.Factory>(ViewContainer.Factory)(identifier)
})).inSingletonScope();
bind(PluginSharedStyle).toSelf().inSingletonScope();
bind(PluginViewRegistry).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(PluginViewRegistry);
bind(PluginIconThemeFactory).toFactory<PluginIconTheme>(({ container }) => (definition: PluginIconThemeDefinition) => {
const child = container.createChild();
child.bind(PluginIconThemeDefinition).toConstantValue(definition);
child.bind(PluginIconTheme).toSelf().inSingletonScope();
return child.get(PluginIconTheme);
});
bind(PluginIconThemeService).toSelf().inSingletonScope();
bind(LabelProviderContribution).toService(PluginIconThemeService);
bind(MenusContributionPointHandler).toSelf().inSingletonScope();
bind(PluginMenuCommandAdapter).toSelf().inSingletonScope();
bind(KeybindingsContributionPointHandler).toSelf().inSingletonScope();
bind(PluginContributionHandler).toSelf().inSingletonScope();
bind(TextContentResourceResolver).toSelf().inSingletonScope();
bind(ResourceResolver).toService(TextContentResourceResolver);
bindContributionProvider(bind, MainPluginApiProvider);
bind(PluginDebugService).toSelf().inSingletonScope();
rebind(DebugService).toService(PluginDebugService);
bind(PluginDebugSessionContributionRegistry).toSelf().inSingletonScope();
rebind(DebugSessionContributionRegistry).toService(PluginDebugSessionContributionRegistry);
bind(CommentsService).to(PluginCommentService).inSingletonScope();
bind(CommentingRangeDecorator).toSelf().inSingletonScope();
bind(CommentsContribution).toSelf().inSingletonScope();
bind(CommentsContext).toSelf().inSingletonScope();
bind(WebviewFrontendSecurityWarnings).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(WebviewFrontendSecurityWarnings);
bind(PluginIconService).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(PluginIconService);
bind(PluginAuthenticationServiceImpl).toSelf().inSingletonScope();
rebind(AuthenticationService).toService(PluginAuthenticationServiceImpl);
bind(PluginTerminalRegistry).toSelf().inSingletonScope();
bind(LanguagePackService).toDynamicValue(ctx => {
const provider = ctx.container.get(WebSocketConnectionProvider);
return provider.createProxy<LanguagePackService>(languagePackServicePath);
}).inSingletonScope();
bind(CellOutputWebviewFactory).toFactory(ctx => () =>
createCellOutputWebviewContainer(ctx.container).get(CellOutputWebviewImpl)
);
bindContributionProvider(bind, ArgumentProcessorContribution);
bind(PluginExtToolbarItemArgumentProcessor).toSelf().inSingletonScope();
bind(ArgumentProcessorContribution).toService(PluginExtToolbarItemArgumentProcessor);
});

View File

@@ -0,0 +1,132 @@
// *****************************************************************************
// 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 React from '@theia/core/shared/react';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { Message } from '@theia/core/shared/@lumino/messaging';
import { PluginMetadata } from '../../common/plugin-protocol';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';
import { AlertMessage } from '@theia/core/lib/browser/widgets/alert-message';
import { HostedPluginSupport, PluginProgressLocation } from '../../hosted/browser/hosted-plugin';
import { ProgressBarFactory } from '@theia/core/lib/browser/progress-bar-factory';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { codicon } from '@theia/core/lib/browser';
import { nls } from '@theia/core/lib/common';
export const PLUGINS_LABEL = nls.localize('theia/plugin-ext/plugins', 'Plugins');
@injectable()
export class PluginWidget extends ReactWidget {
@inject(HostedPluginSupport)
protected readonly pluginService: HostedPluginSupport;
@inject(ProgressBarFactory)
protected readonly progressBarFactory: ProgressBarFactory;
constructor() {
super();
this.id = 'plugins';
this.title.label = PLUGINS_LABEL;
this.title.caption = PLUGINS_LABEL;
this.title.iconClass = codicon('diff-added');
this.title.closable = true;
this.node.tabIndex = 0;
this.addClass('theia-plugins');
this.update();
}
@postConstruct()
protected init(): void {
this.toDispose.push(this.pluginService.onDidChangePlugins(() => this.update()));
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
this.node.focus();
}
protected readonly toDisposeProgress = new DisposableCollection();
protected render(): React.ReactNode {
return <div ref={ref => {
this.toDisposeProgress.dispose();
this.toDispose.push(this.toDisposeProgress);
if (ref) {
this.toDispose.push(this.progressBarFactory({ container: this.node, insertMode: 'prepend', locationId: PluginProgressLocation }));
}
}}>{this.doRender()}</div>;
}
protected doRender(): React.ReactNode {
const plugins = this.pluginService.plugins;
if (!plugins.length) {
return <AlertMessage type='INFO' header='No plugins currently available.' />;
}
return <React.Fragment>{this.renderPlugins(plugins)}</React.Fragment>;
}
protected renderPlugins(plugins: PluginMetadata[]): React.ReactNode {
return <div id='pluginListContainer'>
{plugins.sort((a, b) => this.compareMetadata(a, b)).map(plugin => this.renderPlugin(plugin))}
</div>;
}
private renderPlugin(plugin: PluginMetadata): JSX.Element {
return <div key={plugin.model.name} className={this.createPluginClassName(plugin)}>
<div className='column flexcontainer pluginInformationContainer'>
<div className='row flexcontainer'>
<div className={codicon('list-selection')}></div>
<div title={plugin.model.name} className='pluginName noWrapInfo'>{plugin.model.name}</div>
</div>
<div className='row flexcontainer'>
<div className='pluginVersion'>{plugin.model.version}</div>
</div>
<div className='row flexcontainer'>
<div className='pluginDescription noWrapInfo'>{plugin.model.description}</div>
</div>
<div className='row flexcontainer'>
<div className='pluginPublisher noWrapInfo flexcontainer'>{plugin.model.publisher}</div>
</div>
</div>
</div>;
}
protected createPluginClassName(plugin: PluginMetadata): string {
const classNames = ['pluginHeaderContainer'];
return classNames.join(' ');
}
/**
* Compare two plugins based on their names, and publishers.
* @param a the first plugin metadata.
* @param b the second plugin metadata.
*/
protected compareMetadata(a: PluginMetadata, b: PluginMetadata): number {
// Determine the name of the plugins.
const nameA = a.model.name.toLowerCase();
const nameB = b.model.name.toLowerCase();
// Determine the publisher of the plugin (when names are equal).
const publisherA = a.model.publisher.toLowerCase();
const publisherB = b.model.publisher.toLowerCase();
return (nameA === nameA)
? nameA.localeCompare(nameB)
: publisherA.localeCompare(publisherB);
}
}

View File

@@ -0,0 +1,70 @@
// *****************************************************************************
// 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 { CommandRegistry, CommandContribution, Command } from '@theia/core/lib/common';
import { OpenUriCommandHandler } from './commands';
import URI from '@theia/core/lib/common/uri';
import { TreeViewWidget } from './view/tree-view-widget';
import { CompositeTreeNode, Widget, codicon } from '@theia/core/lib/browser';
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
import { PluginViewWidget } from './view/plugin-view-widget';
@injectable()
export class PluginApiFrontendContribution implements CommandContribution, TabBarToolbarContribution {
@inject(OpenUriCommandHandler)
protected readonly openUriCommandHandler: OpenUriCommandHandler;
static readonly COLLAPSE_ALL_COMMAND = Command.toDefaultLocalizedCommand({
id: 'treeviews.collapseAll',
iconClass: codicon('collapse-all'),
label: 'Collapse All'
});
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(OpenUriCommandHandler.COMMAND_METADATA, {
execute: (arg: URI) => this.openUriCommandHandler.execute(arg),
isVisible: () => false
});
commands.registerCommand(PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND, {
execute: (widget: Widget) => {
if (widget instanceof PluginViewWidget && widget.widgets[0] instanceof TreeViewWidget) {
const model = widget.widgets[0].model;
if (CompositeTreeNode.is(model.root)) {
for (const child of model.root.children) {
if (CompositeTreeNode.is(child)) {
model.collapseAll(child);
}
}
}
}
},
isVisible: (widget: Widget) => widget instanceof PluginViewWidget && widget.widgets[0] instanceof TreeViewWidget && widget.widgets[0].showCollapseAll
});
}
registerToolbarItems(registry: TabBarToolbarRegistry): void {
registry.registerItem({
id: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.id,
command: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.id,
tooltip: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.label,
icon: PluginApiFrontendContribution.COLLAPSE_ALL_COMMAND.iconClass,
priority: 1000
});
}
}

View File

@@ -0,0 +1,38 @@
// *****************************************************************************
// 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 { AbstractViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
import { PLUGINS_LABEL, PluginWidget } from './plugin-ext-widget';
@injectable()
export class PluginFrontendViewContribution extends AbstractViewContribution<PluginWidget> {
public static PLUGINS_WIDGET_FACTORY_ID = 'plugins';
constructor() {
super({
widgetId: PluginFrontendViewContribution.PLUGINS_WIDGET_FACTORY_ID,
widgetName: PLUGINS_LABEL,
defaultWidgetOptions: {
area: 'left',
rank: 400
},
toggleCommandId: 'pluginsView:toggle'
});
}
}

View File

@@ -0,0 +1,92 @@
// *****************************************************************************
// Copyright (C) 2023 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { Endpoint } from '@theia/core/lib/browser';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { inject, injectable } from '@theia/core/shared/inversify';
import { URI } from '@theia/core/shared/vscode-uri';
import { MonacoIconRegistry } from '@theia/monaco/lib/browser/monaco-icon-registry';
import * as path from 'path';
import { IconContribution, DeployedPlugin, IconDefinition } from '../../common/plugin-protocol';
@injectable()
export class PluginIconService implements Disposable {
@inject(MonacoIconRegistry)
protected readonly iconRegistry: MonacoIconRegistry;
protected readonly toDispose = new DisposableCollection();
styleSheet: string = '';
styleElement: HTMLStyleElement;
register(contribution: IconContribution, plugin: DeployedPlugin): Disposable {
const defaultIcon = contribution.defaults;
if (IconContribution.isIconDefinition(defaultIcon)) {
this.registerFontIcon(contribution, defaultIcon);
} else {
this.registerRegularIcon(contribution, defaultIcon.id);
}
return Disposable.NULL;
}
dispose(): void {
this.toDispose.dispose();
}
protected registerFontIcon(contribution: IconContribution, defaultIcon: IconDefinition): void {
const location = this.toPluginUrl(contribution.extensionId, getIconRelativePath(URI.parse(defaultIcon.location).path));
const format = getFileExtension(location.path);
const fontId = getFontId(contribution.extensionId, location.path);
const definition = this.iconRegistry.registerIconFont(fontId, { src: [{ location: location, format }] });
this.iconRegistry.registerIcon(contribution.id, {
fontCharacter: defaultIcon.fontCharacter,
font: {
id: fontId,
definition
}
}, contribution.description);
}
protected registerRegularIcon(contribution: IconContribution, defaultIconId: string): void {
this.iconRegistry.registerIcon(contribution.id, { id: defaultIconId }, contribution.description);
}
protected toPluginUrl(id: string, relativePath: string): URI {
return URI.from(new Endpoint({
path: `hostedPlugin/${this.formatExtensionId(id)}/${encodeURIComponent(relativePath)}`
}).getRestUrl().toComponents());
}
protected formatExtensionId(id: string): string {
return id.replace(/\W/g, '_');
}
}
function getIconRelativePath(iconPath: string): string {
const index = iconPath.indexOf('extension');
return index === -1 ? '' : iconPath.substring(index + 'extension'.length + 1);
}
function getFontId(extensionId: string, fontPath: string): string {
return path.join(extensionId, fontPath);
}
function getFileExtension(filePath: string): string {
const index = filePath.lastIndexOf('.');
return index === -1 ? '' : filePath.substring(index + 1);
}

View File

@@ -0,0 +1,625 @@
// *****************************************************************************
// 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code is copied and modified from:
// https://github.com/microsoft/vscode/blob/7cf4cca47aa025a590fc939af54932042302be63/src/vs/workbench/services/themes/browser/fileIconThemeData.ts
import debounce = require('@theia/core/shared/lodash.debounce');
import * as jsoncparser from 'jsonc-parser';
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
import { IconThemeService, IconTheme, IconThemeDefinition } from '@theia/core/lib/browser/icon-theme-service';
import { IconThemeContribution, DeployedPlugin, UiTheme, getPluginId } from '../../common/plugin-protocol';
import URI from '@theia/core/lib/common/uri';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Emitter } from '@theia/core/lib/common/event';
import { RecursivePartial } from '@theia/core/lib/common/types';
import { LabelProviderContribution, DidChangeLabelEvent, LabelProvider, URIIconReference } from '@theia/core/lib/browser/label-provider';
import { ThemeType } from '@theia/core/lib/common/theme';
import { FileStatNode, DirNode } from '@theia/filesystem/lib/browser';
import { WorkspaceRootNode } from '@theia/navigator/lib/browser/navigator-tree';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileStat, FileChangeType } from '@theia/filesystem/lib/common/files';
import { WorkspaceService } from '@theia/workspace/lib/browser';
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/core/lib/browser/language-service';
import { DEFAULT_ICON_SIZE, PLUGIN_FILE_ICON_CLASS } from './plugin-shared-style';
export interface PluginIconDefinition {
iconPath: string;
fontColor: string;
fontCharacter: string;
fontSize: string;
fontId: string;
}
export interface PluginFontDefinition {
id: string;
weight: string;
style: string;
size: string;
src: { path: string; format: string; }[];
}
export interface PluginIconsAssociation {
folder?: string;
file?: string;
folderExpanded?: string;
rootFolder?: string;
rootFolderExpanded?: string;
folderNames?: { [folderName: string]: string; };
folderNamesExpanded?: { [folderName: string]: string; };
fileExtensions?: { [extension: string]: string; };
fileNames?: { [fileName: string]: string; };
languageIds?: { [languageId: string]: string; };
}
export interface PluginIconDefinitions {
[key: string]: PluginIconDefinition
}
export interface PluginIconThemeDocument extends PluginIconsAssociation {
iconDefinitions: PluginIconDefinitions;
fonts: PluginFontDefinition[];
light?: PluginIconsAssociation;
highContrast?: PluginIconsAssociation;
hidesExplorerArrows?: boolean;
showLanguageModeIcons?: boolean;
}
export const PluginIconThemeFactory = Symbol('PluginIconThemeFactory');
export type PluginIconThemeFactory = (definition: PluginIconThemeDefinition) => PluginIconTheme;
@injectable()
export class PluginIconThemeDefinition implements IconThemeDefinition, IconThemeContribution {
id: string;
label: string;
description?: string;
uri: string;
uiTheme?: UiTheme;
pluginId: string;
packageUri: string;
hasFileIcons?: boolean;
hasFolderIcons?: boolean;
hidesExplorerArrows?: boolean;
showLanguageModeIcons?: boolean;
}
class PluginLanguageIconInfo {
hasSpecificFileIcons: boolean = false;
coveredLanguages: { [languageId: string]: boolean } = {};
};
@injectable()
export class PluginIconTheme extends PluginIconThemeDefinition implements IconTheme, Disposable {
@inject(FileService)
protected readonly fileService: FileService;
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(PluginIconThemeDefinition)
protected readonly definition: PluginIconThemeDefinition;
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
@inject(LanguageService)
protected readonly languageService: LanguageService;
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected readonly toDeactivate = new DisposableCollection();
protected readonly toUnload = new DisposableCollection();
protected readonly toDisposeStyleElement = new DisposableCollection();
protected readonly toDispose = new DisposableCollection(
this.toDeactivate, this.toDisposeStyleElement, this.toUnload, this.onDidChangeEmitter
);
protected packageRootUri: URI;
protected locationUri: URI;
protected styleSheetContent: string | undefined;
protected readonly icons = new Set<string>();
@postConstruct()
protected init(): void {
Object.assign(this, this.definition);
this.packageRootUri = new URI(this.packageUri);
this.locationUri = new URI(this.uri).parent;
}
dispose(): void {
this.toDispose.dispose();
}
protected fireDidChange(): void {
this.onDidChangeEmitter.fire({ affects: () => true });
}
activate(): Disposable {
if (!this.toDeactivate.disposed) {
return this.toDeactivate;
}
this.toDeactivate.push(Disposable.create(() => this.fireDidChange()));
this.doActivate();
return this.toDeactivate;
}
protected async doActivate(): Promise<void> {
await this.load();
this.updateStyleElement();
}
protected updateStyleElement(): void {
this.toDisposeStyleElement.dispose();
if (this.toDeactivate.disposed || !this.styleSheetContent) {
return;
}
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.className = 'theia-icon-theme';
styleElement.innerText = this.styleSheetContent;
document.head.appendChild(styleElement);
const toRemoveStyleElement = Disposable.create(() => styleElement.remove());
this.toDisposeStyleElement.push(toRemoveStyleElement);
this.toDeactivate.push(toRemoveStyleElement);
this.fireDidChange();
}
protected reload = debounce(() => {
this.toUnload.dispose();
this.doActivate();
}, 50);
/**
* This should be aligned with
* https://github.com/microsoft/vscode/blob/7cf4cca47aa025a590fc939af54932042302be63/src/vs/workbench/services/themes/browser/fileIconThemeData.ts#L201
*/
protected async load(): Promise<void> {
if (this.styleSheetContent !== undefined) {
return;
}
this.styleSheetContent = '';
this.toUnload.push(Disposable.create(() => {
this.styleSheetContent = undefined;
this.hasFileIcons = undefined;
this.hasFolderIcons = undefined;
this.hidesExplorerArrows = undefined;
this.icons.clear();
}));
const uri = new URI(this.uri);
const result = await this.fileService.read(uri);
const content = result.value;
const json: RecursivePartial<PluginIconThemeDocument> = jsoncparser.parse(content, undefined, { disallowComments: false });
this.hidesExplorerArrows = !!json.hidesExplorerArrows;
const toUnwatch = this.fileService.watch(uri);
if (this.toUnload.disposed) {
toUnwatch.dispose();
} else {
this.toUnload.push(toUnwatch);
this.toUnload.push(this.fileService.onDidFilesChange(e => {
if (e.contains(uri, FileChangeType.ADDED) || e.contains(uri, FileChangeType.UPDATED)) {
this.reload();
}
}));
}
const iconDefinitions = json.iconDefinitions;
if (!iconDefinitions) {
return;
}
const definitionSelectors = new Map<string, string[]>();
const acceptSelector = (themeType: ThemeType, definitionId: string, ...icons: string[]) => {
if (!iconDefinitions[definitionId]) {
return;
}
let selector = '';
for (const icon of icons) {
if (icon) {
selector += '.' + icon;
this.icons.add(icon);
}
}
if (!selector) {
return;
}
const selectors = definitionSelectors.get(definitionId) || [];
if (themeType !== 'dark') {
selector = '.theia-' + themeType + ' ' + selector;
}
selectors.push(selector + '::before');
definitionSelectors.set(definitionId, selectors);
};
let iconInfo = this.collectSelectors(json, acceptSelector.bind(undefined, 'dark'));
if (json.light) {
iconInfo = this.collectSelectors(json.light, acceptSelector.bind(undefined, 'light'));
}
if (json.highContrast) {
iconInfo = this.collectSelectors(json.highContrast, acceptSelector.bind(undefined, 'hc'));
}
const showLanguageModeIcons = this.showLanguageModeIcons === true
|| json.showLanguageModeIcons === true
|| (iconInfo.hasSpecificFileIcons && json.showLanguageModeIcons !== false);
const fonts = json.fonts;
if (Array.isArray(fonts)) {
for (const font of fonts) {
if (font) {
let src = '';
if (Array.isArray(font.src)) {
for (const srcLocation of font.src) {
if (srcLocation && srcLocation.path) {
const cssUrl = this.toCSSUrl(srcLocation.path);
if (cssUrl) {
if (src) {
src += ', ';
}
src += `${cssUrl} format('${srcLocation.format}')`;
}
}
}
}
if (src) {
this.styleSheetContent += `@font-face {
src: ${src};
font-family: '${font.id}';
font-weight: ${font.weight};
font-style: ${font.style};
}
`;
}
}
}
const firstFont = fonts[0];
if (firstFont && firstFont.id) {
this.styleSheetContent += `.${this.fileIcon}::before, .${this.folderIcon}::before, .${this.rootFolderIcon}::before {
font-family: '${firstFont.id}';
font-size: ${firstFont.size || '150%'};
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
vertical-align: top;
}
`;
}
}
for (const definitionId of definitionSelectors.keys()) {
const iconDefinition = iconDefinitions[definitionId];
const selectors = definitionSelectors.get(definitionId);
if (selectors && iconDefinition) {
const cssUrl = this.toCSSUrl(iconDefinition.iconPath);
if (cssUrl) {
this.styleSheetContent += `${selectors.join(', ')} {
content: ' ';
background-image: ${cssUrl};
background-size: ${DEFAULT_ICON_SIZE}px;
background-position: left center;
background-repeat: no-repeat;
}
`;
}
if (iconDefinition.fontCharacter || iconDefinition.fontColor) {
let body = '';
if (iconDefinition.fontColor) {
body += ` color: ${iconDefinition.fontColor};`;
}
if (iconDefinition.fontCharacter) {
body += ` content: '${iconDefinition.fontCharacter}';`;
}
if (iconDefinition.fontSize) {
body += ` font-size: ${iconDefinition.fontSize};`;
}
if (iconDefinition.fontId) {
body += ` font-family: ${iconDefinition.fontId};`;
}
this.styleSheetContent += `${selectors.join(', ')} {${body} }\n`;
}
}
}
if (showLanguageModeIcons) {
for (const language of this.languageService.languages) {
// only show language icons if there are no more specific icons in the style document
if (!iconInfo.coveredLanguages[language.id]) {
const icon = this.languageService.getIcon(language.id);
if (icon) {
this.icons.add(this.fileIcon);
this.icons.add(this.languageIcon(language.id));
this.icons.add(icon);
}
}
}
}
}
protected toCSSUrl(iconPath: string | undefined): string | undefined {
if (!iconPath) {
return undefined;
}
const iconUri = this.locationUri.resolve(iconPath);
const relativePath = this.packageRootUri.path.relative(iconUri.path.normalize());
return relativePath && `url('${new Endpoint({
path: `hostedPlugin/${this.pluginId}/${encodeURIComponent(relativePath.normalize().toString())}`
}).getRestUrl().toString()}')`;
}
protected escapeCSS(value: string): string {
value = value.replace(/[^\-a-zA-Z0-9]/g, '-');
if (value.charAt(0).match(/[0-9\-]/)) {
value = '-' + value;
}
return value;
}
protected readonly fileIcon = PLUGIN_FILE_ICON_CLASS;
protected readonly folderIcon = 'theia-plugin-folder-icon';
protected readonly folderExpandedIcon = 'theia-plugin-folder-expanded-icon';
protected readonly rootFolderIcon = 'theia-plugin-root-folder-icon';
protected readonly rootFolderExpandedIcon = 'theia-plugin-root-folder-expanded-icon';
protected folderNameIcon(folderName: string): string {
return 'theia-plugin-' + this.escapeCSS(folderName.toLowerCase()) + '-folder-name-icon';
}
protected expandedFolderNameIcon(folderName: string): string {
return 'theia-plugin-' + this.escapeCSS(folderName.toLowerCase()) + '-expanded-folder-name-icon';
}
protected fileNameIcon(fileName: string): string[] {
fileName = fileName.toLowerCase();
const extIndex = fileName.indexOf('.');
const icons = extIndex !== -1 ? this.fileExtensionIcon(fileName.substring(extIndex + 1)) : [];
icons.unshift('theia-plugin-' + this.escapeCSS(fileName) + '-file-name-icon');
return icons;
}
protected fileExtensionIcon(fileExtension: string): string[] {
fileExtension = fileExtension.toString();
const icons = [];
const segments = fileExtension.split('.');
if (segments.length) {
if (segments.length) {
for (let i = 0; i < segments.length; i++) {
icons.push('theia-plugin-' + this.escapeCSS(segments.slice(i).join('.')) + '-ext-file-icon');
}
icons.push('theia-plugin-ext-file-icon'); // extra segment to increase file-ext score
}
}
return icons;
}
protected languageIcon(languageId: string): string {
return 'theia-plugin-' + this.escapeCSS(languageId) + '-lang-file-icon';
}
protected collectSelectors(associations: RecursivePartial<PluginIconsAssociation>, accept: (definitionId: string, ...icons: string[]) => void): PluginLanguageIconInfo {
const iconInfo = new PluginLanguageIconInfo();
if (associations.folder) {
accept(associations.folder, this.folderIcon);
if (associations.folderExpanded === undefined) {
// Use the same icon for expanded state (issue #12727). Check for
// undefined folderExpanded property to allow for
// "folderExpanded": null in case a developer really wants that
accept(associations.folder, this.folderExpandedIcon);
}
this.hasFolderIcons = true;
}
if (associations.folderExpanded) {
accept(associations.folderExpanded, this.folderExpandedIcon);
this.hasFolderIcons = true;
}
const rootFolder = associations.rootFolder || associations.folder;
if (rootFolder) {
accept(rootFolder, this.rootFolderIcon);
this.hasFolderIcons = true;
}
const rootFolderExpanded = associations.rootFolderExpanded || associations.folderExpanded;
if (rootFolderExpanded) {
accept(rootFolderExpanded, this.rootFolderExpandedIcon);
this.hasFolderIcons = true;
}
if (associations.file) {
accept(associations.file, this.fileIcon);
this.hasFileIcons = true;
}
const folderNames = associations.folderNames;
if (folderNames) {
// eslint-disable-next-line guard-for-in
for (const folderName in folderNames) {
accept(folderNames[folderName]!, this.folderNameIcon(folderName), this.folderIcon);
this.hasFolderIcons = true;
}
}
const folderNamesExpanded = associations.folderNamesExpanded;
if (folderNamesExpanded) {
// eslint-disable-next-line guard-for-in
for (const folderName in folderNamesExpanded) {
accept(folderNamesExpanded[folderName]!, this.expandedFolderNameIcon(folderName), this.folderExpandedIcon);
this.hasFolderIcons = true;
}
}
const languageIds = associations.languageIds;
if (languageIds) {
if (!languageIds.jsonc && languageIds.json) {
languageIds.jsonc = languageIds.json;
}
// eslint-disable-next-line guard-for-in
for (const languageId in languageIds) {
accept(languageIds[languageId]!, this.languageIcon(languageId), this.fileIcon);
this.hasFileIcons = true;
iconInfo.hasSpecificFileIcons = true;
iconInfo.coveredLanguages[languageId] = true;
}
}
const fileExtensions = associations.fileExtensions;
if (fileExtensions) {
// eslint-disable-next-line guard-for-in
for (const fileExtension in fileExtensions) {
accept(fileExtensions[fileExtension]!, ...this.fileExtensionIcon(fileExtension), this.fileIcon);
this.hasFileIcons = true;
iconInfo.hasSpecificFileIcons = true;
}
}
const fileNames = associations.fileNames;
if (fileNames) {
// eslint-disable-next-line guard-for-in
for (const fileName in fileNames) {
accept(fileNames[fileName]!, ...this.fileNameIcon(fileName), this.fileIcon);
this.hasFileIcons = true;
iconInfo.hasSpecificFileIcons = true;
}
}
return iconInfo;
}
/**
* This should be aligned with
* https://github.com/microsoft/vscode/blob/7cf4cca47aa025a590fc939af54932042302be63/src/vs/editor/common/services/getIconClasses.ts#L5
*/
getIcon(element: URI | URIIconReference | FileStat | FileStatNode | WorkspaceRootNode): string {
let icon = '';
for (const className of this.getClassNames(element)) {
if (this.icons.has(className)) {
if (icon) {
icon += ' ';
}
icon += className;
}
}
return icon;
}
protected getClassNames(element: URI | URIIconReference | FileStat | FileStatNode | WorkspaceRootNode): string[] {
if (WorkspaceRootNode.is(element)) {
const name = this.labelProvider.getName(element);
if (element.expanded) {
return [this.rootFolderExpandedIcon, this.expandedFolderNameIcon(name)];
}
return [this.rootFolderIcon, this.folderNameIcon(name)];
}
if (DirNode.is(element)) {
if (element.expanded) {
const name = this.labelProvider.getName(element);
return [this.folderExpandedIcon, this.expandedFolderNameIcon(name)];
}
return this.getFolderClassNames(element);
}
if (FileStatNode.is(element)) {
return this.getFileClassNames(element, element.fileStat.resource.toString());
}
if (FileStat.is(element)) {
if (element.isDirectory) {
return this.getFolderClassNames(element);
}
return this.getFileClassNames(element, element.resource.toString());
}
if (URIIconReference.is(element)) {
if (element.id === 'folder') {
return this.getFolderClassNames(element);
}
return this.getFileClassNames(element, element.uri && element.uri.toString());
}
return this.getFileClassNames(element, element.toString());
}
protected getFolderClassNames(element: object): string[] {
const name = this.labelProvider.getName(element);
return [this.folderIcon, this.folderNameIcon(name)];
}
protected getFileClassNames(element: object, uri?: string): string[] {
const name = this.labelProvider.getName(element);
const classNames = this.fileNameIcon(name);
if (uri) {
const parsedURI = new URI(uri);
const isRoot = this.workspaceService.getWorkspaceRootUri(new URI(uri))?.isEqual(parsedURI);
if (isRoot) {
classNames.unshift(this.rootFolderIcon);
} else {
classNames.unshift(this.fileIcon);
}
const language = StandaloneServices.get(ILanguageService).createByFilepathOrFirstLine(parsedURI['codeUri']);
classNames.push(this.languageIcon(language.languageId));
const defaultLanguageIcon = this.languageService.getIcon(language.languageId);
if (defaultLanguageIcon) {
classNames.push(defaultLanguageIcon);
}
}
return classNames;
}
}
@injectable()
export class PluginIconThemeService implements LabelProviderContribution {
@inject(IconThemeService)
protected readonly iconThemeService: IconThemeService;
@inject(PluginIconThemeFactory)
protected readonly iconThemeFactory: PluginIconThemeFactory;
protected readonly onDidChangeEmitter = new Emitter<DidChangeLabelEvent>();
readonly onDidChange = this.onDidChangeEmitter.event;
protected fireDidChange(): void {
this.onDidChangeEmitter.fire({ affects: () => true });
}
register(contribution: IconThemeContribution, plugin: DeployedPlugin): Disposable {
const pluginId = getPluginId(plugin.metadata.model);
const packageUri = plugin.metadata.model.packageUri;
const iconTheme = this.iconThemeFactory({
id: contribution.id,
label: contribution.label || new URI(contribution.uri).path.base,
description: contribution.description,
uri: contribution.uri,
uiTheme: contribution.uiTheme,
pluginId,
packageUri
});
return new DisposableCollection(
iconTheme,
iconTheme.onDidChange(() => this.fireDidChange()),
this.iconThemeService.register(iconTheme)
);
}
canHandle(element: object): number {
const current = this.iconThemeService.getDefinition(this.iconThemeService.current);
if (current instanceof PluginIconTheme && (
(element instanceof URI && element.scheme === 'file') || URIIconReference.is(element) || FileStat.is(element) || FileStatNode.is(element)
)) {
return Number.MAX_SAFE_INTEGER;
}
return 0;
}
getIcon(element: URI | URIIconReference | FileStat | FileStatNode | WorkspaceRootNode): string | undefined {
const current = this.iconThemeService.getDefinition(this.iconThemeService.current);
if (current instanceof PluginIconTheme) {
return current.getIcon(element);
}
return undefined;
}
}

View File

@@ -0,0 +1,154 @@
// *****************************************************************************
// 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { Theme } from '@theia/core/lib/common/theme';
import { IconUrl } from '../../common/plugin-protocol';
import { Reference, SyncReferenceCollection } from '@theia/core/lib/common/reference';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
export interface PluginIconKey {
url: IconUrl;
size?: number;
type?: 'icon' | 'file';
}
export interface PluginIcon extends Disposable {
readonly iconClass: string
}
export const PLUGIN_FILE_ICON_CLASS = 'theia-plugin-file-icon';
export const DEFAULT_ICON_SIZE = 16;
@injectable()
export class PluginSharedStyle {
@inject(ThemeService) protected readonly themeService: ThemeService;
protected style: HTMLStyleElement;
protected readonly rules: {
selector: string;
body: (theme: Theme) => string
}[] = [];
@postConstruct()
protected init(): void {
this.update();
this.themeService.onDidColorThemeChange(() => this.update());
}
protected readonly toUpdate = new DisposableCollection();
protected update(): void {
this.toUpdate.dispose();
const style = this.style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
document.getElementsByTagName('head')[0].appendChild(style);
this.toUpdate.push(Disposable.create(() =>
document.getElementsByTagName('head')[0].removeChild(style)
));
for (const rule of this.rules) {
this.doInsertRule(rule);
}
}
insertRule(selector: string, body: (theme: Theme) => string): Disposable {
const rule = { selector, body };
this.rules.push(rule);
this.doInsertRule(rule);
return Disposable.create(() => {
const index = this.rules.indexOf(rule);
if (index !== -1) {
this.rules.splice(index, 1);
this.deleteRule(selector);
}
});
}
protected doInsertRule({ selector, body }: {
selector: string;
body: (theme: Theme) => string
}): void {
const sheet = (<CSSStyleSheet>this.style.sheet);
const cssBody = body(this.themeService.getCurrentTheme());
sheet.insertRule(selector + ' {\n' + cssBody + '\n}', 0);
}
deleteRule(selector: string): void {
const sheet = (<CSSStyleSheet>this.style.sheet);
const rules = sheet.rules || sheet.cssRules || [];
for (let i = rules.length - 1; i >= 0; i--) {
const rule = rules[i];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((<any>rule).selectorText.indexOf(selector) !== -1) {
sheet.deleteRule(i);
}
}
}
private readonly icons = new SyncReferenceCollection<PluginIconKey, PluginIcon>(key => this.createPluginIcon(key));
toIconClass(url: IconUrl, { size }: { size: number } = { size: DEFAULT_ICON_SIZE }): Reference<PluginIcon> {
return this.icons.acquire({ url, size });
}
toFileIconClass(url: IconUrl): Reference<PluginIcon> {
return this.icons.acquire({ url, type: 'file' });
}
private iconSequence = 0;
protected createPluginIcon(key: PluginIconKey): PluginIcon {
const iconUrl = key.url;
const size = key.size ?? DEFAULT_ICON_SIZE;
const type = key.type ?? 'icon';
const darkIconUrl = PluginSharedStyle.toExternalIconUrl(`${typeof iconUrl === 'object' ? iconUrl.dark : iconUrl}`);
const lightIconUrl = PluginSharedStyle.toExternalIconUrl(`${typeof iconUrl === 'object' ? iconUrl.light : iconUrl}`);
const toDispose = new DisposableCollection();
let iconClass = 'plugin-icon-' + this.iconSequence++;
if (type === 'icon') {
toDispose.push(this.insertRule('.' + iconClass + '::before', theme => `
content: "";
background-position: 2px;
display: block;
width: ${size}px;
height: ${size}px;
background: center no-repeat url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}");
background-size: ${size}px;
`));
} else {
toDispose.push(this.insertRule('.' + iconClass + '::before', theme => `
content: "";
background-image: url("${theme.type === 'light' ? lightIconUrl : darkIconUrl}");
background-size: ${DEFAULT_ICON_SIZE}px;
background-position: left center;
background-repeat: no-repeat;
`));
iconClass += ' ' + PLUGIN_FILE_ICON_CLASS;
}
return { iconClass, dispose: () => toDispose.dispose() };
}
static toExternalIconUrl(iconUrl: string): string {
if (iconUrl.startsWith('hostedPlugin/')) {
return new Endpoint({ path: iconUrl }).getRestUrl().toString();
}
return iconUrl;
}
}

View File

@@ -0,0 +1,55 @@
// *****************************************************************************
// 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 { StorageMain } from '../../common/plugin-api-rpc';
import { PluginServer, PluginStorageKind } from '../../common/plugin-protocol';
import { KeysToAnyValues, KeysToKeysToAnyValue } from '../../common/types';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
export class StorageMainImpl implements StorageMain {
private readonly pluginServer: PluginServer;
private readonly workspaceService: WorkspaceService;
constructor(container: interfaces.Container) {
this.pluginServer = container.get(PluginServer);
this.workspaceService = container.get(WorkspaceService);
}
$set(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise<boolean> {
return this.pluginServer.setStorageValue(key, value, this.toKind(isGlobal));
}
$get(key: string, isGlobal: boolean): Promise<KeysToAnyValues> {
return this.pluginServer.getStorageValue(key, this.toKind(isGlobal));
}
$getAll(isGlobal: boolean): Promise<KeysToKeysToAnyValue> {
return this.pluginServer.getAllStorageValues(this.toKind(isGlobal));
}
protected toKind(isGlobal: boolean): PluginStorageKind {
if (isGlobal) {
return undefined;
}
return {
workspace: this.workspaceService.workspace?.resource.toString(),
roots: this.workspaceService.tryGetRoots().map(root => root.resource.toString())
};
}
}

View File

@@ -0,0 +1,27 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
@injectable()
export class PluginTerminalRegistry {
startCallback: (id: string) => Promise<string>;
start(profileId: string): Promise<string> {
return this.startCallback(profileId);
}
}

View File

@@ -0,0 +1,126 @@
// *****************************************************************************
// 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 {
PreferenceService,
PreferenceServiceImpl,
PreferenceScope,
PreferenceProviderProvider
} from '@theia/core/lib/common/preferences';
import { interfaces } from '@theia/core/shared/inversify';
import {
MAIN_RPC_CONTEXT,
PreferenceRegistryExt,
PreferenceRegistryMain,
PreferenceData,
PreferenceChangeExt,
} from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { ConfigurationTarget } from '../../plugin/types-impl';
import { WorkspaceService } from '@theia/workspace/lib/browser';
import { FileStat } from '@theia/filesystem/lib/common/files';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
export function getPreferences(preferenceProviderProvider: PreferenceProviderProvider, rootFolders: FileStat[]): PreferenceData {
const folders = rootFolders.map(root => root.resource.toString());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return PreferenceScope.getScopes().reduce((result: { [key: number]: any }, scope: PreferenceScope) => {
result[scope] = {};
const provider = preferenceProviderProvider(scope);
if (provider) {
if (scope === PreferenceScope.Folder) {
for (const f of folders) {
const folderPrefs = provider.getPreferences(f);
result[scope][f] = folderPrefs;
}
} else {
result[scope] = provider.getPreferences();
}
}
return result;
}, {} as PreferenceData);
}
export class PreferenceRegistryMainImpl implements PreferenceRegistryMain, Disposable {
private readonly proxy: PreferenceRegistryExt;
private readonly preferenceService: PreferenceService;
protected readonly toDispose = new DisposableCollection();
constructor(prc: RPCProtocol, container: interfaces.Container) {
this.proxy = prc.getProxy(MAIN_RPC_CONTEXT.PREFERENCE_REGISTRY_EXT);
this.preferenceService = container.get(PreferenceService);
const preferenceProviderProvider = container.get<PreferenceProviderProvider>(PreferenceProviderProvider);
const preferenceServiceImpl = container.get(PreferenceServiceImpl);
const workspaceService = container.get(WorkspaceService);
this.toDispose.push(preferenceServiceImpl.onPreferencesChanged(changes => {
// it HAS to be synchronous to propagate changes before update/remove response
const roots = workspaceService.tryGetRoots();
const data = getPreferences(preferenceProviderProvider, roots);
const eventData = Object.values(changes).map<PreferenceChangeExt>(({ scope, domain, preferenceName }) => {
const extScope = scope === PreferenceScope.User ? undefined : domain?.[0];
const newValue = this.preferenceService.get(preferenceName);
return { preferenceName, newValue, scope: extScope };
});
this.proxy.$acceptConfigurationChanged(data, eventData);
}));
}
dispose(): void {
this.toDispose.dispose();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async $updateConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string, value: any, resource?: string, withLanguageOverride?: boolean): Promise<void> {
const scope = this.parseConfigurationTarget(target, resource);
const effectiveKey = this.getEffectiveKey(key, scope, withLanguageOverride, resource);
await this.preferenceService.set(effectiveKey, value, scope, resource);
}
async $removeConfigurationOption(target: boolean | ConfigurationTarget | undefined, key: string, resource?: string, withLanguageOverride?: boolean): Promise<void> {
const scope = this.parseConfigurationTarget(target, resource);
const effectiveKey = this.getEffectiveKey(key, scope, withLanguageOverride, resource);
await this.preferenceService.set(effectiveKey, undefined, scope, resource);
}
private parseConfigurationTarget(target?: boolean | ConfigurationTarget, resource?: string): PreferenceScope {
if (typeof target === 'boolean') {
return target ? PreferenceScope.User : PreferenceScope.Workspace;
}
switch (target) {
case ConfigurationTarget.Global:
return PreferenceScope.User;
case ConfigurationTarget.Workspace:
return PreferenceScope.Workspace;
case ConfigurationTarget.WorkspaceFolder:
return PreferenceScope.Folder;
default:
return resource ? PreferenceScope.Folder : PreferenceScope.Workspace;
}
}
// If the caller does not set `withLanguageOverride = true`, we have to check whether the setting exists with that override already.
protected getEffectiveKey(key: string, scope: PreferenceScope, withLanguageOverride?: boolean, resource?: string): string {
if (withLanguageOverride) { return key; }
const overridden = this.preferenceService.overriddenPreferenceName(key);
if (!overridden) { return key; }
const value = this.preferenceService.inspectInScope(key, scope, resource, withLanguageOverride);
return value === undefined ? overridden.preferenceName : key;
}
}

View File

@@ -0,0 +1,367 @@
// *****************************************************************************
// 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 { InputBoxOptions } from '@theia/plugin';
import { interfaces } from '@theia/core/shared/inversify';
import { RPCProtocol } from '../../common/rpc-protocol';
import {
QuickOpenExt,
QuickOpenMain,
MAIN_RPC_CONTEXT,
TransferInputBox,
TransferQuickPickItem,
TransferQuickInput,
TransferQuickInputButton,
TransferQuickPickOptions
} from '../../common/plugin-api-rpc';
import {
InputOptions,
QuickInput,
QuickInputButton,
QuickInputButtonHandle,
QuickInputService,
QuickPickItem,
QuickPickItemOrSeparator,
codiconArray
} from '@theia/core/lib/browser';
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { MonacoQuickInputService } from '@theia/monaco/lib/browser/monaco-quick-input-service';
import { QuickInputButtons } from '../../plugin/types-impl';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
import { PluginSharedStyle } from './plugin-shared-style';
import { QuickPickSeparator } from '@theia/core';
export interface QuickInputSession {
input: QuickInput;
handlesToItems: Map<number, QuickPickItemOrSeparator>;
}
export class QuickOpenMainImpl implements QuickOpenMain, Disposable {
private quickInputService: QuickInputService;
private proxy: QuickOpenExt;
private delegate: MonacoQuickInputService;
private sharedStyle: PluginSharedStyle;
private readonly items: Record<number, {
resolve(items: QuickPickItemOrSeparator[]): void;
reject(error: Error): void;
}> = {};
protected readonly toDispose = new DisposableCollection();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.QUICK_OPEN_EXT);
this.delegate = container.get(MonacoQuickInputService);
this.quickInputService = container.get(QuickInputService);
this.sharedStyle = container.get(PluginSharedStyle);
}
dispose(): void {
this.toDispose.dispose();
}
async $show(instance: number, options: TransferQuickPickOptions<TransferQuickPickItem>, token: CancellationToken): Promise<number | number[] | undefined> {
const contents = new Promise<QuickPickItemOrSeparator[]>((resolve, reject) => {
this.items[instance] = { resolve, reject };
});
const activeItem = await options.activeItem;
const transformedOptions = {
...options,
onDidFocus: (el: any) => {
if (el) {
this.proxy.$onItemSelected(Number.parseInt((<QuickPickItem>el).id!));
}
},
activeItem: this.isItem(activeItem) ? this.toQuickPickItem(activeItem) : undefined
};
const result = await this.delegate.pick(contents, transformedOptions, token);
if (Array.isArray(result)) {
return result.map(({ id }) => Number.parseInt(id!));
} else if (result) {
return Number.parseInt(result.id!);
}
return undefined;
}
private isItem(item?: TransferQuickPickItem): item is TransferQuickPickItem & { kind: 'item' } {
return item?.kind === 'item';
}
private toIconClasses(path: { light: string; dark: string } | ThemeIcon | string | undefined): string[] {
const iconClasses: string[] = [];
if (ThemeIcon.isThemeIcon(path)) {
const codicon = codiconArray(path.id);
iconClasses.push(...codicon);
} else if (path) {
const iconReference = this.sharedStyle.toIconClass(path);
this.toDispose.push(iconReference);
iconClasses.push(iconReference.object.iconClass);
}
return iconClasses;
}
private toIconClass(path: { light: string; dark: string } | ThemeIcon | string | undefined): string {
return this.toIconClasses(path).join(' ');
}
private toQuickPickItem(item: undefined): undefined;
private toQuickPickItem(item: TransferQuickPickItem & { kind: 'item' }): QuickPickItem;
private toQuickPickItem(item: TransferQuickPickItem & { kind: 'separator' }): QuickPickSeparator;
private toQuickPickItem(item: TransferQuickPickItem): QuickPickItemOrSeparator;
private toQuickPickItem(item: TransferQuickPickItem | undefined): QuickPickItemOrSeparator | undefined {
if (!item) {
return undefined;
} else if (item.kind === 'separator') {
return {
type: 'separator',
label: item.label
};
}
return {
type: 'item',
id: item.handle.toString(),
label: item.label,
description: item.description,
detail: item.detail,
alwaysShow: item.alwaysShow,
iconClasses: this.toIconClasses(item.iconUrl),
buttons: item.buttons ? this.convertToQuickInputButtons(item.buttons) : undefined
};
}
$setItems(instance: number, items: TransferQuickPickItem[]): Promise<any> {
if (this.items[instance]) {
this.items[instance].resolve(items.map(item => this.toQuickPickItem(item)));
delete this.items[instance];
}
return Promise.resolve();
}
$setError(instance: number, error: Error): Promise<void> {
if (this.items[instance]) {
this.items[instance].reject(error);
delete this.items[instance];
}
return Promise.resolve();
}
$input(options: InputBoxOptions, validateInput: boolean, token: CancellationToken): Promise<string | undefined> {
const inputOptions: InputOptions = Object.create(null);
if (options) {
inputOptions.title = options.title;
inputOptions.password = options.password;
inputOptions.placeHolder = options.placeHolder;
inputOptions.valueSelection = options.valueSelection;
inputOptions.prompt = options.prompt;
inputOptions.value = options.value;
inputOptions.ignoreFocusLost = options.ignoreFocusOut;
}
if (validateInput) {
inputOptions.validateInput = (val: string) => this.proxy.$validateInput(val);
}
return this.quickInputService?.input(inputOptions, token);
}
async $showInputBox(options: TransferInputBox, validateInput: boolean): Promise<string | undefined> {
return new Promise<string | undefined>((resolve, reject) => {
const sessionId = options.id;
const toDispose = new DisposableCollection();
const inputBox = this.quickInputService?.createInputBox();
inputBox.prompt = options.prompt;
inputBox.placeholder = options.placeHolder;
inputBox.value = options.value;
if (options.busy) {
inputBox.busy = options.busy;
}
if (options.enabled) {
inputBox.enabled = options.enabled;
}
inputBox.ignoreFocusOut = options.ignoreFocusOut;
inputBox.contextKey = options.contextKey;
if (options.password) {
inputBox.password = options.password;
}
inputBox.step = options.step;
inputBox.title = options.title;
inputBox.description = options.description;
inputBox.totalSteps = options.totalSteps;
inputBox.buttons = options.buttons ? this.convertToQuickInputButtons(options.buttons) : [];
inputBox.validationMessage = options.validationMessage;
if (validateInput) {
options.validateInput = (val: string) => {
this.proxy.$validateInput(val);
};
}
toDispose.push(inputBox.onDidAccept(() => {
this.proxy.$acceptOnDidAccept(sessionId);
resolve(inputBox.value);
}));
toDispose.push(inputBox.onDidChangeValue((value: string) => {
this.proxy.$acceptDidChangeValue(sessionId, value);
inputBox.validationMessage = options.validateInput(value);
}));
toDispose.push(inputBox.onDidTriggerButton((button: any) => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button);
}));
toDispose.push(inputBox.onDidHide(() => {
if (toDispose.disposed) {
return;
}
this.proxy.$acceptOnDidHide(sessionId);
toDispose.dispose();
resolve(undefined);
}));
this.toDispose.push(toDispose);
inputBox.show();
});
}
private sessions = new Map<number, QuickInputSession>();
$createOrUpdate(params: TransferQuickInput): Promise<void> {
const sessionId = params.id;
let session: QuickInputSession;
const candidate = this.sessions.get(sessionId);
if (!candidate) {
if (params.type === 'quickPick') {
const quickPick = this.quickInputService.createQuickPick();
quickPick.onDidAccept(() => {
this.proxy.$acceptOnDidAccept(sessionId);
});
quickPick.onDidChangeActive((items: QuickPickItem[]) => {
this.proxy.$onDidChangeActive(sessionId, items.map(item => Number.parseInt(item.id!)));
});
quickPick.onDidChangeSelection((items: QuickPickItem[]) => {
this.proxy.$onDidChangeSelection(sessionId, items.map(item => Number.parseInt(item.id!)));
});
quickPick.onDidTriggerButton((button: QuickInputButtonHandle) => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button);
});
quickPick.onDidTriggerItemButton(e => {
this.proxy.$onDidTriggerItemButton(sessionId, Number.parseInt(e.item.id!), (e.button as TransferQuickPickItem).handle);
});
quickPick.onDidChangeValue((value: string) => {
this.proxy.$acceptDidChangeValue(sessionId, value);
});
quickPick.onDidHide(() => {
this.proxy.$acceptOnDidHide(sessionId);
});
session = {
input: quickPick,
handlesToItems: new Map()
};
} else {
const inputBox = this.quickInputService.createInputBox();
inputBox.onDidAccept(() => {
this.proxy.$acceptOnDidAccept(sessionId);
});
inputBox.onDidTriggerButton((button: QuickInputButtonHandle) => {
this.proxy.$acceptOnDidTriggerButton(sessionId, button);
});
inputBox.onDidChangeValue((value: string) => {
this.proxy.$acceptDidChangeValue(sessionId, value);
});
inputBox.onDidHide(() => {
this.proxy.$acceptOnDidHide(sessionId);
});
session = {
input: inputBox,
handlesToItems: new Map()
};
}
this.sessions.set(sessionId, session);
} else {
session = candidate;
}
if (session) {
const { input, handlesToItems } = session;
for (const param in params) {
if (param === 'id' || param === 'type') {
continue;
}
if (param === 'visible') {
if (params.visible) {
input.show();
} else {
input.hide();
}
} else if (param === 'items') {
handlesToItems.clear();
const items: QuickPickItemOrSeparator[] = [];
params[param].forEach((transferItem: TransferQuickPickItem) => {
const item = this.toQuickPickItem(transferItem);
items.push(item);
handlesToItems.set(transferItem.handle, item);
});
(input as any)[param] = items;
} else if (param === 'activeItems' || param === 'selectedItems') {
(input as any)[param] = params[param]
.filter((handle: number) => handlesToItems.has(handle))
.map((handle: number) => handlesToItems.get(handle));
} else if (param === 'buttons') {
(input as any)[param] = params.buttons!.map(button => {
if (button.handle === -1) {
return this.quickInputService.backButton;
}
const { iconUrl, tooltip, handle } = button;
return {
tooltip,
handle,
iconClass: this.toIconClass(iconUrl)
};
});
} else {
(input as any)[param] = params[param];
}
}
}
return Promise.resolve(undefined);
}
$hide(): void {
this.delegate.hide();
}
$dispose(sessionId: number): Promise<void> {
const session = this.sessions.get(sessionId);
if (session) {
session.input.dispose();
this.sessions.delete(sessionId);
}
return Promise.resolve(undefined);
}
private convertToQuickInputButtons(buttons: readonly TransferQuickInputButton[]): QuickInputButton[] {
return buttons.map((button, i) => ({
iconClass: this.toIconClass(button.iconUrl),
tooltip: button.tooltip,
handle: button === QuickInputButtons.Back ? -1 : i,
} as QuickInputButton));
}
}

View File

@@ -0,0 +1,518 @@
// *****************************************************************************
// Copyright (C) 2019-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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* 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.52.1/src/vs/workbench/api/browser/mainThreadSCM.ts
import {
MAIN_RPC_CONTEXT,
ScmExt,
SourceControlGroupFeatures,
ScmMain,
SourceControlProviderFeatures,
ScmRawResourceSplices, ScmRawResourceGroup,
ScmActionButton as RpcScmActionButton
} from '../../common/plugin-api-rpc';
import { ScmProvider, ScmResource, ScmResourceDecorations, ScmResourceGroup, ScmCommand, ScmActionButton } from '@theia/scm/lib/browser/scm-provider';
import { ScmRepository } from '@theia/scm/lib/browser/scm-repository';
import { ScmService } from '@theia/scm/lib/browser/scm-service';
import { RPCProtocol } from '../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import URI from '@theia/core/lib/common/uri';
import { URI as vscodeURI } from '@theia/core/shared/vscode-uri';
import { Splice } from '../../common/arrays';
import { UriComponents } from '../../common/uri-components';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { PluginSharedStyle } from './plugin-shared-style';
import { IconUrl } from '../../common';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
export class PluginScmResourceGroup implements ScmResourceGroup {
readonly resources: ScmResource[] = [];
private readonly onDidSpliceEmitter = new Emitter<Splice<ScmResource>>();
readonly onDidSplice = this.onDidSpliceEmitter.event;
get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; }
get contextValue(): string | undefined { return this.features.contextValue; }
private readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
constructor(
readonly handle: number,
public provider: PluginScmProvider,
public features: SourceControlGroupFeatures,
public label: string,
public id: string
) { }
splice(start: number, deleteCount: number, toInsert: ScmResource[]): void {
this.resources.splice(start, deleteCount, ...toInsert);
this.onDidSpliceEmitter.fire({ start, deleteCount, toInsert });
}
updateGroup(features: SourceControlGroupFeatures): void {
this.features = { ...this.features, ...features };
this.onDidChangeEmitter.fire();
}
updateGroupLabel(label: string): void {
this.label = label;
this.onDidChangeEmitter.fire();
}
dispose(): void { }
}
export class PluginScmResource implements ScmResource {
constructor(
private readonly proxy: ScmExt,
private readonly sourceControlHandle: number,
private readonly groupHandle: number,
readonly handle: number,
readonly sourceUri: URI,
readonly group: PluginScmResourceGroup,
readonly decorations: ScmResourceDecorations,
readonly contextValue: string | undefined,
readonly command: ScmCommand | undefined
) { }
open(): Promise<void> {
return this.proxy.$executeResourceCommand(this.sourceControlHandle, this.groupHandle, this.handle);
}
}
export class PluginScmProvider implements ScmProvider {
private _id = this.contextValue;
get id(): string { return this._id; }
readonly groups: PluginScmResourceGroup[] = [];
private readonly groupsByHandle: { [handle: number]: PluginScmResourceGroup; } = Object.create(null);
private readonly onDidChangeResourcesEmitter = new Emitter<void>();
readonly onDidChangeResources: Event<void> = this.onDidChangeResourcesEmitter.event;
private _actionButton: ScmActionButton | undefined;
get actionButton(): ScmActionButton | undefined { return this._actionButton; }
private features: SourceControlProviderFeatures = {};
get handle(): number { return this._handle; }
get label(): string { return this._label; }
get rootUri(): string { return this._rootUri ? this._rootUri.toString() : ''; }
get contextValue(): string { return this._contextValue; }
get commitTemplate(): string { return this.features.commitTemplate || ''; }
get acceptInputCommand(): ScmCommand | undefined {
const command = this.features.acceptInputCommand;
if (command) {
const scmCommand: ScmCommand = command;
scmCommand.command = command.id;
return scmCommand;
}
}
get statusBarCommands(): ScmCommand[] | undefined {
const commands = this.features.statusBarCommands;
return commands?.map(command => {
const scmCommand: ScmCommand = command;
scmCommand.command = command.id;
return scmCommand;
});
}
get count(): number | undefined { return this.features.count; }
private readonly onDidChangeCommitTemplateEmitter = new Emitter<string>();
readonly onDidChangeCommitTemplate: Event<string> = this.onDidChangeCommitTemplateEmitter.event;
private readonly onDidChangeStatusBarCommandsEmitter = new Emitter<ScmCommand[]>();
get onDidChangeStatusBarCommands(): Event<ScmCommand[]> { return this.onDidChangeStatusBarCommandsEmitter.event; }
private readonly onDidChangeEmitter = new Emitter<void>();
readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
private readonly onDidChangeActionButtonEmitter = new Emitter<ScmActionButton | undefined>();
readonly onDidChangeActionButton: Event<ScmActionButton | undefined> = this.onDidChangeActionButtonEmitter.event;
constructor(
private readonly proxy: ScmExt,
private readonly colors: ColorRegistry,
private readonly sharedStyle: PluginSharedStyle,
private readonly _handle: number,
private readonly _contextValue: string,
private readonly _label: string,
private readonly _rootUri: vscodeURI | undefined,
private disposables: DisposableCollection
) { }
updateSourceControl(features: SourceControlProviderFeatures): void {
this.features = { ...this.features, ...features };
this.onDidChangeEmitter.fire();
if (typeof features.commitTemplate !== 'undefined') {
this.onDidChangeCommitTemplateEmitter.fire(this.commitTemplate!);
}
if (typeof features.statusBarCommands !== 'undefined') {
this.onDidChangeStatusBarCommandsEmitter.fire(this.statusBarCommands!);
}
}
registerGroups(resourceGroups: ScmRawResourceGroup[]): void {
const groups = resourceGroups.map(resourceGroup => {
const { handle, id, label, features } = resourceGroup;
const group = new PluginScmResourceGroup(
handle,
this,
features,
label,
id
);
this.groupsByHandle[handle] = group;
return group;
});
this.groups.splice(this.groups.length, 0, ...groups);
}
updateGroup(handle: number, features: SourceControlGroupFeatures): void {
const group = this.groupsByHandle[handle];
if (!group) {
return;
}
group.updateGroup(features);
}
updateGroupLabel(handle: number, label: string): void {
const group = this.groupsByHandle[handle];
if (!group) {
return;
}
group.updateGroupLabel(label);
}
spliceGroupResourceStates(splices: ScmRawResourceSplices[]): void {
for (const splice of splices) {
const groupHandle = splice.handle;
const groupSlices = splice.splices;
const group = this.groupsByHandle[groupHandle];
if (!group) {
console.warn(`SCM group ${groupHandle} not found in provider ${this.label}`);
continue;
}
// reverse the splices sequence in order to apply them correctly
groupSlices.reverse();
for (const groupSlice of groupSlices) {
const { start, deleteCount, rawResources } = groupSlice;
const resources = rawResources.map(rawResource => {
const { handle, sourceUri, icons, tooltip, strikeThrough, faded, contextValue, command } = rawResource;
const icon = this.toIconClass(icons[0]);
const iconDark = this.toIconClass(icons[1]) || icon;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const colorVariable = (rawResource as any).colorId && this.colors.toCssVariableName((rawResource as any).colorId);
const decorations = {
icon,
iconDark,
tooltip,
strikeThrough,
// TODO remove the letter and colorId fields when the FileDecorationProvider is applied, see https://github.com/eclipse-theia/theia/pull/8911
// eslint-disable-next-line @typescript-eslint/no-explicit-any
letter: (rawResource as any).letter || '',
color: colorVariable && `var(${colorVariable})`,
faded
} as ScmResourceDecorations;
return new PluginScmResource(
this.proxy,
this.handle,
groupHandle,
handle,
new URI(vscodeURI.revive(sourceUri)),
group,
decorations,
contextValue || undefined,
command
);
});
group.splice(start, deleteCount, resources);
}
}
this.onDidChangeResourcesEmitter.fire();
}
private toIconClass(icon: IconUrl | ThemeIcon | undefined): string | undefined {
if (!icon) {
return undefined;
}
if (ThemeIcon.isThemeIcon(icon)) {
return ThemeIcon.asClassName(icon);
}
const reference = this.sharedStyle.toIconClass(icon);
this.disposables.push(reference);
return reference.object.iconClass;
}
unregisterGroup(handle: number): void {
const group = this.groupsByHandle[handle];
if (!group) {
return;
}
delete this.groupsByHandle[handle];
this.groups.splice(this.groups.indexOf(group), 1);
}
updateActionButton(actionButton: ScmActionButton | undefined): void {
this._actionButton = actionButton;
this.onDidChangeActionButtonEmitter.fire(actionButton);
}
dispose(): void { }
}
export class ScmMainImpl implements ScmMain {
private readonly proxy: ScmExt;
private readonly scmService: ScmService;
private repositories = new Map<number, ScmRepository>();
private repositoryDisposables = new Map<number, DisposableCollection>();
private readonly disposables = new DisposableCollection();
private readonly colors: ColorRegistry;
private readonly sharedStyle: PluginSharedStyle;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.SCM_EXT);
this.scmService = container.get(ScmService);
this.colors = container.get(ColorRegistry);
this.sharedStyle = container.get(PluginSharedStyle);
}
dispose(): void {
this.repositories.forEach(r => r.dispose());
this.repositories.clear();
this.repositoryDisposables.forEach(d => d.dispose());
this.repositoryDisposables.clear();
this.disposables.dispose();
}
async $registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): Promise<void> {
const provider = new PluginScmProvider(this.proxy, this.colors, this.sharedStyle, handle, id, label, rootUri ? vscodeURI.revive(rootUri) : undefined, this.disposables);
const repository = this.scmService.registerScmProvider(provider, {
input: {
validator: async value => {
const result = await this.proxy.$validateInput(handle, value, value.length);
return result && { message: result[0], type: result[1] };
}
}
}
);
this.repositories.set(handle, repository);
const disposables = new DisposableCollection(
this.scmService.onDidChangeSelectedRepository(r => {
if (r === repository) {
this.proxy.$setSelectedSourceControl(handle);
}
}),
repository.input.onDidChange(() => this.proxy.$onInputBoxValueChange(handle, repository.input.value))
);
if (this.scmService.selectedRepository === repository) {
setTimeout(() => this.proxy.$setSelectedSourceControl(handle), 0);
}
if (repository.input.value) {
setTimeout(() => this.proxy.$onInputBoxValueChange(handle, repository.input.value), 0);
}
this.repositoryDisposables.set(handle, disposables);
}
async $updateSourceControl(handle: number, features: SourceControlProviderFeatures): Promise<void> {
const repository = this.repositories.get(handle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.updateSourceControl(features);
}
async $unregisterSourceControl(handle: number): Promise<void> {
const repository = this.repositories.get(handle);
if (!repository) {
return;
}
this.repositoryDisposables.get(handle)!.dispose();
this.repositoryDisposables.delete(handle);
repository.dispose();
this.repositories.delete(handle);
}
$registerGroups(sourceControlHandle: number, groups: ScmRawResourceGroup[], splices: ScmRawResourceSplices[]): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.registerGroups(groups);
provider.spliceGroupResourceStates(splices);
}
$updateGroup(sourceControlHandle: number, groupHandle: number, features: SourceControlGroupFeatures): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.updateGroup(groupHandle, features);
}
$updateGroupLabel(sourceControlHandle: number, groupHandle: number, label: string): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.updateGroupLabel(groupHandle, label);
}
$spliceResourceStates(sourceControlHandle: number, splices: ScmRawResourceSplices[]): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.spliceGroupResourceStates(splices);
}
$unregisterGroup(sourceControlHandle: number, handle: number): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
provider.unregisterGroup(handle);
}
$setInputBoxValue(sourceControlHandle: number, value: string): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
repository.input.value = value;
}
$setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
repository.input.placeholder = placeholder;
}
$setInputBoxVisible(sourceControlHandle: number, visible: boolean): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
repository.input.visible = visible;
}
$setInputBoxEnabled(sourceControlHandle: number, enabled: boolean): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
repository.input.enabled = enabled;
}
$setActionButton(sourceControlHandle: number, actionButton: RpcScmActionButton | undefined): void {
const repository = this.repositories.get(sourceControlHandle);
if (!repository) {
return;
}
const provider = repository.provider as PluginScmProvider;
// Convert from RPC Command (with .id) to ScmCommand (with .command)
const converted: ScmActionButton | undefined = actionButton ? {
command: {
title: actionButton.command.title,
tooltip: actionButton.command.tooltip,
command: actionButton.command.id,
arguments: actionButton.command.arguments
},
secondaryCommands: actionButton.secondaryCommands?.map(row =>
row.map(cmd => ({
title: cmd.title,
tooltip: cmd.tooltip,
command: cmd.id,
arguments: cmd.arguments
}))
),
enabled: actionButton.enabled,
description: actionButton.description
} : undefined;
provider.updateActionButton(converted);
}
}

View File

@@ -0,0 +1,107 @@
// *****************************************************************************
// Copyright (C) 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
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* 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.55.2/src/vs/workbench/api/browser/mainThreadSecretState.ts
import { SecretsExt, SecretsMain } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { MAIN_RPC_CONTEXT } from '../../common';
import { CredentialsService } from '@theia/core/lib/browser/credentials-service';
export class SecretsMainImpl implements SecretsMain {
private readonly proxy: SecretsExt;
private readonly credentialsService: CredentialsService;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.SECRETS_EXT);
this.credentialsService = container.get(CredentialsService);
this.credentialsService.onDidChangePassword(e => {
const extensionId = e.service.substring(window.location.hostname.length + 1);
this.proxy.$onDidChangePassword({ extensionId, key: e.account });
});
}
private static getFullKey(extensionId: string): string {
return `${window.location.hostname}-${extensionId}`;
}
async $getPassword(extensionId: string, key: string): Promise<string | undefined> {
const fullKey = SecretsMainImpl.getFullKey(extensionId);
const passwordData = await this.credentialsService.getPassword(fullKey, key);
if (passwordData) {
try {
const data = JSON.parse(passwordData);
if (data.extensionId === extensionId) {
return data.content;
}
} catch (e) {
throw new Error('Cannot get password');
}
}
return undefined;
}
async $setPassword(extensionId: string, key: string, value: string): Promise<void> {
const fullKey = SecretsMainImpl.getFullKey(extensionId);
const passwordData = JSON.stringify({
extensionId,
content: value
});
return this.credentialsService.setPassword(fullKey, key, passwordData);
}
async $deletePassword(extensionId: string, key: string): Promise<void> {
try {
const fullKey = SecretsMainImpl.getFullKey(extensionId);
await this.credentialsService.deletePassword(fullKey, key);
} catch (e) {
throw new Error('Cannot delete password');
}
}
async $getKeys(extensionId: string): Promise<string[]> {
return this.doGetKeys(extensionId);
}
private async doGetKeys(extensionId: string): Promise<string[]> {
if (!this.credentialsService.keys) {
throw new Error('CredentialsProvider does not support keys() method');
}
const fullKey = SecretsMainImpl.getFullKey(extensionId);
const allKeys = await this.credentialsService.keys(fullKey);
const keys = allKeys
.map(key => this.parseKey(key))
.filter((parsedKey): parsedKey is { extensionId: string; key: string } => parsedKey !== undefined && parsedKey.extensionId === extensionId)
.map(({ key }) => key);
return keys;
}
private parseKey(key: string): { extensionId: string; key: string } | undefined {
try {
return JSON.parse(key);
} catch {
return undefined;
}
}
}

View File

@@ -0,0 +1,45 @@
// *****************************************************************************
// 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 { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
import { inject, injectable } from '@theia/core/shared/inversify';
import { UriAwareCommandHandler, UriCommandHandler } from '@theia/core/lib/common/uri-command-handler';
import URI from '@theia/core/lib/common/uri';
import { SelectionService } from '@theia/core';
export namespace SelectionProviderCommands {
export const GET_SELECTED_CONTEXT: Command = {
id: 'theia.plugin.workspace.selectedContext'
};
}
@injectable()
export class SelectionProviderCommandContribution implements CommandContribution {
@inject(SelectionService) protected readonly selectionService: SelectionService;
registerCommands(commands: CommandRegistry): void {
commands.registerCommand(SelectionProviderCommands.GET_SELECTED_CONTEXT, this.newMultiUriAwareCommandHandler({
isEnabled: () => true,
isVisible: () => false,
execute: (selectedUris: URI[]) => selectedUris.map(uri => uri.toComponents())
}));
}
protected newMultiUriAwareCommandHandler(handler: UriCommandHandler<URI[]>): UriAwareCommandHandler<URI[]> {
return UriAwareCommandHandler.MultiSelect(this.selectionService, handler);
}
}

View File

@@ -0,0 +1,91 @@
// *****************************************************************************
// 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 { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import * as types from '../../plugin/types-impl';
import { StatusBarMessageRegistryMain, StatusBarMessageRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { StatusBar, StatusBarAlignment, StatusBarEntry } from '@theia/core/lib/browser/status-bar/status-bar';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { RPCProtocol } from '../../common/rpc-protocol';
import { CancellationToken } from '@theia/core';
export class StatusBarMessageRegistryMainImpl implements StatusBarMessageRegistryMain, Disposable {
private readonly delegate: StatusBar;
private readonly entries = new Map<string, StatusBarEntry>();
private readonly proxy: StatusBarMessageRegistryExt;
private readonly toDispose = new DisposableCollection(
Disposable.create(() => { /* mark as not disposed */ })
);
protected readonly colorRegistry: ColorRegistry;
constructor(container: interfaces.Container, rpc: RPCProtocol) {
this.delegate = container.get(StatusBar);
this.colorRegistry = container.get(ColorRegistry);
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.STATUS_BAR_MESSAGE_REGISTRY_EXT);
}
dispose(): void {
this.toDispose.dispose();
}
async $setMessage(id: string,
name: string | undefined,
text: string | undefined,
priority: number,
alignment: number,
color: string | undefined,
backgroundColor: string | undefined,
tooltip: string | MarkdownString | true | undefined,
command: string | undefined,
accessibilityInformation: types.AccessibilityInformation,
args: unknown[] | undefined): Promise<void> {
const entry: StatusBarEntry = {
name,
text: text || '',
priority,
alignment: alignment === types.StatusBarAlignment.Left ? StatusBarAlignment.LEFT : StatusBarAlignment.RIGHT,
color: color && (this.colorRegistry.getCurrentColor(color) || color),
// In contrast to color, the backgroundColor must be a theme color. Thus, do not hand in the plain string if it cannot be resolved.
backgroundColor: backgroundColor && (this.colorRegistry.getCurrentColor(backgroundColor)),
// true is used as a serializable sentinel value to indicate that the tooltip can be retrieved asynchronously
tooltip: tooltip === true ? (token: CancellationToken) => this.proxy.$getMessage(id, token) : tooltip,
command,
accessibilityInformation,
arguments: args
};
const isNewEntry = !this.entries.has(id);
this.entries.set(id, entry);
await this.delegate.setElement(id, entry);
if (this.toDispose.disposed) {
this.$dispose(id);
} else if (isNewEntry) {
this.toDispose.push(Disposable.create(() => this.$dispose(id)));
}
}
$dispose(id: string): void {
const entry = this.entries.get(id);
if (entry) {
this.entries.delete(id);
this.delegate.removeElement(id);
}
}
}

View File

@@ -0,0 +1,353 @@
/*---------------------------------------------------------------------------------------------
* 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/media/review.css */
.comment-range-glyph {
margin-left: 5px;
cursor: pointer;
}
.comment-range-glyph:before {
position: absolute;
content: '';
height: 100%;
width: 0;
left: -2px;
transition: width 80ms linear, left 80ms linear;
}
.monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before,
.monaco-editor .comment-range-glyph.comment-thread:before {
position: absolute;
height: 100%;
width: 9px;
left: -6px;
z-index: 10;
color: black;
text-align: center;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.monaco-editor .comment-diff-added {
border-left: 3px solid var(--theia-editorGutter-commentRangeForeground);
transition: opacity 0.5s;
}
.monaco-editor .comment-diff-added:before {
background: var(--theia-editorGutter-commentRangeForeground);
}
.monaco-editor .margin-view-overlays > div:hover > .comment-range-glyph.comment-diff-added:before {
content: "+";
}
.monaco-editor .comment-range-glyph.comment-thread {
z-index: 20;
border-left: 3px solid #000;
}
.monaco-editor .comment-thread:before {
background: var(--theia-editorGutter-commentRangeForeground);
}
.monaco-editor .comment-range-glyph.comment-thread:before {
content: "◆";
font-size: 10px;
line-height: 100%;
z-index: 20;
}
.monaco-editor .review-widget .body {
overflow: hidden;
}
.monaco-editor .review-widget .head {
box-sizing: border-box;
display: flex;
height: 100%;
}
.monaco-editor .review-widget .head .review-title {
display: inline-block;
font-size: 13px;
margin-left: 20px;
cursor: default;
}
.monaco-editor .review-widget .head .review-actions {
flex: 1;
text-align: right;
padding-right: 2px;
}
.monaco-editor .review-widget .head .review-actions > .monaco-action-bar {
display: inline-block;
}
.monaco-editor .review-widget .head .review-actions > .monaco-action-bar,
.monaco-editor .review-widget .head .review-actions > .monaco-action-bar > .actions-container {
height: 100%;
}
.monaco-editor .review-widget .action-item {
min-width: 18px;
min-height: 20px;
margin-left: 4px;
}
.monaco-editor .review-widget .head .review-actions > .monaco-action-bar .action-label {
width: 16px;
height: 100%;
margin: 0;
color: var(--theia-editorWidget-foreground);
line-height: inherit;
background-repeat: no-repeat;
background-position: center center;
}
.monaco-editor .review-widget>.body {
border-top: 1px solid;
position: relative;
}
.monaco-editor .review-widget .body .comment-form {
margin: 8px 20px;
}
.monaco-editor .review-widget .body .review-comment {
padding: 8px 16px 8px 20px;
display: flex;
}
.monaco-editor .review-widget .body .review-comment .avatar-container {
margin-right: 8px !important;
margin-top: 4px !important;
}
.monaco-editor .review-widget .body .review-comment .avatar-container img.avatar {
height: 28px;
width: 28px;
display: inline-block;
overflow: hidden;
line-height: 1;
vertical-align: middle;
border-radius: 3px;
border-style: none;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents {
user-select: text;
-webkit-user-select: text;
width: 100%;
overflow: hidden;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .author {
line-height: var(--theia-content-line-height);
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .timestamp {
line-height: var(--theia-content-line-height);
margin: 0 5px 0 5px;
padding: 0 2px 0 2px;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .isPending {
margin: 0 5px 0 5px;
padding: 0 2px 0 2px;
font-style: italic;
}
.monaco-editor .review-widget .body .review-comment .review-comment-contents .comment-body {
padding-top: 4px;
}
.monaco-editor .review-widget .body .review-comment .comment-title {
display: flex;
width: 100%;
}
.monaco-editor .review-widget .body .comment-form .theia-comments-input-message-container {
display: none;
}
.monaco-editor .review-widget .body .comment-form.expand .theia-comments-input-message-container,
.edit-textarea .theia-comments-input-message-container {
display: flex;
flex-direction: column;
margin: 0px 0px 7px 0px;
max-height: 400px;
}
.monaco-editor .review-widget .body .comment-form.expand .theia-comments-input-message-container textarea,
.edit-textarea .theia-comments-input-message-container textarea {
line-height: var(--theia-content-line-height);
background: var(--theia-editor-background);
resize: none;
height: 90px;
box-sizing: border-box;
min-height: 32px;
padding: 4px;
border: none;
}
.monaco-editor .review-widget .body .comment-form.expand .theia-comments-input-message-container textarea:placeholder-shown,
.edit-textarea .theia-comments-input-message-container textarea:placeholder-shown {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.monaco-editor .review-widget .body .comment-form.expand .theia-comments-input-message-container textarea:not(:focus),
.edit-textarea .theia-comments-input-message-container textarea:not(:focus) {
border: var(--theia-border-width) solid var(--theia-editor-background);
}
.monaco-editor .review-widget .body .comment-form.expand .theia-comments-input-message-container textarea:focus,
.edit-textarea .theia-comments-input-message-container textarea:focus {
border: var(--theia-border-width) solid var(--theia-focusBorder);
}
.theia-comments-input-message {
width: 100%;
}
.monaco-editor .review-widget .body .comment-body p,
.monaco-editor .review-widget .body .comment-body ul {
margin: 8px 0;
}
.monaco-editor .review-widget .body .comment-body p:first-child,
.monaco-editor .review-widget .body .comment-body ul:first-child {
margin-top: 0;
}
.monaco-editor .review-widget .body .comment-body p:last-child,
.monaco-editor .review-widget .body.comment-body ul:last-child {
margin-bottom: 0;
}
.monaco-editor .review-widget .body .comment-body ul {
padding-left: 20px;
}
.monaco-editor .review-widget .body .comment-body li > p {
margin-bottom: 0;
}
.monaco-editor .review-widget .body .comment-body li > ul {
margin-top: 0;
}
.monaco-editor .review-widget .body .comment-body code {
border-radius: 3px;
padding: 0 0.4em;
}
.monaco-editor .review-widget .body .comment-body span {
white-space: pre;
}
.monaco-editor .review-widget .body .comment-body img {
max-width: 100%;
}
.monaco-editor .review-widget .body .comment-form {
flex: 1;
margin: 0px;
/* Reset margin as it's handled by container */
}
.monaco-editor .review-widget .body .comment-form .form-actions {
display: none;
}
.monaco-editor .review-widget .body .comment-form.expand .form-actions {
display: block;
box-sizing: content-box;
}
.monaco-editor .review-widget .body .comment-form.expand .review-thread-reply-button {
display: none;
}
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button {
text-align: left;
display: block;
width: 100%;
resize: vertical;
background: var(--theia-editor-background);
color: var(--theia-input-foreground);
cursor: text;
font-size: var(--theia-ui-font-size1);
border-radius: 0;
box-sizing: border-box;
padding: 6px 12px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
border: 0px;
outline: 1px solid transparent;
}
.monaco-editor .review-widget .body .comment-form .review-thread-reply-button:focus {
outline-style: solid;
outline-width: 1px;
}
.monaco-editor .review-widget .body .comment-form.expand .form-actions,
.monaco-editor .review-widget .body .edit-container .form-actions {
overflow: auto;
padding: 10px 0;
}
.monaco-editor .review-widget .body .edit-container .form-actions {
display: flex;
justify-content: flex-end;
}
.monaco-editor .review-widget .body .edit-textarea {
margin: 5px 0 10px 0;
}
.monaco-editor .review-widget .body .comment-form.expand .comments-text-button,
.monaco-editor .review-widget .body .edit-container .comments-text-button {
width: auto;
padding: 4px 10px;
margin-left: 5px;
margin-bottom: 5px;
}
.monaco-editor .review-widget .body .comment-form.expand .comments-text-button {
float: right;
}
.theia-comments-inline-actions-container {
display: flex;
justify-content: flex-end;
margin-left: auto;
min-height: 16px;
}
.theia-comments-inline-actions {
display: flex;
margin: 0 3px;
}
.theia-comments-inline-actions a {
color: var(--theia-icon-foreground);
}
.theia-comments-inline-action {
padding: 0px 3px;
font-size: var(--theia-ui-font-size1);
margin: 0 2px;
cursor: pointer;
display: flex;
align-items: center;
}

View File

@@ -0,0 +1,84 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.spinnerContainer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.flexcontainer {
display: flex;
}
.row {
width: 100%;
}
.column {
flex-direction: column;
}
.theia-plugin-view-container {
/*
It might take a second or two until the real plugin mask is loaded
To prevent flickering on the icon, we set a transparent mask instead
Since masks only support images, svg or gradients, we create a transparent gradient here
*/
-webkit-mask: linear-gradient(transparent, transparent);
mask: linear-gradient(transparent, transparent);
background-color: var(--theia-activityBar-inactiveForeground);
}
.theia-plugin-file-icon,
.theia-plugin-file-icon::before,
.theia-plugin-folder-icon,
.theia-plugin-folder-icon::before,
.theia-plugin-folder-expanded-icon,
.theia-plugin-folder-expanded-icon::before,
.theia-plugin-root-folder-icon,
.theia-plugin-root-folder-icon::before,
.theia-plugin-root-folder-expanded-icon,
.theia-plugin-root-folder-expanded-icon::before {
padding-right: var(--theia-ui-padding);
width: var(--theia-icon-size);
height: var(--theia-content-line-height);
line-height: inherit !important;
display: inline-block;
}
.lm-TabBar.theia-app-sides .theia-plugin-file-icon,
.lm-TabBar.theia-app-sides .theia-plugin-file-icon::before,
.lm-TabBar.theia-app-sides .theia-plugin-folder-icon,
.lm-TabBar.theia-app-sides .theia-plugin-folder-icon::before,
.lm-TabBar.theia-app-sides .theia-plugin-folder-expanded-icon,
.lm-TabBar.theia-app-sides .theia-plugin-folder-expanded-icon::before,
.lm-TabBar.theia-app-sides .theia-plugin-root-folder-icon,
.lm-TabBar.theia-app-sides .theia-plugin-root-folder-icon::before,
.lm-TabBar.theia-app-sides .theia-plugin-root-folder-expanded-icon,
.lm-TabBar.theia-app-sides .theia-plugin-root-folder-expanded-icon::before {
padding: 0px !important;
width: var(--theia-private-sidebar-icon-size) !important;
height: var(--theia-private-sidebar-icon-size) !important;
background-size: var(--theia-private-sidebar-icon-size) !important;
font-size: var(--theia-private-sidebar-icon-size) !important;
}
@import "./plugin-sidebar.css";
@import "./webview.css";
@import "./tree.css";

View File

@@ -0,0 +1,73 @@
/********************************************************************************
* 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
********************************************************************************/
.theia-plugins {
min-width: 250px !important;
display: flex;
flex-direction: column;
}
#pluginListContainer {
width: 100%;
box-sizing: border-box;
overflow-y: auto;
flex-grow: 1;
}
.theia-plugins .pluginHeaderContainer {
padding: 5px 15px;
font-size: var(--theia-ui-font-size0);
}
.theia-side-panel .theia-plugins .pluginHeaderContainer {
padding-left: 20px;
}
.theia-plugins .pluginHeaderContainer:hover {
background: var(--theia-list-hoverBackground);
}
.theia-plugins .pluginHeaderContainer .row {
margin: 3px 0;
}
.theia-plugins .pluginName {
flex: 1;
margin-right: 5px;
margin-left: 4px;
font-size: var(--theia-ui-font-size1);
font-weight: 400;
}
.theia-plugins .pluginVersion {
flex: 1;
text-align: left;
font-size: var(--theia-ui-font-size0);
}
.theia-plugins .pluginDescription {
flex: 1;
}
.theia-plugins .pluginPublisher {
font-size: var(--theia-ui-font-size0);
flex: 5;
align-items: center;
}
.plugins-tab-icon::before {
content: "\f0fe";
}

View File

@@ -0,0 +1,54 @@
/********************************************************************************
* Copyright (C) 2018 Red Hat, Inc. and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
.theia-tree-view-icon {
padding-right: var(--theia-ui-padding);
-webkit-font-smoothing: antialiased;
flex-shrink: 0;
padding-left: 6px;
margin-left: -6px;
}
.theia-tree-view-inline-action {
padding: 2px;
}
.theia-tree-view-description {
color: var(--theia-descriptionForeground);
font-size: var(--theia-ui-font-size0);
margin-left: var(--theia-ui-padding);
}
.theia-tree-view .theia-TreeNodeContent {
align-items: center;
height: 100%;
}
.theia-tree-view .theia-TreeContainer .theia-TreeViewInfo {
margin-top: 7px;
margin-bottom: 10px;
margin-left: 17px;
}
.theia-tree-view
.theia-TreeNode:not(:hover):not(.theia-mod-selected)
.theia-tree-view-inline-action {
display: none;
}
.codicon.icon-inline {
font-size: var(--theia-ui-font-size1);
}

View File

@@ -0,0 +1,55 @@
/********************************************************************************
* 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
********************************************************************************/
.theia-webview.lm-mod-hidden {
visibility: hidden;
display: flex !important;
}
.theia-webview {
display: flex;
flex-direction: column;
height: 100%;
}
.theia-webview iframe {
flex-grow: 1;
border: none;
margin: 0;
padding: 0;
}
.theia-webview-icon {
background: none !important;
min-height: 20px;
}
.theia-webview-icon::before {
background-size: 13px;
background-repeat: no-repeat;
vertical-align: middle;
display: inline-block;
text-align: center;
height: 15px;
width: 15px;
content: "";
}
.lm-TabBar.theia-app-sides .theia-webview-icon::before {
width: var(--theia-private-sidebar-icon-size);
height: var(--theia-private-sidebar-icon-size);
background-size: contain;
}

View File

@@ -0,0 +1,413 @@
// *****************************************************************************
// 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 { interfaces } from '@theia/core/shared/inversify';
import { ApplicationShell, PINNED_CLASS, Saveable, TabBar, Title, ViewContainer, Widget } from '@theia/core/lib/browser';
import { AnyInputDto, MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabInputKind, TabModelOperationKind, TabsExt, TabsMain } from '../../../common/plugin-api-rpc';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser/editor-preview-widget';
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
import { toUriComponents } from '../hierarchy/hierarchy-types-converters';
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { DisposableCollection } from '@theia/core';
import { NotebookEditorWidget } from '@theia/notebook/lib/browser';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { MergeEditor } from '@theia/scm/lib/browser/merge-editor/merge-editor';
interface TabInfo {
tab: TabDto;
tabIndex: number;
group: TabGroupDto;
}
export class TabsMainImpl implements TabsMain, Disposable {
private readonly proxy: TabsExt;
private tabGroupModel = new Map<TabBar<Widget>, TabGroupDto>();
private tabInfoLookup = new Map<Title<Widget>, TabInfo>();
private waitQueue = new Map<Widget, Deferred>();
private applicationShell: ApplicationShell;
private disposableTabBarListeners: DisposableCollection = new DisposableCollection();
private disposableTitleListeners: Map<string, DisposableCollection> = new Map();
private toDisposeOnDestroy: DisposableCollection = new DisposableCollection();
private groupIdCounter = 1;
private currentActiveGroup: TabGroupDto;
private tabGroupChanged: boolean = false;
private readonly defaultTabGroup: TabGroupDto = {
groupId: 0,
tabs: [],
isActive: true,
viewColumn: 0
};
constructor(
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TABS_EXT);
this.applicationShell = container.get(ApplicationShell);
this.createTabsModel();
const tabBars = this.applicationShell.mainPanel.tabBars();
for (const tabBar of tabBars) {
this.attachListenersToTabBar(tabBar);
}
this.toDisposeOnDestroy.push(
this.applicationShell.mainPanelRenderer.onDidCreateTabBar(tabBar => {
this.attachListenersToTabBar(tabBar);
this.onTabGroupCreated(tabBar);
})
);
this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetAdded, (mainPanel, widget) => {
if (this.tabGroupChanged || this.tabGroupModel.size === 0) {
this.tabGroupChanged = false;
this.createTabsModel();
// tab Open event is done in backend
} else {
const tabBar = mainPanel.findTabBar(widget.title)!;
const oldTabInfo = this.tabInfoLookup.get(widget.title);
const group = this.tabGroupModel.get(tabBar);
if (group !== oldTabInfo?.group) {
if (oldTabInfo) {
this.onTabClosed(oldTabInfo, widget.title);
}
this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title });
}
}
});
this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (mainPanel, widget) => {
if (!(widget instanceof TabBar)) {
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, widget.title)!;
this.onTabClosed(tabInfo, widget.title);
if (this.tabGroupChanged) {
this.tabGroupChanged = false;
this.createTabsModel();
}
}
});
}
waitForWidget(widget: Widget): Promise<void> {
const deferred = new Deferred<void>();
this.waitQueue.set(widget, deferred);
const timeout = setTimeout(() => {
deferred.resolve(); // resolve to unblock the event
}, 1000);
deferred.promise.then(() => {
clearTimeout(timeout);
});
return deferred.promise;
}
protected createTabsModel(): void {
if (this.applicationShell.mainAreaTabBars.length === 0) {
this.proxy.$acceptEditorTabModel([this.defaultTabGroup]);
return;
}
const newTabGroupModel = new Map<TabBar<Widget>, TabGroupDto>();
this.tabInfoLookup.clear();
this.disposableTitleListeners.forEach(disposable => disposable.dispose());
this.disposableTabBarListeners.dispose();
this.applicationShell.mainAreaTabBars
.forEach(tabBar => {
this.attachListenersToTabBar(tabBar);
const groupDto = this.createTabGroupDto(tabBar);
tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index }));
newTabGroupModel.set(tabBar, groupDto);
});
if (newTabGroupModel.size > 0 && Array.from(newTabGroupModel.values()).indexOf(this.currentActiveGroup) < 0) {
this.currentActiveGroup = this.tabInfoLookup.get(this.applicationShell.mainPanel.currentTitle!)?.group ?? newTabGroupModel.values().next().value;
this.currentActiveGroup.isActive = true;
}
this.tabGroupModel = newTabGroupModel;
this.proxy.$acceptEditorTabModel(Array.from(this.tabGroupModel.values()));
// Resolve all waiting widget promises
this.waitQueue.forEach(deferred => deferred.resolve());
this.waitQueue.clear();
}
protected createTabDto(tabTitle: Title<Widget>, groupId: number, newTab = false): TabDto {
const widget = tabTitle.owner;
const active = newTab || this.getTabBar(tabTitle)?.currentTitle === tabTitle;
return {
id: this.createTabId(tabTitle, groupId),
label: tabTitle.label,
input: this.evaluateTabDtoInput(widget),
isActive: active,
isPinned: tabTitle.className.includes(PINNED_CLASS),
isDirty: Saveable.isDirty(widget),
isPreview: widget instanceof EditorPreviewWidget && widget.isPreview
};
}
protected getTabBar(tabTitle: Title<Widget>): TabBar<Widget> | undefined {
return this.applicationShell.mainPanel.findTabBar(tabTitle);
}
protected createTabId(tabTitle: Title<Widget>, groupId: number): string {
return `${groupId}~${tabTitle.owner.id}`;
}
protected createTabGroupDto(tabBar: TabBar<Widget>): TabGroupDto {
const oldDto = this.tabGroupModel.get(tabBar);
const groupId = oldDto?.groupId ?? this.groupIdCounter++;
const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId));
const viewColumn = 0; // TODO: Implement correct viewColumn handling
return {
groupId,
tabs,
isActive: false,
viewColumn
};
}
protected getTitleDisposables(title: Title<Widget>): DisposableCollection {
let disposable = this.disposableTitleListeners.get(title.owner.id);
if (!disposable) {
disposable = new DisposableCollection();
this.disposableTitleListeners.set(title.owner.id, disposable);
}
return disposable;
}
protected attachListenersToTabBar(tabBar: TabBar<Widget> | undefined): void {
if (!tabBar) {
return;
}
tabBar.titles.forEach(title => {
this.connectToSignal(this.getTitleDisposables(title), title.changed, this.onTabTitleChanged);
});
this.connectToSignal(this.disposableTabBarListeners, tabBar.tabMoved, this.onTabMoved);
this.connectToSignal(this.disposableTabBarListeners, tabBar.disposed, this.onTabGroupClosed);
}
protected evaluateTabDtoInput(widget: Widget): AnyInputDto {
if (widget instanceof EditorPreviewWidget) {
if (widget.editor instanceof MonacoDiffEditor) {
return {
kind: TabInputKind.TextDiffInput,
original: toUriComponents(widget.editor.originalModel.uri),
modified: toUriComponents(widget.editor.modifiedModel.uri)
};
} else {
return {
kind: TabInputKind.TextInput,
uri: toUriComponents(widget.editor.uri.toString())
};
}
} else if (widget instanceof ViewContainer) {
return {
kind: TabInputKind.WebviewEditorInput,
viewType: widget.id
};
} else if (widget instanceof TerminalWidget) {
return {
kind: TabInputKind.TerminalEditorInput
};
} else if (widget instanceof NotebookEditorWidget) {
return {
kind: TabInputKind.NotebookInput,
notebookType: widget.notebookType,
uri: toUriComponents(widget.model?.uri.toString() ?? '')
};
} else if (widget instanceof MergeEditor) {
return {
kind: TabInputKind.TextMergeInput,
base: toUriComponents(widget.baseUri.toString()),
input1: toUriComponents(widget.side1Uri.toString()),
input2: toUriComponents(widget.side1Uri.toString()),
result: toUriComponents(widget.resultUri.toString())
};
}
return { kind: TabInputKind.UnknownInput };
}
protected connectToSignal<T>(disposableList: DisposableCollection,
signal: {
connect(listener: T, context: unknown): void,
disconnect(listener: T, context: unknown): void
}, listener: T): void {
signal.connect(listener, this);
disposableList.push(Disposable.create(() => signal.disconnect(listener, this)));
}
protected tabDtosEqual(a: TabDto, b: TabDto): boolean {
return a.isActive === b.isActive &&
a.isDirty === b.isDirty &&
a.isPinned === b.isPinned &&
a.isPreview === b.isPreview &&
a.id === b.id;
}
protected getOrRebuildModel<T, R>(map: Map<T, R>, key: T): R {
// something broke so we rebuild the model
let item = map.get(key);
if (!item) {
this.createTabsModel();
item = map.get(key)!;
}
return item;
}
// #region event listeners
private onTabCreated(tabBar: TabBar<Widget>, args: TabBar.ITabActivateRequestedArgs<Widget>): void {
const group = this.getOrRebuildModel(this.tabGroupModel, tabBar);
this.connectToSignal(this.getTitleDisposables(args.title), args.title.changed, this.onTabTitleChanged);
const tabDto = this.createTabDto(args.title, group.groupId, true);
this.tabInfoLookup.set(args.title, { group, tab: tabDto, tabIndex: args.index });
group.tabs.forEach(tab => tab.isActive = false);
group.tabs.splice(args.index, 0, tabDto);
this.proxy.$acceptTabOperation({
kind: TabModelOperationKind.TAB_OPEN,
index: args.index,
tabDto,
groupId: group.groupId
});
this.waitQueue.get(args.title.owner)?.resolve();
this.waitQueue.delete(args.title.owner);
}
private onTabTitleChanged(title: Title<Widget>): void {
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, title);
if (!tabInfo) {
return;
}
const oldTabDto = tabInfo.tab;
const newTabDto = this.createTabDto(title, tabInfo.group.groupId);
if (!oldTabDto.isActive && newTabDto.isActive) {
this.currentActiveGroup.tabs.filter(tab => tab.isActive).forEach(tab => tab.isActive = false);
}
if (newTabDto.isActive && !tabInfo.group.isActive) {
tabInfo.group.isActive = true;
this.currentActiveGroup.isActive = false;
this.currentActiveGroup = tabInfo.group;
this.proxy.$acceptTabGroupUpdate(tabInfo.group);
}
if (!this.tabDtosEqual(oldTabDto, newTabDto)) {
tabInfo.group.tabs[tabInfo.tabIndex] = newTabDto;
tabInfo.tab = newTabDto;
this.proxy.$acceptTabOperation({
kind: TabModelOperationKind.TAB_UPDATE,
index: tabInfo.tabIndex,
tabDto: newTabDto,
groupId: tabInfo.group.groupId
});
}
this.waitQueue.get(title.owner)?.resolve();
this.waitQueue.delete(title.owner);
}
private onTabClosed(tabInfo: TabInfo, title: Title<Widget>): void {
this.disposableTitleListeners.get(title.owner.id)?.dispose();
this.disposableTitleListeners.delete(title.owner.id);
tabInfo.group.tabs.splice(tabInfo.tabIndex, 1);
this.tabInfoLookup.delete(title);
this.updateTabIndices(tabInfo, tabInfo.tabIndex);
this.proxy.$acceptTabOperation({
kind: TabModelOperationKind.TAB_CLOSE,
index: tabInfo.tabIndex,
tabDto: this.createTabDto(title, tabInfo.group.groupId),
groupId: tabInfo.group.groupId
});
}
private onTabMoved(tabBar: TabBar<Widget>, args: TabBar.ITabMovedArgs<Widget>): void {
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, args.title)!;
tabInfo.tabIndex = args.toIndex;
const tabDto = this.createTabDto(args.title, tabInfo.group.groupId);
tabInfo.group.tabs.splice(args.fromIndex, 1);
tabInfo.group.tabs.splice(args.toIndex, 0, tabDto);
this.updateTabIndices(tabInfo, args.fromIndex);
this.proxy.$acceptTabOperation({
kind: TabModelOperationKind.TAB_MOVE,
index: args.toIndex,
tabDto,
groupId: tabInfo.group.groupId,
oldIndex: args.fromIndex
});
}
private onTabGroupCreated(tabBar: TabBar<Widget>): void {
this.tabGroupChanged = true;
}
private onTabGroupClosed(tabBar: TabBar<Widget>): void {
this.tabGroupChanged = true;
}
// #endregion
// #region Messages received from Ext Host
$moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean): void {
return;
}
updateTabIndices(tabInfo: TabInfo, startIndex: number): void {
for (const tab of this.tabInfoLookup.values()) {
if (tab.group === tabInfo.group && tab.tabIndex >= startIndex) {
tab.tabIndex = tab.group.tabs.indexOf(tab.tab);
}
}
}
async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean> {
const widgets: Widget[] = [];
for (const tabId of tabIds) {
const cleanedId = tabId.substring(tabId.indexOf('~') + 1);
const widget = this.applicationShell.getWidgetById(cleanedId);
if (widget) {
widgets.push(widget);
}
}
await this.applicationShell.closeMany(widgets);
return true;
}
async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> {
for (const groupId of groupIds) {
tabGroupModel: for (const [bar, groupDto] of this.tabGroupModel) {
if (groupDto.groupId === groupId) {
this.applicationShell.closeTabs(bar);
break tabGroupModel;
}
}
}
return true;
}
// #endregion
dispose(): void {
this.toDisposeOnDestroy.dispose();
this.disposableTabBarListeners.dispose();
this.disposableTitleListeners.forEach(disposable => disposable.dispose());
this.disposableTitleListeners.clear();
}
}

View File

@@ -0,0 +1,268 @@
// *****************************************************************************
// 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 {
TasksMain,
MAIN_RPC_CONTEXT,
TaskExecutionDto,
TasksExt,
TaskDto,
TaskPresentationOptionsDTO
} from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common';
import { TaskProviderRegistry, TaskResolverRegistry, TaskProvider, TaskResolver } from '@theia/task/lib/browser/task-contribution';
import { interfaces } from '@theia/core/shared/inversify';
import { TaskInfo, TaskExitedEvent, TaskConfiguration, TaskOutputPresentation, RevealKind, PanelKind } from '@theia/task/lib/common/task-protocol';
import { TaskWatcher } from '@theia/task/lib/common/task-watcher';
import { TaskService } from '@theia/task/lib/browser/task-service';
import { TaskDefinitionRegistry } from '@theia/task/lib/browser';
const revealKindMap = new Map<number | RevealKind, RevealKind | number>(
[
[1, RevealKind.Always],
[2, RevealKind.Silent],
[3, RevealKind.Never],
[RevealKind.Always, 1],
[RevealKind.Silent, 2],
[RevealKind.Never, 3]
]
);
const panelKindMap = new Map<number | PanelKind, PanelKind | number>(
[
[1, PanelKind.Shared],
[2, PanelKind.Dedicated],
[3, PanelKind.New],
[PanelKind.Shared, 1],
[PanelKind.Dedicated, 2],
[PanelKind.New, 3]
]
);
export class TasksMainImpl implements TasksMain, Disposable {
private readonly proxy: TasksExt;
private readonly taskProviderRegistry: TaskProviderRegistry;
private readonly taskResolverRegistry: TaskResolverRegistry;
private readonly taskWatcher: TaskWatcher;
private readonly taskService: TaskService;
private readonly taskDefinitionRegistry: TaskDefinitionRegistry;
private readonly taskProviders = new Map<number, Disposable>();
private readonly toDispose = new DisposableCollection();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TASKS_EXT);
this.taskProviderRegistry = container.get(TaskProviderRegistry);
this.taskResolverRegistry = container.get(TaskResolverRegistry);
this.taskWatcher = container.get(TaskWatcher);
this.taskService = container.get(TaskService);
this.taskDefinitionRegistry = container.get(TaskDefinitionRegistry);
this.toDispose.push(this.taskWatcher.onTaskCreated((event: TaskInfo) => {
this.proxy.$onDidStartTask({
id: event.taskId,
task: this.fromTaskConfiguration(event.config)
}, event.terminalId!);
}));
this.toDispose.push(this.taskWatcher.onTaskExit((event: TaskExitedEvent) => {
this.proxy.$onDidEndTask(event.taskId);
}));
this.toDispose.push(this.taskWatcher.onDidStartTaskProcess((event: TaskInfo) => {
if (event.processId !== undefined) {
this.proxy.$onDidStartTaskProcess(event.processId, {
id: event.taskId,
task: this.fromTaskConfiguration(event.config)
});
}
}));
this.toDispose.push(this.taskWatcher.onDidEndTaskProcess((event: TaskExitedEvent) => {
if (event.code !== undefined) {
this.proxy.$onDidEndTaskProcess(event.code, event.taskId);
}
}));
// Inform proxy about running tasks form previous session
this.$taskExecutions().then(executions => {
if (executions.length > 0) {
this.proxy.$initLoadedTasks(executions);
}
});
}
dispose(): void {
this.toDispose.dispose();
}
$registerTaskProvider(handle: number, type: string): void {
const taskProvider = this.createTaskProvider(handle);
const taskResolver = this.createTaskResolver(handle);
const toDispose = new DisposableCollection(
this.taskProviderRegistry.register(type, taskProvider, handle),
this.taskResolverRegistry.registerTaskResolver(type, taskResolver),
Disposable.create(() => this.taskProviders.delete(handle))
);
this.taskProviders.set(handle, toDispose);
this.toDispose.push(toDispose);
}
$unregister(handle: number): void {
const disposable = this.taskProviders.get(handle);
if (disposable) {
disposable.dispose();
}
}
async $fetchTasks(taskVersion: string | undefined, taskType: string | undefined): Promise<TaskDto[]> {
if (taskVersion && !taskVersion.startsWith('2.')) { // Theia does not support 1.x or earlier task versions
return [];
}
const token: number = this.taskService.startUserAction();
const [configured, provided] = await Promise.all([
this.taskService.getConfiguredTasks(token),
this.taskService.getProvidedTasks(token)
]);
const result: TaskDto[] = [];
for (const tasks of [configured, provided]) {
for (const task of tasks) {
if (!taskType || (!!this.taskDefinitionRegistry.getDefinition(task) ? task._source === taskType : task.type === taskType)) {
result.push(this.fromTaskConfiguration(task));
}
}
}
return result;
}
async $executeTask(taskDto: TaskDto): Promise<TaskExecutionDto | undefined> {
const taskConfig = this.toTaskConfiguration(taskDto);
const taskInfo = await this.taskService.runTask(taskConfig);
if (taskInfo) {
return {
id: taskInfo.taskId,
task: this.fromTaskConfiguration(taskInfo.config)
};
}
}
async $taskExecutions(): Promise<{
id: number;
task: TaskDto;
}[]> {
const runningTasks = await this.taskService.getRunningTasks();
return runningTasks.map(taskInfo => ({
id: taskInfo.taskId,
task: this.fromTaskConfiguration(taskInfo.config)
}));
}
$terminateTask(id: number): void {
this.taskService.kill(id);
}
async $customExecutionComplete(id: number, exitCode: number | undefined): Promise<void> {
this.taskService.customExecutionComplete(id, exitCode);
}
protected createTaskProvider(handle: number): TaskProvider {
return {
provideTasks: () =>
this.proxy.$provideTasks(handle).then(tasks =>
tasks.map(taskDto =>
this.toTaskConfiguration(taskDto)
)
)
};
}
protected createTaskResolver(handle: number): TaskResolver {
return {
resolveTask: taskConfig =>
this.proxy.$resolveTask(handle, this.fromTaskConfiguration(taskConfig)).then(task =>
this.toTaskConfiguration(task)
)
};
}
protected toTaskConfiguration(taskDto: TaskDto): TaskConfiguration {
const { group, presentation, scope, source, runOptions, ...common } = taskDto ?? {};
const partialConfig: Partial<TaskConfiguration> = {};
if (presentation) {
partialConfig.presentation = this.convertTaskPresentation(presentation);
}
if (group) {
partialConfig.group = {
kind: group.kind,
isDefault: group.isDefault
};
}
return {
...common,
...partialConfig,
runOptions,
_scope: scope,
_source: source,
};
}
protected fromTaskConfiguration(task: TaskConfiguration): TaskDto {
const { group, presentation, _scope, _source, ...common } = task;
const partialDto: Partial<TaskDto> = {};
if (presentation) {
partialDto.presentation = this.convertTaskPresentation(presentation);
}
if (group === 'build' || group === 'test') {
partialDto.group = {
kind: group,
isDefault: false
};
} else if (typeof group === 'object') {
partialDto.group = group;
}
return {
...common,
...partialDto,
scope: _scope,
source: _source,
};
}
private convertTaskPresentation(presentationFrom: undefined): undefined;
private convertTaskPresentation(presentationFrom: TaskOutputPresentation): TaskPresentationOptionsDTO;
private convertTaskPresentation(presentationFrom: TaskPresentationOptionsDTO): TaskOutputPresentation;
private convertTaskPresentation(
presentationFrom: TaskOutputPresentation | TaskPresentationOptionsDTO | undefined
): TaskOutputPresentation | TaskPresentationOptionsDTO | undefined {
if (presentationFrom) {
const { reveal, panel, ...common } = presentationFrom;
const presentationTo: Partial<TaskOutputPresentation | TaskPresentationOptionsDTO> = {};
if (reveal) {
presentationTo.reveal = revealKindMap.get(reveal);
}
if (panel) {
presentationTo.panel = panelKindMap.get(panel)!;
}
return {
...common,
...presentationTo,
};
}
}
}

View File

@@ -0,0 +1,374 @@
// *****************************************************************************
// 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 { ApplicationShell, WidgetOpenerOptions, codicon } from '@theia/core/lib/browser';
import { TerminalEditorLocationOptions } from '@theia/plugin';
import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
import { TerminalProfileService } from '@theia/terminal/lib/browser/terminal-profile-service';
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT, TerminalOptions } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SerializableEnvironmentVariableCollection, ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { TerminalLink, TerminalLinkProvider } from '@theia/terminal/lib/browser/terminal-link-provider';
import { URI } from '@theia/core/lib/common/uri';
import { PluginTerminalRegistry } from './plugin-terminal-registry';
import { CancellationToken, isObject } from '@theia/core';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
import { PluginSharedStyle } from './plugin-shared-style';
import { ThemeIcon } from '@theia/core/lib/common/theme';
import debounce = require('@theia/core/shared/lodash.debounce');
interface TerminalObserverData {
nrOfLinesToMatch: number;
outputMatcherRegex: RegExp
disposables: DisposableCollection;
}
/**
* Plugin api service allows working with terminal emulator.
*/
export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLinkProvider, Disposable {
private readonly terminals: TerminalService;
private readonly terminalProfileService: TerminalProfileService;
private readonly pluginTerminalRegistry: PluginTerminalRegistry;
private readonly hostedPluginSupport: HostedPluginSupport;
private readonly shell: ApplicationShell;
private readonly extProxy: TerminalServiceExt;
private readonly sharedStyle: PluginSharedStyle;
private readonly shellTerminalServer: ShellTerminalServerProxy;
private readonly terminalLinkProviders: string[] = [];
private readonly toDispose = new DisposableCollection();
private readonly observers = new Map<string, TerminalObserverData>();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.terminals = container.get(TerminalService);
this.terminalProfileService = container.get(TerminalProfileService);
this.pluginTerminalRegistry = container.get(PluginTerminalRegistry);
this.hostedPluginSupport = container.get(HostedPluginSupport);
this.sharedStyle = container.get(PluginSharedStyle);
this.shell = container.get(ApplicationShell);
this.shellTerminalServer = container.get(ShellTerminalServerProxy);
this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT);
this.toDispose.push(this.terminals.onDidCreateTerminal(terminal => this.trackTerminal(terminal)));
for (const terminal of this.terminals.all) {
this.trackTerminal(terminal);
}
this.toDispose.push(this.terminals.onDidChangeCurrentTerminal(() => this.updateCurrentTerminal()));
this.updateCurrentTerminal();
this.shellTerminalServer.getEnvVarCollections().then(collections => this.extProxy.$initEnvironmentVariableCollections(collections));
this.pluginTerminalRegistry.startCallback = id => this.startProfile(id);
container.bind(TerminalLinkProvider).toDynamicValue(() => this);
this.toDispose.push(this.terminalProfileService.onDidChangeDefaultShell(shell => {
this.extProxy.$setShell(shell);
}));
}
async startProfile(id: string): Promise<string> {
await this.hostedPluginSupport.activateByTerminalProfile(id);
return this.extProxy.$startProfile(id, CancellationToken.None);
}
$setEnvironmentVariableCollection(persistent: boolean, extensionIdentifier: string, rootUri: string, collection: SerializableEnvironmentVariableCollection): void {
if (collection) {
this.shellTerminalServer.setCollection(extensionIdentifier, rootUri, persistent, collection, collection.description);
} else {
this.shellTerminalServer.deleteCollection(extensionIdentifier);
}
}
dispose(): void {
this.toDispose.dispose();
}
protected updateCurrentTerminal(): void {
const { currentTerminal } = this.terminals;
this.extProxy.$currentTerminalChanged(currentTerminal && currentTerminal.id);
}
protected async trackTerminal(terminal: TerminalWidget): Promise<void> {
let name = terminal.title.label;
this.extProxy.$terminalCreated(terminal.id, name);
const updateTitle = () => {
if (name !== terminal.title.label) {
name = terminal.title.label;
this.extProxy.$terminalNameChanged(terminal.id, name);
}
};
terminal.title.changed.connect(updateTitle);
this.toDispose.push(Disposable.create(() => terminal.title.changed.disconnect(updateTitle)));
const updateProcessId = () => terminal.processId.then(
processId => this.extProxy.$terminalOpened(terminal.id, processId, terminal.terminalId, terminal.dimensions.cols, terminal.dimensions.rows),
() => {/* no-op */ }
);
updateProcessId();
this.toDispose.push(terminal.onDidOpen(() => updateProcessId()));
this.toDispose.push(terminal.onTerminalDidClose(term => this.extProxy.$terminalClosed(term.id, term.exitStatus)));
this.toDispose.push(terminal.onSizeChanged(({ cols, rows }) => {
this.extProxy.$terminalSizeChanged(terminal.id, cols, rows);
}));
this.toDispose.push(terminal.onData(data => {
this.extProxy.$terminalOnInput(terminal.id, data);
this.extProxy.$terminalOnInteraction(terminal.id);
}));
this.toDispose.push(terminal.onShellTypeChanged(shellType => {
this.extProxy.$terminalShellTypeChanged(terminal.id, shellType);
}));
this.observers.forEach((observer, id) => this.observeTerminal(id, terminal, observer));
}
$write(id: string, data: string): void {
const terminal = this.terminals.getById(id);
if (!terminal) {
return;
}
terminal.write(data);
}
$resize(id: string, cols: number, rows: number): void {
const terminal = this.terminals.getById(id);
if (!terminal) {
return;
}
terminal.resize(cols, rows);
}
async $createTerminal(id: string, options: TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise<string> {
const terminal = await this.terminals.newTerminal({
id,
title: options.name,
iconClass: this.toIconClass(options),
shellPath: options.shellPath,
shellArgs: options.shellArgs,
cwd: options.cwd ? new URI(options.cwd) : undefined,
env: options.env,
strictEnv: options.strictEnv,
destroyTermOnClose: true,
useServerTitle: false,
attributes: options.attributes,
hideFromUser: options.hideFromUser,
location: this.getTerminalLocation(options, parentId),
isPseudoTerminal,
isTransient: options.isTransient,
shellIntegrationNonce: options.shellIntegrationNonce ?? undefined
});
if (options.message) {
terminal.writeLine(options.message);
}
terminal.start();
return terminal.id;
}
protected getTerminalLocation(options: TerminalOptions, parentId?: string): TerminalLocation | TerminalEditorLocationOptions | { parentTerminal: string; } | undefined {
if (typeof options.location === 'number' && Object.values(TerminalLocation).includes(options.location)) {
return options.location;
} else if (options.location && typeof options.location === 'object') {
if ('parentTerminal' in options.location) {
if (!parentId) {
throw new Error('parentTerminal is set but no parentId is provided');
}
return { 'parentTerminal': parentId };
} else {
return options.location;
}
}
return undefined;
}
$sendText(id: string, text: string, shouldExecute?: boolean): void {
const terminal = this.terminals.getById(id);
if (terminal) {
text = text.replace(/\r?\n/g, '\r');
if (shouldExecute && text.charAt(text.length - 1) !== '\r') {
text += '\r';
}
terminal.sendText(text);
}
}
$show(id: string, preserveFocus?: boolean): void {
const terminal = this.terminals.getById(id);
if (terminal) {
const options: WidgetOpenerOptions = {};
if (preserveFocus) {
options.mode = 'reveal';
}
this.terminals.open(terminal, options);
}
}
$hide(id: string): void {
const terminal = this.terminals.getById(id);
if (terminal && terminal.isVisible) {
const area = this.shell.getAreaFor(terminal);
if (area) {
this.shell.collapsePanel(area);
}
}
}
$dispose(id: string): void {
const terminal = this.terminals.getById(id);
if (terminal) {
terminal.dispose();
}
}
$setName(id: string, name: string): void {
this.terminals.getById(id)?.setTitle(name);
}
$sendTextByTerminalId(id: number, text: string, addNewLine?: boolean): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
text = text.replace(/\r?\n/g, '\r');
if (addNewLine && text.charAt(text.length - 1) !== '\r') {
text += '\r';
}
terminal.sendText(text);
}
}
$writeByTerminalId(id: number, data: string): void {
const terminal = this.terminals.getByTerminalId(id);
if (!terminal) {
return;
}
terminal.write(data);
}
$resizeByTerminalId(id: number, cols: number, rows: number): void {
const terminal = this.terminals.getByTerminalId(id);
if (!terminal) {
return;
}
terminal.resize(cols, rows);
}
$showByTerminalId(id: number, preserveFocus?: boolean): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
const options: WidgetOpenerOptions = {};
if (preserveFocus) {
options.mode = 'reveal';
}
this.terminals.open(terminal, options);
}
}
$hideByTerminalId(id: number): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal && terminal.isVisible) {
const area = this.shell.getAreaFor(terminal);
if (area) {
this.shell.collapsePanel(area);
}
}
}
$disposeByTerminalId(id: number, waitOnExit?: boolean | string): void {
const terminal = this.terminals.getByTerminalId(id);
if (terminal) {
if (waitOnExit) {
terminal.waitOnExit(waitOnExit);
return;
}
terminal.dispose();
}
}
$setNameByTerminalId(id: number, name: string): void {
this.terminals.getByTerminalId(id)?.setTitle(name);
}
async $registerTerminalLinkProvider(providerId: string): Promise<void> {
this.terminalLinkProviders.push(providerId);
}
async $unregisterTerminalLinkProvider(providerId: string): Promise<void> {
const index = this.terminalLinkProviders.indexOf(providerId);
if (index > -1) {
this.terminalLinkProviders.splice(index, 1);
}
}
$registerTerminalObserver(id: string, nrOfLinesToMatch: number, outputMatcherRegex: string): void {
const observerData = {
nrOfLinesToMatch: nrOfLinesToMatch,
outputMatcherRegex: new RegExp(outputMatcherRegex, 'm'),
disposables: new DisposableCollection()
};
this.observers.set(id, observerData);
this.terminals.all.forEach(terminal => {
this.observeTerminal(id, terminal, observerData);
});
}
protected observeTerminal(observerId: string, terminal: TerminalWidget, observerData: TerminalObserverData): void {
const doMatch = debounce(() => {
const lineCount = Math.min(observerData.nrOfLinesToMatch, terminal.buffer.length);
const lines = terminal.buffer.getLines(terminal.buffer.length - lineCount, lineCount);
const result = lines.join('\n').match(observerData.outputMatcherRegex);
if (result) {
this.extProxy.$reportOutputMatch(observerId, result.map(value => value));
}
});
observerData.disposables.push(terminal.onOutput(output => {
doMatch();
}));
}
protected toIconClass(options: TerminalOptions): string | ThemeIcon | undefined {
const iconColor = isObject<{ id: string }>(options.color) && typeof options.color.id === 'string' ? options.color.id : undefined;
let iconClass: string;
if (options.iconUrl) {
if (typeof options.iconUrl === 'object' && 'id' in options.iconUrl) {
iconClass = codicon(options.iconUrl.id);
} else {
const iconReference = this.sharedStyle.toIconClass(options.iconUrl);
this.toDispose.push(iconReference);
iconClass = iconReference.object.iconClass;
}
} else {
iconClass = codicon('terminal');
}
return iconColor ? { id: iconClass, color: { id: iconColor } } : iconClass;
}
$unregisterTerminalObserver(id: string): void {
const observer = this.observers.get(id);
if (observer) {
observer.disposables.dispose();
this.observers.delete(id);
} else {
throw new Error(`Unregistering unknown terminal observer: ${id}`);
}
}
async provideLinks(line: string, terminal: TerminalWidget, cancellationToken?: CancellationToken | undefined): Promise<TerminalLink[]> {
if (this.terminalLinkProviders.length < 1) {
return [];
}
const links = await this.extProxy.$provideTerminalLinks(line, terminal.id, cancellationToken ?? CancellationToken.None);
return links.map(link => ({ ...link, handle: () => this.extProxy.$handleTerminalLink(link) }));
}
}

View File

@@ -0,0 +1,635 @@
// *****************************************************************************
// 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
// *****************************************************************************
import { SimpleObservableCollection, TreeCollection, observableProperty } from '@theia/test/lib/common/collections';
import {
TestController, TestItem, TestOutputItem, TestRun, TestRunProfile, TestService, TestState, TestStateChangedEvent
} from '@theia/test/lib/browser/test-service';
import { TestExecutionProgressService } from '@theia/test/lib/browser/test-execution-progress-service';
import { AccumulatingTreeDeltaEmitter, CollectionDelta, DeltaKind, TreeDelta, TreeDeltaBuilder } from '@theia/test/lib/common/tree-delta';
import { Emitter, Location, Range } from '@theia/core/shared/vscode-languageserver-protocol';
import { Range as PluginRange, Location as PluginLocation } from '../../common/plugin-api-rpc-model';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { CancellationToken, Disposable, Event, URI } from '@theia/core';
import { MAIN_RPC_CONTEXT, TestControllerUpdate, TestingExt, TestingMain } from '../../common';
import { RPCProtocol } from '../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import {
TestExecutionState, TestItemDTO, TestItemReference, TestOutputDTO,
TestRunDTO, TestRunProfileDTO, TestStateChangeDTO
} from '../../common/test-types';
import { TestRunProfileKind } from '../../plugin/types-impl';
import { CommandRegistryMainImpl } from './command-registry-main';
export class TestItemCollection extends TreeCollection<string, TestItemImpl, TestItemImpl | TestControllerImpl> {
override add(item: TestItemImpl): TestItemImpl | undefined {
item.realParent = this.owner;
return super.add(item);
}
}
export class TestItemImpl implements TestItem {
update(value: Partial<TestItemDTO>): void {
if ('label' in value) {
this.label = value.label!;
}
if ('range' in value) {
this.range = convertRange(value.range);
}
if ('sortKey' in value) {
this.sortKey = value.sortKey!;
}
if ('tags' in value) {
this.tags = value.tags!;
}
if ('busy' in value) {
this.busy = value.busy!;
}
if ('sortKey' in value) {
this.sortKey = value.sortKey;
}
if ('canResolveChildren' in value) {
this.canResolveChildren = value.canResolveChildren!;
}
if ('description' in value) {
this.description = value.description;
}
if ('error' in value) {
this.error = value.error;
}
}
constructor(readonly uri: URI, readonly id: string) {
this.items = new TestItemCollection(this, (v: TestItemImpl) => v.path, (v: TestItemImpl) => v.deltaBuilder);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected notifyPropertyChange(property: keyof TestItemImpl, value: any): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
if (this.path) {
this.deltaBuilder?.reportChanged(this.path, val);
}
}
_deltaBuilder: TreeDeltaBuilder<string, TestItemImpl> | undefined;
get deltaBuilder(): TreeDeltaBuilder<string, TestItemImpl> | undefined {
if (this._deltaBuilder) {
return this._deltaBuilder;
} else if (this.realParent) {
this._deltaBuilder = this.realParent.deltaBuilder;
return this._deltaBuilder;
} else {
return undefined;
}
}
_path: string[] | undefined;
get path(): string[] {
if (this._path) {
return this._path;
} else if (this.realParent instanceof TestItemImpl) {
this._path = [...this.realParent.path, this.id];
return this._path;
} else {
return [this.id];
}
};
get parent(): TestItem | undefined {
const realParent = this.realParent;
if (realParent instanceof TestItemImpl) {
return realParent;
}
return undefined;
}
private _parent?: TestItemImpl | TestControllerImpl;
get realParent(): TestItemImpl | TestControllerImpl | undefined {
return this._parent;
}
set realParent(v: TestItemImpl | TestControllerImpl | undefined) {
this.iterate(item => {
item._path = undefined;
return true;
});
this._parent = v;
}
get controller(): TestControllerImpl | undefined {
return this.realParent?.controller;
}
protected iterate(toDo: (v: TestItemImpl) => boolean): boolean {
if (toDo(this)) {
for (let i = 0; i < this.items.values.length; i++) {
if (!this.items.values[i].iterate(toDo)) {
return false;
}
}
return true;
} else {
return false;
}
}
@observableProperty('notifyPropertyChange')
label: string = '';
@observableProperty('notifyPropertyChange')
range?: Range;
@observableProperty('notifyPropertyChange')
sortKey?: string | undefined;
@observableProperty('notifyPropertyChange')
tags: string[] = [];
@observableProperty('notifyPropertyChange')
busy: boolean = false;
@observableProperty('notifyPropertyChange')
canResolveChildren: boolean = false;
@observableProperty('notifyPropertyChange')
description?: string | undefined;
@observableProperty('notifyPropertyChange')
error?: string | MarkdownString | undefined;
items: TestItemCollection;
get tests(): readonly TestItemImpl[] {
return this.items.values;
}
resolveChildren(): void {
if (this.canResolveChildren) {
this.controller?.resolveChildren(this);
}
}
}
function itemToPath(item: TestItem): string[] {
if (!(item instanceof TestItemImpl)) {
throw new Error(`Not a TestItemImpl: ${item.id}`);
}
return item.path;
}
class TestRunProfileImpl implements TestRunProfile {
label: string;
private _isDefault: boolean;
set isDefault(isDefault: boolean) {
this._isDefault = isDefault;
this.proxy.$onDidChangeDefault(this.controllerId, this.id, isDefault);
}
get isDefault(): boolean {
return this._isDefault;
}
tag: string;
canConfigure: boolean;
update(update: Partial<TestRunProfileDTO>): void {
if ('label' in update) {
this.label = update.label!;
}
if ('isDefault' in update) {
this._isDefault = update.isDefault!;
}
if ('tag' in update) {
this.tag = update.tag!;
}
if ('canConfigure' in update) {
this.canConfigure = update.canConfigure!;
}
}
constructor(
private proxy: TestingExt,
private controllerId: string,
readonly id: string,
readonly kind: TestRunProfileKind,
label: string,
isDefault: boolean,
tag: string) {
this.label = label;
this.isDefault = isDefault;
this.tag = tag;
}
configure(): void {
this.proxy.$onConfigureRunProfile(this.controllerId, this.id);
}
run(name: string, included: TestItem[], excluded: TestItem[], preserveFocus: boolean): void {
this.proxy.$onRunControllerTests([{
controllerId: this.controllerId,
name,
profileId: this.id,
includedTests: included.map(item => itemToPath(item)),
excludedTests: excluded.map(item => itemToPath(item)),
preserveFocus
}]);
}
}
class TestRunImpl implements TestRun {
private testStates: Map<TestItem, TestState> = new Map();
private outputIndices: Map<TestItem, number[]> = new Map();
private outputs: TestOutputItem[] = [];
private onDidChangePropertyEmitter = new Emitter<{ name?: string; isRunning?: boolean; }>();
onDidChangeProperty: Event<{ name?: string; isRunning?: boolean; }> = this.onDidChangePropertyEmitter.event;
constructor(readonly controller: TestControllerImpl, readonly proxy: TestingExt, readonly id: string, name: string) {
this.name = name;
this.isRunning = false;
}
@observableProperty('notifyPropertyChange')
isRunning: boolean;
@observableProperty('notifyPropertyChange')
name: string;
ended(): void {
const stateEvents: TestStateChangedEvent[] = [];
this.testStates.forEach((state, item) => {
if (state.state <= TestExecutionState.Running) {
stateEvents.push({
oldState: state,
newState: undefined,
test: item
});
this.testStates.delete(item);
}
});
if (stateEvents.length > 0) {
this.onDidChangeTestStateEmitter.fire(stateEvents);
}
this.isRunning = false;
}
protected notifyPropertyChange(property: 'name' | 'isRunning', value: unknown): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const val: any = {};
val[property] = value;
this.onDidChangePropertyEmitter.fire(val);
}
cancel(): void {
this.proxy.$onCancelTestRun(this.controller.id, this.id);
}
getTestState(item: TestItem): TestState | undefined {
return this.testStates.get(item);
}
private onDidChangeTestStateEmitter: Emitter<TestStateChangedEvent[]> = new Emitter();
onDidChangeTestState: Event<TestStateChangedEvent[]> = this.onDidChangeTestStateEmitter.event;
getOutput(item?: TestItem | undefined): readonly TestOutputItem[] {
if (!item) {
return this.outputs;
} else {
const indices = this.outputIndices.get(item);
if (!indices) {
return [];
} else {
return indices.map(index => this.outputs[index]);
}
}
}
private onDidChangeTestOutputEmitter: Emitter<[TestItem | undefined, TestOutputItem][]> = new Emitter();
onDidChangeTestOutput: Event<[TestItem | undefined, TestOutputItem][]> = this.onDidChangeTestOutputEmitter.event;
applyChanges(stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void {
const stateEvents: TestStateChangedEvent[] = [];
stateChanges.forEach(change => {
const item = this.controller.findItem(change.itemPath);
if (item) {
const oldState = this.testStates.get(item);
this.testStates.set(item, change);
stateEvents.push({ test: item, oldState: oldState, newState: change });
}
});
const outputEvents: [TestItem | undefined, TestOutputItem][] = [];
outputChanges.forEach(change => {
const output = {
output: change.output,
location: convertLocation(change.location)
};
this.outputs.push(output);
let item = undefined;
if (change.itemPath) {
item = this.controller.findItem(change.itemPath);
if (item) {
let indices = this.outputIndices.get(item);
if (!indices) {
indices = [];
this.outputIndices.set(item, indices);
}
indices.push(this.outputs.length - 1);
}
}
outputEvents.push([item, output]);
});
this.onDidChangeTestStateEmitter.fire(stateEvents);
this.onDidChangeTestOutputEmitter.fire(outputEvents);
}
get items(): readonly TestItem[] {
return [...this.testStates.keys()];
}
}
function convertLocation(location: PluginLocation | undefined): Location | undefined {
if (!location) {
return undefined;
}
return {
uri: location.uri.toString(),
range: convertRange(location.range)
};
}
interface TestCollectionHolder {
items: TestItemCollection;
}
function convertRange(range: PluginRange): Range;
function convertRange(range: PluginRange | undefined): Range | undefined;
function convertRange(range: PluginRange | undefined): Range | undefined {
if (range) {
return {
start: {
line: range.startLineNumber,
character: range.startColumn
}, end: {
line: range.endLineNumber,
character: range.endColumn
}
};
}
return undefined;
}
class TestControllerImpl implements TestController {
private _profiles = new SimpleObservableCollection<TestRunProfileImpl>();
private _runs = new SimpleObservableCollection<TestRunImpl>();
readonly deltaBuilder = new AccumulatingTreeDeltaEmitter<string, TestItemImpl>(300);
canRefresh: boolean;
private canResolveChildren: boolean = false;
readonly items = new TestItemCollection(this, item => item.path, () => this.deltaBuilder);
constructor(private readonly proxy: TestingExt, readonly id: string, public label: string) {
}
refreshTests(token: CancellationToken): Promise<void> {
return this.proxy.$refreshTests(this.id, token);
}
applyDelta(diff: TreeDelta<string, TestItemDTO>[]): void {
this.applyDeltasToCollection(this, diff);
}
withProfile(profileId: string): TestRunProfileImpl {
const profile = this._profiles.values.find(p => p.id === profileId);
if (!profile) {
throw new Error(`No test profile ${profileId} found in controller with id ${this.id} found`);
}
return profile;
}
withRun(runId: string): TestRunImpl {
const run = this._runs.values.find(p => p.id === runId);
if (!run) {
throw new Error(`No test profile ${runId} found in controller with id ${this.id} found`);
}
return run;
}
protected applyDeltasToCollection(root: TestCollectionHolder, deltas: TreeDelta<string, TestItemDTO>[]): void {
deltas.forEach(delta => this.applyDeltaToCollection(root, delta));
}
protected applyDeltaToCollection(root: TestCollectionHolder, delta: TreeDelta<string, TestItemDTO>): void {
if (delta.type === DeltaKind.ADDED || delta.type === DeltaKind.REMOVED) {
const node = this.findNodeInRoot(root, delta.path.slice(0, delta.path.length - 1), 0);
if (node) {
if (delta.type === DeltaKind.ADDED) {
node.items.add(this.createTestItem(delta.value! as TestItemDTO));
} else {
node.items.remove(delta.path[delta.path.length - 1]);
}
}
} else {
const node = this.findNodeInRoot(root, delta.path, 0);
if (node) {
if (delta.type === DeltaKind.CHANGED) {
(node as TestItemImpl).update(delta.value!);
}
if (delta.childDeltas) {
this.applyDeltasToCollection(node, delta.childDeltas);
}
}
}
}
findItem(path: string[]): TestItemImpl | undefined {
if (path.length === 0) {
console.warn('looking for item with zero-path');
return undefined;
}
return this.findNodeInRoot(this, path, 0) as TestItemImpl;
}
protected findNodeInRoot(root: TestCollectionHolder, path: string[], startIndex: number): TestCollectionHolder | undefined {
if (startIndex >= path.length) {
return root;
}
const child = root.items.get(path[startIndex]);
if (!child) {
return undefined;
}
return this.findNodeInRoot(child, path, startIndex + 1);
}
protected createTestItem(value: TestItemDTO): TestItemImpl {
const item = new TestItemImpl(URI.fromComponents(value.uri!), value?.id!);
item.update(value);
value.children?.forEach(child => item.items.add(this.createTestItem(child)));
return item;
}
get controller(): TestControllerImpl {
return this;
}
get testRunProfiles(): readonly TestRunProfile[] {
return this._profiles.values;
}
update(change: Partial<TestControllerUpdate>): void {
if ('canRefresh' in change) {
this.canRefresh = change.canRefresh!;
}
if ('canResolve' in change) {
this.canResolveChildren = change.canResolve!;
}
if ('label' in change) {
this.label = change.label!;
}
}
addProfile(profile: TestRunProfileImpl): void {
this._profiles.add(profile);
}
addRun(runId: string, runName: string, isRunning: boolean): TestRunImpl {
const run = new TestRunImpl(this, this.proxy, runId, runName);
run.isRunning = isRunning;
this._runs.add(run);
return run;
}
onProfilesChanged: Event<CollectionDelta<TestRunProfile, TestRunProfile>> = this._profiles.onChanged;
removeProfile(profileId: string): void {
this._profiles.remove(this.withProfile(profileId));
}
get testRuns(): readonly TestRun[] {
return this._runs.values;
}
onRunsChanged: Event<CollectionDelta<TestRun, TestRun>> = this._runs.onChanged;
get tests(): readonly TestItemImpl[] {
return this.items.values;
}
onItemsChanged: Event<TreeDelta<string, TestItemImpl>[]> = this.deltaBuilder.onDidFlush;
resolveChildren(item: TestItem): void {
if (this.canResolveChildren) {
this.proxy.$onResolveChildren(this.id, itemToPath(item));
}
}
clearRuns(): void {
this._runs.clear();
}
}
export class TestingMainImpl implements TestingMain {
private testService: TestService;
private testExecutionProgressService: TestExecutionProgressService;
private controllerRegistrations = new Map<string, [TestControllerImpl, Disposable]>();
private proxy: TestingExt;
canRefresh: boolean;
constructor(rpc: RPCProtocol, container: interfaces.Container, commandRegistry: CommandRegistryMainImpl) {
this.testService = container.get(TestService);
this.testExecutionProgressService = container.get(TestExecutionProgressService);
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TESTING_EXT);
commandRegistry.registerArgumentProcessor({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
processArgument(arg: any): any {
if (arg instanceof TestItemImpl) {
if (!arg.controller || !arg.path) {
throw new Error(`Passing unattached test item ${arg.id} as a command argument`);
}
return TestItemReference.create(arg.controller.id, arg.path);
}
return arg;
}
});
}
$registerTestController(controllerId: string, label: string): void {
const controller = new TestControllerImpl(this.proxy, controllerId, label);
this.controllerRegistrations.set(controllerId, [controller, this.testService.registerTestController(controller)]);
}
$updateController(controllerId: string, patch: Partial<TestControllerUpdate>): void {
this.withController(controllerId).update(patch);
}
$unregisterTestController(controllerId: string): void {
const registered = this.controllerRegistrations.get(controllerId);
if (registered) {
this.controllerRegistrations.delete(controllerId);
registered[1].dispose();
}
}
private withController(controllerId: string): TestControllerImpl {
const registration = this.controllerRegistrations.get(controllerId);
if (!registration) {
throw new Error(`No test controller with id ${controllerId} found`);
}
return registration[0];
}
$notifyDelta(controllerId: string, diff: TreeDelta<string, TestItemDTO>[]): void {
this.withController(controllerId).applyDelta(diff);
}
$notifyTestRunProfileCreated(controllerId: string, profile: TestRunProfileDTO): void {
const registration = this.controllerRegistrations.get(controllerId);
if (!registration) {
throw new Error(`No test controller with id ${controllerId} found`);
}
registration[0].addProfile(new TestRunProfileImpl(this.proxy, controllerId, profile.id, profile.kind, profile.label, profile.isDefault, profile.tag));
}
$updateTestRunProfile(controllerId: string, profileId: string, update: Partial<TestRunProfileDTO>): void {
this.withController(controllerId).withProfile(profileId).update(update);
}
$removeTestRunProfile(controllerId: string, profileId: string): void {
this.withController(controllerId).removeProfile(profileId);
}
$notifyTestRunCreated(controllerId: string, run: TestRunDTO, preserveFocus: boolean): void {
this.testExecutionProgressService.onTestRunRequested(preserveFocus);
this.withController(controllerId).addRun(run.id, run.name, run.isRunning);
}
$notifyTestStateChanged(controllerId: string, runId: string, stateChanges: TestStateChangeDTO[], outputChanges: TestOutputDTO[]): void {
this.withController(controllerId).withRun(runId).applyChanges(stateChanges, outputChanges);
}
$notifyTestRunEnded(controllerId: string, runId: string): void {
this.withController(controllerId).withRun(runId).ended();
}
}

View File

@@ -0,0 +1,545 @@
// *****************************************************************************
// Copyright (C) 2017 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 monaco from '@theia/monaco-editor-core';
import { StandaloneCodeEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { type ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
import {
TextEditorConfiguration,
EditorChangedPropertiesData,
Selection,
TextEditorConfigurationUpdate,
TextEditorRevealType,
SingleEditOperation,
ApplyEditsOptions,
SnippetEditOptions,
DecorationOptions
} from '../../common/plugin-api-rpc';
import { Range } from '../../common/plugin-api-rpc-model';
import { Emitter, Event } from '@theia/core';
import { TextEditorCursorStyle, cursorStyleToString } from '../../common/editor-options';
import { TextEditorLineNumbersStyle, EndOfLine } from '../../plugin/types-impl';
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
import { EndOfLineSequence, ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { EditorOption, RenderLineNumbersType } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
export class TextEditorMain implements Disposable {
private properties: TextEditorPropertiesMain | undefined;
private editor: MonacoEditor | SimpleMonacoEditor | undefined;
private readonly onPropertiesChangedEmitter = new Emitter<EditorChangedPropertiesData>();
private readonly toDispose = new DisposableCollection(
Disposable.create(() => this.properties = undefined),
this.onPropertiesChangedEmitter
);
constructor(
private id: string,
private model: monaco.editor.IModel | ITextModel,
editor: MonacoEditor | SimpleMonacoEditor
) {
this.toDispose.push(this.model.onDidChangeOptions(() =>
this.updateProperties(undefined)
));
this.setEditor(editor);
this.updateProperties(undefined);
}
dispose(): void {
this.toDispose.dispose();
}
private updateProperties(source?: string): void {
this.setProperties(TextEditorPropertiesMain.readFromEditor(this.properties, this.model, this.editor!), source);
}
private setProperties(newProperties: TextEditorPropertiesMain, source: string | undefined): void {
const result = newProperties.generateDelta(this.properties, source);
this.properties = newProperties;
if (result) {
this.onPropertiesChangedEmitter.fire(result);
}
}
protected readonly toDisposeOnEditor = new DisposableCollection();
private setEditor(editor?: MonacoEditor | SimpleMonacoEditor): void {
if (this.editor === editor) {
return;
}
this.toDisposeOnEditor.dispose();
this.toDispose.push(this.toDisposeOnEditor);
this.editor = editor;
this.toDisposeOnEditor.push(Disposable.create(() => this.editor = undefined));
if (this.editor) {
const monacoEditor = this.editor.getControl();
this.toDisposeOnEditor.push(this.editor.onSelectionChanged(_ => {
this.updateProperties();
}));
this.toDisposeOnEditor.push(monacoEditor.onDidChangeModel(() => {
this.setEditor(undefined);
}));
this.toDisposeOnEditor.push(monacoEditor.onDidChangeCursorSelection(e => {
this.updateProperties(e.source);
}));
this.toDisposeOnEditor.push(monacoEditor.onDidChangeConfiguration(() => {
this.updateProperties();
}));
this.toDisposeOnEditor.push(monacoEditor.onDidLayoutChange(() => {
this.updateProperties();
}));
this.toDisposeOnEditor.push(monacoEditor.onDidScrollChange(() => {
this.updateProperties();
}));
this.updateProperties();
}
}
getId(): string {
return this.id;
}
getModel(): monaco.editor.IModel | ITextModel {
return this.model;
}
getProperties(): TextEditorPropertiesMain | undefined {
return this.properties;
}
get onPropertiesChangedEvent(): Event<EditorChangedPropertiesData> {
return this.onPropertiesChangedEmitter.event;
}
get diffInformation(): ILineChange[] | undefined {
if (!(this.editor instanceof MonacoDiffEditor)) {
return [];
}
return this.editor.diffInformation;
}
setSelections(selections: Selection[]): void {
if (this.editor) {
this.editor.getControl().setSelections(selections);
return;
}
const monacoSelections = selections.map(TextEditorMain.toMonacoSelections);
this.setProperties(new TextEditorPropertiesMain(monacoSelections, this.properties!.options, this.properties!.visibleRanges), undefined);
}
setConfiguration(newConfiguration: TextEditorConfigurationUpdate): void {
this.setIndentConfiguration(newConfiguration);
if (!this.editor) {
return;
}
if (newConfiguration.cursorStyle) {
const newCursorStyle = cursorStyleToString(newConfiguration.cursorStyle);
this.editor.getControl().updateOptions({
cursorStyle: newCursorStyle
});
}
if (typeof newConfiguration.lineNumbers !== 'undefined') {
let lineNumbers: 'on' | 'off' | 'relative' | 'interval';
switch (newConfiguration.lineNumbers) {
case TextEditorLineNumbersStyle.On:
lineNumbers = 'on';
break;
case TextEditorLineNumbersStyle.Relative:
lineNumbers = 'relative';
break;
case TextEditorLineNumbersStyle.Interval:
lineNumbers = 'interval';
break;
default:
lineNumbers = 'off';
}
this.editor.getControl().updateOptions({
lineNumbers: lineNumbers
});
}
}
private setIndentConfiguration(newConfiguration: TextEditorConfigurationUpdate): void {
if (newConfiguration.tabSize === 'auto' || newConfiguration.insertSpaces === 'auto') {
const creationOpts = this.model.getOptions();
let insertSpaces = creationOpts.insertSpaces;
let tabSize = creationOpts.tabSize;
if (newConfiguration.insertSpaces !== 'auto' && typeof newConfiguration.insertSpaces !== 'undefined') {
insertSpaces = newConfiguration.insertSpaces;
}
if (newConfiguration.tabSize !== 'auto' && typeof newConfiguration.tabSize !== 'undefined') {
tabSize = newConfiguration.tabSize;
}
this.model.detectIndentation(insertSpaces, tabSize);
return;
}
const newOpts: monaco.editor.ITextModelUpdateOptions = {};
if (typeof newConfiguration.insertSpaces !== 'undefined') {
newOpts.insertSpaces = newConfiguration.insertSpaces;
}
if (typeof newConfiguration.tabSize !== 'undefined') {
newOpts.tabSize = newConfiguration.tabSize;
}
if (typeof newConfiguration.indentSize !== 'undefined') {
if (newConfiguration.indentSize === 'tabSize') {
newOpts.indentSize = newConfiguration.tabSize;
} else if (typeof newConfiguration.indentSize == 'number') {
newOpts.indentSize = newConfiguration.indentSize;
}
}
this.model.updateOptions(newOpts);
}
revealRange(range: monaco.Range, revealType: TextEditorRevealType): void {
if (!this.editor || this.editor instanceof SimpleMonacoEditor) {
return;
}
switch (revealType) {
case TextEditorRevealType.Default:
this.editor.getControl().revealRange(range, monaco.editor.ScrollType.Smooth);
break;
case TextEditorRevealType.InCenter:
this.editor.getControl().revealRangeInCenter(range, monaco.editor.ScrollType.Smooth);
break;
case TextEditorRevealType.InCenterIfOutsideViewport:
this.editor.getControl().revealRangeInCenterIfOutsideViewport(range, monaco.editor.ScrollType.Smooth);
break;
case TextEditorRevealType.AtTop:
this.editor.getControl().revealRangeAtTop(range, monaco.editor.ScrollType.Smooth);
break;
default:
console.warn(`Unknown revealType: ${revealType}`);
break;
}
}
applyEdits(versionId: number, edits: SingleEditOperation[], opts: ApplyEditsOptions): boolean {
if (this.model.getVersionId() !== versionId) {
// model changed in the meantime
return false;
}
if (!this.editor) {
return false;
}
if (opts.setEndOfLine === EndOfLine.CRLF && !this.isSimpleWidget(this.model)) {
this.model.setEOL(monaco.editor.EndOfLineSequence.CRLF);
} else if (opts.setEndOfLine === EndOfLine.LF && !this.isSimpleWidget(this.model)) {
this.model.setEOL(monaco.editor.EndOfLineSequence.LF);
} else if (opts.setEndOfLine === EndOfLine.CRLF && this.isSimpleWidget(this.model)) {
this.model.setEOL(EndOfLineSequence.CRLF);
} else if (opts.setEndOfLine === EndOfLine.LF && this.isSimpleWidget(this.model)) {
this.model.setEOL(EndOfLineSequence.CRLF);
}
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) : this.editor.getControl().getModel()!.getFullModelRange(),
/* eslint-disable-next-line no-null/no-null */
text: text || null,
forceMoveMarkers: edit.forceMoveMarkers
});
}
if (opts.undoStopBefore) {
this.editor.getControl().pushUndoStop();
}
this.editor.getControl().executeEdits('MainThreadTextEditor', editOperations);
if (opts.undoStopAfter) {
this.editor.getControl().pushUndoStop();
}
return true;
}
insertSnippet(template: string, ranges: Range[], opts: SnippetEditOptions): boolean {
const snippetController: SnippetController2 | null | undefined = this.editor?.getControl().getContribution('snippetController2');
if (!snippetController || !this.editor) { return false; }
const selections = ranges.map(r => new monaco.Selection(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn));
this.editor.getControl().setSelections(selections);
this.editor.focus();
snippetController.insert(template, {
undoStopBefore: opts.undoStopBefore,
undoStopAfter: opts.undoStopAfter,
adjustWhitespace: !opts.keepWhitespace,
overwriteBefore: 0,
overwriteAfter: 0
});
return true;
}
setDecorations(key: string, ranges: DecorationOptions[]): void {
if (!this.editor) {
return;
}
(this.editor.getControl() as unknown as StandaloneCodeEditor)
.setDecorationsByType('Plugin decorations', key, ranges.map(option => Object.assign(option, { color: undefined })));
}
setDecorationsFast(key: string, _ranges: number[]): void {
if (!this.editor) {
return;
}
const ranges: Range[] = [];
const len = Math.floor(_ranges.length / 4);
for (let i = 0; i < len; i++) {
ranges[i] = new monaco.Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]);
}
(this.editor.getControl() as unknown as StandaloneCodeEditor).setDecorationsByTypeFast(key, ranges);
}
private static toMonacoSelections(selection: Selection): monaco.Selection {
return new monaco.Selection(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn);
}
private isSimpleWidget(model: monaco.editor.IModel | ITextModel): model is ITextModel {
return !!(model as ITextModel).isForSimpleWidget;
}
}
interface SnippetInsertOptions {
overwriteBefore: number,
overwriteAfter: number,
undoStopBefore: boolean,
undoStopAfter: boolean,
adjustWhitespace: boolean
}
// TODO move to monaco typings!
interface SnippetController2 extends monaco.editor.IEditorContribution {
insert(template: string, options?: Partial<SnippetInsertOptions>): void;
finish(): void;
cancel(): void;
dispose(): void;
prev(): void;
next(): void;
}
export class TextEditorPropertiesMain {
constructor(
readonly selections: monaco.Selection[],
readonly options: TextEditorConfiguration,
readonly visibleRanges: monaco.Range[]
) {
}
generateDelta(old: TextEditorPropertiesMain | undefined, source: string | undefined): EditorChangedPropertiesData | undefined {
const result: EditorChangedPropertiesData = {
options: undefined,
selections: undefined,
visibleRanges: undefined
};
if (!old || !TextEditorPropertiesMain.selectionsEqual(old.selections, this.selections)) {
result.selections = {
selections: this.selections,
source: source
};
}
if (!old || !TextEditorPropertiesMain.optionsEqual(old.options, this.options)) {
result.options = this.options;
}
if (!old || !TextEditorPropertiesMain.rangesEqual(old.visibleRanges, this.visibleRanges)) {
result.visibleRanges = this.visibleRanges;
}
if (result.selections || result.visibleRanges || result.options) {
return result;
}
return undefined;
}
static readFromEditor(prevProperties: TextEditorPropertiesMain | undefined,
model: monaco.editor.IModel | ITextModel,
editor: MonacoEditor | SimpleMonacoEditor): TextEditorPropertiesMain {
const selections = TextEditorPropertiesMain.getSelectionsFromEditor(prevProperties, editor);
const options = TextEditorPropertiesMain.getOptionsFromEditor(prevProperties, model, editor);
const visibleRanges = TextEditorPropertiesMain.getVisibleRangesFromEditor(prevProperties, editor);
return new TextEditorPropertiesMain(selections, options, visibleRanges);
}
private static getSelectionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor | SimpleMonacoEditor): monaco.Selection[] {
let result: monaco.Selection[] | undefined = undefined;
if (editor && editor instanceof MonacoEditor) {
result = editor.getControl().getSelections() || undefined;
} else if (editor && editor instanceof SimpleMonacoEditor) {
result = editor.getControl().getSelections()?.map(selection => {
const monacoSelection = new monaco.Selection(
selection.selectionStartLineNumber,
selection.selectionStartColumn,
selection.positionLineNumber,
selection.positionColumn);
monacoSelection.setStartPosition(selection.startLineNumber, selection.startColumn);
monacoSelection.setEndPosition(selection.endLineNumber, selection.endColumn);
return monacoSelection;
});
}
if (!result && prevProperties) {
result = prevProperties.selections;
}
if (!result) {
result = [new monaco.Selection(1, 1, 1, 1)];
}
return result;
}
private static getOptionsFromEditor(prevProperties: TextEditorPropertiesMain | undefined,
model: monaco.editor.IModel | ITextModel,
editor: MonacoEditor | SimpleMonacoEditor): TextEditorConfiguration {
if (model.isDisposed()) {
return prevProperties!.options;
}
let cursorStyle: TextEditorCursorStyle;
let lineNumbers: TextEditorLineNumbersStyle;
if (editor && editor instanceof MonacoEditor) {
const editorOptions = editor.getControl().getOptions();
const lineNumbersOpts = editorOptions.get(monaco.editor.EditorOption.lineNumbers);
cursorStyle = editorOptions.get(monaco.editor.EditorOption.cursorStyle);
switch (lineNumbersOpts.renderType) {
case monaco.editor.RenderLineNumbersType.Off:
lineNumbers = TextEditorLineNumbersStyle.Off;
break;
case monaco.editor.RenderLineNumbersType.Relative:
lineNumbers = TextEditorLineNumbersStyle.Relative;
break;
case monaco.editor.RenderLineNumbersType.Interval:
lineNumbers = TextEditorLineNumbersStyle.Interval;
break;
default:
lineNumbers = TextEditorLineNumbersStyle.On;
break;
}
} else if (editor && editor instanceof SimpleMonacoEditor) {
const editorOptions = editor.getControl().getOptions();
const lineNumbersOpts = editorOptions.get(EditorOption.lineNumbers);
cursorStyle = editorOptions.get(EditorOption.cursorStyle);
switch (lineNumbersOpts.renderType) {
case RenderLineNumbersType.Off:
lineNumbers = TextEditorLineNumbersStyle.Off;
break;
case RenderLineNumbersType.Relative:
lineNumbers = TextEditorLineNumbersStyle.Relative;
break;
case RenderLineNumbersType.Interval:
lineNumbers = TextEditorLineNumbersStyle.Interval;
break;
default:
lineNumbers = TextEditorLineNumbersStyle.On;
break;
}
} else if (prevProperties) {
cursorStyle = prevProperties.options.cursorStyle;
lineNumbers = prevProperties.options.lineNumbers;
} else {
cursorStyle = TextEditorCursorStyle.Line;
lineNumbers = TextEditorLineNumbersStyle.On;
}
const modelOptions = model.getOptions();
return {
insertSpaces: modelOptions.insertSpaces,
indentSize: modelOptions.indentSize,
tabSize: modelOptions.tabSize,
cursorStyle,
lineNumbers,
};
}
private static getVisibleRangesFromEditor(prevProperties: TextEditorPropertiesMain | undefined, editor: MonacoEditor | SimpleMonacoEditor): monaco.Range[] {
if (editor) {
return editor.getControl().getVisibleRanges();
}
return [];
}
private static selectionsEqual(a: monaco.Selection[], b: monaco.Selection[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!a[i].equalsSelection(b[i])) {
return false;
}
}
return true;
}
private static optionsEqual(a: TextEditorConfiguration, b: TextEditorConfiguration): boolean {
if (a && !b || !a && b) {
return false;
}
if (!a && !b) {
return true;
}
return (
a.tabSize === b.tabSize
&& a.insertSpaces === b.insertSpaces
&& a.indentSize === b.indentSize
&& a.cursorStyle === b.cursorStyle
&& a.lineNumbers === b.lineNumbers
);
}
private static rangesEqual(a: monaco.Range[], b: monaco.Range[]): boolean {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!a[i].equalsRange(b[i])) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,118 @@
// *****************************************************************************
// 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 { Event, Emitter, ListenerList, Listener } from '@theia/core';
import { MonacoEditorModel, WillSaveMonacoModelEvent } from '@theia/monaco/lib/browser/monaco-editor-model';
import { injectable, inject } from '@theia/core/shared/inversify';
import { MonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service';
import { MonacoWorkspace } from '@theia/monaco/lib/browser/monaco-workspace';
import { Schemes } from '../../common/uri-components';
import URI from '@theia/core/lib/common/uri';
import { Reference } from '@theia/core/lib/common/reference';
@injectable()
export class EditorModelService {
private monacoModelService: MonacoTextModelService;
private modelModeChangedEmitter = new Emitter<{ model: MonacoEditorModel, oldModeId: string }>();
private onModelRemovedEmitter = new Emitter<MonacoEditorModel>();
private modelDirtyEmitter = new Emitter<MonacoEditorModel>();
private modelEncodingEmitter = new Emitter<{ model: MonacoEditorModel, encoding: string }>();
private modelSavedEmitter = new Emitter<MonacoEditorModel>();
private onModelWillSaveListeners: ListenerList<WillSaveMonacoModelEvent, Promise<void>> = new ListenerList();
readonly onModelDirtyChanged = this.modelDirtyEmitter.event;
readonly onModelEncodingChanged = this.modelEncodingEmitter.event;
readonly onModelWillSave = this.onModelWillSaveListeners.registration;
readonly onModelSaved = this.modelSavedEmitter.event;
readonly onModelModeChanged = this.modelModeChangedEmitter.event;
readonly onModelRemoved = this.onModelRemovedEmitter.event;
constructor(@inject(MonacoTextModelService) monacoModelService: MonacoTextModelService,
@inject(MonacoWorkspace) monacoWorkspace: MonacoWorkspace) {
this.monacoModelService = monacoModelService;
monacoModelService.models.forEach(model => this.modelCreated(model));
monacoModelService.onDidCreate(this.modelCreated, this);
monacoWorkspace.onDidCloseTextDocument(model => {
setTimeout(() => {
this.onModelRemovedEmitter.fire(model);
}, 1);
});
}
private modelCreated(model: MonacoEditorModel): void {
model.textEditorModel.onDidChangeLanguage(e => {
this.modelModeChangedEmitter.fire({ model, oldModeId: e.oldLanguage });
});
model.onDidSaveModel(_ => {
this.modelSavedEmitter.fire(model);
});
model.onModelWillSaveModel(async (e: WillSaveMonacoModelEvent) => {
await Listener.awaitAll(e, this.onModelWillSaveListeners);
});
model.onDirtyChanged(_ => {
this.modelDirtyEmitter.fire(model);
});
model.onDidChangeEncoding(encoding => {
this.modelEncodingEmitter.fire({ model, encoding });
});
}
get onModelAdded(): Event<MonacoEditorModel> {
return this.monacoModelService.onDidCreate;
}
getModels(): MonacoEditorModel[] {
return this.monacoModelService.models;
}
async save(uri: URI): Promise<boolean> {
const model = this.monacoModelService.get(uri.toString());
if (model) {
await model.save();
return true;
}
return false;
}
async saveAll(includeUntitled?: boolean): Promise<boolean> {
const saves = [];
for (const model of this.monacoModelService.models) {
const { uri } = model.textEditorModel;
if (model.dirty && (includeUntitled || uri.scheme !== Schemes.untitled)) {
saves.push((async () => {
try {
await model.save();
return true;
} catch (e) {
console.error('Failed to save ', uri.toString(), e);
return false;
}
})());
}
}
const results = await Promise.all(saves);
return results.reduce((a, b) => a && b, true);
}
async createModelReference(uri: URI): Promise<Reference<MonacoEditorModel>> {
return this.monacoModelService.createModelReference(uri);
}
}

View File

@@ -0,0 +1,239 @@
// *****************************************************************************
// 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 {
TextEditorsMain,
MAIN_RPC_CONTEXT,
TextEditorsExt,
TextEditorConfigurationUpdate,
Selection,
TextEditorRevealType,
SingleEditOperation,
ApplyEditsOptions,
DecorationRenderOptions,
ThemeDecorationInstanceRenderOptions,
DecorationOptions,
WorkspaceEditDto,
WorkspaceNotebookCellEditDto,
DocumentsMain,
WorkspaceEditMetadataDto,
SnippetEditOptions,
} from '../../common/plugin-api-rpc';
import { Range, TextDocumentShowOptions } from '../../common/plugin-api-rpc-model';
import { EditorsAndDocumentsMain } from './editors-and-documents-main';
import { RPCProtocol } from '../../common/rpc-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { TextEditorMain } from './text-editor-main';
import { disposed } from '../../common/errors';
import { toMonacoWorkspaceEdit } from './languages-main';
import { MonacoBulkEditService } from '@theia/monaco/lib/browser/monaco-bulk-edit-service';
import { UriComponents } from '../../common/uri-components';
import { Endpoint } from '@theia/core/lib/browser/endpoint';
import * as monaco from '@theia/monaco-editor-core';
import { ResourceEdit } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/bulkEditService';
import { IDecorationRenderOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { ICodeEditorService } from '@theia/monaco-editor-core/esm/vs/editor/browser/services/codeEditorService';
import { type ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
import { ArrayUtils, URI } from '@theia/core';
import { toNotebookWorspaceEdit } from './notebooks/notebooks-main';
import { interfaces } from '@theia/core/shared/inversify';
import { NotebookService } from '@theia/notebook/lib/browser';
export class TextEditorsMainImpl implements TextEditorsMain, Disposable {
private readonly proxy: TextEditorsExt;
private readonly toDispose = new DisposableCollection();
private readonly editorsToDispose = new Map<string, DisposableCollection>();
private readonly fileEndpoint = new Endpoint({ path: 'file' }).getRestUrl();
private readonly bulkEditService: MonacoBulkEditService;
private readonly notebookService: NotebookService;
constructor(
private readonly editorsAndDocuments: EditorsAndDocumentsMain,
private readonly documents: DocumentsMain,
rpc: RPCProtocol,
container: interfaces.Container
) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TEXT_EDITORS_EXT);
this.bulkEditService = container.get(MonacoBulkEditService);
this.notebookService = container.get(NotebookService);
this.toDispose.push(editorsAndDocuments);
this.toDispose.push(editorsAndDocuments.onTextEditorAdd(editors => editors.forEach(this.onTextEditorAdd, this)));
this.toDispose.push(editorsAndDocuments.onTextEditorRemove(editors => editors.forEach(this.onTextEditorRemove, this)));
}
dispose(): void {
this.toDispose.dispose();
}
private onTextEditorAdd(editor: TextEditorMain): void {
const id = editor.getId();
const toDispose = new DisposableCollection(
editor.onPropertiesChangedEvent(e => {
this.proxy.$acceptEditorPropertiesChanged(id, e);
}),
Disposable.create(() => this.editorsToDispose.delete(id))
);
this.editorsToDispose.set(id, toDispose);
this.toDispose.push(toDispose);
}
private onTextEditorRemove(id: string): void {
const disposables = this.editorsToDispose.get(id);
if (disposables) {
disposables.dispose();
}
}
$tryShowTextDocument(uri: UriComponents, options?: TextDocumentShowOptions): Promise<void> {
return this.documents.$tryShowDocument(uri, options);
}
$trySetOptions(id: string, options: TextEditorConfigurationUpdate): Promise<void> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor: ${id}`));
}
this.editorsAndDocuments.getEditor(id)!.setConfiguration(options);
return Promise.resolve();
}
$trySetSelections(id: string, selections: Selection[]): Promise<void> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor: ${id}`));
}
this.editorsAndDocuments.getEditor(id)!.setSelections(selections);
return Promise.resolve();
}
$tryRevealRange(id: string, range: Range, revealType: TextEditorRevealType): Promise<void> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
this.editorsAndDocuments.getEditor(id)!.revealRange(new monaco.Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn), revealType);
return Promise.resolve();
}
$tryApplyEdits(id: string, modelVersionId: number, edits: SingleEditOperation[], opts: ApplyEditsOptions): Promise<boolean> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
return Promise.resolve(this.editorsAndDocuments.getEditor(id)!.applyEdits(modelVersionId, edits, opts));
}
async $tryApplyWorkspaceEdit(dto: WorkspaceEditDto, metadata?: WorkspaceEditMetadataDto): Promise<boolean> {
const [notebookEdits, monacoEdits] = ArrayUtils.partition(dto.edits, edit => WorkspaceNotebookCellEditDto.is(edit));
try {
if (notebookEdits.length > 0) {
const workspaceEdit = toNotebookWorspaceEdit({ edits: notebookEdits });
return this.notebookService.applyWorkspaceEdit(workspaceEdit);
}
if (monacoEdits.length > 0) {
const workspaceEdit = toMonacoWorkspaceEdit({ edits: monacoEdits });
const edits = ResourceEdit.convert(workspaceEdit);
const { isApplied } = await this.bulkEditService.apply(edits, { respectAutoSaveConfig: metadata?.isRefactoring });
return isApplied;
}
return false;
} catch {
return false;
}
}
$tryInsertSnippet(id: string, template: string, ranges: Range[], opts: SnippetEditOptions): Promise<boolean> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
return Promise.resolve(this.editorsAndDocuments.getEditor(id)!.insertSnippet(template, ranges, opts));
}
$registerTextEditorDecorationType(key: string, options: DecorationRenderOptions): void {
this.injectRemoteUris(options);
StandaloneServices.get(ICodeEditorService).registerDecorationType('Plugin decoration', key, options as IDecorationRenderOptions);
this.toDispose.push(Disposable.create(() => this.$removeTextEditorDecorationType(key)));
}
protected injectRemoteUris(options: DecorationRenderOptions | ThemeDecorationInstanceRenderOptions): void {
if (options.before) {
options.before.contentIconPath = this.toRemoteUri(options.before.contentIconPath);
}
if (options.after) {
options.after.contentIconPath = this.toRemoteUri(options.after.contentIconPath);
}
if ('gutterIconPath' in options) {
options.gutterIconPath = this.toRemoteUri(options.gutterIconPath);
}
if ('dark' in options && options.dark) {
this.injectRemoteUris(options.dark);
}
if ('light' in options && options.light) {
this.injectRemoteUris(options.light);
}
}
protected toRemoteUri(uri?: UriComponents): UriComponents | undefined {
if (uri && uri.scheme === 'file') {
return this.fileEndpoint.withQuery(URI.fromComponents(uri).toString()).toComponents();
}
return uri;
}
$removeTextEditorDecorationType(key: string): void {
StandaloneServices.get(ICodeEditorService).removeDecorationType(key);
}
$tryHideEditor(id: string): Promise<void> {
return this.editorsAndDocuments.hideEditor(id);
}
$trySetDecorations(id: string, key: string, ranges: DecorationOptions[]): Promise<void> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
this.editorsAndDocuments.getEditor(id)!.setDecorations(key, ranges);
return Promise.resolve();
}
$trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise<void> {
if (!this.editorsAndDocuments.getEditor(id)) {
return Promise.reject(disposed(`TextEditor(${id})`));
}
this.editorsAndDocuments.getEditor(id)!.setDecorationsFast(key, ranges);
return Promise.resolve();
}
$save(uri: UriComponents): PromiseLike<UriComponents | undefined> {
return this.editorsAndDocuments.save(URI.fromComponents(uri)).then(u => u?.toComponents());
}
$saveAs(uri: UriComponents): PromiseLike<UriComponents | undefined> {
return this.editorsAndDocuments.saveAs(URI.fromComponents(uri)).then(u => u?.toComponents());
}
$saveAll(includeUntitled?: boolean): Promise<boolean> {
return this.editorsAndDocuments.saveAll(includeUntitled);
}
$getDiffInformation(id: string): Promise<ILineChange[]> {
return Promise.resolve(this.editorsAndDocuments.getDiffInformation(id));
}
}

View File

@@ -0,0 +1,246 @@
// *****************************************************************************
// 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 { getIconRegistry } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/iconRegistry';
// @monaco-uplift
// Keep this up-to-date with the table at https://code.visualstudio.com/api/references/icons-in-labels#icon-listing
const codeIconMap: Record<string, string> = {
'accounts-view-bar-icon': 'account',
'breakpoints-activate': 'activate-breakpoints',
'breakpoints-remove-all': 'close-all',
'breakpoints-view-icon': 'debug-alt',
'callhierarchy-incoming': 'call-incoming',
'callhierarchy-outgoing': 'call-outgoing',
'callstack-view-icon': 'debug-alt',
'callstack-view-session': 'bug',
'chat-editor-label-icon': 'comment-discussion',
'comments-view-icon': 'comment-discussion',
'debug-breakpoint': 'debug-breakpoint',
'debug-breakpoint-conditional': 'debug-breakpoint-conditional',
'debug-breakpoint-conditional-disabled': 'debug-breakpoint-conditional-disabled',
'debug-breakpoint-conditional-unverified': 'debug-breakpoint-conditional-unverified',
'debug-breakpoint-data': 'debug-breakpoint-data',
'debug-breakpoint-data-disabled': 'debug-breakpoint-data-disabled',
'debug-breakpoint-data-unverified': 'debug-breakpoint-data-unverified',
'debug-breakpoint-disabled': 'debug-breakpoint-disabled',
'debug-breakpoint-function': 'debug-breakpoint-function',
'debug-breakpoint-function-disabled': 'debug-breakpoint-function-disabled',
'debug-breakpoint-function-unverified': 'debug-breakpoint-function-unverified',
'debug-breakpoint-log': 'debug-breakpoint-log',
'debug-breakpoint-log-disabled': 'debug-breakpoint-log-disabled',
'debug-breakpoint-log-unverified': 'debug-breakpoint-log-unverified',
'debug-breakpoint-unsupported': 'debug-breakpoint-unsupported',
'debug-breakpoint-unverified': 'debug-breakpoint-unverified',
'debug-collapse-all': 'collapse-all',
'debug-configure': 'gear',
'debug-console-clear-all': 'clear-all',
'debug-console-evaluation-input': 'arrow-small-right',
'debug-console-evaluation-prompt': 'chevron-right',
'debug-console-view-icon': 'debug-console',
'debug-continue': 'debug-continue',
'debug-disconnect': 'debug-disconnect',
'debug-gripper': 'gripper',
'debug-hint': 'debug-hint',
'debug-pause': 'debug-pause',
'debug-restart': 'debug-restart',
'debug-restart-frame': 'debug-restart-frame',
'debug-reverse-continue': 'debug-reverse-continue',
'debug-stackframe': 'debug-stackframe',
'debug-stackframe-focused': 'debug-stackframe-focused',
'debug-start': 'debug-start',
'debug-step-back': 'debug-step-back',
'debug-step-into': 'debug-step-into',
'debug-step-out': 'debug-step-out',
'debug-step-over': 'debug-step-over',
'debug-stop': 'debug-stop',
'default-view-icon': 'window',
'diff-editor-next-change': 'arrow-down',
'diff-editor-previous-change': 'arrow-up',
'diff-editor-toggle-whitespace': 'whitespace',
'diff-insert': 'add',
'diff-remove': 'remove',
'diff-review-close': 'close',
'diff-review-insert': 'add',
'diff-review-remove': 'remove',
'explorer-view-icon': 'files',
'extensions-clear-search-results': 'clear-all',
'extensions-configure-recommended': 'pencil',
'extensions-filter': 'filter',
'extensions-info-message': 'info',
'extensions-install-count': 'cloud-download',
'extensions-install-local-in-remote': 'cloud-download',
'extensions-install-workspace-recommended': 'cloud-download',
'extensions-manage': 'gear',
'extensions-rating': 'star',
'extensions-refresh': 'refresh',
'extensions-remote': 'remote',
'extensions-star-empty': 'star-empty',
'extensions-star-full': 'star-full',
'extensions-star-half': 'star-half',
'extensions-sync-enabled': 'sync',
'extensions-sync-ignored': 'sync-ignored',
'extensions-view-icon': 'extensions',
'extensions-warning-message': 'warning',
'find-collapsed': 'chevron-right',
'find-expanded': 'chevron-down',
'find-next-match': 'arrow-down',
'find-previous-match': 'arrow-up',
'find-replace': 'replace',
'find-replace-all': 'replace-all',
'find-selection': 'selection',
'folding-collapsed': 'chevron-right',
'folding-expanded': 'chevron-down',
'getting-started-beginner': 'lightbulb',
'getting-started-codespaces': 'github',
'getting-started-item-checked': 'pass-filled',
'getting-started-item-unchecked': 'circle-large-outline',
'getting-started-setup': 'heart',
'goto-next-location': 'arrow-down',
'goto-previous-location': 'arrow-up',
'keybindings-add': 'add',
'keybindings-edit': 'edit',
'keybindings-record-keys': 'record-keys',
'keybindings-sort': 'sort-precedence',
'loaded-scripts-view-icon': 'debug-alt',
'marker-navigation-next': 'chevron-down',
'marker-navigation-previous': 'chevron-up',
'markers-view-filter': 'filter',
'markers-view-icon': 'warning',
'markers-view-multi-line-collapsed': 'chevron-down',
'markers-view-multi-line-expanded': 'chevron-up',
'notebook-clear': 'clear-all',
'notebook-collapsed': 'chevron-right',
'notebook-delete-cell': 'trash',
'notebook-edit': 'pencil',
'notebook-execute': 'play',
'notebook-execute-all': 'run-all',
'notebook-expanded': 'chevron-down',
'notebook-kernel-configure': 'settings-gear',
'notebook-kernel-select': 'server-environment',
'notebook-mimetype': 'code',
'notebook-move-down': 'arrow-down',
'notebook-move-up': 'arrow-up',
'notebook-open-as-text': 'file-code',
'notebook-render-output': 'preview',
'notebook-revert': 'discard',
'notebook-split-cell': 'split-vertical',
'notebook-state-error': 'error',
'notebook-state-success': 'check',
'notebook-stop': 'primitive-square',
'notebook-stop-edit': 'check',
'notebook-unfold': 'unfold',
'notifications-clear': 'close',
'notifications-clear-all': 'clear-all',
'notifications-collapse': 'chevron-down',
'notifications-configure': 'gear',
'notifications-expand': 'chevron-up',
'notifications-hide': 'chevron-down',
'open-editors-view-icon': 'book',
'outline-view-icon': 'symbol-class',
'output-view-icon': 'output',
'panel-close': 'close',
'panel-maximize': 'chevron-up',
'panel-restore': 'chevron-down',
'parameter-hints-next': 'chevron-down',
'parameter-hints-previous': 'chevron-up',
'ports-forward-icon': 'plus',
'ports-open-browser-icon': 'globe',
'ports-stop-forward-icon': 'x',
'ports-view-icon': 'plug',
'preferences-clear-input': 'clear-all',
'preferences-open-settings': 'go-to-file',
'private-ports-view-icon': 'lock',
'public-ports-view-icon': 'eye',
'refactor-preview-view-icon': 'lightbulb',
'remote-explorer-documentation': 'book',
'remote-explorer-feedback': 'twitter',
'remote-explorer-get-started': 'star',
'remote-explorer-report-issues': 'comment',
'remote-explorer-review-issues': 'issues',
'remote-explorer-view-icon': 'remote-explorer',
'review-comment-collapse': 'chevron-up',
'run-view-icon': 'debug-alt',
'runtime-extensions-editor-label-icon': ' extensions',
'search-clear-results': 'clear-all',
'search-collapse-results': 'collapse-all',
'search-details': 'ellipsis',
'search-editor-label-icon': 'search',
'search-expand-results': 'expand-all',
'search-hide-replace': 'chevron-right',
'search-new-editor': 'new-file',
'search-refresh': 'refresh',
'search-remove': 'close',
'search-replace': 'replace',
'search-replace-all': 'replace-all',
'search-show-context': 'list-selection',
'search-show-replace': 'chevron-down',
'search-stop': 'search-stop',
'search-view-icon': 'search',
'settings-add': 'add',
'settings-discard': 'discard',
'settings-edit': 'edit',
'settings-editor-label-icon': 'settings',
'settings-folder-dropdown': 'triangle-down',
'settings-group-collapsed': 'chevron-right',
'settings-group-expanded': 'chevron-down',
'settings-more-action': 'gear',
'settings-remove': 'close',
'settings-sync-view-icon': 'sync',
'settings-view-bar-icon': 'settings-gear',
'source-control-view-icon': 'source-control',
'suggest-more-info': 'chevron-right',
'tasks-list-configure': 'gear',
'tasks-remove': 'close',
'terminal-kill': 'trash',
'terminal-new': 'add',
'terminal-rename': 'gear',
'terminal-view-icon': 'terminal',
'test-view-icon': 'beaker',
'testing-cancel-icon': 'close',
'testing-debug-icon': 'debug-alt',
'testing-error-icon': 'warning',
'testing-failed-icon': 'close',
'testing-passed-icon': 'pass',
'testing-queued-icon': 'watch',
'testing-run-all-icon': 'run-all',
'testing-run-icon': 'run',
'testing-show-as-list-icon': 'list-tree',
'testing-skipped-icon': 'debug-step-over',
'testing-unset-icon': 'circle-outline',
'timeline-open': 'history',
'timeline-pin': 'pin',
'timeline-refresh': 'refresh',
'timeline-unpin': 'pinned',
'timeline-view-icon': 'history',
'variables-view-icon': 'debug-alt',
'view-pane-container-collapsed': 'chevron-right',
'view-pane-container-expanded': 'chevron-down',
'watch-expressions-add': 'add',
'watch-expressions-add-function-breakpoint': 'add',
'watch-expressions-remove-all': 'close-all',
'watch-view-icon': 'debug-alt',
'widget-close': 'close',
'workspace-trust-editor-label-icon': ' shield'
};
const registry = getIconRegistry();
for (const key in codeIconMap) {
if (codeIconMap.hasOwnProperty(key)) {
registry.registerIcon(key, { id: codeIconMap[key] }, key);
}
}

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2020 Ericsson and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MAIN_RPC_CONTEXT, ThemingMain, ThemingExt } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { ThemeService } from '@theia/core/lib/browser/theming';
import { Disposable } from '@theia/core/lib/common/disposable';
/*---------------------------------------------------------------------------------------------
* 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/bafca191f55a234fad20ab67bb689aacc80e7a1a/src/vs/workbench/api/browser/mainThreadTheming.ts
export class ThemingMainImpl implements ThemingMain {
private readonly proxy: ThemingExt;
private readonly themeChangeListener: Disposable;
constructor(rpc: RPCProtocol, themeService: ThemeService) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.THEMING_EXT);
this.themeChangeListener = themeService.onDidColorThemeChange(e => this.proxy.$onColorThemeChange(e.newTheme.type));
this.proxy.$onColorThemeChange(themeService.getCurrentTheme().type);
}
dispose(): void {
this.themeChangeListener.dispose();
}
}

View File

@@ -0,0 +1,80 @@
// *****************************************************************************
// 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 { MAIN_RPC_CONTEXT, TimelineExt, TimelineMain } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { TimelineService } from '@theia/timeline/lib/browser/timeline-service';
import { Emitter } from '@theia/core/lib/common';
import { URI } from '@theia/core/shared/vscode-uri';
import {
InternalTimelineOptions,
Timeline,
TimelineOptions,
TimelineProviderDescriptor,
TimelineChangeEvent
} from '@theia/timeline/lib/common/timeline-model';
/*---------------------------------------------------------------------------------------------
* 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/afacd2bdfe7060f09df9b9139521718915949757/src/vs/workbench/api/browser/mainThreadTimeline.ts
export class TimelineMainImpl implements TimelineMain {
private readonly proxy: TimelineExt;
private readonly timelineService: TimelineService;
private readonly providerEmitters = new Map<string, Emitter<TimelineChangeEvent>>();
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TIMELINE_EXT);
this.timelineService = container.get<TimelineService>(TimelineService);
}
async $registerTimelineProvider(provider: TimelineProviderDescriptor): Promise<void> {
const proxy = this.proxy;
const emitters = this.providerEmitters;
let onDidChange = emitters.get(provider.id);
if (onDidChange === undefined) {
onDidChange = new Emitter<TimelineChangeEvent>();
emitters.set(provider.id, onDidChange);
}
this.timelineService.registerTimelineProvider({
...provider,
onDidChange: onDidChange.event,
provideTimeline(uri: URI, options: TimelineOptions, internalOptions?: InternalTimelineOptions): Promise<Timeline | undefined> {
return proxy.$getTimeline(provider.id, uri, options, internalOptions);
},
dispose(): void {
emitters.delete(provider.id);
if (onDidChange) {
onDidChange.dispose();
}
}
});
}
async $unregisterTimelineProvider(id: string): Promise<void> {
this.timelineService.unregisterTimelineProvider(id);
}
async $fireTimelineChanged(e: TimelineChangeEvent): Promise<void> {
const emitter = this.providerEmitters.get(e.id);
if (emitter) {
emitter.fire(e);
}
}
}

View File

@@ -0,0 +1,72 @@
// *****************************************************************************
// Copyright (C) 2024 STMicroelectronics.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// 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, URI } from '@theia/core';
import { MAIN_RPC_CONTEXT, UriExt, UriMain } from '../../common';
import { RPCProtocol } from '../../common/rpc-protocol';
import { interfaces } from '@theia/core/shared/inversify';
import { OpenHandler, OpenerOptions, OpenerService } from '@theia/core/lib/browser';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
export class UriMainImpl implements UriMain, Disposable {
private readonly proxy: UriExt;
private handlers = new Set<string>();
private readonly openerService: OpenerService;
private readonly pluginSupport: HostedPluginSupport;
private readonly openHandler: OpenHandler;
constructor(rpc: RPCProtocol, container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.URI_EXT);
this.openerService = container.get(OpenerService);
this.pluginSupport = container.get(HostedPluginSupport);
this.openHandler = {
id: 'theia-plugin-open-handler',
canHandle: async (uri: URI, options?: OpenerOptions | undefined): Promise<number> => {
if (uri.scheme !== FrontendApplicationConfigProvider.get().electron.uriScheme) {
return 0;
}
await this.pluginSupport.activateByUri(uri.scheme, uri.authority);
if (this.handlers.has(uri.authority)) {
return 500;
}
return 0;
},
open: async (uri: URI, options?: OpenerOptions | undefined): Promise<undefined> => {
if (!this.handlers.has(uri.authority)) {
throw new Error(`No plugin to handle this uri: : '${uri}'`);
}
this.proxy.$handleExternalUri(uri.toComponents());
}
};
this.openerService.addHandler?.(this.openHandler);
}
dispose(): void {
this.openerService.removeHandler?.(this.openHandler);
this.handlers.clear();
}
async $registerUriHandler(pluginId: string, extensionDisplayName: string): Promise<void> {
this.handlers.add(pluginId);
}
async $unregisterUriHandler(pluginId: string): Promise<void> {
this.handlers.delete(pluginId);
}
}

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2022 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
@injectable()
export class DnDFileContentStore {
private static id: number = 0;
private files: Map<string, File> = new Map();
addFile(f: File): string {
const id = (DnDFileContentStore.id++).toString();
this.files.set(id, f);
return id;
}
removeFile(id: string): boolean {
return this.files.delete(id);
}
getFile(id: string): File {
const file = this.files.get(id);
if (file) {
return file;
}
throw new Error(`File with id ${id} not found in dnd operation`);
}
}

View File

@@ -0,0 +1,81 @@
// *****************************************************************************
// 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, inject } from '@theia/core/shared/inversify';
import URI from '@theia/core/lib/common/uri';
import { LabelProviderContribution, LabelProvider, URIIconReference } from '@theia/core/lib/browser/label-provider';
import { TreeLabelProvider } from '@theia/core/lib/browser/tree/tree-label-provider';
import { TreeViewNode } from './tree-view-widget';
import { TreeNode } from '@theia/core/lib/browser/tree/tree';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
@injectable()
export class PluginTreeViewNodeLabelProvider implements LabelProviderContribution {
@inject(LabelProvider)
protected readonly labelProvider: LabelProvider;
@inject(TreeLabelProvider)
protected readonly treeLabelProvider: TreeLabelProvider;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
canHandle(element: TreeViewNode | any): number {
if (TreeNode.is(element) && ('resourceUri' in element || 'themeIcon' in element)) {
return Number.MAX_SAFE_INTEGER - 512;
}
return 0;
}
getIcon(node: TreeViewNode): string | undefined {
if (node.icon) {
return node.icon;
}
if (node.themeIcon) {
if (node.themeIcon.id === 'file' || node.themeIcon.id === 'folder') {
const uri = node.resourceUri && new URI(node.resourceUri) || undefined;
if (uri) {
return this.labelProvider.getIcon(URIIconReference.create(node.themeIcon.id, uri));
}
}
return ThemeIcon.asClassName(node.themeIcon);
}
if (node.resourceUri) {
return this.labelProvider.getIcon(new URI(node.resourceUri));
}
return undefined;
}
getName(node: TreeViewNode): string | undefined {
if (node.name) {
return node.name;
}
if (node.resourceUri) {
return this.labelProvider.getName(new URI(node.resourceUri));
}
return undefined;
}
getLongName(node: TreeViewNode): string | undefined {
if (typeof node.description === 'string') {
return node.description;
}
if (node.description === true && node.resourceUri) {
return this.labelProvider.getLongName(new URI(node.resourceUri));
}
return undefined;
}
}

View File

@@ -0,0 +1,963 @@
// *****************************************************************************
// 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, postConstruct, optional } from '@theia/core/shared/inversify';
import {
ApplicationShell, ViewContainer as ViewContainerWidget, WidgetManager, QuickViewService,
ViewContainerIdentifier, ViewContainerTitleOptions, Widget, FrontendApplicationContribution,
StatefulWidget, CommonMenus, TreeViewWelcomeWidget, ViewContainerPart, BaseWidget,
} from '@theia/core/lib/browser';
import { ViewContainer, View, ViewWelcome, PluginViewType } from '../../../common';
import { PluginSharedStyle } from '../plugin-shared-style';
import { DebugWidget } from '@theia/debug/lib/browser/view/debug-widget';
import { PluginViewWidget, PluginViewWidgetIdentifier } from './plugin-view-widget';
import { SCM_VIEW_CONTAINER_ID, ScmContribution } from '@theia/scm/lib/browser/scm-contribution';
import { EXPLORER_VIEW_CONTAINER_ID, FileNavigatorWidget, FILE_NAVIGATOR_ID } from '@theia/navigator/lib/browser';
import { FileNavigatorContribution } from '@theia/navigator/lib/browser/navigator-contribution';
import { DebugFrontendApplicationContribution } from '@theia/debug/lib/browser/debug-frontend-application-contribution';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { ViewContextKeyService } from './view-context-key-service';
import { PROBLEMS_WIDGET_ID } from '@theia/markers/lib/browser/problem/problem-widget';
import { OutputWidget } from '@theia/output/lib/browser/output-widget';
import { DebugConsoleContribution } from '@theia/debug/lib/browser/console/debug-console-contribution';
import { TreeViewWidget } from './tree-view-widget';
import { SEARCH_VIEW_CONTAINER_ID } from '@theia/search-in-workspace/lib/browser/search-in-workspace-factory';
import { TEST_VIEW_CONTAINER_ID } from '@theia/test/lib/browser/view/test-view-contribution';
import { WebviewView, WebviewViewResolver } from '../webview-views/webview-views';
import { WebviewWidget, WebviewWidgetIdentifier } from '../webview/webview';
import { CancellationToken } from '@theia/core/lib/common/cancellation';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { nls } from '@theia/core';
import { TheiaDockPanel } from '@theia/core/lib/browser/shell/theia-dock-panel';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables';
export const PLUGIN_VIEW_FACTORY_ID = 'plugin-view';
export const PLUGIN_VIEW_CONTAINER_FACTORY_ID = 'plugin-view-container';
export const PLUGIN_VIEW_DATA_FACTORY_ID = 'plugin-view-data';
export type ViewDataProvider = (params: { state?: object, viewInfo: View }) => Promise<TreeViewWidget>;
export interface ViewContainerInfo {
id: string
location: string
options: ViewContainerTitleOptions
onViewAdded: () => void
}
@injectable()
export class PluginViewRegistry implements FrontendApplicationContribution {
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
@inject(PluginSharedStyle)
protected readonly style: PluginSharedStyle;
@inject(WidgetManager)
protected readonly widgetManager: WidgetManager;
@inject(ScmContribution)
protected readonly scm: ScmContribution;
@inject(FileNavigatorContribution)
protected readonly explorer: FileNavigatorContribution;
@inject(DebugFrontendApplicationContribution)
protected readonly debug: DebugFrontendApplicationContribution;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(MenuModelRegistry)
protected readonly menus: MenuModelRegistry;
@inject(QuickViewService) @optional()
protected readonly quickView: QuickViewService;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(ViewContextKeyService)
protected readonly viewContextKeys: ViewContextKeyService;
protected readonly onDidExpandViewEmitter = new Emitter<string>();
readonly onDidExpandView = this.onDidExpandViewEmitter.event;
private readonly views = new Map<string, [string, View]>();
private readonly viewsWelcome = new Map<string, ViewWelcome[]>();
private readonly viewContainers = new Map<string, ViewContainerInfo>();
private readonly containerViews = new Map<string, string[]>();
private readonly viewClauseContexts = new Map<string, Set<string> | undefined>();
private readonly viewDataProviders = new Map<string, ViewDataProvider>();
private readonly viewDataState = new Map<string, object>();
private readonly webviewViewResolvers = new Map<string, WebviewViewResolver>();
protected readonly onNewResolverRegisteredEmitter = new Emitter<{ readonly viewType: string }>();
readonly onNewResolverRegistered = this.onNewResolverRegisteredEmitter.event;
private readonly webviewViewRevivals = new Map<string, { readonly webview: WebviewView; readonly revival: Deferred<void> }>();
private nextViewContainerId = 0;
private static readonly BUILTIN_VIEW_CONTAINERS = new Set<string>([
'explorer',
'scm',
'search',
'test',
'debug'
]);
private static readonly ID_MAPPINGS: Map<string, string> = new Map([
// VS Code Viewlets
[EXPLORER_VIEW_CONTAINER_ID, 'workbench.view.explorer'],
[SCM_VIEW_CONTAINER_ID, 'workbench.view.scm'],
[SEARCH_VIEW_CONTAINER_ID, 'workbench.view.search'],
[DebugWidget.ID, 'workbench.view.debug'],
['vsx-extensions-view-container', 'workbench.view.extensions'], // cannot use the id from 'vsx-registry' package because of circular dependency
[PROBLEMS_WIDGET_ID, 'workbench.panel.markers'],
[TEST_VIEW_CONTAINER_ID, 'workbench.view.testing'],
[OutputWidget.ID, 'workbench.panel.output'],
[DebugConsoleContribution.options.id, 'workbench.panel.repl'],
// Theia does not have a single terminal widget, but instead each terminal gets its own widget. Therefore "the terminal widget is active" doesn't make sense in Theia
// [TERMINAL_WIDGET_FACTORY_ID, 'workbench.panel.terminal'],
// [?? , 'workbench.panel.comments'] not sure what this mean: we don't show comments in sidebars nor the bottom
]);
@postConstruct()
protected init(): void {
// TODO workbench.panel.comments - Theia does not have a proper comments view yet
this.updateFocusedView();
this.shell.onDidChangeActiveWidget(() => this.updateFocusedView());
this.widgetManager.onWillCreateWidget(({ factoryId, widget, waitUntil }) => {
if (factoryId === EXPLORER_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) {
waitUntil(this.prepareViewContainer('explorer', widget));
}
if (factoryId === SCM_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) {
waitUntil(this.prepareViewContainer('scm', widget));
}
if (factoryId === SEARCH_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) {
waitUntil(this.prepareViewContainer('search', widget));
}
if (factoryId === TEST_VIEW_CONTAINER_ID && widget instanceof ViewContainerWidget) {
waitUntil(this.prepareViewContainer('test', widget));
}
if (factoryId === DebugWidget.ID && widget instanceof DebugWidget) {
const viewContainer = widget['sessionWidget']['viewContainer'];
waitUntil(this.prepareViewContainer('debug', viewContainer));
}
if (factoryId === PLUGIN_VIEW_CONTAINER_FACTORY_ID && widget instanceof ViewContainerWidget) {
waitUntil(this.prepareViewContainer(this.toViewContainerId(widget.options), widget));
}
if (factoryId === PLUGIN_VIEW_FACTORY_ID && widget instanceof PluginViewWidget) {
waitUntil(this.prepareView(widget));
}
});
this.widgetManager.onDidCreateWidget(event => {
if (event.widget instanceof FileNavigatorWidget) {
const disposable = new DisposableCollection();
disposable.push(this.registerViewWelcome({
view: 'explorer',
content: nls.localizeByDefault(
'You have not yet opened a folder.\n{0}',
`[${nls.localizeByDefault('Open Folder')}](command:workbench.action.files.openFolder)`
),
order: 0
}));
disposable.push(event.widget.onDidDispose(() => disposable.dispose()));
}
});
this.contextKeyService.onDidChange(e => {
for (const [, view] of this.views.values()) {
const clauseContext = this.viewClauseContexts.get(view.id);
if (clauseContext && e.affects(clauseContext)) {
this.updateViewVisibility(view.id);
}
}
for (const [viewId, viewWelcomes] of this.viewsWelcome) {
for (const [index] of viewWelcomes.entries()) {
const viewWelcomeId = this.toViewWelcomeId(index, viewId);
const clauseContext = this.viewClauseContexts.get(viewWelcomeId);
if (clauseContext && e.affects(clauseContext)) {
this.updateViewWelcomeVisibility(viewId);
}
}
}
});
const hookDockPanelKey = (panel: TheiaDockPanel, key: ContextKey<string>) => {
let toDisposeOnActivate = new DisposableCollection();
panel.onDidChangeCurrent(title => {
toDisposeOnActivate.dispose();
toDisposeOnActivate = new DisposableCollection();
if (title && title.owner instanceof BaseWidget) {
const widget = title.owner;
let value = PluginViewRegistry.ID_MAPPINGS.get(widget.id);
if (!value) {
if (widget.id.startsWith(PLUGIN_VIEW_CONTAINER_FACTORY_ID)) {
value = this.toViewContainerId({ id: widget.id });
}
}
const setKey = () => {
if (widget.isVisible && value) {
key.set(value);
} else {
key.reset();
}
};
toDisposeOnActivate.push(widget.onDidChangeVisibility(() => {
setKey();
}));
setKey();
}
});
};
this.shell.initialized.then(() => {
hookDockPanelKey(this.shell.leftPanelHandler.dockPanel, this.viewContextKeys.activeViewlet);
hookDockPanelKey(this.shell.rightPanelHandler.dockPanel, this.viewContextKeys.activeAuxiliary);
hookDockPanelKey(this.shell.bottomPanel, this.viewContextKeys.activePanel);
});
}
protected async updateViewWelcomeVisibility(viewId: string): Promise<void> {
const widget = await this.getTreeViewWelcomeWidget(viewId);
if (widget) {
widget.handleWelcomeContextChange();
}
}
protected async updateViewVisibility(viewId: string): Promise<void> {
const widget = await this.getView(viewId);
if (!widget) {
if (this.isViewVisible(viewId)) {
await this.openView(viewId);
}
return;
}
const viewInfo = this.views.get(viewId);
if (!viewInfo) {
return;
}
const [viewContainerId] = viewInfo;
const viewContainer = await this.getPluginViewContainer(viewContainerId);
if (!viewContainer) {
return;
}
const part = viewContainer.getPartFor(widget);
if (!part) {
return;
}
widget.updateViewVisibility(() =>
part.setHidden(!this.isViewVisible(viewId))
);
}
protected isViewVisible(viewId: string): boolean {
const viewInfo = this.views.get(viewId);
if (!viewInfo) {
return false;
}
const [, view] = viewInfo;
return view.when === undefined || view.when === 'true' || this.contextKeyService.match(view.when);
}
registerViewContainer(location: string, viewContainer: ViewContainer): Disposable {
const containerId = `workbench.view.extension.${viewContainer.id}`;
if (this.viewContainers.has(containerId)) {
console.warn('view container such id already registered: ', JSON.stringify(viewContainer));
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
const containerClass = 'theia-plugin-view-container';
let themeIconClass = '';
const iconClass = 'plugin-view-container-icon-' + this.nextViewContainerId++; // having dots in class would not work for css, so we need to generate an id.
if (viewContainer.themeIcon) {
const icon = ThemeIcon.fromString(viewContainer.themeIcon);
if (icon) {
themeIconClass = ThemeIcon.asClassName(icon) ?? '';
}
}
if (!themeIconClass) {
const iconUrl = PluginSharedStyle.toExternalIconUrl(viewContainer.iconUrl);
toDispose.push(this.style.insertRule('.' + containerClass + '.' + iconClass, () => `
mask: url('${iconUrl}') no-repeat 50% 50%;
-webkit-mask: url('${iconUrl}') no-repeat 50% 50%;
`));
}
toDispose.push(this.doRegisterViewContainer(containerId, location, {
label: viewContainer.title,
// The container class automatically sets a mask; if we're using a theme icon, we don't want one.
iconClass: (themeIconClass || containerClass) + ' ' + iconClass,
closeable: true
}));
return toDispose;
}
protected async toggleViewContainer(id: string): Promise<void> {
let widget = await this.getPluginViewContainer(id);
if (widget && widget.isAttached) {
widget.dispose();
} else {
widget = await this.openViewContainer(id);
if (widget) {
this.shell.activateWidget(widget.id);
}
}
}
protected doRegisterViewContainer(id: string, location: string, options: ViewContainerTitleOptions): Disposable {
const toDispose = new DisposableCollection();
toDispose.push(Disposable.create(() => this.viewContainers.delete(id)));
const toggleCommandId = `plugin.view-container.${id}.toggle`;
// Some plugins may register empty view containers.
// We should not register commands for them immediately, as that leads to bad UX.
// Instead, we register commands the first time we add a view to them.
let activate = () => {
toDispose.push(this.commands.registerCommand({
id: toggleCommandId,
category: nls.localizeByDefault('View'),
label: nls.localizeByDefault('Toggle {0}', options.label)
}, {
execute: () => this.toggleViewContainer(id)
}));
toDispose.push(this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
commandId: toggleCommandId,
label: options.label
}));
toDispose.push(this.quickView?.registerItem({
label: options.label,
open: async () => {
const widget = await this.openViewContainer(id);
if (widget) {
this.shell.activateWidget(widget.id);
}
}
}));
toDispose.push(Disposable.create(async () => {
const widget = await this.getPluginViewContainer(id);
if (widget) {
widget.dispose();
}
}));
// Ignore every subsequent activation call
activate = () => { };
};
this.viewContainers.set(id, {
id,
location,
options,
onViewAdded: () => activate()
});
return toDispose;
}
getContainerViews(viewContainerId: string): string[] {
return this.containerViews.get(viewContainerId) || [];
}
registerView(viewContainerId: string, view: View): Disposable {
if (!PluginViewRegistry.BUILTIN_VIEW_CONTAINERS.has(viewContainerId)) {
// if it's not a built-in view container, it must be a contributed view container, see https://github.com/eclipse-theia/theia/issues/13249
viewContainerId = `workbench.view.extension.${viewContainerId}`;
}
if (this.views.has(view.id)) {
console.warn('view with such id already registered: ', JSON.stringify(view));
return Disposable.NULL;
}
const toDispose = new DisposableCollection();
view.when = view.when?.trim();
this.views.set(view.id, [viewContainerId, view]);
toDispose.push(Disposable.create(() => this.views.delete(view.id)));
const containerInfo = this.viewContainers.get(viewContainerId);
if (containerInfo) {
containerInfo.onViewAdded();
}
const containerViews = this.getContainerViews(viewContainerId);
containerViews.push(view.id);
this.containerViews.set(viewContainerId, containerViews);
toDispose.push(Disposable.create(() => {
const index = containerViews.indexOf(view.id);
if (index !== -1) {
containerViews.splice(index, 1);
}
}));
if (view.when && view.when !== 'false' && view.when !== 'true') {
const keys = this.contextKeyService.parseKeys(view.when);
if (keys) {
this.viewClauseContexts.set(view.id, keys);
toDispose.push(Disposable.create(() => this.viewClauseContexts.delete(view.id)));
}
}
toDispose.push(this.quickView?.registerItem({
label: view.name,
when: view.when,
open: () => this.openView(view.id, { activate: true })
}));
toDispose.push(this.commands.registerCommand({ id: `${view.id}.focus` }, {
execute: async () => { await this.openView(view.id, { activate: true }); }
}));
toDispose.push(this.commands.registerCommand({ id: `${view.id}.open` }, {
execute: async () => { await this.openView(view.id, { activate: true }); }
}));
return toDispose;
}
async resolveWebviewView(viewId: string, webview: WebviewView, cancellation: CancellationToken): Promise<void> {
const resolver = this.webviewViewResolvers.get(viewId);
if (resolver) {
return resolver.resolve(webview, cancellation);
}
const pendingRevival = this.webviewViewRevivals.get(viewId);
if (pendingRevival) {
return pendingRevival.revival.promise;
}
const pending = new Deferred<void>();
this.webviewViewRevivals.set(viewId, { webview, revival: pending });
return pending.promise;
}
async registerWebviewView(viewId: string, resolver: WebviewViewResolver): Promise<Disposable> {
if (this.webviewViewResolvers.has(viewId)) {
throw new Error(`View resolver already registered for ${viewId}`);
}
this.webviewViewResolvers.set(viewId, resolver);
this.onNewResolverRegisteredEmitter.fire({ viewType: viewId });
const toDispose = new DisposableCollection(Disposable.create(() => this.webviewViewResolvers.delete(viewId)));
this.initView(viewId, toDispose);
const pendingRevival = this.webviewViewRevivals.get(viewId);
if (pendingRevival) {
resolver.resolve(pendingRevival.webview, CancellationToken.None).then(() => {
this.webviewViewRevivals.delete(viewId);
pendingRevival.revival.resolve();
});
}
return toDispose;
}
protected async createNewWebviewView(viewId: string): Promise<WebviewView> {
const webview = await this.widgetManager.getOrCreateWidget<WebviewWidget>(
WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{
id: generateUuid(),
viewId,
});
webview.setContentOptions({ allowScripts: true });
let _description: string | undefined;
let _resolved = false;
let _pendingResolution: Promise<void> | undefined;
const webviewView: WebviewView = {
webview,
get onDidChangeVisibility(): Event<boolean> { return webview.onDidChangeVisibility; },
get onDidDispose(): Event<void> { return webview.onDidDispose; },
get title(): string | undefined { return webview.title.label; },
set title(value: string | undefined) { webview.title.label = value || ''; },
get description(): string | undefined { return _description; },
set description(value: string | undefined) { _description = value; },
dispose: () => {
_resolved = false;
webview.dispose();
toDispose.dispose();
},
resolve: async () => {
if (_resolved) {
return;
}
if (_pendingResolution) {
return _pendingResolution;
}
_pendingResolution = this.resolveWebviewView(viewId, webviewView, CancellationToken.None).then(() => {
_resolved = true;
_pendingResolution = undefined;
});
return _pendingResolution;
},
show: webview.show
};
const toDispose = this.onNewResolverRegistered(resolver => {
if (resolver.viewType === viewId) {
// Potentially re-activate if we have a new resolver
webviewView.resolve();
}
});
webviewView.resolve();
return webviewView;
}
registerViewWelcome(viewWelcome: ViewWelcome): Disposable {
const toDispose = new DisposableCollection();
const viewsWelcome = this.viewsWelcome.get(viewWelcome.view) || [];
if (viewsWelcome.some(e => e.content === viewWelcome.content)) {
return toDispose;
}
viewsWelcome.push(viewWelcome);
this.viewsWelcome.set(viewWelcome.view, viewsWelcome);
this.handleViewWelcomeChange(viewWelcome.view);
toDispose.push(Disposable.create(() => {
const index = viewsWelcome.indexOf(viewWelcome);
if (index !== -1) {
viewsWelcome.splice(index, 1);
}
this.handleViewWelcomeChange(viewWelcome.view);
}));
if (viewWelcome.when) {
const index = viewsWelcome.indexOf(viewWelcome);
const viewWelcomeId = this.toViewWelcomeId(index, viewWelcome.view);
this.viewClauseContexts.set(viewWelcomeId, this.contextKeyService.parseKeys(viewWelcome.when));
toDispose.push(Disposable.create(() => this.viewClauseContexts.delete(viewWelcomeId)));
}
return toDispose;
}
async handleViewWelcomeChange(viewId: string): Promise<void> {
const widget = await this.getTreeViewWelcomeWidget(viewId);
if (widget) {
widget.handleViewWelcomeContentChange(this.getViewWelcomes(viewId));
}
}
protected async getTreeViewWelcomeWidget(viewId: string): Promise<TreeViewWelcomeWidget | undefined> {
switch (viewId) {
case 'explorer':
return this.widgetManager.getWidget<TreeViewWelcomeWidget>(FILE_NAVIGATOR_ID);
default:
return this.widgetManager.getWidget<TreeViewWelcomeWidget>(PLUGIN_VIEW_DATA_FACTORY_ID, { id: viewId });
}
}
getViewWelcomes(viewId: string): ViewWelcome[] {
return this.viewsWelcome.get(viewId) || [];
}
async getView(viewId: string): Promise<PluginViewWidget | undefined> {
if (!this.views.has(viewId)) {
return undefined;
}
return this.widgetManager.getWidget<PluginViewWidget>(PLUGIN_VIEW_FACTORY_ID, this.toPluginViewWidgetIdentifier(viewId));
}
async openView(viewId: string, options?: { activate?: boolean, reveal?: boolean }): Promise<PluginViewWidget | undefined> {
const view = await this.doOpenView(viewId);
if (view && options) {
if (options.activate === true) {
await this.shell.activateWidget(view.id);
} else if (options.reveal === true) {
await this.shell.revealWidget(view.id);
}
}
return view;
}
protected async doOpenView(viewId: string): Promise<PluginViewWidget | undefined> {
const widget = await this.getView(viewId);
if (widget) {
return widget;
}
const data = this.views.get(viewId);
if (!data) {
return undefined;
}
const [containerId] = data;
await this.openViewContainer(containerId);
return this.getView(viewId);
}
protected async prepareView(widget: PluginViewWidget): Promise<void> {
const data = this.views.get(widget.options.viewId);
if (!data) {
return;
}
const [, view] = data;
if (!widget.title.label) {
widget.title.label = view.name;
}
const currentDataWidget = widget.widgets[0];
const webviewId = currentDataWidget instanceof WebviewWidget ? currentDataWidget.identifier?.id : undefined;
const viewDataWidget = await this.createViewDataWidget(view.id, webviewId);
if (widget.isDisposed) {
viewDataWidget?.dispose();
return;
}
if (currentDataWidget !== viewDataWidget) {
if (currentDataWidget) {
currentDataWidget.dispose();
}
if (viewDataWidget) {
widget.addWidget(viewDataWidget);
}
}
}
protected getOrCreateViewContainerWidget(containerId: string): Promise<ViewContainerWidget> {
const identifier = this.toViewContainerIdentifier(containerId);
return this.widgetManager.getOrCreateWidget<ViewContainerWidget>(PLUGIN_VIEW_CONTAINER_FACTORY_ID, identifier);
}
async openViewContainer(containerId: string): Promise<ViewContainerWidget | undefined> {
if (containerId === 'explorer') {
const widget = await this.explorer.openView();
if (widget.parent instanceof ViewContainerWidget) {
return widget.parent;
}
return undefined;
}
if (containerId === 'scm') {
const widget = await this.scm.openView();
if (widget.parent instanceof ViewContainerWidget) {
return widget.parent;
}
return undefined;
}
if (containerId === 'debug') {
const widget = await this.debug.openView();
return widget['sessionWidget']['viewContainer'];
}
const data = this.viewContainers.get(containerId);
if (!data) {
return undefined;
}
const { location } = data;
const containerWidget = await this.getOrCreateViewContainerWidget(containerId);
if (!containerWidget.isAttached) {
await this.shell.addWidget(containerWidget, {
area: ApplicationShell.isSideArea(location) ? location : 'left',
rank: Number.MAX_SAFE_INTEGER
});
}
return containerWidget;
}
protected async prepareViewContainer(viewContainerId: string, containerWidget: ViewContainerWidget): Promise<void> {
const data = this.viewContainers.get(viewContainerId);
if (data) {
const { options } = data;
containerWidget.setTitleOptions(options);
}
for (const viewId of this.getContainerViews(viewContainerId)) {
const identifier = this.toPluginViewWidgetIdentifier(viewId);
// Keep existing widget in its current container and reregister its part to the plugin view widget events.
const existingWidget = this.widgetManager.tryGetWidget<PluginViewWidget>(PLUGIN_VIEW_FACTORY_ID, identifier);
if (existingWidget && existingWidget.currentViewContainerId) {
const currentContainer = await this.getPluginViewContainer(existingWidget.currentViewContainerId);
if (currentContainer && this.registerWidgetPartEvents(existingWidget, currentContainer)) {
continue;
}
}
const widget = await this.widgetManager.getOrCreateWidget<PluginViewWidget>(PLUGIN_VIEW_FACTORY_ID, identifier);
if (containerWidget.getTrackableWidgets().indexOf(widget) === -1) {
containerWidget.addWidget(widget, {
initiallyCollapsed: !!containerWidget.getParts().length,
initiallyHidden: !this.isViewVisible(viewId)
});
}
this.registerWidgetPartEvents(widget, containerWidget);
}
}
protected registerWidgetPartEvents(widget: PluginViewWidget, containerWidget: ViewContainerWidget): ViewContainerPart | undefined {
const part = containerWidget.getPartFor(widget);
if (part) {
widget.currentViewContainerId = this.getViewContainerId(containerWidget);
part.onDidMove(event => { widget.currentViewContainerId = this.getViewContainerId(event); });
// if a view is explicitly hidden then suppress updating visibility based on `when` closure
part.onDidChangeVisibility(() => widget.suppressUpdateViewVisibility = part.isHidden);
const tryFireOnDidExpandView = () => {
if (widget.widgets.length === 0) {
if (!part.collapsed && part.isVisible) {
const viewId = this.toViewId(widget.options);
this.onDidExpandViewEmitter.fire(viewId);
}
} else {
toFire.dispose();
}
};
const toFire = new DisposableCollection(
part.onCollapsed(tryFireOnDidExpandView),
part.onDidChangeVisibility(tryFireOnDidExpandView)
);
tryFireOnDidExpandView();
return part;
}
};
protected getViewContainerId(container: ViewContainerWidget): string | undefined {
const description = this.widgetManager.getDescription(container);
switch (description?.factoryId) {
case EXPLORER_VIEW_CONTAINER_ID: return 'explorer';
case SCM_VIEW_CONTAINER_ID: return 'scm';
case SEARCH_VIEW_CONTAINER_ID: return 'search';
case TEST_VIEW_CONTAINER_ID: return 'test';
case undefined: return container.parent?.parent instanceof DebugWidget ? 'debug' : container.id;
case PLUGIN_VIEW_CONTAINER_FACTORY_ID: return this.toViewContainerId(description.options);
default: return container.id;
}
}
protected async getPluginViewContainer(viewContainerId: string): Promise<ViewContainerWidget | undefined> {
if (viewContainerId === 'explorer') {
return this.widgetManager.getWidget<ViewContainerWidget>(EXPLORER_VIEW_CONTAINER_ID);
}
if (viewContainerId === 'scm') {
return this.widgetManager.getWidget<ViewContainerWidget>(SCM_VIEW_CONTAINER_ID);
}
if (viewContainerId === 'search') {
return this.widgetManager.getWidget<ViewContainerWidget>(SEARCH_VIEW_CONTAINER_ID);
}
if (viewContainerId === 'test') {
return this.widgetManager.getWidget<ViewContainerWidget>(TEST_VIEW_CONTAINER_ID);
}
if (viewContainerId === 'debug') {
const debug = await this.widgetManager.getWidget(DebugWidget.ID);
if (debug instanceof DebugWidget) {
return debug['sessionWidget']['viewContainer'];
}
}
const identifier = this.toViewContainerIdentifier(viewContainerId);
return this.widgetManager.getWidget<ViewContainerWidget>(PLUGIN_VIEW_CONTAINER_FACTORY_ID, identifier);
}
protected async initViewContainer(containerId: string): Promise<void> {
let viewContainer = await this.getPluginViewContainer(containerId);
if (!viewContainer) {
viewContainer = await this.openViewContainer(containerId);
if (viewContainer && !viewContainer.getParts().filter(part => !part.isHidden).length) {
// close view containers without any visible view parts
viewContainer.dispose();
}
} else {
await this.prepareViewContainer(this.toViewContainerId(viewContainer.options), viewContainer);
}
}
async initWidgets(): Promise<void> {
const promises: Promise<void>[] = [];
for (const id of this.viewContainers.keys()) {
promises.push((async () => {
await this.initViewContainer(id);
})().catch(console.error));
}
promises.push((async () => {
const explorer = await this.widgetManager.getWidget(EXPLORER_VIEW_CONTAINER_ID);
if (explorer instanceof ViewContainerWidget) {
await this.prepareViewContainer('explorer', explorer);
}
})().catch(console.error));
promises.push((async () => {
const scm = await this.widgetManager.getWidget(SCM_VIEW_CONTAINER_ID);
if (scm instanceof ViewContainerWidget) {
await this.prepareViewContainer('scm', scm);
}
})().catch(console.error));
promises.push((async () => {
const search = await this.widgetManager.getWidget(SEARCH_VIEW_CONTAINER_ID);
if (search instanceof ViewContainerWidget) {
await this.prepareViewContainer('search', search);
}
})().catch(console.error));
promises.push((async () => {
const test = await this.widgetManager.getWidget(TEST_VIEW_CONTAINER_ID);
if (test instanceof ViewContainerWidget) {
await this.prepareViewContainer('test', test);
}
})().catch(console.error));
promises.push((async () => {
const debug = await this.widgetManager.getWidget(DebugWidget.ID);
if (debug instanceof DebugWidget) {
const viewContainer = debug['sessionWidget']['viewContainer'];
await this.prepareViewContainer('debug', viewContainer);
}
})().catch(console.error));
await Promise.all(promises);
}
async removeStaleWidgets(): Promise<void> {
const views = this.widgetManager.getWidgets(PLUGIN_VIEW_FACTORY_ID);
for (const view of views) {
if (view instanceof PluginViewWidget) {
const id = this.toViewId(view.options);
if (!this.views.has(id)) {
view.dispose();
}
}
}
const viewContainers = this.widgetManager.getWidgets(PLUGIN_VIEW_CONTAINER_FACTORY_ID);
for (const viewContainer of viewContainers) {
if (viewContainer instanceof ViewContainerWidget) {
const id = this.toViewContainerId(viewContainer.options);
if (!this.viewContainers.has(id)) {
viewContainer.dispose();
}
}
}
}
protected toViewContainerIdentifier(viewContainerId: string): ViewContainerIdentifier {
return { id: PLUGIN_VIEW_CONTAINER_FACTORY_ID + ':' + viewContainerId, progressLocationId: viewContainerId };
}
protected toViewContainerId(identifier: ViewContainerIdentifier): string {
return identifier.id.substring(PLUGIN_VIEW_CONTAINER_FACTORY_ID.length + 1);
}
protected toPluginViewWidgetIdentifier(viewId: string): PluginViewWidgetIdentifier {
return { id: PLUGIN_VIEW_FACTORY_ID + ':' + viewId, viewId };
}
protected toViewId(identifier: PluginViewWidgetIdentifier): string {
return identifier.viewId;
}
protected toViewWelcomeId(index: number, viewId: string): string {
return `view-welcome.${viewId}.${index}`;
}
/**
* retrieve restored layout state from previous user session but close widgets
* widgets should be opened only when view data providers are registered
*/
onDidInitializeLayout(): void {
const widgets = this.widgetManager.getWidgets(PLUGIN_VIEW_DATA_FACTORY_ID);
for (const widget of widgets) {
if (StatefulWidget.is(widget)) {
const state = widget.storeState();
if (state) {
this.viewDataState.set(widget.id, state);
}
}
widget.dispose();
}
}
registerViewDataProvider(viewId: string, provider: ViewDataProvider): Disposable {
if (this.viewDataProviders.has(viewId)) {
console.error(`data provider for '${viewId}' view is already registered`);
return Disposable.NULL;
}
this.viewDataProviders.set(viewId, provider);
const toDispose = new DisposableCollection(Disposable.create(() => {
this.viewDataProviders.delete(viewId);
this.viewDataState.delete(viewId);
}));
this.initView(viewId, toDispose);
return toDispose;
}
protected async initView(viewId: string, toDispose: DisposableCollection): Promise<void> {
const view = await this.getView(viewId);
if (toDispose.disposed) {
return;
}
if (view) {
if (view.isVisible) {
await this.prepareView(view);
} else {
const toDisposeOnDidExpandView = new DisposableCollection(this.onDidExpandView(async id => {
if (id === viewId) {
unsubscribe();
await this.prepareView(view);
}
}));
const unsubscribe = () => toDisposeOnDidExpandView.dispose();
view.disposed.connect(unsubscribe);
toDisposeOnDidExpandView.push(Disposable.create(() => view.disposed.disconnect(unsubscribe)));
toDispose.push(toDisposeOnDidExpandView);
}
}
}
protected async createViewDataWidget(viewId: string, webviewId?: string): Promise<Widget | undefined> {
const view = this.views.get(viewId);
if (view?.[1]?.type === PluginViewType.Webview) {
return this.createWebviewWidget(viewId, webviewId);
}
const provider = this.viewDataProviders.get(viewId);
if (!view || !provider) {
return undefined;
}
const [, viewInfo] = view;
const state = this.viewDataState.get(viewId);
const widget = await provider({ state, viewInfo });
widget.handleViewWelcomeContentChange(this.getViewWelcomes(viewId));
if (StatefulWidget.is(widget)) {
this.storeViewDataStateOnDispose(viewId, widget);
} else {
this.viewDataState.delete(viewId);
}
return widget;
}
protected async createWebviewWidget(viewId: string, webviewId?: string): Promise<Widget | undefined> {
if (!webviewId) {
const webviewView = await this.createNewWebviewView(viewId);
webviewId = webviewView.webview.identifier.id;
}
const webviewWidget = this.widgetManager.getWidget(WebviewWidget.FACTORY_ID, <WebviewWidgetIdentifier>{ id: webviewId, viewId });
return webviewWidget;
}
protected storeViewDataStateOnDispose(viewId: string, widget: Widget & StatefulWidget): void {
const dispose = widget.dispose.bind(widget);
widget.dispose = () => {
const state = widget.storeState();
if (state) {
this.viewDataState.set(viewId, state);
}
dispose();
};
}
protected isVisibleWidget(widget: Widget): boolean {
return !widget.isDisposed && widget.isVisible;
}
protected updateFocusedView(): void {
const widget = this.shell.activeWidget;
if (widget instanceof PluginViewWidget) {
this.viewContextKeys.focusedView.set(widget.options.viewId);
} else {
this.viewContextKeys.focusedView.reset();
}
}
}

View File

@@ -0,0 +1,179 @@
// *****************************************************************************
// 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, inject, postConstruct } from '@theia/core/shared/inversify';
import { Panel, Widget } from '@theia/core/shared/@lumino/widgets';
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
import { CommandRegistry } from '@theia/core/lib/common/command';
import { StatefulWidget } from '@theia/core/lib/browser/shell/shell-layout-restorer';
import { Message } from '@theia/core/shared/@lumino/messaging';
import { TreeViewWidget } from './tree-view-widget';
import { DescriptionWidget, DynamicToolbarWidget } from '@theia/core/lib/browser/view-container';
import { DisposableCollection, Emitter, Event } from '@theia/core/lib/common';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class PluginViewWidgetIdentifier {
id: string;
viewId: string;
}
@injectable()
export class PluginViewWidget extends Panel implements StatefulWidget, DescriptionWidget, DynamicToolbarWidget {
currentViewContainerId?: string;
protected _message?: string;
protected _description: string = '';
protected _suppressUpdateViewVisibility = false;
protected updatingViewVisibility = false;
protected onDidChangeDescriptionEmitter = new Emitter<void>();
protected toDispose = new DisposableCollection(this.onDidChangeDescriptionEmitter);
protected readonly onDidChangeToolbarItemsEmitter = new Emitter<void>();
get onDidChangeToolbarItems(): Event<void> {
return this.onDidChangeToolbarItemsEmitter.event;
}
@inject(MenuModelRegistry)
protected readonly menus: MenuModelRegistry;
@inject(CommandRegistry)
protected readonly commands: CommandRegistry;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(PluginViewWidgetIdentifier)
readonly options: PluginViewWidgetIdentifier;
constructor() {
super();
this.node.tabIndex = -1;
this.node.style.height = '100%';
}
@postConstruct()
protected init(): void {
this.id = this.options.id;
const localContext = this.contextKeyService.createScoped(this.node);
localContext.setContext('view', this.options.viewId);
}
get onDidChangeDescription(): Event<void> {
return this.onDidChangeDescriptionEmitter.event;
}
protected override onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
const widget = this.widgets[0];
if (widget) {
widget.activate();
this.updateWidgetMessage();
} else {
this.node.focus();
}
}
storeState(): PluginViewWidget.State {
return {
label: this.title.label,
message: this.message,
widgets: this.widgets,
suppressUpdateViewVisibility: this._suppressUpdateViewVisibility,
currentViewContainerId: this.currentViewContainerId
};
}
restoreState(state: PluginViewWidget.State): void {
this.title.label = state.label;
this.message = state.message;
this.suppressUpdateViewVisibility = state.suppressUpdateViewVisibility;
this.currentViewContainerId = state.currentViewContainerId;
for (const widget of state.widgets) {
this.addWidget(widget);
}
}
set suppressUpdateViewVisibility(suppressUpdateViewVisibility: boolean) {
this._suppressUpdateViewVisibility = !this.updatingViewVisibility && suppressUpdateViewVisibility;
}
updateViewVisibility(cb: () => void): void {
if (this._suppressUpdateViewVisibility) {
return;
}
try {
this.updatingViewVisibility = true;
cb();
} finally {
this.updatingViewVisibility = false;
}
}
get message(): string | undefined {
return this._message;
}
set message(message: string | undefined) {
this._message = message;
this.updateWidgetMessage();
}
get description(): string {
return this._description;
}
set description(description: string) {
this._description = description;
this.onDidChangeDescriptionEmitter.fire();
}
private updateWidgetMessage(): void {
const widget = this.widgets[0];
if (widget) {
if (widget instanceof TreeViewWidget) {
widget.message = this._message;
}
}
}
override addWidget(widget: Widget): void {
super.addWidget(widget);
this.updateWidgetMessage();
this.onDidChangeToolbarItemsEmitter.fire();
}
override insertWidget(index: number, widget: Widget): void {
super.insertWidget(index, widget);
this.updateWidgetMessage();
this.onDidChangeToolbarItemsEmitter.fire();
}
override dispose(): void {
this.toDispose.dispose();
super.dispose();
}
}
export namespace PluginViewWidget {
export interface State {
label: string,
message?: string,
widgets: ReadonlyArray<Widget>,
suppressUpdateViewVisibility: boolean;
currentViewContainerId: string | undefined;
}
}

View File

@@ -0,0 +1,51 @@
/********************************************************************************
* Copyright (C) 2021 1C-Soft LLC and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable, interfaces, named } from '@theia/core/shared/inversify';
import { AbstractTreeDecoratorService, TreeDecorator } from '@theia/core/lib/browser/tree/tree-decorator';
import { bindContributionProvider, ContributionProvider, isObject } from '@theia/core';
import { TreeNode } from '@theia/core/lib/browser';
import { TreeItem } from '@theia/plugin';
import URI from '@theia/core/lib/common/uri';
import { FileTreeDecoratorAdapter } from '@theia/filesystem/lib/browser';
export const TreeViewDecorator = Symbol('TreeViewDecorator');
@injectable()
export class TreeViewDecoratorAdapter extends FileTreeDecoratorAdapter {
protected override getUriForNode(node: TreeNode | TreeItem): string | undefined {
if (this.isTreeItem(node)) {
return new URI(node.resourceUri).toString();
}
}
protected isTreeItem(node: unknown): node is TreeItem {
return isObject<TreeItem>(node) && !!node.resourceUri;
}
}
@injectable()
export class TreeViewDecoratorService extends AbstractTreeDecoratorService {
constructor(@inject(ContributionProvider) @named(TreeViewDecorator) contributions: ContributionProvider<TreeDecorator>) {
super(contributions.getContributions());
}
}
export function bindTreeViewDecoratorUtilities(bind: interfaces.Bind): void {
bind(TreeViewDecoratorAdapter).toSelf().inSingletonScope();
bindContributionProvider(bind, TreeViewDecorator);
bind(TreeViewDecorator).toService(TreeViewDecoratorAdapter);
}

View File

@@ -0,0 +1,942 @@
// *****************************************************************************
// Copyright (C) 2018-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, inject, postConstruct } from '@theia/core/shared/inversify';
import { TreeViewsExt, TreeViewItemCollapsibleState, TreeViewItem, TreeViewItemReference, ThemeIcon, DataTransferFileDTO } from '../../../common/plugin-api-rpc';
import { Command } from '../../../common/plugin-api-rpc-model';
import {
TreeNode,
NodeProps,
SelectableTreeNode,
ExpandableTreeNode,
CompositeTreeNode,
TreeImpl,
TREE_NODE_SEGMENT_CLASS,
TREE_NODE_SEGMENT_GROW_CLASS,
TREE_NODE_TAIL_CLASS,
TreeModelImpl,
TreeViewWelcomeWidget,
TooltipAttributes,
TreeSelection,
HoverService,
ApplicationShell,
KeybindingRegistry
} from '@theia/core/lib/browser';
import { MenuPath, MenuModelRegistry, CommandMenu, AcceleratorSource } from '@theia/core/lib/common/menu';
import * as React from '@theia/core/shared/react';
import { PluginSharedStyle } from '../plugin-shared-style';
import { ACTION_ITEM, Widget } from '@theia/core/lib/browser/widgets/widget';
import { Emitter, Event } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { View } from '../../../common/plugin-protocol';
import { URI } from '@theia/core/lib/common/uri';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { AccessibilityInformation } from '@theia/plugin';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { CancellationTokenSource, CancellationToken, Mutable } from '@theia/core/lib/common';
import { mixin } from '../../../common/types';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { DnDFileContentStore } from './dnd-file-content-store';
export const TREE_NODE_HYPERLINK = 'theia-TreeNodeHyperlink';
export const VIEW_ITEM_CONTEXT_MENU: MenuPath = ['view-item-context-menu'];
export const VIEW_ITEM_INLINE_MENU: MenuPath = ['view-item-context-menu', 'inline'];
export interface SelectionEventHandler {
readonly node: SelectableTreeNode;
readonly contextSelection: boolean;
}
export interface TreeViewNode extends SelectableTreeNode, DecoratedTreeNode {
contextValue?: string;
command?: Command;
resourceUri?: string;
themeIcon?: ThemeIcon;
tooltip?: string | MarkdownString;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
description?: string | boolean | any;
accessibilityInformation?: AccessibilityInformation;
}
export namespace TreeViewNode {
export function is(arg: TreeNode | undefined): arg is TreeViewNode {
return !!arg && SelectableTreeNode.is(arg) && DecoratedTreeNode.is(arg);
}
}
export class ResolvableTreeViewNode implements TreeViewNode {
contextValue?: string;
command?: Command;
resourceUri?: string;
themeIcon?: ThemeIcon;
tooltip?: string | MarkdownString;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
description?: string | boolean | any;
accessibilityInformation?: AccessibilityInformation;
selected: boolean;
focus?: boolean;
id: string;
name?: string;
icon?: string;
visible?: boolean;
parent: Readonly<CompositeTreeNode>;
previousSibling?: TreeNode;
nextSibling?: TreeNode;
busy?: number;
decorationData: WidgetDecoration.Data;
resolve: ((token: CancellationToken) => Promise<void>);
private _resolved = false;
private resolving: Deferred<void> | undefined;
constructor(treeViewNode: Partial<TreeViewNode>, resolve: (token: CancellationToken) => Promise<TreeViewItem | undefined>) {
mixin(this, treeViewNode);
this.resolve = async (token: CancellationToken) => {
if (this.resolving) {
return this.resolving.promise;
}
if (!this._resolved) {
this.resolving = new Deferred();
const resolvedTreeItem = await resolve(token);
if (resolvedTreeItem) {
this.command = this.command ?? resolvedTreeItem.command;
this.tooltip = this.tooltip ?? resolvedTreeItem.tooltip;
}
this.resolving.resolve();
this.resolving = undefined;
}
if (!token.isCancellationRequested) {
this._resolved = true;
}
};
}
reset(): void {
this._resolved = false;
this.resolving = undefined;
this.command = undefined;
this.tooltip = undefined;
}
get resolved(): boolean {
return this._resolved;
}
}
export class ResolvableCompositeTreeViewNode extends ResolvableTreeViewNode implements CompositeTreeViewNode {
expanded: boolean;
children: readonly TreeNode[];
constructor(
treeViewNode: Pick<CompositeTreeViewNode, 'children' | 'expanded'> & Partial<TreeViewNode>,
resolve: (token: CancellationToken) => Promise<TreeViewItem | undefined>) {
super(treeViewNode, resolve);
this.expanded = treeViewNode.expanded;
this.children = treeViewNode.children;
}
}
export interface CompositeTreeViewNode extends TreeViewNode, ExpandableTreeNode, CompositeTreeNode {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
description?: string | boolean | any;
}
export namespace CompositeTreeViewNode {
export function is(arg: TreeNode | undefined): arg is CompositeTreeViewNode {
return TreeViewNode.is(arg) && ExpandableTreeNode.is(arg) && CompositeTreeNode.is(arg);
}
}
@injectable()
export class TreeViewWidgetOptions {
id: string;
manageCheckboxStateManually: boolean | undefined;
showCollapseAll: boolean | undefined;
multiSelect: boolean | undefined;
dragMimeTypes: string[] | undefined;
dropMimeTypes: string[] | undefined;
}
@injectable()
export class PluginTree extends TreeImpl {
@inject(PluginSharedStyle)
protected readonly sharedStyle: PluginSharedStyle;
@inject(TreeViewWidgetOptions)
protected readonly options: TreeViewWidgetOptions;
@inject(MessageService)
protected readonly notification: MessageService;
protected readonly onDidChangeWelcomeStateEmitter: Emitter<void> = new Emitter<void>();
readonly onDidChangeWelcomeState = this.onDidChangeWelcomeStateEmitter.event;
private _proxy: TreeViewsExt | undefined;
private _viewInfo: View | undefined;
private _isEmpty: boolean;
private _hasTreeItemResolve: Promise<boolean> = Promise.resolve(false);
set proxy(proxy: TreeViewsExt | undefined) {
this._proxy = proxy;
if (proxy) {
this._hasTreeItemResolve = proxy.$hasResolveTreeItem(this.options.id);
} else {
this._hasTreeItemResolve = Promise.resolve(false);
}
}
get proxy(): TreeViewsExt | undefined {
return this._proxy;
}
get hasTreeItemResolve(): Promise<boolean> {
return this._hasTreeItemResolve;
}
set viewInfo(viewInfo: View) {
this._viewInfo = viewInfo;
}
get isEmpty(): boolean {
return this._isEmpty;
}
protected override async resolveChildren(parent: CompositeTreeNode): Promise<TreeNode[]> {
if (!this._proxy) {
return super.resolveChildren(parent);
}
const children = await this.fetchChildren(this._proxy, parent);
const hasResolve = await this.hasTreeItemResolve;
return children.map(value => hasResolve ? this.createResolvableTreeNode(value, parent) : this.createTreeNode(value, parent));
}
protected async fetchChildren(proxy: TreeViewsExt, parent: CompositeTreeNode): Promise<TreeViewItem[]> {
try {
const children = await proxy.$getChildren(this.options.id, parent.id);
const oldEmpty = this._isEmpty;
this._isEmpty = !parent.id && (!children || children.length === 0);
if (oldEmpty !== this._isEmpty) {
this.onDidChangeWelcomeStateEmitter.fire();
}
return children || [];
} catch (e) {
if (e) {
console.error(`Failed to fetch children for '${this.options.id}'`, e);
const label = this._viewInfo ? this._viewInfo.name : this.options.id;
this.notification.error(`${label}: ${e.message}`);
}
return [];
}
}
protected createTreeNode(item: TreeViewItem, parent: CompositeTreeNode): TreeNode {
const update: Partial<TreeViewNode> = this.createTreeNodeUpdate(item);
const node = this.getNode(item.id);
if (item.collapsibleState !== undefined && item.collapsibleState !== TreeViewItemCollapsibleState.None) {
if (CompositeTreeViewNode.is(node)) {
return Object.assign(node, update);
}
return Object.assign({
id: item.id,
parent,
visible: true,
selected: false,
expanded: TreeViewItemCollapsibleState.Expanded === item.collapsibleState,
children: [],
command: item.command
}, update);
}
if (TreeViewNode.is(node) && !ExpandableTreeNode.is(node)) {
return Object.assign(node, update, { command: item.command });
}
return Object.assign({
id: item.id,
parent,
visible: true,
selected: false,
command: item.command,
}, update);
}
override markAsChecked(node: Mutable<TreeNode>, checked: boolean): void {
function findParentsToChange(child: TreeNode, nodes: TreeNode[]): void {
if ((child.parent?.checkboxInfo !== undefined && child.parent.checkboxInfo.checked !== checked) &&
(!checked || !child.parent.children.some(candidate => candidate !== child && candidate.checkboxInfo?.checked === false))) {
nodes.push(child.parent);
findParentsToChange(child.parent, nodes);
}
}
function findChildrenToChange(parent: TreeNode, nodes: TreeNode[]): void {
if (CompositeTreeNode.is(parent)) {
parent.children.forEach(child => {
if (child.checkboxInfo !== undefined && child.checkboxInfo.checked !== checked) {
nodes.push(child);
}
findChildrenToChange(child, nodes);
});
}
}
const nodesToChange = [node];
if (!this.options.manageCheckboxStateManually) {
findParentsToChange(node, nodesToChange);
findChildrenToChange(node, nodesToChange);
}
nodesToChange.forEach(n => n.checkboxInfo!.checked = checked);
this.onDidUpdateEmitter.fire(nodesToChange);
this.proxy?.$checkStateChanged(this.options.id, [{ id: node.id, checked: checked }]);
}
/** Creates a resolvable tree node. If a node already exists, reset it because the underlying TreeViewItem might have been disposed in the backend. */
protected createResolvableTreeNode(item: TreeViewItem, parent: CompositeTreeNode): TreeNode {
const update: Partial<TreeViewNode> = this.createTreeNodeUpdate(item);
const node = this.getNode(item.id);
// Node is a composite node that might contain children
if (item.collapsibleState !== undefined && item.collapsibleState !== TreeViewItemCollapsibleState.None) {
// Reuse existing composite node and reset it
if (node instanceof ResolvableCompositeTreeViewNode) {
node.reset();
return Object.assign(node, update);
}
// Create new composite node
const compositeNode = Object.assign({
id: item.id,
parent,
visible: true,
selected: false,
expanded: TreeViewItemCollapsibleState.Expanded === item.collapsibleState,
children: [],
command: item.command
}, update);
return new ResolvableCompositeTreeViewNode(compositeNode, async (token: CancellationToken) => this._proxy?.$resolveTreeItem(this.options.id, item.id, token));
}
// Node is a leaf
// Reuse existing node and reset it.
if (node instanceof ResolvableTreeViewNode && !ExpandableTreeNode.is(node)) {
node.reset();
return Object.assign(node, update);
}
const treeNode = Object.assign({
id: item.id,
parent,
visible: true,
selected: false,
command: item.command,
}, update);
return new ResolvableTreeViewNode(treeNode, async (token: CancellationToken) => this._proxy?.$resolveTreeItem(this.options.id, item.id, token));
}
protected createTreeNodeUpdate(item: TreeViewItem): Partial<TreeViewNode> {
const decorationData = this.toDecorationData(item);
const icon = this.toIconClass(item);
const resourceUri = item.resourceUri && URI.fromComponents(item.resourceUri).toString();
const themeIcon = item.themeIcon ? item.themeIcon : item.collapsibleState !== TreeViewItemCollapsibleState.None ? { id: 'folder' } : undefined;
return {
name: item.label,
decorationData,
icon,
description: item.description,
themeIcon,
resourceUri,
tooltip: item.tooltip,
contextValue: item.contextValue,
command: item.command,
checkboxInfo: item.checkboxInfo,
accessibilityInformation: item.accessibilityInformation,
};
}
protected toDecorationData(item: TreeViewItem): WidgetDecoration.Data {
let decoration: WidgetDecoration.Data = {};
if (item.highlights) {
const highlight = {
ranges: item.highlights.map(h => ({ offset: h[0], length: h[1] - h[0] }))
};
decoration = { highlight };
}
return decoration;
}
protected toIconClass(item: TreeViewItem): string | undefined {
if (item.icon) {
return 'fa ' + item.icon;
}
if (item.iconUrl) {
const reference = this.sharedStyle.toIconClass(item.iconUrl);
this.toDispose.push(reference);
return reference.object.iconClass;
}
return undefined;
}
}
@injectable()
export class PluginTreeModel extends TreeModelImpl {
@inject(PluginTree)
protected override readonly tree: PluginTree;
set proxy(proxy: TreeViewsExt | undefined) {
this.tree.proxy = proxy;
}
get proxy(): TreeViewsExt | undefined {
return this.tree.proxy;
}
get hasTreeItemResolve(): Promise<boolean> {
return this.tree.hasTreeItemResolve;
}
set viewInfo(viewInfo: View) {
this.tree.viewInfo = viewInfo;
}
get isTreeEmpty(): boolean {
return this.tree.isEmpty;
}
get onDidChangeWelcomeState(): Event<void> {
return this.tree.onDidChangeWelcomeState;
}
override doOpenNode(node: TreeNode): void {
super.doOpenNode(node);
if (node instanceof ResolvableTreeViewNode) {
node.resolve(CancellationToken.None);
}
}
}
@injectable()
export class TreeViewWidget extends TreeViewWelcomeWidget {
async refresh(items?: string[]): Promise<void> {
if (items) {
for (const id of items) {
const node = this.model.getNode(id);
if (CompositeTreeNode.is(node)) {
await this.model.refresh(node);
}
};
} else {
this.model.refresh();
}
}
protected _contextSelection = false;
@inject(ApplicationShell)
protected readonly applicationShell: ApplicationShell;
@inject(MenuModelRegistry)
protected readonly menus: MenuModelRegistry;
@inject(KeybindingRegistry)
protected readonly keybindings: KeybindingRegistry;
@inject(ContextKeyService)
protected readonly contextKeys: ContextKeyService;
@inject(TreeViewWidgetOptions)
readonly options: TreeViewWidgetOptions;
@inject(PluginTreeModel)
override readonly model: PluginTreeModel;
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@inject(HoverService)
protected readonly hoverService: HoverService;
@inject(ColorRegistry)
protected readonly colorRegistry: ColorRegistry;
@inject(DnDFileContentStore)
protected readonly dndFileContentStore: DnDFileContentStore;
protected treeDragType: string;
protected readonly expansionTimeouts: Map<string, number> = new Map();
@postConstruct()
protected override init(): void {
super.init();
this.id = this.options.id;
this.addClass('theia-tree-view');
this.node.style.height = '100%';
this.model.onDidChangeWelcomeState(this.update, this);
this.toDispose.push(this.model.onDidChangeWelcomeState(this.update, this));
this.toDispose.push(this.onDidChangeVisibilityEmitter);
this.toDispose.push(this.contextKeyService.onDidChange(() => this.update()));
this.toDispose.push(this.keybindings.onKeybindingsChanged(() => this.update()));
this.treeDragType = `application/vnd.code.tree.${this.id.toLowerCase()}`;
}
get showCollapseAll(): boolean {
return this.options.showCollapseAll || false;
}
protected override renderIcon(node: TreeNode, props: NodeProps): React.ReactNode {
const icon = this.toNodeIcon(node);
if (icon) {
let style: React.CSSProperties | undefined;
if (TreeViewNode.is(node) && node.themeIcon?.color) {
const color = this.colorRegistry.getCurrentColor(node.themeIcon.color.id);
if (color) {
style = { color };
}
}
return <div className={icon + ' theia-tree-view-icon'} style={style}></div>;
}
return undefined;
}
protected override renderCaption(node: TreeViewNode, props: NodeProps): React.ReactNode {
const classes = [TREE_NODE_SEGMENT_CLASS];
if (!this.hasTrailingSuffixes(node)) {
classes.push(TREE_NODE_SEGMENT_GROW_CLASS);
}
const className = classes.join(' ');
let attrs: React.HTMLAttributes<HTMLElement> & Partial<TooltipAttributes> = {
...this.decorateCaption(node, {}),
className,
id: node.id
};
if (node.accessibilityInformation) {
attrs = {
...attrs,
'aria-label': node.accessibilityInformation.label,
'role': node.accessibilityInformation.role
};
}
if (!node.tooltip && node instanceof ResolvableTreeViewNode) {
let configuredTip = false;
let source: CancellationTokenSource | undefined;
attrs = {
...attrs,
onMouseLeave: () => source?.cancel(),
onMouseEnter: async event => {
const target = event.currentTarget; // event.currentTarget will be null after awaiting node resolve()
if (configuredTip) {
if (MarkdownString.is(node.tooltip)) {
this.hoverService.requestHover({
content: node.tooltip,
target: event.target as HTMLElement,
position: 'right'
});
}
return;
}
if (!node.resolved) {
source = new CancellationTokenSource();
const token = source.token;
await node.resolve(token);
if (token.isCancellationRequested) {
return;
}
}
if (MarkdownString.is(node.tooltip)) {
this.hoverService.requestHover({
content: node.tooltip,
target: event.target as HTMLElement,
position: 'right'
});
} else {
const title = node.tooltip ||
(node.resourceUri && this.labelProvider.getLongName(new URI(node.resourceUri)))
|| this.toNodeName(node);
target.title = title;
}
configuredTip = true;
}
};
} else if (MarkdownString.is(node.tooltip)) {
attrs = {
...attrs,
onMouseEnter: event => {
this.hoverService.requestHover({
content: node.tooltip!,
target: event.target as HTMLElement,
position: 'right'
});
}
};
} else {
const title = node.tooltip ||
(node.resourceUri && this.labelProvider.getLongName(new URI(node.resourceUri)))
|| this.toNodeName(node);
attrs = {
...attrs,
title
};
}
const children: React.ReactNode[] = [];
const caption = this.toNodeName(node);
const highlight = this.getDecorationData(node, 'highlight')[0];
if (highlight) {
children.push(this.toReactNode(caption, highlight));
}
const searchHighlight = this.searchHighlights && this.searchHighlights.get(node.id);
if (searchHighlight) {
children.push(...this.toReactNode(caption, searchHighlight));
} else if (!highlight) {
children.push(caption);
}
const description = this.toNodeDescription(node);
if (description) {
children.push(<span className='theia-tree-view-description'>{description}</span>);
}
return <div {...attrs}>{...children}</div>;
}
protected override createNodeAttributes(node: TreeViewNode, props: NodeProps): React.Attributes & React.HTMLAttributes<HTMLElement> {
const attrs = super.createNodeAttributes(node, props);
if (this.options.dragMimeTypes) {
attrs.onDragStart = event => this.handleDragStartEvent(node, event);
attrs.onDragEnd = event => this.handleDragEnd(node, event);
attrs.draggable = true;
}
if (this.options.dropMimeTypes) {
attrs.onDrop = event => this.handleDropEvent(node, event);
attrs.onDragEnter = event => this.handleDragEnter(node, event);
attrs.onDragLeave = event => this.handleDragLeave(node, event);
attrs.onDragOver = event => this.handleDragOver(event);
}
return attrs;
}
handleDragLeave(node: TreeViewNode, event: React.DragEvent<HTMLElement>): void {
const timeout = this.expansionTimeouts.get(node.id);
if (typeof timeout !== 'undefined') {
console.debug(`dragleave ${node.id} canceling timeout`);
clearTimeout(timeout);
this.expansionTimeouts.delete(node.id);
}
}
handleDragEnter(node: TreeViewNode, event: React.DragEvent<HTMLElement>): void {
console.debug(`dragenter ${node.id}`);
if (ExpandableTreeNode.is(node)) {
console.debug(`dragenter ${node.id} starting timeout`);
this.expansionTimeouts.set(node.id, window.setTimeout(() => {
console.debug(`dragenter ${node.id} timeout reached`);
this.model.expandNode(node);
}, 500));
}
}
protected override createContainerAttributes(): React.HTMLAttributes<HTMLElement> {
const attrs = super.createContainerAttributes();
if (this.options.dropMimeTypes) {
attrs.onDrop = event => this.handleDropEvent(undefined, event);
attrs.onDragOver = event => this.handleDragOver(event);
}
return attrs;
}
protected handleDragStartEvent(node: TreeViewNode, event: React.DragEvent<HTMLElement>): void {
event.dataTransfer!.setData(this.treeDragType, '');
let selectedNodes: TreeViewNode[] = [];
if (this.model.selectedNodes.find(selected => TreeNode.equals(selected, node))) {
selectedNodes = this.model.selectedNodes.filter(TreeViewNode.is);
} else {
selectedNodes = [node];
}
this.options.dragMimeTypes!.forEach(type => {
if (type === 'text/uri-list') {
ApplicationShell.setDraggedEditorUris(event.dataTransfer, selectedNodes.filter(n => n.resourceUri).map(n => new URI(n.resourceUri)));
} else {
event.dataTransfer.setData(type, '');
}
});
this.model.proxy!.$dragStarted(this.options.id, selectedNodes.map(selected => selected.id), CancellationToken.None).then(maybeUris => {
if (maybeUris) {
this.applicationShell.addAdditionalDraggedEditorUris(maybeUris.map(uri => URI.fromComponents(uri)));
}
});
}
handleDragEnd(node: TreeViewNode, event: React.DragEvent<HTMLElement>): void {
this.applicationShell.clearAdditionalDraggedEditorUris();
this.model.proxy!.$dragEnd(this.id);
}
handleDragOver(event: React.DragEvent<HTMLElement>): void {
const hasFiles = (items: DataTransferItemList) => {
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
return true;
}
}
return false;
};
if (event.dataTransfer) {
const canDrop = event.dataTransfer.types.some(type => this.options.dropMimeTypes!.includes(type)) ||
event.dataTransfer.types.includes(this.treeDragType) ||
this.options.dropMimeTypes!.includes('files') && hasFiles(event.dataTransfer.items);
if (canDrop) {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
} else {
event.dataTransfer.dropEffect = 'none';
}
event.stopPropagation();
}
}
protected handleDropEvent(node: TreeViewNode | undefined, event: React.DragEvent<HTMLElement>): void {
if (event.dataTransfer) {
const items: [string, string | DataTransferFileDTO][] = [];
let files: string[] = [];
try {
for (let i = 0; i < event.dataTransfer.items.length; i++) {
const transferItem = event.dataTransfer.items[i];
if (transferItem.type !== this.treeDragType) {
// do not pass the artificial drag data to the extension
const f = event.dataTransfer.items[i].getAsFile();
if (f) {
const fileId = this.dndFileContentStore.addFile(f);
files.push(fileId);
const path = window.electronTheiaCore.getPathForFile(f);
const uri = path ? {
scheme: 'file',
path: path,
authority: '',
query: '',
fragment: ''
} : undefined;
items.push([transferItem.type, new DataTransferFileDTO(f.name, fileId, uri)]);
} else {
const textData = event.dataTransfer.getData(transferItem.type);
if (textData) {
items.push([transferItem.type, textData]);
}
}
}
}
if (items.length > 0 || event.dataTransfer.types.includes(this.treeDragType)) {
event.preventDefault();
event.stopPropagation();
this.model.proxy?.$drop(this.id, node?.id, items, CancellationToken.None).finally(() => {
for (const file of files) {
this.dndFileContentStore.removeFile(file);
}
});
files = [];
}
} catch (e) {
for (const file of files) {
this.dndFileContentStore.removeFile(file);
}
throw e;
}
}
}
protected override renderTailDecorations(treeViewNode: TreeViewNode, props: NodeProps): React.ReactNode {
return this.contextKeys.with({ view: this.id, viewItem: treeViewNode.contextValue }, () => {
const menu = this.menus.getMenu(VIEW_ITEM_INLINE_MENU);
const args = this.toContextMenuArgs(treeViewNode);
const inlineCommands = menu?.children.filter((item): item is CommandMenu => CommandMenu.is(item)) || [];
const tailDecorations = super.renderTailDecorations(treeViewNode, props);
return <React.Fragment>
{inlineCommands.length > 0 && <div className={TREE_NODE_SEGMENT_CLASS + ' flex'}>
{inlineCommands.map((item, index) => this.renderInlineCommand(item, index, this.focusService.hasFocus(treeViewNode), args))}
</div>}
{tailDecorations !== undefined && <div className={TREE_NODE_SEGMENT_CLASS + ' flex'}>{tailDecorations}</div>}
</React.Fragment>;
});
}
toTreeViewItemReference(treeNode: TreeNode): TreeViewItemReference {
return { viewId: this.id, itemId: treeNode.id };
}
protected resolveKeybindingForCommand(command: string | undefined): string {
let result = '';
if (command) {
const bindings = this.keybindings.getKeybindingsForCommand(command);
let found = false;
if (bindings && bindings.length > 0) {
bindings.forEach(binding => {
if (!found && this.keybindings.isEnabledInScope(binding, this.node)) {
found = true;
result = ` (${this.keybindings.acceleratorFor(binding, '+')})`;
}
});
}
}
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected renderInlineCommand(actionMenuNode: CommandMenu, index: number, tabbable: boolean, args: any[]): React.ReactNode {
const nodePath = [...VIEW_ITEM_INLINE_MENU, actionMenuNode.id];
if (!actionMenuNode.icon || !actionMenuNode.isVisible(nodePath, this.contextKeys, undefined)) {
return false;
}
const className = [TREE_NODE_SEGMENT_CLASS, TREE_NODE_TAIL_CLASS, actionMenuNode.icon, ACTION_ITEM, 'theia-tree-view-inline-action'].join(' ');
const tabIndex = tabbable ? 0 : undefined;
const titleString = actionMenuNode.label + (AcceleratorSource.is(actionMenuNode) ? actionMenuNode.getAccelerator(undefined).join('+') : '');
return <div key={index} className={className} title={titleString} tabIndex={tabIndex} onClick={e => {
e.stopPropagation();
actionMenuNode.run(nodePath, ...args);
}} />;
}
protected override toContextMenuArgs(target: SelectableTreeNode): [TreeViewItemReference, TreeViewItemReference[]] | [TreeViewItemReference] {
if (this.options.multiSelect) {
return [this.toTreeViewItemReference(target), this.model.selectedNodes.map(node => this.toTreeViewItemReference(node))];
} else {
return [this.toTreeViewItemReference(target)];
}
}
override setFlag(flag: Widget.Flag): void {
super.setFlag(flag);
if (flag === Widget.Flag.IsVisible) {
this.onDidChangeVisibilityEmitter.fire(this.isVisible);
}
}
override clearFlag(flag: Widget.Flag): void {
super.clearFlag(flag);
if (flag === Widget.Flag.IsVisible) {
this.onDidChangeVisibilityEmitter.fire(this.isVisible);
}
}
override handleEnter(event: KeyboardEvent): void {
super.handleEnter(event);
this.tryExecuteCommand();
}
protected override tapNode(node?: TreeNode): void {
super.tapNode(node);
this.findCommands(node).then(commandMap => {
if (commandMap.size > 0) {
this.tryExecuteCommandMap(commandMap);
} else if (node && this.isExpandable(node)) {
this.model.toggleNodeExpansion(node);
}
});
}
// execute TreeItem.command if present
protected async tryExecuteCommand(node?: TreeNode): Promise<void> {
this.tryExecuteCommandMap(await this.findCommands(node));
}
protected tryExecuteCommandMap(commandMap: Map<string, unknown[]>): void {
commandMap.forEach((args, commandId) => {
this.commands.executeCommand(commandId, ...args);
});
}
protected async findCommands(node?: TreeNode): Promise<Map<string, unknown[]>> {
const commandMap = new Map<string, unknown[]>();
const treeNodes = (node ? [node] : this.model.selectedNodes) as TreeViewNode[];
if (await this.model.hasTreeItemResolve) {
const cancellationToken = new CancellationTokenSource().token;
// Resolve all resolvable nodes that don't have a command and haven't been resolved.
const allResolved = Promise.all(treeNodes.map(maybeNeedsResolve => {
if (!maybeNeedsResolve.command && maybeNeedsResolve instanceof ResolvableTreeViewNode && !maybeNeedsResolve.resolved) {
return maybeNeedsResolve.resolve(cancellationToken).catch(err => {
console.error(`Failed to resolve tree item '${maybeNeedsResolve.id}'`, err);
});
}
return Promise.resolve(maybeNeedsResolve);
}));
// Only need to wait but don't need the values because tree items are resolved in place.
await allResolved;
}
for (const treeNode of treeNodes) {
if (treeNode && treeNode.command) {
commandMap.set(treeNode.command.id, treeNode.command.arguments || []);
}
}
return commandMap;
}
private _message: string | undefined;
get message(): string | undefined {
return this._message;
}
set message(message: string | undefined) {
this._message = message;
this.update();
}
protected override render(): React.ReactNode {
return React.createElement('div', this.createContainerAttributes(), this.renderSearchInfo(), this.renderTree(this.model));
}
protected renderSearchInfo(): React.ReactNode {
if (this._message) {
return <div className='theia-TreeViewInfo'>{this._message}</div>;
}
return undefined;
}
override shouldShowWelcomeView(): boolean {
return (this.model.proxy === undefined || this.model.isTreeEmpty) && this.message === undefined;
}
protected override handleContextMenuEvent(node: TreeNode | undefined, event: React.MouseEvent<HTMLElement, MouseEvent>): void {
if (SelectableTreeNode.is(node)) {
// Keep the selection for the context menu, if the widget support multi-selection and the right click happens on an already selected node.
if (!this.props.multiSelect || !node.selected) {
const type = !!this.props.multiSelect && this.hasCtrlCmdMask(event) ? TreeSelection.SelectionType.TOGGLE : TreeSelection.SelectionType.DEFAULT;
this.model.addSelection({ node, type });
}
this.focusService.setFocus(node);
const contextMenuPath = this.props.contextMenuPath;
if (contextMenuPath) {
const { x, y } = event.nativeEvent;
const args = this.toContextMenuArgs(node);
const contextKeyService = this.contextKeyService.createOverlay([
['viewItem', (TreeViewNode.is(node) && node.contextValue) || undefined],
['view', this.options.id]
]);
setTimeout(() => this.contextMenuRenderer.render({
menuPath: contextMenuPath,
anchor: { x, y },
args,
contextKeyService,
context: event.currentTarget
}), 10);
}
}
event.stopPropagation();
event.preventDefault();
}
}

View File

@@ -0,0 +1,207 @@
// *****************************************************************************
// 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 { MAIN_RPC_CONTEXT, TreeViewsMain, TreeViewsExt, TreeViewRevealOptions, RegisterTreeDataProviderOptions } from '../../../common/plugin-api-rpc';
import { RPCProtocol } from '../../../common/rpc-protocol';
import { PluginViewRegistry, PLUGIN_VIEW_DATA_FACTORY_ID } from './plugin-view-registry';
import {
SelectableTreeNode,
ExpandableTreeNode,
CompositeTreeNode,
WidgetManager,
BadgeService
} from '@theia/core/lib/browser';
import { Disposable, DisposableCollection } from '@theia/core';
import { TreeViewWidget, TreeViewNode, PluginTreeModel, TreeViewWidgetOptions } from './tree-view-widget';
import { PluginViewWidget } from './plugin-view-widget';
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
import { DnDFileContentStore } from './dnd-file-content-store';
import { ViewBadge } from '@theia/plugin';
export class TreeViewsMainImpl implements TreeViewsMain, Disposable {
private readonly proxy: TreeViewsExt;
private readonly viewRegistry: PluginViewRegistry;
private readonly widgetManager: WidgetManager;
private readonly fileContentStore: DnDFileContentStore;
private readonly badgeService: BadgeService;
private readonly treeViewProviders = new Map<string, Disposable>();
private readonly toDispose = new DisposableCollection(
Disposable.create(() => { /* mark as not disposed */ })
);
constructor(rpc: RPCProtocol, private container: interfaces.Container) {
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TREE_VIEWS_EXT);
this.viewRegistry = container.get(PluginViewRegistry);
this.widgetManager = this.container.get(WidgetManager);
this.fileContentStore = this.container.get(DnDFileContentStore);
this.badgeService = this.container.get(BadgeService);
}
dispose(): void {
this.toDispose.dispose();
}
async $registerTreeDataProvider(treeViewId: string, $options: RegisterTreeDataProviderOptions): Promise<void> {
this.treeViewProviders.set(treeViewId, this.viewRegistry.registerViewDataProvider(treeViewId, async ({ state, viewInfo }) => {
const options: TreeViewWidgetOptions = {
id: treeViewId,
manageCheckboxStateManually: $options.manageCheckboxStateManually,
showCollapseAll: $options.showCollapseAll,
multiSelect: $options.canSelectMany,
dragMimeTypes: $options.dragMimeTypes,
dropMimeTypes: $options.dropMimeTypes
};
const widget = await this.widgetManager.getOrCreateWidget<TreeViewWidget>(PLUGIN_VIEW_DATA_FACTORY_ID, options);
widget.model.viewInfo = viewInfo;
if (state) {
widget.restoreState(state);
// ensure that state is completely restored
await widget.model.refresh();
} else if (!widget.model.root) {
const root: CompositeTreeNode & ExpandableTreeNode = {
id: '',
parent: undefined,
name: '',
visible: false,
expanded: true,
children: []
};
widget.model.root = root;
}
if (this.toDispose.disposed) {
widget.model.proxy = undefined;
} else {
widget.model.proxy = this.proxy;
this.toDispose.push(Disposable.create(() => widget.model.proxy = undefined));
this.handleTreeEvents(widget.id, widget);
}
widget.model.refresh();
return widget;
}));
this.toDispose.push(Disposable.create(() => this.$unregisterTreeDataProvider(treeViewId)));
}
async $unregisterTreeDataProvider(treeViewId: string): Promise<void> {
const treeDataProvider = this.treeViewProviders.get(treeViewId);
if (treeDataProvider) {
this.treeViewProviders.delete(treeViewId);
treeDataProvider.dispose();
}
}
async $readDroppedFile(contentId: string): Promise<BinaryBuffer> {
const file = this.fileContentStore.getFile(contentId);
const buffer = await file.arrayBuffer();
return BinaryBuffer.wrap(new Uint8Array(buffer));
}
async $refresh(treeViewId: string, items?: string[]): Promise<void> {
const viewPanel = await this.viewRegistry.getView(treeViewId);
const widget = viewPanel && viewPanel.widgets[0];
if (widget instanceof TreeViewWidget) {
await widget.refresh(items);
}
}
// elementParentChain parameter contain a list of tree ids from root to the revealed node
// all parents of the revealed node should be fetched and expanded in order for it to reveal
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async $reveal(treeViewId: string, elementParentChain: string[], options: TreeViewRevealOptions): Promise<any> {
const viewPanel = await this.viewRegistry.openView(treeViewId, { activate: options.focus, reveal: true });
const widget = viewPanel && viewPanel.widgets[0];
if (widget instanceof TreeViewWidget) {
// pop last element which is the node to reveal
const elementId = elementParentChain.pop();
await this.expandParentChain(widget.model, elementParentChain);
const treeNode = widget.model.getNode(elementId);
if (treeNode) {
if (options.expand && ExpandableTreeNode.is(treeNode)) {
await widget.model.expandNode(treeNode);
}
if (options.select && SelectableTreeNode.is(treeNode)) {
widget.model.selectNode(treeNode);
}
}
}
}
/**
* Expand all parents of the node to reveal from root. This should also fetch missing nodes to the frontend.
*/
private async expandParentChain(model: PluginTreeModel, elementParentChain: string[]): Promise<void> {
for (const elementId of elementParentChain) {
const treeNode = model.getNode(elementId);
if (ExpandableTreeNode.is(treeNode)) {
await model.expandNode(treeNode);
}
}
}
async $setMessage(treeViewId: string, message: string): Promise<void> {
const viewPanel = await this.viewRegistry.getView(treeViewId);
if (viewPanel instanceof PluginViewWidget) {
viewPanel.message = message;
}
}
async $setTitle(treeViewId: string, title: string): Promise<void> {
const viewPanel = await this.viewRegistry.getView(treeViewId);
if (viewPanel) {
viewPanel.title.label = title;
}
}
async $setDescription(treeViewId: string, description: string): Promise<void> {
const viewPanel = await this.viewRegistry.getView(treeViewId);
if (viewPanel) {
viewPanel.description = description;
}
}
async $setBadge(treeViewId: string, badge: ViewBadge | undefined): Promise<void> {
const viewPanel = await this.viewRegistry.getView(treeViewId);
if (viewPanel) {
this.badgeService.showBadge(viewPanel, badge);
}
}
async setChecked(treeViewWidget: TreeViewWidget, changedNodes: TreeViewNode[]): Promise<void> {
await this.proxy.$checkStateChanged(treeViewWidget.id, changedNodes.map(node => ({
id: node.id,
checked: !!node.checkboxInfo?.checked
})));
}
protected handleTreeEvents(treeViewId: string, treeViewWidget: TreeViewWidget): void {
this.toDispose.push(treeViewWidget.model.onExpansionChanged(event => {
this.proxy.$setExpanded(treeViewId, event.id, event.expanded);
}));
this.toDispose.push(treeViewWidget.model.onSelectionChanged(event => {
this.proxy.$setSelection(treeViewId, event.map((node: TreeViewNode) => node.id));
}));
const updateVisible = () => this.proxy.$setVisible(treeViewId, treeViewWidget.isVisible);
updateVisible();
this.toDispose.push(treeViewWidget.onDidChangeVisibility(() => updateVisible()));
}
}

View File

@@ -0,0 +1,64 @@
// *****************************************************************************
// 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, postConstruct, inject } from '@theia/core/shared/inversify';
import { ContextKey, ContextKeyService } from '@theia/core/lib/browser/context-key-service';
@injectable()
export class ViewContextKeyService {
protected _viewItem: ContextKey<string>;
get viewItem(): ContextKey<string> {
return this._viewItem;
}
// for the next three keys, see https://code.visualstudio.com/api/references/when-clause-contexts#visible-view-container-when-clause-context
protected _activeViewlet: ContextKey<string>;
get activeViewlet(): ContextKey<string> {
return this._activeViewlet;
}
protected _activePanel: ContextKey<string>;
get activePanel(): ContextKey<string> {
return this._activePanel;
}
protected _activeAuxiliary: ContextKey<string>;
get activeAuxiliary(): ContextKey<string> {
return this._activeAuxiliary;
}
protected _focusedView: ContextKey<string>;
get focusedView(): ContextKey<string> {
return this._focusedView;
}
@inject(ContextKeyService)
protected readonly contextKeyService: ContextKeyService;
@postConstruct()
protected init(): void {
this._viewItem = this.contextKeyService.createKey('viewItem', '');
this._activeViewlet = this.contextKeyService.createKey('activeViewlet', '');
this._activePanel = this.contextKeyService.createKey('activePanel', '');
this._activeAuxiliary = this.contextKeyService.createKey('activeAuxiliary', '');
this._focusedView = this.contextKeyService.createKey('focusedView', '');
}
match(expression: string | undefined): boolean {
return !expression || this.contextKeyService.match(expression);
}
}

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