deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
403
packages/plugin-ext/src/main/browser/authentication-main.ts
Normal file
403
packages/plugin-ext/src/main/browser/authentication-main.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// code copied and modified from https://github.com/microsoft/vscode/blob/1.47.3/src/vs/workbench/api/browser/mainThreadAuthentication.ts
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { AuthenticationExt, AuthenticationMain, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { MessageService } from '@theia/core/lib/common/message-service';
|
||||
import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
|
||||
import {
|
||||
AuthenticationProvider,
|
||||
AuthenticationProviderSessionOptions,
|
||||
AuthenticationService,
|
||||
AuthenticationSession,
|
||||
AuthenticationSessionAccountInformation,
|
||||
readAllowedExtensions
|
||||
} from '@theia/core/lib/browser/authentication-service';
|
||||
import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
|
||||
import * as theia from '@theia/plugin';
|
||||
import { QuickPickValue } from '@theia/core/lib/browser/quick-input/quick-input-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { isObject } from '@theia/core';
|
||||
|
||||
export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; }
|
||||
|
||||
export class AuthenticationMainImpl implements AuthenticationMain {
|
||||
private readonly proxy: AuthenticationExt;
|
||||
private readonly messageService: MessageService;
|
||||
private readonly storageService: StorageService;
|
||||
private readonly authenticationService: AuthenticationService;
|
||||
private readonly quickPickService: QuickPickService;
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.AUTHENTICATION_EXT);
|
||||
this.messageService = container.get(MessageService);
|
||||
this.storageService = container.get(StorageService);
|
||||
this.authenticationService = container.get(AuthenticationService);
|
||||
this.quickPickService = container.get(QuickPickService);
|
||||
|
||||
this.authenticationService.onDidChangeSessions(e => {
|
||||
this.proxy.$onDidChangeAuthenticationSessions({ id: e.providerId, label: e.label });
|
||||
});
|
||||
}
|
||||
|
||||
async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise<void> {
|
||||
const provider = new AuthenticationProviderImpl(this.proxy, id, label, supportsMultipleAccounts, this.storageService, this.messageService);
|
||||
this.authenticationService.registerAuthenticationProvider(id, provider);
|
||||
}
|
||||
|
||||
async $unregisterAuthenticationProvider(id: string): Promise<void> {
|
||||
this.authenticationService.unregisterAuthenticationProvider(id);
|
||||
}
|
||||
|
||||
async $updateSessions(id: string, event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void> {
|
||||
this.authenticationService.updateSessions(id, event);
|
||||
}
|
||||
|
||||
$logout(providerId: string, sessionId: string): Promise<void> {
|
||||
return this.authenticationService.logout(providerId, sessionId);
|
||||
}
|
||||
|
||||
$getAccounts(providerId: string): Thenable<readonly theia.AuthenticationSessionAccountInformation[]> {
|
||||
return this.authenticationService.getSessions(providerId).then(sessions => sessions.map(session => session.account));
|
||||
}
|
||||
|
||||
async $getSession(providerId: string, scopeListOrRequest: ReadonlyArray<string> | theia.AuthenticationWwwAuthenticateRequest, extensionId: string, extensionName: string,
|
||||
options: theia.AuthenticationGetSessionOptions): Promise<theia.AuthenticationSession | undefined> {
|
||||
const sessions = await this.authenticationService.getSessions(providerId, scopeListOrRequest, options?.account);
|
||||
|
||||
// Error cases
|
||||
if (options.forceNewSession && options.createIfNone) {
|
||||
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, createIfNone');
|
||||
}
|
||||
if (options.forceNewSession && options.silent) {
|
||||
throw new Error('Invalid combination of options. Please remove one of the following: forceNewSession, silent');
|
||||
}
|
||||
if (options.createIfNone && options.silent) {
|
||||
throw new Error('Invalid combination of options. Please remove one of the following: createIfNone, silent');
|
||||
}
|
||||
|
||||
const supportsMultipleAccounts = this.authenticationService.supportsMultipleAccounts(providerId);
|
||||
// Check if the sessions we have are valid
|
||||
if (!options.forceNewSession && sessions.length) {
|
||||
if (supportsMultipleAccounts) {
|
||||
if (options.clearSessionPreference) {
|
||||
await this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, undefined);
|
||||
} else {
|
||||
const existingSessionPreference = await this.storageService.getData(`authentication-session-${extensionName}-${providerId}`);
|
||||
if (existingSessionPreference) {
|
||||
const matchingSession = sessions.find(session => session.id === existingSessionPreference);
|
||||
if (matchingSession && await this.isAccessAllowed(providerId, matchingSession.account.label, extensionId)) {
|
||||
return matchingSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (await this.isAccessAllowed(providerId, sessions[0].account.label, extensionId)) {
|
||||
return sessions[0];
|
||||
}
|
||||
}
|
||||
|
||||
// We may need to prompt because we don't have a valid session modal flows
|
||||
if (options.createIfNone || options.forceNewSession) {
|
||||
const providerName = this.authenticationService.getLabel(providerId);
|
||||
let detail: string | undefined;
|
||||
if (isAuthenticationGetSessionPresentationOptions(options.forceNewSession)) {
|
||||
detail = options.forceNewSession.detail;
|
||||
} else if (isAuthenticationGetSessionPresentationOptions(options.createIfNone)) {
|
||||
detail = options.createIfNone.detail;
|
||||
}
|
||||
const shouldForceNewSession = !!options.forceNewSession;
|
||||
const recreatingSession = shouldForceNewSession && !sessions.length;
|
||||
|
||||
const isAllowed = await this.loginPrompt(providerName, extensionName, recreatingSession, detail);
|
||||
if (!isAllowed) {
|
||||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
|
||||
const session = sessions?.length && !shouldForceNewSession && supportsMultipleAccounts
|
||||
? await this.selectSession(providerId, providerName, extensionId, extensionName, sessions, scopeListOrRequest, !!options.clearSessionPreference)
|
||||
: await this.authenticationService.login(providerId, scopeListOrRequest);
|
||||
await this.setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
|
||||
return session;
|
||||
}
|
||||
|
||||
// passive flows (silent or default)
|
||||
const validSession = sessions.find(s => this.isAccessAllowed(providerId, s.account.label, extensionId));
|
||||
if (!options.silent && !validSession) {
|
||||
this.authenticationService.requestNewSession(providerId, scopeListOrRequest, extensionId, extensionName);
|
||||
}
|
||||
return validSession;
|
||||
}
|
||||
|
||||
protected async selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string,
|
||||
potentialSessions: Readonly<AuthenticationSession[]>, scopeListOrRequest: ReadonlyArray<string> | theia.AuthenticationWwwAuthenticateRequest,
|
||||
clearSessionPreference: boolean): Promise<theia.AuthenticationSession> {
|
||||
|
||||
if (!potentialSessions.length) {
|
||||
throw new Error('No potential sessions found');
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const items: QuickPickValue<{ session?: AuthenticationSession, account?: AuthenticationSessionAccountInformation }>[] = potentialSessions.map(session => ({
|
||||
label: session.account.label,
|
||||
value: { session }
|
||||
}));
|
||||
items.push({
|
||||
label: nls.localizeByDefault('Sign in to another account'),
|
||||
value: {}
|
||||
});
|
||||
|
||||
// VS Code has code here that pushes accounts that have no active sessions. However, since we do not store
|
||||
// any accounts that don't have sessions, we dont' do this.
|
||||
const selected = await this.quickPickService.show(items,
|
||||
{
|
||||
title: nls.localizeByDefault("The extension '{0}' wants to access a {1} account", extensionName, providerName),
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
if (selected) {
|
||||
|
||||
// if we ever have accounts without sessions, pass the account to the login call
|
||||
const session = selected.value?.session ?? await this.authenticationService.login(providerId, scopeListOrRequest);
|
||||
const accountName = session.account.label;
|
||||
|
||||
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
if (!allowList.find(allowed => allowed.id === extensionId)) {
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
|
||||
}
|
||||
|
||||
this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, session.id);
|
||||
|
||||
resolve(session);
|
||||
|
||||
} else {
|
||||
reject('User did not consent to account access');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
|
||||
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
const extensionData = allowList.find(extension => extension.id === extensionId);
|
||||
if (extensionData) {
|
||||
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
|
||||
return true;
|
||||
}
|
||||
const choice = await this.messageService.info(`The extension '${extensionName}' wants to access the ${providerName} account '${accountName}'.`, 'Allow', 'Cancel');
|
||||
|
||||
const allow = choice === 'Allow';
|
||||
if (allow) {
|
||||
await addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
|
||||
}
|
||||
|
||||
return allow;
|
||||
}
|
||||
|
||||
protected async loginPrompt(providerName: string, extensionName: string, recreatingSession: boolean, detail?: string): Promise<boolean> {
|
||||
const msg = document.createElement('span');
|
||||
msg.textContent = recreatingSession
|
||||
? nls.localizeByDefault("The extension '{0}' wants you to sign in again using {1}.", extensionName, providerName)
|
||||
: nls.localizeByDefault("The extension '{0}' wants to sign in using {1}.", extensionName, providerName);
|
||||
|
||||
if (detail) {
|
||||
const detailElement = document.createElement('p');
|
||||
detailElement.textContent = detail;
|
||||
msg.appendChild(detailElement);
|
||||
}
|
||||
|
||||
return !!await new ConfirmDialog({
|
||||
title: nls.localize('theia/plugin-ext/authentication-main/loginTitle', 'Login'),
|
||||
msg,
|
||||
ok: nls.localizeByDefault('Allow'),
|
||||
cancel: Dialog.CANCEL
|
||||
}).open();
|
||||
}
|
||||
|
||||
protected async isAccessAllowed(providerId: string, accountName: string, extensionId: string): Promise<boolean> {
|
||||
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
return !!allowList.find(allowed => allowed.id === extensionId);
|
||||
}
|
||||
|
||||
protected async setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise<void> {
|
||||
const allowList = await readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
if (!allowList.find(allowed => allowed.id === extensionId)) {
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.setData(`authentication-trusted-extensions-${providerId}-${accountName}`, JSON.stringify(allowList));
|
||||
}
|
||||
this.storageService.setData(`authentication-session-${extensionName}-${providerId}`, sessionId);
|
||||
}
|
||||
|
||||
$onDidChangeSessions(providerId: string, event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): void {
|
||||
this.authenticationService.updateSessions(providerId, event);
|
||||
}
|
||||
}
|
||||
|
||||
function isAuthenticationGetSessionPresentationOptions(arg: unknown): arg is theia.AuthenticationGetSessionPresentationOptions {
|
||||
return isObject<theia.AuthenticationGetSessionPresentationOptions>(arg) && typeof arg.detail === 'string';
|
||||
}
|
||||
|
||||
async function addAccountUsage(storageService: StorageService, providerId: string, accountName: string, extensionId: string, extensionName: string): Promise<void> {
|
||||
const accountKey = `authentication-${providerId}-${accountName}-usages`;
|
||||
const usages = await readAccountUsages(storageService, providerId, accountName);
|
||||
|
||||
const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId);
|
||||
if (existingUsageIndex > -1) {
|
||||
usages.splice(existingUsageIndex, 1, {
|
||||
extensionId,
|
||||
extensionName,
|
||||
lastUsed: Date.now()
|
||||
});
|
||||
} else {
|
||||
usages.push({
|
||||
extensionId,
|
||||
extensionName,
|
||||
lastUsed: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
await storageService.setData(accountKey, JSON.stringify(usages));
|
||||
}
|
||||
|
||||
interface AccountUsage {
|
||||
extensionId: string;
|
||||
extensionName: string;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
export class AuthenticationProviderImpl implements AuthenticationProvider {
|
||||
/** map from account name to session ids */
|
||||
private accounts = new Map<string, string[]>();
|
||||
/** map from session id to account name */
|
||||
private sessions = new Map<string, string>();
|
||||
|
||||
readonly onDidChangeSessions: theia.Event<theia.AuthenticationProviderAuthenticationSessionsChangeEvent>;
|
||||
|
||||
constructor(
|
||||
private readonly proxy: AuthenticationExt,
|
||||
public readonly id: string,
|
||||
public readonly label: string,
|
||||
public readonly supportsMultipleAccounts: boolean,
|
||||
private readonly storageService: StorageService,
|
||||
private readonly messageService: MessageService
|
||||
) { }
|
||||
|
||||
public hasSessions(): boolean {
|
||||
return !!this.sessions.size;
|
||||
}
|
||||
|
||||
private registerSession(session: theia.AuthenticationSession): void {
|
||||
this.sessions.set(session.id, session.account.label);
|
||||
|
||||
const existingSessionsForAccount = this.accounts.get(session.account.label);
|
||||
if (existingSessionsForAccount) {
|
||||
this.accounts.set(session.account.label, existingSessionsForAccount.concat(session.id));
|
||||
return;
|
||||
} else {
|
||||
this.accounts.set(session.account.label, [session.id]);
|
||||
}
|
||||
}
|
||||
|
||||
async signOut(accountName: string): Promise<void> {
|
||||
const accountUsages = await readAccountUsages(this.storageService, this.id, accountName);
|
||||
const sessionsForAccount = this.accounts.get(accountName);
|
||||
const result = await this.messageService.info(accountUsages.length
|
||||
? nls.localizeByDefault("The account '{0}' has been used by: \n\n{1}\n\n Sign out from these extensions?", accountName,
|
||||
accountUsages.map(usage => usage.extensionName).join(', '))
|
||||
: nls.localizeByDefault("Sign out of '{0}'?", accountName),
|
||||
nls.localizeByDefault('Sign Out'),
|
||||
Dialog.CANCEL);
|
||||
|
||||
if (result && result === nls.localizeByDefault('Sign Out') && sessionsForAccount) {
|
||||
sessionsForAccount.forEach(sessionId => this.removeSession(sessionId));
|
||||
removeAccountUsage(this.storageService, this.id, accountName);
|
||||
}
|
||||
}
|
||||
|
||||
async getSessions(scopes?: string[], account?: AuthenticationSessionAccountInformation): Promise<ReadonlyArray<theia.AuthenticationSession>> {
|
||||
return this.proxy.$getSessions(this.id, scopes, { account: account });
|
||||
}
|
||||
|
||||
async updateSessionItems(event: theia.AuthenticationProviderAuthenticationSessionsChangeEvent): Promise<void> {
|
||||
const { added, removed } = event;
|
||||
const session = await this.proxy.$getSessions(this.id, undefined, {});
|
||||
const addedSessions = added ? session.filter(s => added.some(addedSession => addedSession.id === s.id)) : [];
|
||||
|
||||
removed?.forEach(removedSession => {
|
||||
const sessionId = removedSession.id;
|
||||
if (sessionId) {
|
||||
const accountName = this.sessions.get(sessionId);
|
||||
if (accountName) {
|
||||
this.sessions.delete(sessionId);
|
||||
const sessionsForAccount = this.accounts.get(accountName) || [];
|
||||
const sessionIndex = sessionsForAccount.indexOf(sessionId);
|
||||
sessionsForAccount.splice(sessionIndex);
|
||||
|
||||
if (!sessionsForAccount.length) {
|
||||
this.accounts.delete(accountName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addedSessions.forEach(s => this.registerSession(s));
|
||||
}
|
||||
|
||||
async login(scopes: string[], options: AuthenticationProviderSessionOptions): Promise<theia.AuthenticationSession> {
|
||||
return this.createSession(scopes, options);
|
||||
}
|
||||
|
||||
async logout(sessionId: string): Promise<void> {
|
||||
return this.removeSession(sessionId);
|
||||
}
|
||||
|
||||
createSession(scopes: string[], options: AuthenticationProviderSessionOptions): Thenable<theia.AuthenticationSession> {
|
||||
return this.proxy.$createSession(this.id, scopes, options);
|
||||
}
|
||||
|
||||
removeSession(sessionId: string): Thenable<void> {
|
||||
return this.proxy.$removeSession(this.id, sessionId)
|
||||
.then(() => {
|
||||
this.messageService.info(nls.localize('theia/plugin-ext/authentication-main/signedOut', 'Successfully signed out.'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function readAccountUsages(storageService: StorageService, providerId: string, accountName: string): Promise<AccountUsage[]> {
|
||||
const accountKey = `authentication-${providerId}-${accountName}-usages`;
|
||||
const storedUsages: string | undefined = await storageService.getData(accountKey);
|
||||
let usages: AccountUsage[] = [];
|
||||
if (storedUsages) {
|
||||
try {
|
||||
usages = JSON.parse(storedUsages);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
function removeAccountUsage(storageService: StorageService, providerId: string, accountName: string): void {
|
||||
const accountKey = `authentication-${providerId}-${accountName}-usages`;
|
||||
storageService.setData(accountKey, undefined);
|
||||
}
|
||||
38
packages/plugin-ext/src/main/browser/clipboard-main.ts
Normal file
38
packages/plugin-ext/src/main/browser/clipboard-main.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 RedHat and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { ClipboardMain } from '../../common';
|
||||
import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
|
||||
|
||||
export class ClipboardMainImpl implements ClipboardMain {
|
||||
|
||||
protected readonly clipboardService: ClipboardService;
|
||||
|
||||
constructor(container: interfaces.Container) {
|
||||
this.clipboardService = container.get(ClipboardService);
|
||||
}
|
||||
|
||||
async $readText(): Promise<string> {
|
||||
const result = await this.clipboardService.readText();
|
||||
return result;
|
||||
}
|
||||
|
||||
async $writeText(value: string): Promise<void> {
|
||||
await this.clipboardService.writeText(value);
|
||||
}
|
||||
|
||||
}
|
||||
130
packages/plugin-ext/src/main/browser/command-registry-main.ts
Normal file
130
packages/plugin-ext/src/main/browser/command-registry-main.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import * as theia from '@theia/plugin';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { CommandRegistryMain, CommandRegistryExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser';
|
||||
import { PluginContributionHandler } from './plugin-contribution-handler';
|
||||
import { ArgumentProcessor } from '../../common/commands';
|
||||
import { ContributionProvider } from '@theia/core';
|
||||
|
||||
export const ArgumentProcessorContribution = Symbol('ArgumentProcessorContribution');
|
||||
|
||||
export class CommandRegistryMainImpl implements CommandRegistryMain, Disposable {
|
||||
private readonly proxy: CommandRegistryExt;
|
||||
private readonly commands = new Map<string, Disposable>();
|
||||
private readonly handlers = new Map<string, Disposable>();
|
||||
private readonly delegate: CommandRegistry;
|
||||
private readonly keyBinding: KeybindingRegistry;
|
||||
private readonly contributions: PluginContributionHandler;
|
||||
|
||||
private readonly argumentProcessors: ArgumentProcessor[] = [];
|
||||
|
||||
protected readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMAND_REGISTRY_EXT);
|
||||
this.delegate = container.get(CommandRegistry);
|
||||
this.keyBinding = container.get(KeybindingRegistry);
|
||||
this.contributions = container.get(PluginContributionHandler);
|
||||
|
||||
container.getNamed<ContributionProvider<ArgumentProcessor>>(ContributionProvider, ArgumentProcessorContribution).getContributions().forEach(processor => {
|
||||
this.registerArgumentProcessor(processor);
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
registerArgumentProcessor(processor: ArgumentProcessor): Disposable {
|
||||
this.argumentProcessors.push(processor);
|
||||
return Disposable.create(() => {
|
||||
const index = this.argumentProcessors.lastIndexOf(processor);
|
||||
if (index >= 0) {
|
||||
this.argumentProcessors.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$registerCommand(command: theia.CommandDescription): void {
|
||||
const id = command.id;
|
||||
this.commands.set(id, this.contributions.registerCommand(command));
|
||||
this.toDispose.push(Disposable.create(() => this.$unregisterCommand(id)));
|
||||
}
|
||||
$unregisterCommand(id: string): void {
|
||||
const command = this.commands.get(id);
|
||||
if (command) {
|
||||
command.dispose();
|
||||
this.commands.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
$registerHandler(id: string): void {
|
||||
this.handlers.set(id, this.contributions.registerCommandHandler(id, (...args) =>
|
||||
this.proxy.$executeCommand(id, ...args.map(arg => this.argumentProcessors.reduce((currentValue, processor) => processor.processArgument(currentValue), arg)))
|
||||
));
|
||||
this.toDispose.push(Disposable.create(() => this.$unregisterHandler(id)));
|
||||
}
|
||||
$unregisterHandler(id: string): void {
|
||||
const handler = this.handlers.get(id);
|
||||
if (handler) {
|
||||
handler.dispose();
|
||||
this.handlers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async $executeCommand<T>(id: string, ...args: any[]): Promise<T | undefined> {
|
||||
if (!this.delegate.getCommand(id)) {
|
||||
throw new Error(`Command with id '${id}' is not registered.`);
|
||||
}
|
||||
try {
|
||||
return await this.delegate.executeCommand<T>(id, ...args);
|
||||
} catch (e) {
|
||||
// Command handler may be not active at the moment so the error must be caught. See https://github.com/eclipse-theia/theia/pull/6687#discussion_r354810079
|
||||
if ('code' in e && e['code'] === 'NO_ACTIVE_HANDLER') {
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$getKeyBinding(commandId: string): PromiseLike<theia.CommandKeyBinding[] | undefined> {
|
||||
try {
|
||||
const keyBindings = this.keyBinding.getKeybindingsForCommand(commandId);
|
||||
if (keyBindings) {
|
||||
// transform inner type to CommandKeyBinding
|
||||
return Promise.resolve(keyBindings.map(keyBinding => ({ id: commandId, value: keyBinding.keybinding })));
|
||||
} else {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
$getCommands(): PromiseLike<string[]> {
|
||||
return Promise.resolve(this.delegate.commandIds);
|
||||
}
|
||||
|
||||
}
|
||||
104
packages/plugin-ext/src/main/browser/commands.ts
Normal file
104
packages/plugin-ext/src/main/browser/commands.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Command, CommandService } from '@theia/core/lib/common/command';
|
||||
import { AbstractDialog } from '@theia/core/lib/browser';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import * as DOMPurify from '@theia/core/shared/dompurify';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
@injectable()
|
||||
export class OpenUriCommandHandler {
|
||||
public static readonly COMMAND_METADATA: Command = {
|
||||
id: 'theia.open'
|
||||
};
|
||||
|
||||
private openNewTabDialog: OpenNewTabDialog;
|
||||
|
||||
constructor(
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService,
|
||||
@inject(CommandService)
|
||||
protected readonly commandService: CommandService
|
||||
) {
|
||||
this.openNewTabDialog = new OpenNewTabDialog(windowService);
|
||||
}
|
||||
|
||||
public execute(resource: URI | string | undefined): void {
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uriString = resource.toString();
|
||||
if (uriString.startsWith('http://') || uriString.startsWith('https://')) {
|
||||
this.openWebUri(uriString);
|
||||
} else {
|
||||
this.commandService.executeCommand('editor.action.openLink', uriString);
|
||||
}
|
||||
}
|
||||
|
||||
private openWebUri(uri: string): void {
|
||||
try {
|
||||
this.windowService.openNewWindow(uri);
|
||||
} catch (err) {
|
||||
// browser has blocked opening of a new tab
|
||||
this.openNewTabDialog.showOpenNewTabDialog(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OpenNewTabDialog extends AbstractDialog<string> {
|
||||
protected readonly windowService: WindowService;
|
||||
protected readonly openButton: HTMLButtonElement;
|
||||
protected readonly messageNode: HTMLDivElement;
|
||||
protected readonly linkNode: HTMLAnchorElement;
|
||||
value: string;
|
||||
|
||||
constructor(windowService: WindowService) {
|
||||
super({
|
||||
title: nls.localize('theia/plugin/blockNewTab', 'Your browser prevented opening of a new tab')
|
||||
});
|
||||
this.windowService = windowService;
|
||||
|
||||
this.linkNode = document.createElement('a');
|
||||
this.linkNode.target = '_blank';
|
||||
this.linkNode.setAttribute('style', 'color: var(--theia-editorWidget-foreground);');
|
||||
this.contentNode.appendChild(this.linkNode);
|
||||
|
||||
const messageNode = document.createElement('div');
|
||||
messageNode.innerText = 'You are going to open: ';
|
||||
messageNode.appendChild(this.linkNode);
|
||||
this.contentNode.appendChild(messageNode);
|
||||
|
||||
this.appendCloseButton();
|
||||
this.openButton = this.appendAcceptButton(nls.localizeByDefault('Open'));
|
||||
}
|
||||
|
||||
showOpenNewTabDialog(uri: string): void {
|
||||
this.value = uri;
|
||||
|
||||
this.linkNode.innerHTML = DOMPurify.sanitize(uri);
|
||||
this.linkNode.href = uri;
|
||||
this.openButton.onclick = () => {
|
||||
this.windowService.openNewWindow(uri);
|
||||
};
|
||||
|
||||
// show dialog window to user
|
||||
this.open();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { Disposable } from '@theia/core/lib/common';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentGlyphWidget.ts
|
||||
|
||||
export class CommentGlyphWidget implements Disposable {
|
||||
|
||||
private lineNumber!: number;
|
||||
private editor: monaco.editor.ICodeEditor;
|
||||
private commentsDecorations: string[] = [];
|
||||
readonly commentsOptions: monaco.editor.IModelDecorationOptions;
|
||||
constructor(editor: monaco.editor.ICodeEditor) {
|
||||
this.commentsOptions = {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: 'comment-range-glyph comment-thread'
|
||||
};
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
getPosition(): number {
|
||||
const model = this.editor.getModel();
|
||||
const range = model && this.commentsDecorations && this.commentsDecorations.length
|
||||
? model.getDecorationRange(this.commentsDecorations[0])
|
||||
: null;
|
||||
|
||||
return range ? range.startLineNumber : this.lineNumber;
|
||||
}
|
||||
|
||||
setLineNumber(lineNumber: number): void {
|
||||
this.lineNumber = lineNumber;
|
||||
const commentsDecorations = [{
|
||||
range: {
|
||||
startLineNumber: lineNumber, startColumn: 1,
|
||||
endLineNumber: lineNumber, endColumn: 1
|
||||
},
|
||||
options: this.commentsOptions
|
||||
}];
|
||||
|
||||
this.commentsDecorations = this.editor.deltaDecorations(this.commentsDecorations, commentsDecorations);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.commentsDecorations) {
|
||||
this.editor.deltaDecorations(this.commentsDecorations, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,791 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { MonacoEditorZoneWidget } from '@theia/monaco/lib/browser/monaco-editor-zone-widget';
|
||||
import {
|
||||
Comment,
|
||||
CommentMode,
|
||||
CommentThread,
|
||||
CommentThreadState,
|
||||
CommentThreadCollapsibleState
|
||||
} from '../../../common/plugin-api-rpc-model';
|
||||
import { CommentGlyphWidget } from './comment-glyph-widget';
|
||||
import { BaseWidget, DISABLED_CLASS } from '@theia/core/lib/browser';
|
||||
import * as React from '@theia/core/shared/react';
|
||||
import { MouseTargetType } from '@theia/editor/lib/browser';
|
||||
import { CommentsService } from './comments-service';
|
||||
import {
|
||||
CommandMenu,
|
||||
CommandRegistry,
|
||||
CompoundMenuNode,
|
||||
isObject,
|
||||
DisposableCollection,
|
||||
MenuModelRegistry,
|
||||
MenuPath
|
||||
} from '@theia/core/lib/common';
|
||||
import { CommentsContext } from './comments-context';
|
||||
import { RefObject } from '@theia/core/shared/react';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { createRoot, Root } from '@theia/core/shared/react-dom/client';
|
||||
import { CommentAuthorInformation } from '@theia/plugin';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts
|
||||
|
||||
export const COMMENT_THREAD_CONTEXT: MenuPath = ['comment_thread-context-menu'];
|
||||
export const COMMENT_CONTEXT: MenuPath = ['comment-context-menu'];
|
||||
export const COMMENT_TITLE: MenuPath = ['comment-title-menu'];
|
||||
|
||||
export class CommentThreadWidget extends BaseWidget {
|
||||
|
||||
protected readonly zoneWidget: MonacoEditorZoneWidget;
|
||||
protected readonly containerNodeRoot: Root;
|
||||
protected readonly commentGlyphWidget: CommentGlyphWidget;
|
||||
protected readonly commentFormRef: RefObject<CommentForm> = React.createRef<CommentForm>();
|
||||
|
||||
protected isExpanded?: boolean;
|
||||
|
||||
constructor(
|
||||
editor: monaco.editor.IStandaloneCodeEditor,
|
||||
private _owner: string,
|
||||
private _commentThread: CommentThread,
|
||||
private commentService: CommentsService,
|
||||
protected readonly menus: MenuModelRegistry,
|
||||
protected readonly commentsContext: CommentsContext,
|
||||
protected readonly contextKeyService: ContextKeyService,
|
||||
protected readonly commands: CommandRegistry
|
||||
) {
|
||||
super();
|
||||
this.toDispose.push(this.zoneWidget = new MonacoEditorZoneWidget(editor));
|
||||
this.containerNodeRoot = createRoot(this.zoneWidget.containerNode);
|
||||
this.toDispose.push(this.commentGlyphWidget = new CommentGlyphWidget(editor));
|
||||
this.toDispose.push(this._commentThread.onDidChangeCollapsibleState(state => {
|
||||
if (state === CommentThreadCollapsibleState.Expanded && !this.isExpanded) {
|
||||
const lineNumber = this._commentThread.range?.startLineNumber ?? 0;
|
||||
|
||||
this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (state === CommentThreadCollapsibleState.Collapsed && this.isExpanded) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
}));
|
||||
this.commentsContext.commentIsEmpty.set(true);
|
||||
this.toDispose.push(this.zoneWidget.editor.onMouseDown(e => this.onEditorMouseDown(e)));
|
||||
|
||||
this.toDispose.push(this._commentThread.onDidChangeCanReply(_canReply => {
|
||||
const commentForm = this.commentFormRef.current;
|
||||
if (commentForm) {
|
||||
commentForm.update();
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this._commentThread.onDidChangeState(_state => {
|
||||
this.update();
|
||||
}));
|
||||
const contextMenu = this.menus.getMenu(COMMENT_THREAD_CONTEXT);
|
||||
contextMenu?.children.forEach(node => {
|
||||
if (node.onDidChange) {
|
||||
this.toDispose.push(node.onDidChange(() => {
|
||||
const commentForm = this.commentFormRef.current;
|
||||
if (commentForm) {
|
||||
commentForm.update();
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getGlyphPosition(): number {
|
||||
return this.commentGlyphWidget.getPosition();
|
||||
}
|
||||
|
||||
public collapse(): void {
|
||||
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed;
|
||||
if (this._commentThread.comments && this._commentThread.comments.length === 0) {
|
||||
this.deleteCommentThread();
|
||||
}
|
||||
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private deleteCommentThread(): void {
|
||||
this.dispose();
|
||||
this.commentService.disposeCommentThread(this.owner, this._commentThread.threadId);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
super.dispose();
|
||||
if (this.commentGlyphWidget) {
|
||||
this.commentGlyphWidget.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
toggleExpand(lineNumber: number): void {
|
||||
if (this.isExpanded) {
|
||||
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Collapsed;
|
||||
this.hide();
|
||||
if (!this._commentThread.comments || !this._commentThread.comments.length) {
|
||||
this.deleteCommentThread();
|
||||
}
|
||||
} else {
|
||||
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded;
|
||||
this.display({ afterLineNumber: lineNumber, afterColumn: 1, heightInLines: 2 });
|
||||
}
|
||||
}
|
||||
|
||||
override hide(): void {
|
||||
this.zoneWidget.hide();
|
||||
this.isExpanded = false;
|
||||
super.hide();
|
||||
}
|
||||
|
||||
display(options: MonacoEditorZoneWidget.Options): void {
|
||||
this.isExpanded = true;
|
||||
if (this._commentThread.collapsibleState && this._commentThread.collapsibleState !== CommentThreadCollapsibleState.Expanded) {
|
||||
return;
|
||||
}
|
||||
this.commentGlyphWidget.setLineNumber(options.afterLineNumber);
|
||||
this._commentThread.collapsibleState = CommentThreadCollapsibleState.Expanded;
|
||||
this.zoneWidget.show(options);
|
||||
this.update();
|
||||
}
|
||||
|
||||
private onEditorMouseDown(e: monaco.editor.IEditorMouseEvent): void {
|
||||
const range = e.target.range;
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.event.leftButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.target.detail;
|
||||
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
|
||||
|
||||
// don't collide with folding and git decorations
|
||||
if (gutterOffsetX > 14) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseDownInfo = { lineNumber: range.startLineNumber };
|
||||
|
||||
const { lineNumber } = mouseDownInfo;
|
||||
|
||||
if (!range || range.startLineNumber !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.target.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.commentGlyphWidget && this.commentGlyphWidget.getPosition() !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.element.className.indexOf('comment-thread') >= 0) {
|
||||
this.toggleExpand(lineNumber);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._commentThread.collapsibleState === CommentThreadCollapsibleState.Collapsed) {
|
||||
this.display({ afterLineNumber: mouseDownInfo.lineNumber, heightInLines: 2 });
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
public get owner(): string {
|
||||
return this._owner;
|
||||
}
|
||||
|
||||
public get commentThread(): CommentThread {
|
||||
return this._commentThread;
|
||||
}
|
||||
|
||||
private getThreadLabel(): string {
|
||||
let label: string | undefined;
|
||||
label = this._commentThread.label;
|
||||
|
||||
if (label === undefined) {
|
||||
if (this._commentThread.comments && this._commentThread.comments.length) {
|
||||
const onlyUnique = (value: Comment, index: number, self: Comment[]) => self.indexOf(value) === index;
|
||||
const participantsList = this._commentThread.comments.filter(onlyUnique).map(comment => `@${comment.userName}`).join(', ');
|
||||
const resolutionState = this._commentThread.state === CommentThreadState.Resolved ? '(Resolved)' : '(Unresolved)';
|
||||
label = `Participants: ${participantsList} ${resolutionState}`;
|
||||
} else {
|
||||
label = 'Start discussion';
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
override update(): void {
|
||||
if (!this.isExpanded) {
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2);
|
||||
const lineHeight = this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight);
|
||||
const arrowHeight = Math.round(lineHeight / 3);
|
||||
const frameThickness = Math.round(lineHeight / 9) * 2;
|
||||
const body = this.zoneWidget.containerNode.getElementsByClassName('body')[0];
|
||||
|
||||
const computedLinesNumber = Math.ceil((headHeight + (body?.clientHeight ?? 0) + arrowHeight + frameThickness + 8 /** margin bottom to avoid margin collapse */)
|
||||
/ lineHeight);
|
||||
this.zoneWidget.show({ afterLineNumber: this._commentThread.range?.startLineNumber ?? 0, heightInLines: computedLinesNumber });
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
const headHeight = Math.ceil(this.zoneWidget.editor.getOption(monaco.editor.EditorOption.lineHeight) * 1.2);
|
||||
this.containerNodeRoot.render(<div className={'review-widget'}>
|
||||
<div className={'head'} style={{ height: headHeight, lineHeight: `${headHeight}px` }}>
|
||||
<div className={'review-title'}>
|
||||
<span className={'filename'}>{this.getThreadLabel()}</span>
|
||||
</div>
|
||||
<div className={'review-actions'}>
|
||||
<div className={'monaco-action-bar animated'}>
|
||||
<ul className={'actions-container'} role={'toolbar'}>
|
||||
<li className={'action-item'} role={'presentation'}>
|
||||
<a className={'action-label codicon expand-review-action codicon-chevron-up'}
|
||||
role={'button'}
|
||||
tabIndex={0}
|
||||
title={'Collapse'}
|
||||
onClick={() => this.collapse()}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'body'}>
|
||||
<div className={'comments-container'} role={'presentation'} tabIndex={0}>
|
||||
{this._commentThread.comments?.map((comment, index) => <ReviewComment
|
||||
key={index}
|
||||
contextKeyService={this.contextKeyService}
|
||||
commentsContext={this.commentsContext}
|
||||
menus={this.menus}
|
||||
comment={comment}
|
||||
commentForm={this.commentFormRef}
|
||||
commands={this.commands}
|
||||
commentThread={this._commentThread}
|
||||
/>)}
|
||||
</div>
|
||||
<CommentForm contextKeyService={this.contextKeyService}
|
||||
commentsContext={this.commentsContext}
|
||||
commands={this.commands}
|
||||
commentThread={this._commentThread}
|
||||
menus={this.menus}
|
||||
widget={this}
|
||||
ref={this.commentFormRef}
|
||||
/>
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
namespace CommentForm {
|
||||
export interface Props {
|
||||
menus: MenuModelRegistry,
|
||||
commentThread: CommentThread;
|
||||
commands: CommandRegistry;
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
widget: CommentThreadWidget;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentForm<P extends CommentForm.Props = CommentForm.Props> extends React.Component<P, CommentForm.State> {
|
||||
private inputRef: RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>();
|
||||
private inputValue: string = '';
|
||||
private readonly getInput = () => this.inputValue;
|
||||
private toDisposeOnUnmount = new DisposableCollection();
|
||||
private readonly clearInput: () => void = () => {
|
||||
const input = this.inputRef.current;
|
||||
if (input) {
|
||||
this.inputValue = '';
|
||||
input.value = this.inputValue;
|
||||
this.props.commentsContext.commentIsEmpty.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
update(): void {
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
protected expand = () => {
|
||||
this.setState({ expanded: true });
|
||||
// Wait for the widget to be rendered.
|
||||
setTimeout(() => {
|
||||
// Update the widget's height.
|
||||
this.props.widget.update();
|
||||
this.inputRef.current?.focus();
|
||||
}, 100);
|
||||
};
|
||||
protected collapse = () => {
|
||||
this.setState({ expanded: false });
|
||||
// Wait for the widget to be rendered.
|
||||
setTimeout(() => {
|
||||
// Update the widget's height.
|
||||
this.props.widget.update();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
override componentDidMount(): void {
|
||||
// Wait for the widget to be rendered.
|
||||
setTimeout(() => {
|
||||
this.inputRef.current?.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
override componentWillUnmount(): void {
|
||||
this.toDisposeOnUnmount.dispose();
|
||||
}
|
||||
|
||||
private readonly onInput: (event: React.FormEvent) => void = (event: React.FormEvent) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const value = (event.target as any).value;
|
||||
if (this.inputValue.length === 0 || value.length === 0) {
|
||||
this.props.commentsContext.commentIsEmpty.set(value.length === 0);
|
||||
}
|
||||
this.inputValue = value;
|
||||
};
|
||||
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
this.state = {
|
||||
expanded: false
|
||||
};
|
||||
|
||||
const setState = this.setState.bind(this);
|
||||
this.setState = newState => {
|
||||
setState(newState);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the comment form with textarea, actions, and reply button.
|
||||
*
|
||||
* @returns The rendered comment form
|
||||
*/
|
||||
protected renderCommentForm(): React.ReactNode {
|
||||
const { commentThread, commentsContext, contextKeyService, menus } = this.props;
|
||||
const hasExistingComments = commentThread.comments && commentThread.comments.length > 0;
|
||||
|
||||
// Determine when to show the expanded form:
|
||||
// - When state.expanded is true (user clicked the reply button)
|
||||
// - When there are no existing comments (new thread)
|
||||
const shouldShowExpanded = this.state.expanded || (commentThread.comments && commentThread.comments.length === 0);
|
||||
|
||||
return commentThread.canReply ? (
|
||||
<div className={`comment-form${shouldShowExpanded ? ' expand' : ''}`}>
|
||||
<div className={'theia-comments-input-message-container'}>
|
||||
<textarea className={'theia-comments-input-message theia-input'}
|
||||
spellCheck={false}
|
||||
placeholder={hasExistingComments ? 'Reply...' : 'Type a new comment'}
|
||||
onInput={this.onInput}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onBlur={(event: any) => {
|
||||
if (event.target.value.length > 0) {
|
||||
return;
|
||||
}
|
||||
if (event.relatedTarget && event.relatedTarget.className === 'comments-button comments-text-button theia-button') {
|
||||
this.state = { expanded: false };
|
||||
return;
|
||||
}
|
||||
this.collapse();
|
||||
}}
|
||||
ref={this.inputRef}>
|
||||
</textarea>
|
||||
</div>
|
||||
<CommentActions menu={menus.getMenu(COMMENT_THREAD_CONTEXT)}
|
||||
menuPath={[]}
|
||||
contextKeyService={contextKeyService}
|
||||
commentsContext={commentsContext}
|
||||
commentThread={commentThread}
|
||||
getInput={this.getInput}
|
||||
clearInput={this.clearInput}
|
||||
/>
|
||||
<button className={'review-thread-reply-button'} title={'Reply...'} onClick={this.expand}>Reply...</button>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the author information section.
|
||||
*
|
||||
* @param authorInfo The author information to display
|
||||
* @returns The rendered author information section
|
||||
*/
|
||||
protected renderAuthorInfo(authorInfo: CommentAuthorInformation): React.ReactNode {
|
||||
return (
|
||||
<div className={'avatar-container'}>
|
||||
{authorInfo.iconPath && (
|
||||
<img className={'avatar'} src={authorInfo.iconPath.toString()} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { commentThread } = this.props;
|
||||
|
||||
if (!commentThread.canReply) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there's author info, wrap in a container with author info on the left
|
||||
if (isCommentAuthorInformation(commentThread.canReply)) {
|
||||
return (
|
||||
<div className={'review-comment'}>
|
||||
{this.renderAuthorInfo(commentThread.canReply)}
|
||||
<div className={'review-comment-contents'}>
|
||||
<div className={'comment-title monaco-mouse-cursor-text'}>
|
||||
<strong className={'author'}>{commentThread.canReply.name}</strong>
|
||||
</div>
|
||||
{this.renderCommentForm()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Otherwise, just return the comment form
|
||||
return (
|
||||
<div className={'review-comment'}>
|
||||
<div className={'review-comment-contents'}>
|
||||
{this.renderCommentForm()}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
function isCommentAuthorInformation(item: unknown): item is CommentAuthorInformation {
|
||||
return isObject(item) && 'name' in item;
|
||||
}
|
||||
|
||||
namespace ReviewComment {
|
||||
export interface Props {
|
||||
menus: MenuModelRegistry,
|
||||
comment: Comment;
|
||||
commentThread: CommentThread;
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
commands: CommandRegistry;
|
||||
commentForm: RefObject<CommentForm>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
hover: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class ReviewComment<P extends ReviewComment.Props = ReviewComment.Props> extends React.Component<P, ReviewComment.State> {
|
||||
|
||||
constructor(props: P) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hover: false
|
||||
};
|
||||
|
||||
const setState = this.setState.bind(this);
|
||||
this.setState = newState => {
|
||||
setState(newState);
|
||||
};
|
||||
}
|
||||
|
||||
protected detectHover = (element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const hover = element.matches(':hover');
|
||||
this.setState({ hover });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
protected showHover = () => this.setState({ hover: true });
|
||||
protected hideHover = () => this.setState({ hover: false });
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { comment, commentForm, contextKeyService, commentsContext, menus, commands, commentThread } = this.props;
|
||||
const commentUniqueId = comment.uniqueIdInThread;
|
||||
const { hover } = this.state;
|
||||
commentsContext.comment.set(comment.contextValue);
|
||||
return <div className={'review-comment'}
|
||||
tabIndex={-1}
|
||||
aria-label={`${comment.userName}, ${comment.body.value}`}
|
||||
ref={this.detectHover}
|
||||
onMouseEnter={this.showHover}
|
||||
onMouseLeave={this.hideHover}>
|
||||
<div className={'avatar-container'}>
|
||||
<img className={'avatar'} src={comment.userIconPath} />
|
||||
</div>
|
||||
<div className={'review-comment-contents'}>
|
||||
<div className={'comment-title monaco-mouse-cursor-text'}>
|
||||
<strong className={'author'}>{comment.userName}</strong>
|
||||
<small className={'timestamp'}>{this.localeDate(comment.timestamp)}</small>
|
||||
<span className={'isPending'}>{comment.label}</span>
|
||||
<div className={'theia-comments-inline-actions-container'}>
|
||||
<div className={'theia-comments-inline-actions'} role={'toolbar'}>
|
||||
{hover && menus.getMenuNode(COMMENT_TITLE) && menus.getMenu(COMMENT_TITLE)?.children.map((node, index): React.ReactNode => CommandMenu.is(node) &&
|
||||
<CommentsInlineAction key={index} {...{
|
||||
node, nodePath: [...COMMENT_TITLE, node.id], commands, commentThread, commentUniqueId,
|
||||
contextKeyService, commentsContext
|
||||
}} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CommentBody value={comment.body.value}
|
||||
isVisible={comment.mode === undefined || comment.mode === CommentMode.Preview} />
|
||||
<CommentEditContainer contextKeyService={contextKeyService}
|
||||
commentsContext={commentsContext}
|
||||
menus={menus}
|
||||
comment={comment}
|
||||
commentThread={commentThread}
|
||||
commentForm={commentForm}
|
||||
commands={commands} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
protected localeDate(timestamp: string | undefined): string {
|
||||
if (timestamp === undefined) {
|
||||
return '';
|
||||
}
|
||||
const date = new Date(timestamp);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
namespace CommentBody {
|
||||
export interface Props {
|
||||
value: string
|
||||
isVisible: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentBody extends React.Component<CommentBody.Props> {
|
||||
override render(): React.ReactNode {
|
||||
const { value, isVisible } = this.props;
|
||||
if (!isVisible) {
|
||||
return false;
|
||||
}
|
||||
return <div className={'comment-body monaco-mouse-cursor-text'}>
|
||||
<div>
|
||||
<p>{value}</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
namespace CommentEditContainer {
|
||||
export interface Props {
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
menus: MenuModelRegistry,
|
||||
comment: Comment;
|
||||
commentThread: CommentThread;
|
||||
commentForm: RefObject<CommentForm>;
|
||||
commands: CommandRegistry;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentEditContainer extends React.Component<CommentEditContainer.Props> {
|
||||
private readonly inputRef: RefObject<HTMLTextAreaElement> = React.createRef<HTMLTextAreaElement>();
|
||||
private dirtyCommentMode: CommentMode | undefined;
|
||||
private dirtyCommentFormState: boolean | undefined;
|
||||
|
||||
override componentDidUpdate(prevProps: Readonly<CommentEditContainer.Props>, prevState: Readonly<{}>): void {
|
||||
const commentFormState = this.props.commentForm.current?.state;
|
||||
const mode = this.props.comment.mode;
|
||||
if (this.dirtyCommentMode !== mode || (this.dirtyCommentFormState !== commentFormState?.expanded && !commentFormState?.expanded)) {
|
||||
const currentInput = this.inputRef.current;
|
||||
if (currentInput) {
|
||||
// Wait for the widget to be rendered.
|
||||
setTimeout(() => {
|
||||
currentInput.focus();
|
||||
currentInput.setSelectionRange(currentInput.value.length, currentInput.value.length);
|
||||
}, 50);
|
||||
}
|
||||
}
|
||||
this.dirtyCommentMode = mode;
|
||||
this.dirtyCommentFormState = commentFormState?.expanded;
|
||||
}
|
||||
|
||||
override render(): React.ReactNode {
|
||||
const { menus, comment, commands, commentThread, contextKeyService, commentsContext } = this.props;
|
||||
if (!(comment.mode === CommentMode.Editing)) {
|
||||
return false;
|
||||
}
|
||||
return <div className={'edit-container'}>
|
||||
<div className={'edit-textarea'}>
|
||||
<div className={'theia-comments-input-message-container'}>
|
||||
<textarea className={'theia-comments-input-message theia-input'}
|
||||
spellCheck={false}
|
||||
defaultValue={comment.body.value}
|
||||
ref={this.inputRef} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'form-actions'}>
|
||||
{menus.getMenu(COMMENT_CONTEXT)?.children.map((node, index): React.ReactNode => {
|
||||
const onClick = () => {
|
||||
commands.executeCommand(node.id, {
|
||||
commentControlHandle: commentThread.controllerHandle,
|
||||
commentThreadHandle: commentThread.commentThreadHandle,
|
||||
commentUniqueId: comment.uniqueIdInThread,
|
||||
text: this.inputRef.current ? this.inputRef.current.value : ''
|
||||
});
|
||||
};
|
||||
return CommandMenu.is(node) &&
|
||||
<CommentAction key={index} {...{
|
||||
node, nodePath: [...COMMENT_CONTEXT, node.id], comment,
|
||||
commands, onClick, contextKeyService, commentsContext, commentThread
|
||||
}} />;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
namespace CommentsInlineAction {
|
||||
export interface Props {
|
||||
nodePath: MenuPath,
|
||||
node: CommandMenu;
|
||||
commentThread: CommentThread;
|
||||
commentUniqueId: number;
|
||||
commands: CommandRegistry;
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentsInlineAction extends React.Component<CommentsInlineAction.Props> {
|
||||
override render(): React.ReactNode {
|
||||
const { node, nodePath, commands, contextKeyService, commentThread, commentUniqueId } = this.props;
|
||||
if (node.isVisible(nodePath, contextKeyService, undefined, {
|
||||
thread: commentThread,
|
||||
commentUniqueId
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
return <div className='theia-comments-inline-action'>
|
||||
<a className={node.icon}
|
||||
title={node.label}
|
||||
onClick={() => {
|
||||
commands.executeCommand(node.id, {
|
||||
thread: commentThread,
|
||||
commentUniqueId: commentUniqueId
|
||||
});
|
||||
}} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
namespace CommentActions {
|
||||
export interface Props {
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
menuPath: MenuPath,
|
||||
menu: CompoundMenuNode | undefined;
|
||||
commentThread: CommentThread;
|
||||
getInput: () => string;
|
||||
clearInput: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentActions extends React.Component<CommentActions.Props> {
|
||||
override render(): React.ReactNode {
|
||||
const { contextKeyService, commentsContext, menuPath, menu, commentThread, getInput, clearInput } = this.props;
|
||||
return <div className={'form-actions'}>
|
||||
{menu?.children.map((node, index) => CommandMenu.is(node) &&
|
||||
<CommentAction key={index}
|
||||
nodePath={menuPath}
|
||||
node={node}
|
||||
onClick={() => {
|
||||
node.run(
|
||||
[...menuPath, menu.id], {
|
||||
thread: commentThread,
|
||||
text: getInput()
|
||||
});
|
||||
clearInput();
|
||||
}}
|
||||
commentThread={commentThread}
|
||||
contextKeyService={contextKeyService}
|
||||
commentsContext={commentsContext}
|
||||
/>)}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
namespace CommentAction {
|
||||
export interface Props {
|
||||
commentThread: CommentThread;
|
||||
contextKeyService: ContextKeyService;
|
||||
commentsContext: CommentsContext;
|
||||
nodePath: MenuPath,
|
||||
node: CommandMenu;
|
||||
onClick: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentAction extends React.Component<CommentAction.Props> {
|
||||
override render(): React.ReactNode {
|
||||
const classNames = ['comments-button', 'comments-text-button', 'theia-button'];
|
||||
const { node, nodePath, contextKeyService, onClick, commentThread } = this.props;
|
||||
if (!node.isVisible(nodePath, contextKeyService, undefined, {
|
||||
thread: commentThread
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
const isEnabled = node.isEnabled(nodePath, {
|
||||
thread: commentThread
|
||||
});
|
||||
if (!isEnabled) {
|
||||
classNames.push(DISABLED_CLASS);
|
||||
}
|
||||
return <button
|
||||
className={classNames.join(' ')}
|
||||
tabIndex={0}
|
||||
role={'button'}
|
||||
onClick={() => {
|
||||
if (isEnabled) {
|
||||
onClick();
|
||||
}
|
||||
}}>{node.label}
|
||||
</button>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
|
||||
|
||||
@injectable()
|
||||
export class CommentsContext {
|
||||
|
||||
@inject(ContextKeyService)
|
||||
protected readonly contextKeyService: ContextKeyService;
|
||||
protected readonly contextKeys: Set<string> = new Set();
|
||||
protected _commentIsEmpty: ContextKey<boolean>;
|
||||
protected _commentController: ContextKey<string | undefined>;
|
||||
protected _comment: ContextKey<string | undefined>;
|
||||
|
||||
get commentController(): ContextKey<string | undefined> {
|
||||
return this._commentController;
|
||||
}
|
||||
|
||||
get comment(): ContextKey<string | undefined> {
|
||||
return this._comment;
|
||||
}
|
||||
|
||||
get commentIsEmpty(): ContextKey<boolean> {
|
||||
return this._commentIsEmpty;
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.contextKeys.add('commentIsEmpty');
|
||||
this._commentController = this.contextKeyService.createKey<string | undefined>('commentController', undefined);
|
||||
this._comment = this.contextKeyService.createKey<string | undefined>('comment', undefined);
|
||||
this._commentIsEmpty = this.contextKeyService.createKey<boolean>('commentIsEmpty', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { CommentingRangeDecorator } from './comments-decorator';
|
||||
import { EditorManager, EditorMouseEvent, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
|
||||
import { CommentThreadWidget } from './comment-thread-widget';
|
||||
import { CommentsService, CommentInfoMain } from './comments-service';
|
||||
import { CommentThread } from '../../../common/plugin-api-rpc-model';
|
||||
import { CommandRegistry, DisposableCollection, MenuModelRegistry } from '@theia/core/lib/common';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
|
||||
import { Uri } from '@theia/plugin';
|
||||
import { CommentsContext } from './comments-context';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/comments.contribution.ts
|
||||
|
||||
@injectable()
|
||||
export class CommentsContribution {
|
||||
|
||||
private addInProgress!: boolean;
|
||||
private commentWidgets: CommentThreadWidget[];
|
||||
private commentInfos: CommentInfoMain[];
|
||||
private emptyThreadsToAddQueue: [number, EditorMouseEvent | undefined][] = [];
|
||||
|
||||
@inject(MenuModelRegistry) protected readonly menus: MenuModelRegistry;
|
||||
@inject(CommentsContext) protected readonly commentsContext: CommentsContext;
|
||||
@inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
|
||||
@inject(CommandRegistry) protected readonly commands: CommandRegistry;
|
||||
|
||||
constructor(@inject(CommentingRangeDecorator) protected readonly rangeDecorator: CommentingRangeDecorator,
|
||||
@inject(CommentsService) protected readonly commentService: CommentsService,
|
||||
@inject(EditorManager) protected readonly editorManager: EditorManager) {
|
||||
this.commentWidgets = [];
|
||||
this.commentInfos = [];
|
||||
this.commentService.onDidSetResourceCommentInfos(e => {
|
||||
const editor = this.getCurrentEditor();
|
||||
const editorURI = editor && editor.editor instanceof MonacoDiffEditor && editor.editor.diffEditor.getModifiedEditor().getModel();
|
||||
if (editorURI && editorURI.toString() === e.resource.toString()) {
|
||||
this.setComments(e.commentInfos.filter(commentInfo => commentInfo !== null));
|
||||
}
|
||||
});
|
||||
this.editorManager.onCreated(async widget => {
|
||||
const disposables = new DisposableCollection();
|
||||
const editor = widget.editor;
|
||||
if (editor instanceof MonacoDiffEditor) {
|
||||
const originalEditorModel = editor.diffEditor.getOriginalEditor().getModel();
|
||||
if (originalEditorModel) {
|
||||
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
|
||||
const originalComments = await this.commentService.getComments(originalEditorModel.uri as Uri);
|
||||
if (originalComments) {
|
||||
this.rangeDecorator.update(editor.diffEditor.getOriginalEditor(), <CommentInfoMain[]>originalComments.filter(c => !!c));
|
||||
}
|
||||
}
|
||||
const modifiedEditorModel = editor.diffEditor.getModifiedEditor().getModel();
|
||||
if (modifiedEditorModel) {
|
||||
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
|
||||
const modifiedComments = await this.commentService.getComments(modifiedEditorModel.uri as Uri);
|
||||
if (modifiedComments) {
|
||||
this.rangeDecorator.update(editor.diffEditor.getModifiedEditor(), <CommentInfoMain[]>modifiedComments.filter(c => !!c));
|
||||
}
|
||||
}
|
||||
disposables.push(editor.onMouseDown(e => this.onEditorMouseDown(e)));
|
||||
disposables.push(this.commentService.onDidUpdateCommentThreads(async e => {
|
||||
const editorURI = editor.document.uri;
|
||||
const commentInfo = this.commentInfos.filter(info => info.owner === e.owner);
|
||||
if (!commentInfo || !commentInfo.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const added = e.added.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
const removed = e.removed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
const changed = e.changed.filter(thread => thread.resource && thread.resource.toString() === editorURI.toString());
|
||||
|
||||
removed.forEach(thread => {
|
||||
const matchedZones = this.commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner
|
||||
&& zoneWidget.commentThread.threadId === thread.threadId && zoneWidget.commentThread.threadId !== '');
|
||||
if (matchedZones.length) {
|
||||
const matchedZone = matchedZones[0];
|
||||
const index = this.commentWidgets.indexOf(matchedZone);
|
||||
this.commentWidgets.splice(index, 1);
|
||||
matchedZone.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
changed.forEach(thread => {
|
||||
const matchedZones = this.commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner
|
||||
&& zoneWidget.commentThread.threadId === thread.threadId);
|
||||
if (matchedZones.length) {
|
||||
const matchedZone = matchedZones[0];
|
||||
matchedZone.update();
|
||||
}
|
||||
});
|
||||
added.forEach(thread => {
|
||||
this.displayCommentThread(e.owner, thread);
|
||||
this.commentInfos.filter(info => info.owner === e.owner)[0].threads.push(thread);
|
||||
});
|
||||
})
|
||||
);
|
||||
editor.onDispose(() => {
|
||||
disposables.dispose();
|
||||
});
|
||||
this.beginCompute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onEditorMouseDown(e: EditorMouseEvent): void {
|
||||
let mouseDownInfo = null;
|
||||
|
||||
const range = e.target.range;
|
||||
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.type !== monaco.editor.MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = e.target.detail;
|
||||
const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
|
||||
|
||||
// don't collide with folding and git decorations
|
||||
if (gutterOffsetX > 14) {
|
||||
return;
|
||||
}
|
||||
|
||||
mouseDownInfo = { lineNumber: range.start };
|
||||
|
||||
const { lineNumber } = mouseDownInfo;
|
||||
mouseDownInfo = null;
|
||||
|
||||
if (!range || range.start !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.target.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.element.className.indexOf('comment-diff-added') >= 0) {
|
||||
this.addOrToggleCommentAtLine(e.target.position!.line + 1, e);
|
||||
}
|
||||
}
|
||||
|
||||
private async beginCompute(): Promise<void> {
|
||||
const editorModel = this.editor && this.editor.getModel();
|
||||
const editorURI = this.editor && editorModel && editorModel.uri;
|
||||
if (editorURI) {
|
||||
// need to cast because of vscode issue https://github.com/microsoft/vscode/issues/190584
|
||||
const comments = await this.commentService.getComments(editorURI as Uri);
|
||||
this.setComments(<CommentInfoMain[]>comments.filter(c => !!c));
|
||||
}
|
||||
}
|
||||
|
||||
private setComments(commentInfos: CommentInfoMain[]): void {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commentInfos = commentInfos;
|
||||
}
|
||||
|
||||
get editor(): monaco.editor.IStandaloneCodeEditor | undefined {
|
||||
const editor = this.getCurrentEditor();
|
||||
if (editor && editor.editor instanceof MonacoDiffEditor) {
|
||||
return editor.editor.diffEditor.getModifiedEditor();
|
||||
}
|
||||
}
|
||||
|
||||
private displayCommentThread(owner: string, thread: CommentThread): void {
|
||||
const editor = this.editor;
|
||||
if (editor) {
|
||||
const provider = this.commentService.getCommentController(owner);
|
||||
if (provider) {
|
||||
this.commentsContext.commentController.set(provider.id);
|
||||
}
|
||||
const zoneWidget = new CommentThreadWidget(editor, owner, thread, this.commentService, this.menus, this.commentsContext, this.contextKeyService, this.commands);
|
||||
zoneWidget.display({ afterLineNumber: thread.range?.startLineNumber || 0, heightInLines: 5 });
|
||||
const currentEditor = this.getCurrentEditor();
|
||||
if (currentEditor) {
|
||||
currentEditor.onDispose(() => zoneWidget.dispose());
|
||||
}
|
||||
this.commentWidgets.push(zoneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
public async addOrToggleCommentAtLine(lineNumber: number, e: EditorMouseEvent | undefined): Promise<void> {
|
||||
// If an add is already in progress, queue the next add and process it after the current one finishes to
|
||||
// prevent empty comment threads from being added to the same line.
|
||||
if (!this.addInProgress) {
|
||||
this.addInProgress = true;
|
||||
// The widget's position is undefined until the widget has been displayed, so rely on the glyph position instead
|
||||
const existingCommentsAtLine = this.commentWidgets.filter(widget => widget.getGlyphPosition() === lineNumber);
|
||||
if (existingCommentsAtLine.length) {
|
||||
existingCommentsAtLine.forEach(widget => widget.toggleExpand(lineNumber));
|
||||
this.processNextThreadToAdd();
|
||||
return;
|
||||
} else {
|
||||
this.addCommentAtLine(lineNumber, e);
|
||||
}
|
||||
} else {
|
||||
this.emptyThreadsToAddQueue.push([lineNumber, e]);
|
||||
}
|
||||
}
|
||||
|
||||
private processNextThreadToAdd(): void {
|
||||
this.addInProgress = false;
|
||||
const info = this.emptyThreadsToAddQueue.shift();
|
||||
if (info) {
|
||||
this.addOrToggleCommentAtLine(info[0], info[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentEditor(): EditorWidget | undefined {
|
||||
return this.editorManager.currentEditor;
|
||||
}
|
||||
|
||||
public addCommentAtLine(lineNumber: number, e: EditorMouseEvent | undefined): Promise<void> {
|
||||
const newCommentInfos = this.rangeDecorator.getMatchedCommentAction(lineNumber);
|
||||
const editor = this.getCurrentEditor();
|
||||
if (!editor) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (!newCommentInfos.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const { ownerId } = newCommentInfos[0]!;
|
||||
this.addCommentAtLine2(lineNumber, ownerId);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public addCommentAtLine2(lineNumber: number, ownerId: string): void {
|
||||
const editorModel = this.editor && this.editor.getModel();
|
||||
const editorURI = this.editor && editorModel && editorModel.uri;
|
||||
if (editorURI) {
|
||||
this.commentService.createCommentThreadTemplate(ownerId, URI.parse(editorURI.toString()), {
|
||||
startLineNumber: lineNumber,
|
||||
endLineNumber: lineNumber,
|
||||
startColumn: 1,
|
||||
endColumn: 1
|
||||
});
|
||||
this.processNextThreadToAdd();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { CommentInfoMain } from './comments-service';
|
||||
import { CommentingRanges, Range } from '../../../common/plugin-api-rpc-model';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
|
||||
@injectable()
|
||||
export class CommentingRangeDecorator {
|
||||
|
||||
private decorationOptions: monaco.editor.IModelDecorationOptions;
|
||||
private commentingRangeDecorations: CommentingRangeDecoration[] = [];
|
||||
|
||||
constructor() {
|
||||
this.decorationOptions = {
|
||||
isWholeLine: true,
|
||||
linesDecorationsClassName: 'comment-range-glyph comment-diff-added'
|
||||
};
|
||||
}
|
||||
|
||||
public update(editor: monaco.editor.ICodeEditor, commentInfos: CommentInfoMain[]): void {
|
||||
const model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentingRangeDecorations: CommentingRangeDecoration[] = [];
|
||||
for (const info of commentInfos) {
|
||||
info.commentingRanges.ranges.forEach(range => {
|
||||
commentingRangeDecorations.push(new CommentingRangeDecoration(editor, info.owner, info.extensionId, info.label,
|
||||
range, this.decorationOptions, info.commentingRanges));
|
||||
});
|
||||
}
|
||||
|
||||
const oldDecorations = this.commentingRangeDecorations.map(decoration => decoration.id);
|
||||
editor.deltaDecorations(oldDecorations, []);
|
||||
|
||||
this.commentingRangeDecorations = commentingRangeDecorations;
|
||||
}
|
||||
|
||||
public getMatchedCommentAction(line: number): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges }[] {
|
||||
const result = [];
|
||||
for (const decoration of this.commentingRangeDecorations) {
|
||||
const range = decoration.getActiveRange();
|
||||
if (range && range.startLineNumber <= line && line <= range.endLineNumber) {
|
||||
result.push(decoration.getCommentAction());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class CommentingRangeDecoration {
|
||||
private decorationId: string;
|
||||
|
||||
public get id(): string {
|
||||
return this.decorationId;
|
||||
}
|
||||
|
||||
constructor(private _editor: monaco.editor.ICodeEditor, private _ownerId: string, private _extensionId: string | undefined,
|
||||
private _label: string | undefined, private _range: Range, commentingOptions: monaco.editor.IModelDecorationOptions,
|
||||
private commentingRangesInfo: CommentingRanges) {
|
||||
const startLineNumber = _range.startLineNumber;
|
||||
const endLineNumber = _range.endLineNumber;
|
||||
const commentingRangeDecorations = [{
|
||||
range: {
|
||||
startLineNumber: startLineNumber, startColumn: 1,
|
||||
endLineNumber: endLineNumber, endColumn: 1
|
||||
},
|
||||
options: commentingOptions
|
||||
}];
|
||||
|
||||
this.decorationId = this._editor.deltaDecorations([], commentingRangeDecorations)[0];
|
||||
}
|
||||
|
||||
public getCommentAction(): { ownerId: string, extensionId: string | undefined, label: string | undefined, commentingRangesInfo: CommentingRanges } {
|
||||
return {
|
||||
extensionId: this._extensionId,
|
||||
label: this._label,
|
||||
ownerId: this._ownerId,
|
||||
commentingRangesInfo: this.commentingRangesInfo
|
||||
};
|
||||
}
|
||||
|
||||
public getOriginalRange(): Range {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
public getActiveRange(): Range | undefined {
|
||||
const range = this._editor.getModel()!.getDecorationRange(this.decorationId);
|
||||
if (range) {
|
||||
return range;
|
||||
}
|
||||
}
|
||||
}
|
||||
484
packages/plugin-ext/src/main/browser/comments/comments-main.ts
Normal file
484
packages/plugin-ext/src/main/browser/comments/comments-main.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import {
|
||||
Range,
|
||||
Comment,
|
||||
CommentInput,
|
||||
CommentOptions,
|
||||
CommentThread,
|
||||
CommentThreadChangedEvent
|
||||
} from '../../../common/plugin-api-rpc-model';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { CommentThreadCollapsibleState, CommentThreadState } from '../../../plugin/types-impl';
|
||||
import {
|
||||
CommentProviderFeatures,
|
||||
CommentsExt,
|
||||
CommentsMain,
|
||||
CommentThreadChanges,
|
||||
MAIN_RPC_CONTEXT
|
||||
} from '../../../common/plugin-api-rpc';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { CommentsService, CommentInfoMain } from './comments-service';
|
||||
import { UriComponents } from '../../../common/uri-components';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { CancellationToken } from '@theia/core/lib/common';
|
||||
import { RPCProtocol } from '../../../common/rpc-protocol';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { generateUuid } from '@theia/core/lib/common/uuid';
|
||||
import { CommentsContribution } from './comments-contribution';
|
||||
import { CommentAuthorInformation } from '@theia/plugin';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/api/browser/mainThreadComments.ts
|
||||
|
||||
export class CommentThreadImpl implements CommentThread, Disposable {
|
||||
private _input?: CommentInput;
|
||||
get input(): CommentInput | undefined {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
set input(value: CommentInput | undefined) {
|
||||
this._input = value;
|
||||
this.onDidChangeInputEmitter.fire(value);
|
||||
}
|
||||
|
||||
private readonly onDidChangeInputEmitter = new Emitter<CommentInput | undefined>();
|
||||
get onDidChangeInput(): Event<CommentInput | undefined> { return this.onDidChangeInputEmitter.event; }
|
||||
|
||||
private _label: string | undefined;
|
||||
|
||||
get label(): string | undefined {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set label(label: string | undefined) {
|
||||
this._label = label;
|
||||
this.onDidChangeLabelEmitter.fire(this._label);
|
||||
}
|
||||
|
||||
private readonly onDidChangeLabelEmitter = new Emitter<string | undefined>();
|
||||
readonly onDidChangeLabel: Event<string | undefined> = this.onDidChangeLabelEmitter.event;
|
||||
|
||||
private _contextValue: string | undefined;
|
||||
|
||||
get contextValue(): string | undefined {
|
||||
return this._contextValue;
|
||||
}
|
||||
|
||||
set contextValue(context: string | undefined) {
|
||||
this._contextValue = context;
|
||||
}
|
||||
|
||||
private _comments: Comment[] | undefined;
|
||||
|
||||
public get comments(): Comment[] | undefined {
|
||||
return this._comments;
|
||||
}
|
||||
|
||||
public set comments(newComments: Comment[] | undefined) {
|
||||
this._comments = newComments;
|
||||
this.onDidChangeCommentsEmitter.fire(this._comments);
|
||||
}
|
||||
|
||||
private readonly onDidChangeCommentsEmitter = new Emitter<Comment[] | undefined>();
|
||||
get onDidChangeComments(): Event<Comment[] | undefined> { return this.onDidChangeCommentsEmitter.event; }
|
||||
|
||||
set range(range: Range | undefined) {
|
||||
this._range = range;
|
||||
this.onDidChangeRangeEmitter.fire(this._range);
|
||||
}
|
||||
|
||||
get range(): Range | undefined {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
private readonly onDidChangeRangeEmitter = new Emitter<Range | undefined>();
|
||||
public onDidChangeRange = this.onDidChangeRangeEmitter.event;
|
||||
|
||||
private _collapsibleState: CommentThreadCollapsibleState | undefined;
|
||||
get collapsibleState(): CommentThreadCollapsibleState | undefined {
|
||||
return this._collapsibleState;
|
||||
}
|
||||
|
||||
set collapsibleState(newState: CommentThreadCollapsibleState | undefined) {
|
||||
this._collapsibleState = newState;
|
||||
this.onDidChangeCollapsibleStateEmitter.fire(this._collapsibleState);
|
||||
}
|
||||
|
||||
private readonly onDidChangeCollapsibleStateEmitter = new Emitter<CommentThreadCollapsibleState | undefined>();
|
||||
readonly onDidChangeCollapsibleState = this.onDidChangeCollapsibleStateEmitter.event;
|
||||
|
||||
private _state: CommentThreadState | undefined;
|
||||
get state(): CommentThreadState | undefined {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
set state(newState: CommentThreadState | undefined) {
|
||||
if (this._state !== newState) {
|
||||
this._state = newState;
|
||||
this.onDidChangeStateEmitter.fire(this._state);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly onDidChangeStateEmitter = new Emitter<CommentThreadState | undefined>();
|
||||
readonly onDidChangeState = this.onDidChangeStateEmitter.event;
|
||||
|
||||
private readonly onDidChangeCanReplyEmitter = new Emitter<boolean | CommentAuthorInformation>();
|
||||
readonly onDidChangeCanReply = this.onDidChangeCanReplyEmitter.event;
|
||||
|
||||
private _isDisposed: boolean;
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
private _canReply: boolean | CommentAuthorInformation = true;
|
||||
get canReply(): boolean | CommentAuthorInformation {
|
||||
return this._canReply;
|
||||
}
|
||||
|
||||
set canReply(canReply: boolean | CommentAuthorInformation) {
|
||||
this._canReply = canReply;
|
||||
this.onDidChangeCanReplyEmitter.fire(this._canReply);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public commentThreadHandle: number,
|
||||
public controllerHandle: number,
|
||||
public extensionId: string,
|
||||
public threadId: string,
|
||||
public resource: string,
|
||||
private _range: Range | undefined
|
||||
) {
|
||||
this._isDisposed = false;
|
||||
}
|
||||
|
||||
batchUpdate(changes: CommentThreadChanges): void {
|
||||
const modified = (value: keyof CommentThreadChanges): boolean =>
|
||||
Object.prototype.hasOwnProperty.call(changes, value);
|
||||
|
||||
if (modified('range')) { this._range = changes.range; }
|
||||
if (modified('label')) { this._label = changes.label; }
|
||||
if (modified('contextValue')) { this._contextValue = changes.contextValue; }
|
||||
if (modified('comments')) { this._comments = changes.comments; }
|
||||
if (modified('collapseState')) { this._collapsibleState = changes.collapseState; }
|
||||
if (modified('state')) { this._state = changes.state; }
|
||||
if (modified('canReply')) { this._canReply = changes.canReply!; }
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this.onDidChangeCollapsibleStateEmitter.dispose();
|
||||
this.onDidChangeStateEmitter.dispose();
|
||||
this.onDidChangeCommentsEmitter.dispose();
|
||||
this.onDidChangeInputEmitter.dispose();
|
||||
this.onDidChangeLabelEmitter.dispose();
|
||||
this.onDidChangeRangeEmitter.dispose();
|
||||
this.onDidChangeCanReplyEmitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentController {
|
||||
get handle(): number {
|
||||
return this._handle;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get contextValue(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get proxy(): CommentsExt {
|
||||
return this._proxy;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
get options(): CommentOptions | undefined {
|
||||
return this._features.options;
|
||||
}
|
||||
|
||||
private readonly threads: Map<number, CommentThreadImpl> = new Map<number, CommentThreadImpl>();
|
||||
public activeCommentThread?: CommentThread;
|
||||
|
||||
get features(): CommentProviderFeatures {
|
||||
return this._features;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: CommentsExt,
|
||||
private readonly _commentService: CommentsService,
|
||||
private readonly _handle: number,
|
||||
private readonly _uniqueId: string,
|
||||
private readonly _id: string,
|
||||
private readonly _label: string,
|
||||
private _features: CommentProviderFeatures
|
||||
) { }
|
||||
|
||||
updateFeatures(features: CommentProviderFeatures): void {
|
||||
this._features = features;
|
||||
}
|
||||
|
||||
createCommentThread(extensionId: string,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
range: Range | undefined,
|
||||
): CommentThread {
|
||||
const thread = new CommentThreadImpl(
|
||||
commentThreadHandle,
|
||||
this.handle,
|
||||
extensionId,
|
||||
threadId,
|
||||
URI.revive(resource).toString(),
|
||||
range
|
||||
);
|
||||
|
||||
this.threads.set(commentThreadHandle, thread);
|
||||
|
||||
this._commentService.updateComments(this._uniqueId, {
|
||||
added: [thread],
|
||||
removed: [],
|
||||
changed: []
|
||||
});
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
updateCommentThread(commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
changes: CommentThreadChanges): void {
|
||||
const thread = this.getKnownThread(commentThreadHandle);
|
||||
thread.batchUpdate(changes);
|
||||
|
||||
this._commentService.updateComments(this._uniqueId, {
|
||||
added: [],
|
||||
removed: [],
|
||||
changed: [thread]
|
||||
});
|
||||
}
|
||||
|
||||
deleteCommentThread(commentThreadHandle: number): void {
|
||||
const thread = this.getKnownThread(commentThreadHandle);
|
||||
this.threads.delete(commentThreadHandle);
|
||||
|
||||
this._commentService.updateComments(this._uniqueId, {
|
||||
added: [],
|
||||
removed: [thread],
|
||||
changed: []
|
||||
});
|
||||
|
||||
thread.dispose();
|
||||
}
|
||||
|
||||
deleteCommentThreadMain(commentThreadId: string): void {
|
||||
this.threads.forEach(thread => {
|
||||
if (thread.threadId === commentThreadId) {
|
||||
this._proxy.$deleteCommentThread(this._handle, thread.commentThreadHandle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateInput(input: string): void {
|
||||
const thread = this.activeCommentThread;
|
||||
|
||||
if (thread && thread.input) {
|
||||
const commentInput = thread.input;
|
||||
commentInput.value = input;
|
||||
thread.input = commentInput;
|
||||
}
|
||||
}
|
||||
|
||||
private getKnownThread(commentThreadHandle: number): CommentThreadImpl {
|
||||
const thread = this.threads.get(commentThreadHandle);
|
||||
if (!thread) {
|
||||
throw new Error('unknown thread');
|
||||
}
|
||||
return thread;
|
||||
}
|
||||
|
||||
async getDocumentComments(resource: URI, token: CancellationToken): Promise<CommentInfoMain> {
|
||||
const ret: CommentThread[] = [];
|
||||
for (const thread of [...this.threads.keys()]) {
|
||||
const commentThread = this.threads.get(thread)!;
|
||||
if (commentThread.resource === resource.toString()) {
|
||||
ret.push(commentThread);
|
||||
}
|
||||
}
|
||||
|
||||
const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
|
||||
|
||||
return <CommentInfoMain>{
|
||||
owner: this._uniqueId,
|
||||
label: this.label,
|
||||
threads: ret,
|
||||
commentingRanges: {
|
||||
resource: resource,
|
||||
ranges: commentingRanges?.ranges || [],
|
||||
fileComments: !!commentingRanges?.fileComments
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getCommentingRanges(resource: URI, token: CancellationToken): Promise<{ ranges: Range[]; fileComments: boolean } | undefined> {
|
||||
const commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
|
||||
return commentingRanges;
|
||||
}
|
||||
|
||||
getAllComments(): CommentThread[] {
|
||||
const ret: CommentThread[] = [];
|
||||
for (const thread of [...this.threads.keys()]) {
|
||||
ret.push(this.threads.get(thread)!);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
createCommentThreadTemplate(resource: UriComponents, range: Range): void {
|
||||
this._proxy.$createCommentThreadTemplate(this.handle, resource, range);
|
||||
}
|
||||
|
||||
async updateCommentThreadTemplate(threadHandle: number, range: Range): Promise<void> {
|
||||
await this._proxy.$updateCommentThreadTemplate(this.handle, threadHandle, range);
|
||||
}
|
||||
}
|
||||
|
||||
export class CommentsMainImp implements CommentsMain {
|
||||
private readonly proxy: CommentsExt;
|
||||
private documentProviders = new Map<number, Disposable>();
|
||||
private workspaceProviders = new Map<number, Disposable>();
|
||||
private handlers = new Map<number, string>();
|
||||
private commentControllers = new Map<number, CommentController>();
|
||||
|
||||
private activeCommentThread?: CommentThread;
|
||||
private readonly commentService: CommentsService;
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.COMMENTS_EXT);
|
||||
container.get(CommentsContribution);
|
||||
this.commentService = container.get(CommentsService);
|
||||
this.commentService.onDidChangeActiveCommentThread(async thread => {
|
||||
const handle = (thread as CommentThread).controllerHandle;
|
||||
const controller = this.commentControllers.get(handle);
|
||||
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeCommentThread = thread as CommentThread;
|
||||
controller.activeCommentThread = this.activeCommentThread;
|
||||
});
|
||||
}
|
||||
|
||||
$registerCommentController(handle: number, id: string, label: string): void {
|
||||
const providerId = generateUuid();
|
||||
this.handlers.set(handle, providerId);
|
||||
|
||||
const provider = new CommentController(this.proxy, this.commentService, handle, providerId, id, label, {});
|
||||
this.commentService.registerCommentController(providerId, provider);
|
||||
this.commentControllers.set(handle, provider);
|
||||
this.commentService.setWorkspaceComments(String(handle), []);
|
||||
}
|
||||
|
||||
$unregisterCommentController(handle: number): void {
|
||||
const providerId = this.handlers.get(handle);
|
||||
if (typeof providerId !== 'string') {
|
||||
throw new Error('unknown handler');
|
||||
}
|
||||
this.commentService.unregisterCommentController(providerId);
|
||||
this.handlers.delete(handle);
|
||||
this.commentControllers.delete(handle);
|
||||
}
|
||||
|
||||
$updateCommentControllerFeatures(handle: number, features: CommentProviderFeatures): void {
|
||||
const provider = this.commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
provider.updateFeatures(features);
|
||||
}
|
||||
|
||||
$createCommentThread(handle: number,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
range: Range | undefined,
|
||||
extensionId: string
|
||||
): CommentThread | undefined {
|
||||
const provider = this.commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return provider.createCommentThread(extensionId, commentThreadHandle, threadId, resource, range);
|
||||
}
|
||||
|
||||
$updateCommentThread(handle: number,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
changes: CommentThreadChanges): void {
|
||||
const provider = this.commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return provider.updateCommentThread(commentThreadHandle, threadId, resource, changes);
|
||||
}
|
||||
|
||||
$deleteCommentThread(handle: number, commentThreadHandle: number): void {
|
||||
const provider = this.commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
return provider.deleteCommentThread(commentThreadHandle);
|
||||
}
|
||||
|
||||
private getHandler(handle: number): string {
|
||||
if (!this.handlers.has(handle)) {
|
||||
throw new Error('Unknown handler');
|
||||
}
|
||||
return this.handlers.get(handle)!;
|
||||
}
|
||||
|
||||
$onDidCommentThreadsChange(handle: number, event: CommentThreadChangedEvent): void {
|
||||
const providerId = this.getHandler(handle);
|
||||
this.commentService.updateComments(providerId, event);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.workspaceProviders.forEach(value => value.dispose());
|
||||
this.workspaceProviders.clear();
|
||||
this.documentProviders.forEach(value => value.dispose());
|
||||
this.documentProviders.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import {
|
||||
Range,
|
||||
CommentInfo,
|
||||
CommentingRanges,
|
||||
CommentThread,
|
||||
CommentThreadChangedEvent,
|
||||
CommentThreadChangedEventMain
|
||||
} from '../../../common/plugin-api-rpc-model';
|
||||
import { CommentController } from './comments-main';
|
||||
import { CancellationToken } from '@theia/core/lib/common/cancellation';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.3/src/vs/workbench/contrib/comments/browser/commentService.ts
|
||||
|
||||
export interface ResourceCommentThreadEvent {
|
||||
resource: URI;
|
||||
commentInfos: CommentInfoMain[];
|
||||
}
|
||||
|
||||
export interface CommentInfoMain extends CommentInfo {
|
||||
owner: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceCommentThreadsEventMain {
|
||||
ownerId: string;
|
||||
commentThreads: CommentThread[];
|
||||
}
|
||||
|
||||
export const CommentsService = Symbol('CommentsService');
|
||||
|
||||
export interface CommentsService {
|
||||
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent>;
|
||||
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain>;
|
||||
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain>;
|
||||
readonly onDidChangeActiveCommentThread: Event<CommentThread | null>;
|
||||
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }>;
|
||||
readonly onDidSetDataProvider: Event<void>;
|
||||
readonly onDidDeleteDataProvider: Event<string>;
|
||||
|
||||
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void;
|
||||
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void;
|
||||
|
||||
removeWorkspaceComments(owner: string): void;
|
||||
|
||||
registerCommentController(owner: string, commentControl: CommentController): void;
|
||||
|
||||
unregisterCommentController(owner: string): void;
|
||||
|
||||
getCommentController(owner: string): CommentController | undefined;
|
||||
|
||||
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void;
|
||||
|
||||
updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void>;
|
||||
|
||||
updateComments(ownerId: string, event: CommentThreadChangedEvent): void;
|
||||
|
||||
disposeCommentThread(ownerId: string, threadId: string): void;
|
||||
|
||||
getComments(resource: URI): Promise<(CommentInfoMain | null)[]>;
|
||||
|
||||
getCommentingRanges(resource: URI): Promise<Range[]>;
|
||||
|
||||
setActiveCommentThread(commentThread: CommentThread | null): void;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PluginCommentService implements CommentsService {
|
||||
|
||||
private readonly onDidSetDataProviderEmitter: Emitter<void> = new Emitter<void>();
|
||||
readonly onDidSetDataProvider: Event<void> = this.onDidSetDataProviderEmitter.event;
|
||||
|
||||
private readonly onDidDeleteDataProviderEmitter: Emitter<string> = new Emitter<string>();
|
||||
readonly onDidDeleteDataProvider: Event<string> = this.onDidDeleteDataProviderEmitter.event;
|
||||
|
||||
private readonly onDidSetResourceCommentInfosEmitter: Emitter<ResourceCommentThreadEvent> = new Emitter<ResourceCommentThreadEvent>();
|
||||
readonly onDidSetResourceCommentInfos: Event<ResourceCommentThreadEvent> = this.onDidSetResourceCommentInfosEmitter.event;
|
||||
|
||||
private readonly onDidSetAllCommentThreadsEmitter: Emitter<WorkspaceCommentThreadsEventMain> = new Emitter<WorkspaceCommentThreadsEventMain>();
|
||||
readonly onDidSetAllCommentThreads: Event<WorkspaceCommentThreadsEventMain> = this.onDidSetAllCommentThreadsEmitter.event;
|
||||
|
||||
private readonly onDidUpdateCommentThreadsEmitter: Emitter<CommentThreadChangedEventMain> = new Emitter<CommentThreadChangedEventMain>();
|
||||
readonly onDidUpdateCommentThreads: Event<CommentThreadChangedEventMain> = this.onDidUpdateCommentThreadsEmitter.event;
|
||||
|
||||
private readonly onDidChangeActiveCommentThreadEmitter = new Emitter<CommentThread | null>();
|
||||
readonly onDidChangeActiveCommentThread = this.onDidChangeActiveCommentThreadEmitter.event;
|
||||
|
||||
private readonly onDidChangeActiveCommentingRangeEmitter = new Emitter<{ range: Range, commentingRangesInfo: CommentingRanges }>();
|
||||
readonly onDidChangeActiveCommentingRange: Event<{ range: Range, commentingRangesInfo: CommentingRanges }> = this.onDidChangeActiveCommentingRangeEmitter.event;
|
||||
|
||||
private commentControls = new Map<string, CommentController>();
|
||||
|
||||
setActiveCommentThread(commentThread: CommentThread | null): void {
|
||||
this.onDidChangeActiveCommentThreadEmitter.fire(commentThread);
|
||||
}
|
||||
|
||||
setDocumentComments(resource: URI, commentInfos: CommentInfoMain[]): void {
|
||||
this.onDidSetResourceCommentInfosEmitter.fire({ resource, commentInfos });
|
||||
}
|
||||
|
||||
setWorkspaceComments(owner: string, commentsByResource: CommentThread[]): void {
|
||||
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: commentsByResource });
|
||||
}
|
||||
|
||||
removeWorkspaceComments(owner: string): void {
|
||||
this.onDidSetAllCommentThreadsEmitter.fire({ ownerId: owner, commentThreads: [] });
|
||||
}
|
||||
|
||||
registerCommentController(owner: string, commentControl: CommentController): void {
|
||||
this.commentControls.set(owner, commentControl);
|
||||
this.onDidSetDataProviderEmitter.fire();
|
||||
}
|
||||
|
||||
unregisterCommentController(owner: string): void {
|
||||
this.commentControls.delete(owner);
|
||||
this.onDidDeleteDataProviderEmitter.fire(owner);
|
||||
}
|
||||
|
||||
getCommentController(owner: string): CommentController | undefined {
|
||||
return this.commentControls.get(owner);
|
||||
}
|
||||
|
||||
createCommentThreadTemplate(owner: string, resource: URI, range: Range): void {
|
||||
const commentController = this.commentControls.get(owner);
|
||||
|
||||
if (!commentController) {
|
||||
return;
|
||||
}
|
||||
|
||||
commentController.createCommentThreadTemplate(resource, range);
|
||||
}
|
||||
|
||||
async updateCommentThreadTemplate(owner: string, threadHandle: number, range: Range): Promise<void> {
|
||||
const commentController = this.commentControls.get(owner);
|
||||
|
||||
if (!commentController) {
|
||||
return;
|
||||
}
|
||||
|
||||
await commentController.updateCommentThreadTemplate(threadHandle, range);
|
||||
}
|
||||
|
||||
disposeCommentThread(owner: string, threadId: string): void {
|
||||
const controller = this.getCommentController(owner);
|
||||
if (controller) {
|
||||
controller.deleteCommentThreadMain(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
updateComments(ownerId: string, event: CommentThreadChangedEvent): void {
|
||||
const evt: CommentThreadChangedEventMain = Object.assign({}, event, { owner: ownerId });
|
||||
this.onDidUpdateCommentThreadsEmitter.fire(evt);
|
||||
}
|
||||
|
||||
async getComments(resource: URI): Promise<(CommentInfoMain | null)[]> {
|
||||
const commentControlResult: Promise<CommentInfoMain | null>[] = [];
|
||||
|
||||
this.commentControls.forEach(control => {
|
||||
commentControlResult.push(control.getDocumentComments(resource, CancellationToken.None)
|
||||
.catch(e => {
|
||||
console.log(e);
|
||||
return null;
|
||||
}));
|
||||
});
|
||||
|
||||
return Promise.all(commentControlResult);
|
||||
}
|
||||
|
||||
async getCommentingRanges(resource: URI): Promise<Range[]> {
|
||||
const commentControlResult: Promise<{ ranges: Range[]; fileComments: boolean } | undefined>[] = [];
|
||||
|
||||
this.commentControls.forEach(control => {
|
||||
commentControlResult.push(control.getCommentingRanges(resource, CancellationToken.None));
|
||||
});
|
||||
|
||||
const ret = await Promise.all(commentControlResult);
|
||||
return ret.reduce<Range[]>((prev, curr) => {
|
||||
if (curr) {
|
||||
prev.push(...curr.ranges);
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import {
|
||||
ApplicationShell, DiffUris, OpenHandler, OpenerOptions, SplitWidget, Widget, WidgetManager, WidgetOpenerOptions, getDefaultHandler, defaultHandlerPriority
|
||||
} from '@theia/core/lib/browser';
|
||||
import { CustomEditor, CustomEditorPriority, CustomEditorSelector } from '../../../common';
|
||||
import { CustomEditorWidget } from './custom-editor-widget';
|
||||
import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
|
||||
import { generateUuid } from '@theia/core/lib/common/uuid';
|
||||
import { DisposableCollection, Emitter, PreferenceService } from '@theia/core';
|
||||
import { match } from '@theia/core/lib/common/glob';
|
||||
|
||||
export class CustomEditorOpener implements OpenHandler {
|
||||
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
|
||||
private readonly onDidOpenCustomEditorEmitter = new Emitter<[CustomEditorWidget, WidgetOpenerOptions?]>();
|
||||
readonly onDidOpenCustomEditor = this.onDidOpenCustomEditorEmitter.event;
|
||||
|
||||
constructor(
|
||||
private readonly editor: CustomEditor,
|
||||
protected readonly shell: ApplicationShell,
|
||||
protected readonly widgetManager: WidgetManager,
|
||||
protected readonly editorRegistry: PluginCustomEditorRegistry,
|
||||
protected readonly preferenceService: PreferenceService
|
||||
) {
|
||||
this.id = CustomEditorOpener.toCustomEditorId(this.editor.viewType);
|
||||
this.label = this.editor.displayName;
|
||||
}
|
||||
|
||||
static toCustomEditorId(editorViewType: string): string {
|
||||
return `custom-editor-${editorViewType}`;
|
||||
}
|
||||
|
||||
canHandle(uri: URI, options?: OpenerOptions): number {
|
||||
let priority = 0;
|
||||
const { selector } = this.editor;
|
||||
if (DiffUris.isDiffUri(uri)) {
|
||||
const [left, right] = DiffUris.decode(uri);
|
||||
if (this.matches(selector, right) && this.matches(selector, left)) {
|
||||
if (getDefaultHandler(right, this.preferenceService) === this.editor.viewType) {
|
||||
priority = defaultHandlerPriority;
|
||||
} else {
|
||||
priority = this.getPriority();
|
||||
}
|
||||
}
|
||||
} else if (this.matches(selector, uri)) {
|
||||
if (getDefaultHandler(uri, this.preferenceService) === this.editor.viewType) {
|
||||
priority = defaultHandlerPriority;
|
||||
} else {
|
||||
priority = this.getPriority();
|
||||
}
|
||||
}
|
||||
return priority;
|
||||
}
|
||||
|
||||
canOpenWith(uri: URI): number {
|
||||
if (this.matches(this.editor.selector, uri)) {
|
||||
return this.getPriority();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getPriority(): number {
|
||||
switch (this.editor.priority) {
|
||||
case CustomEditorPriority.default: return 500;
|
||||
case CustomEditorPriority.builtin: return 400;
|
||||
/** `option` should not open the custom-editor by default. */
|
||||
case CustomEditorPriority.option: return 1;
|
||||
default: return 200;
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly pendingWidgetPromises = new Map<string, Promise<CustomEditorWidget>>();
|
||||
protected async openCustomEditor(uri: URI, options?: WidgetOpenerOptions): Promise<CustomEditorWidget> {
|
||||
let widget: CustomEditorWidget | undefined;
|
||||
let isNewWidget = false;
|
||||
const uriString = uri.toString();
|
||||
let widgetPromise = this.pendingWidgetPromises.get(uriString);
|
||||
if (widgetPromise) {
|
||||
widget = await widgetPromise;
|
||||
} else {
|
||||
const widgets = this.widgetManager.getWidgets(CustomEditorWidget.FACTORY_ID) as CustomEditorWidget[];
|
||||
widget = widgets.find(w => w.viewType === this.editor.viewType && w.resource.toString() === uriString);
|
||||
if (!widget) {
|
||||
isNewWidget = true;
|
||||
const id = generateUuid();
|
||||
widgetPromise = this.widgetManager.getOrCreateWidget<CustomEditorWidget>(CustomEditorWidget.FACTORY_ID, { id }).then(async w => {
|
||||
try {
|
||||
w.viewType = this.editor.viewType;
|
||||
w.resource = uri;
|
||||
await this.editorRegistry.resolveWidget(w);
|
||||
if (options?.widgetOptions) {
|
||||
await this.shell.addWidget(w, options.widgetOptions);
|
||||
}
|
||||
return w;
|
||||
} catch (e) {
|
||||
w.dispose();
|
||||
throw e;
|
||||
}
|
||||
}).finally(() => this.pendingWidgetPromises.delete(uriString));
|
||||
this.pendingWidgetPromises.set(uriString, widgetPromise);
|
||||
widget = await widgetPromise;
|
||||
}
|
||||
}
|
||||
if (options?.mode === 'activate') {
|
||||
await this.shell.activateWidget(widget.id);
|
||||
} else if (options?.mode === 'reveal') {
|
||||
await this.shell.revealWidget(widget.id);
|
||||
}
|
||||
if (isNewWidget) {
|
||||
this.onDidOpenCustomEditorEmitter.fire([widget, options]);
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected async openSideBySide(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
|
||||
const [leftUri, rightUri] = DiffUris.decode(uri);
|
||||
const widget = await this.widgetManager.getOrCreateWidget<SplitWidget>(
|
||||
CustomEditorWidget.SIDE_BY_SIDE_FACTORY_ID, { uri: uri.toString(), viewType: this.editor.viewType });
|
||||
if (!widget.panes.length) { // a new widget
|
||||
const trackedDisposables = new DisposableCollection(widget);
|
||||
try {
|
||||
const createPane = async (paneUri: URI) => {
|
||||
let pane = await this.openCustomEditor(paneUri);
|
||||
if (pane.isAttached) {
|
||||
await this.shell.closeWidget(pane.id);
|
||||
if (!pane.isDisposed) { // user canceled
|
||||
return undefined;
|
||||
}
|
||||
pane = await this.openCustomEditor(paneUri);
|
||||
}
|
||||
return pane;
|
||||
};
|
||||
|
||||
const rightPane = await createPane(rightUri);
|
||||
if (!rightPane) {
|
||||
trackedDisposables.dispose();
|
||||
return undefined;
|
||||
}
|
||||
trackedDisposables.push(rightPane);
|
||||
|
||||
const leftPane = await createPane(leftUri);
|
||||
if (!leftPane) {
|
||||
trackedDisposables.dispose();
|
||||
return undefined;
|
||||
}
|
||||
trackedDisposables.push(leftPane);
|
||||
|
||||
widget.addPane(leftPane);
|
||||
widget.addPane(rightPane);
|
||||
|
||||
// dispose the widget if either of its panes gets externally disposed
|
||||
leftPane.disposed.connect(() => widget.dispose());
|
||||
rightPane.disposed.connect(() => widget.dispose());
|
||||
|
||||
if (options?.widgetOptions) {
|
||||
await this.shell.addWidget(widget, options.widgetOptions);
|
||||
}
|
||||
} catch (e) {
|
||||
trackedDisposables.dispose();
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (options?.mode === 'activate') {
|
||||
await this.shell.activateWidget(widget.id);
|
||||
} else if (options?.mode === 'reveal') {
|
||||
await this.shell.revealWidget(widget.id);
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
async open(uri: URI, options?: WidgetOpenerOptions): Promise<Widget | undefined> {
|
||||
options = { ...options };
|
||||
options.mode ??= 'activate';
|
||||
options.widgetOptions ??= { area: 'main' };
|
||||
return DiffUris.isDiffUri(uri) ? this.openSideBySide(uri, options) : this.openCustomEditor(uri, options);
|
||||
}
|
||||
|
||||
matches(selectors: CustomEditorSelector[], resource: URI): boolean {
|
||||
return selectors.some(selector => this.selectorMatches(selector, resource));
|
||||
}
|
||||
|
||||
selectorMatches(selector: CustomEditorSelector, resource: URI): boolean {
|
||||
if (selector.filenamePattern) {
|
||||
if (match(selector.filenamePattern.toLowerCase(), resource.path.name.toLowerCase() + resource.path.ext.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/contrib/customEditor/browser/customEditors.ts
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Reference } from '@theia/core/lib/common/reference';
|
||||
import { CustomEditorModel } from './custom-editors-main';
|
||||
|
||||
@injectable()
|
||||
export class CustomEditorService {
|
||||
protected _models = new CustomEditorModelManager();
|
||||
get models(): CustomEditorModelManager { return this._models; }
|
||||
}
|
||||
|
||||
export class CustomEditorModelManager {
|
||||
|
||||
private readonly references = new Map<string, {
|
||||
readonly viewType: string,
|
||||
readonly model: Promise<CustomEditorModel>,
|
||||
counter: number
|
||||
}>();
|
||||
|
||||
add(resource: URI, viewType: string, model: Promise<CustomEditorModel>): Promise<Reference<CustomEditorModel>> {
|
||||
const key = this.key(resource, viewType);
|
||||
const existing = this.references.get(key);
|
||||
if (existing) {
|
||||
throw new Error('Model already exists');
|
||||
}
|
||||
|
||||
this.references.set(key, { viewType, model, counter: 0 });
|
||||
return this.tryRetain(resource, viewType)!;
|
||||
}
|
||||
|
||||
async get(resource: URI, viewType: string): Promise<CustomEditorModel | undefined> {
|
||||
const key = this.key(resource, viewType);
|
||||
const entry = this.references.get(key);
|
||||
return entry?.model;
|
||||
}
|
||||
|
||||
tryRetain(resource: URI, viewType: string): Promise<Reference<CustomEditorModel>> | undefined {
|
||||
const key = this.key(resource, viewType);
|
||||
|
||||
const entry = this.references.get(key);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
entry.counter++;
|
||||
|
||||
return entry.model.then(model => ({
|
||||
object: model,
|
||||
dispose: once(() => {
|
||||
if (--entry!.counter <= 0) {
|
||||
entry.model.then(x => x.dispose());
|
||||
this.references.delete(key);
|
||||
}
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
disposeAllModelsForView(viewType: string): void {
|
||||
for (const [key, value] of this.references) {
|
||||
if (value.viewType === viewType) {
|
||||
value.model.then(x => x.dispose());
|
||||
this.references.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private key(resource: URI, viewType: string): string {
|
||||
return `${resource.toString()}@@@${viewType}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function once<T extends Function>(this: unknown, fn: T): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return function (): unknown {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
|
||||
didCall = true;
|
||||
result = fn.apply(_this, arguments);
|
||||
|
||||
return result;
|
||||
} as unknown as T;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { ApplicationShell, UndoRedoHandler } from '@theia/core/lib/browser';
|
||||
import { CustomEditorWidget } from './custom-editor-widget';
|
||||
|
||||
@injectable()
|
||||
export class CustomEditorUndoRedoHandler implements UndoRedoHandler<CustomEditorWidget> {
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly applicationShell: ApplicationShell;
|
||||
|
||||
priority = 190;
|
||||
select(): CustomEditorWidget | undefined {
|
||||
const current = this.applicationShell.currentWidget;
|
||||
if (current instanceof CustomEditorWidget) {
|
||||
return current;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
undo(item: CustomEditorWidget): void {
|
||||
item.undo();
|
||||
}
|
||||
redo(item: CustomEditorWidget): void {
|
||||
item.redo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { CustomEditorWidget } from '../custom-editors/custom-editor-widget';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { WebviewWidgetIdentifier, WebviewWidgetExternalEndpoint } from '../webview/webview';
|
||||
import { WebviewEnvironment } from '../webview/webview-environment';
|
||||
|
||||
export class CustomEditorWidgetFactory {
|
||||
|
||||
readonly id = CustomEditorWidget.FACTORY_ID;
|
||||
|
||||
protected readonly container: interfaces.Container;
|
||||
|
||||
constructor(container: interfaces.Container) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
async createWidget(identifier: WebviewWidgetIdentifier): Promise<CustomEditorWidget> {
|
||||
const externalEndpoint = await this.container.get(WebviewEnvironment).externalEndpoint();
|
||||
let endpoint = externalEndpoint.replace('{{uuid}}', identifier.id);
|
||||
if (endpoint[endpoint.length - 1] === '/') {
|
||||
endpoint = endpoint.slice(0, endpoint.length - 1);
|
||||
}
|
||||
const child = this.container.createChild();
|
||||
child.bind(WebviewWidgetIdentifier).toConstantValue(identifier);
|
||||
child.bind(WebviewWidgetExternalEndpoint).toConstantValue(endpoint);
|
||||
return child.get(CustomEditorWidget);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileOperation } from '@theia/filesystem/lib/common/files';
|
||||
import { ApplicationShell, DelegatingSaveable, NavigatableWidget, Saveable, SaveableSource } from '@theia/core/lib/browser';
|
||||
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
|
||||
import { Reference } from '@theia/core/lib/common/reference';
|
||||
import { WebviewWidget } from '../webview/webview';
|
||||
import { CustomEditorModel } from './custom-editors-main';
|
||||
import { CustomEditorWidget as CustomEditorWidgetShape } from '@theia/editor/lib/browser';
|
||||
|
||||
@injectable()
|
||||
export class CustomEditorWidget extends WebviewWidget implements CustomEditorWidgetShape, SaveableSource, NavigatableWidget {
|
||||
static override FACTORY_ID = 'plugin-custom-editor';
|
||||
static readonly SIDE_BY_SIDE_FACTORY_ID = CustomEditorWidget.FACTORY_ID + '.side-by-side';
|
||||
|
||||
resource: URI;
|
||||
|
||||
protected _modelRef: Reference<CustomEditorModel | undefined> = { object: undefined, dispose: () => { } };
|
||||
get modelRef(): Reference<CustomEditorModel | undefined> {
|
||||
return this._modelRef;
|
||||
}
|
||||
set modelRef(modelRef: Reference<CustomEditorModel>) {
|
||||
this._modelRef.dispose();
|
||||
this._modelRef = modelRef;
|
||||
this.delegatingSaveable.delegate = modelRef.object;
|
||||
this.doUpdateContent();
|
||||
}
|
||||
|
||||
// ensures that saveable is available even if modelRef.object is undefined
|
||||
protected readonly delegatingSaveable = new DelegatingSaveable();
|
||||
get saveable(): Saveable {
|
||||
return this.delegatingSaveable;
|
||||
}
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(SaveableService)
|
||||
protected readonly saveService: SaveableService;
|
||||
|
||||
@postConstruct()
|
||||
protected override init(): void {
|
||||
super.init();
|
||||
this.id = CustomEditorWidget.FACTORY_ID + ':' + this.identifier.id;
|
||||
this.toDispose.push(this.fileService.onDidRunOperation(e => {
|
||||
if (e.isOperation(FileOperation.MOVE)) {
|
||||
this.doMove(e.target.resource);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
this._modelRef.object?.undo();
|
||||
}
|
||||
|
||||
redo(): void {
|
||||
this._modelRef.object?.redo();
|
||||
}
|
||||
|
||||
getResourceUri(): URI | undefined {
|
||||
return this.resource;
|
||||
}
|
||||
|
||||
createMoveToUri(resourceUri: URI): URI | undefined {
|
||||
return this.resource.withPath(resourceUri.path);
|
||||
}
|
||||
|
||||
override storeState(): CustomEditorWidget.State {
|
||||
return {
|
||||
...super.storeState(),
|
||||
strResource: this.resource.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
override restoreState(oldState: CustomEditorWidget.State): void {
|
||||
const { strResource } = oldState;
|
||||
this.resource = new URI(strResource);
|
||||
super.restoreState(oldState);
|
||||
}
|
||||
|
||||
onMove(handler: (newResource: URI) => Promise<void>): void {
|
||||
this._moveHandler = handler;
|
||||
}
|
||||
|
||||
private _moveHandler?: (newResource: URI) => void;
|
||||
|
||||
private doMove(target: URI): void {
|
||||
if (this._moveHandler) {
|
||||
this._moveHandler(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CustomEditorWidget {
|
||||
export interface State extends WebviewWidget.State {
|
||||
strResource: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/api/browser/mainThreadCustomEditors.ts
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { MAIN_RPC_CONTEXT, CustomEditorsMain, CustomEditorsExt, CustomTextEditorCapabilities } from '../../../common/plugin-api-rpc';
|
||||
import { RPCProtocol } from '../../../common/rpc-protocol';
|
||||
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
|
||||
import { PluginCustomEditorRegistry } from './plugin-custom-editor-registry';
|
||||
import { Emitter } from '@theia/core';
|
||||
import { UriComponents } from '../../../common/uri-components';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import TheiaURI from '@theia/core/lib/common/uri';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Reference } from '@theia/core/lib/common/reference';
|
||||
import { CancellationToken, CancellationTokenSource } from '@theia/core/lib/common/cancellation';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { EditorModelService } from '../text-editor-model-service';
|
||||
import { CustomEditorService } from './custom-editor-service';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { UndoRedoService } from '@theia/editor/lib/browser/undo-redo-service';
|
||||
import { WebviewsMainImpl } from '../webviews-main';
|
||||
import { WidgetManager } from '@theia/core/lib/browser/widget-manager';
|
||||
import { ApplicationShell, LabelProvider, Saveable, SaveAsOptions, SaveOptions } from '@theia/core/lib/browser';
|
||||
import { WebviewPanelOptions } from '@theia/plugin';
|
||||
import { EditorPreferences } from '@theia/editor/lib/common/editor-preferences';
|
||||
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
||||
|
||||
const enum CustomEditorModelType {
|
||||
Custom,
|
||||
Text,
|
||||
}
|
||||
|
||||
export class CustomEditorsMainImpl implements CustomEditorsMain, Disposable {
|
||||
protected readonly pluginService: HostedPluginSupport;
|
||||
protected readonly shell: ApplicationShell;
|
||||
protected readonly textModelService: EditorModelService;
|
||||
protected readonly fileService: FileService;
|
||||
protected readonly customEditorService: CustomEditorService;
|
||||
protected readonly undoRedoService: UndoRedoService;
|
||||
protected readonly customEditorRegistry: PluginCustomEditorRegistry;
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
protected readonly widgetManager: WidgetManager;
|
||||
protected readonly editorPreferences: EditorPreferences;
|
||||
private readonly proxy: CustomEditorsExt;
|
||||
private readonly editorProviders = new Map<string, Disposable>();
|
||||
|
||||
constructor(rpc: RPCProtocol,
|
||||
container: interfaces.Container,
|
||||
readonly webviewsMain: WebviewsMainImpl,
|
||||
) {
|
||||
this.pluginService = container.get(HostedPluginSupport);
|
||||
this.shell = container.get(ApplicationShell);
|
||||
this.textModelService = container.get(EditorModelService);
|
||||
this.fileService = container.get(FileService);
|
||||
this.customEditorService = container.get(CustomEditorService);
|
||||
this.undoRedoService = container.get(UndoRedoService);
|
||||
this.customEditorRegistry = container.get(PluginCustomEditorRegistry);
|
||||
this.labelProvider = container.get(LabelProvider);
|
||||
this.editorPreferences = container.get(EditorPreferences);
|
||||
this.widgetManager = container.get(WidgetManager);
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.CUSTOM_EDITORS_EXT);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const disposable of this.editorProviders.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.editorProviders.clear();
|
||||
}
|
||||
|
||||
$registerTextEditorProvider(
|
||||
viewType: string, options: WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void {
|
||||
this.registerEditorProvider(CustomEditorModelType.Text, viewType, options, capabilities, true);
|
||||
}
|
||||
|
||||
$registerCustomEditorProvider(viewType: string, options: WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void {
|
||||
this.registerEditorProvider(CustomEditorModelType.Custom, viewType, options, {}, supportsMultipleEditorsPerDocument);
|
||||
}
|
||||
|
||||
protected async registerEditorProvider(
|
||||
modelType: CustomEditorModelType,
|
||||
viewType: string,
|
||||
options: WebviewPanelOptions,
|
||||
capabilities: CustomTextEditorCapabilities,
|
||||
supportsMultipleEditorsPerDocument: boolean,
|
||||
): Promise<void> {
|
||||
if (this.editorProviders.has(viewType)) {
|
||||
throw new Error(`Provider for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
const disposables = new DisposableCollection();
|
||||
|
||||
disposables.push(
|
||||
this.customEditorRegistry.registerResolver(viewType, async widget => {
|
||||
|
||||
const { resource, identifier } = widget;
|
||||
widget.options = options;
|
||||
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
let modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, cancellationSource.token);
|
||||
widget.modelRef = modelRef;
|
||||
|
||||
widget.onDidDispose(() => {
|
||||
// If the model is still dirty, make sure we have time to save it
|
||||
if (modelRef.object.dirty) {
|
||||
const sub = modelRef.object.onDirtyChanged(() => {
|
||||
if (!modelRef.object.dirty) {
|
||||
sub.dispose();
|
||||
modelRef.dispose();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelRef.dispose();
|
||||
});
|
||||
|
||||
if (capabilities.supportsMove) {
|
||||
const onMoveCancelTokenSource = new CancellationTokenSource();
|
||||
widget.onMove(async (newResource: TheiaURI) => {
|
||||
const oldModel = modelRef;
|
||||
modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, onMoveCancelTokenSource.token);
|
||||
this.proxy.$onMoveCustomEditor(identifier.id, newResource.toComponents(), viewType);
|
||||
oldModel.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
this.webviewsMain.hookWebview(widget);
|
||||
widget.title.label = this.labelProvider.getName(resource);
|
||||
|
||||
const _cancellationSource = new CancellationTokenSource();
|
||||
await this.proxy.$resolveWebviewEditor(
|
||||
resource.toComponents(),
|
||||
identifier.id,
|
||||
viewType,
|
||||
widget.title.label,
|
||||
widget.viewState.position,
|
||||
options,
|
||||
_cancellationSource.token
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
this.editorProviders.set(viewType, disposables);
|
||||
}
|
||||
|
||||
$unregisterEditorProvider(viewType: string): void {
|
||||
const provider = this.editorProviders.get(viewType);
|
||||
if (!provider) {
|
||||
throw new Error(`No provider for ${viewType} registered`);
|
||||
}
|
||||
|
||||
provider.dispose();
|
||||
this.editorProviders.delete(viewType);
|
||||
|
||||
this.customEditorService.models.disposeAllModelsForView(viewType);
|
||||
}
|
||||
|
||||
protected async getOrCreateCustomEditorModel(
|
||||
modelType: CustomEditorModelType,
|
||||
resource: TheiaURI,
|
||||
viewType: string,
|
||||
cancellationToken: CancellationToken,
|
||||
): Promise<Reference<CustomEditorModel>> {
|
||||
const existingModel = this.customEditorService.models.tryRetain(resource, viewType);
|
||||
if (existingModel) {
|
||||
return existingModel;
|
||||
}
|
||||
|
||||
switch (modelType) {
|
||||
case CustomEditorModelType.Text: {
|
||||
const model = CustomTextEditorModel.create(viewType, resource, this.textModelService);
|
||||
return this.customEditorService.models.add(resource, viewType, model);
|
||||
}
|
||||
case CustomEditorModelType.Custom: {
|
||||
const model = MainCustomEditorModel.create(this.proxy, viewType, resource, this.undoRedoService, this.fileService, cancellationToken);
|
||||
return this.customEditorService.models.add(resource, viewType, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async getCustomEditorModel(resourceComponents: UriComponents, viewType: string): Promise<MainCustomEditorModel> {
|
||||
const resource = URI.revive(resourceComponents);
|
||||
const model = await this.customEditorService.models.get(new TheiaURI(resource), viewType);
|
||||
if (!model || !(model instanceof MainCustomEditorModel)) {
|
||||
throw new Error('Could not find model for custom editor');
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {
|
||||
const model = await this.getCustomEditorModel(resourceComponents, viewType);
|
||||
model.pushEdit(editId, label);
|
||||
}
|
||||
|
||||
async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {
|
||||
const model = await this.getCustomEditorModel(resourceComponents, viewType);
|
||||
model.changeContent();
|
||||
}
|
||||
}
|
||||
|
||||
export interface CustomEditorModel extends Saveable, Disposable {
|
||||
readonly viewType: string;
|
||||
readonly resource: URI;
|
||||
readonly readonly: boolean;
|
||||
readonly dirty: boolean;
|
||||
|
||||
revert(options?: Saveable.RevertOptions): Promise<void>;
|
||||
saveCustomEditor(options?: SaveOptions): Promise<void>;
|
||||
saveCustomEditorAs?(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise<void>;
|
||||
|
||||
undo(): void;
|
||||
redo(): void;
|
||||
}
|
||||
|
||||
export class MainCustomEditorModel implements CustomEditorModel {
|
||||
private currentEditIndex: number = -1;
|
||||
private savePoint: number = -1;
|
||||
private isDirtyFromContentChange = false;
|
||||
private ongoingSave?: CancellationTokenSource;
|
||||
private readonly edits: Array<number> = [];
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
private readonly onDirtyChangedEmitter = new Emitter<void>();
|
||||
readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
|
||||
|
||||
private readonly onContentChangedEmitter = new Emitter<void>();
|
||||
readonly onContentChanged = this.onContentChangedEmitter.event;
|
||||
|
||||
static async create(
|
||||
proxy: CustomEditorsExt,
|
||||
viewType: string,
|
||||
resource: TheiaURI,
|
||||
undoRedoService: UndoRedoService,
|
||||
fileService: FileService,
|
||||
cancellation: CancellationToken,
|
||||
): Promise<MainCustomEditorModel> {
|
||||
const { editable } = await proxy.$createCustomDocument(resource.toComponents(), viewType, {}, cancellation);
|
||||
return new MainCustomEditorModel(proxy, viewType, resource, editable, undoRedoService, fileService);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private proxy: CustomEditorsExt,
|
||||
readonly viewType: string,
|
||||
private readonly editorResource: TheiaURI,
|
||||
private readonly editable: boolean,
|
||||
private readonly undoRedoService: UndoRedoService,
|
||||
private readonly fileService: FileService
|
||||
) {
|
||||
this.toDispose.push(this.onDirtyChangedEmitter);
|
||||
}
|
||||
|
||||
get resource(): URI {
|
||||
return URI.from(this.editorResource.toComponents());
|
||||
}
|
||||
|
||||
get dirty(): boolean {
|
||||
if (this.isDirtyFromContentChange) {
|
||||
return true;
|
||||
}
|
||||
if (this.edits.length > 0) {
|
||||
return this.savePoint !== this.currentEditIndex;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get readonly(): boolean {
|
||||
return !this.editable;
|
||||
}
|
||||
|
||||
setProxy(proxy: CustomEditorsExt): void {
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.editable) {
|
||||
this.undoRedoService.removeElements(this.editorResource);
|
||||
}
|
||||
this.proxy.$disposeCustomDocument(this.resource, this.viewType);
|
||||
}
|
||||
|
||||
changeContent(): void {
|
||||
this.change(() => {
|
||||
this.isDirtyFromContentChange = true;
|
||||
});
|
||||
}
|
||||
|
||||
pushEdit(editId: number, label: string | undefined): void {
|
||||
if (!this.editable) {
|
||||
throw new Error('Document is not editable');
|
||||
}
|
||||
|
||||
this.change(() => {
|
||||
this.spliceEdits(editId);
|
||||
this.currentEditIndex = this.edits.length - 1;
|
||||
});
|
||||
|
||||
this.undoRedoService.pushElement(
|
||||
this.editorResource,
|
||||
() => this.undo(),
|
||||
() => this.redo(),
|
||||
);
|
||||
}
|
||||
|
||||
async revert(options?: Saveable.RevertOptions): Promise<void> {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentEditIndex === this.savePoint && !this.isDirtyFromContentChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
await this.proxy.$revert(this.resource, this.viewType, cancellationSource.token);
|
||||
this.change(() => {
|
||||
this.isDirtyFromContentChange = false;
|
||||
this.currentEditIndex = this.savePoint;
|
||||
this.spliceEdits();
|
||||
});
|
||||
}
|
||||
|
||||
async save(options?: SaveOptions): Promise<void> {
|
||||
await this.saveCustomEditor(options);
|
||||
}
|
||||
|
||||
async saveCustomEditor(options?: SaveOptions): Promise<void> {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cancelable = new CancellationTokenSource();
|
||||
const savePromise = this.proxy.$save(this.resource, this.viewType, cancelable.token);
|
||||
this.ongoingSave?.cancel();
|
||||
this.ongoingSave = cancelable;
|
||||
|
||||
try {
|
||||
await savePromise;
|
||||
|
||||
if (this.ongoingSave === cancelable) { // Make sure we are still doing the same save
|
||||
this.change(() => {
|
||||
this.isDirtyFromContentChange = false;
|
||||
this.savePoint = this.currentEditIndex;
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (this.ongoingSave === cancelable) { // Make sure we are still doing the same save
|
||||
this.ongoingSave = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async saveAs(options: SaveAsOptions): Promise<void> {
|
||||
await this.saveCustomEditorAs(new TheiaURI(this.resource), options.target, options);
|
||||
}
|
||||
|
||||
async saveCustomEditorAs(resource: TheiaURI, targetResource: TheiaURI, options?: SaveOptions): Promise<void> {
|
||||
if (this.editable) {
|
||||
const source = new CancellationTokenSource();
|
||||
await this.proxy.$saveAs(this.resource, this.viewType, targetResource.toComponents(), source.token);
|
||||
this.change(() => {
|
||||
this.savePoint = this.currentEditIndex;
|
||||
});
|
||||
} else {
|
||||
// Since the editor is readonly, just copy the file over
|
||||
await this.fileService.copy(resource, targetResource, { overwrite: false });
|
||||
}
|
||||
}
|
||||
|
||||
async undo(): Promise<void> {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentEditIndex < 0) {
|
||||
// nothing to undo
|
||||
return;
|
||||
}
|
||||
|
||||
const undoneEdit = this.edits[this.currentEditIndex];
|
||||
this.change(() => {
|
||||
--this.currentEditIndex;
|
||||
});
|
||||
await this.proxy.$undo(this.resource, this.viewType, undoneEdit, this.dirty);
|
||||
}
|
||||
|
||||
async redo(): Promise<void> {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentEditIndex >= this.edits.length - 1) {
|
||||
// nothing to redo
|
||||
return;
|
||||
}
|
||||
|
||||
const redoneEdit = this.edits[this.currentEditIndex + 1];
|
||||
this.change(() => {
|
||||
++this.currentEditIndex;
|
||||
});
|
||||
await this.proxy.$redo(this.resource, this.viewType, redoneEdit, this.dirty);
|
||||
}
|
||||
|
||||
private spliceEdits(editToInsert?: number): void {
|
||||
const start = this.currentEditIndex + 1;
|
||||
const toRemove = this.edits.length - this.currentEditIndex;
|
||||
|
||||
const removedEdits = typeof editToInsert === 'number'
|
||||
? this.edits.splice(start, toRemove, editToInsert)
|
||||
: this.edits.splice(start, toRemove);
|
||||
|
||||
if (removedEdits.length) {
|
||||
this.proxy.$disposeEdits(this.resource, this.viewType, removedEdits);
|
||||
}
|
||||
}
|
||||
|
||||
private change(makeEdit: () => void): void {
|
||||
const wasDirty = this.dirty;
|
||||
makeEdit();
|
||||
|
||||
if (this.dirty !== wasDirty) {
|
||||
this.onDirtyChangedEmitter.fire();
|
||||
}
|
||||
this.onContentChangedEmitter.fire();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// copied from https://github.com/microsoft/vscode/blob/53eac52308c4611000a171cc7bf1214293473c78/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts
|
||||
export class CustomTextEditorModel implements CustomEditorModel {
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
private readonly onDirtyChangedEmitter = new Emitter<void>();
|
||||
readonly onDirtyChanged = this.onDirtyChangedEmitter.event;
|
||||
private readonly onContentChangedEmitter = new Emitter<void>();
|
||||
readonly onContentChanged = this.onContentChangedEmitter.event;
|
||||
|
||||
static async create(
|
||||
viewType: string,
|
||||
resource: TheiaURI,
|
||||
editorModelService: EditorModelService
|
||||
): Promise<CustomTextEditorModel> {
|
||||
const model = await editorModelService.createModelReference(resource);
|
||||
model.object.suppressOpenEditorWhenDirty = true;
|
||||
return new CustomTextEditorModel(viewType, resource, model);
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly viewType: string,
|
||||
readonly editorResource: TheiaURI,
|
||||
private readonly model: Reference<MonacoEditorModel>
|
||||
) {
|
||||
this.toDispose.push(
|
||||
this.editorTextModel.onDirtyChanged(e => {
|
||||
this.onDirtyChangedEmitter.fire();
|
||||
})
|
||||
);
|
||||
this.toDispose.push(
|
||||
this.editorTextModel.onContentChanged(e => {
|
||||
this.onContentChangedEmitter.fire();
|
||||
})
|
||||
);
|
||||
this.toDispose.push(this.onDirtyChangedEmitter);
|
||||
this.toDispose.push(this.onContentChangedEmitter);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
this.model.dispose();
|
||||
}
|
||||
|
||||
get resource(): URI {
|
||||
return URI.from(this.editorResource.toComponents());
|
||||
}
|
||||
|
||||
get dirty(): boolean {
|
||||
return this.editorTextModel.dirty;
|
||||
};
|
||||
|
||||
get readonly(): boolean {
|
||||
return Boolean(this.editorTextModel.readOnly);
|
||||
}
|
||||
|
||||
get editorTextModel(): MonacoEditorModel {
|
||||
return this.model.object;
|
||||
}
|
||||
|
||||
revert(options?: Saveable.RevertOptions): Promise<void> {
|
||||
return this.editorTextModel.revert(options);
|
||||
}
|
||||
|
||||
save(options?: SaveOptions): Promise<void> {
|
||||
return this.saveCustomEditor(options);
|
||||
}
|
||||
|
||||
serialize(): Promise<BinaryBuffer> {
|
||||
return this.editorTextModel.serialize();
|
||||
}
|
||||
|
||||
saveCustomEditor(options?: SaveOptions): Promise<void> {
|
||||
return this.editorTextModel.save(options);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
this.editorTextModel.undo();
|
||||
}
|
||||
|
||||
redo(): void {
|
||||
this.editorTextModel.redo();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 SAP SE or an SAP affiliate company and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { CustomEditor, DeployedPlugin } from '../../../common';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { CustomEditorOpener } from './custom-editor-opener';
|
||||
import { Emitter, PreferenceService } from '@theia/core';
|
||||
import { ApplicationShell, DefaultOpenerService, OpenWithService, WidgetManager } from '@theia/core/lib/browser';
|
||||
import { CustomEditorWidget } from './custom-editor-widget';
|
||||
|
||||
@injectable()
|
||||
export class PluginCustomEditorRegistry {
|
||||
private readonly editors = new Map<string, CustomEditor>();
|
||||
private readonly pendingEditors = new Map<CustomEditorWidget, { deferred: Deferred<void>, disposable: Disposable }>();
|
||||
private readonly resolvers = new Map<string, (widget: CustomEditorWidget) => Promise<void>>();
|
||||
|
||||
private readonly onWillOpenCustomEditorEmitter = new Emitter<string>();
|
||||
readonly onWillOpenCustomEditor = this.onWillOpenCustomEditorEmitter.event;
|
||||
|
||||
@inject(DefaultOpenerService)
|
||||
protected readonly defaultOpenerService: DefaultOpenerService;
|
||||
|
||||
@inject(WidgetManager)
|
||||
protected readonly widgetManager: WidgetManager;
|
||||
|
||||
@inject(ApplicationShell)
|
||||
protected readonly shell: ApplicationShell;
|
||||
|
||||
@inject(OpenWithService)
|
||||
protected readonly openWithService: OpenWithService;
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected readonly preferenceService: PreferenceService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.widgetManager.onDidCreateWidget(({ factoryId, widget }) => {
|
||||
if (factoryId === CustomEditorWidget.FACTORY_ID && widget instanceof CustomEditorWidget) {
|
||||
const restoreState = widget.restoreState.bind(widget);
|
||||
|
||||
widget.restoreState = state => {
|
||||
if (state.viewType && state.strResource) {
|
||||
restoreState(state);
|
||||
this.resolveWidget(widget);
|
||||
} else {
|
||||
widget.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
registerCustomEditor(editor: CustomEditor, plugin: DeployedPlugin): Disposable {
|
||||
if (this.editors.has(editor.viewType)) {
|
||||
console.warn('editor with such id already registered: ', JSON.stringify(editor));
|
||||
return Disposable.NULL;
|
||||
}
|
||||
this.editors.set(editor.viewType, editor);
|
||||
|
||||
const toDispose = new DisposableCollection();
|
||||
toDispose.push(Disposable.create(() => this.editors.delete(editor.viewType)));
|
||||
|
||||
const editorOpenHandler = new CustomEditorOpener(
|
||||
editor,
|
||||
this.shell,
|
||||
this.widgetManager,
|
||||
this,
|
||||
this.preferenceService
|
||||
);
|
||||
toDispose.push(this.defaultOpenerService.addHandler(editorOpenHandler));
|
||||
toDispose.push(
|
||||
this.openWithService.registerHandler({
|
||||
id: editor.viewType,
|
||||
label: editorOpenHandler.label,
|
||||
providerName: plugin.metadata.model.displayName,
|
||||
canHandle: uri => editorOpenHandler.canOpenWith(uri),
|
||||
open: uri => editorOpenHandler.open(uri)
|
||||
})
|
||||
);
|
||||
return toDispose;
|
||||
}
|
||||
|
||||
async resolveWidget(widget: CustomEditorWidget): Promise<void> {
|
||||
const resolver = this.resolvers.get(widget.viewType);
|
||||
if (resolver) {
|
||||
await resolver(widget);
|
||||
} else {
|
||||
const deferred = new Deferred<void>();
|
||||
const disposable = widget.onDidDispose(() => this.pendingEditors.delete(widget));
|
||||
this.pendingEditors.set(widget, { deferred, disposable });
|
||||
this.onWillOpenCustomEditorEmitter.fire(widget.viewType);
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
registerResolver(viewType: string, resolver: (widget: CustomEditorWidget) => Promise<void>): Disposable {
|
||||
if (this.resolvers.has(viewType)) {
|
||||
throw new Error(`Resolver for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
for (const [editorWidget, { deferred, disposable }] of this.pendingEditors.entries()) {
|
||||
if (editorWidget.viewType === viewType) {
|
||||
resolver(editorWidget).then(() => deferred.resolve(), err => deferred.reject(err)).finally(() => disposable.dispose());
|
||||
this.pendingEditors.delete(editorWidget);
|
||||
}
|
||||
}
|
||||
|
||||
this.resolvers.set(viewType, resolver);
|
||||
return Disposable.create(() => this.resolvers.delete(viewType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { IDataTransferItem, IReadonlyVSDataTransfer } from '@theia/monaco-editor-core/esm/vs/base/common/dataTransfer';
|
||||
import { DataTransferDTO, DataTransferItemDTO } from '../../../common/plugin-api-rpc-model';
|
||||
import { URI } from '../../../plugin/types-impl';
|
||||
|
||||
export namespace DataTransferItem {
|
||||
export async function from(mime: string, item: IDataTransferItem): Promise<DataTransferItemDTO> {
|
||||
const stringValue = await item.asString();
|
||||
|
||||
if (mime === 'text/uri-list') {
|
||||
return {
|
||||
asString: '',
|
||||
fileData: undefined,
|
||||
uriListData: serializeUriList(stringValue),
|
||||
};
|
||||
}
|
||||
|
||||
const fileValue = item.asFile();
|
||||
return {
|
||||
asString: stringValue,
|
||||
fileData: fileValue ? { id: fileValue.id, name: fileValue.name, uri: fileValue.uri } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeUriList(stringValue: string): ReadonlyArray<string | URI> {
|
||||
return stringValue.split('\r\n').map(part => {
|
||||
if (part.startsWith('#')) {
|
||||
return part;
|
||||
}
|
||||
|
||||
try {
|
||||
return URI.parse(part);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return part;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export namespace DataTransfer {
|
||||
export async function toDataTransferDTO(value: IReadonlyVSDataTransfer): Promise<DataTransferDTO> {
|
||||
return {
|
||||
items: await Promise.all(
|
||||
Array.from(value)
|
||||
.map(
|
||||
async ([mime, item]) => [mime, await DataTransferItem.from(mime, item)]
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
400
packages/plugin-ext/src/main/browser/debug/debug-main.ts
Normal file
400
packages/plugin-ext/src/main/browser/debug/debug-main.ts
Normal file
@@ -0,0 +1,400 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { RPCProtocol } from '../../../common/rpc-protocol';
|
||||
import {
|
||||
DebugConfigurationProviderDescriptor,
|
||||
DebugMain,
|
||||
DebugExt,
|
||||
MAIN_RPC_CONTEXT
|
||||
} from '../../../common/plugin-api-rpc';
|
||||
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { Breakpoint, DebugStackFrameDTO, DebugThreadDTO, WorkspaceFolder } from '../../../common/plugin-api-rpc-model';
|
||||
import { LabelProvider } from '@theia/core/lib/browser';
|
||||
import { EditorManager } from '@theia/editor/lib/browser';
|
||||
import { BreakpointManager, BreakpointsChangeEvent } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
|
||||
import { DebugSourceBreakpoint } from '@theia/debug/lib/browser/model/debug-source-breakpoint';
|
||||
import { URI as Uri } from '@theia/core/shared/vscode-uri';
|
||||
import { SourceBreakpoint, FunctionBreakpoint } from '@theia/debug/lib/browser/breakpoint/breakpoint-marker';
|
||||
import { DebugConfiguration, DebugSessionOptions } from '@theia/debug/lib/common/debug-configuration';
|
||||
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
|
||||
import { DebugProtocol } from '@vscode/debugprotocol';
|
||||
import { DebugConfigurationManager } from '@theia/debug/lib/browser/debug-configuration-manager';
|
||||
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
|
||||
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
||||
import { DebugPreferences } from '@theia/debug/lib/common/debug-preferences';
|
||||
import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution';
|
||||
import { PluginDebugConfigurationProvider } from './plugin-debug-configuration-provider';
|
||||
import { PluginDebugSessionContributionRegistrator, PluginDebugSessionContributionRegistry } from './plugin-debug-session-contribution-registry';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { PluginDebugSessionFactory } from './plugin-debug-session-factory';
|
||||
import { PluginDebugService } from './plugin-debug-service';
|
||||
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
|
||||
import { DebugFunctionBreakpoint } from '@theia/debug/lib/browser/model/debug-function-breakpoint';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { ConsoleSessionManager } from '@theia/console/lib/browser/console-session-manager';
|
||||
import { DebugConsoleSession } from '@theia/debug/lib/browser/console/debug-console-session';
|
||||
import { CommandService, ContributionProvider } from '@theia/core/lib/common';
|
||||
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
|
||||
import { ConnectionImpl } from '../../../common/connection';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { DebugSessionOptions as TheiaDebugSessionOptions } from '@theia/debug/lib/browser/debug-session-options';
|
||||
import { DebugStackFrame } from '@theia/debug/lib/browser/model/debug-stack-frame';
|
||||
import { DebugThread } from '@theia/debug/lib/browser/model/debug-thread';
|
||||
import { TestService } from '@theia/test/lib/browser/test-service';
|
||||
|
||||
export class DebugMainImpl implements DebugMain, Disposable {
|
||||
private readonly debugExt: DebugExt;
|
||||
|
||||
private readonly sessionManager: DebugSessionManager;
|
||||
private readonly labelProvider: LabelProvider;
|
||||
private readonly editorManager: EditorManager;
|
||||
private readonly breakpointsManager: BreakpointManager;
|
||||
private readonly consoleSessionManager: ConsoleSessionManager;
|
||||
private readonly configurationManager: DebugConfigurationManager;
|
||||
private readonly terminalService: TerminalService;
|
||||
private readonly messages: MessageClient;
|
||||
private readonly outputChannelManager: OutputChannelManager;
|
||||
private readonly debugPreferences: DebugPreferences;
|
||||
private readonly sessionContributionRegistrator: PluginDebugSessionContributionRegistrator;
|
||||
private readonly pluginDebugService: PluginDebugService;
|
||||
private readonly fileService: FileService;
|
||||
private readonly pluginService: HostedPluginSupport;
|
||||
private readonly debugContributionProvider: ContributionProvider<DebugContribution>;
|
||||
private readonly testService: TestService;
|
||||
private readonly workspaceService: WorkspaceService;
|
||||
private readonly commandService: CommandService;
|
||||
|
||||
private readonly debuggerContributions = new Map<string, DisposableCollection>();
|
||||
private readonly configurationProviders = new Map<number, DisposableCollection>();
|
||||
private readonly toDispose = new DisposableCollection();
|
||||
|
||||
constructor(rpc: RPCProtocol, readonly connectionMain: ConnectionImpl, container: interfaces.Container) {
|
||||
this.debugExt = rpc.getProxy(MAIN_RPC_CONTEXT.DEBUG_EXT);
|
||||
this.sessionManager = container.get(DebugSessionManager);
|
||||
this.labelProvider = container.get(LabelProvider);
|
||||
this.editorManager = container.get(EditorManager);
|
||||
this.breakpointsManager = container.get(BreakpointManager);
|
||||
this.consoleSessionManager = container.get(ConsoleSessionManager);
|
||||
this.configurationManager = container.get(DebugConfigurationManager);
|
||||
this.terminalService = container.get(TerminalService);
|
||||
this.messages = container.get(MessageClient);
|
||||
this.outputChannelManager = container.get(OutputChannelManager);
|
||||
this.debugPreferences = container.get(DebugPreferences);
|
||||
this.pluginDebugService = container.get(PluginDebugService);
|
||||
this.sessionContributionRegistrator = container.get(PluginDebugSessionContributionRegistry);
|
||||
this.debugContributionProvider = container.getNamed(ContributionProvider, DebugContribution);
|
||||
this.fileService = container.get(FileService);
|
||||
this.pluginService = container.get(HostedPluginSupport);
|
||||
this.testService = container.get(TestService);
|
||||
this.workspaceService = container.get(WorkspaceService);
|
||||
this.commandService = container.get(CommandService);
|
||||
|
||||
const fireDidChangeBreakpoints = ({ added, removed, changed }: BreakpointsChangeEvent<SourceBreakpoint | FunctionBreakpoint>) => {
|
||||
this.debugExt.$breakpointsDidChange(
|
||||
this.toTheiaPluginApiBreakpoints(added),
|
||||
removed.map(b => b.id),
|
||||
this.toTheiaPluginApiBreakpoints(changed)
|
||||
);
|
||||
};
|
||||
this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getBreakpoints()), [], []);
|
||||
this.debugExt.$breakpointsDidChange(this.toTheiaPluginApiBreakpoints(this.breakpointsManager.getFunctionBreakpoints()), [], []);
|
||||
|
||||
this.toDispose.pushAll([
|
||||
this.breakpointsManager.onDidChangeBreakpoints(fireDidChangeBreakpoints),
|
||||
this.breakpointsManager.onDidChangeFunctionBreakpoints(fireDidChangeBreakpoints),
|
||||
this.sessionManager.onDidCreateDebugSession(debugSession => this.debugExt.$sessionDidCreate(debugSession.id)),
|
||||
this.sessionManager.onDidStartDebugSession(debugSession => this.debugExt.$sessionDidStart(debugSession.id)),
|
||||
this.sessionManager.onDidDestroyDebugSession(debugSession => this.debugExt.$sessionDidDestroy(debugSession.id)),
|
||||
this.sessionManager.onDidChangeActiveDebugSession(event => this.debugExt.$sessionDidChange(event.current && event.current.id)),
|
||||
this.sessionManager.onDidReceiveDebugSessionCustomEvent(event => this.debugExt.$onSessionCustomEvent(event.session.id, event.event, event.body)),
|
||||
this.sessionManager.onDidFocusStackFrame(stackFrame => this.debugExt.$onDidChangeActiveFrame(this.toDebugStackFrameDTO(stackFrame))),
|
||||
this.sessionManager.onDidFocusThread(debugThread => this.debugExt.$onDidChangeActiveThread(this.toDebugThreadDTO(debugThread))),
|
||||
]);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
async $appendToDebugConsole(value: string): Promise<void> {
|
||||
const session = this.consoleSessionManager.selectedSession;
|
||||
if (session instanceof DebugConsoleSession) {
|
||||
session.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
async $appendLineToDebugConsole(value: string): Promise<void> {
|
||||
const session = this.consoleSessionManager.selectedSession;
|
||||
if (session instanceof DebugConsoleSession) {
|
||||
session.appendLine(value);
|
||||
}
|
||||
}
|
||||
|
||||
async $registerDebuggerContribution(description: DebuggerDescription): Promise<void> {
|
||||
const debugType = description.type;
|
||||
const terminalOptionsExt = await this.debugExt.$getTerminalCreationOptions(debugType);
|
||||
if (this.toDispose.disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const debugSessionFactory = new PluginDebugSessionFactory(
|
||||
this.terminalService,
|
||||
this.editorManager,
|
||||
this.breakpointsManager,
|
||||
this.labelProvider,
|
||||
this.messages,
|
||||
this.outputChannelManager,
|
||||
this.debugPreferences,
|
||||
async (sessionId: string) => {
|
||||
const connection = await this.connectionMain.ensureConnection(sessionId);
|
||||
return connection;
|
||||
},
|
||||
this.fileService,
|
||||
terminalOptionsExt,
|
||||
this.debugContributionProvider,
|
||||
this.testService,
|
||||
this.workspaceService,
|
||||
this.commandService,
|
||||
);
|
||||
|
||||
const toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.debuggerContributions.delete(debugType))
|
||||
);
|
||||
this.debuggerContributions.set(debugType, toDispose);
|
||||
toDispose.pushAll([
|
||||
this.pluginDebugService.registerDebugAdapterContribution(
|
||||
new PluginDebugAdapterContribution(description, this.debugExt, this.pluginService)
|
||||
),
|
||||
this.sessionContributionRegistrator.registerDebugSessionContribution({
|
||||
debugType: description.type,
|
||||
debugSessionFactory: () => debugSessionFactory
|
||||
})
|
||||
]);
|
||||
this.toDispose.push(Disposable.create(() => this.$unregisterDebuggerConfiguration(debugType)));
|
||||
}
|
||||
|
||||
async $unregisterDebuggerConfiguration(debugType: string): Promise<void> {
|
||||
const disposable = this.debuggerContributions.get(debugType);
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
$registerDebugConfigurationProvider(description: DebugConfigurationProviderDescriptor): void {
|
||||
const handle = description.handle;
|
||||
const toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.configurationProviders.delete(handle))
|
||||
);
|
||||
this.configurationProviders.set(handle, toDispose);
|
||||
|
||||
toDispose.push(
|
||||
this.pluginDebugService.registerDebugConfigurationProvider(new PluginDebugConfigurationProvider(description, this.debugExt))
|
||||
);
|
||||
|
||||
this.toDispose.push(Disposable.create(() => this.$unregisterDebugConfigurationProvider(handle)));
|
||||
}
|
||||
|
||||
async $unregisterDebugConfigurationProvider(handle: number): Promise<void> {
|
||||
const disposable = this.configurationProviders.get(handle);
|
||||
if (disposable) {
|
||||
disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async $addBreakpoints(breakpoints: Breakpoint[]): Promise<void> {
|
||||
const newBreakpoints = new Map<string, Breakpoint>();
|
||||
breakpoints.forEach(b => newBreakpoints.set(b.id, b));
|
||||
this.breakpointsManager.findMarkers({
|
||||
dataFilter: data => {
|
||||
// install only new breakpoints
|
||||
if (newBreakpoints.has(data.id)) {
|
||||
newBreakpoints.delete(data.id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
let addedFunctionBreakpoints = false;
|
||||
const functionBreakpoints = this.breakpointsManager.getFunctionBreakpoints();
|
||||
for (const breakpoint of functionBreakpoints) {
|
||||
// install only new breakpoints
|
||||
if (newBreakpoints.has(breakpoint.id)) {
|
||||
newBreakpoints.delete(breakpoint.id);
|
||||
}
|
||||
}
|
||||
for (const breakpoint of newBreakpoints.values()) {
|
||||
if (breakpoint.location) {
|
||||
const location = breakpoint.location;
|
||||
const column = breakpoint.location.range.startColumn;
|
||||
this.breakpointsManager.addBreakpoint({
|
||||
id: breakpoint.id,
|
||||
uri: Uri.revive(location.uri).toString(),
|
||||
enabled: breakpoint.enabled,
|
||||
raw: {
|
||||
line: breakpoint.location.range.startLineNumber + 1,
|
||||
column: column > 0 ? column + 1 : undefined,
|
||||
condition: breakpoint.condition,
|
||||
hitCondition: breakpoint.hitCondition,
|
||||
logMessage: breakpoint.logMessage
|
||||
}
|
||||
});
|
||||
} else if (breakpoint.functionName) {
|
||||
addedFunctionBreakpoints = true;
|
||||
functionBreakpoints.push({
|
||||
id: breakpoint.id,
|
||||
enabled: breakpoint.enabled,
|
||||
raw: {
|
||||
name: breakpoint.functionName
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (addedFunctionBreakpoints) {
|
||||
this.breakpointsManager.setFunctionBreakpoints(functionBreakpoints);
|
||||
}
|
||||
}
|
||||
|
||||
async $getDebugProtocolBreakpoint(sessionId: string, breakpointId: string): Promise<DebugProtocol.Breakpoint | undefined> {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
if (session) {
|
||||
return session.getBreakpoint(breakpointId)?.raw;
|
||||
} else {
|
||||
throw new Error(`Debug session '${sessionId}' not found`);
|
||||
}
|
||||
}
|
||||
|
||||
async $removeBreakpoints(breakpoints: string[]): Promise<void> {
|
||||
const { labelProvider, breakpointsManager, editorManager } = this;
|
||||
const session = this.sessionManager.currentSession;
|
||||
|
||||
const ids = new Set<string>(breakpoints);
|
||||
for (const origin of this.breakpointsManager.findMarkers({ dataFilter: data => ids.has(data.id) })) {
|
||||
const breakpoint = new DebugSourceBreakpoint(origin.data, { labelProvider, breakpoints: breakpointsManager, editorManager, session }, this.commandService);
|
||||
breakpoint.remove();
|
||||
}
|
||||
for (const origin of this.breakpointsManager.getFunctionBreakpoints()) {
|
||||
if (ids.has(origin.id)) {
|
||||
const breakpoint = new DebugFunctionBreakpoint(origin, { labelProvider, breakpoints: breakpointsManager, editorManager, session });
|
||||
breakpoint.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async $customRequest(sessionId: string, command: string, args?: any): Promise<DebugProtocol.Response> {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
if (session) {
|
||||
return session.sendCustomRequest(command, args);
|
||||
}
|
||||
|
||||
throw new Error(`Debug session '${sessionId}' not found`);
|
||||
}
|
||||
|
||||
async $startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, options: DebugSessionOptions): Promise<boolean> {
|
||||
// search for matching options
|
||||
let sessionOptions: TheiaDebugSessionOptions | undefined;
|
||||
if (typeof nameOrConfiguration === 'string') {
|
||||
for (const configOptions of this.configurationManager.all) {
|
||||
if (configOptions.name === nameOrConfiguration) {
|
||||
sessionOptions = configOptions;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sessionOptions = {
|
||||
name: nameOrConfiguration.name,
|
||||
configuration: nameOrConfiguration
|
||||
};
|
||||
}
|
||||
|
||||
if (!sessionOptions) {
|
||||
console.error(`There is no debug configuration for ${nameOrConfiguration}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// translate given extra data
|
||||
const workspaceFolderUri = folder && Uri.revive(folder.uri).toString();
|
||||
if (TheiaDebugSessionOptions.isConfiguration(sessionOptions)) {
|
||||
sessionOptions = { ...sessionOptions, configuration: { ...sessionOptions.configuration, ...options }, workspaceFolderUri };
|
||||
} else {
|
||||
sessionOptions = { ...sessionOptions, ...options, workspaceFolderUri };
|
||||
}
|
||||
sessionOptions.testRun = options.testRun;
|
||||
|
||||
// start options
|
||||
const session = await this.sessionManager.start(sessionOptions);
|
||||
return !!session;
|
||||
}
|
||||
|
||||
async $stopDebugging(sessionId?: string): Promise<void> {
|
||||
if (sessionId) {
|
||||
const session = this.sessionManager.getSession(sessionId);
|
||||
return this.sessionManager.terminateSession(session);
|
||||
}
|
||||
// Terminate all sessions if no session is provided.
|
||||
for (const session of this.sessionManager.sessions) {
|
||||
this.sessionManager.terminateSession(session);
|
||||
}
|
||||
}
|
||||
|
||||
private toDebugStackFrameDTO(stackFrame: DebugStackFrame | undefined): DebugStackFrameDTO | undefined {
|
||||
return stackFrame ? {
|
||||
sessionId: stackFrame.session.id,
|
||||
frameId: stackFrame.frameId,
|
||||
threadId: stackFrame.thread.threadId
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
private toDebugThreadDTO(debugThread: DebugThread | undefined): DebugThreadDTO | undefined {
|
||||
return debugThread ? {
|
||||
sessionId: debugThread.session.id,
|
||||
threadId: debugThread.threadId
|
||||
} : undefined;
|
||||
}
|
||||
|
||||
private toTheiaPluginApiBreakpoints(breakpoints: (SourceBreakpoint | FunctionBreakpoint)[]): Breakpoint[] {
|
||||
return breakpoints.map(b => this.toTheiaPluginApiBreakpoint(b));
|
||||
}
|
||||
|
||||
private toTheiaPluginApiBreakpoint(breakpoint: SourceBreakpoint | FunctionBreakpoint): Breakpoint {
|
||||
if ('uri' in breakpoint) {
|
||||
const raw = breakpoint.raw;
|
||||
return {
|
||||
id: breakpoint.id,
|
||||
enabled: breakpoint.enabled,
|
||||
condition: breakpoint.raw.condition,
|
||||
hitCondition: breakpoint.raw.hitCondition,
|
||||
logMessage: raw.logMessage,
|
||||
location: {
|
||||
uri: Uri.parse(breakpoint.uri),
|
||||
range: {
|
||||
startLineNumber: raw.line - 1,
|
||||
startColumn: (raw.column || 1) - 1,
|
||||
endLineNumber: raw.line - 1,
|
||||
endColumn: (raw.column || 1) - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: breakpoint.id,
|
||||
enabled: breakpoint.enabled,
|
||||
functionName: breakpoint.raw.name
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DebugExt } from '../../../common/plugin-api-rpc';
|
||||
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { DebuggerDescription } from '@theia/debug/lib/common/debug-service';
|
||||
import { HostedPluginSupport } from '../../../hosted/browser/hosted-plugin';
|
||||
|
||||
/**
|
||||
* Plugin [DebugAdapterContribution](#DebugAdapterContribution).
|
||||
*/
|
||||
export class PluginDebugAdapterContribution {
|
||||
constructor(
|
||||
protected readonly description: DebuggerDescription,
|
||||
protected readonly debugExt: DebugExt,
|
||||
protected readonly pluginService: HostedPluginSupport) { }
|
||||
|
||||
get type(): string {
|
||||
return this.description.type;
|
||||
}
|
||||
|
||||
get label(): MaybePromise<string | undefined> {
|
||||
return this.description.label;
|
||||
}
|
||||
|
||||
async createDebugSession(config: DebugConfiguration, workspaceFolder: string | undefined): Promise<string> {
|
||||
await this.pluginService.activateByDebug('onDebugAdapterProtocolTracker', config.type);
|
||||
return this.debugExt.$createDebugSession(config, workspaceFolder);
|
||||
}
|
||||
|
||||
async terminateDebugSession(sessionId: string): Promise<void> {
|
||||
this.debugExt.$terminateDebugSession(sessionId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2022 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import {
|
||||
DebugConfigurationProvider,
|
||||
DebugConfigurationProviderDescriptor,
|
||||
DebugConfigurationProviderTriggerKind,
|
||||
DebugExt
|
||||
} from '../../../common/plugin-api-rpc';
|
||||
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
|
||||
|
||||
export class PluginDebugConfigurationProvider implements DebugConfigurationProvider {
|
||||
/**
|
||||
* After https://github.com/eclipse-theia/theia/pull/13196, the debug config handles might change.
|
||||
* Store the original handle to be able to call the extension host when getting by handle.
|
||||
*/
|
||||
protected readonly originalHandle: number;
|
||||
public handle: number;
|
||||
public type: string;
|
||||
public triggerKind: DebugConfigurationProviderTriggerKind;
|
||||
provideDebugConfigurations: (folder: string | undefined) => Promise<DebugConfiguration[]>;
|
||||
resolveDebugConfiguration: (
|
||||
folder: string | undefined,
|
||||
debugConfiguration: DebugConfiguration
|
||||
) => Promise<DebugConfiguration | undefined | null>;
|
||||
resolveDebugConfigurationWithSubstitutedVariables: (
|
||||
folder: string | undefined,
|
||||
debugConfiguration: DebugConfiguration
|
||||
) => Promise<DebugConfiguration | undefined | null>;
|
||||
|
||||
constructor(
|
||||
description: DebugConfigurationProviderDescriptor,
|
||||
protected readonly debugExt: DebugExt
|
||||
) {
|
||||
this.handle = description.handle;
|
||||
this.originalHandle = this.handle;
|
||||
this.type = description.type;
|
||||
this.triggerKind = description.trigger;
|
||||
|
||||
if (description.provideDebugConfiguration) {
|
||||
this.provideDebugConfigurations = async (folder: string | undefined) => this.debugExt.$provideDebugConfigurationsByHandle(this.originalHandle, folder);
|
||||
}
|
||||
|
||||
if (description.resolveDebugConfigurations) {
|
||||
this.resolveDebugConfiguration =
|
||||
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
|
||||
this.debugExt.$resolveDebugConfigurationByHandle(this.originalHandle, folder, debugConfiguration);
|
||||
}
|
||||
|
||||
if (description.resolveDebugConfigurationWithSubstitutedVariables) {
|
||||
this.resolveDebugConfigurationWithSubstitutedVariables =
|
||||
async (folder: string | undefined, debugConfiguration: DebugConfiguration) =>
|
||||
this.debugExt.$resolveDebugConfigurationWithSubstitutedVariablesByHandle(this.originalHandle, folder, debugConfiguration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DebuggerDescription, DebugPath, DebugService } from '@theia/debug/lib/common/debug-service';
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
import { deepClone, Emitter, Event, nls } from '@theia/core';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { DebugConfiguration } from '@theia/debug/lib/common/debug-configuration';
|
||||
import { IJSONSchema, IJSONSchemaSnippet } from '@theia/core/lib/common/json-schema';
|
||||
import { PluginDebugAdapterContribution } from './plugin-debug-adapter-contribution';
|
||||
import { PluginDebugConfigurationProvider } from './plugin-debug-configuration-provider';
|
||||
import { injectable, inject, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { CommandIdVariables } from '@theia/variable-resolver/lib/common/variable-types';
|
||||
import { DebugConfigurationProviderTriggerKind } from '../../../common/plugin-api-rpc';
|
||||
import { DebuggerContribution } from '../../../common/plugin-protocol';
|
||||
import { DebugRequestTypes } from '@theia/debug/lib/browser/debug-session-connection';
|
||||
import * as theia from '@theia/plugin';
|
||||
|
||||
/**
|
||||
* Debug service to work with plugin and extension contributions.
|
||||
*/
|
||||
@injectable()
|
||||
export class PluginDebugService implements DebugService {
|
||||
|
||||
protected readonly onDidChangeDebuggersEmitter = new Emitter<void>();
|
||||
get onDidChangeDebuggers(): Event<void> {
|
||||
return this.onDidChangeDebuggersEmitter.event;
|
||||
}
|
||||
|
||||
protected readonly debuggers: DebuggerContribution[] = [];
|
||||
protected readonly contributors = new Map<string, PluginDebugAdapterContribution>();
|
||||
protected readonly configurationProviders = new Map<number, PluginDebugConfigurationProvider>();
|
||||
protected readonly toDispose = new DisposableCollection(this.onDidChangeDebuggersEmitter);
|
||||
|
||||
protected readonly onDidChangeDebugConfigurationProvidersEmitter = new Emitter<void>();
|
||||
get onDidChangeDebugConfigurationProviders(): Event<void> {
|
||||
return this.onDidChangeDebugConfigurationProvidersEmitter.event;
|
||||
}
|
||||
|
||||
// maps session and contribution
|
||||
protected readonly sessionId2contrib = new Map<string, PluginDebugAdapterContribution>();
|
||||
protected delegated: DebugService;
|
||||
|
||||
@inject(WebSocketConnectionProvider)
|
||||
protected readonly connectionProvider: WebSocketConnectionProvider;
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.delegated = this.connectionProvider.createProxy<DebugService>(DebugPath);
|
||||
this.toDispose.pushAll([
|
||||
Disposable.create(() => this.delegated.dispose()),
|
||||
Disposable.create(() => {
|
||||
for (const sessionId of this.sessionId2contrib.keys()) {
|
||||
const contrib = this.sessionId2contrib.get(sessionId)!;
|
||||
contrib.terminateDebugSession(sessionId);
|
||||
}
|
||||
this.sessionId2contrib.clear();
|
||||
})]);
|
||||
}
|
||||
|
||||
registerDebugAdapterContribution(contrib: PluginDebugAdapterContribution): Disposable {
|
||||
const { type } = contrib;
|
||||
|
||||
if (this.contributors.has(type)) {
|
||||
console.warn(`Debugger with type '${type}' already registered.`);
|
||||
return Disposable.NULL;
|
||||
}
|
||||
|
||||
this.contributors.set(type, contrib);
|
||||
return Disposable.create(() => this.unregisterDebugAdapterContribution(type));
|
||||
}
|
||||
|
||||
unregisterDebugAdapterContribution(debugType: string): void {
|
||||
this.contributors.delete(debugType);
|
||||
}
|
||||
|
||||
// debouncing to send a single notification for multiple registrations at initialization time
|
||||
fireOnDidConfigurationProvidersChanged = debounce(() => {
|
||||
this.onDidChangeDebugConfigurationProvidersEmitter.fire();
|
||||
}, 100);
|
||||
|
||||
registerDebugConfigurationProvider(provider: PluginDebugConfigurationProvider): Disposable {
|
||||
if (this.configurationProviders.has(provider.handle)) {
|
||||
const configuration = this.configurationProviders.get(provider.handle);
|
||||
if (configuration && configuration.type !== provider.type) {
|
||||
console.warn(`Different debug configuration provider with type '${configuration.type}' already registered.`);
|
||||
provider.handle = this.configurationProviders.size;
|
||||
}
|
||||
}
|
||||
const handle = provider.handle;
|
||||
this.configurationProviders.set(handle, provider);
|
||||
this.fireOnDidConfigurationProvidersChanged();
|
||||
return Disposable.create(() => this.unregisterDebugConfigurationProvider(handle));
|
||||
}
|
||||
|
||||
unregisterDebugConfigurationProvider(handle: number): void {
|
||||
this.configurationProviders.delete(handle);
|
||||
this.fireOnDidConfigurationProvidersChanged();
|
||||
}
|
||||
|
||||
async debugTypes(): Promise<string[]> {
|
||||
const debugTypes = new Set(await this.delegated.debugTypes());
|
||||
for (const contribution of this.debuggers) {
|
||||
debugTypes.add(contribution.type);
|
||||
}
|
||||
for (const debugType of this.contributors.keys()) {
|
||||
debugTypes.add(debugType);
|
||||
}
|
||||
return [...debugTypes];
|
||||
}
|
||||
|
||||
async provideDebugConfigurations(debugType: keyof DebugRequestTypes, workspaceFolderUri: string | undefined): Promise<theia.DebugConfiguration[]> {
|
||||
const pluginProviders =
|
||||
Array.from(this.configurationProviders.values()).filter(p => (
|
||||
p.triggerKind === DebugConfigurationProviderTriggerKind.Initial &&
|
||||
(p.type === debugType || p.type === '*') &&
|
||||
p.provideDebugConfigurations
|
||||
));
|
||||
|
||||
if (pluginProviders.length === 0) {
|
||||
return this.delegated.provideDebugConfigurations(debugType, workspaceFolderUri);
|
||||
}
|
||||
|
||||
const results: DebugConfiguration[] = [];
|
||||
await Promise.all(pluginProviders.map(async p => {
|
||||
const result = await p.provideDebugConfigurations(workspaceFolderUri);
|
||||
if (result) {
|
||||
results.push(...result);
|
||||
}
|
||||
}));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async fetchDynamicDebugConfiguration(name: string, providerType: string, folder?: string): Promise<DebugConfiguration | undefined> {
|
||||
const pluginProviders =
|
||||
Array.from(this.configurationProviders.values()).filter(p => (
|
||||
p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic &&
|
||||
p.type === providerType &&
|
||||
p.provideDebugConfigurations
|
||||
));
|
||||
|
||||
for (const provider of pluginProviders) {
|
||||
const configurations = await provider.provideDebugConfigurations(folder);
|
||||
for (const configuration of configurations) {
|
||||
if (configuration.name === name) {
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async provideDynamicDebugConfigurations(folder?: string): Promise<Record<string, DebugConfiguration[]>> {
|
||||
const pluginProviders =
|
||||
Array.from(this.configurationProviders.values()).filter(p => (
|
||||
p.triggerKind === DebugConfigurationProviderTriggerKind.Dynamic &&
|
||||
p.provideDebugConfigurations
|
||||
));
|
||||
|
||||
const configurationsRecord: Record<string, DebugConfiguration[]> = {};
|
||||
|
||||
await Promise.all(pluginProviders.map(async provider => {
|
||||
const configurations = await provider.provideDebugConfigurations(folder);
|
||||
let configurationsPerType = configurationsRecord[provider.type];
|
||||
configurationsPerType = configurationsPerType ? configurationsPerType.concat(configurations) : configurations;
|
||||
|
||||
if (configurationsPerType.length > 0) {
|
||||
configurationsRecord[provider.type] = configurationsPerType;
|
||||
}
|
||||
}));
|
||||
|
||||
return configurationsRecord;
|
||||
}
|
||||
|
||||
async resolveDebugConfiguration(
|
||||
config: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null> {
|
||||
const allProviders = Array.from(this.configurationProviders.values());
|
||||
|
||||
const resolvers = allProviders
|
||||
.filter(p => p.type === config.type && !!p.resolveDebugConfiguration)
|
||||
.map(p => p.resolveDebugConfiguration);
|
||||
|
||||
// Append debug type '*' at the end
|
||||
resolvers.push(
|
||||
...allProviders
|
||||
.filter(p => p.type === '*' && !!p.resolveDebugConfiguration)
|
||||
.map(p => p.resolveDebugConfiguration)
|
||||
);
|
||||
|
||||
const resolved = await this.resolveDebugConfigurationByResolversChain(config, workspaceFolderUri, resolvers);
|
||||
|
||||
return resolved ? this.delegated.resolveDebugConfiguration(resolved, workspaceFolderUri) : resolved;
|
||||
}
|
||||
|
||||
async resolveDebugConfigurationWithSubstitutedVariables(
|
||||
config: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined
|
||||
): Promise<DebugConfiguration | undefined | null> {
|
||||
const allProviders = Array.from(this.configurationProviders.values());
|
||||
|
||||
const resolvers = allProviders
|
||||
.filter(p => p.type === config.type && !!p.resolveDebugConfigurationWithSubstitutedVariables)
|
||||
.map(p => p.resolveDebugConfigurationWithSubstitutedVariables);
|
||||
|
||||
// Append debug type '*' at the end
|
||||
resolvers.push(
|
||||
...allProviders
|
||||
.filter(p => p.type === '*' && !!p.resolveDebugConfigurationWithSubstitutedVariables)
|
||||
.map(p => p.resolveDebugConfigurationWithSubstitutedVariables)
|
||||
);
|
||||
|
||||
const resolved = await this.resolveDebugConfigurationByResolversChain(config, workspaceFolderUri, resolvers);
|
||||
|
||||
return resolved
|
||||
? this.delegated.resolveDebugConfigurationWithSubstitutedVariables(resolved, workspaceFolderUri)
|
||||
: resolved;
|
||||
}
|
||||
|
||||
protected async resolveDebugConfigurationByResolversChain(
|
||||
config: DebugConfiguration,
|
||||
workspaceFolderUri: string | undefined,
|
||||
resolvers: ((
|
||||
folder: string | undefined,
|
||||
debugConfiguration: DebugConfiguration
|
||||
) => Promise<DebugConfiguration | null | undefined>)[]
|
||||
): Promise<DebugConfiguration | undefined | null> {
|
||||
let resolved: DebugConfiguration | undefined | null = config;
|
||||
for (const resolver of resolvers) {
|
||||
try {
|
||||
if (!resolved) {
|
||||
// A provider has indicated to stop and process undefined or null as per specified in the vscode API
|
||||
// https://code.visualstudio.com/api/references/vscode-api#DebugConfigurationProvider
|
||||
break;
|
||||
}
|
||||
resolved = await resolver(workspaceFolderUri, resolved);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
registerDebugger(contribution: DebuggerContribution): Disposable {
|
||||
this.debuggers.push(contribution);
|
||||
return Disposable.create(() => {
|
||||
const index = this.debuggers.indexOf(contribution);
|
||||
if (index !== -1) {
|
||||
this.debuggers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async provideDebuggerVariables(debugType: string): Promise<CommandIdVariables> {
|
||||
for (const contribution of this.debuggers) {
|
||||
if (contribution.type === debugType) {
|
||||
const variables = contribution.variables;
|
||||
if (variables && Object.keys(variables).length > 0) {
|
||||
return variables;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
async getDebuggersForLanguage(language: string): Promise<DebuggerDescription[]> {
|
||||
const debuggers = await this.delegated.getDebuggersForLanguage(language);
|
||||
|
||||
for (const contributor of this.debuggers) {
|
||||
const languages = contributor.languages;
|
||||
if (languages && languages.indexOf(language) !== -1) {
|
||||
const { label, type } = contributor;
|
||||
debuggers.push({ type, label: label || type });
|
||||
}
|
||||
}
|
||||
|
||||
return debuggers;
|
||||
}
|
||||
|
||||
async getSchemaAttributes(debugType: string): Promise<IJSONSchema[]> {
|
||||
let schemas = await this.delegated.getSchemaAttributes(debugType);
|
||||
for (const contribution of this.debuggers) {
|
||||
if (contribution.configurationAttributes &&
|
||||
(contribution.type === debugType || contribution.type === '*' || debugType === '*')) {
|
||||
schemas = schemas.concat(this.resolveSchemaAttributes(contribution.type, contribution.configurationAttributes));
|
||||
}
|
||||
}
|
||||
return schemas;
|
||||
}
|
||||
|
||||
protected resolveSchemaAttributes(type: string, configurationAttributes: { [request: string]: IJSONSchema }): IJSONSchema[] {
|
||||
const taskSchema = {};
|
||||
return Object.keys(configurationAttributes).map(request => {
|
||||
const attributes: IJSONSchema = deepClone(configurationAttributes[request]);
|
||||
const defaultRequired = ['name', 'type', 'request'];
|
||||
attributes.required = attributes.required && attributes.required.length ? defaultRequired.concat(attributes.required) : defaultRequired;
|
||||
attributes.additionalProperties = false;
|
||||
attributes.type = 'object';
|
||||
if (!attributes.properties) {
|
||||
attributes.properties = {};
|
||||
}
|
||||
const properties = attributes.properties;
|
||||
properties['type'] = {
|
||||
enum: [type],
|
||||
description: nls.localizeByDefault('Type of configuration.'),
|
||||
pattern: '^(?!node2)',
|
||||
errorMessage: nls.localizeByDefault('The debug type is not recognized. Make sure that you have a corresponding debug extension installed and that it is enabled.'),
|
||||
patternErrorMessage: nls.localizeByDefault('"node2" is no longer supported, use "node" instead and set the "protocol" attribute to "inspector".')
|
||||
};
|
||||
properties['name'] = {
|
||||
type: 'string',
|
||||
description: nls.localizeByDefault('Name of configuration; appears in the launch configuration dropdown menu.'),
|
||||
default: 'Launch'
|
||||
};
|
||||
properties['request'] = {
|
||||
enum: [request],
|
||||
description: nls.localizeByDefault('Request type of configuration. Can be "launch" or "attach".'),
|
||||
};
|
||||
properties['debugServer'] = {
|
||||
type: 'number',
|
||||
description: nls.localizeByDefault(
|
||||
'For debug extension development only: if a port is specified VS Code tries to connect to a debug adapter running in server mode'
|
||||
),
|
||||
default: 4711
|
||||
};
|
||||
properties['preLaunchTask'] = {
|
||||
anyOf: [taskSchema, {
|
||||
type: ['string'],
|
||||
}],
|
||||
default: '',
|
||||
description: nls.localizeByDefault('Task to run before debug session starts.')
|
||||
};
|
||||
properties['postDebugTask'] = {
|
||||
anyOf: [taskSchema, {
|
||||
type: ['string'],
|
||||
}],
|
||||
default: '',
|
||||
description: nls.localizeByDefault('Task to run after debug session ends.')
|
||||
};
|
||||
properties['internalConsoleOptions'] = {
|
||||
enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'],
|
||||
default: 'openOnFirstSessionStart',
|
||||
description: nls.localizeByDefault('Controls when the internal Debug Console should open.')
|
||||
};
|
||||
properties['suppressMultipleSessionWarning'] = {
|
||||
type: 'boolean',
|
||||
description: nls.localizeByDefault('Disable the warning when trying to start the same debug configuration more than once.'),
|
||||
default: true
|
||||
};
|
||||
|
||||
const osProperties = Object.assign({}, properties);
|
||||
properties['windows'] = {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Windows specific launch configuration attributes.'),
|
||||
properties: osProperties
|
||||
};
|
||||
properties['osx'] = {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('OS X specific launch configuration attributes.'),
|
||||
properties: osProperties
|
||||
};
|
||||
properties['linux'] = {
|
||||
type: 'object',
|
||||
description: nls.localizeByDefault('Linux specific launch configuration attributes.'),
|
||||
properties: osProperties
|
||||
};
|
||||
Object.keys(attributes.properties).forEach(name => {
|
||||
// Use schema allOf property to get independent error reporting #21113
|
||||
attributes!.properties![name].pattern = attributes!.properties![name].pattern || '^(?!.*\\$\\{(env|config|command)\\.)';
|
||||
attributes!.properties![name].patternErrorMessage = attributes!.properties![name].patternErrorMessage ||
|
||||
nls.localizeByDefault("'env.', 'config.' and 'command.' are deprecated, use 'env:', 'config:' and 'command:' instead.");
|
||||
});
|
||||
|
||||
return attributes;
|
||||
});
|
||||
}
|
||||
|
||||
async getConfigurationSnippets(): Promise<IJSONSchemaSnippet[]> {
|
||||
let snippets = await this.delegated.getConfigurationSnippets();
|
||||
|
||||
for (const contribution of this.debuggers) {
|
||||
if (contribution.configurationSnippets) {
|
||||
snippets = snippets.concat(contribution.configurationSnippets);
|
||||
}
|
||||
}
|
||||
|
||||
return snippets;
|
||||
}
|
||||
|
||||
async createDebugSession(config: DebugConfiguration, workspaceFolder: string | undefined): Promise<string> {
|
||||
const contributor = this.contributors.get(config.type);
|
||||
if (contributor) {
|
||||
const sessionId = await contributor.createDebugSession(config, workspaceFolder);
|
||||
this.sessionId2contrib.set(sessionId, contributor);
|
||||
return sessionId;
|
||||
} else {
|
||||
return this.delegated.createDebugSession(config, workspaceFolder);
|
||||
}
|
||||
}
|
||||
|
||||
async terminateDebugSession(sessionId: string): Promise<void> {
|
||||
const contributor = this.sessionId2contrib.get(sessionId);
|
||||
if (contributor) {
|
||||
this.sessionId2contrib.delete(sessionId);
|
||||
return contributor.terminateDebugSession(sessionId);
|
||||
} else {
|
||||
return this.delegated.terminateDebugSession(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DebugSessionContributionRegistry, DebugSessionContribution } from '@theia/debug/lib/browser/debug-session-contribution';
|
||||
import { injectable, inject, named, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
|
||||
/**
|
||||
* Debug session contribution registrator.
|
||||
*/
|
||||
export interface PluginDebugSessionContributionRegistrator {
|
||||
/**
|
||||
* Registers [DebugSessionContribution](#DebugSessionContribution).
|
||||
* @param contrib contribution
|
||||
*/
|
||||
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable;
|
||||
|
||||
/**
|
||||
* Unregisters [DebugSessionContribution](#DebugSessionContribution).
|
||||
* @param debugType the debug type
|
||||
*/
|
||||
unregisterDebugSessionContribution(debugType: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin debug session contribution registry implementation with functionality
|
||||
* to register / unregister plugin contributions.
|
||||
*/
|
||||
@injectable()
|
||||
export class PluginDebugSessionContributionRegistry implements DebugSessionContributionRegistry, PluginDebugSessionContributionRegistrator {
|
||||
protected readonly contribs = new Map<string, DebugSessionContribution>();
|
||||
|
||||
@inject(ContributionProvider) @named(DebugSessionContribution)
|
||||
protected readonly contributions: ContributionProvider<DebugSessionContribution>;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
for (const contrib of this.contributions.getContributions()) {
|
||||
this.contribs.set(contrib.debugType, contrib);
|
||||
}
|
||||
}
|
||||
|
||||
get(debugType: string): DebugSessionContribution | undefined {
|
||||
return this.contribs.get(debugType);
|
||||
}
|
||||
|
||||
registerDebugSessionContribution(contrib: DebugSessionContribution): Disposable {
|
||||
const { debugType } = contrib;
|
||||
|
||||
if (this.contribs.has(debugType)) {
|
||||
console.warn(`Debug session contribution already registered for ${debugType}`);
|
||||
return Disposable.NULL;
|
||||
}
|
||||
|
||||
this.contribs.set(debugType, contrib);
|
||||
return Disposable.create(() => this.unregisterDebugSessionContribution(debugType));
|
||||
}
|
||||
|
||||
unregisterDebugSessionContribution(debugType: string): void {
|
||||
this.contribs.delete(debugType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DefaultDebugSessionFactory } from '@theia/debug/lib/browser/debug-session-contribution';
|
||||
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
|
||||
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
|
||||
import { BreakpointManager } from '@theia/debug/lib/browser/breakpoint/breakpoint-manager';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { MessageClient } from '@theia/core/lib/common/message-service-protocol';
|
||||
import { OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
||||
import { DebugPreferences } from '@theia/debug/lib/common/debug-preferences';
|
||||
import { DebugConfigurationSessionOptions, TestRunReference } from '@theia/debug/lib/browser/debug-session-options';
|
||||
import { DebugSession } from '@theia/debug/lib/browser/debug-session';
|
||||
import { DebugSessionConnection } from '@theia/debug/lib/browser/debug-session-connection';
|
||||
import { TerminalWidgetOptions, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
|
||||
import { TerminalOptionsExt } from '../../../common/plugin-api-rpc';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { DebugContribution } from '@theia/debug/lib/browser/debug-contribution';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import { PluginChannel } from '../../../common/connection';
|
||||
import { TestService } from '@theia/test/lib/browser/test-service';
|
||||
import { DebugSessionManager } from '@theia/debug/lib/browser/debug-session-manager';
|
||||
import { CommandService } from '@theia/core';
|
||||
|
||||
export class PluginDebugSession extends DebugSession {
|
||||
constructor(
|
||||
override readonly id: string,
|
||||
override readonly options: DebugConfigurationSessionOptions,
|
||||
override readonly parentSession: DebugSession | undefined,
|
||||
testService: TestService,
|
||||
testRun: TestRunReference | undefined,
|
||||
sessionManager: DebugSessionManager,
|
||||
protected override readonly connection: DebugSessionConnection,
|
||||
protected override readonly terminalServer: TerminalService,
|
||||
protected override readonly editorManager: EditorManager,
|
||||
protected override readonly breakpoints: BreakpointManager,
|
||||
protected override readonly labelProvider: LabelProvider,
|
||||
protected override readonly messages: MessageClient,
|
||||
protected override readonly fileService: FileService,
|
||||
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
|
||||
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
|
||||
protected override readonly workspaceService: WorkspaceService,
|
||||
debugPreferences: DebugPreferences,
|
||||
protected override readonly commandService: CommandService) {
|
||||
super(id, options, parentSession, testService, testRun, sessionManager, connection, terminalServer, editorManager, breakpoints,
|
||||
labelProvider, messages, fileService, debugContributionProvider,
|
||||
workspaceService, debugPreferences, commandService);
|
||||
}
|
||||
|
||||
protected override async doCreateTerminal(terminalWidgetOptions: TerminalWidgetOptions): Promise<TerminalWidget> {
|
||||
terminalWidgetOptions = Object.assign({}, terminalWidgetOptions, this.terminalOptionsExt);
|
||||
return super.doCreateTerminal(terminalWidgetOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Session factory for a client debug session that communicates with debug adapter contributed as plugin.
|
||||
* The main difference is to use a connection factory that creates [Channel](#Channel) over Rpc channel.
|
||||
*/
|
||||
export class PluginDebugSessionFactory extends DefaultDebugSessionFactory {
|
||||
constructor(
|
||||
protected override readonly terminalService: TerminalService,
|
||||
protected override readonly editorManager: EditorManager,
|
||||
protected override readonly breakpoints: BreakpointManager,
|
||||
protected override readonly labelProvider: LabelProvider,
|
||||
protected override readonly messages: MessageClient,
|
||||
protected override readonly outputChannelManager: OutputChannelManager,
|
||||
protected override readonly debugPreferences: DebugPreferences,
|
||||
protected readonly connectionFactory: (sessionId: string) => Promise<PluginChannel>,
|
||||
protected override readonly fileService: FileService,
|
||||
protected readonly terminalOptionsExt: TerminalOptionsExt | undefined,
|
||||
protected override readonly debugContributionProvider: ContributionProvider<DebugContribution>,
|
||||
protected override readonly testService: TestService,
|
||||
protected override readonly workspaceService: WorkspaceService,
|
||||
protected override readonly commandService: CommandService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
override get(manager: DebugSessionManager, sessionId: string, options: DebugConfigurationSessionOptions, parentSession?: DebugSession): DebugSession {
|
||||
const connection = new DebugSessionConnection(
|
||||
sessionId,
|
||||
this.connectionFactory,
|
||||
this.getTraceOutputChannel());
|
||||
|
||||
return new PluginDebugSession(
|
||||
sessionId,
|
||||
options,
|
||||
parentSession,
|
||||
this.testService,
|
||||
options.testRun,
|
||||
manager,
|
||||
connection,
|
||||
this.terminalService,
|
||||
this.editorManager,
|
||||
this.breakpoints,
|
||||
this.labelProvider,
|
||||
this.messages,
|
||||
this.fileService,
|
||||
this.terminalOptionsExt,
|
||||
this.debugContributionProvider,
|
||||
this.workspaceService,
|
||||
this.debugPreferences,
|
||||
this.commandService
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import {
|
||||
DecorationData,
|
||||
DecorationRequest,
|
||||
DecorationsExt,
|
||||
DecorationsMain,
|
||||
MAIN_RPC_CONTEXT
|
||||
} from '../../../common/plugin-api-rpc';
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { RPCProtocol } from '../../../common/rpc-protocol';
|
||||
import { UriComponents } from '../../../common/uri-components';
|
||||
import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
|
||||
import { CancellationToken } from '@theia/core/lib/common/cancellation';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Decoration, DecorationsService } from '@theia/core/lib/browser/decorations-service';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/api/browser/mainThreadDecorations.ts#L85
|
||||
|
||||
class DecorationRequestsQueue {
|
||||
|
||||
private idPool = 0;
|
||||
private requests = new Map<number, DecorationRequest>();
|
||||
private resolver = new Map<number, (data: DecorationData) => void>();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private timer: any;
|
||||
|
||||
constructor(
|
||||
private readonly proxy: DecorationsExt,
|
||||
private readonly handle: number
|
||||
) {
|
||||
}
|
||||
|
||||
enqueue(uri: URI, token: CancellationToken): Promise<DecorationData> {
|
||||
const id = ++this.idPool;
|
||||
const result = new Promise<DecorationData>(resolve => {
|
||||
this.requests.set(id, { id, uri: VSCodeURI.parse(uri.toString()) });
|
||||
this.resolver.set(id, resolve);
|
||||
this.processQueue();
|
||||
});
|
||||
token.onCancellationRequested(() => {
|
||||
this.requests.delete(id);
|
||||
this.resolver.delete(id);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
if (typeof this.timer === 'number') {
|
||||
// already queued
|
||||
return;
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
// make request
|
||||
const requests = this.requests;
|
||||
const resolver = this.resolver;
|
||||
this.proxy.$provideDecorations(this.handle, [...requests.values()], CancellationToken.None).then(data => {
|
||||
for (const [id, resolve] of resolver) {
|
||||
resolve(data[id]);
|
||||
}
|
||||
});
|
||||
|
||||
// reset
|
||||
this.requests = new Map();
|
||||
this.resolver = new Map();
|
||||
this.timer = undefined;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export class DecorationsMainImpl implements DecorationsMain, Disposable {
|
||||
|
||||
private readonly proxy: DecorationsExt;
|
||||
private readonly providers = new Map<number, [Emitter<URI[]>, Disposable]>();
|
||||
private readonly decorationsService: DecorationsService;
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DECORATIONS_EXT);
|
||||
this.decorationsService = container.get(DecorationsService);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.providers.forEach(value => value.forEach(v => v.dispose()));
|
||||
this.providers.clear();
|
||||
}
|
||||
|
||||
async $registerDecorationProvider(handle: number): Promise<void> {
|
||||
const emitter = new Emitter<URI[]>();
|
||||
const queue = new DecorationRequestsQueue(this.proxy, handle);
|
||||
const registration = this.decorationsService.registerDecorationsProvider({
|
||||
onDidChange: emitter.event,
|
||||
provideDecorations: async (uri, token) => {
|
||||
const data = await queue.enqueue(uri, token);
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
const [bubble, tooltip, letter, themeColor] = data;
|
||||
return <Decoration>{
|
||||
weight: 10,
|
||||
bubble: bubble ?? false,
|
||||
colorId: themeColor?.id,
|
||||
tooltip,
|
||||
letter
|
||||
};
|
||||
}
|
||||
});
|
||||
this.providers.set(handle, [emitter, registration]);
|
||||
}
|
||||
|
||||
$onDidChange(handle: number, resources: UriComponents[]): void {
|
||||
const providerSet = this.providers.get(handle);
|
||||
if (providerSet) {
|
||||
const [emitter] = providerSet;
|
||||
emitter.fire(resources && resources.map(r => new URI(VSCodeURI.revive(r).toString())));
|
||||
}
|
||||
}
|
||||
|
||||
$unregisterDecorationProvider(handle: number): void {
|
||||
const provider = this.providers.get(handle);
|
||||
if (provider) {
|
||||
provider.forEach(p => p.dispose());
|
||||
this.providers.delete(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
packages/plugin-ext/src/main/browser/dialogs-main.ts
Normal file
185
packages/plugin-ext/src/main/browser/dialogs-main.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { OpenDialogOptionsMain, SaveDialogOptionsMain, DialogsMain, UploadDialogOptionsMain } from '../../common/plugin-api-rpc';
|
||||
import { OpenFileDialogProps, SaveFileDialogProps, FileDialogService } from '@theia/filesystem/lib/browser';
|
||||
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { FileUploadService } from '@theia/filesystem/lib/common/upload/file-upload';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileStat } from '@theia/filesystem/lib/common/files';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { nls } from '@theia/core';
|
||||
|
||||
export class DialogsMainImpl implements DialogsMain {
|
||||
|
||||
private workspaceService: WorkspaceService;
|
||||
private fileService: FileService;
|
||||
private environments: EnvVariablesServer;
|
||||
|
||||
private fileDialogService: FileDialogService;
|
||||
private uploadService: FileUploadService;
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this.workspaceService = container.get(WorkspaceService);
|
||||
this.fileService = container.get(FileService);
|
||||
this.environments = container.get(EnvVariablesServer);
|
||||
this.fileDialogService = container.get(FileDialogService);
|
||||
this.uploadService = container.get(FileUploadService);
|
||||
}
|
||||
|
||||
protected async getRootStat(defaultUri: string | undefined): Promise<FileStat | undefined> {
|
||||
let rootStat: FileStat | undefined;
|
||||
|
||||
// Try to use default URI as root
|
||||
if (defaultUri) {
|
||||
try {
|
||||
rootStat = await this.fileService.resolve(new URI(defaultUri));
|
||||
} catch {
|
||||
rootStat = undefined;
|
||||
}
|
||||
|
||||
// Try to use as root the parent folder of existing file URI/non existing URI
|
||||
if (rootStat && !rootStat.isDirectory || !rootStat) {
|
||||
try {
|
||||
rootStat = await this.fileService.resolve(new URI(defaultUri).parent);
|
||||
} catch {
|
||||
rootStat = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use workspace service root if there is no pre-configured URI
|
||||
if (!rootStat) {
|
||||
rootStat = (await this.workspaceService.roots)[0];
|
||||
}
|
||||
|
||||
// Try to use current user home if root folder is still not taken
|
||||
if (!rootStat) {
|
||||
const homeDirUri = await this.environments.getHomeDirUri();
|
||||
try {
|
||||
rootStat = await this.fileService.resolve(new URI(homeDirUri));
|
||||
} catch { }
|
||||
}
|
||||
|
||||
return rootStat;
|
||||
}
|
||||
|
||||
async $showOpenDialog(options: OpenDialogOptionsMain): Promise<string[] | undefined> {
|
||||
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
|
||||
if (!rootStat) {
|
||||
throw new Error('Unable to find the rootStat');
|
||||
}
|
||||
|
||||
try {
|
||||
const canSelectFiles = typeof options.canSelectFiles === 'boolean' ? options.canSelectFiles : true;
|
||||
const canSelectFolders = typeof options.canSelectFolders === 'boolean' ? options.canSelectFolders : true;
|
||||
|
||||
let title = options.title;
|
||||
if (!title) {
|
||||
if (canSelectFiles && canSelectFolders) {
|
||||
title = 'Open';
|
||||
} else {
|
||||
if (canSelectFiles) {
|
||||
title = 'Open File';
|
||||
} else {
|
||||
title = 'Open Folder';
|
||||
}
|
||||
if (options.canSelectMany) {
|
||||
title += '(s)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create open file dialog props
|
||||
const dialogProps = {
|
||||
title: title,
|
||||
openLabel: options.openLabel,
|
||||
canSelectFiles: options.canSelectFiles,
|
||||
canSelectFolders: options.canSelectFolders,
|
||||
canSelectMany: options.canSelectMany,
|
||||
filters: options.filters
|
||||
} as OpenFileDialogProps;
|
||||
|
||||
const result = await this.fileDialogService.showOpenDialog(dialogProps, rootStat);
|
||||
if (Array.isArray(result)) {
|
||||
return result.map(uri => uri.path.toString());
|
||||
} else {
|
||||
return result ? [result].map(uri => uri.path.toString()) : undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async $showSaveDialog(options: SaveDialogOptionsMain): Promise<string | undefined> {
|
||||
const rootStat = await this.getRootStat(options.defaultUri ? options.defaultUri : undefined);
|
||||
|
||||
// File name field should be empty unless the URI is a file
|
||||
let fileNameValue = '';
|
||||
if (options.defaultUri) {
|
||||
let defaultURIStat: FileStat | undefined;
|
||||
try {
|
||||
defaultURIStat = await this.fileService.resolve(new URI(options.defaultUri));
|
||||
} catch { }
|
||||
if (defaultURIStat && !defaultURIStat.isDirectory || !defaultURIStat) {
|
||||
fileNameValue = new URI(options.defaultUri).path.base;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Create save file dialog props
|
||||
const dialogProps = {
|
||||
title: options.title ?? nls.localizeByDefault('Save'),
|
||||
saveLabel: options.saveLabel,
|
||||
filters: options.filters,
|
||||
inputValue: fileNameValue
|
||||
} as SaveFileDialogProps;
|
||||
|
||||
const result = await this.fileDialogService.showSaveDialog(dialogProps, rootStat);
|
||||
if (result) {
|
||||
return result.path.toString();
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async $showUploadDialog(options: UploadDialogOptionsMain): Promise<string[] | undefined> {
|
||||
const rootStat = await this.getRootStat(options.defaultUri);
|
||||
|
||||
// Fail if root not fount
|
||||
if (!rootStat) {
|
||||
throw new Error('Failed to resolve base directory where files should be uploaded');
|
||||
}
|
||||
|
||||
const uploadResult = await this.uploadService.upload(rootStat.resource.toString());
|
||||
|
||||
if (uploadResult) {
|
||||
return uploadResult.uploaded;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@lumino/messaging';
|
||||
import { codiconArray, Key } from '@theia/core/lib/browser';
|
||||
import { AbstractDialog } from '@theia/core/lib/browser/dialogs';
|
||||
import '../../../../src/main/browser/dialogs/style/modal-notification.css';
|
||||
import { MainMessageItem, MainMessageOptions } from '../../../common/plugin-api-rpc';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export enum MessageType {
|
||||
Error = 'error',
|
||||
Warning = 'warning',
|
||||
Info = 'info'
|
||||
}
|
||||
|
||||
const NOTIFICATION = 'modal-Notification';
|
||||
const ICON = 'icon';
|
||||
const TEXT = 'text';
|
||||
const DETAIL = 'detail';
|
||||
|
||||
@injectable()
|
||||
export class ModalNotification extends AbstractDialog<string | undefined> {
|
||||
|
||||
protected actionTitle: string | undefined;
|
||||
|
||||
constructor() {
|
||||
super({ title: FrontendApplicationConfigProvider.get().applicationName });
|
||||
}
|
||||
|
||||
protected override onCloseRequest(msg: Message): void {
|
||||
this.actionTitle = undefined;
|
||||
this.accept();
|
||||
}
|
||||
|
||||
get value(): string | undefined {
|
||||
return this.actionTitle;
|
||||
}
|
||||
|
||||
showDialog(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): Promise<string | undefined> {
|
||||
this.contentNode.appendChild(this.createMessageNode(messageType, text, options, actions));
|
||||
return this.open();
|
||||
}
|
||||
|
||||
protected createMessageNode(messageType: MessageType, text: string, options: MainMessageOptions, actions: MainMessageItem[]): HTMLElement {
|
||||
const messageNode = document.createElement('div');
|
||||
messageNode.classList.add(NOTIFICATION);
|
||||
|
||||
const iconContainer = messageNode.appendChild(document.createElement('div'));
|
||||
iconContainer.classList.add(ICON);
|
||||
const iconElement = iconContainer.appendChild(document.createElement('i'));
|
||||
iconElement.classList.add(...this.toIconClass(messageType), messageType.toString());
|
||||
|
||||
const textContainer = messageNode.appendChild(document.createElement('div'));
|
||||
textContainer.classList.add(TEXT);
|
||||
const textElement = textContainer.appendChild(document.createElement('p'));
|
||||
textElement.textContent = text;
|
||||
|
||||
if (options.detail) {
|
||||
const detailContainer = textContainer.appendChild(document.createElement('div'));
|
||||
detailContainer.classList.add(DETAIL);
|
||||
const detailElement = detailContainer.appendChild(document.createElement('p'));
|
||||
detailElement.textContent = options.detail;
|
||||
}
|
||||
|
||||
actions.forEach((action: MainMessageItem, index: number) => {
|
||||
const button = index === 0
|
||||
? this.appendAcceptButton(action.title)
|
||||
: this.createButton(action.title);
|
||||
button.classList.add('main');
|
||||
this.controlPanel.appendChild(button);
|
||||
this.addKeyListener(button,
|
||||
Key.ENTER,
|
||||
() => {
|
||||
this.actionTitle = action.title;
|
||||
this.accept();
|
||||
},
|
||||
'click');
|
||||
});
|
||||
if (actions.length <= 0) {
|
||||
this.appendAcceptButton();
|
||||
} else if (!actions.some(action => action.isCloseAffordance === true)) {
|
||||
this.appendCloseButton(nls.localizeByDefault('Close'));
|
||||
}
|
||||
|
||||
return messageNode;
|
||||
}
|
||||
|
||||
protected toIconClass(icon: MessageType): string[] {
|
||||
if (icon === MessageType.Error) {
|
||||
return codiconArray('error');
|
||||
}
|
||||
if (icon === MessageType.Warning) {
|
||||
return codiconArray('warning');
|
||||
}
|
||||
return codiconArray('info');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
.modal-Notification {
|
||||
pointer-events: all;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
clear: both;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
min-width: 200px;
|
||||
max-width: min(66vw, 800px);
|
||||
background-color: var(--theia-editorWidget-background);
|
||||
min-height: 35px;
|
||||
margin-bottom: 1px;
|
||||
color: var(--theia-editorWidget-foreground);
|
||||
}
|
||||
|
||||
.modal-Notification .icon {
|
||||
display: inline-block;
|
||||
font-size: 20px;
|
||||
padding: 5px 0;
|
||||
width: 35px;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.modal-Notification .icon .codicon {
|
||||
line-height: inherit;
|
||||
vertical-align: middle;
|
||||
font-size: calc(var(--theia-ui-padding) * 5);
|
||||
color: var(--theia-editorInfo-foreground);
|
||||
}
|
||||
|
||||
.modal-Notification .icon .error {
|
||||
color: var(--theia-editorError-foreground);
|
||||
}
|
||||
|
||||
.modal-Notification .icon .warning {
|
||||
color: var(--theia-editorWarning-foreground);
|
||||
}
|
||||
|
||||
.modal-Notification .text {
|
||||
order: 2;
|
||||
display: inline-block;
|
||||
max-height: min(66vh, 600px);
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
align-self: center;
|
||||
flex: 1 100%;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.modal-Notification .text > p {
|
||||
margin: 0;
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
font-family: var(--theia-ui-font-family);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.modal-Notification .buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
order: 3;
|
||||
white-space: nowrap;
|
||||
align-self: flex-end;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.modal-Notification .buttons > button {
|
||||
background-color: var(--theia-button-background);
|
||||
color: var(--theia-button-foreground);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
margin: 0;
|
||||
font-size: var(--theia-ui-font-size1);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-Notification .buttons > button:hover {
|
||||
background-color: var(--theia-button-hoverBackground);
|
||||
}
|
||||
|
||||
.modal-Notification .detail {
|
||||
align-self: center;
|
||||
order: 3;
|
||||
flex: 1 100%;
|
||||
color: var(--theia-descriptionForeground);
|
||||
}
|
||||
|
||||
.modal-Notification .detail > p {
|
||||
margin: calc(var(--theia-ui-padding) * 2) 0px 0px 0px;
|
||||
}
|
||||
|
||||
.modal-Notification .text {
|
||||
padding: calc(var(--theia-ui-padding) * 1.5);
|
||||
}
|
||||
294
packages/plugin-ext/src/main/browser/documents-main.ts
Normal file
294
packages/plugin-ext/src/main/browser/documents-main.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { DocumentsMain, MAIN_RPC_CONTEXT, DocumentsExt } from '../../common/plugin-api-rpc';
|
||||
import { UriComponents } from '../../common/uri-components';
|
||||
import { EditorsAndDocumentsMain } from './editors-and-documents-main';
|
||||
import { DisposableCollection, Disposable, UntitledResourceResolver } from '@theia/core';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { EditorModelService } from './text-editor-model-service';
|
||||
import { EditorOpenerOptions, EncodingMode } from '@theia/editor/lib/browser';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { URI as CodeURI } from '@theia/core/shared/vscode-uri';
|
||||
import { ApplicationShell, SaveReason } from '@theia/core/lib/browser';
|
||||
import { TextDocumentShowOptions } from '../../common/plugin-api-rpc-model';
|
||||
import { Range } from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
import { Reference } from '@theia/core/lib/common/reference';
|
||||
import { dispose } from '../../common/disposable-util';
|
||||
import { MonacoLanguages } from '@theia/monaco/lib/browser/monaco-languages';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { TextDocumentChangeReason } from '../../plugin/types-impl';
|
||||
import { NotebookDocumentsMainImpl } from './notebooks/notebook-documents-main';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
export class ModelReferenceCollection {
|
||||
|
||||
private data = new Array<{ length: number, dispose(): void }>();
|
||||
private length = 0;
|
||||
|
||||
constructor(
|
||||
private readonly maxAge: number = 1000 * 60 * 3,
|
||||
private readonly maxLength: number = 1024 * 1024 * 80
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
this.data = dispose(this.data) || [];
|
||||
}
|
||||
|
||||
add(ref: Reference<MonacoEditorModel>): void {
|
||||
const length = ref.object.textEditorModel.getValueLength();
|
||||
const handle = setTimeout(_dispose, this.maxAge);
|
||||
const entry = { length, dispose: _dispose };
|
||||
const self = this;
|
||||
function _dispose(): void {
|
||||
const idx = self.data.indexOf(entry);
|
||||
if (idx >= 0) {
|
||||
self.length -= length;
|
||||
ref.dispose();
|
||||
clearTimeout(handle);
|
||||
self.data.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
|
||||
this.data.push(entry);
|
||||
this.length += length;
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
private cleanup(): void {
|
||||
while (this.length > this.maxLength) {
|
||||
this.data[0].dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class DocumentsMainImpl implements DocumentsMain, Disposable {
|
||||
|
||||
private readonly proxy: DocumentsExt;
|
||||
private readonly syncedModels = new Map<string, Disposable>();
|
||||
private readonly modelReferenceCache = new ModelReferenceCollection();
|
||||
|
||||
protected saveTimeout = 1750;
|
||||
|
||||
private readonly toDispose = new DisposableCollection(this.modelReferenceCache);
|
||||
|
||||
constructor(
|
||||
editorsAndDocuments: EditorsAndDocumentsMain,
|
||||
notebookDocuments: NotebookDocumentsMainImpl,
|
||||
private readonly modelService: EditorModelService,
|
||||
rpc: RPCProtocol,
|
||||
private openerService: OpenerService,
|
||||
private shell: ApplicationShell,
|
||||
private untitledResourceResolver: UntitledResourceResolver,
|
||||
private languageService: MonacoLanguages
|
||||
) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.DOCUMENTS_EXT);
|
||||
|
||||
this.toDispose.push(editorsAndDocuments);
|
||||
this.toDispose.push(editorsAndDocuments.onDocumentAdd(documents => documents.forEach(this.onModelAdded, this)));
|
||||
this.toDispose.push(editorsAndDocuments.onDocumentRemove(documents => documents.forEach(this.onModelRemoved, this)));
|
||||
this.toDispose.push(modelService.onModelModeChanged(this.onModelChanged, this));
|
||||
|
||||
this.toDispose.push(notebookDocuments.onDidAddNotebookCellModel(this.onModelAdded, this));
|
||||
|
||||
this.toDispose.push(modelService.onModelSaved(m => {
|
||||
this.proxy.$acceptModelSaved(m.textEditorModel.uri);
|
||||
}));
|
||||
this.toDispose.push(modelService.onModelWillSave(async e => {
|
||||
|
||||
const saveReason = e.options?.saveReason ?? SaveReason.Manual;
|
||||
|
||||
const edits = await this.proxy.$acceptModelWillSave(new URI(e.model.uri).toComponents(), saveReason.valueOf(), this.saveTimeout);
|
||||
const editOperations: monaco.editor.IIdentifiedSingleEditOperation[] = [];
|
||||
for (const edit of edits) {
|
||||
const { range, text } = edit;
|
||||
if (!range && !text) {
|
||||
continue;
|
||||
}
|
||||
if (range && range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn && !edit.text) {
|
||||
continue;
|
||||
}
|
||||
|
||||
editOperations.push({
|
||||
range: range ? monaco.Range.lift(range) : e.model.textEditorModel.getFullModelRange(),
|
||||
/* eslint-disable-next-line no-null/no-null */
|
||||
text: text || null,
|
||||
forceMoveMarkers: edit.forceMoveMarkers
|
||||
});
|
||||
}
|
||||
e.model.textEditorModel.applyEdits(editOperations);
|
||||
}));
|
||||
this.toDispose.push(modelService.onModelDirtyChanged(m => {
|
||||
this.proxy.$acceptDirtyStateChanged(m.textEditorModel.uri, m.dirty);
|
||||
}));
|
||||
this.toDispose.push(modelService.onModelEncodingChanged(e => {
|
||||
this.proxy.$acceptEncodingChanged(e.model.textEditorModel.uri, e.encoding);
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private onModelChanged(event: { model: MonacoEditorModel, oldModeId: string }): void {
|
||||
const modelUrl = event.model.textEditorModel.uri;
|
||||
if (this.syncedModels.has(modelUrl.toString())) {
|
||||
this.proxy.$acceptModelModeChanged(modelUrl, event.oldModeId, event.model.languageId);
|
||||
}
|
||||
}
|
||||
|
||||
private onModelAdded(model: MonacoEditorModel): void {
|
||||
const modelUri = model.textEditorModel.uri;
|
||||
const key = modelUri.toString();
|
||||
|
||||
const toDispose = new DisposableCollection(
|
||||
model.textEditorModel.onDidChangeContent(e =>
|
||||
this.proxy.$acceptModelChanged(modelUri, {
|
||||
eol: e.eol,
|
||||
versionId: e.versionId,
|
||||
reason: e.isRedoing ? TextDocumentChangeReason.Redo : e.isUndoing ? TextDocumentChangeReason.Undo : undefined,
|
||||
changes: e.changes.map(c =>
|
||||
({
|
||||
text: c.text,
|
||||
range: c.range,
|
||||
rangeLength: c.rangeLength,
|
||||
rangeOffset: c.rangeOffset
|
||||
}))
|
||||
}, model.dirty)
|
||||
),
|
||||
Disposable.create(() => this.syncedModels.delete(key))
|
||||
);
|
||||
this.syncedModels.set(key, toDispose);
|
||||
this.toDispose.push(toDispose);
|
||||
}
|
||||
|
||||
private onModelRemoved(url: monaco.Uri): void {
|
||||
const model = this.syncedModels.get(url.toString());
|
||||
if (model) {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async $tryCreateDocument(options?: { language?: string; content?: string; encoding?: string }): Promise<UriComponents> {
|
||||
const language = options?.language && this.languageService.getExtension(options.language);
|
||||
const content = options?.content;
|
||||
const encoding = options?.encoding;
|
||||
const resource = await this.untitledResourceResolver.createUntitledResource(content, language, undefined, encoding);
|
||||
return monaco.Uri.parse(resource.uri.toString());
|
||||
}
|
||||
|
||||
async $tryShowDocument(uri: UriComponents, options?: TextDocumentShowOptions): Promise<void> {
|
||||
// Removing try-catch block here makes it not possible to handle errors.
|
||||
// Following message is appeared in browser console
|
||||
// - Uncaught (in promise) Error: Cannot read property 'message' of undefined.
|
||||
try {
|
||||
const editorOptions = DocumentsMainImpl.toEditorOpenerOptions(this.shell, options);
|
||||
const uriArg = new URI(CodeURI.revive(uri));
|
||||
const opener = await this.openerService.getOpener(uriArg, editorOptions);
|
||||
await opener.open(uriArg, editorOptions);
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async $trySaveDocument(uri: UriComponents): Promise<boolean> {
|
||||
return this.modelService.save(new URI(CodeURI.revive(uri)));
|
||||
}
|
||||
|
||||
async $tryOpenDocument(uri: UriComponents, encoding?: string): Promise<boolean> {
|
||||
// Convert URI to Theia URI
|
||||
const theiaUri = new URI(CodeURI.revive(uri));
|
||||
|
||||
// Create model reference
|
||||
const ref = await this.modelService.createModelReference(theiaUri);
|
||||
|
||||
if (ref.object) {
|
||||
// If we have encoding option, make sure to apply it
|
||||
if (encoding && ref.object.setEncoding) {
|
||||
try {
|
||||
await ref.object.setEncoding(encoding, EncodingMode.Decode);
|
||||
} catch (e) {
|
||||
// If encoding fails, log error but continue
|
||||
console.error(`Failed to set encoding ${encoding} for ${theiaUri.toString()}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
this.modelReferenceCache.add(ref);
|
||||
return true;
|
||||
} else {
|
||||
ref.dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static toEditorOpenerOptions(shell: ApplicationShell, options?: TextDocumentShowOptions): EditorOpenerOptions | undefined {
|
||||
if (!options) {
|
||||
return undefined;
|
||||
}
|
||||
let range: Range | undefined;
|
||||
if (options.selection) {
|
||||
const selection = options.selection;
|
||||
range = {
|
||||
start: { line: selection.startLineNumber - 1, character: selection.startColumn - 1 },
|
||||
end: { line: selection.endLineNumber - 1, character: selection.endColumn - 1 }
|
||||
};
|
||||
}
|
||||
/* fall back to side group -> split relative to the active widget */
|
||||
let widgetOptions: ApplicationShell.WidgetOptions | undefined = { mode: 'split-right' };
|
||||
let viewColumn = options.viewColumn;
|
||||
if (viewColumn === -2) {
|
||||
/* show besides -> compute current column and adjust viewColumn accordingly */
|
||||
const tabBars = shell.mainAreaTabBars;
|
||||
const currentTabBar = shell.currentTabBar;
|
||||
if (currentTabBar) {
|
||||
const currentColumn = tabBars.indexOf(currentTabBar);
|
||||
if (currentColumn > -1) {
|
||||
// +2 because conversion from 0-based to 1-based index and increase of 1
|
||||
viewColumn = currentColumn + 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (viewColumn === undefined || viewColumn === -1) {
|
||||
/* active group -> skip (default behaviour) */
|
||||
widgetOptions = undefined;
|
||||
} else if (viewColumn > 0 && shell.mainAreaTabBars.length > 0) {
|
||||
const tabBars = shell.mainAreaTabBars;
|
||||
if (viewColumn <= tabBars.length) {
|
||||
// convert to zero-based index
|
||||
const tabBar = tabBars[viewColumn - 1];
|
||||
if (tabBar?.currentTitle) {
|
||||
widgetOptions = { ref: tabBar.currentTitle.owner };
|
||||
}
|
||||
} else {
|
||||
const tabBar = tabBars[tabBars.length - 1];
|
||||
if (tabBar?.currentTitle) {
|
||||
widgetOptions!.ref = tabBar.currentTitle.owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
selection: range,
|
||||
mode: options.preserveFocus ? 'reveal' : 'activate',
|
||||
preview: options.preview,
|
||||
widgetOptions
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import * as monaco from '@theia/monaco-editor-core';
|
||||
import { type ILineChange } from '@theia/monaco-editor-core/esm/vs/editor/common/diff/legacyLinesDiffComputer';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import {
|
||||
MAIN_RPC_CONTEXT,
|
||||
EditorsAndDocumentsExt,
|
||||
EditorsAndDocumentsDelta,
|
||||
ModelAddedData,
|
||||
TextEditorAddData,
|
||||
EditorPosition
|
||||
} from '../../common/plugin-api-rpc';
|
||||
import { Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { EditorModelService } from './text-editor-model-service';
|
||||
import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model';
|
||||
import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor';
|
||||
import { TextEditorMain } from './text-editor-main';
|
||||
import { DisposableCollection, Emitter, URI } from '@theia/core';
|
||||
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
|
||||
import { SaveableService } from '@theia/core/lib/browser/saveable-service';
|
||||
import { TabsMainImpl } from './tabs/tabs-main';
|
||||
import { NotebookCellEditorService, NotebookEditorWidgetService } from '@theia/notebook/lib/browser';
|
||||
import { SimpleMonacoEditor } from '@theia/monaco/lib/browser/simple-monaco-editor';
|
||||
import { EncodingRegistry } from '@theia/core/lib/browser/encoding-registry';
|
||||
|
||||
export class EditorsAndDocumentsMain implements Disposable {
|
||||
|
||||
private readonly proxy: EditorsAndDocumentsExt;
|
||||
|
||||
private readonly stateComputer: EditorAndDocumentStateComputer;
|
||||
private readonly textEditors = new Map<string, TextEditorMain>();
|
||||
|
||||
private readonly modelService: EditorModelService;
|
||||
private readonly editorManager: EditorManager;
|
||||
private readonly saveResourceService: SaveableService;
|
||||
private readonly encodingRegistry: EncodingRegistry;
|
||||
|
||||
private readonly onTextEditorAddEmitter = new Emitter<TextEditorMain[]>();
|
||||
private readonly onTextEditorRemoveEmitter = new Emitter<string[]>();
|
||||
private readonly onDocumentAddEmitter = new Emitter<MonacoEditorModel[]>();
|
||||
private readonly onDocumentRemoveEmitter = new Emitter<monaco.Uri[]>();
|
||||
|
||||
readonly onTextEditorAdd = this.onTextEditorAddEmitter.event;
|
||||
readonly onTextEditorRemove = this.onTextEditorRemoveEmitter.event;
|
||||
readonly onDocumentAdd = this.onDocumentAddEmitter.event;
|
||||
readonly onDocumentRemove = this.onDocumentRemoveEmitter.event;
|
||||
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.textEditors.clear())
|
||||
);
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container, tabsMain: TabsMainImpl) {
|
||||
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.EDITORS_AND_DOCUMENTS_EXT);
|
||||
|
||||
this.editorManager = container.get(EditorManager);
|
||||
this.modelService = container.get(EditorModelService);
|
||||
this.saveResourceService = container.get(SaveableService);
|
||||
this.encodingRegistry = container.get(EncodingRegistry);
|
||||
|
||||
this.stateComputer = new EditorAndDocumentStateComputer(d => this.onDelta(d),
|
||||
this.editorManager,
|
||||
container.get(NotebookCellEditorService),
|
||||
container.get(NotebookEditorWidgetService),
|
||||
this.modelService, tabsMain);
|
||||
this.toDispose.push(this.stateComputer);
|
||||
this.toDispose.push(this.onTextEditorAddEmitter);
|
||||
this.toDispose.push(this.onTextEditorRemoveEmitter);
|
||||
this.toDispose.push(this.onDocumentAddEmitter);
|
||||
this.toDispose.push(this.onDocumentRemoveEmitter);
|
||||
}
|
||||
|
||||
listen(): void {
|
||||
this.stateComputer.listen();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private onDelta(delta: EditorAndDocumentStateDelta): void {
|
||||
const removedEditors = new Array<string>();
|
||||
const addedEditors = new Array<TextEditorMain>();
|
||||
|
||||
const removedDocuments = delta.removedDocuments.map(d => d.textEditorModel.uri);
|
||||
|
||||
for (const editor of delta.addedEditors) {
|
||||
const textEditorMain = new TextEditorMain(editor.id, editor.editor.getControl().getModel()!, editor.editor);
|
||||
this.textEditors.set(editor.id, textEditorMain);
|
||||
this.toDispose.push(textEditorMain);
|
||||
addedEditors.push(textEditorMain);
|
||||
}
|
||||
|
||||
for (const { id } of delta.removedEditors) {
|
||||
const textEditorMain = this.textEditors.get(id);
|
||||
if (textEditorMain) {
|
||||
textEditorMain.dispose();
|
||||
this.textEditors.delete(id);
|
||||
removedEditors.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const deltaExt: EditorsAndDocumentsDelta = {};
|
||||
let empty = true;
|
||||
|
||||
if (delta.newActiveEditor !== undefined) {
|
||||
empty = false;
|
||||
deltaExt.newActiveEditor = delta.newActiveEditor;
|
||||
}
|
||||
if (removedDocuments.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.removedDocuments = removedDocuments;
|
||||
}
|
||||
if (removedEditors.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.removedEditors = removedEditors;
|
||||
}
|
||||
if (delta.addedDocuments.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.addedDocuments = delta.addedDocuments.map(d => this.toModelAddData(d));
|
||||
}
|
||||
if (delta.addedEditors.length > 0) {
|
||||
empty = false;
|
||||
deltaExt.addedEditors = addedEditors.map(e => this.toTextEditorAddData(e));
|
||||
}
|
||||
|
||||
if (!empty) {
|
||||
this.proxy.$acceptEditorsAndDocumentsDelta(deltaExt);
|
||||
this.onDocumentRemoveEmitter.fire(removedDocuments);
|
||||
this.onDocumentAddEmitter.fire(delta.addedDocuments);
|
||||
this.onTextEditorRemoveEmitter.fire(removedEditors);
|
||||
this.onTextEditorAddEmitter.fire(addedEditors);
|
||||
}
|
||||
}
|
||||
|
||||
private toModelAddData(model: MonacoEditorModel): ModelAddedData {
|
||||
return {
|
||||
uri: model.textEditorModel.uri,
|
||||
versionId: model.textEditorModel.getVersionId(),
|
||||
lines: model.textEditorModel.getLinesContent(),
|
||||
languageId: model.getLanguageId(),
|
||||
EOL: model.textEditorModel.getEOL(),
|
||||
modeId: model.languageId,
|
||||
isDirty: model.dirty,
|
||||
encoding: this.encodingRegistry.getEncodingForResource(URI.fromComponents(model.textEditorModel.uri), model.getEncoding())
|
||||
};
|
||||
}
|
||||
|
||||
private toTextEditorAddData(textEditor: TextEditorMain): TextEditorAddData {
|
||||
const properties = textEditor.getProperties();
|
||||
return {
|
||||
id: textEditor.getId(),
|
||||
documentUri: textEditor.getModel().uri,
|
||||
options: properties!.options,
|
||||
selections: properties!.selections,
|
||||
visibleRanges: properties!.visibleRanges,
|
||||
editorPosition: this.findEditorPosition(textEditor)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private findEditorPosition(editor: TextEditorMain): EditorPosition | undefined {
|
||||
return EditorPosition.ONE; // TODO: fix this when Theia has support splitting editors
|
||||
}
|
||||
|
||||
getEditor(id: string): TextEditorMain | undefined {
|
||||
return this.textEditors.get(id);
|
||||
}
|
||||
|
||||
async save(uri: URI): Promise<URI | undefined> {
|
||||
const editor = await this.editorManager.getByUri(uri);
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
return this.saveResourceService.save(editor);
|
||||
}
|
||||
|
||||
async saveAs(uri: URI): Promise<URI | undefined> {
|
||||
const editor = await this.editorManager.getByUri(uri);
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
if (!this.saveResourceService.canSaveAs(editor)) {
|
||||
return undefined;
|
||||
}
|
||||
return this.saveResourceService.saveAs(editor);
|
||||
}
|
||||
|
||||
saveAll(includeUntitled?: boolean): Promise<boolean> {
|
||||
return this.modelService.saveAll(includeUntitled);
|
||||
}
|
||||
|
||||
hideEditor(id: string): Promise<void> {
|
||||
for (const editorWidget of this.editorManager.all) {
|
||||
const monacoEditor = MonacoEditor.get(editorWidget);
|
||||
if (monacoEditor) {
|
||||
if (id === new EditorSnapshot(monacoEditor).id) {
|
||||
editorWidget.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
getDiffInformation(id: string): ILineChange[] {
|
||||
const editor = this.getEditor(id);
|
||||
return editor?.diffInformation || [];
|
||||
}
|
||||
}
|
||||
|
||||
class EditorAndDocumentStateComputer implements Disposable {
|
||||
private currentState: EditorAndDocumentState | undefined;
|
||||
private readonly editors = new Map<string, DisposableCollection>();
|
||||
private readonly toDispose = new DisposableCollection(
|
||||
Disposable.create(() => this.currentState = undefined)
|
||||
);
|
||||
|
||||
constructor(
|
||||
private callback: (delta: EditorAndDocumentStateDelta) => void,
|
||||
private readonly editorService: EditorManager,
|
||||
private readonly cellEditorService: NotebookCellEditorService,
|
||||
private readonly notebookWidgetService: NotebookEditorWidgetService,
|
||||
private readonly modelService: EditorModelService,
|
||||
private readonly tabsMain: TabsMainImpl
|
||||
) { }
|
||||
|
||||
listen(): void {
|
||||
if (this.toDispose.disposed) {
|
||||
return;
|
||||
}
|
||||
this.toDispose.push(this.editorService.onCreated(async widget => {
|
||||
await this.tabsMain.waitForWidget(widget);
|
||||
this.onTextEditorAdd(widget);
|
||||
this.update();
|
||||
}));
|
||||
this.toDispose.push(this.editorService.onCurrentEditorChanged(async widget => {
|
||||
if (widget) {
|
||||
await this.tabsMain.waitForWidget(widget);
|
||||
}
|
||||
this.update();
|
||||
}));
|
||||
this.toDispose.push(this.modelService.onModelAdded(this.onModelAdded, this));
|
||||
this.toDispose.push(this.modelService.onModelRemoved(() => this.update()));
|
||||
|
||||
this.toDispose.push(this.cellEditorService.onDidChangeCellEditors(() => this.update()));
|
||||
|
||||
this.toDispose.push(this.notebookWidgetService.onDidChangeCurrentEditor(() => {
|
||||
this.currentState = this.currentState && new EditorAndDocumentState(
|
||||
this.currentState.documents,
|
||||
this.currentState.editors,
|
||||
undefined
|
||||
);
|
||||
}));
|
||||
|
||||
for (const widget of this.editorService.all) {
|
||||
this.onTextEditorAdd(widget);
|
||||
}
|
||||
this.update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.toDispose.dispose();
|
||||
}
|
||||
|
||||
private onModelAdded(model: MonacoEditorModel): void {
|
||||
if (!this.currentState) {
|
||||
this.update();
|
||||
return;
|
||||
}
|
||||
this.currentState = new EditorAndDocumentState(
|
||||
this.currentState.documents.add(model),
|
||||
this.currentState.editors,
|
||||
this.currentState.activeEditor);
|
||||
|
||||
this.callback(new EditorAndDocumentStateDelta(
|
||||
[],
|
||||
[model],
|
||||
[],
|
||||
[],
|
||||
undefined,
|
||||
undefined
|
||||
));
|
||||
}
|
||||
|
||||
private onTextEditorAdd(widget: EditorWidget): void {
|
||||
if (widget.isDisposed) {
|
||||
return;
|
||||
}
|
||||
const editor = MonacoEditor.get(widget);
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const id = editor.getControl().getId();
|
||||
const toDispose = new DisposableCollection(
|
||||
editor.onDispose(() => this.onTextEditorRemove(editor)),
|
||||
Disposable.create(() => this.editors.delete(id))
|
||||
);
|
||||
this.editors.set(id, toDispose);
|
||||
this.toDispose.push(toDispose);
|
||||
}
|
||||
|
||||
private onTextEditorRemove(e: MonacoEditor): void {
|
||||
const toDispose = this.editors.get(e.getControl().getId());
|
||||
if (toDispose) {
|
||||
toDispose.dispose();
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
const models = new Set<MonacoEditorModel>();
|
||||
for (const model of this.modelService.getModels()) {
|
||||
models.add(model);
|
||||
}
|
||||
|
||||
let activeId: string | null = null;
|
||||
const activeEditor = MonacoEditor.getCurrent(this.editorService) ?? this.cellEditorService.getActiveCell();
|
||||
|
||||
const editors = new Map<string, EditorSnapshot>();
|
||||
for (const widget of this.editorService.all) {
|
||||
const editor = MonacoEditor.get(widget);
|
||||
// VS Code tracks only visible widgets
|
||||
if (!editor || !widget.isVisible) {
|
||||
continue;
|
||||
}
|
||||
const model = editor.getControl().getModel();
|
||||
if (model && !model.isDisposed()) {
|
||||
const editorSnapshot = new EditorSnapshot(editor);
|
||||
editors.set(editorSnapshot.id, editorSnapshot);
|
||||
if (activeEditor === editor) {
|
||||
activeId = editorSnapshot.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const editor of this.cellEditorService.allCellEditors) {
|
||||
if (editor.getControl()?.getModel()) {
|
||||
const editorSnapshot = new EditorSnapshot(editor);
|
||||
editors.set(editorSnapshot.id, editorSnapshot);
|
||||
if (activeEditor === editor) {
|
||||
activeId = editorSnapshot.id;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const newState = new EditorAndDocumentState(models, editors, activeId);
|
||||
const delta = EditorAndDocumentState.compute(this.currentState, newState);
|
||||
if (!delta.isEmpty) {
|
||||
this.currentState = newState;
|
||||
this.callback(delta);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class EditorAndDocumentStateDelta {
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
constructor(
|
||||
readonly removedDocuments: MonacoEditorModel[],
|
||||
readonly addedDocuments: MonacoEditorModel[],
|
||||
readonly removedEditors: EditorSnapshot[],
|
||||
readonly addedEditors: EditorSnapshot[],
|
||||
readonly oldActiveEditor: string | null | undefined,
|
||||
readonly newActiveEditor: string | null | undefined
|
||||
) {
|
||||
this.isEmpty = this.removedDocuments.length === 0
|
||||
&& this.addedDocuments.length === 0
|
||||
&& this.addedEditors.length === 0
|
||||
&& this.removedEditors.length === 0
|
||||
&& this.newActiveEditor === this.oldActiveEditor;
|
||||
}
|
||||
}
|
||||
|
||||
class EditorAndDocumentState {
|
||||
|
||||
constructor(
|
||||
readonly documents: Set<MonacoEditorModel>,
|
||||
readonly editors: Map<string, EditorSnapshot>,
|
||||
readonly activeEditor: string | null | undefined) {
|
||||
}
|
||||
|
||||
static compute(before: EditorAndDocumentState | undefined, after: EditorAndDocumentState): EditorAndDocumentStateDelta {
|
||||
if (!before) {
|
||||
return new EditorAndDocumentStateDelta(
|
||||
[],
|
||||
Array.from(after.documents),
|
||||
[],
|
||||
Array.from(after.editors.values()),
|
||||
undefined,
|
||||
after.activeEditor
|
||||
);
|
||||
}
|
||||
|
||||
const documentDelta = Delta.ofSets(before.documents, after.documents);
|
||||
const editorDelta = Delta.ofMaps(before.editors, after.editors);
|
||||
const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
|
||||
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
|
||||
return new EditorAndDocumentStateDelta(
|
||||
documentDelta.removed,
|
||||
documentDelta.added,
|
||||
editorDelta.removed,
|
||||
editorDelta.added,
|
||||
oldActiveEditor,
|
||||
newActiveEditor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorSnapshot {
|
||||
readonly id: string;
|
||||
constructor(readonly editor: MonacoEditor | SimpleMonacoEditor) {
|
||||
this.id = `${editor.getControl().getId()},${editor.getControl().getModel()!.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Delta {
|
||||
|
||||
export function ofSets<T>(before: Set<T>, after: Set<T>): { removed: T[], added: T[] } {
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
before.forEach(element => {
|
||||
if (!after.has(element)) {
|
||||
removed.push(element);
|
||||
}
|
||||
});
|
||||
after.forEach(element => {
|
||||
if (!before.has(element)) {
|
||||
added.push(element);
|
||||
}
|
||||
});
|
||||
return { removed, added };
|
||||
}
|
||||
|
||||
export function ofMaps<K, V>(before: Map<K, V>, after: Map<K, V>): { removed: V[], added: V[] } {
|
||||
const removed: V[] = [];
|
||||
const added: V[] = [];
|
||||
before.forEach((value, index) => {
|
||||
if (!after.has(index)) {
|
||||
removed.push(value);
|
||||
}
|
||||
});
|
||||
after.forEach((value, index) => {
|
||||
if (!before.has(index)) {
|
||||
added.push(value);
|
||||
}
|
||||
});
|
||||
return { removed, added };
|
||||
}
|
||||
}
|
||||
60
packages/plugin-ext/src/main/browser/env-main.ts
Normal file
60
packages/plugin-ext/src/main/browser/env-main.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { QueryParameters } from '../../common/env';
|
||||
export { EnvMainImpl } from '../common/env-main';
|
||||
|
||||
/**
|
||||
* Returns query parameters from current page.
|
||||
*/
|
||||
export function getQueryParameters(): QueryParameters {
|
||||
const queryParameters: QueryParameters = {};
|
||||
if (window.location.search !== '') {
|
||||
const queryParametersString = window.location.search.substring(1); // remove question mark
|
||||
const params = queryParametersString.split('&');
|
||||
for (const pair of params) {
|
||||
if (pair === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const keyValue = pair.split('=');
|
||||
let key: string = keyValue[0];
|
||||
let value: string = keyValue[1] ? keyValue[1] : '';
|
||||
try {
|
||||
key = decodeURIComponent(key);
|
||||
if (value !== '') {
|
||||
value = decodeURIComponent(value);
|
||||
}
|
||||
} catch (error) {
|
||||
// skip malformed URI sequence
|
||||
continue;
|
||||
}
|
||||
|
||||
const existedValue = queryParameters[key];
|
||||
if (existedValue) {
|
||||
if (existedValue instanceof Array) {
|
||||
existedValue.push(value);
|
||||
} else {
|
||||
// existed value is string
|
||||
queryParameters[key] = [existedValue, value];
|
||||
}
|
||||
} else {
|
||||
queryParameters[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return queryParameters;
|
||||
}
|
||||
267
packages/plugin-ext/src/main/browser/file-system-main-impl.ts
Normal file
267
packages/plugin-ext/src/main/browser/file-system-main-impl.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// based on https://github.com/microsoft/vscode/blob/04c36be045a94fee58e5f8992d3e3fd980294a84/src/vs/workbench/api/browser/mainThreadFileSystem.ts
|
||||
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable @typescript-eslint/tslint/config */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { interfaces } from '@theia/core/shared/inversify';
|
||||
import CoreURI from '@theia/core/lib/common/uri';
|
||||
import { BinaryBuffer } from '@theia/core/lib/common/buffer';
|
||||
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { Event, Emitter } from '@theia/core/lib/common/event';
|
||||
import { MAIN_RPC_CONTEXT, FileSystemMain, FileSystemExt, IFileChangeDto } from '../../common/plugin-api-rpc';
|
||||
import { RPCProtocol } from '../../common/rpc-protocol';
|
||||
import { UriComponents } from '../../common/uri-components';
|
||||
import {
|
||||
FileSystemProviderCapabilities, Stat, FileType, FileSystemProviderErrorCode, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, FileWriteOptions, WatchOptions,
|
||||
FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability,
|
||||
FileStat, FileChange, FileOperationError, FileOperationResult, ReadOnlyMessageFileSystemProvider
|
||||
} from '@theia/filesystem/lib/common/files';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { MarkdownString } from '../../common/plugin-api-rpc-model';
|
||||
|
||||
type IDisposable = Disposable;
|
||||
|
||||
export class FileSystemMainImpl implements FileSystemMain, Disposable {
|
||||
|
||||
private readonly _proxy: FileSystemExt;
|
||||
private readonly _fileProvider = new Map<number, RemoteFileSystemProvider>();
|
||||
private readonly _fileService: FileService;
|
||||
private readonly _disposables = new DisposableCollection();
|
||||
|
||||
constructor(rpc: RPCProtocol, container: interfaces.Container) {
|
||||
this._proxy = rpc.getProxy(MAIN_RPC_CONTEXT.FILE_SYSTEM_EXT);
|
||||
this._fileService = container.get(FileService);
|
||||
|
||||
for (const { scheme, capabilities } of this._fileService.listCapabilities()) {
|
||||
this._proxy.$acceptProviderInfos(scheme, capabilities);
|
||||
}
|
||||
|
||||
this._disposables.push(this._fileService.onDidChangeFileSystemProviderRegistrations(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider?.capabilities)));
|
||||
this._disposables.push(this._fileService.onDidChangeFileSystemProviderCapabilities(e => this._proxy.$acceptProviderInfos(e.scheme, e.provider.capabilities)));
|
||||
this._disposables.push(Disposable.create(() => this._fileProvider.forEach(value => value.dispose())));
|
||||
this._disposables.push(Disposable.create(() => this._fileProvider.clear()));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
$registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: MarkdownString): void {
|
||||
this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy, readonlyMessage));
|
||||
}
|
||||
|
||||
$unregisterProvider(handle: number): void {
|
||||
const provider = this._fileProvider.get(handle);
|
||||
if (provider) {
|
||||
provider.dispose();
|
||||
this._fileProvider.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
$onFileSystemChange(handle: number, changes: IFileChangeDto[]): void {
|
||||
const fileProvider = this._fileProvider.get(handle);
|
||||
if (!fileProvider) {
|
||||
throw new Error('Unknown file provider');
|
||||
}
|
||||
fileProvider.$onFileSystemChange(changes);
|
||||
}
|
||||
|
||||
// --- consumer fs, vscode.workspace.fs
|
||||
|
||||
$stat(uri: UriComponents): Promise<Stat> {
|
||||
return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: true }).then(stat => ({
|
||||
ctime: stat.ctime,
|
||||
mtime: stat.mtime,
|
||||
size: stat.size,
|
||||
type: FileStat.asFileType(stat)
|
||||
})).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$readdir(uri: UriComponents): Promise<[string, FileType][]> {
|
||||
return this._fileService.resolve(new CoreURI(URI.revive(uri)), { resolveMetadata: false }).then(stat => {
|
||||
if (!stat.isDirectory) {
|
||||
const err = new Error(stat.name);
|
||||
err.name = FileSystemProviderErrorCode.FileNotADirectory;
|
||||
throw err;
|
||||
}
|
||||
return !stat.children ? [] : stat.children.map(child => [child.name, FileStat.asFileType(child)] as [string, FileType]);
|
||||
}).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$readFile(uri: UriComponents): Promise<BinaryBuffer> {
|
||||
return this._fileService.readFile(new CoreURI(URI.revive(uri))).then(file => file.value).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$writeFile(uri: UriComponents, content: BinaryBuffer): Promise<void> {
|
||||
return this._fileService.writeFile(new CoreURI(URI.revive(uri)), content)
|
||||
.then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.move(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), {
|
||||
...opts,
|
||||
fromUserGesture: false
|
||||
}).then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.copy(new CoreURI(URI.revive(source)), new CoreURI(URI.revive(target)), {
|
||||
...opts,
|
||||
fromUserGesture: false
|
||||
}).then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$mkdir(uri: UriComponents): Promise<void> {
|
||||
return this._fileService.createFolder(new CoreURI(URI.revive(uri)), { fromUserGesture: false })
|
||||
.then(() => undefined).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
$delete(uri: UriComponents, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._fileService.delete(new CoreURI(URI.revive(uri)), opts).catch(FileSystemMainImpl._handleError);
|
||||
}
|
||||
|
||||
private static _handleError(err: any): never {
|
||||
if (err instanceof FileOperationError) {
|
||||
switch (err.fileOperationResult) {
|
||||
case FileOperationResult.FILE_NOT_FOUND:
|
||||
err.name = FileSystemProviderErrorCode.FileNotFound;
|
||||
break;
|
||||
case FileOperationResult.FILE_IS_DIRECTORY:
|
||||
err.name = FileSystemProviderErrorCode.FileIsADirectory;
|
||||
break;
|
||||
case FileOperationResult.FILE_PERMISSION_DENIED:
|
||||
err.name = FileSystemProviderErrorCode.NoPermissions;
|
||||
break;
|
||||
case FileOperationResult.FILE_MOVE_CONFLICT:
|
||||
err.name = FileSystemProviderErrorCode.FileExists;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class RemoteFileSystemProvider implements FileSystemProviderWithFileReadWriteCapability, FileSystemProviderWithOpenReadWriteCloseCapability, FileSystemProviderWithFileFolderCopyCapability, ReadOnlyMessageFileSystemProvider {
|
||||
|
||||
private readonly _onDidChange = new Emitter<readonly FileChange[]>();
|
||||
private readonly _registration: IDisposable;
|
||||
|
||||
readonly onDidChangeFile: Event<readonly FileChange[]> = this._onDidChange.event;
|
||||
readonly onFileWatchError: Event<void> = new Emitter<void>().event; // dummy, never fired
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
readonly onDidChangeReadOnlyMessage: Event<MarkdownString | undefined> = Event.None;
|
||||
|
||||
constructor(
|
||||
fileService: FileService,
|
||||
scheme: string,
|
||||
capabilities: FileSystemProviderCapabilities,
|
||||
private readonly _handle: number,
|
||||
private readonly _proxy: FileSystemExt,
|
||||
public readonly readOnlyMessage: MarkdownString | undefined = undefined
|
||||
) {
|
||||
this.capabilities = capabilities;
|
||||
this._registration = fileService.registerProvider(scheme, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._registration.dispose();
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
watch(resource: CoreURI, opts: WatchOptions) {
|
||||
const session = Math.random();
|
||||
this._proxy.$watch(this._handle, session, resource['codeUri'], opts);
|
||||
return Disposable.create(() => {
|
||||
this._proxy.$unwatch(this._handle, session);
|
||||
});
|
||||
}
|
||||
|
||||
$onFileSystemChange(changes: IFileChangeDto[]): void {
|
||||
this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange));
|
||||
}
|
||||
|
||||
private static _createFileChange(dto: IFileChangeDto): FileChange {
|
||||
return { resource: new CoreURI(URI.revive(dto.resource)), type: dto.type };
|
||||
}
|
||||
|
||||
// --- forwarding calls
|
||||
|
||||
stat(resource: CoreURI): Promise<Stat> {
|
||||
return this._proxy.$stat(this._handle, resource['codeUri']).then(undefined, err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
readFile(resource: CoreURI): Promise<Uint8Array> {
|
||||
return this._proxy.$readFile(this._handle, resource['codeUri']).then(buffer => buffer.buffer);
|
||||
}
|
||||
|
||||
writeFile(resource: CoreURI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
return this._proxy.$writeFile(this._handle, resource['codeUri'], BinaryBuffer.wrap(content), opts);
|
||||
}
|
||||
|
||||
delete(resource: CoreURI, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._proxy.$delete(this._handle, resource['codeUri'], opts);
|
||||
}
|
||||
|
||||
mkdir(resource: CoreURI): Promise<void> {
|
||||
return this._proxy.$mkdir(this._handle, resource['codeUri']);
|
||||
}
|
||||
|
||||
readdir(resource: CoreURI): Promise<[string, FileType][]> {
|
||||
return this._proxy.$readdir(this._handle, resource['codeUri']);
|
||||
}
|
||||
|
||||
rename(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$rename(this._handle, resource['codeUri'], target['codeUri'], opts);
|
||||
}
|
||||
|
||||
copy(resource: CoreURI, target: CoreURI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$copy(this._handle, resource['codeUri'], target['codeUri'], opts);
|
||||
}
|
||||
|
||||
open(resource: CoreURI, opts: FileOpenOptions): Promise<number> {
|
||||
return this._proxy.$open(this._handle, resource['codeUri'], opts);
|
||||
}
|
||||
|
||||
close(fd: number): Promise<void> {
|
||||
return this._proxy.$close(this._handle, fd);
|
||||
}
|
||||
|
||||
read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this._proxy.$read(this._handle, fd, pos, length).then(readData => {
|
||||
data.set(readData.buffer, offset);
|
||||
return readData.byteLength;
|
||||
});
|
||||
}
|
||||
|
||||
write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
||||
return this._proxy.$write(this._handle, fd, pos, BinaryBuffer.wrap(data).slice(offset, offset + length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { CallHierarchyIncomingCall, CallHierarchyItem, CallHierarchyOutgoingCall } from '@theia/callhierarchy/lib/browser';
|
||||
import * as languageProtocol from '@theia/core/shared/vscode-languageserver-protocol';
|
||||
import { URI } from '@theia/core/shared/vscode-uri';
|
||||
import { TypeHierarchyItem } from '@theia/typehierarchy/lib/browser';
|
||||
import * as rpc from '../../../common/plugin-api-rpc';
|
||||
import * as model from '../../../common/plugin-api-rpc-model';
|
||||
import { UriComponents } from '../../../common/uri-components';
|
||||
|
||||
export function toUriComponents(uri: string): UriComponents {
|
||||
return URI.parse(uri);
|
||||
}
|
||||
|
||||
export function fromUriComponents(uri: UriComponents): string {
|
||||
return URI.revive(uri).toString();
|
||||
}
|
||||
|
||||
export function fromLocation(location: languageProtocol.Location): model.Location {
|
||||
return <model.Location>{
|
||||
uri: URI.parse(location.uri),
|
||||
range: fromRange(location.range)
|
||||
};
|
||||
}
|
||||
|
||||
export function toLocation(uri: UriComponents, range: model.Range): languageProtocol.Location {
|
||||
return {
|
||||
uri: URI.revive(uri).toString(),
|
||||
range: toRange(range)
|
||||
};
|
||||
}
|
||||
|
||||
export function fromPosition(position: languageProtocol.Position): rpc.Position {
|
||||
return <rpc.Position>{
|
||||
lineNumber: position.line,
|
||||
column: position.character
|
||||
};
|
||||
}
|
||||
|
||||
export function fromRange(range: languageProtocol.Range): model.Range {
|
||||
const { start, end } = range;
|
||||
return {
|
||||
startLineNumber: start.line + 1,
|
||||
startColumn: start.character + 1,
|
||||
endLineNumber: end.line + 1,
|
||||
endColumn: end.character + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function toRange(range: model.Range): languageProtocol.Range {
|
||||
return languageProtocol.Range.create(
|
||||
range.startLineNumber - 1,
|
||||
range.startColumn - 1,
|
||||
range.endLineNumber - 1,
|
||||
range.endColumn - 1,
|
||||
);
|
||||
}
|
||||
|
||||
export namespace SymbolKindConverter {
|
||||
export function fromSymbolKind(kind: languageProtocol.SymbolKind): model.SymbolKind {
|
||||
switch (kind) {
|
||||
case languageProtocol.SymbolKind.File: return model.SymbolKind.File;
|
||||
case languageProtocol.SymbolKind.Module: return model.SymbolKind.Module;
|
||||
case languageProtocol.SymbolKind.Namespace: return model.SymbolKind.Namespace;
|
||||
case languageProtocol.SymbolKind.Package: return model.SymbolKind.Package;
|
||||
case languageProtocol.SymbolKind.Class: return model.SymbolKind.Class;
|
||||
case languageProtocol.SymbolKind.Method: return model.SymbolKind.Method;
|
||||
case languageProtocol.SymbolKind.Property: return model.SymbolKind.Property;
|
||||
case languageProtocol.SymbolKind.Field: return model.SymbolKind.Field;
|
||||
case languageProtocol.SymbolKind.Constructor: return model.SymbolKind.Constructor;
|
||||
case languageProtocol.SymbolKind.Enum: return model.SymbolKind.Enum;
|
||||
case languageProtocol.SymbolKind.Interface: return model.SymbolKind.Interface;
|
||||
case languageProtocol.SymbolKind.Function: return model.SymbolKind.Function;
|
||||
case languageProtocol.SymbolKind.Variable: return model.SymbolKind.Variable;
|
||||
case languageProtocol.SymbolKind.Constant: return model.SymbolKind.Constant;
|
||||
case languageProtocol.SymbolKind.String: return model.SymbolKind.String;
|
||||
case languageProtocol.SymbolKind.Number: return model.SymbolKind.Number;
|
||||
case languageProtocol.SymbolKind.Boolean: return model.SymbolKind.Boolean;
|
||||
case languageProtocol.SymbolKind.Array: return model.SymbolKind.Array;
|
||||
case languageProtocol.SymbolKind.Object: return model.SymbolKind.Object;
|
||||
case languageProtocol.SymbolKind.Key: return model.SymbolKind.Key;
|
||||
case languageProtocol.SymbolKind.Null: return model.SymbolKind.Null;
|
||||
case languageProtocol.SymbolKind.EnumMember: return model.SymbolKind.EnumMember;
|
||||
case languageProtocol.SymbolKind.Struct: return model.SymbolKind.Struct;
|
||||
case languageProtocol.SymbolKind.Event: return model.SymbolKind.Event;
|
||||
case languageProtocol.SymbolKind.Operator: return model.SymbolKind.Operator;
|
||||
case languageProtocol.SymbolKind.TypeParameter: return model.SymbolKind.TypeParameter;
|
||||
default: return model.SymbolKind.Property;
|
||||
}
|
||||
}
|
||||
export function toSymbolKind(kind: model.SymbolKind): languageProtocol.SymbolKind {
|
||||
switch (kind) {
|
||||
case model.SymbolKind.File: return languageProtocol.SymbolKind.File;
|
||||
case model.SymbolKind.Module: return languageProtocol.SymbolKind.Module;
|
||||
case model.SymbolKind.Namespace: return languageProtocol.SymbolKind.Namespace;
|
||||
case model.SymbolKind.Package: return languageProtocol.SymbolKind.Package;
|
||||
case model.SymbolKind.Class: return languageProtocol.SymbolKind.Class;
|
||||
case model.SymbolKind.Method: return languageProtocol.SymbolKind.Method;
|
||||
case model.SymbolKind.Property: return languageProtocol.SymbolKind.Property;
|
||||
case model.SymbolKind.Field: return languageProtocol.SymbolKind.Field;
|
||||
case model.SymbolKind.Constructor: return languageProtocol.SymbolKind.Constructor;
|
||||
case model.SymbolKind.Enum: return languageProtocol.SymbolKind.Enum;
|
||||
case model.SymbolKind.Interface: return languageProtocol.SymbolKind.Interface;
|
||||
case model.SymbolKind.Function: return languageProtocol.SymbolKind.Function;
|
||||
case model.SymbolKind.Variable: return languageProtocol.SymbolKind.Variable;
|
||||
case model.SymbolKind.Constant: return languageProtocol.SymbolKind.Constant;
|
||||
case model.SymbolKind.String: return languageProtocol.SymbolKind.String;
|
||||
case model.SymbolKind.Number: return languageProtocol.SymbolKind.Number;
|
||||
case model.SymbolKind.Boolean: return languageProtocol.SymbolKind.Boolean;
|
||||
case model.SymbolKind.Array: return languageProtocol.SymbolKind.Array;
|
||||
case model.SymbolKind.Object: return languageProtocol.SymbolKind.Object;
|
||||
case model.SymbolKind.Key: return languageProtocol.SymbolKind.Key;
|
||||
case model.SymbolKind.Null: return languageProtocol.SymbolKind.Null;
|
||||
case model.SymbolKind.EnumMember: return languageProtocol.SymbolKind.EnumMember;
|
||||
case model.SymbolKind.Struct: return languageProtocol.SymbolKind.Struct;
|
||||
case model.SymbolKind.Event: return languageProtocol.SymbolKind.Event;
|
||||
case model.SymbolKind.Operator: return languageProtocol.SymbolKind.Operator;
|
||||
case model.SymbolKind.TypeParameter: return languageProtocol.SymbolKind.TypeParameter;
|
||||
default: return languageProtocol.SymbolKind.Property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function toItemHierarchyDefinition(modelItem: model.HierarchyItem): TypeHierarchyItem | CallHierarchyItem {
|
||||
return {
|
||||
...modelItem,
|
||||
kind: SymbolKindConverter.toSymbolKind(modelItem.kind),
|
||||
range: toRange(modelItem.range),
|
||||
selectionRange: toRange(modelItem.selectionRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromItemHierarchyDefinition(definition: TypeHierarchyItem | CallHierarchyItem): model.HierarchyItem {
|
||||
return {
|
||||
...definition,
|
||||
kind: SymbolKindConverter.fromSymbolKind(definition.kind),
|
||||
range: fromRange(definition.range),
|
||||
selectionRange: fromRange(definition.range),
|
||||
};
|
||||
}
|
||||
|
||||
export function toCaller(caller: model.CallHierarchyIncomingCall): CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: toItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(toRange)
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCaller(caller: CallHierarchyIncomingCall): model.CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: fromItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(fromRange)
|
||||
};
|
||||
}
|
||||
|
||||
export function toCallee(callee: model.CallHierarchyOutgoingCall): CallHierarchyOutgoingCall {
|
||||
return {
|
||||
to: toItemHierarchyDefinition(callee.to),
|
||||
fromRanges: callee.fromRanges.map(toRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCallHierarchyCallerToModelCallHierarchyIncomingCall(caller: CallHierarchyIncomingCall): model.CallHierarchyIncomingCall {
|
||||
return {
|
||||
from: fromItemHierarchyDefinition(caller.from),
|
||||
fromRanges: caller.fromRanges.map(fromRange),
|
||||
};
|
||||
}
|
||||
|
||||
export function fromCallHierarchyCalleeToModelCallHierarchyOutgoingCall(callee: CallHierarchyOutgoingCall): model.CallHierarchyOutgoingCall {
|
||||
return {
|
||||
to: fromItemHierarchyDefinition(callee.to),
|
||||
fromRanges: callee.fromRanges.map(fromRange),
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
51
packages/plugin-ext/src/main/browser/label-service-main.ts
Normal file
51
packages/plugin-ext/src/main/browser/label-service-main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
1453
packages/plugin-ext/src/main/browser/languages-main.ts
Normal file
1453
packages/plugin-ext/src/main/browser/languages-main.ts
Normal file
File diff suppressed because it is too large
Load Diff
176
packages/plugin-ext/src/main/browser/lm-main.ts
Normal file
176
packages/plugin-ext/src/main/browser/lm-main.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
34
packages/plugin-ext/src/main/browser/localization-main.ts
Normal file
34
packages/plugin-ext/src/main/browser/localization-main.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2023 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
53
packages/plugin-ext/src/main/browser/logger-main.ts
Normal file
53
packages/plugin-ext/src/main/browser/logger-main.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
218
packages/plugin-ext/src/main/browser/main-context.ts
Normal file
218
packages/plugin-ext/src/main/browser/main-context.ts
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
131
packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts
Normal file
131
packages/plugin-ext/src/main/browser/notebooks/notebook-dto.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
159
packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts
Normal file
159
packages/plugin-ext/src/main/browser/notebooks/notebooks-main.ts
Normal 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
|
||||
}))
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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' });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
26
packages/plugin-ext/src/main/browser/notification-main.ts
Normal file
26
packages/plugin-ext/src/main/browser/notification-main.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
});
|
||||
132
packages/plugin-ext/src/main/browser/plugin-ext-widget.tsx
Normal file
132
packages/plugin-ext/src/main/browser/plugin-ext-widget.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
92
packages/plugin-ext/src/main/browser/plugin-icon-service.ts
Normal file
92
packages/plugin-ext/src/main/browser/plugin-icon-service.ts
Normal 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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
154
packages/plugin-ext/src/main/browser/plugin-shared-style.ts
Normal file
154
packages/plugin-ext/src/main/browser/plugin-shared-style.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
55
packages/plugin-ext/src/main/browser/plugin-storage.ts
Normal file
55
packages/plugin-ext/src/main/browser/plugin-storage.ts
Normal 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())
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
126
packages/plugin-ext/src/main/browser/preference-registry-main.ts
Normal file
126
packages/plugin-ext/src/main/browser/preference-registry-main.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
367
packages/plugin-ext/src/main/browser/quick-open-main.ts
Normal file
367
packages/plugin-ext/src/main/browser/quick-open-main.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
518
packages/plugin-ext/src/main/browser/scm-main.ts
Normal file
518
packages/plugin-ext/src/main/browser/scm-main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
107
packages/plugin-ext/src/main/browser/secrets-main.ts
Normal file
107
packages/plugin-ext/src/main/browser/secrets-main.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
353
packages/plugin-ext/src/main/browser/style/comments.css
Normal file
353
packages/plugin-ext/src/main/browser/style/comments.css
Normal 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;
|
||||
}
|
||||
84
packages/plugin-ext/src/main/browser/style/index.css
Normal file
84
packages/plugin-ext/src/main/browser/style/index.css
Normal file
@@ -0,0 +1,84 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 Red Hat, Inc. and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
.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";
|
||||
@@ -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";
|
||||
}
|
||||
54
packages/plugin-ext/src/main/browser/style/tree.css
Normal file
54
packages/plugin-ext/src/main/browser/style/tree.css
Normal 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);
|
||||
}
|
||||
55
packages/plugin-ext/src/main/browser/style/webview.css
Normal file
55
packages/plugin-ext/src/main/browser/style/webview.css
Normal 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;
|
||||
}
|
||||
413
packages/plugin-ext/src/main/browser/tabs/tabs-main.ts
Normal file
413
packages/plugin-ext/src/main/browser/tabs/tabs-main.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
268
packages/plugin-ext/src/main/browser/tasks-main.ts
Normal file
268
packages/plugin-ext/src/main/browser/tasks-main.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
374
packages/plugin-ext/src/main/browser/terminal-main.ts
Normal file
374
packages/plugin-ext/src/main/browser/terminal-main.ts
Normal 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) }));
|
||||
}
|
||||
|
||||
}
|
||||
635
packages/plugin-ext/src/main/browser/test-main.ts
Normal file
635
packages/plugin-ext/src/main/browser/test-main.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
545
packages/plugin-ext/src/main/browser/text-editor-main.ts
Normal file
545
packages/plugin-ext/src/main/browser/text-editor-main.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
239
packages/plugin-ext/src/main/browser/text-editors-main.ts
Normal file
239
packages/plugin-ext/src/main/browser/text-editors-main.ts
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
246
packages/plugin-ext/src/main/browser/theme-icon-override.ts
Normal file
246
packages/plugin-ext/src/main/browser/theme-icon-override.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
42
packages/plugin-ext/src/main/browser/theming-main.ts
Normal file
42
packages/plugin-ext/src/main/browser/theming-main.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
80
packages/plugin-ext/src/main/browser/timeline-main.ts
Normal file
80
packages/plugin-ext/src/main/browser/timeline-main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
72
packages/plugin-ext/src/main/browser/uri-main.ts
Normal file
72
packages/plugin-ext/src/main/browser/uri-main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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`);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
179
packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts
Normal file
179
packages/plugin-ext/src/main/browser/view/plugin-view-widget.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
942
packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Normal file
942
packages/plugin-ext/src/main/browser/view/tree-view-widget.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
207
packages/plugin-ext/src/main/browser/view/tree-views-main.ts
Normal file
207
packages/plugin-ext/src/main/browser/view/tree-views-main.ts
Normal 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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user