mirror of
https://github.com/coder/code-server.git
synced 2026-05-06 20:41:59 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
|
||||
// --- other interested parties
|
||||
import { JSONValidationExtensionPoint } from 'vs/workbench/api/common/jsonValidationExtensionPoint';
|
||||
import { ColorExtensionPoint } from 'vs/workbench/services/themes/common/colorExtensionPoint';
|
||||
import { TokenClassificationExtensionPoints } from 'vs/workbench/services/themes/common/tokenClassificationExtensionPoint';
|
||||
import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEditor/browser/languageConfigurationExtensionPoint';
|
||||
|
||||
// --- mainThread participants
|
||||
import './mainThreadBulkEdits';
|
||||
import './mainThreadCodeInsets';
|
||||
import './mainThreadClipboard';
|
||||
import './mainThreadCommands';
|
||||
import './mainThreadConfiguration';
|
||||
import './mainThreadConsole';
|
||||
import './mainThreadDebugService';
|
||||
import './mainThreadDecorations';
|
||||
import './mainThreadDiagnostics';
|
||||
import './mainThreadDialogs';
|
||||
import './mainThreadDocumentContentProviders';
|
||||
import './mainThreadDocuments';
|
||||
import './mainThreadDocumentsAndEditors';
|
||||
import './mainThreadEditor';
|
||||
import './mainThreadEditors';
|
||||
import './mainThreadErrors';
|
||||
import './mainThreadExtensionService';
|
||||
import './mainThreadFileSystem';
|
||||
import './mainThreadFileSystemEventService';
|
||||
import './mainThreadKeytar';
|
||||
import './mainThreadLanguageFeatures';
|
||||
import './mainThreadLanguages';
|
||||
import './mainThreadLogService';
|
||||
import './mainThreadMessageService';
|
||||
import './mainThreadOutputService';
|
||||
import './mainThreadProgress';
|
||||
import './mainThreadQuickOpen';
|
||||
import './mainThreadRemoteConnectionData';
|
||||
import './mainThreadSaveParticipant';
|
||||
import './mainThreadSCM';
|
||||
import './mainThreadSearch';
|
||||
import './mainThreadStatusBar';
|
||||
import './mainThreadStorage';
|
||||
import './mainThreadTelemetry';
|
||||
import './mainThreadTerminalService';
|
||||
import './mainThreadTheming';
|
||||
import './mainThreadTreeViews';
|
||||
import './mainThreadDownloadService';
|
||||
import './mainThreadUrls';
|
||||
import './mainThreadWindow';
|
||||
import './mainThreadWebviewManager';
|
||||
import './mainThreadWorkspace';
|
||||
import './mainThreadComments';
|
||||
import './mainThreadNotebook';
|
||||
import './mainThreadTask';
|
||||
import './mainThreadLabelService';
|
||||
import './mainThreadTunnelService';
|
||||
import './mainThreadAuthentication';
|
||||
import './mainThreadTimeline';
|
||||
import 'vs/workbench/api/common/apiCommands';
|
||||
|
||||
export class ExtensionPoints implements IWorkbenchContribution {
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
// Classes that handle extension points...
|
||||
this.instantiationService.createInstance(JSONValidationExtensionPoint);
|
||||
this.instantiationService.createInstance(ColorExtensionPoint);
|
||||
this.instantiationService.createInstance(TokenClassificationExtensionPoints);
|
||||
this.instantiationService.createInstance(LanguageConfigurationFileHandler);
|
||||
}
|
||||
}
|
||||
|
||||
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExtensionPoints, LifecyclePhase.Starting);
|
||||
@@ -0,0 +1,508 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import * as nls from 'vs/nls';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IAuthenticationService, AllowedExtension, readAllowedExtensions, getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { fromNow } from 'vs/base/common/date';
|
||||
import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
|
||||
|
||||
const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser'];
|
||||
|
||||
interface IAccountUsage {
|
||||
extensionId: string;
|
||||
extensionName: string;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] {
|
||||
const accountKey = `${providerId}-${accountName}-usages`;
|
||||
const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL);
|
||||
let usages: IAccountUsage[] = [];
|
||||
if (storedUsages) {
|
||||
try {
|
||||
usages = JSON.parse(storedUsages);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void {
|
||||
const accountKey = `${providerId}-${accountName}-usages`;
|
||||
storageService.remove(accountKey, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) {
|
||||
const accountKey = `${providerId}-${accountName}-usages`;
|
||||
const usages = 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()
|
||||
});
|
||||
}
|
||||
|
||||
storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
export class MainThreadAuthenticationProvider extends Disposable {
|
||||
private _accounts = new Map<string, string[]>(); // Map account name to session ids
|
||||
private _sessions = new Map<string, string>(); // Map account id to name
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostAuthenticationShape,
|
||||
public readonly id: string,
|
||||
public readonly label: string,
|
||||
public readonly supportsMultipleAccounts: boolean,
|
||||
private readonly notificationService: INotificationService,
|
||||
private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
private readonly storageService: IStorageService,
|
||||
private readonly quickInputService: IQuickInputService,
|
||||
private readonly dialogService: IDialogService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
return this.registerCommandsAndContextMenuItems();
|
||||
}
|
||||
|
||||
public hasSessions(): boolean {
|
||||
return !!this._sessions.size;
|
||||
}
|
||||
|
||||
public manageTrustedExtensions(accountName: string) {
|
||||
const quickPick = this.quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>();
|
||||
quickPick.canSelectMany = true;
|
||||
const allowedExtensions = readAllowedExtensions(this.storageService, this.id, accountName);
|
||||
const usages = readAccountUsages(this.storageService, this.id, accountName);
|
||||
const items = allowedExtensions.map(extension => {
|
||||
const usage = usages.find(usage => extension.id === usage.extensionId);
|
||||
return {
|
||||
label: extension.name,
|
||||
description: usage
|
||||
? nls.localize({ key: 'accountLastUsedDate', comment: ['The placeholder {0} is a string with time information, such as "3 days ago"'] }, "Last used this account {0}", fromNow(usage.lastUsed, true))
|
||||
: nls.localize('notUsed', "Has not used this account"),
|
||||
extension
|
||||
};
|
||||
});
|
||||
|
||||
quickPick.items = items;
|
||||
quickPick.selectedItems = items;
|
||||
quickPick.title = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
|
||||
quickPick.placeholder = nls.localize('manageExensions', "Choose which extensions can access this account");
|
||||
|
||||
quickPick.onDidAccept(() => {
|
||||
const updatedAllowedList = quickPick.selectedItems.map(item => item.extension);
|
||||
this.storageService.store(`${this.id}-${accountName}`, JSON.stringify(updatedAllowedList), StorageScope.GLOBAL);
|
||||
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.onDidHide(() => {
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
}
|
||||
|
||||
private async registerCommandsAndContextMenuItems(): Promise<void> {
|
||||
try {
|
||||
const sessions = await this._proxy.$getSessions(this.id);
|
||||
sessions.forEach(session => this.registerSession(session));
|
||||
} catch (_) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
private registerSession(session: modes.AuthenticationSession) {
|
||||
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]);
|
||||
}
|
||||
|
||||
this.storageKeysSyncRegistryService.registerStorageKey({ key: `${this.id}-${session.account.label}`, version: 1 });
|
||||
}
|
||||
|
||||
async signOut(accountName: string): Promise<void> {
|
||||
const accountUsages = readAccountUsages(this.storageService, this.id, accountName);
|
||||
const sessionsForAccount = this._accounts.get(accountName);
|
||||
|
||||
const result = await this.dialogService.confirm({
|
||||
title: nls.localize('signOutConfirm', "Sign out of {0}", accountName),
|
||||
message: accountUsages.length
|
||||
? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", accountName, accountUsages.map(usage => usage.extensionName).join('\n'))
|
||||
: nls.localize('signOutMessageSimple', "Sign out of {0}?", accountName)
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
|
||||
removeAccountUsage(this.storageService, this.id, accountName);
|
||||
}
|
||||
}
|
||||
|
||||
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
|
||||
return this._proxy.$getSessions(this.id);
|
||||
}
|
||||
|
||||
async updateSessionItems(event: modes.AuthenticationSessionsChangeEvent): Promise<void> {
|
||||
const { added, removed } = event;
|
||||
const session = await this._proxy.$getSessions(this.id);
|
||||
const addedSessions = session.filter(session => added.some(id => id === session.id));
|
||||
|
||||
removed.forEach(sessionId => {
|
||||
const accountName = this._sessions.get(sessionId);
|
||||
if (accountName) {
|
||||
this._sessions.delete(sessionId);
|
||||
let sessionsForAccount = this._accounts.get(accountName) || [];
|
||||
const sessionIndex = sessionsForAccount.indexOf(sessionId);
|
||||
sessionsForAccount.splice(sessionIndex);
|
||||
|
||||
if (!sessionsForAccount.length) {
|
||||
this._accounts.delete(accountName);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addedSessions.forEach(session => this.registerSession(session));
|
||||
}
|
||||
|
||||
login(scopes: string[]): Promise<modes.AuthenticationSession> {
|
||||
return this._proxy.$login(this.id, scopes);
|
||||
}
|
||||
|
||||
async logout(sessionId: string): Promise<void> {
|
||||
await this._proxy.$logout(this.id, sessionId);
|
||||
this.notificationService.info(nls.localize('signedOut', "Successfully signed out."));
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadAuthentication)
|
||||
export class MainThreadAuthentication extends Disposable implements MainThreadAuthenticationShape {
|
||||
private readonly _proxy: ExtHostAuthenticationShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@ICredentialsService private readonly credentialsService: ICredentialsService,
|
||||
@IEncryptionService private readonly encryptionService: IEncryptionService,
|
||||
@IProductService private readonly productService: IProductService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
|
||||
|
||||
this._register(this.authenticationService.onDidChangeSessions(e => {
|
||||
this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.label, e.event);
|
||||
}));
|
||||
|
||||
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(info => {
|
||||
this._proxy.$onDidChangeAuthenticationProviders([info], []);
|
||||
}));
|
||||
|
||||
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(info => {
|
||||
this._proxy.$onDidChangeAuthenticationProviders([], [info]);
|
||||
}));
|
||||
|
||||
this._proxy.$setProviders(this.authenticationService.declaredProviders);
|
||||
|
||||
this._register(this.authenticationService.onDidChangeDeclaredProviders(e => {
|
||||
this._proxy.$setProviders(e);
|
||||
}));
|
||||
|
||||
this._register(this.credentialsService.onDidChangePassword(_ => {
|
||||
this._proxy.$onDidChangePassword();
|
||||
}));
|
||||
}
|
||||
|
||||
$getProviderIds(): Promise<string[]> {
|
||||
return Promise.resolve(this.authenticationService.getProviderIds());
|
||||
}
|
||||
|
||||
async $registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): Promise<void> {
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, label, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService, this.quickInputService, this.dialogService);
|
||||
await provider.initialize();
|
||||
this.authenticationService.registerAuthenticationProvider(id, provider);
|
||||
}
|
||||
|
||||
$unregisterAuthenticationProvider(id: string): void {
|
||||
this.authenticationService.unregisterAuthenticationProvider(id);
|
||||
}
|
||||
|
||||
$ensureProvider(id: string): Promise<void> {
|
||||
return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate);
|
||||
}
|
||||
|
||||
$sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
|
||||
this.authenticationService.sessionsUpdate(id, event);
|
||||
}
|
||||
|
||||
$getSessions(id: string): Promise<ReadonlyArray<modes.AuthenticationSession>> {
|
||||
return this.authenticationService.getSessions(id);
|
||||
}
|
||||
|
||||
$login(providerId: string, scopes: string[]): Promise<modes.AuthenticationSession> {
|
||||
return this.authenticationService.login(providerId, scopes);
|
||||
}
|
||||
|
||||
$logout(providerId: string, sessionId: string): Promise<void> {
|
||||
return this.authenticationService.logout(providerId, sessionId);
|
||||
}
|
||||
|
||||
async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise<void> {
|
||||
return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName);
|
||||
}
|
||||
|
||||
async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise<modes.AuthenticationSession | undefined> {
|
||||
const orderedScopes = scopes.sort().join(' ');
|
||||
const sessions = (await this.$getSessions(providerId)).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes);
|
||||
const label = this.authenticationService.getLabel(providerId);
|
||||
|
||||
if (sessions.length) {
|
||||
if (!this.authenticationService.supportsMultipleAccounts(providerId)) {
|
||||
const session = sessions[0];
|
||||
const allowed = await this.$getSessionsPrompt(providerId, session.account.label, label, extensionId, extensionName);
|
||||
if (allowed) {
|
||||
return session;
|
||||
} else {
|
||||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
}
|
||||
|
||||
// On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid
|
||||
const selected = await this.$selectSession(providerId, label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference);
|
||||
return sessions.find(session => session.id === selected.id);
|
||||
} else {
|
||||
if (options.createIfNone) {
|
||||
const isAllowed = await this.$loginPrompt(label, extensionName);
|
||||
if (!isAllowed) {
|
||||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
|
||||
const session = await this.authenticationService.login(providerId, scopes);
|
||||
await this.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
|
||||
return session;
|
||||
} else {
|
||||
await this.$requestNewSession(providerId, scopes, extensionId, extensionName);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise<modes.AuthenticationSession> {
|
||||
if (!potentialSessions.length) {
|
||||
throw new Error('No potential sessions found');
|
||||
}
|
||||
|
||||
if (clearSessionPreference) {
|
||||
this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL);
|
||||
} else {
|
||||
const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL);
|
||||
if (existingSessionPreference) {
|
||||
const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference);
|
||||
if (matchingSession) {
|
||||
const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.label, providerName, extensionId, extensionName);
|
||||
if (allowed) {
|
||||
return matchingSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>();
|
||||
quickPick.ignoreFocusOut = true;
|
||||
const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => {
|
||||
return {
|
||||
label: session.account.label,
|
||||
session
|
||||
};
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: nls.localize('useOtherAccount', "Sign in to another account")
|
||||
});
|
||||
|
||||
quickPick.items = items;
|
||||
quickPick.title = nls.localize(
|
||||
{
|
||||
key: 'selectAccount',
|
||||
comment: ['The placeholder {0} is the name of an extension. {1} is the name of the type of account, such as Microsoft or GitHub.']
|
||||
},
|
||||
"The extension '{0}' wants to access a {1} account",
|
||||
extensionName,
|
||||
providerName);
|
||||
quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName);
|
||||
|
||||
quickPick.onDidAccept(async _ => {
|
||||
const selected = quickPick.selectedItems[0];
|
||||
|
||||
const session = selected.session ?? await this.authenticationService.login(providerId, scopes);
|
||||
|
||||
const accountName = session.account.label;
|
||||
|
||||
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
if (!allowList.find(allowed => allowed.id === extensionId)) {
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL);
|
||||
|
||||
quickPick.dispose();
|
||||
resolve(session);
|
||||
});
|
||||
|
||||
quickPick.onDidHide(_ => {
|
||||
if (!quickPick.selectedItems[0]) {
|
||||
reject('User did not consent to account access');
|
||||
}
|
||||
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
});
|
||||
}
|
||||
|
||||
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
|
||||
const allowList = 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 remoteConnection = this.remoteAgentService.getConnection();
|
||||
const isVSO = remoteConnection !== null
|
||||
? remoteConnection.remoteAuthority.startsWith('vsonline')
|
||||
: isWeb;
|
||||
|
||||
if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) {
|
||||
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
const { choice } = await this.dialogService.show(
|
||||
Severity.Info,
|
||||
nls.localize('confirmAuthenticationAccess', "The extension '{0}' wants to access the {1} account '{2}'.", extensionName, providerName, accountName),
|
||||
[nls.localize('allow', "Allow"), nls.localize('cancel', "Cancel")],
|
||||
{
|
||||
cancelId: 1
|
||||
}
|
||||
);
|
||||
|
||||
const allow = choice === 0;
|
||||
if (allow) {
|
||||
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
return allow;
|
||||
}
|
||||
|
||||
async $loginPrompt(providerName: string, extensionName: string): Promise<boolean> {
|
||||
const { choice } = await this.dialogService.show(
|
||||
Severity.Info,
|
||||
nls.localize('confirmLogin', "The extension '{0}' wants to sign in using {1}.", extensionName, providerName),
|
||||
[nls.localize('allow', "Allow"), nls.localize('cancel', "Cancel")],
|
||||
{
|
||||
cancelId: 1
|
||||
}
|
||||
);
|
||||
|
||||
return choice === 0;
|
||||
}
|
||||
|
||||
async $setTrustedExtensionAndAccountPreference(providerId: string, accountName: string, extensionId: string, extensionName: string, sessionId: string): Promise<void> {
|
||||
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
if (!allowList.find(allowed => allowed.id === extensionId)) {
|
||||
allowList.push({ id: extensionId, name: extensionName });
|
||||
this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private getFullKey(extensionId: string): string {
|
||||
return `${this.productService.urlProtocol}${extensionId}`;
|
||||
}
|
||||
|
||||
async $getPassword(extensionId: string, key: string): Promise<string | undefined> {
|
||||
const fullKey = this.getFullKey(extensionId);
|
||||
const password = await this.credentialsService.getPassword(fullKey, key);
|
||||
const decrypted = password && await this.encryptionService.decrypt(password);
|
||||
|
||||
if (decrypted) {
|
||||
try {
|
||||
const value = JSON.parse(decrypted);
|
||||
if (value.extensionId === extensionId) {
|
||||
return value.content;
|
||||
}
|
||||
} catch (_) {
|
||||
throw new Error('Cannot get password');
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async $setPassword(extensionId: string, key: string, value: string): Promise<void> {
|
||||
const fullKey = this.getFullKey(extensionId);
|
||||
const toEncrypt = JSON.stringify({
|
||||
extensionId,
|
||||
content: value
|
||||
});
|
||||
const encrypted = await this.encryptionService.encrypt(toEncrypt);
|
||||
return this.credentialsService.setPassword(fullKey, key, encrypted);
|
||||
}
|
||||
|
||||
async $deletePassword(extensionId: string, key: string): Promise<void> {
|
||||
try {
|
||||
const fullKey = this.getFullKey(extensionId);
|
||||
await this.credentialsService.deletePassword(fullKey, key);
|
||||
} catch (_) {
|
||||
throw new Error('Cannot delete password');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
|
||||
function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] {
|
||||
if (!data?.edits) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ResourceEdit[] = [];
|
||||
for (let edit of revive<IWorkspaceEditDto>(data).edits) {
|
||||
if (edit._type === WorkspaceEditType.File) {
|
||||
result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata));
|
||||
} else if (edit._type === WorkspaceEditType.Text) {
|
||||
result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata));
|
||||
} else if (edit._type === WorkspaceEditType.Cell) {
|
||||
result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadBulkEdits)
|
||||
export class MainThreadBulkEdits implements MainThreadBulkEditsShape {
|
||||
|
||||
constructor(
|
||||
_extHostContext: IExtHostContext,
|
||||
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
|
||||
) { }
|
||||
|
||||
dispose(): void { }
|
||||
|
||||
$tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise<boolean> {
|
||||
const edits = reviveWorkspaceEditDto2(dto);
|
||||
return this._bulkEditService.apply(edits).then(() => true, _err => false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadClipboardShape } from '../common/extHost.protocol';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadClipboard)
|
||||
export class MainThreadClipboard implements MainThreadClipboardShape {
|
||||
|
||||
constructor(
|
||||
_context: any,
|
||||
@IClipboardService private readonly _clipboardService: IClipboardService,
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
// nothing
|
||||
}
|
||||
|
||||
$readText(): Promise<string> {
|
||||
return this._clipboardService.readText();
|
||||
}
|
||||
|
||||
$writeText(value: string): Promise<void> {
|
||||
return this._clipboardService.writeText(value);
|
||||
}
|
||||
}
|
||||
149
lib/vscode/src/vs/workbench/api/browser/mainThreadCodeInsets.ts
Normal file
149
lib/vscode/src/vs/workbench/api/browser/mainThreadCodeInsets.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'vs/base/common/uri';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { MainContext, MainThreadEditorInsetsShape, IExtHostContext, ExtHostEditorInsetsShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from '../common/extHostCustomers';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IActiveCodeEditor, IViewZone } from 'vs/editor/browser/editorBrowser';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
// todo@joh move these things back into something like contrib/insets
|
||||
class EditorWebviewZone implements IViewZone {
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
readonly afterLineNumber: number;
|
||||
readonly afterColumn: number;
|
||||
readonly heightInLines: number;
|
||||
|
||||
private _id?: string;
|
||||
// suppressMouseDown?: boolean | undefined;
|
||||
// heightInPx?: number | undefined;
|
||||
// minWidthInPx?: number | undefined;
|
||||
// marginDomNode?: HTMLElement | null | undefined;
|
||||
// onDomNodeTop?: ((top: number) => void) | undefined;
|
||||
// onComputedHeight?: ((height: number) => void) | undefined;
|
||||
|
||||
constructor(
|
||||
readonly editor: IActiveCodeEditor,
|
||||
readonly line: number,
|
||||
readonly height: number,
|
||||
readonly webview: WebviewElement,
|
||||
) {
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.style.zIndex = '10'; // without this, the webview is not interactive
|
||||
this.afterLineNumber = line;
|
||||
this.afterColumn = 1;
|
||||
this.heightInLines = height;
|
||||
|
||||
editor.changeViewZones(accessor => this._id = accessor.addZone(this));
|
||||
webview.mountTo(this.domNode);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.editor.changeViewZones(accessor => this._id && accessor.removeZone(this._id));
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadEditorInsets)
|
||||
export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
|
||||
|
||||
private readonly _proxy: ExtHostEditorInsetsShape;
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _insets = new Map<number, EditorWebviewZone>();
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@IWebviewService private readonly _webviewService: IWebviewService,
|
||||
) {
|
||||
this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
async $createEditorInset(handle: number, id: string, uri: UriComponents, line: number, height: number, options: modes.IWebviewOptions, extensionId: ExtensionIdentifier, extensionLocation: UriComponents): Promise<void> {
|
||||
|
||||
let editor: IActiveCodeEditor | undefined;
|
||||
id = id.substr(0, id.indexOf(',')); //todo@joh HACK
|
||||
|
||||
for (const candidate of this._editorService.listCodeEditors()) {
|
||||
if (candidate.getId() === id && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))) {
|
||||
editor = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!editor) {
|
||||
setTimeout(() => this._proxy.$onDidDispose(handle));
|
||||
return;
|
||||
}
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
const webview = this._webviewService.createWebviewElement('' + handle, {
|
||||
enableFindWidget: false,
|
||||
}, {
|
||||
allowScripts: options.enableScripts,
|
||||
localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined
|
||||
}, { id: extensionId, location: URI.revive(extensionLocation) });
|
||||
|
||||
const webviewZone = new EditorWebviewZone(editor, line, height, webview);
|
||||
|
||||
const remove = () => {
|
||||
disposables.dispose();
|
||||
this._proxy.$onDidDispose(handle);
|
||||
this._insets.delete(handle);
|
||||
};
|
||||
|
||||
disposables.add(editor.onDidChangeModel(remove));
|
||||
disposables.add(editor.onDidDispose(remove));
|
||||
disposables.add(webviewZone);
|
||||
disposables.add(webview);
|
||||
disposables.add(webview.onMessage(msg => this._proxy.$onDidReceiveMessage(handle, msg)));
|
||||
|
||||
this._insets.set(handle, webviewZone);
|
||||
}
|
||||
|
||||
$disposeEditorInset(handle: number): void {
|
||||
const inset = this.getInset(handle);
|
||||
this._insets.delete(handle);
|
||||
inset.dispose();
|
||||
|
||||
}
|
||||
|
||||
$setHtml(handle: number, value: string): void {
|
||||
const inset = this.getInset(handle);
|
||||
inset.webview.html = value;
|
||||
}
|
||||
|
||||
$setOptions(handle: number, options: modes.IWebviewOptions): void {
|
||||
const inset = this.getInset(handle);
|
||||
inset.webview.contentOptions = {
|
||||
...options,
|
||||
localResourceRoots: options.localResourceRoots?.map(components => URI.from(components)),
|
||||
};
|
||||
}
|
||||
|
||||
async $postMessage(handle: number, value: any): Promise<boolean> {
|
||||
const inset = this.getInset(handle);
|
||||
inset.webview.postMessage(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
private getInset(handle: number): EditorWebviewZone {
|
||||
const inset = this._insets.get(handle);
|
||||
if (!inset) {
|
||||
throw new Error('Unknown inset');
|
||||
}
|
||||
return inset;
|
||||
}
|
||||
}
|
||||
110
lib/vscode/src/vs/workbench/api/browser/mainThreadCommands.ts
Normal file
110
lib/vscode/src/vs/workbench/api/browser/mainThreadCommands.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICommandService, CommandsRegistry, ICommandHandlerDescription } from 'vs/platform/commands/common/commands';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ExtHostContext, MainThreadCommandsShape, ExtHostCommandsShape, MainContext, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadCommands)
|
||||
export class MainThreadCommands implements MainThreadCommandsShape {
|
||||
|
||||
private readonly _commandRegistrations = new Map<string, IDisposable>();
|
||||
private readonly _generateCommandsDocumentationRegistration: IDisposable;
|
||||
private readonly _proxy: ExtHostCommandsShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IExtensionService private readonly _extensionService: IExtensionService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostCommands);
|
||||
|
||||
this._generateCommandsDocumentationRegistration = CommandsRegistry.registerCommand('_generateCommandsDocumentation', () => this._generateCommandsDocumentation());
|
||||
}
|
||||
|
||||
dispose() {
|
||||
dispose(this._commandRegistrations.values());
|
||||
this._commandRegistrations.clear();
|
||||
|
||||
this._generateCommandsDocumentationRegistration.dispose();
|
||||
}
|
||||
|
||||
private _generateCommandsDocumentation(): Promise<void> {
|
||||
return this._proxy.$getContributedCommandHandlerDescriptions().then(result => {
|
||||
// add local commands
|
||||
const commands = CommandsRegistry.getCommands();
|
||||
for (const [id, command] of commands) {
|
||||
if (command.description) {
|
||||
result[id] = command.description;
|
||||
}
|
||||
}
|
||||
|
||||
// print all as markdown
|
||||
const all: string[] = [];
|
||||
for (let id in result) {
|
||||
all.push('`' + id + '` - ' + _generateMarkdown(result[id]));
|
||||
}
|
||||
console.log(all.join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
$registerCommand(id: string): void {
|
||||
this._commandRegistrations.set(
|
||||
id,
|
||||
CommandsRegistry.registerCommand(id, (accessor, ...args) => {
|
||||
return this._proxy.$executeContributedCommand(id, ...args).then(result => {
|
||||
return revive(result);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
$unregisterCommand(id: string): void {
|
||||
const command = this._commandRegistrations.get(id);
|
||||
if (command) {
|
||||
command.dispose();
|
||||
this._commandRegistrations.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
async $executeCommand<T>(id: string, args: any[], retry: boolean): Promise<T | undefined> {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
args[i] = revive(args[i]);
|
||||
}
|
||||
if (retry && args.length > 0 && !CommandsRegistry.getCommand(id)) {
|
||||
await this._extensionService.activateByEvent(`onCommand:${id}`);
|
||||
throw new Error('$executeCommand:retry');
|
||||
}
|
||||
return this._commandService.executeCommand<T>(id, ...args);
|
||||
}
|
||||
|
||||
$getCommands(): Promise<string[]> {
|
||||
return Promise.resolve([...CommandsRegistry.getCommands().keys()]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- command doc
|
||||
|
||||
function _generateMarkdown(description: string | ICommandHandlerDescription): string {
|
||||
if (typeof description === 'string') {
|
||||
return description;
|
||||
} else {
|
||||
const parts = [description.description];
|
||||
parts.push('\n\n');
|
||||
if (description.args) {
|
||||
for (let arg of description.args) {
|
||||
parts.push(`* _${arg.name}_ - ${arg.description || ''}\n`);
|
||||
}
|
||||
}
|
||||
if (description.returns) {
|
||||
parts.push(`* _(returns)_ - ${description.returns}`);
|
||||
}
|
||||
parts.push('\n\n');
|
||||
return parts.join('');
|
||||
}
|
||||
}
|
||||
540
lib/vscode/src/vs/workbench/api/browser/mainThreadComments.ts
Normal file
540
lib/vscode/src/vs/workbench/api/browser/mainThreadComments.ts
Normal file
@@ -0,0 +1,540 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ICommentInfo, ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
|
||||
import { CommentsPanel } from 'vs/workbench/contrib/comments/browser/commentsView';
|
||||
import { CommentProviderFeatures, ExtHostCommentsShape, ExtHostContext, IExtHostContext, MainContext, MainThreadCommentsShape, CommentThreadChanges } from '../common/extHost.protocol';
|
||||
import { COMMENTS_VIEW_ID, COMMENTS_VIEW_TITLE } from 'vs/workbench/contrib/comments/browser/commentsTreeViewer';
|
||||
import { ViewContainer, IViewContainersRegistry, Extensions as ViewExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, IViewDescriptorService } from 'vs/workbench/common/views';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
|
||||
export class MainThreadCommentThread implements modes.CommentThread {
|
||||
private _input?: modes.CommentInput;
|
||||
get input(): modes.CommentInput | undefined {
|
||||
return this._input;
|
||||
}
|
||||
|
||||
set input(value: modes.CommentInput | undefined) {
|
||||
this._input = value;
|
||||
this._onDidChangeInput.fire(value);
|
||||
}
|
||||
|
||||
private readonly _onDidChangeInput = new Emitter<modes.CommentInput | undefined>();
|
||||
get onDidChangeInput(): Event<modes.CommentInput | undefined> { return this._onDidChangeInput.event; }
|
||||
|
||||
private _label: string | undefined;
|
||||
|
||||
get label(): string | undefined {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
set label(label: string | undefined) {
|
||||
this._label = label;
|
||||
this._onDidChangeLabel.fire(this._label);
|
||||
}
|
||||
|
||||
private _contextValue: string | undefined;
|
||||
|
||||
get contextValue(): string | undefined {
|
||||
return this._contextValue;
|
||||
}
|
||||
|
||||
set contextValue(context: string | undefined) {
|
||||
this._contextValue = context;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeLabel = new Emitter<string | undefined>();
|
||||
readonly onDidChangeLabel: Event<string | undefined> = this._onDidChangeLabel.event;
|
||||
|
||||
private _comments: modes.Comment[] | undefined;
|
||||
|
||||
public get comments(): modes.Comment[] | undefined {
|
||||
return this._comments;
|
||||
}
|
||||
|
||||
public set comments(newComments: modes.Comment[] | undefined) {
|
||||
this._comments = newComments;
|
||||
this._onDidChangeComments.fire(this._comments);
|
||||
}
|
||||
|
||||
private readonly _onDidChangeComments = new Emitter<modes.Comment[] | undefined>();
|
||||
get onDidChangeComments(): Event<modes.Comment[] | undefined> { return this._onDidChangeComments.event; }
|
||||
|
||||
set range(range: IRange) {
|
||||
this._range = range;
|
||||
this._onDidChangeRange.fire(this._range);
|
||||
}
|
||||
|
||||
get range(): IRange {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeCanReply = new Emitter<boolean>();
|
||||
get onDidChangeCanReply(): Event<boolean> { return this._onDidChangeCanReply.event; }
|
||||
set canReply(state: boolean) {
|
||||
this._canReply = state;
|
||||
this._onDidChangeCanReply.fire(this._canReply);
|
||||
}
|
||||
|
||||
get canReply() {
|
||||
return this._canReply;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeRange = new Emitter<IRange>();
|
||||
public onDidChangeRange = this._onDidChangeRange.event;
|
||||
|
||||
private _collapsibleState: modes.CommentThreadCollapsibleState | undefined;
|
||||
get collapsibleState() {
|
||||
return this._collapsibleState;
|
||||
}
|
||||
|
||||
set collapsibleState(newState: modes.CommentThreadCollapsibleState | undefined) {
|
||||
this._collapsibleState = newState;
|
||||
this._onDidChangeCollasibleState.fire(this._collapsibleState);
|
||||
}
|
||||
|
||||
private readonly _onDidChangeCollasibleState = new Emitter<modes.CommentThreadCollapsibleState | undefined>();
|
||||
public onDidChangeCollasibleState = this._onDidChangeCollasibleState.event;
|
||||
|
||||
private _isDisposed: boolean;
|
||||
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public commentThreadHandle: number,
|
||||
public controllerHandle: number,
|
||||
public extensionId: string,
|
||||
public threadId: string,
|
||||
public resource: string,
|
||||
private _range: IRange,
|
||||
private _canReply: boolean
|
||||
) {
|
||||
this._isDisposed = false;
|
||||
}
|
||||
|
||||
batchUpdate(changes: CommentThreadChanges) {
|
||||
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('canReply')) { this.canReply = changes.canReply!; }
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._isDisposed = true;
|
||||
this._onDidChangeCollasibleState.dispose();
|
||||
this._onDidChangeComments.dispose();
|
||||
this._onDidChangeInput.dispose();
|
||||
this._onDidChangeLabel.dispose();
|
||||
this._onDidChangeRange.dispose();
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: 7,
|
||||
commentControlHandle: this.controllerHandle,
|
||||
commentThreadHandle: this.commentThreadHandle,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadCommentController {
|
||||
get handle(): number {
|
||||
return this._handle;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get contextValue(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get proxy(): ExtHostCommentsShape {
|
||||
return this._proxy;
|
||||
}
|
||||
|
||||
get label(): string {
|
||||
return this._label;
|
||||
}
|
||||
|
||||
private _reactions: modes.CommentReaction[] | undefined;
|
||||
|
||||
get reactions() {
|
||||
return this._reactions;
|
||||
}
|
||||
|
||||
set reactions(reactions: modes.CommentReaction[] | undefined) {
|
||||
this._reactions = reactions;
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this._features.options;
|
||||
}
|
||||
|
||||
private readonly _threads: Map<number, MainThreadCommentThread> = new Map<number, MainThreadCommentThread>();
|
||||
public activeCommentThread?: MainThreadCommentThread;
|
||||
|
||||
get features(): CommentProviderFeatures {
|
||||
return this._features;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostCommentsShape,
|
||||
private readonly _commentService: ICommentService,
|
||||
private readonly _handle: number,
|
||||
private readonly _uniqueId: string,
|
||||
private readonly _id: string,
|
||||
private readonly _label: string,
|
||||
private _features: CommentProviderFeatures
|
||||
) { }
|
||||
|
||||
updateFeatures(features: CommentProviderFeatures) {
|
||||
this._features = features;
|
||||
}
|
||||
|
||||
createCommentThread(extensionId: string,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
range: IRange,
|
||||
): modes.CommentThread {
|
||||
let thread = new MainThreadCommentThread(
|
||||
commentThreadHandle,
|
||||
this.handle,
|
||||
extensionId,
|
||||
threadId,
|
||||
URI.revive(resource).toString(),
|
||||
range,
|
||||
true
|
||||
);
|
||||
|
||||
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 {
|
||||
let thread = this.getKnownThread(commentThreadHandle);
|
||||
thread.batchUpdate(changes);
|
||||
|
||||
this._commentService.updateComments(this._uniqueId, {
|
||||
added: [],
|
||||
removed: [],
|
||||
changed: [thread]
|
||||
});
|
||||
}
|
||||
|
||||
deleteCommentThread(commentThreadHandle: number) {
|
||||
let thread = this.getKnownThread(commentThreadHandle);
|
||||
this._threads.delete(commentThreadHandle);
|
||||
|
||||
this._commentService.updateComments(this._uniqueId, {
|
||||
added: [],
|
||||
removed: [thread],
|
||||
changed: []
|
||||
});
|
||||
|
||||
thread.dispose();
|
||||
}
|
||||
|
||||
deleteCommentThreadMain(commentThreadId: string) {
|
||||
this._threads.forEach(thread => {
|
||||
if (thread.threadId === commentThreadId) {
|
||||
this._proxy.$deleteCommentThread(this._handle, thread.commentThreadHandle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateInput(input: string) {
|
||||
let thread = this.activeCommentThread;
|
||||
|
||||
if (thread && thread.input) {
|
||||
let commentInput = thread.input;
|
||||
commentInput.value = input;
|
||||
thread.input = commentInput;
|
||||
}
|
||||
}
|
||||
|
||||
private getKnownThread(commentThreadHandle: number): MainThreadCommentThread {
|
||||
const thread = this._threads.get(commentThreadHandle);
|
||||
if (!thread) {
|
||||
throw new Error('unknown thread');
|
||||
}
|
||||
return thread;
|
||||
}
|
||||
|
||||
async getDocumentComments(resource: URI, token: CancellationToken) {
|
||||
let ret: modes.CommentThread[] = [];
|
||||
for (let thread of [...this._threads.keys()]) {
|
||||
const commentThread = this._threads.get(thread)!;
|
||||
if (commentThread.resource === resource.toString()) {
|
||||
ret.push(commentThread);
|
||||
}
|
||||
}
|
||||
|
||||
let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
|
||||
|
||||
return <ICommentInfo>{
|
||||
owner: this._uniqueId,
|
||||
label: this.label,
|
||||
threads: ret,
|
||||
commentingRanges: {
|
||||
resource: resource,
|
||||
ranges: commentingRanges || []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getCommentingRanges(resource: URI, token: CancellationToken): Promise<IRange[]> {
|
||||
let commentingRanges = await this._proxy.$provideCommentingRanges(this.handle, resource, token);
|
||||
return commentingRanges || [];
|
||||
}
|
||||
|
||||
async toggleReaction(uri: URI, thread: modes.CommentThread, comment: modes.Comment, reaction: modes.CommentReaction, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$toggleReaction(this._handle, thread.commentThreadHandle, uri, comment, reaction);
|
||||
}
|
||||
|
||||
getAllComments(): MainThreadCommentThread[] {
|
||||
let ret: MainThreadCommentThread[] = [];
|
||||
for (let thread of [...this._threads.keys()]) {
|
||||
ret.push(this._threads.get(thread)!);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
createCommentThreadTemplate(resource: UriComponents, range: IRange): void {
|
||||
this._proxy.$createCommentThreadTemplate(this.handle, resource, range);
|
||||
}
|
||||
|
||||
async updateCommentThreadTemplate(threadHandle: number, range: IRange) {
|
||||
await this._proxy.$updateCommentThreadTemplate(this.handle, threadHandle, range);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: 6,
|
||||
handle: this.handle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadComments)
|
||||
export class MainThreadComments extends Disposable implements MainThreadCommentsShape {
|
||||
private readonly _proxy: ExtHostCommentsShape;
|
||||
private _documentProviders = new Map<number, IDisposable>();
|
||||
private _workspaceProviders = new Map<number, IDisposable>();
|
||||
private _handlers = new Map<number, string>();
|
||||
private _commentControllers = new Map<number, MainThreadCommentController>();
|
||||
|
||||
private _activeCommentThread?: MainThreadCommentThread;
|
||||
private readonly _activeCommentThreadDisposables = this._register(new DisposableStore());
|
||||
|
||||
private _openViewListener: IDisposable | null = null;
|
||||
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ICommentService private readonly _commentService: ICommentService,
|
||||
@IViewsService private readonly _viewsService: IViewsService,
|
||||
@IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostComments);
|
||||
|
||||
this._register(this._commentService.onDidChangeActiveCommentThread(async thread => {
|
||||
let handle = (thread as MainThreadCommentThread).controllerHandle;
|
||||
let controller = this._commentControllers.get(handle);
|
||||
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._activeCommentThreadDisposables.clear();
|
||||
this._activeCommentThread = thread as MainThreadCommentThread;
|
||||
controller.activeCommentThread = this._activeCommentThread;
|
||||
}));
|
||||
}
|
||||
|
||||
$registerCommentController(handle: number, id: string, label: string): void {
|
||||
const providerId = generateUuid();
|
||||
this._handlers.set(handle, providerId);
|
||||
|
||||
const provider = new MainThreadCommentController(this._proxy, this._commentService, handle, providerId, id, label, {});
|
||||
this._commentService.registerCommentController(providerId, provider);
|
||||
this._commentControllers.set(handle, provider);
|
||||
|
||||
const commentsPanelAlreadyConstructed = !!this._viewDescriptorService.getViewDescriptorById(COMMENTS_VIEW_ID);
|
||||
if (!commentsPanelAlreadyConstructed) {
|
||||
this.registerView(commentsPanelAlreadyConstructed);
|
||||
this.registerViewOpenedListener(commentsPanelAlreadyConstructed);
|
||||
}
|
||||
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 {
|
||||
let provider = this._commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
provider.updateFeatures(features);
|
||||
}
|
||||
|
||||
$createCommentThread(handle: number,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
range: IRange,
|
||||
extensionId: ExtensionIdentifier
|
||||
): modes.CommentThread | undefined {
|
||||
let provider = this._commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return provider.createCommentThread(extensionId.value, commentThreadHandle, threadId, resource, range);
|
||||
}
|
||||
|
||||
$updateCommentThread(handle: number,
|
||||
commentThreadHandle: number,
|
||||
threadId: string,
|
||||
resource: UriComponents,
|
||||
changes: CommentThreadChanges): void {
|
||||
let provider = this._commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return provider.updateCommentThread(commentThreadHandle, threadId, resource, changes);
|
||||
}
|
||||
|
||||
$deleteCommentThread(handle: number, commentThreadHandle: number) {
|
||||
let provider = this._commentControllers.get(handle);
|
||||
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
|
||||
return provider.deleteCommentThread(commentThreadHandle);
|
||||
}
|
||||
|
||||
private registerView(commentsViewAlreadyRegistered: boolean) {
|
||||
if (!commentsViewAlreadyRegistered) {
|
||||
const VIEW_CONTAINER: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
id: COMMENTS_VIEW_ID,
|
||||
name: COMMENTS_VIEW_TITLE,
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
|
||||
storageId: COMMENTS_VIEW_TITLE,
|
||||
hideIfEmpty: true,
|
||||
icon: Codicon.commentDiscussion.classNames,
|
||||
order: 10,
|
||||
}, ViewContainerLocation.Panel);
|
||||
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews([{
|
||||
id: COMMENTS_VIEW_ID,
|
||||
name: COMMENTS_VIEW_TITLE,
|
||||
canToggleVisibility: false,
|
||||
ctorDescriptor: new SyncDescriptor(CommentsPanel),
|
||||
canMoveView: true,
|
||||
containerIcon: Codicon.commentDiscussion.classNames,
|
||||
focusCommand: {
|
||||
id: 'workbench.action.focusCommentsPanel'
|
||||
}
|
||||
}], VIEW_CONTAINER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the comments view has never been opened, the constructor for it has not yet run so it has
|
||||
* no listeners for comment threads being set or updated. Listen for the view opening for the
|
||||
* first time and send it comments then.
|
||||
*/
|
||||
private registerViewOpenedListener(commentsPanelAlreadyConstructed: boolean) {
|
||||
if (!commentsPanelAlreadyConstructed && !this._openViewListener) {
|
||||
this._openViewListener = this._viewsService.onDidChangeViewVisibility(e => {
|
||||
if (e.id === COMMENTS_VIEW_ID && e.visible) {
|
||||
[...this._commentControllers.keys()].forEach(handle => {
|
||||
let threads = this._commentControllers.get(handle)!.getAllComments();
|
||||
|
||||
if (threads.length) {
|
||||
const providerId = this.getHandler(handle);
|
||||
this._commentService.setWorkspaceComments(providerId, threads);
|
||||
}
|
||||
});
|
||||
|
||||
if (this._openViewListener) {
|
||||
this._openViewListener.dispose();
|
||||
this._openViewListener = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getHandler(handle: number) {
|
||||
if (!this._handlers.has(handle)) {
|
||||
throw new Error('Unknown handler');
|
||||
}
|
||||
return this._handlers.get(handle)!;
|
||||
}
|
||||
|
||||
$onDidCommentThreadsChange(handle: number, event: modes.CommentThreadChangedEvent) {
|
||||
// notify comment service
|
||||
const providerId = this.getHandler(handle);
|
||||
this._commentService.updateComments(providerId, event);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this._workspaceProviders.forEach(value => dispose(value));
|
||||
this._workspaceProviders.clear();
|
||||
this._documentProviders.forEach(value => dispose(value));
|
||||
this._documentProviders.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, getScopes } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
|
||||
import { MainThreadConfigurationShape, MainContext, ExtHostContext, IExtHostContext, IConfigurationInitData } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ConfigurationTarget, IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadConfiguration)
|
||||
export class MainThreadConfiguration implements MainThreadConfigurationShape {
|
||||
|
||||
private readonly _configurationListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
) {
|
||||
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostConfiguration);
|
||||
|
||||
proxy.$initializeConfiguration(this._getConfigurationData());
|
||||
this._configurationListener = configurationService.onDidChangeConfiguration(e => {
|
||||
proxy.$acceptConfigurationChanged(this._getConfigurationData(), e.change);
|
||||
});
|
||||
}
|
||||
|
||||
private _getConfigurationData(): IConfigurationInitData {
|
||||
const configurationData: IConfigurationInitData = { ...(this.configurationService.getConfigurationData()!), configurationScopes: [] };
|
||||
// Send configurations scopes only in development mode.
|
||||
if (!this._environmentService.isBuilt || this._environmentService.isExtensionDevelopment) {
|
||||
configurationData.configurationScopes = getScopes();
|
||||
}
|
||||
return configurationData;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._configurationListener.dispose();
|
||||
}
|
||||
|
||||
$updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise<void> {
|
||||
overrides = { resource: overrides?.resource ? URI.revive(overrides.resource) : undefined, overrideIdentifier: overrides?.overrideIdentifier };
|
||||
return this.writeConfiguration(target, key, value, overrides, scopeToLanguage);
|
||||
}
|
||||
|
||||
$removeConfigurationOption(target: ConfigurationTarget | null, key: string, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise<void> {
|
||||
overrides = { resource: overrides?.resource ? URI.revive(overrides.resource) : undefined, overrideIdentifier: overrides?.overrideIdentifier };
|
||||
return this.writeConfiguration(target, key, undefined, overrides, scopeToLanguage);
|
||||
}
|
||||
|
||||
private writeConfiguration(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides, scopeToLanguage: boolean | undefined): Promise<void> {
|
||||
target = target !== null && target !== undefined ? target : this.deriveConfigurationTarget(key, overrides);
|
||||
const configurationValue = this.configurationService.inspect(key, overrides);
|
||||
switch (target) {
|
||||
case ConfigurationTarget.MEMORY:
|
||||
return this._updateValue(key, value, target, configurationValue?.memory?.override, overrides, scopeToLanguage);
|
||||
case ConfigurationTarget.WORKSPACE_FOLDER:
|
||||
return this._updateValue(key, value, target, configurationValue?.workspaceFolder?.override, overrides, scopeToLanguage);
|
||||
case ConfigurationTarget.WORKSPACE:
|
||||
return this._updateValue(key, value, target, configurationValue?.workspace?.override, overrides, scopeToLanguage);
|
||||
case ConfigurationTarget.USER_REMOTE:
|
||||
return this._updateValue(key, value, target, configurationValue?.userRemote?.override, overrides, scopeToLanguage);
|
||||
default:
|
||||
return this._updateValue(key, value, target, configurationValue?.userLocal?.override, overrides, scopeToLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateValue(key: string, value: any, configurationTarget: ConfigurationTarget, overriddenValue: any | undefined, overrides: IConfigurationOverrides, scopeToLanguage: boolean | undefined): Promise<void> {
|
||||
overrides = scopeToLanguage === true ? overrides
|
||||
: scopeToLanguage === false ? { resource: overrides.resource }
|
||||
: overrides.overrideIdentifier && overriddenValue !== undefined ? overrides
|
||||
: { resource: overrides.resource };
|
||||
return this.configurationService.updateValue(key, value, overrides, configurationTarget, true);
|
||||
}
|
||||
|
||||
private deriveConfigurationTarget(key: string, overrides: IConfigurationOverrides): ConfigurationTarget {
|
||||
if (overrides.resource && this._workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
|
||||
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
|
||||
if (configurationProperties[key] && (configurationProperties[key].scope === ConfigurationScope.RESOURCE || configurationProperties[key].scope === ConfigurationScope.LANGUAGE_OVERRIDABLE)) {
|
||||
return ConfigurationTarget.WORKSPACE_FOLDER;
|
||||
}
|
||||
}
|
||||
return ConfigurationTarget.WORKSPACE;
|
||||
}
|
||||
}
|
||||
52
lib/vscode/src/vs/workbench/api/browser/mainThreadConsole.ts
Normal file
52
lib/vscode/src/vs/workbench/api/browser/mainThreadConsole.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadConsoleShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IRemoteConsoleLog, log } from 'vs/base/common/console';
|
||||
import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil';
|
||||
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadConsole)
|
||||
export class MainThreadConsole implements MainThreadConsoleShape {
|
||||
|
||||
private readonly _isExtensionDevHost: boolean;
|
||||
private readonly _isExtensionDevTestFromCli: boolean;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService,
|
||||
) {
|
||||
const devOpts = parseExtensionDevOptions(this._environmentService);
|
||||
this._isExtensionDevHost = devOpts.isExtensionDevHost;
|
||||
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
|
||||
$logExtensionHostMessage(entry: IRemoteConsoleLog): void {
|
||||
// Send to local console unless we run tests from cli
|
||||
if (!this._isExtensionDevTestFromCli) {
|
||||
log(entry, 'Extension Host');
|
||||
}
|
||||
|
||||
// Log on main side if running tests from cli
|
||||
if (this._isExtensionDevTestFromCli) {
|
||||
logRemoteEntry(this._logService, entry);
|
||||
}
|
||||
|
||||
// Broadcast to other windows if we are in development mode
|
||||
else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) {
|
||||
this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources';
|
||||
import { multibyteAwareBtoa } from 'vs/base/browser/dom';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels';
|
||||
import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor';
|
||||
import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor';
|
||||
import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput';
|
||||
import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory';
|
||||
import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor';
|
||||
import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel';
|
||||
import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput';
|
||||
import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService';
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
|
||||
const enum CustomEditorModelType {
|
||||
Custom,
|
||||
Text,
|
||||
}
|
||||
|
||||
export class MainThreadCustomEditors extends Disposable implements extHostProtocol.MainThreadCustomEditorsShape {
|
||||
|
||||
private readonly _proxyCustomEditors: extHostProtocol.ExtHostCustomEditorsShape;
|
||||
|
||||
private readonly _editorProviders = new Map<string, IDisposable>();
|
||||
|
||||
constructor(
|
||||
context: extHostProtocol.IExtHostContext,
|
||||
private readonly mainThreadWebview: MainThreadWebviews,
|
||||
private readonly mainThreadWebviewPanels: MainThreadWebviewPanels,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IWorkingCopyService workingCopyService: IWorkingCopyService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
|
||||
@ICustomEditorService private readonly _customEditorService: ICustomEditorService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IBackupFileService private readonly _backupService: IBackupFileService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._proxyCustomEditors = context.getProxy(extHostProtocol.ExtHostContext.ExtHostCustomEditors);
|
||||
|
||||
this._register(workingCopyFileService.registerWorkingCopyProvider((editorResource) => {
|
||||
const matchedWorkingCopies: IWorkingCopy[] = [];
|
||||
|
||||
for (const workingCopy of workingCopyService.workingCopies) {
|
||||
if (workingCopy instanceof MainThreadCustomEditorModel) {
|
||||
if (isEqualOrParent(editorResource, workingCopy.editorResource)) {
|
||||
matchedWorkingCopies.push(workingCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matchedWorkingCopies;
|
||||
}));
|
||||
|
||||
// This reviver's only job is to activate custom editor extensions.
|
||||
this._register(_webviewWorkbenchService.registerResolver({
|
||||
canResolve: (webview: WebviewInput) => {
|
||||
if (webview instanceof CustomEditorInput) {
|
||||
extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
resolveWebview: () => { throw new Error('not implemented'); }
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
for (const disposable of this._editorProviders.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
|
||||
this._editorProviders.clear();
|
||||
}
|
||||
|
||||
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
|
||||
this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true);
|
||||
}
|
||||
|
||||
public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void {
|
||||
this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument);
|
||||
}
|
||||
|
||||
private registerEditorProvider(
|
||||
modelType: CustomEditorModelType,
|
||||
extension: WebviewExtensionDescription,
|
||||
viewType: string,
|
||||
options: modes.IWebviewPanelOptions,
|
||||
capabilities: extHostProtocol.CustomTextEditorCapabilities,
|
||||
supportsMultipleEditorsPerDocument: boolean,
|
||||
): void {
|
||||
if (this._editorProviders.has(viewType)) {
|
||||
throw new Error(`Provider for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, {
|
||||
supportsMultipleEditorsPerDocument
|
||||
}));
|
||||
|
||||
disposables.add(this._webviewWorkbenchService.registerResolver({
|
||||
canResolve: (webviewInput) => {
|
||||
return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType;
|
||||
},
|
||||
resolveWebview: async (webviewInput: CustomEditorInput, cancellation: CancellationToken) => {
|
||||
const handle = webviewInput.id;
|
||||
const resource = webviewInput.resource;
|
||||
|
||||
this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput);
|
||||
webviewInput.webview.options = options;
|
||||
webviewInput.webview.extension = extension;
|
||||
|
||||
let modelRef: IReference<ICustomEditorModel>;
|
||||
try {
|
||||
modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cancellation.isCancellationRequested) {
|
||||
modelRef.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
webviewInput.webview.onDidDispose(() => {
|
||||
// If the model is still dirty, make sure we have time to save it
|
||||
if (modelRef.object.isDirty()) {
|
||||
const sub = modelRef.object.onDidChangeDirty(() => {
|
||||
if (!modelRef.object.isDirty()) {
|
||||
sub.dispose();
|
||||
modelRef.dispose();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modelRef.dispose();
|
||||
});
|
||||
|
||||
if (capabilities.supportsMove) {
|
||||
webviewInput.onMove(async (newResource: URI) => {
|
||||
const oldModel = modelRef;
|
||||
modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None);
|
||||
this._proxyCustomEditors.$onMoveCustomEditor(handle, newResource, viewType);
|
||||
oldModel.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this._proxyCustomEditors.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType);
|
||||
modelRef.dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._editorProviders.set(viewType, disposables);
|
||||
}
|
||||
|
||||
public $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);
|
||||
}
|
||||
|
||||
private async getOrCreateCustomEditorModel(
|
||||
modelType: CustomEditorModelType,
|
||||
resource: URI,
|
||||
viewType: string,
|
||||
options: { backupId?: string },
|
||||
cancellation: CancellationToken,
|
||||
): Promise<IReference<ICustomEditorModel>> {
|
||||
const existingModel = this._customEditorService.models.tryRetain(resource, viewType);
|
||||
if (existingModel) {
|
||||
return existingModel;
|
||||
}
|
||||
|
||||
switch (modelType) {
|
||||
case CustomEditorModelType.Text:
|
||||
{
|
||||
const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource);
|
||||
return this._customEditorService.models.add(resource, viewType, model);
|
||||
}
|
||||
case CustomEditorModelType.Custom:
|
||||
{
|
||||
const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => {
|
||||
return Array.from(this.mainThreadWebviewPanels.webviewInputs)
|
||||
.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];
|
||||
}, cancellation, this._backupService);
|
||||
return this._customEditorService.models.add(resource, viewType, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise<void> {
|
||||
const model = await this.getCustomEditorModel(resourceComponents, viewType);
|
||||
model.pushEdit(editId, label);
|
||||
}
|
||||
|
||||
public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise<void> {
|
||||
const model = await this.getCustomEditorModel(resourceComponents, viewType);
|
||||
model.changeContent();
|
||||
}
|
||||
|
||||
private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) {
|
||||
const resource = URI.revive(resourceComponents);
|
||||
const model = await this._customEditorService.models.get(resource, viewType);
|
||||
if (!model || !(model instanceof MainThreadCustomEditorModel)) {
|
||||
throw new Error('Could not find model for webview editor');
|
||||
}
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
namespace HotExitState {
|
||||
export const enum Type {
|
||||
Allowed,
|
||||
NotAllowed,
|
||||
Pending,
|
||||
}
|
||||
|
||||
export const Allowed = Object.freeze({ type: Type.Allowed } as const);
|
||||
export const NotAllowed = Object.freeze({ type: Type.NotAllowed } as const);
|
||||
|
||||
export class Pending {
|
||||
readonly type = Type.Pending;
|
||||
|
||||
constructor(
|
||||
public readonly operation: CancelablePromise<string>,
|
||||
) { }
|
||||
}
|
||||
|
||||
export type State = typeof Allowed | typeof NotAllowed | Pending;
|
||||
}
|
||||
|
||||
|
||||
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {
|
||||
|
||||
private _fromBackup: boolean = false;
|
||||
private _hotExitState: HotExitState.State = HotExitState.Allowed;
|
||||
private _backupId: string | undefined;
|
||||
|
||||
private _currentEditIndex: number = -1;
|
||||
private _savePoint: number = -1;
|
||||
private readonly _edits: Array<number> = [];
|
||||
private _isDirtyFromContentChange = false;
|
||||
|
||||
private _ongoingSave?: CancelablePromise<void>;
|
||||
|
||||
public static async create(
|
||||
instantiationService: IInstantiationService,
|
||||
proxy: extHostProtocol.ExtHostCustomEditorsShape,
|
||||
viewType: string,
|
||||
resource: URI,
|
||||
options: { backupId?: string },
|
||||
getEditors: () => CustomEditorInput[],
|
||||
cancellation: CancellationToken,
|
||||
_backupFileService: IBackupFileService,
|
||||
) {
|
||||
const { editable } = await proxy.$createCustomDocument(resource, viewType, options.backupId, cancellation);
|
||||
return instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, !!options.backupId, editable, getEditors);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: extHostProtocol.ExtHostCustomEditorsShape,
|
||||
private readonly _viewType: string,
|
||||
private readonly _editorResource: URI,
|
||||
fromBackup: boolean,
|
||||
private readonly _editable: boolean,
|
||||
private readonly _getEditors: () => CustomEditorInput[],
|
||||
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IUndoRedoService private readonly _undoService: IUndoRedoService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IWorkingCopyService workingCopyService: IWorkingCopyService,
|
||||
@IPathService private readonly _pathService: IPathService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._fromBackup = fromBackup;
|
||||
|
||||
if (_editable) {
|
||||
this._register(workingCopyService.registerWorkingCopy(this));
|
||||
}
|
||||
}
|
||||
|
||||
get editorResource() {
|
||||
return this._editorResource;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._editable) {
|
||||
this._undoService.removeElements(this._editorResource);
|
||||
}
|
||||
this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
//#region IWorkingCopy
|
||||
|
||||
public get resource() {
|
||||
// Make sure each custom editor has a unique resource for backup and edits
|
||||
return MainThreadCustomEditorModel.toWorkingCopyResource(this._viewType, this._editorResource);
|
||||
}
|
||||
|
||||
private static toWorkingCopyResource(viewType: string, resource: URI) {
|
||||
const authority = viewType.replace(/[^a-z0-9\-_]/gi, '-');
|
||||
const path = `/${multibyteAwareBtoa(resource.with({ query: null, fragment: null }).toString(true))}`;
|
||||
return URI.from({
|
||||
scheme: Schemas.vscodeCustomEditor,
|
||||
authority: authority,
|
||||
path: path,
|
||||
query: JSON.stringify(resource.toJSON()),
|
||||
});
|
||||
}
|
||||
|
||||
public get name() {
|
||||
return basename(this._labelService.getUriLabel(this._editorResource));
|
||||
}
|
||||
|
||||
public get capabilities(): WorkingCopyCapabilities {
|
||||
return this.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None;
|
||||
}
|
||||
|
||||
public isDirty(): boolean {
|
||||
if (this._isDirtyFromContentChange) {
|
||||
return true;
|
||||
}
|
||||
if (this._edits.length > 0) {
|
||||
return this._savePoint !== this._currentEditIndex;
|
||||
}
|
||||
return this._fromBackup;
|
||||
}
|
||||
|
||||
private isUntitled() {
|
||||
return this._editorResource.scheme === Schemas.untitled;
|
||||
}
|
||||
|
||||
private readonly _onDidChangeDirty: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDirty: Event<void> = this._onDidChangeDirty.event;
|
||||
|
||||
private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
|
||||
|
||||
//#endregion
|
||||
|
||||
public isReadonly() {
|
||||
return !this._editable;
|
||||
}
|
||||
|
||||
public get viewType() {
|
||||
return this._viewType;
|
||||
}
|
||||
|
||||
public get backupId() {
|
||||
return this._backupId;
|
||||
}
|
||||
|
||||
public pushEdit(editId: number, label: string | undefined) {
|
||||
if (!this._editable) {
|
||||
throw new Error('Document is not editable');
|
||||
}
|
||||
|
||||
this.change(() => {
|
||||
this.spliceEdits(editId);
|
||||
this._currentEditIndex = this._edits.length - 1;
|
||||
});
|
||||
|
||||
this._undoService.pushElement({
|
||||
type: UndoRedoElementType.Resource,
|
||||
resource: this._editorResource,
|
||||
label: label ?? localize('defaultEditLabel', "Edit"),
|
||||
undo: () => this.undo(),
|
||||
redo: () => this.redo(),
|
||||
});
|
||||
}
|
||||
|
||||
public changeContent() {
|
||||
this.change(() => {
|
||||
this._isDirtyFromContentChange = true;
|
||||
});
|
||||
}
|
||||
|
||||
private 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._editorResource, this.viewType, undoneEdit, this.isDirty());
|
||||
}
|
||||
|
||||
private 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._editorResource, this.viewType, redoneEdit, this.isDirty());
|
||||
}
|
||||
|
||||
private spliceEdits(editToInsert?: number) {
|
||||
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._editorResource, this._viewType, removedEdits);
|
||||
}
|
||||
}
|
||||
|
||||
private change(makeEdit: () => void): void {
|
||||
const wasDirty = this.isDirty();
|
||||
makeEdit();
|
||||
this._onDidChangeContent.fire();
|
||||
|
||||
if (this.isDirty() !== wasDirty) {
|
||||
this._onDidChangeDirty.fire();
|
||||
}
|
||||
}
|
||||
|
||||
public async revert(_options?: IRevertOptions) {
|
||||
if (!this._editable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange && !this._fromBackup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);
|
||||
this.change(() => {
|
||||
this._isDirtyFromContentChange = false;
|
||||
this._fromBackup = false;
|
||||
this._currentEditIndex = this._savePoint;
|
||||
this.spliceEdits();
|
||||
});
|
||||
}
|
||||
|
||||
public async save(options?: ISaveOptions): Promise<boolean> {
|
||||
return !!await this.saveCustomEditor(options);
|
||||
}
|
||||
|
||||
public async saveCustomEditor(options?: ISaveOptions): Promise<URI | undefined> {
|
||||
if (!this._editable) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.isUntitled()) {
|
||||
const targetUri = await this.suggestUntitledSavePath(options);
|
||||
if (!targetUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.saveCustomEditorAs(this._editorResource, targetUri, options);
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
|
||||
this._ongoingSave?.cancel();
|
||||
this._ongoingSave = savePromise;
|
||||
|
||||
try {
|
||||
await savePromise;
|
||||
|
||||
if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save
|
||||
this.change(() => {
|
||||
this._isDirtyFromContentChange = false;
|
||||
this._savePoint = this._currentEditIndex;
|
||||
this._fromBackup = false;
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (this._ongoingSave === savePromise) { // Make sure we are still doing the same save
|
||||
this._ongoingSave = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return this._editorResource;
|
||||
}
|
||||
|
||||
private suggestUntitledSavePath(options: ISaveOptions | undefined): Promise<URI | undefined> {
|
||||
if (!this.isUntitled()) {
|
||||
throw new Error('Resource is not untitled');
|
||||
}
|
||||
|
||||
const remoteAuthority = this._environmentService.remoteAuthority;
|
||||
const localResource = toLocalResource(this._editorResource, remoteAuthority, this._pathService.defaultUriScheme);
|
||||
|
||||
return this._fileDialogService.pickFileToSave(localResource, options?.availableFileSystems);
|
||||
}
|
||||
|
||||
public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {
|
||||
if (this._editable) {
|
||||
// TODO: handle cancellation
|
||||
await createCancelablePromise(token => this._proxy.$onSaveAs(this._editorResource, this.viewType, targetResource, token));
|
||||
this.change(() => {
|
||||
this._savePoint = this._currentEditIndex;
|
||||
});
|
||||
return true;
|
||||
} else {
|
||||
// Since the editor is readonly, just copy the file over
|
||||
await this._fileService.copy(resource, targetResource, false /* overwrite */);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public async backup(token: CancellationToken): Promise<IWorkingCopyBackup> {
|
||||
const editors = this._getEditors();
|
||||
if (!editors.length) {
|
||||
throw new Error('No editors found for resource, cannot back up');
|
||||
}
|
||||
const primaryEditor = editors[0];
|
||||
|
||||
const backupData: IWorkingCopyBackup<CustomDocumentBackupData> = {
|
||||
meta: {
|
||||
viewType: this.viewType,
|
||||
editorResource: this._editorResource,
|
||||
backupId: '',
|
||||
extension: primaryEditor.extension ? {
|
||||
id: primaryEditor.extension.id.value,
|
||||
location: primaryEditor.extension.location,
|
||||
} : undefined,
|
||||
webview: {
|
||||
id: primaryEditor.id,
|
||||
options: primaryEditor.webview.options,
|
||||
state: primaryEditor.webview.state,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!this._editable) {
|
||||
return backupData;
|
||||
}
|
||||
|
||||
if (this._hotExitState.type === HotExitState.Type.Pending) {
|
||||
this._hotExitState.operation.cancel();
|
||||
}
|
||||
|
||||
const pendingState = new HotExitState.Pending(
|
||||
createCancelablePromise(token =>
|
||||
this._proxy.$backup(this._editorResource.toJSON(), this.viewType, token)));
|
||||
this._hotExitState = pendingState;
|
||||
|
||||
try {
|
||||
const backupId = await pendingState.operation;
|
||||
// Make sure state has not changed in the meantime
|
||||
if (this._hotExitState === pendingState) {
|
||||
this._hotExitState = HotExitState.Allowed;
|
||||
backupData.meta!.backupId = backupId;
|
||||
this._backupId = backupId;
|
||||
}
|
||||
} catch (e) {
|
||||
if (isPromiseCanceledError(e)) {
|
||||
// This is expected
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Otherwise it could be a real error. Make sure state has not changed in the meantime.
|
||||
if (this._hotExitState === pendingState) {
|
||||
this._hotExitState = HotExitState.NotAllowed;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._hotExitState === HotExitState.Allowed) {
|
||||
return backupData;
|
||||
}
|
||||
|
||||
throw new Error('Cannot back up in this state');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI as uri, UriComponents } from 'vs/base/common/uri';
|
||||
import { IDebugService, IConfig, IDebugConfigurationProvider, IBreakpoint, IFunctionBreakpoint, IBreakpointData, IDebugAdapter, IDebugAdapterDescriptorFactory, IDebugSession, IDebugAdapterFactory, IDataBreakpoint, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import {
|
||||
ExtHostContext, ExtHostDebugServiceShape, MainThreadDebugServiceShape, DebugSessionUUID, MainContext,
|
||||
IExtHostContext, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, ISourceBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto, IDataBreakpointDto, IStartDebuggingOptions, IDebugConfiguration
|
||||
} from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import severity from 'vs/base/common/severity';
|
||||
import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter';
|
||||
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { convertToVSCPaths, convertToDAPaths } from 'vs/workbench/contrib/debug/common/debugUtils';
|
||||
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDebugService)
|
||||
export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory {
|
||||
|
||||
private readonly _proxy: ExtHostDebugServiceShape;
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _breakpointEventsActive: boolean | undefined;
|
||||
private readonly _debugAdapters: Map<number, ExtensionHostDebugAdapter>;
|
||||
private _debugAdaptersHandleCounter = 1;
|
||||
private readonly _debugConfigurationProviders: Map<number, IDebugConfigurationProvider>;
|
||||
private readonly _debugAdapterDescriptorFactories: Map<number, IDebugAdapterDescriptorFactory>;
|
||||
private readonly _sessions: Set<DebugSessionUUID>;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IDebugService private readonly debugService: IDebugService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDebugService);
|
||||
this._toDispose.add(debugService.onDidNewSession(session => {
|
||||
this._proxy.$acceptDebugSessionStarted(this.getSessionDto(session));
|
||||
this._toDispose.add(session.onDidChangeName(name => {
|
||||
this._proxy.$acceptDebugSessionNameChanged(this.getSessionDto(session), name);
|
||||
}));
|
||||
}));
|
||||
// Need to start listening early to new session events because a custom event can come while a session is initialising
|
||||
this._toDispose.add(debugService.onWillNewSession(session => {
|
||||
this._toDispose.add(session.onDidCustomEvent(event => this._proxy.$acceptDebugSessionCustomEvent(this.getSessionDto(session), event)));
|
||||
}));
|
||||
this._toDispose.add(debugService.onDidEndSession(session => {
|
||||
this._proxy.$acceptDebugSessionTerminated(this.getSessionDto(session));
|
||||
this._sessions.delete(session.getId());
|
||||
}));
|
||||
this._toDispose.add(debugService.getViewModel().onDidFocusSession(session => {
|
||||
this._proxy.$acceptDebugSessionActiveChanged(this.getSessionDto(session));
|
||||
}));
|
||||
|
||||
this._debugAdapters = new Map();
|
||||
this._debugConfigurationProviders = new Map();
|
||||
this._debugAdapterDescriptorFactories = new Map();
|
||||
this._sessions = new Set();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
|
||||
// interface IDebugAdapterProvider
|
||||
|
||||
createDebugAdapter(session: IDebugSession): IDebugAdapter {
|
||||
const handle = this._debugAdaptersHandleCounter++;
|
||||
const da = new ExtensionHostDebugAdapter(this, handle, this._proxy, session);
|
||||
this._debugAdapters.set(handle, da);
|
||||
return da;
|
||||
}
|
||||
|
||||
substituteVariables(folder: IWorkspaceFolder | undefined, config: IConfig): Promise<IConfig> {
|
||||
return Promise.resolve(this._proxy.$substituteVariables(folder ? folder.uri : undefined, config));
|
||||
}
|
||||
|
||||
runInTerminal(args: DebugProtocol.RunInTerminalRequestArguments): Promise<number | undefined> {
|
||||
return this._proxy.$runInTerminal(args);
|
||||
}
|
||||
|
||||
// RPC methods (MainThreadDebugServiceShape)
|
||||
|
||||
public $registerDebugTypes(debugTypes: string[]) {
|
||||
this._toDispose.add(this.debugService.getConfigurationManager().registerDebugAdapterFactory(debugTypes, this));
|
||||
}
|
||||
|
||||
public $startBreakpointEvents(): void {
|
||||
|
||||
if (!this._breakpointEventsActive) {
|
||||
this._breakpointEventsActive = true;
|
||||
|
||||
// set up a handler to send more
|
||||
this._toDispose.add(this.debugService.getModel().onDidChangeBreakpoints(e => {
|
||||
// Ignore session only breakpoint events since they should only reflect in the UI
|
||||
if (e && !e.sessionOnly) {
|
||||
const delta: IBreakpointsDeltaDto = {};
|
||||
if (e.added) {
|
||||
delta.added = this.convertToDto(e.added);
|
||||
}
|
||||
if (e.removed) {
|
||||
delta.removed = e.removed.map(x => x.getId());
|
||||
}
|
||||
if (e.changed) {
|
||||
delta.changed = this.convertToDto(e.changed);
|
||||
}
|
||||
|
||||
if (delta.added || delta.removed || delta.changed) {
|
||||
this._proxy.$acceptBreakpointsDelta(delta);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// send all breakpoints
|
||||
const bps = this.debugService.getModel().getBreakpoints();
|
||||
const fbps = this.debugService.getModel().getFunctionBreakpoints();
|
||||
const dbps = this.debugService.getModel().getDataBreakpoints();
|
||||
if (bps.length > 0 || fbps.length > 0) {
|
||||
this._proxy.$acceptBreakpointsDelta({
|
||||
added: this.convertToDto(bps).concat(this.convertToDto(fbps)).concat(this.convertToDto(dbps))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public $registerBreakpoints(DTOs: Array<ISourceMultiBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto>): Promise<void> {
|
||||
|
||||
for (let dto of DTOs) {
|
||||
if (dto.type === 'sourceMulti') {
|
||||
const rawbps = dto.lines.map(l =>
|
||||
<IBreakpointData>{
|
||||
id: l.id,
|
||||
enabled: l.enabled,
|
||||
lineNumber: l.line + 1,
|
||||
column: l.character > 0 ? l.character + 1 : undefined, // a column value of 0 results in an omitted column attribute; see #46784
|
||||
condition: l.condition,
|
||||
hitCondition: l.hitCondition,
|
||||
logMessage: l.logMessage
|
||||
}
|
||||
);
|
||||
this.debugService.addBreakpoints(uri.revive(dto.uri), rawbps);
|
||||
} else if (dto.type === 'function') {
|
||||
this.debugService.addFunctionBreakpoint(dto.functionName, dto.id);
|
||||
} else if (dto.type === 'data') {
|
||||
this.debugService.addDataBreakpoint(dto.label, dto.dataId, dto.canPersist, dto.accessTypes);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public $unregisterBreakpoints(breakpointIds: string[], functionBreakpointIds: string[], dataBreakpointIds: string[]): Promise<void> {
|
||||
breakpointIds.forEach(id => this.debugService.removeBreakpoints(id));
|
||||
functionBreakpointIds.forEach(id => this.debugService.removeFunctionBreakpoints(id));
|
||||
dataBreakpointIds.forEach(id => this.debugService.removeDataBreakpoints(id));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public $registerDebugConfigurationProvider(debugType: string, providerTriggerKind: DebugConfigurationProviderTriggerKind, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise<void> {
|
||||
|
||||
const provider = <IDebugConfigurationProvider>{
|
||||
type: debugType,
|
||||
triggerKind: providerTriggerKind
|
||||
};
|
||||
if (hasProvide) {
|
||||
provider.provideDebugConfigurations = (folder, token) => {
|
||||
return this._proxy.$provideDebugConfigurations(handle, folder, token);
|
||||
};
|
||||
}
|
||||
if (hasResolve) {
|
||||
provider.resolveDebugConfiguration = (folder, config, token) => {
|
||||
return this._proxy.$resolveDebugConfiguration(handle, folder, config, token);
|
||||
};
|
||||
}
|
||||
if (hasResolve2) {
|
||||
provider.resolveDebugConfigurationWithSubstitutedVariables = (folder, config, token) => {
|
||||
return this._proxy.$resolveDebugConfigurationWithSubstitutedVariables(handle, folder, config, token);
|
||||
};
|
||||
}
|
||||
if (hasProvideDebugAdapter) {
|
||||
console.info('DebugConfigurationProvider.debugAdapterExecutable is deprecated and will be removed soon; please use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead.');
|
||||
provider.debugAdapterExecutable = (folder) => {
|
||||
return this._proxy.$legacyDebugAdapterExecutable(handle, folder);
|
||||
};
|
||||
}
|
||||
this._debugConfigurationProviders.set(handle, provider);
|
||||
this._toDispose.add(this.debugService.getConfigurationManager().registerDebugConfigurationProvider(provider));
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public $unregisterDebugConfigurationProvider(handle: number): void {
|
||||
const provider = this._debugConfigurationProviders.get(handle);
|
||||
if (provider) {
|
||||
this._debugConfigurationProviders.delete(handle);
|
||||
this.debugService.getConfigurationManager().unregisterDebugConfigurationProvider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
public $registerDebugAdapterDescriptorFactory(debugType: string, handle: number): Promise<void> {
|
||||
|
||||
const provider = <IDebugAdapterDescriptorFactory>{
|
||||
type: debugType,
|
||||
createDebugAdapterDescriptor: session => {
|
||||
return Promise.resolve(this._proxy.$provideDebugAdapter(handle, this.getSessionDto(session)));
|
||||
}
|
||||
};
|
||||
this._debugAdapterDescriptorFactories.set(handle, provider);
|
||||
this._toDispose.add(this.debugService.getConfigurationManager().registerDebugAdapterDescriptorFactory(provider));
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public $unregisterDebugAdapterDescriptorFactory(handle: number): void {
|
||||
const provider = this._debugAdapterDescriptorFactories.get(handle);
|
||||
if (provider) {
|
||||
this._debugAdapterDescriptorFactories.delete(handle);
|
||||
this.debugService.getConfigurationManager().unregisterDebugAdapterDescriptorFactory(provider);
|
||||
}
|
||||
}
|
||||
|
||||
private getSession(sessionId: DebugSessionUUID | undefined): IDebugSession | undefined {
|
||||
if (sessionId) {
|
||||
return this.debugService.getModel().getSession(sessionId, true);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, options: IStartDebuggingOptions): Promise<boolean> {
|
||||
const folderUri = folder ? uri.revive(folder) : undefined;
|
||||
const launch = this.debugService.getConfigurationManager().getLaunch(folderUri);
|
||||
const parentSession = this.getSession(options.parentSessionID);
|
||||
const debugOptions: IDebugSessionOptions = {
|
||||
noDebug: options.noDebug,
|
||||
parentSession,
|
||||
repl: options.repl,
|
||||
compact: options.compact,
|
||||
compoundRoot: parentSession?.compoundRoot
|
||||
};
|
||||
return this.debugService.startDebugging(launch, nameOrConfig, debugOptions).then(success => {
|
||||
return success;
|
||||
}, err => {
|
||||
return Promise.reject(new Error(err && err.message ? err.message : 'cannot start debugging'));
|
||||
});
|
||||
}
|
||||
|
||||
public $setDebugSessionName(sessionId: DebugSessionUUID, name: string): void {
|
||||
const session = this.debugService.getModel().getSession(sessionId);
|
||||
if (session) {
|
||||
session.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
public $customDebugAdapterRequest(sessionId: DebugSessionUUID, request: string, args: any): Promise<any> {
|
||||
const session = this.debugService.getModel().getSession(sessionId, true);
|
||||
if (session) {
|
||||
return session.customRequest(request, args).then(response => {
|
||||
if (response && response.success) {
|
||||
return response.body;
|
||||
} else {
|
||||
return Promise.reject(new Error(response ? response.message : 'custom request failed'));
|
||||
}
|
||||
});
|
||||
}
|
||||
return Promise.reject(new Error('debug session not found'));
|
||||
}
|
||||
|
||||
public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise<DebugProtocol.Breakpoint | undefined> {
|
||||
const session = this.debugService.getModel().getSession(sessionId, true);
|
||||
if (session) {
|
||||
return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId));
|
||||
}
|
||||
return Promise.reject(new Error('debug session not found'));
|
||||
}
|
||||
|
||||
public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise<void> {
|
||||
if (sessionId) {
|
||||
const session = this.debugService.getModel().getSession(sessionId, true);
|
||||
if (session) {
|
||||
return this.debugService.stopSession(session);
|
||||
}
|
||||
} else { // stop all
|
||||
return this.debugService.stopSession(undefined);
|
||||
}
|
||||
return Promise.reject(new Error('debug session not found'));
|
||||
}
|
||||
|
||||
public $appendDebugConsole(value: string): void {
|
||||
// Use warning as severity to get the orange color for messages coming from the debug extension
|
||||
const session = this.debugService.getViewModel().focusedSession;
|
||||
if (session) {
|
||||
session.appendToRepl(value, severity.Warning);
|
||||
}
|
||||
}
|
||||
|
||||
public $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage) {
|
||||
this.getDebugAdapter(handle).acceptMessage(convertToVSCPaths(message, false));
|
||||
}
|
||||
|
||||
public $acceptDAError(handle: number, name: string, message: string, stack: string) {
|
||||
this.getDebugAdapter(handle).fireError(handle, new Error(`${name}: ${message}\n${stack}`));
|
||||
}
|
||||
|
||||
public $acceptDAExit(handle: number, code: number, signal: string) {
|
||||
this.getDebugAdapter(handle).fireExit(handle, code, signal);
|
||||
}
|
||||
|
||||
private getDebugAdapter(handle: number): ExtensionHostDebugAdapter {
|
||||
const adapter = this._debugAdapters.get(handle);
|
||||
if (!adapter) {
|
||||
throw new Error('Invalid debug adapter');
|
||||
}
|
||||
return adapter;
|
||||
}
|
||||
|
||||
// dto helpers
|
||||
|
||||
public $sessionCached(sessionID: string) {
|
||||
// remember that the EH has cached the session and we do not have to send it again
|
||||
this._sessions.add(sessionID);
|
||||
}
|
||||
|
||||
|
||||
getSessionDto(session: undefined): undefined;
|
||||
getSessionDto(session: IDebugSession): IDebugSessionDto;
|
||||
getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined;
|
||||
getSessionDto(session: IDebugSession | undefined): IDebugSessionDto | undefined {
|
||||
if (session) {
|
||||
const sessionID = <DebugSessionUUID>session.getId();
|
||||
if (this._sessions.has(sessionID)) {
|
||||
return sessionID;
|
||||
} else {
|
||||
// this._sessions.add(sessionID); // #69534: see $sessionCached above
|
||||
return {
|
||||
id: sessionID,
|
||||
type: session.configuration.type,
|
||||
name: session.configuration.name,
|
||||
folderUri: session.root ? session.root.uri : undefined,
|
||||
configuration: session.configuration
|
||||
};
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private convertToDto(bps: (ReadonlyArray<IBreakpoint | IFunctionBreakpoint | IDataBreakpoint>)): Array<ISourceBreakpointDto | IFunctionBreakpointDto | IDataBreakpointDto> {
|
||||
return bps.map(bp => {
|
||||
if ('name' in bp) {
|
||||
const fbp = <IFunctionBreakpoint>bp;
|
||||
return <IFunctionBreakpointDto>{
|
||||
type: 'function',
|
||||
id: fbp.getId(),
|
||||
enabled: fbp.enabled,
|
||||
condition: fbp.condition,
|
||||
hitCondition: fbp.hitCondition,
|
||||
logMessage: fbp.logMessage,
|
||||
functionName: fbp.name
|
||||
};
|
||||
} else if ('dataId' in bp) {
|
||||
const dbp = <IDataBreakpoint>bp;
|
||||
return <IDataBreakpointDto>{
|
||||
type: 'data',
|
||||
id: dbp.getId(),
|
||||
dataId: dbp.dataId,
|
||||
enabled: dbp.enabled,
|
||||
condition: dbp.condition,
|
||||
hitCondition: dbp.hitCondition,
|
||||
logMessage: dbp.logMessage,
|
||||
label: dbp.description,
|
||||
canPersist: dbp.canPersist
|
||||
};
|
||||
} else {
|
||||
const sbp = <IBreakpoint>bp;
|
||||
return <ISourceBreakpointDto>{
|
||||
type: 'source',
|
||||
id: sbp.getId(),
|
||||
enabled: sbp.enabled,
|
||||
condition: sbp.condition,
|
||||
hitCondition: sbp.hitCondition,
|
||||
logMessage: sbp.logMessage,
|
||||
uri: sbp.uri,
|
||||
line: sbp.lineNumber > 0 ? sbp.lineNumber - 1 : 0,
|
||||
character: (typeof sbp.column === 'number' && sbp.column > 0) ? sbp.column - 1 : 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DebugAdapter that communicates via extension protocol with another debug adapter.
|
||||
*/
|
||||
class ExtensionHostDebugAdapter extends AbstractDebugAdapter {
|
||||
|
||||
constructor(private readonly _ds: MainThreadDebugService, private _handle: number, private _proxy: ExtHostDebugServiceShape, private _session: IDebugSession) {
|
||||
super();
|
||||
}
|
||||
|
||||
fireError(handle: number, err: Error) {
|
||||
this._onError.fire(err);
|
||||
}
|
||||
|
||||
fireExit(handle: number, code: number, signal: string) {
|
||||
this._onExit.fire(code);
|
||||
}
|
||||
|
||||
startSession(): Promise<void> {
|
||||
return Promise.resolve(this._proxy.$startDASession(this._handle, this._ds.getSessionDto(this._session)));
|
||||
}
|
||||
|
||||
sendMessage(message: DebugProtocol.ProtocolMessage): void {
|
||||
this._proxy.$sendDAMessage(this._handle, convertToDAPaths(message, true));
|
||||
}
|
||||
|
||||
async stopSession(): Promise<void> {
|
||||
await this.cancelPendingRequests();
|
||||
return Promise.resolve(this._proxy.$stopDASession(this._handle));
|
||||
}
|
||||
}
|
||||
123
lib/vscode/src/vs/workbench/api/browser/mainThreadDecorations.ts
Normal file
123
lib/vscode/src/vs/workbench/api/browser/mainThreadDecorations.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ExtHostContext, MainContext, IExtHostContext, MainThreadDecorationsShape, ExtHostDecorationsShape, DecorationData, DecorationRequest } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDecorationsService, IDecorationData } from 'vs/workbench/services/decorations/browser/decorations';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
class DecorationRequestsQueue {
|
||||
|
||||
private _idPool = 0;
|
||||
private _requests = new Map<number, DecorationRequest>();
|
||||
private _resolver = new Map<number, (data: DecorationData) => any>();
|
||||
|
||||
private _timer: any;
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostDecorationsShape,
|
||||
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 });
|
||||
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 (let [id, resolve] of resolver) {
|
||||
resolve(data[id]);
|
||||
}
|
||||
});
|
||||
|
||||
// reset
|
||||
this._requests = new Map();
|
||||
this._resolver = new Map();
|
||||
this._timer = undefined;
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDecorations)
|
||||
export class MainThreadDecorations implements MainThreadDecorationsShape {
|
||||
|
||||
private readonly _provider = new Map<number, [Emitter<URI[]>, IDisposable]>();
|
||||
private readonly _proxy: ExtHostDecorationsShape;
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@IDecorationsService private readonly _decorationsService: IDecorationsService
|
||||
) {
|
||||
this._proxy = context.getProxy(ExtHostContext.ExtHostDecorations);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._provider.forEach(value => dispose(value));
|
||||
this._provider.clear();
|
||||
}
|
||||
|
||||
$registerDecorationProvider(handle: number, label: string): void {
|
||||
const emitter = new Emitter<URI[]>();
|
||||
const queue = new DecorationRequestsQueue(this._proxy, handle);
|
||||
const registration = this._decorationsService.registerDecorationsProvider({
|
||||
label,
|
||||
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 <IDecorationData>{
|
||||
weight: 10,
|
||||
bubble: bubble ?? false,
|
||||
color: themeColor?.id,
|
||||
tooltip,
|
||||
letter
|
||||
};
|
||||
}
|
||||
});
|
||||
this._provider.set(handle, [emitter, registration]);
|
||||
}
|
||||
|
||||
$onDidChange(handle: number, resources: UriComponents[]): void {
|
||||
const provider = this._provider.get(handle);
|
||||
if (provider) {
|
||||
const [emitter] = provider;
|
||||
emitter.fire(resources && resources.map(r => URI.revive(r)));
|
||||
}
|
||||
}
|
||||
|
||||
$unregisterDecorationProvider(handle: number): void {
|
||||
const provider = this._provider.get(handle);
|
||||
if (provider) {
|
||||
dispose(provider);
|
||||
this._provider.delete(handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMarkerService, IMarkerData } from 'vs/platform/markers/common/markers';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { MainThreadDiagnosticsShape, MainContext, IExtHostContext, ExtHostDiagnosticsShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDiagnostics)
|
||||
export class MainThreadDiagnostics implements MainThreadDiagnosticsShape {
|
||||
|
||||
private readonly _activeOwners = new Set<string>();
|
||||
|
||||
private readonly _proxy: ExtHostDiagnosticsShape;
|
||||
private readonly _markerListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IMarkerService private readonly _markerService: IMarkerService,
|
||||
@IUriIdentityService private readonly _uriIdentService: IUriIdentityService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDiagnostics);
|
||||
|
||||
this._markerListener = this._markerService.onMarkerChanged(this._forwardMarkers, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._markerListener.dispose();
|
||||
this._activeOwners.forEach(owner => this._markerService.changeAll(owner, []));
|
||||
this._activeOwners.clear();
|
||||
}
|
||||
|
||||
private _forwardMarkers(resources: readonly URI[]): void {
|
||||
const data: [UriComponents, IMarkerData[]][] = [];
|
||||
for (const resource of resources) {
|
||||
data.push([
|
||||
resource,
|
||||
this._markerService.read({ resource }).filter(marker => !this._activeOwners.has(marker.owner))
|
||||
]);
|
||||
}
|
||||
this._proxy.$acceptMarkersChange(data);
|
||||
}
|
||||
|
||||
$changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void {
|
||||
for (let entry of entries) {
|
||||
let [uri, markers] = entry;
|
||||
if (markers) {
|
||||
for (const marker of markers) {
|
||||
if (marker.relatedInformation) {
|
||||
for (const relatedInformation of marker.relatedInformation) {
|
||||
relatedInformation.resource = URI.revive(relatedInformation.resource);
|
||||
}
|
||||
}
|
||||
if (marker.code && typeof marker.code !== 'string') {
|
||||
marker.code.target = URI.revive(marker.code.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._markerService.changeOne(owner, this._uriIdentService.asCanonicalUri(URI.revive(uri)), markers);
|
||||
}
|
||||
this._activeOwners.add(owner);
|
||||
}
|
||||
|
||||
$clear(owner: string): void {
|
||||
this._markerService.changeAll(owner, []);
|
||||
this._activeOwners.delete(owner);
|
||||
}
|
||||
}
|
||||
62
lib/vscode/src/vs/workbench/api/browser/mainThreadDialogs.ts
Normal file
62
lib/vscode/src/vs/workbench/api/browser/mainThreadDialogs.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { MainThreadDiaglogsShape, MainContext, IExtHostContext, MainThreadDialogOpenOptions, MainThreadDialogSaveOptions } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { forEach } from 'vs/base/common/collections';
|
||||
import { IFileDialogService, IOpenDialogOptions, ISaveDialogOptions } from 'vs/platform/dialogs/common/dialogs';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDialogs)
|
||||
export class MainThreadDialogs implements MainThreadDiaglogsShape {
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
|
||||
$showOpenDialog(options?: MainThreadDialogOpenOptions): Promise<URI[] | undefined> {
|
||||
return Promise.resolve(this._fileDialogService.showOpenDialog(MainThreadDialogs._convertOpenOptions(options)));
|
||||
}
|
||||
|
||||
$showSaveDialog(options?: MainThreadDialogSaveOptions): Promise<URI | undefined> {
|
||||
return Promise.resolve(this._fileDialogService.showSaveDialog(MainThreadDialogs._convertSaveOptions(options)));
|
||||
}
|
||||
|
||||
private static _convertOpenOptions(options?: MainThreadDialogOpenOptions): IOpenDialogOptions {
|
||||
const result: IOpenDialogOptions = {
|
||||
openLabel: options?.openLabel || undefined,
|
||||
canSelectFiles: options?.canSelectFiles || (!options?.canSelectFiles && !options?.canSelectFolders),
|
||||
canSelectFolders: options?.canSelectFolders,
|
||||
canSelectMany: options?.canSelectMany,
|
||||
defaultUri: options?.defaultUri ? URI.revive(options.defaultUri) : undefined,
|
||||
title: options?.title || undefined
|
||||
};
|
||||
if (options?.filters) {
|
||||
result.filters = [];
|
||||
forEach(options.filters, entry => result.filters!.push({ name: entry.key, extensions: entry.value }));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _convertSaveOptions(options?: MainThreadDialogSaveOptions): ISaveDialogOptions {
|
||||
const result: ISaveDialogOptions = {
|
||||
defaultUri: options?.defaultUri ? URI.revive(options.defaultUri) : undefined,
|
||||
saveLabel: options?.saveLabel || undefined,
|
||||
title: options?.title || undefined
|
||||
};
|
||||
if (options?.filters) {
|
||||
result.filters = [];
|
||||
forEach(options.filters, entry => result.filters!.push({ name: entry.key, extensions: entry.value }));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostDocumentContentProvidersShape, IExtHostContext, MainContext, MainThreadDocumentContentProvidersShape } from '../common/extHost.protocol';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDocumentContentProviders)
|
||||
export class MainThreadDocumentContentProviders implements MainThreadDocumentContentProvidersShape {
|
||||
|
||||
private readonly _resourceContentProvider = new Map<number, IDisposable>();
|
||||
private readonly _pendingUpdate = new Map<string, CancellationTokenSource>();
|
||||
private readonly _proxy: ExtHostDocumentContentProvidersShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentContentProviders);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._resourceContentProvider.values());
|
||||
dispose(this._pendingUpdate.values());
|
||||
}
|
||||
|
||||
$registerTextContentProvider(handle: number, scheme: string): void {
|
||||
const registration = this._textModelResolverService.registerTextModelContentProvider(scheme, {
|
||||
provideTextContent: (uri: URI): Promise<ITextModel | null> => {
|
||||
return this._proxy.$provideTextDocumentContent(handle, uri).then(value => {
|
||||
if (typeof value === 'string') {
|
||||
const firstLineText = value.substr(0, 1 + value.search(/\r?\n/));
|
||||
const languageSelection = this._modeService.createByFilepathOrFirstLine(uri, firstLineText);
|
||||
return this._modelService.createModel(value, languageSelection, uri);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
this._resourceContentProvider.set(handle, registration);
|
||||
}
|
||||
|
||||
$unregisterTextContentProvider(handle: number): void {
|
||||
const registration = this._resourceContentProvider.get(handle);
|
||||
if (registration) {
|
||||
registration.dispose();
|
||||
this._resourceContentProvider.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
$onVirtualDocumentChange(uri: UriComponents, value: string): void {
|
||||
const model = this._modelService.getModel(URI.revive(uri));
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
// cancel and dispose an existing update
|
||||
const pending = this._pendingUpdate.get(model.id);
|
||||
if (pending) {
|
||||
pending.cancel();
|
||||
}
|
||||
|
||||
// create and keep update token
|
||||
const myToken = new CancellationTokenSource();
|
||||
this._pendingUpdate.set(model.id, myToken);
|
||||
|
||||
this._editorWorkerService.computeMoreMinimalEdits(model.uri, [{ text: value, range: model.getFullModelRange() }]).then(edits => {
|
||||
// remove token
|
||||
this._pendingUpdate.delete(model.id);
|
||||
|
||||
if (myToken.token.isCancellationRequested) {
|
||||
// ignore this
|
||||
return;
|
||||
}
|
||||
if (edits && edits.length > 0) {
|
||||
// use the evil-edit as these models show in readonly-editor only
|
||||
model.applyEdits(edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
|
||||
}
|
||||
}).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
302
lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts
Normal file
302
lib/vscode/src/vs/workbench/api/browser/mainThreadDocuments.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import { IReference, dispose, Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { IFileService, FileOperation } from 'vs/platform/files/common/files';
|
||||
import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors';
|
||||
import { ExtHostContext, ExtHostDocumentsShape, IExtHostContext, MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ITextEditorModel } from 'vs/workbench/common/editor';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { toLocalResource, extUri, IExtUri } from 'vs/base/common/resources';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
|
||||
export class BoundModelReferenceCollection {
|
||||
|
||||
private _data = new Array<{ uri: URI, length: number, dispose(): void }>();
|
||||
private _length = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _extUri: IExtUri,
|
||||
private readonly _maxAge: number = 1000 * 60 * 3,
|
||||
private readonly _maxLength: number = 1024 * 1024 * 80,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._data = dispose(this._data);
|
||||
}
|
||||
|
||||
remove(uri: URI): void {
|
||||
for (const entry of [...this._data] /* copy array because dispose will modify it */) {
|
||||
if (this._extUri.isEqualOrParent(entry.uri, uri)) {
|
||||
entry.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(uri: URI, ref: IReference<ITextEditorModel>): void {
|
||||
const length = ref.object.textEditorModel.getValueLength();
|
||||
let handle: any;
|
||||
let entry: { uri: URI, length: number, dispose(): void };
|
||||
const dispose = () => {
|
||||
const idx = this._data.indexOf(entry);
|
||||
if (idx >= 0) {
|
||||
this._length -= length;
|
||||
ref.dispose();
|
||||
clearTimeout(handle);
|
||||
this._data.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
handle = setTimeout(dispose, this._maxAge);
|
||||
entry = { uri, length, dispose };
|
||||
|
||||
this._data.push(entry);
|
||||
this._length += length;
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
private _cleanup(): void {
|
||||
while (this._length > this._maxLength) {
|
||||
this._data[0].dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ModelTracker extends Disposable {
|
||||
|
||||
private _knownVersionId: number;
|
||||
|
||||
constructor(
|
||||
private readonly _model: ITextModel,
|
||||
private readonly _onIsCaughtUpWithContentChanges: Emitter<URI>,
|
||||
private readonly _proxy: ExtHostDocumentsShape,
|
||||
private readonly _textFileService: ITextFileService,
|
||||
) {
|
||||
super();
|
||||
this._knownVersionId = this._model.getVersionId();
|
||||
this._register(this._model.onDidChangeContent((e) => {
|
||||
this._knownVersionId = e.versionId;
|
||||
this._proxy.$acceptModelChanged(this._model.uri, e, this._textFileService.isDirty(this._model.uri));
|
||||
if (this.isCaughtUpWithContentChanges()) {
|
||||
this._onIsCaughtUpWithContentChanges.fire(this._model.uri);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public isCaughtUpWithContentChanges(): boolean {
|
||||
return (this._model.getVersionId() === this._knownVersionId);
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadDocuments extends Disposable implements MainThreadDocumentsShape {
|
||||
|
||||
private _onIsCaughtUpWithContentChanges = this._register(new Emitter<URI>());
|
||||
public readonly onIsCaughtUpWithContentChanges = this._onIsCaughtUpWithContentChanges.event;
|
||||
|
||||
private readonly _modelService: IModelService;
|
||||
private readonly _textModelResolverService: ITextModelService;
|
||||
private readonly _textFileService: ITextFileService;
|
||||
private readonly _fileService: IFileService;
|
||||
private readonly _environmentService: IWorkbenchEnvironmentService;
|
||||
private readonly _uriIdentityService: IUriIdentityService;
|
||||
|
||||
private _modelTrackers: { [modelUrl: string]: ModelTracker; };
|
||||
private readonly _proxy: ExtHostDocumentsShape;
|
||||
private readonly _modelIsSynced = new Set<string>();
|
||||
private readonly _modelReferenceCollection: BoundModelReferenceCollection;
|
||||
|
||||
constructor(
|
||||
documentsAndEditors: MainThreadDocumentsAndEditors,
|
||||
extHostContext: IExtHostContext,
|
||||
@IModelService modelService: IModelService,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ITextModelService textModelResolverService: ITextModelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
|
||||
@IPathService private readonly _pathService: IPathService
|
||||
) {
|
||||
super();
|
||||
this._modelService = modelService;
|
||||
this._textModelResolverService = textModelResolverService;
|
||||
this._textFileService = textFileService;
|
||||
this._fileService = fileService;
|
||||
this._environmentService = environmentService;
|
||||
this._uriIdentityService = uriIdentityService;
|
||||
|
||||
this._modelReferenceCollection = this._register(new BoundModelReferenceCollection(uriIdentityService.extUri));
|
||||
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments);
|
||||
|
||||
this._register(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this)));
|
||||
this._register(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this)));
|
||||
this._register(modelService.onModelModeChanged(this._onModelModeChanged, this));
|
||||
|
||||
this._register(textFileService.files.onDidSave(e => {
|
||||
if (this._shouldHandleFileEvent(e.model.resource)) {
|
||||
this._proxy.$acceptModelSaved(e.model.resource);
|
||||
}
|
||||
}));
|
||||
this._register(textFileService.files.onDidChangeDirty(m => {
|
||||
if (this._shouldHandleFileEvent(m.resource)) {
|
||||
this._proxy.$acceptDirtyStateChanged(m.resource, m.isDirty());
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
if (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE) {
|
||||
for (const { source } of e.files) {
|
||||
if (source) {
|
||||
this._modelReferenceCollection.remove(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._modelTrackers = Object.create(null);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
Object.keys(this._modelTrackers).forEach((modelUrl) => {
|
||||
this._modelTrackers[modelUrl].dispose();
|
||||
});
|
||||
this._modelTrackers = Object.create(null);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public isCaughtUpWithContentChanges(resource: URI): boolean {
|
||||
const modelUrl = resource.toString();
|
||||
if (this._modelTrackers[modelUrl]) {
|
||||
return this._modelTrackers[modelUrl].isCaughtUpWithContentChanges();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _shouldHandleFileEvent(resource: URI): boolean {
|
||||
const model = this._modelService.getModel(resource);
|
||||
return !!model && shouldSynchronizeModel(model);
|
||||
}
|
||||
|
||||
private _onModelAdded(model: ITextModel): void {
|
||||
// Same filter as in mainThreadEditorsTracker
|
||||
if (!shouldSynchronizeModel(model)) {
|
||||
// don't synchronize too large models
|
||||
return;
|
||||
}
|
||||
const modelUrl = model.uri;
|
||||
this._modelIsSynced.add(modelUrl.toString());
|
||||
this._modelTrackers[modelUrl.toString()] = new ModelTracker(model, this._onIsCaughtUpWithContentChanges, this._proxy, this._textFileService);
|
||||
}
|
||||
|
||||
private _onModelModeChanged(event: { model: ITextModel; oldModeId: string; }): void {
|
||||
let { model, oldModeId } = event;
|
||||
const modelUrl = model.uri;
|
||||
if (!this._modelIsSynced.has(modelUrl.toString())) {
|
||||
return;
|
||||
}
|
||||
this._proxy.$acceptModelModeChanged(model.uri, oldModeId, model.getLanguageIdentifier().language);
|
||||
}
|
||||
|
||||
private _onModelRemoved(modelUrl: URI): void {
|
||||
const strModelUrl = modelUrl.toString();
|
||||
if (!this._modelIsSynced.has(strModelUrl)) {
|
||||
return;
|
||||
}
|
||||
this._modelIsSynced.delete(strModelUrl);
|
||||
this._modelTrackers[strModelUrl].dispose();
|
||||
delete this._modelTrackers[strModelUrl];
|
||||
}
|
||||
|
||||
// --- from extension host process
|
||||
|
||||
$trySaveDocument(uri: UriComponents): Promise<boolean> {
|
||||
return this._textFileService.save(URI.revive(uri)).then(target => !!target);
|
||||
}
|
||||
|
||||
$tryOpenDocument(uriData: UriComponents): Promise<URI> {
|
||||
const inputUri = URI.revive(uriData);
|
||||
if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) {
|
||||
return Promise.reject(new Error(`Invalid uri. Scheme and authority or path must be set.`));
|
||||
}
|
||||
|
||||
const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri);
|
||||
|
||||
let promise: Promise<URI>;
|
||||
switch (canonicalUri.scheme) {
|
||||
case Schemas.untitled:
|
||||
promise = this._handleUntitledScheme(canonicalUri);
|
||||
break;
|
||||
case Schemas.file:
|
||||
default:
|
||||
promise = this._handleAsResourceInput(canonicalUri);
|
||||
break;
|
||||
}
|
||||
|
||||
return promise.then(documentUri => {
|
||||
if (!documentUri) {
|
||||
return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}`));
|
||||
} else if (!extUri.isEqual(documentUri, canonicalUri)) {
|
||||
return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`));
|
||||
} else if (!this._modelIsSynced.has(canonicalUri.toString())) {
|
||||
return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`));
|
||||
} else {
|
||||
return canonicalUri;
|
||||
}
|
||||
}, err => {
|
||||
return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`));
|
||||
});
|
||||
}
|
||||
|
||||
$tryCreateDocument(options?: { language?: string, content?: string }): Promise<URI> {
|
||||
return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined);
|
||||
}
|
||||
|
||||
private _handleAsResourceInput(uri: URI): Promise<URI> {
|
||||
return this._textModelResolverService.createModelReference(uri).then(ref => {
|
||||
this._modelReferenceCollection.add(uri, ref);
|
||||
return ref.object.textEditorModel.uri;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleUntitledScheme(uri: URI): Promise<URI> {
|
||||
const asLocalUri = toLocalResource(uri, this._environmentService.remoteAuthority, this._pathService.defaultUriScheme);
|
||||
return this._fileService.resolve(asLocalUri).then(stats => {
|
||||
// don't create a new file ontop of an existing file
|
||||
return Promise.reject(new Error('file already exists'));
|
||||
}, err => {
|
||||
return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
private _doCreateUntitled(associatedResource?: URI, mode?: string, initialValue?: string): Promise<URI> {
|
||||
return this._textFileService.untitled.resolve({
|
||||
associatedResource,
|
||||
mode,
|
||||
initialValue
|
||||
}).then(model => {
|
||||
const resource = model.resource;
|
||||
|
||||
if (!this._modelIsSynced.has(resource.toString())) {
|
||||
throw new Error(`expected URI ${resource.toString()} to have come to LIFE`);
|
||||
}
|
||||
|
||||
this._proxy.$acceptDirtyStateChanged(resource, true); // mark as dirty
|
||||
|
||||
return resource;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor, isCodeEditor, isDiffEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocuments';
|
||||
import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor';
|
||||
import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors';
|
||||
import { ExtHostContext, ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta, IExtHostContext, IModelAddedData, ITextEditorAddData, MainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { EditorViewColumn, editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor';
|
||||
import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor';
|
||||
import { IEditorPane } from 'vs/workbench/common/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IPathService } from 'vs/workbench/services/path/common/pathService';
|
||||
|
||||
namespace delta {
|
||||
|
||||
export function ofSets<T>(before: Set<T>, after: Set<T>): { removed: T[], added: T[] } {
|
||||
const removed: T[] = [];
|
||||
const added: T[] = [];
|
||||
for (let element of before) {
|
||||
if (!after.has(element)) {
|
||||
removed.push(element);
|
||||
}
|
||||
}
|
||||
for (let element of after) {
|
||||
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[] = [];
|
||||
for (let [index, value] of before) {
|
||||
if (!after.has(index)) {
|
||||
removed.push(value);
|
||||
}
|
||||
}
|
||||
for (let [index, value] of after) {
|
||||
if (!before.has(index)) {
|
||||
added.push(value);
|
||||
}
|
||||
}
|
||||
return { removed, added };
|
||||
}
|
||||
}
|
||||
|
||||
class TextEditorSnapshot {
|
||||
|
||||
readonly id: string;
|
||||
|
||||
constructor(
|
||||
readonly editor: IActiveCodeEditor,
|
||||
) {
|
||||
this.id = `${editor.getId()},${editor.getModel().id}`;
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentAndEditorStateDelta {
|
||||
|
||||
readonly isEmpty: boolean;
|
||||
|
||||
constructor(
|
||||
readonly removedDocuments: ITextModel[],
|
||||
readonly addedDocuments: ITextModel[],
|
||||
readonly removedEditors: TextEditorSnapshot[],
|
||||
readonly addedEditors: TextEditorSnapshot[],
|
||||
readonly oldActiveEditor: string | null | undefined,
|
||||
readonly newActiveEditor: string | null | undefined,
|
||||
) {
|
||||
this.isEmpty = this.removedDocuments.length === 0
|
||||
&& this.addedDocuments.length === 0
|
||||
&& this.removedEditors.length === 0
|
||||
&& this.addedEditors.length === 0
|
||||
&& oldActiveEditor === newActiveEditor;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
let ret = 'DocumentAndEditorStateDelta\n';
|
||||
ret += `\tRemoved Documents: [${this.removedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;
|
||||
ret += `\tAdded Documents: [${this.addedDocuments.map(d => d.uri.toString(true)).join(', ')}]\n`;
|
||||
ret += `\tRemoved Editors: [${this.removedEditors.map(e => e.id).join(', ')}]\n`;
|
||||
ret += `\tAdded Editors: [${this.addedEditors.map(e => e.id).join(', ')}]\n`;
|
||||
ret += `\tNew Active Editor: ${this.newActiveEditor}\n`;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentAndEditorState {
|
||||
|
||||
static compute(before: DocumentAndEditorState | undefined, after: DocumentAndEditorState): DocumentAndEditorStateDelta {
|
||||
if (!before) {
|
||||
return new DocumentAndEditorStateDelta(
|
||||
[], [...after.documents.values()],
|
||||
[], [...after.textEditors.values()],
|
||||
undefined, after.activeEditor
|
||||
);
|
||||
}
|
||||
const documentDelta = delta.ofSets(before.documents, after.documents);
|
||||
const editorDelta = delta.ofMaps(before.textEditors, after.textEditors);
|
||||
const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
|
||||
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
|
||||
|
||||
return new DocumentAndEditorStateDelta(
|
||||
documentDelta.removed, documentDelta.added,
|
||||
editorDelta.removed, editorDelta.added,
|
||||
oldActiveEditor, newActiveEditor
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly documents: Set<ITextModel>,
|
||||
readonly textEditors: Map<string, TextEditorSnapshot>,
|
||||
readonly activeEditor: string | null | undefined,
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
const enum ActiveEditorOrder {
|
||||
Editor, Panel
|
||||
}
|
||||
|
||||
class MainThreadDocumentAndEditorStateComputer {
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _toDisposeOnEditorRemove = new Map<string, IDisposable>();
|
||||
private _currentState?: DocumentAndEditorState;
|
||||
private _activeEditorOrder: ActiveEditorOrder = ActiveEditorOrder.Editor;
|
||||
|
||||
constructor(
|
||||
private readonly _onDidChangeState: (delta: DocumentAndEditorStateDelta) => void,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IPanelService private readonly _panelService: IPanelService
|
||||
) {
|
||||
this._modelService.onModelAdded(this._updateStateOnModelAdd, this, this._toDispose);
|
||||
this._modelService.onModelRemoved(_ => this._updateState(), this, this._toDispose);
|
||||
this._editorService.onDidActiveEditorChange(_ => this._updateState(), this, this._toDispose);
|
||||
|
||||
this._codeEditorService.onCodeEditorAdd(this._onDidAddEditor, this, this._toDispose);
|
||||
this._codeEditorService.onCodeEditorRemove(this._onDidRemoveEditor, this, this._toDispose);
|
||||
this._codeEditorService.listCodeEditors().forEach(this._onDidAddEditor, this);
|
||||
|
||||
this._panelService.onDidPanelOpen(_ => this._activeEditorOrder = ActiveEditorOrder.Panel, undefined, this._toDispose);
|
||||
this._panelService.onDidPanelClose(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);
|
||||
this._editorService.onDidVisibleEditorsChange(_ => this._activeEditorOrder = ActiveEditorOrder.Editor, undefined, this._toDispose);
|
||||
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
|
||||
private _onDidAddEditor(e: ICodeEditor): void {
|
||||
this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable(
|
||||
e.onDidChangeModel(() => this._updateState()),
|
||||
e.onDidFocusEditorText(() => this._updateState()),
|
||||
e.onDidFocusEditorWidget(() => this._updateState(e))
|
||||
));
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
private _onDidRemoveEditor(e: ICodeEditor): void {
|
||||
const sub = this._toDisposeOnEditorRemove.get(e.getId());
|
||||
if (sub) {
|
||||
this._toDisposeOnEditorRemove.delete(e.getId());
|
||||
sub.dispose();
|
||||
this._updateState();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateStateOnModelAdd(model: ITextModel): void {
|
||||
if (!shouldSynchronizeModel(model)) {
|
||||
// ignore
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._currentState) {
|
||||
// too early
|
||||
this._updateState();
|
||||
return;
|
||||
}
|
||||
|
||||
// small (fast) delta
|
||||
this._currentState = new DocumentAndEditorState(
|
||||
this._currentState.documents.add(model),
|
||||
this._currentState.textEditors,
|
||||
this._currentState.activeEditor
|
||||
);
|
||||
|
||||
this._onDidChangeState(new DocumentAndEditorStateDelta(
|
||||
[], [model],
|
||||
[], [],
|
||||
undefined, undefined
|
||||
));
|
||||
}
|
||||
|
||||
private _updateState(widgetFocusCandidate?: ICodeEditor): void {
|
||||
|
||||
// models: ignore too large models
|
||||
const models = new Set<ITextModel>();
|
||||
for (const model of this._modelService.getModels()) {
|
||||
if (shouldSynchronizeModel(model)) {
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
// editor: only take those that have a not too large model
|
||||
const editors = new Map<string, TextEditorSnapshot>();
|
||||
let activeEditor: string | null = null; // Strict null work. This doesn't like being undefined!
|
||||
|
||||
for (const editor of this._codeEditorService.listCodeEditors()) {
|
||||
if (editor.isSimpleWidget) {
|
||||
continue;
|
||||
}
|
||||
const model = editor.getModel();
|
||||
if (editor.hasModel() && model && shouldSynchronizeModel(model)
|
||||
&& !model.isDisposed() // model disposed
|
||||
&& Boolean(this._modelService.getModel(model.uri)) // model disposing, the flag didn't flip yet but the model service already removed it
|
||||
) {
|
||||
const apiEditor = new TextEditorSnapshot(editor);
|
||||
editors.set(apiEditor.id, apiEditor);
|
||||
if (editor.hasTextFocus() || (widgetFocusCandidate === editor && editor.hasWidgetFocus())) {
|
||||
// text focus has priority, widget focus is tricky because multiple
|
||||
// editors might claim widget focus at the same time. therefore we use a
|
||||
// candidate (which is the editor that has raised an widget focus event)
|
||||
// in addition to the widget focus check
|
||||
activeEditor = apiEditor.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// active editor: if none of the previous editors had focus we try
|
||||
// to match output panels or the active workbench editor with
|
||||
// one of editor we have just computed
|
||||
if (!activeEditor) {
|
||||
let candidate: IEditor | undefined;
|
||||
if (this._activeEditorOrder === ActiveEditorOrder.Editor) {
|
||||
candidate = this._getActiveEditorFromEditorPart() || this._getActiveEditorFromPanel();
|
||||
} else {
|
||||
candidate = this._getActiveEditorFromPanel() || this._getActiveEditorFromEditorPart();
|
||||
}
|
||||
|
||||
if (candidate) {
|
||||
for (const snapshot of editors.values()) {
|
||||
if (candidate === snapshot.editor) {
|
||||
activeEditor = snapshot.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compute new state and compare against old
|
||||
const newState = new DocumentAndEditorState(models, editors, activeEditor);
|
||||
const delta = DocumentAndEditorState.compute(this._currentState, newState);
|
||||
if (!delta.isEmpty) {
|
||||
this._currentState = newState;
|
||||
this._onDidChangeState(delta);
|
||||
}
|
||||
}
|
||||
|
||||
private _getActiveEditorFromPanel(): IEditor | undefined {
|
||||
const panel = this._panelService.getActivePanel();
|
||||
if (panel instanceof BaseTextEditor && isCodeEditor(panel.getControl())) {
|
||||
return panel.getControl();
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _getActiveEditorFromEditorPart(): IEditor | undefined {
|
||||
let activeTextEditorControl = this._editorService.activeTextEditorControl;
|
||||
if (isDiffEditor(activeTextEditorControl)) {
|
||||
activeTextEditorControl = activeTextEditorControl.getModifiedEditor();
|
||||
}
|
||||
return activeTextEditorControl;
|
||||
}
|
||||
}
|
||||
|
||||
@extHostCustomer
|
||||
export class MainThreadDocumentsAndEditors {
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _proxy: ExtHostDocumentsAndEditorsShape;
|
||||
private readonly _mainThreadDocuments: MainThreadDocuments;
|
||||
private readonly _textEditors = new Map<string, MainThreadTextEditor>();
|
||||
|
||||
private readonly _onTextEditorAdd = new Emitter<MainThreadTextEditor[]>();
|
||||
private readonly _onTextEditorRemove = new Emitter<string[]>();
|
||||
private readonly _onDocumentAdd = new Emitter<ITextModel[]>();
|
||||
private readonly _onDocumentRemove = new Emitter<URI[]>();
|
||||
|
||||
readonly onTextEditorAdd: Event<MainThreadTextEditor[]> = this._onTextEditorAdd.event;
|
||||
readonly onTextEditorRemove: Event<string[]> = this._onTextEditorRemove.event;
|
||||
readonly onDocumentAdd: Event<ITextModel[]> = this._onDocumentAdd.event;
|
||||
readonly onDocumentRemove: Event<URI[]> = this._onDocumentRemove.event;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ITextModelService textModelResolverService: ITextModelService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
@IBulkEditService bulkEditService: IBulkEditService,
|
||||
@IPanelService panelService: IPanelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
@IClipboardService private readonly _clipboardService: IClipboardService,
|
||||
@IPathService pathService: IPathService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors);
|
||||
|
||||
this._mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService, pathService));
|
||||
extHostContext.set(MainContext.MainThreadDocuments, this._mainThreadDocuments);
|
||||
|
||||
const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService));
|
||||
extHostContext.set(MainContext.MainThreadTextEditors, mainThreadTextEditors);
|
||||
|
||||
// It is expected that the ctor of the state computer calls our `_onDelta`.
|
||||
this._toDispose.add(new MainThreadDocumentAndEditorStateComputer(delta => this._onDelta(delta), _modelService, codeEditorService, this._editorService, panelService));
|
||||
|
||||
this._toDispose.add(this._onTextEditorAdd);
|
||||
this._toDispose.add(this._onTextEditorRemove);
|
||||
this._toDispose.add(this._onDocumentAdd);
|
||||
this._toDispose.add(this._onDocumentRemove);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
|
||||
private _onDelta(delta: DocumentAndEditorStateDelta): void {
|
||||
|
||||
let removedDocuments: URI[];
|
||||
const removedEditors: string[] = [];
|
||||
const addedEditors: MainThreadTextEditor[] = [];
|
||||
|
||||
// removed models
|
||||
removedDocuments = delta.removedDocuments.map(m => m.uri);
|
||||
|
||||
// added editors
|
||||
for (const apiEditor of delta.addedEditors) {
|
||||
const mainThreadEditor = new MainThreadTextEditor(apiEditor.id, apiEditor.editor.getModel(),
|
||||
apiEditor.editor, { onGainedFocus() { }, onLostFocus() { } }, this._mainThreadDocuments, this._modelService, this._clipboardService);
|
||||
|
||||
this._textEditors.set(apiEditor.id, mainThreadEditor);
|
||||
addedEditors.push(mainThreadEditor);
|
||||
}
|
||||
|
||||
// removed editors
|
||||
for (const { id } of delta.removedEditors) {
|
||||
const mainThreadEditor = this._textEditors.get(id);
|
||||
if (mainThreadEditor) {
|
||||
mainThreadEditor.dispose();
|
||||
this._textEditors.delete(id);
|
||||
removedEditors.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
const extHostDelta: IDocumentsAndEditorsDelta = Object.create(null);
|
||||
let empty = true;
|
||||
if (delta.newActiveEditor !== undefined) {
|
||||
empty = false;
|
||||
extHostDelta.newActiveEditor = delta.newActiveEditor;
|
||||
}
|
||||
if (removedDocuments.length > 0) {
|
||||
empty = false;
|
||||
extHostDelta.removedDocuments = removedDocuments;
|
||||
}
|
||||
if (removedEditors.length > 0) {
|
||||
empty = false;
|
||||
extHostDelta.removedEditors = removedEditors;
|
||||
}
|
||||
if (delta.addedDocuments.length > 0) {
|
||||
empty = false;
|
||||
extHostDelta.addedDocuments = delta.addedDocuments.map(m => this._toModelAddData(m));
|
||||
}
|
||||
if (delta.addedEditors.length > 0) {
|
||||
empty = false;
|
||||
extHostDelta.addedEditors = addedEditors.map(e => this._toTextEditorAddData(e));
|
||||
}
|
||||
|
||||
if (!empty) {
|
||||
// first update ext host
|
||||
this._proxy.$acceptDocumentsAndEditorsDelta(extHostDelta);
|
||||
// second update dependent state listener
|
||||
this._onDocumentRemove.fire(removedDocuments);
|
||||
this._onDocumentAdd.fire(delta.addedDocuments);
|
||||
this._onTextEditorRemove.fire(removedEditors);
|
||||
this._onTextEditorAdd.fire(addedEditors);
|
||||
}
|
||||
}
|
||||
|
||||
private _toModelAddData(model: ITextModel): IModelAddedData {
|
||||
return {
|
||||
uri: model.uri,
|
||||
versionId: model.getVersionId(),
|
||||
lines: model.getLinesContent(),
|
||||
EOL: model.getEOL(),
|
||||
modeId: model.getLanguageIdentifier().language,
|
||||
isDirty: this._textFileService.isDirty(model.uri)
|
||||
};
|
||||
}
|
||||
|
||||
private _toTextEditorAddData(textEditor: MainThreadTextEditor): ITextEditorAddData {
|
||||
const props = textEditor.getProperties();
|
||||
return {
|
||||
id: textEditor.getId(),
|
||||
documentUri: textEditor.getModel().uri,
|
||||
options: props.options,
|
||||
selections: props.selections,
|
||||
visibleRanges: props.visibleRanges,
|
||||
editorPosition: this._findEditorPosition(textEditor)
|
||||
};
|
||||
}
|
||||
|
||||
private _findEditorPosition(editor: MainThreadTextEditor): EditorViewColumn | undefined {
|
||||
for (const editorPane of this._editorService.visibleEditorPanes) {
|
||||
if (editor.matches(editorPane)) {
|
||||
return editorGroupToViewColumn(this._editorGroupService, editorPane.group);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
findTextEditorIdFor(editorPane: IEditorPane): string | undefined {
|
||||
for (const [id, editor] of this._textEditors) {
|
||||
if (editor.matches(editorPane)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getEditor(id: string): MainThreadTextEditor | undefined {
|
||||
return this._textEditors.get(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { MainContext, IExtHostContext, MainThreadDownloadServiceShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDownloadService } from 'vs/platform/download/common/download';
|
||||
import { UriComponents, URI } from 'vs/base/common/uri';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadDownloadService)
|
||||
export class MainThreadDownloadService extends Disposable implements MainThreadDownloadServiceShape {
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IDownloadService private readonly downloadService: IDownloadService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
$download(uri: UriComponents, to: UriComponents): Promise<void> {
|
||||
return this.downloadService.download(URI.revive(uri), URI.revive(to));
|
||||
}
|
||||
|
||||
}
|
||||
547
lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts
Normal file
547
lib/vscode/src/vs/workbench/api/browser/mainThreadEditor.ts
Normal file
@@ -0,0 +1,547 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { RenderLineNumbersType, TextEditorCursorStyle, cursorStyleToString, EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { ISelection, Selection } from 'vs/editor/common/core/selection';
|
||||
import { IDecorationOptions, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { ISingleEditOperation, ITextModel, ITextModelUpdateOptions, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
|
||||
import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { IEditorPane } from 'vs/workbench/common/editor';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { equals } from 'vs/base/common/arrays';
|
||||
import { CodeEditorStateFlag, EditorState } from 'vs/editor/browser/core/editorState';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
|
||||
import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocuments';
|
||||
|
||||
export interface IFocusTracker {
|
||||
onGainedFocus(): void;
|
||||
onLostFocus(): void;
|
||||
}
|
||||
|
||||
export class MainThreadTextEditorProperties {
|
||||
|
||||
public static readFromEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): MainThreadTextEditorProperties {
|
||||
const selections = MainThreadTextEditorProperties._readSelectionsFromCodeEditor(previousProperties, codeEditor);
|
||||
const options = MainThreadTextEditorProperties._readOptionsFromCodeEditor(previousProperties, model, codeEditor);
|
||||
const visibleRanges = MainThreadTextEditorProperties._readVisibleRangesFromCodeEditor(previousProperties, codeEditor);
|
||||
return new MainThreadTextEditorProperties(selections, options, visibleRanges);
|
||||
}
|
||||
|
||||
private static _readSelectionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Selection[] {
|
||||
let result: Selection[] | null = null;
|
||||
if (codeEditor) {
|
||||
result = codeEditor.getSelections();
|
||||
}
|
||||
if (!result && previousProperties) {
|
||||
result = previousProperties.selections;
|
||||
}
|
||||
if (!result) {
|
||||
result = [new Selection(1, 1, 1, 1)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _readOptionsFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, model: ITextModel, codeEditor: ICodeEditor | null): IResolvedTextEditorConfiguration {
|
||||
if (model.isDisposed()) {
|
||||
if (previousProperties) {
|
||||
// shutdown time
|
||||
return previousProperties.options;
|
||||
} else {
|
||||
throw new Error('No valid properties');
|
||||
}
|
||||
}
|
||||
|
||||
let cursorStyle: TextEditorCursorStyle;
|
||||
let lineNumbers: RenderLineNumbersType;
|
||||
if (codeEditor) {
|
||||
const options = codeEditor.getOptions();
|
||||
const lineNumbersOpts = options.get(EditorOption.lineNumbers);
|
||||
cursorStyle = options.get(EditorOption.cursorStyle);
|
||||
lineNumbers = lineNumbersOpts.renderType;
|
||||
} else if (previousProperties) {
|
||||
cursorStyle = previousProperties.options.cursorStyle;
|
||||
lineNumbers = previousProperties.options.lineNumbers;
|
||||
} else {
|
||||
cursorStyle = TextEditorCursorStyle.Line;
|
||||
lineNumbers = RenderLineNumbersType.On;
|
||||
}
|
||||
|
||||
const modelOptions = model.getOptions();
|
||||
return {
|
||||
insertSpaces: modelOptions.insertSpaces,
|
||||
tabSize: modelOptions.tabSize,
|
||||
indentSize: modelOptions.indentSize,
|
||||
cursorStyle: cursorStyle,
|
||||
lineNumbers: lineNumbers
|
||||
};
|
||||
}
|
||||
|
||||
private static _readVisibleRangesFromCodeEditor(previousProperties: MainThreadTextEditorProperties | null, codeEditor: ICodeEditor | null): Range[] {
|
||||
if (codeEditor) {
|
||||
return codeEditor.getVisibleRanges();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly selections: Selection[],
|
||||
public readonly options: IResolvedTextEditorConfiguration,
|
||||
public readonly visibleRanges: Range[]
|
||||
) {
|
||||
}
|
||||
|
||||
public generateDelta(oldProps: MainThreadTextEditorProperties | null, selectionChangeSource: string | null): IEditorPropertiesChangeData | null {
|
||||
const delta: IEditorPropertiesChangeData = {
|
||||
options: null,
|
||||
selections: null,
|
||||
visibleRanges: null
|
||||
};
|
||||
|
||||
if (!oldProps || !MainThreadTextEditorProperties._selectionsEqual(oldProps.selections, this.selections)) {
|
||||
delta.selections = {
|
||||
selections: this.selections,
|
||||
source: withNullAsUndefined(selectionChangeSource)
|
||||
};
|
||||
}
|
||||
|
||||
if (!oldProps || !MainThreadTextEditorProperties._optionsEqual(oldProps.options, this.options)) {
|
||||
delta.options = this.options;
|
||||
}
|
||||
|
||||
if (!oldProps || !MainThreadTextEditorProperties._rangesEqual(oldProps.visibleRanges, this.visibleRanges)) {
|
||||
delta.visibleRanges = this.visibleRanges;
|
||||
}
|
||||
|
||||
if (delta.selections || delta.options || delta.visibleRanges) {
|
||||
// something changed
|
||||
return delta;
|
||||
}
|
||||
// nothing changed
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _selectionsEqual(a: readonly Selection[], b: readonly Selection[]): boolean {
|
||||
return equals(a, b, (aValue, bValue) => aValue.equalsSelection(bValue));
|
||||
}
|
||||
|
||||
private static _rangesEqual(a: readonly Range[], b: readonly Range[]): boolean {
|
||||
return equals(a, b, (aValue, bValue) => aValue.equalsRange(bValue));
|
||||
}
|
||||
|
||||
private static _optionsEqual(a: IResolvedTextEditorConfiguration, b: IResolvedTextEditorConfiguration): boolean {
|
||||
if (a && !b || !a && b) {
|
||||
return false;
|
||||
}
|
||||
if (!a && !b) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
a.tabSize === b.tabSize
|
||||
&& a.indentSize === b.indentSize
|
||||
&& a.insertSpaces === b.insertSpaces
|
||||
&& a.cursorStyle === b.cursorStyle
|
||||
&& a.lineNumbers === b.lineNumbers
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Text Editor that is permanently bound to the same model.
|
||||
* It can be bound or not to a CodeEditor.
|
||||
*/
|
||||
export class MainThreadTextEditor {
|
||||
|
||||
private readonly _id: string;
|
||||
private readonly _model: ITextModel;
|
||||
private readonly _mainThreadDocuments: MainThreadDocuments;
|
||||
private readonly _modelService: IModelService;
|
||||
private readonly _clipboardService: IClipboardService;
|
||||
private readonly _modelListeners = new DisposableStore();
|
||||
private _codeEditor: ICodeEditor | null;
|
||||
private readonly _focusTracker: IFocusTracker;
|
||||
private readonly _codeEditorListeners = new DisposableStore();
|
||||
|
||||
private _properties: MainThreadTextEditorProperties | null;
|
||||
private readonly _onPropertiesChanged: Emitter<IEditorPropertiesChangeData>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
model: ITextModel,
|
||||
codeEditor: ICodeEditor,
|
||||
focusTracker: IFocusTracker,
|
||||
mainThreadDocuments: MainThreadDocuments,
|
||||
modelService: IModelService,
|
||||
clipboardService: IClipboardService,
|
||||
) {
|
||||
this._id = id;
|
||||
this._model = model;
|
||||
this._codeEditor = null;
|
||||
this._properties = null;
|
||||
this._focusTracker = focusTracker;
|
||||
this._mainThreadDocuments = mainThreadDocuments;
|
||||
this._modelService = modelService;
|
||||
this._clipboardService = clipboardService;
|
||||
|
||||
this._onPropertiesChanged = new Emitter<IEditorPropertiesChangeData>();
|
||||
|
||||
this._modelListeners.add(this._model.onDidChangeOptions((e) => {
|
||||
this._updatePropertiesNow(null);
|
||||
}));
|
||||
|
||||
this.setCodeEditor(codeEditor);
|
||||
this._updatePropertiesNow(null);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._modelListeners.dispose();
|
||||
this._codeEditor = null;
|
||||
this._codeEditorListeners.dispose();
|
||||
}
|
||||
|
||||
private _updatePropertiesNow(selectionChangeSource: string | null): void {
|
||||
this._setProperties(
|
||||
MainThreadTextEditorProperties.readFromEditor(this._properties, this._model, this._codeEditor),
|
||||
selectionChangeSource
|
||||
);
|
||||
}
|
||||
|
||||
private _setProperties(newProperties: MainThreadTextEditorProperties, selectionChangeSource: string | null): void {
|
||||
const delta = newProperties.generateDelta(this._properties, selectionChangeSource);
|
||||
this._properties = newProperties;
|
||||
if (delta) {
|
||||
this._onPropertiesChanged.fire(delta);
|
||||
}
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public getModel(): ITextModel {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
public getCodeEditor(): ICodeEditor | null {
|
||||
return this._codeEditor;
|
||||
}
|
||||
|
||||
public hasCodeEditor(codeEditor: ICodeEditor | null): boolean {
|
||||
return (this._codeEditor === codeEditor);
|
||||
}
|
||||
|
||||
public setCodeEditor(codeEditor: ICodeEditor | null): void {
|
||||
if (this.hasCodeEditor(codeEditor)) {
|
||||
// Nothing to do...
|
||||
return;
|
||||
}
|
||||
this._codeEditorListeners.clear();
|
||||
|
||||
this._codeEditor = codeEditor;
|
||||
if (this._codeEditor) {
|
||||
|
||||
// Catch early the case that this code editor gets a different model set and disassociate from this model
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidChangeModel(() => {
|
||||
this.setCodeEditor(null);
|
||||
}));
|
||||
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidFocusEditorWidget(() => {
|
||||
this._focusTracker.onGainedFocus();
|
||||
}));
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidBlurEditorWidget(() => {
|
||||
this._focusTracker.onLostFocus();
|
||||
}));
|
||||
|
||||
let nextSelectionChangeSource: string | null = null;
|
||||
this._codeEditorListeners.add(this._mainThreadDocuments.onIsCaughtUpWithContentChanges((uri) => {
|
||||
if (uri.toString() === this._model.uri.toString()) {
|
||||
const selectionChangeSource = nextSelectionChangeSource;
|
||||
nextSelectionChangeSource = null;
|
||||
this._updatePropertiesNow(selectionChangeSource);
|
||||
}
|
||||
}));
|
||||
|
||||
const isValidCodeEditor = () => {
|
||||
// Due to event timings, it is possible that there is a model change event not yet delivered to us.
|
||||
// > e.g. a model change event is emitted to a listener which then decides to update editor options
|
||||
// > In this case the editor configuration change event reaches us first.
|
||||
// So simply check that the model is still attached to this code editor
|
||||
return (this._codeEditor && this._codeEditor.getModel() === this._model);
|
||||
};
|
||||
|
||||
const updateProperties = (selectionChangeSource: string | null) => {
|
||||
// Some editor events get delivered faster than model content changes. This is
|
||||
// problematic, as this leads to editor properties reaching the extension host
|
||||
// too soon, before the model content change that was the root cause.
|
||||
//
|
||||
// If this case is identified, then let's update editor properties on the next model
|
||||
// content change instead.
|
||||
if (this._mainThreadDocuments.isCaughtUpWithContentChanges(this._model.uri)) {
|
||||
nextSelectionChangeSource = null;
|
||||
this._updatePropertiesNow(selectionChangeSource);
|
||||
} else {
|
||||
// update editor properties on the next model content change
|
||||
nextSelectionChangeSource = selectionChangeSource;
|
||||
}
|
||||
};
|
||||
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidChangeCursorSelection((e) => {
|
||||
// selection
|
||||
if (!isValidCodeEditor()) {
|
||||
return;
|
||||
}
|
||||
updateProperties(e.source);
|
||||
}));
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidChangeConfiguration((e) => {
|
||||
// options
|
||||
if (!isValidCodeEditor()) {
|
||||
return;
|
||||
}
|
||||
updateProperties(null);
|
||||
}));
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidLayoutChange(() => {
|
||||
// visibleRanges
|
||||
if (!isValidCodeEditor()) {
|
||||
return;
|
||||
}
|
||||
updateProperties(null);
|
||||
}));
|
||||
this._codeEditorListeners.add(this._codeEditor.onDidScrollChange(() => {
|
||||
// visibleRanges
|
||||
if (!isValidCodeEditor()) {
|
||||
return;
|
||||
}
|
||||
updateProperties(null);
|
||||
}));
|
||||
this._updatePropertiesNow(null);
|
||||
}
|
||||
}
|
||||
|
||||
public isVisible(): boolean {
|
||||
return !!this._codeEditor;
|
||||
}
|
||||
|
||||
public getProperties(): MainThreadTextEditorProperties {
|
||||
return this._properties!;
|
||||
}
|
||||
|
||||
public get onPropertiesChanged(): Event<IEditorPropertiesChangeData> {
|
||||
return this._onPropertiesChanged.event;
|
||||
}
|
||||
|
||||
public setSelections(selections: ISelection[]): void {
|
||||
if (this._codeEditor) {
|
||||
this._codeEditor.setSelections(selections);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelections = selections.map(Selection.liftSelection);
|
||||
this._setProperties(
|
||||
new MainThreadTextEditorProperties(newSelections, this._properties!.options, this._properties!.visibleRanges),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private _setIndentConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {
|
||||
const creationOpts = this._modelService.getCreationOptions(this._model.getLanguageIdentifier().language, this._model.uri, this._model.isForSimpleWidget);
|
||||
|
||||
if (newConfiguration.tabSize === 'auto' || newConfiguration.insertSpaces === 'auto') {
|
||||
// one of the options was set to 'auto' => detect indentation
|
||||
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: 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 = newOpts.tabSize || creationOpts.tabSize;
|
||||
} else {
|
||||
newOpts.indentSize = newConfiguration.indentSize;
|
||||
}
|
||||
}
|
||||
this._model.updateOptions(newOpts);
|
||||
}
|
||||
|
||||
public setConfiguration(newConfiguration: ITextEditorConfigurationUpdate): void {
|
||||
this._setIndentConfiguration(newConfiguration);
|
||||
|
||||
if (!this._codeEditor) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newConfiguration.cursorStyle) {
|
||||
const newCursorStyle = cursorStyleToString(newConfiguration.cursorStyle);
|
||||
this._codeEditor.updateOptions({
|
||||
cursorStyle: newCursorStyle
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof newConfiguration.lineNumbers !== 'undefined') {
|
||||
let lineNumbers: 'on' | 'off' | 'relative';
|
||||
switch (newConfiguration.lineNumbers) {
|
||||
case RenderLineNumbersType.On:
|
||||
lineNumbers = 'on';
|
||||
break;
|
||||
case RenderLineNumbersType.Relative:
|
||||
lineNumbers = 'relative';
|
||||
break;
|
||||
default:
|
||||
lineNumbers = 'off';
|
||||
}
|
||||
this._codeEditor.updateOptions({
|
||||
lineNumbers: lineNumbers
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public setDecorations(key: string, ranges: IDecorationOptions[]): void {
|
||||
if (!this._codeEditor) {
|
||||
return;
|
||||
}
|
||||
this._codeEditor.setDecorations(key, ranges);
|
||||
}
|
||||
|
||||
public setDecorationsFast(key: string, _ranges: number[]): void {
|
||||
if (!this._codeEditor) {
|
||||
return;
|
||||
}
|
||||
const ranges: Range[] = [];
|
||||
for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) {
|
||||
ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]);
|
||||
}
|
||||
this._codeEditor.setDecorationsFast(key, ranges);
|
||||
}
|
||||
|
||||
public revealRange(range: IRange, revealType: TextEditorRevealType): void {
|
||||
if (!this._codeEditor) {
|
||||
return;
|
||||
}
|
||||
switch (revealType) {
|
||||
case TextEditorRevealType.Default:
|
||||
this._codeEditor.revealRange(range, ScrollType.Smooth);
|
||||
break;
|
||||
case TextEditorRevealType.InCenter:
|
||||
this._codeEditor.revealRangeInCenter(range, ScrollType.Smooth);
|
||||
break;
|
||||
case TextEditorRevealType.InCenterIfOutsideViewport:
|
||||
this._codeEditor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);
|
||||
break;
|
||||
case TextEditorRevealType.AtTop:
|
||||
this._codeEditor.revealRangeAtTop(range, ScrollType.Smooth);
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown revealType: ${revealType}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public isFocused(): boolean {
|
||||
if (this._codeEditor) {
|
||||
return this._codeEditor.hasTextFocus();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public matches(editor: IEditorPane): boolean {
|
||||
if (!editor) {
|
||||
return false;
|
||||
}
|
||||
return editor.getControl() === this._codeEditor;
|
||||
}
|
||||
|
||||
public applyEdits(versionIdCheck: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): boolean {
|
||||
if (this._model.getVersionId() !== versionIdCheck) {
|
||||
// throw new Error('Model has changed in the meantime!');
|
||||
// model changed in the meantime
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this._codeEditor) {
|
||||
// console.warn('applyEdits on invisible editor');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof opts.setEndOfLine !== 'undefined') {
|
||||
this._model.pushEOL(opts.setEndOfLine);
|
||||
}
|
||||
|
||||
const transformedEdits = edits.map((edit): IIdentifiedSingleEditOperation => {
|
||||
return {
|
||||
range: Range.lift(edit.range),
|
||||
text: edit.text,
|
||||
forceMoveMarkers: edit.forceMoveMarkers
|
||||
};
|
||||
});
|
||||
|
||||
if (opts.undoStopBefore) {
|
||||
this._codeEditor.pushUndoStop();
|
||||
}
|
||||
this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits);
|
||||
if (opts.undoStopAfter) {
|
||||
this._codeEditor.pushUndoStop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async insertSnippet(template: string, ranges: readonly IRange[], opts: IUndoStopOptions) {
|
||||
|
||||
if (!this._codeEditor || !this._codeEditor.hasModel()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if clipboard is required and only iff read it (async)
|
||||
let clipboardText: string | undefined;
|
||||
const needsTemplate = SnippetParser.guessNeedsClipboard(template);
|
||||
if (needsTemplate) {
|
||||
const state = new EditorState(this._codeEditor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
|
||||
clipboardText = await this._clipboardService.readText();
|
||||
if (!state.validate(this._codeEditor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const snippetController = SnippetController2.get(this._codeEditor);
|
||||
|
||||
// // cancel previous snippet mode
|
||||
// snippetController.leaveSnippet();
|
||||
|
||||
// set selection, focus editor
|
||||
const selections = ranges.map(r => new Selection(r.startLineNumber, r.startColumn, r.endLineNumber, r.endColumn));
|
||||
this._codeEditor.setSelections(selections);
|
||||
this._codeEditor.focus();
|
||||
|
||||
// make modifications
|
||||
snippetController.insert(template, {
|
||||
overwriteBefore: 0, overwriteAfter: 0,
|
||||
undoStopBefore: opts.undoStopBefore, undoStopAfter: opts.undoStopAfter,
|
||||
clipboardText
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
363
lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts
Normal file
363
lib/vscode/src/vs/workbench/api/browser/mainThreadEditors.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { disposed } from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { equals as objectEquals } from 'vs/base/common/objects';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { ISelection } from 'vs/editor/common/core/selection';
|
||||
import { IDecorationOptions, IDecorationRenderOptions, ILineChange } from 'vs/editor/common/editorCommon';
|
||||
import { ISingleEditOperation } from 'vs/editor/common/model';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { IEditorOptions, ITextEditorOptions, IResourceEditorInput, EditorActivation } from 'vs/platform/editor/common/editor';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors';
|
||||
import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor';
|
||||
import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { EditorViewColumn, editorGroupToViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
|
||||
|
||||
function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] {
|
||||
if (!data?.edits) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: ResourceEdit[] = [];
|
||||
for (let edit of revive<IWorkspaceEditDto>(data).edits) {
|
||||
if (edit._type === WorkspaceEditType.File) {
|
||||
result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata));
|
||||
} else if (edit._type === WorkspaceEditType.Text) {
|
||||
result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata));
|
||||
} else if (edit._type === WorkspaceEditType.Cell) {
|
||||
result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export class MainThreadTextEditors implements MainThreadTextEditorsShape {
|
||||
|
||||
private static INSTANCE_COUNT: number = 0;
|
||||
|
||||
private readonly _instanceId: string;
|
||||
private readonly _proxy: ExtHostEditorsShape;
|
||||
private readonly _documentsAndEditors: MainThreadDocumentsAndEditors;
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _textEditorsListenersMap: { [editorId: string]: IDisposable[]; };
|
||||
private _editorPositionData: ITextEditorPositionData | null;
|
||||
private _registeredDecorationTypes: { [decorationType: string]: boolean; };
|
||||
|
||||
constructor(
|
||||
documentsAndEditors: MainThreadDocumentsAndEditors,
|
||||
extHostContext: IExtHostContext,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService
|
||||
) {
|
||||
this._instanceId = String(++MainThreadTextEditors.INSTANCE_COUNT);
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors);
|
||||
this._documentsAndEditors = documentsAndEditors;
|
||||
|
||||
this._textEditorsListenersMap = Object.create(null);
|
||||
this._editorPositionData = null;
|
||||
|
||||
this._toDispose.add(documentsAndEditors.onTextEditorAdd(editors => editors.forEach(this._onTextEditorAdd, this)));
|
||||
this._toDispose.add(documentsAndEditors.onTextEditorRemove(editors => editors.forEach(this._onTextEditorRemove, this)));
|
||||
|
||||
this._toDispose.add(this._editorService.onDidVisibleEditorsChange(() => this._updateActiveAndVisibleTextEditors()));
|
||||
this._toDispose.add(this._editorGroupService.onDidRemoveGroup(() => this._updateActiveAndVisibleTextEditors()));
|
||||
this._toDispose.add(this._editorGroupService.onDidMoveGroup(() => this._updateActiveAndVisibleTextEditors()));
|
||||
|
||||
this._registeredDecorationTypes = Object.create(null);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
Object.keys(this._textEditorsListenersMap).forEach((editorId) => {
|
||||
dispose(this._textEditorsListenersMap[editorId]);
|
||||
});
|
||||
this._textEditorsListenersMap = Object.create(null);
|
||||
this._toDispose.dispose();
|
||||
for (let decorationType in this._registeredDecorationTypes) {
|
||||
this._codeEditorService.removeDecorationType(decorationType);
|
||||
}
|
||||
this._registeredDecorationTypes = Object.create(null);
|
||||
}
|
||||
|
||||
private _onTextEditorAdd(textEditor: MainThreadTextEditor): void {
|
||||
const id = textEditor.getId();
|
||||
const toDispose: IDisposable[] = [];
|
||||
toDispose.push(textEditor.onPropertiesChanged((data) => {
|
||||
this._proxy.$acceptEditorPropertiesChanged(id, data);
|
||||
}));
|
||||
|
||||
this._textEditorsListenersMap[id] = toDispose;
|
||||
}
|
||||
|
||||
private _onTextEditorRemove(id: string): void {
|
||||
dispose(this._textEditorsListenersMap[id]);
|
||||
delete this._textEditorsListenersMap[id];
|
||||
}
|
||||
|
||||
private _updateActiveAndVisibleTextEditors(): void {
|
||||
|
||||
// editor columns
|
||||
const editorPositionData = this._getTextEditorPositionData();
|
||||
if (!objectEquals(this._editorPositionData, editorPositionData)) {
|
||||
this._editorPositionData = editorPositionData;
|
||||
this._proxy.$acceptEditorPositionData(this._editorPositionData);
|
||||
}
|
||||
}
|
||||
|
||||
private _getTextEditorPositionData(): ITextEditorPositionData {
|
||||
const result: ITextEditorPositionData = Object.create(null);
|
||||
for (let editorPane of this._editorService.visibleEditorPanes) {
|
||||
const id = this._documentsAndEditors.findTextEditorIdFor(editorPane);
|
||||
if (id) {
|
||||
result[id] = editorGroupToViewColumn(this._editorGroupService, editorPane.group);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- from extension host process
|
||||
|
||||
async $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): Promise<string | undefined> {
|
||||
const uri = URI.revive(resource);
|
||||
|
||||
const editorOptions: ITextEditorOptions = {
|
||||
preserveFocus: options.preserveFocus,
|
||||
pinned: options.pinned,
|
||||
selection: options.selection,
|
||||
// preserve pre 1.38 behaviour to not make group active when preserveFocus: true
|
||||
// but make sure to restore the editor to fix https://github.com/microsoft/vscode/issues/79633
|
||||
activation: options.preserveFocus ? EditorActivation.RESTORE : undefined,
|
||||
override: false
|
||||
};
|
||||
|
||||
const input: IResourceEditorInput = {
|
||||
resource: uri,
|
||||
options: editorOptions
|
||||
};
|
||||
|
||||
const editor = await this._editorService.openEditor(input, viewColumnToEditorGroup(this._editorGroupService, options.position));
|
||||
if (!editor) {
|
||||
return undefined;
|
||||
}
|
||||
return this._documentsAndEditors.findTextEditorIdFor(editor);
|
||||
}
|
||||
|
||||
async $tryShowEditor(id: string, position?: EditorViewColumn): Promise<void> {
|
||||
const mainThreadEditor = this._documentsAndEditors.getEditor(id);
|
||||
if (mainThreadEditor) {
|
||||
const model = mainThreadEditor.getModel();
|
||||
await this._editorService.openEditor({
|
||||
resource: model.uri,
|
||||
options: { preserveFocus: false }
|
||||
}, viewColumnToEditorGroup(this._editorGroupService, position));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async $tryHideEditor(id: string): Promise<void> {
|
||||
const mainThreadEditor = this._documentsAndEditors.getEditor(id);
|
||||
if (mainThreadEditor) {
|
||||
const editorPanes = this._editorService.visibleEditorPanes;
|
||||
for (let editorPane of editorPanes) {
|
||||
if (mainThreadEditor.matches(editorPane)) {
|
||||
return editorPane.group.closeEditor(editorPane.input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$trySetSelections(id: string, selections: ISelection[]): Promise<void> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
editor.setSelections(selections);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$trySetDecorations(id: string, key: string, ranges: IDecorationOptions[]): Promise<void> {
|
||||
key = `${this._instanceId}-${key}`;
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
editor.setDecorations(key, ranges);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise<void> {
|
||||
key = `${this._instanceId}-${key}`;
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
editor.setDecorationsFast(key, ranges);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise<void> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
editor.revealRange(range, revealType);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
$trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Promise<void> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
editor.setConfiguration(options);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise<boolean> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
return Promise.resolve(editor.applyEdits(modelVersionId, edits, opts));
|
||||
}
|
||||
|
||||
$tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise<boolean> {
|
||||
const edits = reviveWorkspaceEditDto2(dto);
|
||||
return this._bulkEditService.apply(edits).then(() => true, _err => false);
|
||||
}
|
||||
|
||||
$tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise<boolean> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
if (!editor) {
|
||||
return Promise.reject(disposed(`TextEditor(${id})`));
|
||||
}
|
||||
return Promise.resolve(editor.insertSnippet(template, ranges, opts));
|
||||
}
|
||||
|
||||
$registerTextEditorDecorationType(key: string, options: IDecorationRenderOptions): void {
|
||||
key = `${this._instanceId}-${key}`;
|
||||
this._registeredDecorationTypes[key] = true;
|
||||
this._codeEditorService.registerDecorationType(key, options);
|
||||
}
|
||||
|
||||
$removeTextEditorDecorationType(key: string): void {
|
||||
key = `${this._instanceId}-${key}`;
|
||||
delete this._registeredDecorationTypes[key];
|
||||
this._codeEditorService.removeDecorationType(key);
|
||||
}
|
||||
|
||||
$getDiffInformation(id: string): Promise<ILineChange[]> {
|
||||
const editor = this._documentsAndEditors.getEditor(id);
|
||||
|
||||
if (!editor) {
|
||||
return Promise.reject(new Error('No such TextEditor'));
|
||||
}
|
||||
|
||||
const codeEditor = editor.getCodeEditor();
|
||||
if (!codeEditor) {
|
||||
return Promise.reject(new Error('No such CodeEditor'));
|
||||
}
|
||||
|
||||
const codeEditorId = codeEditor.getId();
|
||||
const diffEditors = this._codeEditorService.listDiffEditors();
|
||||
const [diffEditor] = diffEditors.filter(d => d.getOriginalEditor().getId() === codeEditorId || d.getModifiedEditor().getId() === codeEditorId);
|
||||
|
||||
if (diffEditor) {
|
||||
return Promise.resolve(diffEditor.getLineChanges() || []);
|
||||
}
|
||||
|
||||
const dirtyDiffContribution = codeEditor.getContribution('editor.contrib.dirtydiff');
|
||||
|
||||
if (dirtyDiffContribution) {
|
||||
return Promise.resolve((dirtyDiffContribution as any).getChanges());
|
||||
}
|
||||
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- commands
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.open', async function (accessor: ServicesAccessor, args: [URI, IEditorOptions, EditorViewColumn, string?]) {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editorGroupService = accessor.get(IEditorGroupsService);
|
||||
const openerService = accessor.get(IOpenerService);
|
||||
|
||||
const [resource, options, position, label] = args;
|
||||
|
||||
if (options || typeof position === 'number') {
|
||||
// use editor options or editor view column as a hint to use the editor service for opening
|
||||
await editorService.openEditor({ resource, options, label }, viewColumnToEditorGroup(editorGroupService, position));
|
||||
return;
|
||||
}
|
||||
|
||||
if (resource && resource.scheme === 'command') {
|
||||
// do not allow to execute commands from here
|
||||
return;
|
||||
|
||||
}
|
||||
// finally, delegate to opener service
|
||||
await openerService.open(resource);
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.openWith', (accessor: ServicesAccessor, args: [URI, string, ITextEditorOptions | undefined, EditorViewColumn | undefined]) => {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editorGroupsService = accessor.get(IEditorGroupsService);
|
||||
const configurationService = accessor.get(IConfigurationService);
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
|
||||
const [resource, id, options, position] = args;
|
||||
|
||||
const group = editorGroupsService.getGroup(viewColumnToEditorGroup(editorGroupsService, position)) ?? editorGroupsService.activeGroup;
|
||||
const textOptions: ITextEditorOptions = options ? { ...options, override: false } : { override: false };
|
||||
|
||||
const input = editorService.createEditorInput({ resource });
|
||||
return openEditorWith(input, id, textOptions, group, editorService, configurationService, quickInputService);
|
||||
});
|
||||
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.diff', async function (accessor: ServicesAccessor, args: [URI, URI, string, string, IEditorOptions, EditorViewColumn]) {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editorGroupService = accessor.get(IEditorGroupsService);
|
||||
|
||||
let [leftResource, rightResource, label, description, options, position] = args;
|
||||
|
||||
if (!options || typeof options !== 'object') {
|
||||
options = {
|
||||
preserveFocus: false
|
||||
};
|
||||
}
|
||||
|
||||
await editorService.openEditor({ leftResource, rightResource, label, description, options }, viewColumnToEditorGroup(editorGroupService, position));
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_workbench.revertAllDirty', async function (accessor: ServicesAccessor) {
|
||||
const environmentService = accessor.get(IEnvironmentService);
|
||||
if (!environmentService.extensionTestsLocationURI) {
|
||||
throw new Error('Command is only available when running extension tests.');
|
||||
}
|
||||
|
||||
const workingCopyService = accessor.get(IWorkingCopyService);
|
||||
for (const workingCopy of workingCopyService.dirtyWorkingCopies) {
|
||||
await workingCopy.revert({ soft: true });
|
||||
}
|
||||
});
|
||||
27
lib/vscode/src/vs/workbench/api/browser/mainThreadErrors.ts
Normal file
27
lib/vscode/src/vs/workbench/api/browser/mainThreadErrors.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SerializedError, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadErrorsShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadErrors)
|
||||
export class MainThreadErrors implements MainThreadErrorsShape {
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
|
||||
$onUnexpectedError(err: any | SerializedError): void {
|
||||
if (err && err.$isError) {
|
||||
const { name, message, stack } = err;
|
||||
err = new Error();
|
||||
err.message = message;
|
||||
err.name = name;
|
||||
err.stack = stack;
|
||||
}
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { SerializedError } from 'vs/base/common/errors';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { IExtensionService, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadExtensionService)
|
||||
export class MainThreadExtensionService implements MainThreadExtensionServiceShape {
|
||||
|
||||
private readonly _extensionService: IExtensionService;
|
||||
private readonly _notificationService: INotificationService;
|
||||
private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService;
|
||||
private readonly _hostService: IHostService;
|
||||
private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@INotificationService notificationService: INotificationService,
|
||||
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
|
||||
@IHostService hostService: IHostService,
|
||||
@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService
|
||||
) {
|
||||
this._extensionService = extensionService;
|
||||
this._notificationService = notificationService;
|
||||
this._extensionsWorkbenchService = extensionsWorkbenchService;
|
||||
this._hostService = hostService;
|
||||
this._extensionEnablementService = extensionEnablementService;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
$activateExtension(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise<void> {
|
||||
return this._extensionService._activateById(extensionId, reason);
|
||||
}
|
||||
async $onWillActivateExtension(extensionId: ExtensionIdentifier): Promise<void> {
|
||||
this._extensionService._onWillActivateExtension(extensionId);
|
||||
}
|
||||
$onDidActivateExtension(extensionId: ExtensionIdentifier, codeLoadingTime: number, activateCallTime: number, activateResolvedTime: number, activationReason: ExtensionActivationReason): void {
|
||||
this._extensionService._onDidActivateExtension(extensionId, codeLoadingTime, activateCallTime, activateResolvedTime, activationReason);
|
||||
}
|
||||
$onExtensionRuntimeError(extensionId: ExtensionIdentifier, data: SerializedError): void {
|
||||
const error = new Error();
|
||||
error.name = data.name;
|
||||
error.message = data.message;
|
||||
error.stack = data.stack;
|
||||
this._extensionService._onExtensionRuntimeError(extensionId, error);
|
||||
console.error(`[${extensionId}]${error.message}`);
|
||||
console.error(error.stack);
|
||||
}
|
||||
async $onExtensionActivationError(extensionId: ExtensionIdentifier, activationError: ExtensionActivationError): Promise<void> {
|
||||
if (typeof activationError === 'string') {
|
||||
this._extensionService._logOrShowMessage(Severity.Error, activationError);
|
||||
} else {
|
||||
this._handleMissingDependency(extensionId, activationError.dependency);
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMissingDependency(extensionId: ExtensionIdentifier, missingDependency: string): Promise<void> {
|
||||
const extension = await this._extensionService.getExtension(extensionId.value);
|
||||
if (extension) {
|
||||
const local = await this._extensionsWorkbenchService.queryLocal();
|
||||
const installedDependency = local.filter(i => areSameExtensions(i.identifier, { id: missingDependency }))[0];
|
||||
if (installedDependency) {
|
||||
await this._handleMissingInstalledDependency(extension, installedDependency.local!);
|
||||
} else {
|
||||
await this._handleMissingNotInstalledDependency(extension, missingDependency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMissingInstalledDependency(extension: IExtensionDescription, missingInstalledDependency: ILocalExtension): Promise<void> {
|
||||
const extName = extension.displayName || extension.name;
|
||||
if (this._extensionEnablementService.isEnabled(missingInstalledDependency)) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('reload window', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is not loaded. Would you like to reload the window to load the extension?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name),
|
||||
actions: {
|
||||
primary: [new Action('reload', localize('reload', "Reload Window"), '', true, () => this._hostService.reload())]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const enablementState = this._extensionEnablementService.getEnablementState(missingInstalledDependency);
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('disabledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is disabled. Would you like to enable the extension and reload the window?", extName, missingInstalledDependency.manifest.displayName || missingInstalledDependency.manifest.name),
|
||||
actions: {
|
||||
primary: [new Action('enable', localize('enable dep', "Enable and Reload"), '', true,
|
||||
() => this._extensionEnablementService.setEnablement([missingInstalledDependency], enablementState === EnablementState.DisabledGlobally ? EnablementState.EnabledGlobally : EnablementState.EnabledWorkspace)
|
||||
.then(() => this._hostService.reload(), e => this._notificationService.error(e)))]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _handleMissingNotInstalledDependency(extension: IExtensionDescription, missingDependency: string): Promise<void> {
|
||||
const extName = extension.displayName || extension.name;
|
||||
const dependencyExtension = (await this._extensionsWorkbenchService.queryGallery({ names: [missingDependency] }, CancellationToken.None)).firstPage[0];
|
||||
if (dependencyExtension) {
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: localize('uninstalledDep', "Cannot activate the '{0}' extension because it depends on the '{1}' extension, which is not installed. Would you like to install the extension and reload the window?", extName, dependencyExtension.displayName),
|
||||
actions: {
|
||||
primary: [new Action('install', localize('install missing dep', "Install and Reload"), '', true,
|
||||
() => this._extensionsWorkbenchService.install(dependencyExtension)
|
||||
.then(() => this._hostService.reload(), e => this._notificationService.error(e)))]
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._notificationService.error(localize('unknownDep', "Cannot activate the '{0}' extension because it depends on an unknown '{1}' extension .", extName, missingDependency));
|
||||
}
|
||||
}
|
||||
|
||||
async $onExtensionHostExit(code: number): Promise<void> {
|
||||
this._extensionService._onExtensionHostExit(code);
|
||||
}
|
||||
}
|
||||
244
lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts
Normal file
244
lib/vscode/src/vs/workbench/api/browser/mainThreadFileSystem.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { FileWriteOptions, FileSystemProviderCapabilities, IFileChange, IFileService, IStat, IWatchOptions, FileType, FileOverwriteOptions, FileDeleteOptions, FileOpenOptions, IFileStat, FileOperationError, FileOperationResult, FileSystemProviderErrorCode, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithFileFolderCopyCapability } from 'vs/platform/files/common/files';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostFileSystemShape, IExtHostContext, IFileChangeDto, MainContext, MainThreadFileSystemShape } from '../common/extHost.protocol';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadFileSystem)
|
||||
export class MainThreadFileSystem implements MainThreadFileSystemShape {
|
||||
|
||||
private readonly _proxy: ExtHostFileSystemShape;
|
||||
private readonly _fileProvider = new Map<number, RemoteFileSystemProvider>();
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystem);
|
||||
|
||||
const infoProxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemInfo);
|
||||
|
||||
for (let entry of _fileService.listCapabilities()) {
|
||||
infoProxy.$acceptProviderInfos(entry.scheme, entry.capabilities);
|
||||
}
|
||||
this._disposables.add(_fileService.onDidChangeFileSystemProviderRegistrations(e => infoProxy.$acceptProviderInfos(e.scheme, e.provider?.capabilities ?? null)));
|
||||
this._disposables.add(_fileService.onDidChangeFileSystemProviderCapabilities(e => infoProxy.$acceptProviderInfos(e.scheme, e.provider.capabilities)));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
dispose(this._fileProvider.values());
|
||||
this._fileProvider.clear();
|
||||
}
|
||||
|
||||
async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): Promise<void> {
|
||||
this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy));
|
||||
}
|
||||
|
||||
$unregisterProvider(handle: number): void {
|
||||
this._fileProvider.get(handle)?.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<IStat> {
|
||||
return this._fileService.resolve(URI.revive(uri), { resolveMetadata: true }).then(stat => {
|
||||
return {
|
||||
ctime: stat.ctime,
|
||||
mtime: stat.mtime,
|
||||
size: stat.size,
|
||||
type: MainThreadFileSystem._asFileType(stat)
|
||||
};
|
||||
}).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$readdir(uri: UriComponents): Promise<[string, FileType][]> {
|
||||
return this._fileService.resolve(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, MainThreadFileSystem._asFileType(child)] as [string, FileType]);
|
||||
}).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
private static _asFileType(stat: IFileStat): FileType {
|
||||
let res = 0;
|
||||
if (stat.isFile) {
|
||||
res += FileType.File;
|
||||
|
||||
} else if (stat.isDirectory) {
|
||||
res += FileType.Directory;
|
||||
}
|
||||
if (stat.isSymbolicLink) {
|
||||
res += FileType.SymbolicLink;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
$readFile(uri: UriComponents): Promise<VSBuffer> {
|
||||
return this._fileService.readFile(URI.revive(uri)).then(file => file.value).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$writeFile(uri: UriComponents, content: VSBuffer): Promise<void> {
|
||||
return this._fileService.writeFile(URI.revive(uri), content)
|
||||
.then(() => undefined).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$rename(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.move(URI.revive(source), URI.revive(target), opts.overwrite)
|
||||
.then(() => undefined).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$copy(source: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._fileService.copy(URI.revive(source), URI.revive(target), opts.overwrite)
|
||||
.then(() => undefined).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$mkdir(uri: UriComponents): Promise<void> {
|
||||
return this._fileService.createFolder(URI.revive(uri))
|
||||
.then(() => undefined).catch(MainThreadFileSystem._handleError);
|
||||
}
|
||||
|
||||
$delete(uri: UriComponents, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._fileService.del(URI.revive(uri), opts).catch(MainThreadFileSystem._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 IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileFolderCopyCapability {
|
||||
|
||||
private readonly _onDidChange = new Emitter<readonly IFileChange[]>();
|
||||
private readonly _registration: IDisposable;
|
||||
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChange.event;
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
constructor(
|
||||
fileService: IFileService,
|
||||
scheme: string,
|
||||
capabilities: FileSystemProviderCapabilities,
|
||||
private readonly _handle: number,
|
||||
private readonly _proxy: ExtHostFileSystemShape
|
||||
) {
|
||||
this.capabilities = capabilities;
|
||||
this._registration = fileService.registerProvider(scheme, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._registration.dispose();
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions) {
|
||||
const session = Math.random();
|
||||
this._proxy.$watch(this._handle, session, resource, opts);
|
||||
return toDisposable(() => {
|
||||
this._proxy.$unwatch(this._handle, session);
|
||||
});
|
||||
}
|
||||
|
||||
$onFileSystemChange(changes: IFileChangeDto[]): void {
|
||||
this._onDidChange.fire(changes.map(RemoteFileSystemProvider._createFileChange));
|
||||
}
|
||||
|
||||
private static _createFileChange(dto: IFileChangeDto): IFileChange {
|
||||
return { resource: URI.revive(dto.resource), type: dto.type };
|
||||
}
|
||||
|
||||
// --- forwarding calls
|
||||
|
||||
stat(resource: URI): Promise<IStat> {
|
||||
return this._proxy.$stat(this._handle, resource).then(undefined, err => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
readFile(resource: URI): Promise<Uint8Array> {
|
||||
return this._proxy.$readFile(this._handle, resource).then(buffer => buffer.buffer);
|
||||
}
|
||||
|
||||
writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
return this._proxy.$writeFile(this._handle, resource, VSBuffer.wrap(content), opts);
|
||||
}
|
||||
|
||||
delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
return this._proxy.$delete(this._handle, resource, opts);
|
||||
}
|
||||
|
||||
mkdir(resource: URI): Promise<void> {
|
||||
return this._proxy.$mkdir(this._handle, resource);
|
||||
}
|
||||
|
||||
readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
return this._proxy.$readdir(this._handle, resource);
|
||||
}
|
||||
|
||||
rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$rename(this._handle, resource, target, opts);
|
||||
}
|
||||
|
||||
copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return this._proxy.$copy(this._handle, resource, target, opts);
|
||||
}
|
||||
|
||||
open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
||||
return this._proxy.$open(this._handle, resource, 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, VSBuffer.wrap(data).slice(offset, offset + length));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { FileChangeType, IFileService } from 'vs/platform/files/common/files';
|
||||
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
|
||||
@extHostCustomer
|
||||
export class MainThreadFileSystemEventService {
|
||||
|
||||
private readonly _listener = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService
|
||||
) {
|
||||
|
||||
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
|
||||
|
||||
// file system events - (changes the editor and other make)
|
||||
const events: FileSystemEvents = {
|
||||
created: [],
|
||||
changed: [],
|
||||
deleted: []
|
||||
};
|
||||
this._listener.add(fileService.onDidFilesChange(event => {
|
||||
for (let change of event.changes) {
|
||||
switch (change.type) {
|
||||
case FileChangeType.ADDED:
|
||||
events.created.push(change.resource);
|
||||
break;
|
||||
case FileChangeType.UPDATED:
|
||||
events.changed.push(change.resource);
|
||||
break;
|
||||
case FileChangeType.DELETED:
|
||||
events.deleted.push(change.resource);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
proxy.$onFileEvent(events);
|
||||
events.created.length = 0;
|
||||
events.changed.length = 0;
|
||||
events.deleted.length = 0;
|
||||
}));
|
||||
|
||||
|
||||
// BEFORE file operation
|
||||
workingCopyFileService.addFileOperationParticipant({
|
||||
participate: (files, operation, progress, timeout, token) => {
|
||||
return proxy.$onWillRunFileOperation(operation, files, timeout, token);
|
||||
}
|
||||
});
|
||||
|
||||
// AFTER file operation
|
||||
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files)));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._listener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
id: 'files',
|
||||
properties: {
|
||||
'files.participants.timeout': {
|
||||
type: 'number',
|
||||
default: 5000,
|
||||
markdownDescription: localize('files.participants.timeout', "Timeout in milliseconds after which file participants for create, rename, and delete are cancelled. Use `0` to disable participants."),
|
||||
}
|
||||
}
|
||||
});
|
||||
41
lib/vscode/src/vs/workbench/api/browser/mainThreadKeytar.ts
Normal file
41
lib/vscode/src/vs/workbench/api/browser/mainThreadKeytar.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadKeytarShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadKeytar)
|
||||
export class MainThreadKeytar implements MainThreadKeytarShape {
|
||||
|
||||
constructor(
|
||||
_extHostContext: IExtHostContext,
|
||||
@ICredentialsService private readonly _credentialsService: ICredentialsService,
|
||||
) { }
|
||||
|
||||
async $getPassword(service: string, account: string): Promise<string | null> {
|
||||
return this._credentialsService.getPassword(service, account);
|
||||
}
|
||||
|
||||
async $setPassword(service: string, account: string, password: string): Promise<void> {
|
||||
return this._credentialsService.setPassword(service, account, password);
|
||||
}
|
||||
|
||||
async $deletePassword(service: string, account: string): Promise<boolean> {
|
||||
return this._credentialsService.deletePassword(service, account);
|
||||
}
|
||||
|
||||
async $findPassword(service: string): Promise<string | null> {
|
||||
return this._credentialsService.findPassword(service);
|
||||
}
|
||||
|
||||
async $findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
|
||||
return this._credentialsService.findCredentials(service);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MainContext, MainThreadLabelServiceShape, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ResourceLabelFormatter, ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadLabelService)
|
||||
export class MainThreadLabelService implements MainThreadLabelServiceShape {
|
||||
|
||||
private readonly _resourceLabelFormatters = new Map<number, IDisposable>();
|
||||
|
||||
constructor(
|
||||
_: IExtHostContext,
|
||||
@ILabelService private readonly _labelService: ILabelService
|
||||
) { }
|
||||
|
||||
$registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void {
|
||||
// Dynamicily registered formatters should have priority over those contributed via package.json
|
||||
formatter.priority = true;
|
||||
const disposable = this._labelService.registerFormatter(formatter);
|
||||
this._resourceLabelFormatters.set(handle, disposable);
|
||||
}
|
||||
|
||||
$unregisterResourceLabelFormatter(handle: number): void {
|
||||
dispose(this._resourceLabelFormatters.get(handle));
|
||||
this._resourceLabelFormatters.delete(handle);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { ITextModel, ISingleEditOperation } from 'vs/editor/common/model';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import * as search from 'vs/workbench/contrib/search/common/search';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Position as EditorPosition } from 'vs/editor/common/core/position';
|
||||
import { Range as EditorRange, IRange } from 'vs/editor/common/core/range';
|
||||
import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ILanguageConfigurationDto, IRegExpDto, IIndentationRuleDto, IOnEnterRuleDto, ILocationDto, IWorkspaceSymbolDto, reviveWorkspaceEditDto, IDocumentFilterDto, IDefinitionLinkDto, ISignatureHelpProviderMetadataDto, ILinkDto, ICallHierarchyItemDto, ISuggestDataDto, ICodeActionDto, ISuggestDataDtoField, ISuggestResultDtoField, ICodeActionProviderMetadataDto, ILanguageWordDefinitionDto } from '../common/extHost.protocol';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadLanguageFeatures)
|
||||
export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape {
|
||||
|
||||
private readonly _proxy: ExtHostLanguageFeaturesShape;
|
||||
private readonly _modeService: IModeService;
|
||||
private readonly _registrations = new Map<number, IDisposable>();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IModeService modeService: IModeService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageFeatures);
|
||||
this._modeService = modeService;
|
||||
|
||||
if (this._modeService) {
|
||||
const updateAllWordDefinitions = () => {
|
||||
const langWordPairs = LanguageConfigurationRegistry.getWordDefinitions();
|
||||
let wordDefinitionDtos: ILanguageWordDefinitionDto[] = [];
|
||||
for (const [languageId, wordDefinition] of langWordPairs) {
|
||||
const language = this._modeService.getLanguageIdentifier(languageId);
|
||||
if (!language) {
|
||||
continue;
|
||||
}
|
||||
wordDefinitionDtos.push({
|
||||
languageId: language.language,
|
||||
regexSource: wordDefinition.source,
|
||||
regexFlags: wordDefinition.flags
|
||||
});
|
||||
}
|
||||
this._proxy.$setWordDefinitions(wordDefinitionDtos);
|
||||
};
|
||||
LanguageConfigurationRegistry.onDidChange((e) => {
|
||||
const wordDefinition = LanguageConfigurationRegistry.getWordDefinition(e.languageIdentifier.id);
|
||||
this._proxy.$setWordDefinitions([{
|
||||
languageId: e.languageIdentifier.language,
|
||||
regexSource: wordDefinition.source,
|
||||
regexFlags: wordDefinition.flags
|
||||
}]);
|
||||
});
|
||||
updateAllWordDefinitions();
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const registration of this._registrations.values()) {
|
||||
registration.dispose();
|
||||
}
|
||||
this._registrations.clear();
|
||||
}
|
||||
|
||||
$unregister(handle: number): void {
|
||||
const registration = this._registrations.get(handle);
|
||||
if (registration) {
|
||||
registration.dispose();
|
||||
this._registrations.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
//#region --- revive functions
|
||||
|
||||
private static _reviveLocationDto(data?: ILocationDto): modes.Location;
|
||||
private static _reviveLocationDto(data?: ILocationDto[]): modes.Location[];
|
||||
private static _reviveLocationDto(data: ILocationDto | ILocationDto[] | undefined): modes.Location | modes.Location[] | undefined {
|
||||
if (!data) {
|
||||
return data;
|
||||
} else if (Array.isArray(data)) {
|
||||
data.forEach(l => MainThreadLanguageFeatures._reviveLocationDto(l));
|
||||
return <modes.Location[]>data;
|
||||
} else {
|
||||
data.uri = URI.revive(data.uri);
|
||||
return <modes.Location>data;
|
||||
}
|
||||
}
|
||||
|
||||
private static _reviveLocationLinkDto(data: IDefinitionLinkDto): modes.LocationLink;
|
||||
private static _reviveLocationLinkDto(data: IDefinitionLinkDto[]): modes.LocationLink[];
|
||||
private static _reviveLocationLinkDto(data: IDefinitionLinkDto | IDefinitionLinkDto[]): modes.LocationLink | modes.LocationLink[] {
|
||||
if (!data) {
|
||||
return <modes.LocationLink>data;
|
||||
} else if (Array.isArray(data)) {
|
||||
data.forEach(l => MainThreadLanguageFeatures._reviveLocationLinkDto(l));
|
||||
return <modes.LocationLink[]>data;
|
||||
} else {
|
||||
data.uri = URI.revive(data.uri);
|
||||
return <modes.LocationLink>data;
|
||||
}
|
||||
}
|
||||
|
||||
private static _reviveWorkspaceSymbolDto(data: IWorkspaceSymbolDto): search.IWorkspaceSymbol;
|
||||
private static _reviveWorkspaceSymbolDto(data: IWorkspaceSymbolDto[]): search.IWorkspaceSymbol[];
|
||||
private static _reviveWorkspaceSymbolDto(data: undefined): undefined;
|
||||
private static _reviveWorkspaceSymbolDto(data: IWorkspaceSymbolDto | IWorkspaceSymbolDto[] | undefined): search.IWorkspaceSymbol | search.IWorkspaceSymbol[] | undefined {
|
||||
if (!data) {
|
||||
return <undefined>data;
|
||||
} else if (Array.isArray(data)) {
|
||||
data.forEach(MainThreadLanguageFeatures._reviveWorkspaceSymbolDto);
|
||||
return <search.IWorkspaceSymbol[]>data;
|
||||
} else {
|
||||
data.location = MainThreadLanguageFeatures._reviveLocationDto(data.location);
|
||||
return <search.IWorkspaceSymbol>data;
|
||||
}
|
||||
}
|
||||
|
||||
private static _reviveCodeActionDto(data: ReadonlyArray<ICodeActionDto>): modes.CodeAction[] {
|
||||
if (data) {
|
||||
data.forEach(code => reviveWorkspaceEditDto(code.edit));
|
||||
}
|
||||
return <modes.CodeAction[]>data;
|
||||
}
|
||||
|
||||
private static _reviveLinkDTO(data: ILinkDto): modes.ILink {
|
||||
if (data.url && typeof data.url !== 'string') {
|
||||
data.url = URI.revive(data.url);
|
||||
}
|
||||
return <modes.ILink>data;
|
||||
}
|
||||
|
||||
private static _reviveCallHierarchyItemDto(data: ICallHierarchyItemDto | undefined): callh.CallHierarchyItem {
|
||||
if (data) {
|
||||
data.uri = URI.revive(data.uri);
|
||||
}
|
||||
return data as callh.CallHierarchyItem;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
// --- outline
|
||||
|
||||
$registerDocumentSymbolProvider(handle: number, selector: IDocumentFilterDto[], displayName: string): void {
|
||||
this._registrations.set(handle, modes.DocumentSymbolProviderRegistry.register(selector, <modes.DocumentSymbolProvider>{
|
||||
displayName,
|
||||
provideDocumentSymbols: (model: ITextModel, token: CancellationToken): Promise<modes.DocumentSymbol[] | undefined> => {
|
||||
return this._proxy.$provideDocumentSymbols(handle, model.uri, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- code lens
|
||||
|
||||
$registerCodeLensSupport(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void {
|
||||
|
||||
const provider = <modes.CodeLensProvider>{
|
||||
provideCodeLenses: (model: ITextModel, token: CancellationToken): Promise<modes.CodeLensList | undefined> => {
|
||||
return this._proxy.$provideCodeLenses(handle, model.uri, token).then(listDto => {
|
||||
if (!listDto) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
lenses: listDto.lenses,
|
||||
dispose: () => listDto.cacheId && this._proxy.$releaseCodeLenses(handle, listDto.cacheId)
|
||||
};
|
||||
});
|
||||
},
|
||||
resolveCodeLens: (_model: ITextModel, codeLens: modes.CodeLens, token: CancellationToken): Promise<modes.CodeLens | undefined> => {
|
||||
return this._proxy.$resolveCodeLens(handle, codeLens, token);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof eventHandle === 'number') {
|
||||
const emitter = new Emitter<modes.CodeLensProvider>();
|
||||
this._registrations.set(eventHandle, emitter);
|
||||
provider.onDidChange = emitter.event;
|
||||
}
|
||||
|
||||
this._registrations.set(handle, modes.CodeLensProviderRegistry.register(selector, provider));
|
||||
}
|
||||
|
||||
$emitCodeLensEvent(eventHandle: number, event?: any): void {
|
||||
const obj = this._registrations.get(eventHandle);
|
||||
if (obj instanceof Emitter) {
|
||||
obj.fire(event);
|
||||
}
|
||||
}
|
||||
|
||||
// --- declaration
|
||||
|
||||
$registerDefinitionSupport(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.DefinitionProviderRegistry.register(selector, <modes.DefinitionProvider>{
|
||||
provideDefinition: (model, position, token): Promise<modes.LocationLink[]> => {
|
||||
return this._proxy.$provideDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
$registerDeclarationSupport(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.DeclarationProviderRegistry.register(selector, <modes.DeclarationProvider>{
|
||||
provideDeclaration: (model, position, token) => {
|
||||
return this._proxy.$provideDeclaration(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
$registerImplementationSupport(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.ImplementationProviderRegistry.register(selector, <modes.ImplementationProvider>{
|
||||
provideImplementation: (model, position, token): Promise<modes.LocationLink[]> => {
|
||||
return this._proxy.$provideImplementation(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
$registerTypeDefinitionSupport(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.TypeDefinitionProviderRegistry.register(selector, <modes.TypeDefinitionProvider>{
|
||||
provideTypeDefinition: (model, position, token): Promise<modes.LocationLink[]> => {
|
||||
return this._proxy.$provideTypeDefinition(handle, model.uri, position, token).then(MainThreadLanguageFeatures._reviveLocationLinkDto);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- extra info
|
||||
|
||||
$registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.HoverProviderRegistry.register(selector, <modes.HoverProvider>{
|
||||
provideHover: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<modes.Hover | undefined> => {
|
||||
return this._proxy.$provideHover(handle, model.uri, position, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- debug hover
|
||||
|
||||
$registerEvaluatableExpressionProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.EvaluatableExpressionProviderRegistry.register(selector, <modes.EvaluatableExpressionProvider>{
|
||||
provideEvaluatableExpression: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<modes.EvaluatableExpression | undefined> => {
|
||||
return this._proxy.$provideEvaluatableExpression(handle, model.uri, position, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- occurrences
|
||||
|
||||
$registerDocumentHighlightProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.DocumentHighlightProviderRegistry.register(selector, <modes.DocumentHighlightProvider>{
|
||||
provideDocumentHighlights: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<modes.DocumentHighlight[] | undefined> => {
|
||||
return this._proxy.$provideDocumentHighlights(handle, model.uri, position, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- on type rename
|
||||
|
||||
$registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], wordPattern?: IRegExpDto): void {
|
||||
const revivedWordPattern = wordPattern ? MainThreadLanguageFeatures._reviveRegExp(wordPattern) : undefined;
|
||||
this._registrations.set(handle, modes.OnTypeRenameProviderRegistry.register(selector, <modes.OnTypeRenameProvider>{
|
||||
wordPattern: revivedWordPattern,
|
||||
provideOnTypeRenameRanges: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> => {
|
||||
const res = await this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token);
|
||||
if (res) {
|
||||
return {
|
||||
ranges: res.ranges,
|
||||
wordPattern: res.wordPattern ? MainThreadLanguageFeatures._reviveRegExp(res.wordPattern) : undefined
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- references
|
||||
|
||||
$registerReferenceSupport(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.ReferenceProviderRegistry.register(selector, <modes.ReferenceProvider>{
|
||||
provideReferences: (model: ITextModel, position: EditorPosition, context: modes.ReferenceContext, token: CancellationToken): Promise<modes.Location[]> => {
|
||||
return this._proxy.$provideReferences(handle, model.uri, position, context, token).then(MainThreadLanguageFeatures._reviveLocationDto);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- quick fix
|
||||
|
||||
$registerQuickFixSupport(handle: number, selector: IDocumentFilterDto[], metadata: ICodeActionProviderMetadataDto, displayName: string, supportsResolve: boolean): void {
|
||||
const provider: modes.CodeActionProvider = {
|
||||
provideCodeActions: async (model: ITextModel, rangeOrSelection: EditorRange | Selection, context: modes.CodeActionContext, token: CancellationToken): Promise<modes.CodeActionList | undefined> => {
|
||||
const listDto = await this._proxy.$provideCodeActions(handle, model.uri, rangeOrSelection, context, token);
|
||||
if (!listDto) {
|
||||
return undefined;
|
||||
}
|
||||
return <modes.CodeActionList>{
|
||||
actions: MainThreadLanguageFeatures._reviveCodeActionDto(listDto.actions),
|
||||
dispose: () => {
|
||||
if (typeof listDto.cacheId === 'number') {
|
||||
this._proxy.$releaseCodeActions(handle, listDto.cacheId);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
providedCodeActionKinds: metadata.providedKinds,
|
||||
documentation: metadata.documentation,
|
||||
displayName
|
||||
};
|
||||
|
||||
if (supportsResolve) {
|
||||
provider.resolveCodeAction = async (codeAction: modes.CodeAction, token: CancellationToken): Promise<modes.CodeAction> => {
|
||||
const data = await this._proxy.$resolveCodeAction(handle, (<ICodeActionDto>codeAction).cacheId!, token);
|
||||
codeAction.edit = reviveWorkspaceEditDto(data);
|
||||
return codeAction;
|
||||
};
|
||||
}
|
||||
|
||||
this._registrations.set(handle, modes.CodeActionProviderRegistry.register(selector, provider));
|
||||
}
|
||||
|
||||
// --- formatting
|
||||
|
||||
$registerDocumentFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void {
|
||||
this._registrations.set(handle, modes.DocumentFormattingEditProviderRegistry.register(selector, <modes.DocumentFormattingEditProvider>{
|
||||
extensionId,
|
||||
displayName,
|
||||
provideDocumentFormattingEdits: (model: ITextModel, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined> => {
|
||||
return this._proxy.$provideDocumentFormattingEdits(handle, model.uri, options, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
$registerRangeFormattingSupport(handle: number, selector: IDocumentFilterDto[], extensionId: ExtensionIdentifier, displayName: string): void {
|
||||
this._registrations.set(handle, modes.DocumentRangeFormattingEditProviderRegistry.register(selector, <modes.DocumentRangeFormattingEditProvider>{
|
||||
extensionId,
|
||||
displayName,
|
||||
provideDocumentRangeFormattingEdits: (model: ITextModel, range: EditorRange, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined> => {
|
||||
return this._proxy.$provideDocumentRangeFormattingEdits(handle, model.uri, range, options, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
$registerOnTypeFormattingSupport(handle: number, selector: IDocumentFilterDto[], autoFormatTriggerCharacters: string[], extensionId: ExtensionIdentifier): void {
|
||||
this._registrations.set(handle, modes.OnTypeFormattingEditProviderRegistry.register(selector, <modes.OnTypeFormattingEditProvider>{
|
||||
extensionId,
|
||||
autoFormatTriggerCharacters,
|
||||
provideOnTypeFormattingEdits: (model: ITextModel, position: EditorPosition, ch: string, options: modes.FormattingOptions, token: CancellationToken): Promise<ISingleEditOperation[] | undefined> => {
|
||||
return this._proxy.$provideOnTypeFormattingEdits(handle, model.uri, position, ch, options, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- navigate type
|
||||
|
||||
$registerNavigateTypeSupport(handle: number): void {
|
||||
let lastResultId: number | undefined;
|
||||
this._registrations.set(handle, search.WorkspaceSymbolProviderRegistry.register(<search.IWorkspaceSymbolProvider>{
|
||||
provideWorkspaceSymbols: (search: string, token: CancellationToken): Promise<search.IWorkspaceSymbol[]> => {
|
||||
return this._proxy.$provideWorkspaceSymbols(handle, search, token).then(result => {
|
||||
if (lastResultId !== undefined) {
|
||||
this._proxy.$releaseWorkspaceSymbols(handle, lastResultId);
|
||||
}
|
||||
lastResultId = result._id;
|
||||
return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(result.symbols);
|
||||
});
|
||||
},
|
||||
resolveWorkspaceSymbol: (item: search.IWorkspaceSymbol, token: CancellationToken): Promise<search.IWorkspaceSymbol | undefined> => {
|
||||
return this._proxy.$resolveWorkspaceSymbol(handle, item, token).then(i => {
|
||||
if (i) {
|
||||
return MainThreadLanguageFeatures._reviveWorkspaceSymbolDto(i);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- rename
|
||||
|
||||
$registerRenameSupport(handle: number, selector: IDocumentFilterDto[], supportResolveLocation: boolean): void {
|
||||
this._registrations.set(handle, modes.RenameProviderRegistry.register(selector, <modes.RenameProvider>{
|
||||
provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken) => {
|
||||
return this._proxy.$provideRenameEdits(handle, model.uri, position, newName, token).then(reviveWorkspaceEditDto);
|
||||
},
|
||||
resolveRenameLocation: supportResolveLocation
|
||||
? (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<modes.RenameLocation | undefined> => this._proxy.$resolveRenameLocation(handle, model.uri, position, token)
|
||||
: undefined
|
||||
}));
|
||||
}
|
||||
|
||||
// --- semantic tokens
|
||||
|
||||
$registerDocumentSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend, eventHandle: number | undefined): void {
|
||||
let event: Event<void> | undefined = undefined;
|
||||
if (typeof eventHandle === 'number') {
|
||||
const emitter = new Emitter<void>();
|
||||
this._registrations.set(eventHandle, emitter);
|
||||
event = emitter.event;
|
||||
}
|
||||
this._registrations.set(handle, modes.DocumentSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentSemanticTokensProvider(this._proxy, handle, legend, event)));
|
||||
}
|
||||
|
||||
$emitDocumentSemanticTokensEvent(eventHandle: number): void {
|
||||
const obj = this._registrations.get(eventHandle);
|
||||
if (obj instanceof Emitter) {
|
||||
obj.fire(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
$registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void {
|
||||
this._registrations.set(handle, modes.DocumentRangeSemanticTokensProviderRegistry.register(selector, new MainThreadDocumentRangeSemanticTokensProvider(this._proxy, handle, legend)));
|
||||
}
|
||||
|
||||
// --- suggest
|
||||
|
||||
private static _inflateSuggestDto(defaultRange: IRange | { insert: IRange, replace: IRange }, data: ISuggestDataDto): modes.CompletionItem {
|
||||
|
||||
return {
|
||||
label: data[ISuggestDataDtoField.label2] ?? data[ISuggestDataDtoField.label],
|
||||
kind: data[ISuggestDataDtoField.kind] ?? modes.CompletionItemKind.Property,
|
||||
tags: data[ISuggestDataDtoField.kindModifier],
|
||||
detail: data[ISuggestDataDtoField.detail],
|
||||
documentation: data[ISuggestDataDtoField.documentation],
|
||||
sortText: data[ISuggestDataDtoField.sortText],
|
||||
filterText: data[ISuggestDataDtoField.filterText],
|
||||
preselect: data[ISuggestDataDtoField.preselect],
|
||||
insertText: typeof data.h === 'undefined' ? data[ISuggestDataDtoField.label] : data.h,
|
||||
range: data[ISuggestDataDtoField.range] ?? defaultRange,
|
||||
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
|
||||
commitCharacters: data[ISuggestDataDtoField.commitCharacters],
|
||||
additionalTextEdits: data[ISuggestDataDtoField.additionalTextEdits],
|
||||
command: data[ISuggestDataDtoField.command],
|
||||
// not-standard
|
||||
_id: data.x,
|
||||
};
|
||||
}
|
||||
|
||||
$registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void {
|
||||
const provider: modes.CompletionItemProvider = {
|
||||
triggerCharacters,
|
||||
_debugDisplayName: displayName,
|
||||
provideCompletionItems: async (model: ITextModel, position: EditorPosition, context: modes.CompletionContext, token: CancellationToken): Promise<modes.CompletionList | undefined> => {
|
||||
const result = await this._proxy.$provideCompletionItems(handle, model.uri, position, context, token);
|
||||
if (!result) {
|
||||
return result;
|
||||
}
|
||||
return {
|
||||
suggestions: result[ISuggestResultDtoField.completions].map(d => MainThreadLanguageFeatures._inflateSuggestDto(result[ISuggestResultDtoField.defaultRanges], d)),
|
||||
incomplete: result[ISuggestResultDtoField.isIncomplete] || false,
|
||||
duration: result[ISuggestResultDtoField.duration],
|
||||
dispose: () => {
|
||||
if (typeof result.x === 'number') {
|
||||
this._proxy.$releaseCompletionItems(handle, result.x);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
if (supportsResolveDetails) {
|
||||
provider.resolveCompletionItem = (suggestion, token) => {
|
||||
return this._proxy.$resolveCompletionItem(handle, suggestion._id!, token).then(result => {
|
||||
if (!result) {
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
let newSuggestion = MainThreadLanguageFeatures._inflateSuggestDto(suggestion.range, result);
|
||||
return mixin(suggestion, newSuggestion, true);
|
||||
});
|
||||
};
|
||||
}
|
||||
this._registrations.set(handle, modes.CompletionProviderRegistry.register(selector, provider));
|
||||
}
|
||||
|
||||
// --- parameter hints
|
||||
|
||||
$registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void {
|
||||
this._registrations.set(handle, modes.SignatureHelpProviderRegistry.register(selector, <modes.SignatureHelpProvider>{
|
||||
|
||||
signatureHelpTriggerCharacters: metadata.triggerCharacters,
|
||||
signatureHelpRetriggerCharacters: metadata.retriggerCharacters,
|
||||
|
||||
provideSignatureHelp: async (model: ITextModel, position: EditorPosition, token: CancellationToken, context: modes.SignatureHelpContext): Promise<modes.SignatureHelpResult | undefined> => {
|
||||
const result = await this._proxy.$provideSignatureHelp(handle, model.uri, position, context, token);
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
value: result,
|
||||
dispose: () => {
|
||||
this._proxy.$releaseSignatureHelp(handle, result.id);
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- links
|
||||
|
||||
$registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void {
|
||||
const provider: modes.LinkProvider = {
|
||||
provideLinks: (model, token) => {
|
||||
return this._proxy.$provideDocumentLinks(handle, model.uri, token).then(dto => {
|
||||
if (!dto) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
links: dto.links.map(MainThreadLanguageFeatures._reviveLinkDTO),
|
||||
dispose: () => {
|
||||
if (typeof dto.id === 'number') {
|
||||
this._proxy.$releaseDocumentLinks(handle, dto.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
if (supportsResolve) {
|
||||
provider.resolveLink = (link, token) => {
|
||||
const dto: ILinkDto = link;
|
||||
if (!dto.cacheId) {
|
||||
return link;
|
||||
}
|
||||
return this._proxy.$resolveDocumentLink(handle, dto.cacheId, token).then(obj => {
|
||||
return obj && MainThreadLanguageFeatures._reviveLinkDTO(obj);
|
||||
});
|
||||
};
|
||||
}
|
||||
this._registrations.set(handle, modes.LinkProviderRegistry.register(selector, provider));
|
||||
}
|
||||
|
||||
// --- colors
|
||||
|
||||
$registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
const proxy = this._proxy;
|
||||
this._registrations.set(handle, modes.ColorProviderRegistry.register(selector, <modes.DocumentColorProvider>{
|
||||
provideDocumentColors: (model, token) => {
|
||||
return proxy.$provideDocumentColors(handle, model.uri, token)
|
||||
.then(documentColors => {
|
||||
return documentColors.map(documentColor => {
|
||||
const [red, green, blue, alpha] = documentColor.color;
|
||||
const color = {
|
||||
red: red,
|
||||
green: green,
|
||||
blue: blue,
|
||||
alpha
|
||||
};
|
||||
|
||||
return {
|
||||
color,
|
||||
range: documentColor.range
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
provideColorPresentations: (model, colorInfo, token) => {
|
||||
return proxy.$provideColorPresentations(handle, model.uri, {
|
||||
color: [colorInfo.color.red, colorInfo.color.green, colorInfo.color.blue, colorInfo.color.alpha],
|
||||
range: colorInfo.range
|
||||
}, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- folding
|
||||
|
||||
$registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void {
|
||||
const provider = <modes.FoldingRangeProvider>{
|
||||
provideFoldingRanges: (model, context, token) => {
|
||||
return this._proxy.$provideFoldingRanges(handle, model.uri, context, token);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof eventHandle === 'number') {
|
||||
const emitter = new Emitter<modes.FoldingRangeProvider>();
|
||||
this._registrations.set(eventHandle, emitter);
|
||||
provider.onDidChange = emitter.event;
|
||||
}
|
||||
|
||||
this._registrations.set(handle, modes.FoldingRangeProviderRegistry.register(selector, provider));
|
||||
}
|
||||
|
||||
$emitFoldingRangeEvent(eventHandle: number, event?: any): void {
|
||||
const obj = this._registrations.get(eventHandle);
|
||||
if (obj instanceof Emitter) {
|
||||
obj.fire(event);
|
||||
}
|
||||
}
|
||||
|
||||
// -- smart select
|
||||
|
||||
$registerSelectionRangeProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, modes.SelectionRangeRegistry.register(selector, {
|
||||
provideSelectionRanges: (model, positions, token) => {
|
||||
return this._proxy.$provideSelectionRanges(handle, model.uri, positions, token);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- call hierarchy
|
||||
|
||||
$registerCallHierarchyProvider(handle: number, selector: IDocumentFilterDto[]): void {
|
||||
this._registrations.set(handle, callh.CallHierarchyProviderRegistry.register(selector, {
|
||||
|
||||
prepareCallHierarchy: async (document, position, token) => {
|
||||
const items = await this._proxy.$prepareCallHierarchy(handle, document.uri, position, token);
|
||||
if (!items) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
dispose: () => {
|
||||
for (const item of items) {
|
||||
this._proxy.$releaseCallHierarchy(handle, item._sessionId);
|
||||
}
|
||||
},
|
||||
roots: items.map(MainThreadLanguageFeatures._reviveCallHierarchyItemDto)
|
||||
};
|
||||
},
|
||||
|
||||
provideOutgoingCalls: async (item, token) => {
|
||||
const outgoing = await this._proxy.$provideCallHierarchyOutgoingCalls(handle, item._sessionId, item._itemId, token);
|
||||
if (!outgoing) {
|
||||
return outgoing;
|
||||
}
|
||||
outgoing.forEach(value => {
|
||||
value.to = MainThreadLanguageFeatures._reviveCallHierarchyItemDto(value.to);
|
||||
});
|
||||
return <any>outgoing;
|
||||
},
|
||||
provideIncomingCalls: async (item, token) => {
|
||||
const incoming = await this._proxy.$provideCallHierarchyIncomingCalls(handle, item._sessionId, item._itemId, token);
|
||||
if (!incoming) {
|
||||
return incoming;
|
||||
}
|
||||
incoming.forEach(value => {
|
||||
value.from = MainThreadLanguageFeatures._reviveCallHierarchyItemDto(value.from);
|
||||
});
|
||||
return <any>incoming;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// --- configuration
|
||||
|
||||
private static _reviveRegExp(regExp: IRegExpDto): RegExp {
|
||||
return new RegExp(regExp.pattern, regExp.flags);
|
||||
}
|
||||
|
||||
private static _reviveIndentationRule(indentationRule: IIndentationRuleDto): IndentationRule {
|
||||
return {
|
||||
decreaseIndentPattern: MainThreadLanguageFeatures._reviveRegExp(indentationRule.decreaseIndentPattern),
|
||||
increaseIndentPattern: MainThreadLanguageFeatures._reviveRegExp(indentationRule.increaseIndentPattern),
|
||||
indentNextLinePattern: indentationRule.indentNextLinePattern ? MainThreadLanguageFeatures._reviveRegExp(indentationRule.indentNextLinePattern) : undefined,
|
||||
unIndentedLinePattern: indentationRule.unIndentedLinePattern ? MainThreadLanguageFeatures._reviveRegExp(indentationRule.unIndentedLinePattern) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private static _reviveOnEnterRule(onEnterRule: IOnEnterRuleDto): OnEnterRule {
|
||||
return {
|
||||
beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText),
|
||||
afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined,
|
||||
oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined,
|
||||
action: onEnterRule.action
|
||||
};
|
||||
}
|
||||
|
||||
private static _reviveOnEnterRules(onEnterRules: IOnEnterRuleDto[]): OnEnterRule[] {
|
||||
return onEnterRules.map(MainThreadLanguageFeatures._reviveOnEnterRule);
|
||||
}
|
||||
|
||||
$setLanguageConfiguration(handle: number, languageId: string, _configuration: ILanguageConfigurationDto): void {
|
||||
|
||||
const configuration: LanguageConfiguration = {
|
||||
comments: _configuration.comments,
|
||||
brackets: _configuration.brackets,
|
||||
wordPattern: _configuration.wordPattern ? MainThreadLanguageFeatures._reviveRegExp(_configuration.wordPattern) : undefined,
|
||||
indentationRules: _configuration.indentationRules ? MainThreadLanguageFeatures._reviveIndentationRule(_configuration.indentationRules) : undefined,
|
||||
onEnterRules: _configuration.onEnterRules ? MainThreadLanguageFeatures._reviveOnEnterRules(_configuration.onEnterRules) : undefined,
|
||||
|
||||
autoClosingPairs: undefined,
|
||||
surroundingPairs: undefined,
|
||||
__electricCharacterSupport: undefined
|
||||
};
|
||||
|
||||
if (_configuration.__characterPairSupport) {
|
||||
// backwards compatibility
|
||||
configuration.autoClosingPairs = _configuration.__characterPairSupport.autoClosingPairs;
|
||||
}
|
||||
|
||||
if (_configuration.__electricCharacterSupport && _configuration.__electricCharacterSupport.docComment) {
|
||||
configuration.__electricCharacterSupport = {
|
||||
docComment: {
|
||||
open: _configuration.__electricCharacterSupport.docComment.open,
|
||||
close: _configuration.__electricCharacterSupport.docComment.close
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
|
||||
if (languageIdentifier) {
|
||||
this._registrations.set(handle, LanguageConfigurationRegistry.register(languageIdentifier, configuration));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class MainThreadDocumentSemanticTokensProvider implements modes.DocumentSemanticTokensProvider {
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostLanguageFeaturesShape,
|
||||
private readonly _handle: number,
|
||||
private readonly _legend: modes.SemanticTokensLegend,
|
||||
public readonly onDidChange: Event<void> | undefined,
|
||||
) {
|
||||
}
|
||||
|
||||
public releaseDocumentSemanticTokens(resultId: string | undefined): void {
|
||||
if (resultId) {
|
||||
this._proxy.$releaseDocumentSemanticTokens(this._handle, parseInt(resultId, 10));
|
||||
}
|
||||
}
|
||||
|
||||
public getLegend(): modes.SemanticTokensLegend {
|
||||
return this._legend;
|
||||
}
|
||||
|
||||
async provideDocumentSemanticTokens(model: ITextModel, lastResultId: string | null, token: CancellationToken): Promise<modes.SemanticTokens | modes.SemanticTokensEdits | null> {
|
||||
const nLastResultId = lastResultId ? parseInt(lastResultId, 10) : 0;
|
||||
const encodedDto = await this._proxy.$provideDocumentSemanticTokens(this._handle, model.uri, nLastResultId, token);
|
||||
if (!encodedDto) {
|
||||
return null;
|
||||
}
|
||||
if (token.isCancellationRequested) {
|
||||
return null;
|
||||
}
|
||||
const dto = decodeSemanticTokensDto(encodedDto);
|
||||
if (dto.type === 'full') {
|
||||
return {
|
||||
resultId: String(dto.id),
|
||||
data: dto.data
|
||||
};
|
||||
}
|
||||
return {
|
||||
resultId: String(dto.id),
|
||||
edits: dto.deltas
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadDocumentRangeSemanticTokensProvider implements modes.DocumentRangeSemanticTokensProvider {
|
||||
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostLanguageFeaturesShape,
|
||||
private readonly _handle: number,
|
||||
private readonly _legend: modes.SemanticTokensLegend,
|
||||
) {
|
||||
}
|
||||
|
||||
public getLegend(): modes.SemanticTokensLegend {
|
||||
return this._legend;
|
||||
}
|
||||
|
||||
async provideDocumentRangeSemanticTokens(model: ITextModel, range: EditorRange, token: CancellationToken): Promise<modes.SemanticTokens | null> {
|
||||
const encodedDto = await this._proxy.$provideDocumentRangeSemanticTokens(this._handle, model.uri, range, token);
|
||||
if (!encodedDto) {
|
||||
return null;
|
||||
}
|
||||
if (token.isCancellationRequested) {
|
||||
return null;
|
||||
}
|
||||
const dto = decodeSemanticTokensDto(encodedDto);
|
||||
if (dto.type === 'full') {
|
||||
return {
|
||||
resultId: String(dto.id),
|
||||
data: dto.data
|
||||
};
|
||||
}
|
||||
throw new Error(`Unexpected`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { MainThreadLanguagesShape, MainContext, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { StandardTokenType } from 'vs/editor/common/modes';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadLanguages)
|
||||
export class MainThreadLanguages implements MainThreadLanguagesShape {
|
||||
|
||||
constructor(
|
||||
_extHostContext: IExtHostContext,
|
||||
@IModeService private readonly _modeService: IModeService,
|
||||
@IModelService private readonly _modelService: IModelService
|
||||
) {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// nothing
|
||||
}
|
||||
|
||||
$getLanguages(): Promise<string[]> {
|
||||
return Promise.resolve(this._modeService.getRegisteredModes());
|
||||
}
|
||||
|
||||
$changeLanguage(resource: UriComponents, languageId: string): Promise<void> {
|
||||
const uri = URI.revive(resource);
|
||||
const model = this._modelService.getModel(uri);
|
||||
if (!model) {
|
||||
return Promise.reject(new Error('Invalid uri'));
|
||||
}
|
||||
const languageIdentifier = this._modeService.getLanguageIdentifier(languageId);
|
||||
if (!languageIdentifier || languageIdentifier.language !== languageId) {
|
||||
return Promise.reject(new Error(`Unknown language id: ${languageId}`));
|
||||
}
|
||||
this._modelService.setMode(model, this._modeService.create(languageId));
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async $tokensAtPosition(resource: UriComponents, position: IPosition): Promise<undefined | { type: StandardTokenType, range: IRange }> {
|
||||
const uri = URI.revive(resource);
|
||||
const model = this._modelService.getModel(uri);
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
model.tokenizeIfCheap(position.lineNumber);
|
||||
const tokens = model.getLineTokens(position.lineNumber);
|
||||
const idx = tokens.findTokenIndexAtOffset(position.column - 1);
|
||||
return {
|
||||
type: tokens.getStandardTokenType(idx),
|
||||
range: new Range(position.lineNumber, 1 + tokens.getStartOffset(idx), position.lineNumber, 1 + tokens.getEndOffset(idx))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtHostContext, ExtHostContext, MainThreadLogShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { UriComponents, URI } from 'vs/base/common/uri';
|
||||
import { FileLogService } from 'vs/platform/log/common/fileLogService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadLog)
|
||||
export class MainThreadLogService implements MainThreadLogShape {
|
||||
|
||||
private readonly _loggers = new Map<string, FileLogService>();
|
||||
private readonly _logListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@IInstantiationService private readonly _instaService: IInstantiationService,
|
||||
) {
|
||||
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostLogService);
|
||||
this._logListener = _logService.onDidChangeLogLevel(level => {
|
||||
proxy.$setLevel(level);
|
||||
this._loggers.forEach(value => value.setLevel(level));
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._logListener.dispose();
|
||||
this._loggers.forEach(value => value.dispose());
|
||||
this._loggers.clear();
|
||||
}
|
||||
|
||||
$log(file: UriComponents, level: LogLevel, message: any[]): void {
|
||||
const uri = URI.revive(file);
|
||||
let logger = this._loggers.get(uri.toString());
|
||||
if (!logger) {
|
||||
logger = this._instaService.createInstance(FileLogService, basename(file.path), URI.revive(file), this._logService.getLevel());
|
||||
this._loggers.set(uri.toString(), logger);
|
||||
}
|
||||
logger.log(level, message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { Action, IAction } from 'vs/base/common/actions';
|
||||
import { MainThreadMessageServiceShape, MainContext, IExtHostContext, MainThreadMessageOptions } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadMessageService)
|
||||
export class MainThreadMessageService implements MainThreadMessageServiceShape {
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@IDialogService private readonly _dialogService: IDialogService
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
|
||||
$showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise<number | undefined> {
|
||||
if (options.modal) {
|
||||
return this._showModalMessage(severity, message, commands);
|
||||
} else {
|
||||
return this._showMessage(severity, message, commands, options.extension);
|
||||
}
|
||||
}
|
||||
|
||||
private _showMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], extension: IExtensionDescription | undefined): Promise<number | undefined> {
|
||||
|
||||
return new Promise<number | undefined>(resolve => {
|
||||
|
||||
const primaryActions: MessageItemAction[] = [];
|
||||
|
||||
class MessageItemAction extends Action {
|
||||
constructor(id: string, label: string, handle: number) {
|
||||
super(id, label, undefined, true, () => {
|
||||
resolve(handle);
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ManageExtensionAction extends Action {
|
||||
constructor(id: ExtensionIdentifier, label: string, commandService: ICommandService) {
|
||||
super(id.value, label, undefined, true, () => {
|
||||
return commandService.executeCommand('_extensions.manage', id.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
commands.forEach(command => {
|
||||
primaryActions.push(new MessageItemAction('_extension_message_handle_' + command.handle, command.title, command.handle));
|
||||
});
|
||||
|
||||
let source: string | undefined;
|
||||
if (extension) {
|
||||
source = nls.localize('extensionSource', "{0} (Extension)", extension.displayName || extension.name);
|
||||
}
|
||||
|
||||
if (!source) {
|
||||
source = nls.localize('defaultSource', "Extension");
|
||||
}
|
||||
|
||||
const secondaryActions: IAction[] = [];
|
||||
if (extension && !extension.isUnderDevelopment) {
|
||||
secondaryActions.push(new ManageExtensionAction(extension.identifier, nls.localize('manageExtension', "Manage Extension"), this._commandService));
|
||||
}
|
||||
|
||||
const messageHandle = this._notificationService.notify({
|
||||
severity,
|
||||
message,
|
||||
actions: { primary: primaryActions, secondary: secondaryActions },
|
||||
source
|
||||
});
|
||||
|
||||
// if promise has not been resolved yet, now is the time to ensure a return value
|
||||
// otherwise if already resolved it means the user clicked one of the buttons
|
||||
Event.once(messageHandle.onDidClose)(() => {
|
||||
dispose(primaryActions);
|
||||
dispose(secondaryActions);
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise<number | undefined> {
|
||||
let cancelId: number | undefined = undefined;
|
||||
|
||||
const buttons = commands.map((command, index) => {
|
||||
if (command.isCloseAffordance === true) {
|
||||
cancelId = index;
|
||||
}
|
||||
|
||||
return command.title;
|
||||
});
|
||||
|
||||
if (cancelId === undefined) {
|
||||
if (buttons.length > 0) {
|
||||
buttons.push(nls.localize('cancel', "Cancel"));
|
||||
} else {
|
||||
buttons.push(nls.localize('ok', "OK"));
|
||||
}
|
||||
|
||||
cancelId = buttons.length - 1;
|
||||
}
|
||||
|
||||
const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId });
|
||||
return choice === commands.length ? undefined : commands[choice].handle;
|
||||
}
|
||||
}
|
||||
771
lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts
Normal file
771
lib/vscode/src/vs/workbench/api/browser/mainThreadNotebook.ts
Normal file
@@ -0,0 +1,771 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IRelativePattern } from 'vs/base/common/glob';
|
||||
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IExtUri } from 'vs/base/common/resources';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService';
|
||||
import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, IMainCellDto, INotebookDecorationRenderOptions, INotebookDocumentFilter, INotebookEditorModel, INotebookExclusiveDocumentFilter, NotebookCellOutputsSplice, NotebookCellsChangeType, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService';
|
||||
import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol';
|
||||
|
||||
class DocumentAndEditorState {
|
||||
static 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 };
|
||||
}
|
||||
|
||||
static 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 };
|
||||
}
|
||||
|
||||
static compute(before: DocumentAndEditorState | undefined, after: DocumentAndEditorState): INotebookDocumentsAndEditorsDelta {
|
||||
if (!before) {
|
||||
const apiEditors = [];
|
||||
for (let id in after.textEditors) {
|
||||
const editor = after.textEditors.get(id)!;
|
||||
apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.getSelectionHandles(), visibleRanges: editor.visibleRanges });
|
||||
}
|
||||
|
||||
return {
|
||||
addedDocuments: [],
|
||||
addedEditors: apiEditors,
|
||||
visibleEditors: [...after.visibleEditors].map(editor => editor[0])
|
||||
};
|
||||
}
|
||||
const documentDelta = DocumentAndEditorState.ofSets(before.documents, after.documents);
|
||||
const editorDelta = DocumentAndEditorState.ofMaps(before.textEditors, after.textEditors);
|
||||
const addedAPIEditors = editorDelta.added.map(add => ({
|
||||
id: add.getId(),
|
||||
documentUri: add.uri!,
|
||||
selections: add.getSelectionHandles(),
|
||||
visibleRanges: add.visibleRanges
|
||||
}));
|
||||
|
||||
const removedAPIEditors = editorDelta.removed.map(removed => removed.getId());
|
||||
|
||||
// const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined;
|
||||
const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined;
|
||||
|
||||
const visibleEditorDelta = DocumentAndEditorState.ofMaps(before.visibleEditors, after.visibleEditors);
|
||||
|
||||
return {
|
||||
addedDocuments: documentDelta.added.map((e: NotebookTextModel): INotebookModelAddedData => {
|
||||
return {
|
||||
viewType: e.viewType,
|
||||
uri: e.uri,
|
||||
metadata: e.metadata,
|
||||
versionId: e.versionId,
|
||||
cells: e.cells.map(cell => ({
|
||||
handle: cell.handle,
|
||||
uri: cell.uri,
|
||||
source: cell.textBuffer.getLinesContent(),
|
||||
eol: cell.textBuffer.getEOL(),
|
||||
language: cell.language,
|
||||
cellKind: cell.cellKind,
|
||||
outputs: cell.outputs,
|
||||
metadata: cell.metadata
|
||||
})),
|
||||
contentOptions: e.transientOptions,
|
||||
// attachedEditor: editorId ? {
|
||||
// id: editorId,
|
||||
// selections: document.textModel.selections
|
||||
// } : undefined
|
||||
};
|
||||
}),
|
||||
removedDocuments: documentDelta.removed.map(e => e.uri),
|
||||
addedEditors: addedAPIEditors,
|
||||
removedEditors: removedAPIEditors,
|
||||
newActiveEditor: newActiveEditor,
|
||||
visibleEditors: visibleEditorDelta.added.length === 0 && visibleEditorDelta.removed.length === 0
|
||||
? undefined
|
||||
: [...after.visibleEditors].map(editor => editor[0])
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly documents: Set<NotebookTextModel>,
|
||||
readonly textEditors: Map<string, IEditor>,
|
||||
readonly activeEditor: string | null | undefined,
|
||||
readonly visibleEditors: Map<string, IEditor>
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadNotebook)
|
||||
export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape {
|
||||
private readonly _notebookProviders = new Map<string, { controller: IMainNotebookController, disposable: IDisposable }>();
|
||||
private readonly _notebookKernelProviders = new Map<number, { extension: NotebookExtensionDescription, emitter: Emitter<URI | undefined>, provider: IDisposable }>();
|
||||
private readonly _proxy: ExtHostNotebookShape;
|
||||
private _toDisposeOnEditorRemove = new Map<string, IDisposable>();
|
||||
private _currentState?: DocumentAndEditorState;
|
||||
private _editorEventListenersMapping: Map<string, DisposableStore> = new Map();
|
||||
private _documentEventListenersMapping: ResourceMap<DisposableStore> = new ResourceMap();
|
||||
private readonly _cellStatusBarEntries: Map<number, IDisposable> = new Map();
|
||||
private readonly _modelReferenceCollection: BoundModelReferenceCollection;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@INotebookService private _notebookService: INotebookService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IEditorService private readonly editorService: IEditorService,
|
||||
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService,
|
||||
@IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService,
|
||||
@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,
|
||||
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook);
|
||||
this._modelReferenceCollection = new BoundModelReferenceCollection(this._uriIdentityService.extUri);
|
||||
this._register(this._modelReferenceCollection);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
async $tryApplyEdits(_viewType: string, resource: UriComponents, modelVersionId: number, cellEdits: ICellEditOperation[]): Promise<boolean> {
|
||||
const textModel = this._notebookService.getNotebookTextModel(URI.from(resource));
|
||||
if (!textModel) {
|
||||
return false;
|
||||
}
|
||||
this._notebookService.transformEditsOutputs(textModel, cellEdits);
|
||||
return textModel.applyEdits(modelVersionId, cellEdits, true, undefined, () => undefined, undefined);
|
||||
}
|
||||
|
||||
private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) {
|
||||
if (delta.addedDocuments !== undefined && delta.addedDocuments.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta.removedDocuments !== undefined && delta.removedDocuments.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta.addedEditors !== undefined && delta.addedEditors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta.removedEditors !== undefined && delta.removedEditors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta.visibleEditors !== undefined && delta.visibleEditors.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (delta.newActiveEditor !== undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private _emitDelta(delta: INotebookDocumentsAndEditorsDelta) {
|
||||
if (this._isDeltaEmpty(delta)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this._proxy.$acceptDocumentAndEditorsDelta(delta);
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this._notebookService.listNotebookEditors().forEach((e) => {
|
||||
this._addNotebookEditor(e);
|
||||
});
|
||||
|
||||
this._register(this._notebookService.onDidChangeActiveEditor(e => {
|
||||
this._updateState();
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onDidChangeVisibleEditors(e => {
|
||||
if (this._notebookProviders.size > 0) {
|
||||
if (!this._currentState) {
|
||||
// no current state means we didn't even create editors in ext host yet.
|
||||
return;
|
||||
}
|
||||
|
||||
// we can't simply update visibleEditors as we need to check if we should create editors first.
|
||||
this._updateState();
|
||||
}
|
||||
}));
|
||||
|
||||
const notebookEditorAddedHandler = (editor: IEditor) => {
|
||||
if (!this._editorEventListenersMapping.has(editor.getId())) {
|
||||
const disposableStore = new DisposableStore();
|
||||
disposableStore.add(editor.onDidChangeVisibleRanges(() => {
|
||||
this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges }, selections: null });
|
||||
}));
|
||||
|
||||
disposableStore.add(editor.onDidChangeSelection(() => {
|
||||
const selectionHandles = editor.getSelectionHandles();
|
||||
this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: null, selections: { selections: selectionHandles } });
|
||||
}));
|
||||
|
||||
this._editorEventListenersMapping.set(editor.getId(), disposableStore);
|
||||
}
|
||||
};
|
||||
|
||||
this._register(this._notebookService.onNotebookEditorAdd(editor => {
|
||||
notebookEditorAddedHandler(editor);
|
||||
this._addNotebookEditor(editor);
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onNotebookEditorsRemove(editors => {
|
||||
this._removeNotebookEditor(editors);
|
||||
|
||||
editors.forEach(editor => {
|
||||
this._editorEventListenersMapping.get(editor.getId())?.dispose();
|
||||
this._editorEventListenersMapping.delete(editor.getId());
|
||||
});
|
||||
}));
|
||||
|
||||
this._notebookService.listNotebookEditors().forEach(editor => {
|
||||
notebookEditorAddedHandler(editor);
|
||||
});
|
||||
|
||||
const cellToDto = (cell: NotebookCellTextModel): IMainCellDto => {
|
||||
return {
|
||||
handle: cell.handle,
|
||||
uri: cell.uri,
|
||||
source: cell.textBuffer.getLinesContent(),
|
||||
eol: cell.textBuffer.getEOL(),
|
||||
language: cell.language,
|
||||
cellKind: cell.cellKind,
|
||||
outputs: cell.outputs,
|
||||
metadata: cell.metadata
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const notebookDocumentAddedHandler = (textModel: NotebookTextModel) => {
|
||||
if (!this._documentEventListenersMapping.has(textModel.uri)) {
|
||||
const disposableStore = new DisposableStore();
|
||||
disposableStore.add(textModel!.onDidChangeContent(event => {
|
||||
const dto = event.rawEvents.map(e => {
|
||||
const data =
|
||||
e.kind === NotebookCellsChangeType.ModelChange || e.kind === NotebookCellsChangeType.Initialize
|
||||
? {
|
||||
kind: e.kind,
|
||||
versionId: event.versionId,
|
||||
changes: e.changes.map(diff => [diff[0], diff[1], diff[2].map(cell => cellToDto(cell as NotebookCellTextModel))] as [number, number, IMainCellDto[]])
|
||||
}
|
||||
: (
|
||||
e.kind === NotebookCellsChangeType.Move
|
||||
? {
|
||||
kind: e.kind,
|
||||
index: e.index,
|
||||
length: e.length,
|
||||
newIdx: e.newIdx,
|
||||
versionId: event.versionId,
|
||||
cells: e.cells.map(cell => cellToDto(cell as NotebookCellTextModel))
|
||||
}
|
||||
: e
|
||||
);
|
||||
|
||||
return data;
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO@rebornix, @jrieken
|
||||
* When a document is modified, it will trigger onDidChangeContent events.
|
||||
* The first event listener is this one, which doesn't know if the text model is dirty or not. It can ask `workingCopyService` but get the wrong result
|
||||
* The second event listener is `NotebookEditorModel`, which will then set `isDirty` to `true`.
|
||||
* Since `e.transient` decides if the model should be dirty or not, we will use the same logic here.
|
||||
*/
|
||||
const hasNonTransientEvent = event.rawEvents.find(e => !e.transient);
|
||||
this._proxy.$acceptModelChanged(textModel.uri, {
|
||||
rawEvents: dto,
|
||||
versionId: event.versionId
|
||||
}, !!hasNonTransientEvent);
|
||||
|
||||
const hasDocumentMetadataChangeEvent = event.rawEvents.find(e => e.kind === NotebookCellsChangeType.ChangeDocumentMetadata);
|
||||
if (!!hasDocumentMetadataChangeEvent) {
|
||||
this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { metadata: textModel.metadata });
|
||||
}
|
||||
}));
|
||||
this._documentEventListenersMapping.set(textModel!.uri, disposableStore);
|
||||
}
|
||||
};
|
||||
|
||||
this._notebookService.listNotebookDocuments().forEach(notebookDocumentAddedHandler);
|
||||
this._register(this._notebookService.onDidAddNotebookDocument(document => {
|
||||
notebookDocumentAddedHandler(document);
|
||||
this._updateState();
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onDidRemoveNotebookDocument(uri => {
|
||||
this._documentEventListenersMapping.get(uri)?.dispose();
|
||||
this._documentEventListenersMapping.delete(uri);
|
||||
this._updateState();
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onDidChangeNotebookActiveKernel(e => {
|
||||
this._proxy.$acceptNotebookActiveKernelChange(e);
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onNotebookDocumentSaved(e => {
|
||||
this._proxy.$acceptModelSaved(e);
|
||||
}));
|
||||
|
||||
const updateOrder = () => {
|
||||
let userOrder = this.configurationService.getValue<string[]>(DisplayOrderKey);
|
||||
this._proxy.$acceptDisplayOrder({
|
||||
defaultOrder: this.accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER,
|
||||
userOrder: userOrder
|
||||
});
|
||||
};
|
||||
|
||||
updateOrder();
|
||||
|
||||
this._register(this.configurationService.onDidChangeConfiguration(e => {
|
||||
if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) {
|
||||
updateOrder();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => {
|
||||
updateOrder();
|
||||
}));
|
||||
|
||||
const activeEditorPane = this.editorService.activeEditorPane as any | undefined;
|
||||
const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined;
|
||||
this._updateState(notebookEditor);
|
||||
}
|
||||
|
||||
private _addNotebookEditor(e: IEditor) {
|
||||
this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable(
|
||||
e.onDidChangeModel(() => this._updateState()),
|
||||
e.onDidFocusEditorWidget(() => {
|
||||
this._updateState(e);
|
||||
}),
|
||||
));
|
||||
|
||||
const activeEditorPane = this.editorService.activeEditorPane as any | undefined;
|
||||
const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined;
|
||||
this._updateState(notebookEditor);
|
||||
}
|
||||
|
||||
private _removeNotebookEditor(editors: IEditor[]) {
|
||||
editors.forEach(e => {
|
||||
const sub = this._toDisposeOnEditorRemove.get(e.getId());
|
||||
if (sub) {
|
||||
this._toDisposeOnEditorRemove.delete(e.getId());
|
||||
sub.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
this._updateState();
|
||||
}
|
||||
|
||||
private async _updateState(focusedNotebookEditor?: IEditor) {
|
||||
let activeEditor: string | null = null;
|
||||
|
||||
const activeEditorPane = this.editorService.activeEditorPane as any | undefined;
|
||||
if (activeEditorPane?.isNotebookEditor) {
|
||||
const notebookEditor = (activeEditorPane.getControl() as INotebookEditor);
|
||||
activeEditor = notebookEditor && notebookEditor.hasModel() ? notebookEditor!.getId() : null;
|
||||
}
|
||||
|
||||
const documentEditorsMap = new Map<string, IEditor>();
|
||||
|
||||
const editors = new Map<string, IEditor>();
|
||||
this._notebookService.listNotebookEditors().forEach(editor => {
|
||||
if (editor.hasModel()) {
|
||||
editors.set(editor.getId(), editor);
|
||||
documentEditorsMap.set(editor.textModel!.uri.toString(), editor);
|
||||
}
|
||||
});
|
||||
|
||||
const visibleEditorsMap = new Map<string, IEditor>();
|
||||
this.editorService.visibleEditorPanes.forEach(editor => {
|
||||
if ((editor as any).isNotebookEditor) {
|
||||
const nbEditorWidget = (editor as any).getControl() as INotebookEditor;
|
||||
if (nbEditorWidget && editors.has(nbEditorWidget.getId())) {
|
||||
visibleEditorsMap.set(nbEditorWidget.getId(), nbEditorWidget);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const documents = new Set<NotebookTextModel>();
|
||||
this._notebookService.listNotebookDocuments().forEach(document => {
|
||||
documents.add(document);
|
||||
});
|
||||
|
||||
if (!activeEditor && focusedNotebookEditor && focusedNotebookEditor.hasModel()) {
|
||||
activeEditor = focusedNotebookEditor.getId();
|
||||
}
|
||||
|
||||
// editors always have view model attached, which means there is already a document in exthost.
|
||||
const newState = new DocumentAndEditorState(documents, editors, activeEditor, visibleEditorsMap);
|
||||
const delta = DocumentAndEditorState.compute(this._currentState, newState);
|
||||
// const isEmptyChange = (!delta.addedDocuments || delta.addedDocuments.length === 0)
|
||||
// && (!delta.removedDocuments || delta.removedDocuments.length === 0)
|
||||
// && (!delta.addedEditors || delta.addedEditors.length === 0)
|
||||
// && (!delta.removedEditors || delta.removedEditors.length === 0)
|
||||
// && (delta.newActiveEditor === undefined)
|
||||
|
||||
// if (!isEmptyChange) {
|
||||
this._currentState = newState;
|
||||
await this._emitDelta(delta);
|
||||
// }
|
||||
}
|
||||
|
||||
async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, options: {
|
||||
transientOutputs: boolean;
|
||||
transientMetadata: TransientMetadata;
|
||||
viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; };
|
||||
}): Promise<void> {
|
||||
let contentOptions = { transientOutputs: options.transientOutputs, transientMetadata: options.transientMetadata };
|
||||
|
||||
const controller: IMainNotebookController = {
|
||||
supportBackup,
|
||||
get options() {
|
||||
return contentOptions;
|
||||
},
|
||||
set options(newOptions) {
|
||||
contentOptions.transientMetadata = newOptions.transientMetadata;
|
||||
contentOptions.transientOutputs = newOptions.transientOutputs;
|
||||
},
|
||||
viewOptions: options.viewOptions,
|
||||
reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => {
|
||||
const data = await this._proxy.$resolveNotebookData(viewType, mainthreadTextModel.uri);
|
||||
mainthreadTextModel.updateLanguages(data.languages);
|
||||
mainthreadTextModel.metadata = data.metadata;
|
||||
mainthreadTextModel.transientOptions = contentOptions;
|
||||
|
||||
const edits: ICellEditOperation[] = [
|
||||
{ editType: CellEditType.Replace, index: 0, count: mainthreadTextModel.cells.length, cells: data.cells }
|
||||
];
|
||||
|
||||
this._notebookService.transformEditsOutputs(mainthreadTextModel, edits);
|
||||
await new Promise(resolve => {
|
||||
DOM.scheduleAtNextAnimationFrame(() => {
|
||||
const ret = mainthreadTextModel!.applyEdits(mainthreadTextModel!.versionId, edits, true, undefined, () => undefined, undefined);
|
||||
resolve(ret);
|
||||
});
|
||||
});
|
||||
},
|
||||
resolveNotebookDocument: async (viewType: string, uri: URI, backupId?: string) => {
|
||||
const data = await this._proxy.$resolveNotebookData(viewType, uri, backupId);
|
||||
return {
|
||||
data,
|
||||
transientOptions: contentOptions
|
||||
};
|
||||
},
|
||||
resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => {
|
||||
await this._proxy.$resolveNotebookEditor(viewType, uri, editorId);
|
||||
},
|
||||
onDidReceiveMessage: (editorId: string, rendererType: string | undefined, message: unknown) => {
|
||||
this._proxy.$onDidReceiveMessage(editorId, rendererType, message);
|
||||
},
|
||||
save: async (uri: URI, token: CancellationToken) => {
|
||||
return this._proxy.$saveNotebook(viewType, uri, token);
|
||||
},
|
||||
saveAs: async (uri: URI, target: URI, token: CancellationToken) => {
|
||||
return this._proxy.$saveNotebookAs(viewType, uri, target, token);
|
||||
},
|
||||
backup: async (uri: URI, token: CancellationToken) => {
|
||||
return this._proxy.$backup(viewType, uri, token);
|
||||
}
|
||||
};
|
||||
|
||||
const disposable = this._notebookService.registerNotebookController(viewType, extension, controller);
|
||||
this._notebookProviders.set(viewType, { controller, disposable });
|
||||
return;
|
||||
}
|
||||
|
||||
async $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientMetadata: TransientMetadata; }): Promise<void> {
|
||||
const provider = this._notebookProviders.get(viewType);
|
||||
|
||||
if (provider && options) {
|
||||
provider.controller.options = options;
|
||||
this._notebookService.listNotebookDocuments().forEach(document => {
|
||||
if (document.viewType === viewType) {
|
||||
document.transientOptions = provider.controller.options;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async $unregisterNotebookProvider(viewType: string): Promise<void> {
|
||||
const entry = this._notebookProviders.get(viewType);
|
||||
if (entry) {
|
||||
entry.disposable.dispose();
|
||||
this._notebookProviders.delete(viewType);
|
||||
}
|
||||
}
|
||||
|
||||
async $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise<void> {
|
||||
const emitter = new Emitter<URI | undefined>();
|
||||
const that = this;
|
||||
const provider = this._notebookService.registerNotebookKernelProvider({
|
||||
providerExtensionId: extension.id.value,
|
||||
providerDescription: extension.description,
|
||||
onDidChangeKernels: emitter.event,
|
||||
selector: documentFilter,
|
||||
provideKernels: async (uri: URI, token: CancellationToken) => {
|
||||
const kernels = await that._proxy.$provideNotebookKernels(handle, uri, token);
|
||||
return kernels.map(kernel => {
|
||||
return {
|
||||
...kernel,
|
||||
providerHandle: handle
|
||||
};
|
||||
});
|
||||
},
|
||||
resolveKernel: (editorId: string, uri: URI, kernelId: string, token: CancellationToken) => {
|
||||
return that._proxy.$resolveNotebookKernel(handle, editorId, uri, kernelId, token);
|
||||
},
|
||||
executeNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => {
|
||||
this.logService.debug('MainthreadNotebooks.registerNotebookKernelProvider#executeNotebook', uri.path, kernelId, cellHandle);
|
||||
return that._proxy.$executeNotebookKernelFromProvider(handle, uri, kernelId, cellHandle);
|
||||
},
|
||||
cancelNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => {
|
||||
this.logService.debug('MainthreadNotebooks.registerNotebookKernelProvider#cancelNotebook', uri.path, kernelId, cellHandle);
|
||||
return that._proxy.$cancelNotebookKernelFromProvider(handle, uri, kernelId, cellHandle);
|
||||
},
|
||||
});
|
||||
this._notebookKernelProviders.set(handle, {
|
||||
extension,
|
||||
emitter,
|
||||
provider
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async $unregisterNotebookKernelProvider(handle: number): Promise<void> {
|
||||
const entry = this._notebookKernelProviders.get(handle);
|
||||
|
||||
if (entry) {
|
||||
entry.emitter.dispose();
|
||||
entry.provider.dispose();
|
||||
this._notebookKernelProviders.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
$onNotebookKernelChange(handle: number, uriComponents: UriComponents): void {
|
||||
const entry = this._notebookKernelProviders.get(handle);
|
||||
|
||||
entry?.emitter.fire(uriComponents ? URI.revive(uriComponents) : undefined);
|
||||
}
|
||||
|
||||
async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void> {
|
||||
this.logService.debug('MainThreadNotebooks#updateNotebookLanguages', resource.path, languages);
|
||||
const textModel = this._notebookService.getNotebookTextModel(URI.from(resource));
|
||||
textModel?.updateLanguages(languages);
|
||||
}
|
||||
|
||||
async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise<void> {
|
||||
this.logService.debug('MainThreadNotebooks#spliceNotebookCellOutputs', resource.path, cellHandle);
|
||||
const textModel = this._notebookService.getNotebookTextModel(URI.from(resource));
|
||||
|
||||
if (!textModel) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._notebookService.transformSpliceOutputs(textModel, splices);
|
||||
const cell = textModel.cells.find(cell => cell.handle === cellHandle);
|
||||
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
textModel.applyEdits(textModel.versionId, [
|
||||
{
|
||||
editType: CellEditType.OutputsSplice,
|
||||
index: textModel.cells.indexOf(cell),
|
||||
splices
|
||||
}
|
||||
], true, undefined, () => undefined, undefined);
|
||||
}
|
||||
|
||||
async $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise<boolean> {
|
||||
const editor = this._notebookService.getNotebookEditor(editorId) as INotebookEditor | undefined;
|
||||
if (editor?.isNotebookEditor) {
|
||||
editor.postMessage(forRendererId, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$onUndoableContentChange(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void {
|
||||
const textModel = this._notebookService.getNotebookTextModel(URI.from(resource));
|
||||
|
||||
if (textModel) {
|
||||
textModel.handleUnknownUndoableEdit(label, () => {
|
||||
const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook }));
|
||||
return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, isDirty);
|
||||
}, () => {
|
||||
const isDirty = this._workingCopyService.isDirty(textModel.uri.with({ scheme: Schemas.vscodeNotebook }));
|
||||
return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, isDirty);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$onContentChange(resource: UriComponents, viewType: string): void {
|
||||
const textModel = this._notebookService.getNotebookTextModel(URI.from(resource));
|
||||
|
||||
if (textModel) {
|
||||
textModel.applyEdits(textModel.versionId, [
|
||||
{
|
||||
editType: CellEditType.Unknown
|
||||
}
|
||||
], true, undefined, () => undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
async $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType) {
|
||||
const editor = this._notebookService.listNotebookEditors().find(editor => editor.getId() === id);
|
||||
if (editor && editor.isNotebookEditor) {
|
||||
const notebookEditor = editor as INotebookEditor;
|
||||
const viewModel = notebookEditor.viewModel;
|
||||
const cell = viewModel?.viewCells[range.start];
|
||||
if (!cell) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (revealType) {
|
||||
case NotebookEditorRevealType.Default:
|
||||
notebookEditor.revealInView(cell);
|
||||
break;
|
||||
case NotebookEditorRevealType.InCenter:
|
||||
notebookEditor.revealInCenter(cell);
|
||||
break;
|
||||
case NotebookEditorRevealType.InCenterIfOutsideViewport:
|
||||
notebookEditor.revealInCenterIfOutsideViewport(cell);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$registerNotebookEditorDecorationType(key: string, options: INotebookDecorationRenderOptions) {
|
||||
this._notebookService.registerEditorDecorationType(key, options);
|
||||
}
|
||||
|
||||
$removeNotebookEditorDecorationType(key: string) {
|
||||
this._notebookService.removeEditorDecorationType(key);
|
||||
}
|
||||
|
||||
$trySetDecorations(id: string, range: ICellRange, key: string) {
|
||||
const editor = this._notebookService.listNotebookEditors().find(editor => editor.getId() === id);
|
||||
if (editor && editor.isNotebookEditor) {
|
||||
const notebookEditor = editor as INotebookEditor;
|
||||
notebookEditor.setEditorDecorations(key, range);
|
||||
}
|
||||
}
|
||||
|
||||
async $setStatusBarEntry(id: number, rawStatusBarEntry: INotebookCellStatusBarEntryDto): Promise<void> {
|
||||
const statusBarEntry = {
|
||||
...rawStatusBarEntry,
|
||||
...{ cellResource: URI.revive(rawStatusBarEntry.cellResource) }
|
||||
};
|
||||
|
||||
const existingEntry = this._cellStatusBarEntries.get(id);
|
||||
if (existingEntry) {
|
||||
existingEntry.dispose();
|
||||
}
|
||||
|
||||
if (statusBarEntry.visible) {
|
||||
this._cellStatusBarEntries.set(
|
||||
id,
|
||||
this.cellStatusBarService.addEntry(statusBarEntry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async $tryOpenDocument(uriComponents: UriComponents, viewType?: string): Promise<URI> {
|
||||
const uri = URI.revive(uriComponents);
|
||||
const ref = await this._notebookModelResolverService.resolve(uri, viewType);
|
||||
this._modelReferenceCollection.add(uri, ref);
|
||||
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class BoundModelReferenceCollection {
|
||||
|
||||
private _data = new Array<{ uri: URI, dispose(): void }>();
|
||||
|
||||
constructor(
|
||||
private readonly _extUri: IExtUri,
|
||||
private readonly _maxAge: number = 1000 * 60 * 3,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._data = dispose(this._data);
|
||||
}
|
||||
|
||||
remove(uri: URI): void {
|
||||
for (const entry of [...this._data] /* copy array because dispose will modify it */) {
|
||||
if (this._extUri.isEqualOrParent(entry.uri, uri)) {
|
||||
entry.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(uri: URI, ref: IReference<INotebookEditorModel>): void {
|
||||
let handle: any;
|
||||
let entry: { uri: URI, dispose(): void };
|
||||
const dispose = () => {
|
||||
const idx = this._data.indexOf(entry);
|
||||
if (idx >= 0) {
|
||||
ref.dispose();
|
||||
clearTimeout(handle);
|
||||
this._data.splice(idx, 1);
|
||||
}
|
||||
};
|
||||
handle = setTimeout(dispose, this._maxAge);
|
||||
entry = { uri, dispose };
|
||||
|
||||
this._data.push(entry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IOutputService, IOutputChannel, OUTPUT_VIEW_ID } from 'vs/workbench/contrib/output/common/output';
|
||||
import { Extensions, IOutputChannelRegistry } from 'vs/workbench/services/output/common/output';
|
||||
import { MainThreadOutputServiceShape, MainContext, IExtHostContext, ExtHostOutputServiceShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { UriComponents, URI } from 'vs/base/common/uri';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IViewsService } from 'vs/workbench/common/views';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadOutputService)
|
||||
export class MainThreadOutputService extends Disposable implements MainThreadOutputServiceShape {
|
||||
|
||||
private static _idPool = 1;
|
||||
|
||||
private readonly _proxy: ExtHostOutputServiceShape;
|
||||
private readonly _outputService: IOutputService;
|
||||
private readonly _viewsService: IViewsService;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IOutputService outputService: IOutputService,
|
||||
@IViewsService viewsService: IViewsService
|
||||
) {
|
||||
super();
|
||||
this._outputService = outputService;
|
||||
this._viewsService = viewsService;
|
||||
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostOutputService);
|
||||
|
||||
const setVisibleChannel = () => {
|
||||
const visibleChannel = this._viewsService.isViewVisible(OUTPUT_VIEW_ID) ? this._outputService.getActiveChannel() : undefined;
|
||||
this._proxy.$setVisibleChannel(visibleChannel ? visibleChannel.id : null);
|
||||
};
|
||||
this._register(Event.any<any>(this._outputService.onActiveOutputChannel, Event.filter(this._viewsService.onDidChangeViewVisibility, ({ id }) => id === OUTPUT_VIEW_ID))(() => setVisibleChannel()));
|
||||
setVisibleChannel();
|
||||
}
|
||||
|
||||
public $register(label: string, log: boolean, file?: UriComponents): Promise<string> {
|
||||
const id = 'extension-output-#' + (MainThreadOutputService._idPool++);
|
||||
Registry.as<IOutputChannelRegistry>(Extensions.OutputChannels).registerChannel({ id, label, file: file ? URI.revive(file) : undefined, log });
|
||||
this._register(toDisposable(() => this.$dispose(id)));
|
||||
return Promise.resolve(id);
|
||||
}
|
||||
|
||||
public $append(channelId: string, value: string): Promise<void> | undefined {
|
||||
const channel = this._getChannel(channelId);
|
||||
if (channel) {
|
||||
channel.append(value);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $update(channelId: string): Promise<void> | undefined {
|
||||
const channel = this._getChannel(channelId);
|
||||
if (channel) {
|
||||
channel.update();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $clear(channelId: string, till: number): Promise<void> | undefined {
|
||||
const channel = this._getChannel(channelId);
|
||||
if (channel) {
|
||||
channel.clear(till);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $reveal(channelId: string, preserveFocus: boolean): Promise<void> | undefined {
|
||||
const channel = this._getChannel(channelId);
|
||||
if (channel) {
|
||||
this._outputService.showChannel(channel.id, preserveFocus);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $close(channelId: string): Promise<void> | undefined {
|
||||
if (this._viewsService.isViewVisible(OUTPUT_VIEW_ID)) {
|
||||
const activeChannel = this._outputService.getActiveChannel();
|
||||
if (activeChannel && channelId === activeChannel.id) {
|
||||
this._viewsService.closeView(OUTPUT_VIEW_ID);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public $dispose(channelId: string): Promise<void> | undefined {
|
||||
const channel = this._getChannel(channelId);
|
||||
if (channel) {
|
||||
channel.dispose();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getChannel(channelId: string): IOutputChannel | undefined {
|
||||
return this._outputService.getChannel(channelId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IProgress, IProgressService, IProgressStep, ProgressLocation, IProgressOptions, IProgressNotificationOptions } from 'vs/platform/progress/common/progress';
|
||||
import { MainThreadProgressShape, MainContext, IExtHostContext, ExtHostProgressShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
class ManageExtensionAction extends Action {
|
||||
constructor(id: ExtensionIdentifier, label: string, commandService: ICommandService) {
|
||||
super(id.value, label, undefined, true, () => {
|
||||
return commandService.executeCommand('_extensions.manage', id.value);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadProgress)
|
||||
export class MainThreadProgress implements MainThreadProgressShape {
|
||||
|
||||
private readonly _progressService: IProgressService;
|
||||
private _progress = new Map<number, { resolve: () => void, progress: IProgress<IProgressStep> }>();
|
||||
private readonly _proxy: ExtHostProgressShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IProgressService progressService: IProgressService,
|
||||
@ICommandService private readonly _commandService: ICommandService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostProgress);
|
||||
this._progressService = progressService;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._progress.forEach(handle => handle.resolve());
|
||||
this._progress.clear();
|
||||
}
|
||||
|
||||
$startProgress(handle: number, options: IProgressOptions, extension?: IExtensionDescription): void {
|
||||
const task = this._createTask(handle);
|
||||
|
||||
if (options.location === ProgressLocation.Notification && extension && !extension.isUnderDevelopment) {
|
||||
const notificationOptions: IProgressNotificationOptions = {
|
||||
...options,
|
||||
location: ProgressLocation.Notification,
|
||||
secondaryActions: [new ManageExtensionAction(extension.identifier, localize('manageExtension', "Manage Extension"), this._commandService)]
|
||||
};
|
||||
|
||||
options = notificationOptions;
|
||||
}
|
||||
|
||||
this._progressService.withProgress(options, task, () => this._proxy.$acceptProgressCanceled(handle));
|
||||
}
|
||||
|
||||
$progressReport(handle: number, message: IProgressStep): void {
|
||||
const entry = this._progress.get(handle);
|
||||
if (entry) {
|
||||
entry.progress.report(message);
|
||||
}
|
||||
}
|
||||
|
||||
$progressEnd(handle: number): void {
|
||||
const entry = this._progress.get(handle);
|
||||
if (entry) {
|
||||
entry.resolve();
|
||||
this._progress.delete(handle);
|
||||
}
|
||||
}
|
||||
|
||||
private _createTask(handle: number) {
|
||||
return (progress: IProgress<IProgressStep>) => {
|
||||
return new Promise<void>(resolve => {
|
||||
this._progress.set(handle, { resolve, progress });
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
221
lib/vscode/src/vs/workbench/api/browser/mainThreadQuickOpen.ts
Normal file
221
lib/vscode/src/vs/workbench/api/browser/mainThreadQuickOpen.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPickOptions, IInputOptions, IQuickInputService, IQuickInput } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, TransferQuickPickItems, MainContext, IExtHostContext, TransferQuickInput, TransferQuickInputButton, IInputBoxOptions } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
interface QuickInputSession {
|
||||
input: IQuickInput;
|
||||
handlesToItems: Map<number, TransferQuickPickItems>;
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadQuickOpen)
|
||||
export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
|
||||
|
||||
private readonly _proxy: ExtHostQuickOpenShape;
|
||||
private readonly _quickInputService: IQuickInputService;
|
||||
private readonly _items: Record<number, {
|
||||
resolve(items: TransferQuickPickItems[]): void;
|
||||
reject(error: Error): void;
|
||||
}> = {};
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IQuickInputService quickInputService: IQuickInputService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostQuickOpen);
|
||||
this._quickInputService = quickInputService;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
$show(instance: number, options: IPickOptions<TransferQuickPickItems>, token: CancellationToken): Promise<number | number[] | undefined> {
|
||||
const contents = new Promise<TransferQuickPickItems[]>((resolve, reject) => {
|
||||
this._items[instance] = { resolve, reject };
|
||||
});
|
||||
|
||||
options = {
|
||||
...options,
|
||||
onDidFocus: el => {
|
||||
if (el) {
|
||||
this._proxy.$onItemSelected((<TransferQuickPickItems>el).handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (options.canPickMany) {
|
||||
return this._quickInputService.pick(contents, options as { canPickMany: true }, token).then(items => {
|
||||
if (items) {
|
||||
return items.map(item => item.handle);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
} else {
|
||||
return this._quickInputService.pick(contents, options, token).then(item => {
|
||||
if (item) {
|
||||
return item.handle;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$setItems(instance: number, items: TransferQuickPickItems[]): Promise<void> {
|
||||
if (this._items[instance]) {
|
||||
this._items[instance].resolve(items);
|
||||
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
|
||||
|
||||
$input(options: IInputBoxOptions | undefined, validateInput: boolean, token: CancellationToken): Promise<string | undefined> {
|
||||
const inputOptions: IInputOptions = Object.create(null);
|
||||
|
||||
if (options) {
|
||||
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 = (value) => {
|
||||
return this._proxy.$validateInput(value);
|
||||
};
|
||||
}
|
||||
|
||||
return this._quickInputService.input(inputOptions, token);
|
||||
}
|
||||
|
||||
// ---- QuickInput
|
||||
|
||||
private sessions = new Map<number, QuickInputSession>();
|
||||
|
||||
$createOrUpdate(params: TransferQuickInput): Promise<void> {
|
||||
const sessionId = params.id;
|
||||
let session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
if (params.type === 'quickPick') {
|
||||
const input = this._quickInputService.createQuickPick();
|
||||
input.onDidAccept(() => {
|
||||
this._proxy.$onDidAccept(sessionId);
|
||||
});
|
||||
input.onDidChangeActive(items => {
|
||||
this._proxy.$onDidChangeActive(sessionId, items.map(item => (item as TransferQuickPickItems).handle));
|
||||
});
|
||||
input.onDidChangeSelection(items => {
|
||||
this._proxy.$onDidChangeSelection(sessionId, items.map(item => (item as TransferQuickPickItems).handle));
|
||||
});
|
||||
input.onDidTriggerButton(button => {
|
||||
this._proxy.$onDidTriggerButton(sessionId, (button as TransferQuickInputButton).handle);
|
||||
});
|
||||
input.onDidChangeValue(value => {
|
||||
this._proxy.$onDidChangeValue(sessionId, value);
|
||||
});
|
||||
input.onDidHide(() => {
|
||||
this._proxy.$onDidHide(sessionId);
|
||||
});
|
||||
session = {
|
||||
input,
|
||||
handlesToItems: new Map()
|
||||
};
|
||||
} else {
|
||||
const input = this._quickInputService.createInputBox();
|
||||
input.onDidAccept(() => {
|
||||
this._proxy.$onDidAccept(sessionId);
|
||||
});
|
||||
input.onDidTriggerButton(button => {
|
||||
this._proxy.$onDidTriggerButton(sessionId, (button as TransferQuickInputButton).handle);
|
||||
});
|
||||
input.onDidChangeValue(value => {
|
||||
this._proxy.$onDidChangeValue(sessionId, value);
|
||||
});
|
||||
input.onDidHide(() => {
|
||||
this._proxy.$onDidHide(sessionId);
|
||||
});
|
||||
session = {
|
||||
input,
|
||||
handlesToItems: new Map()
|
||||
};
|
||||
}
|
||||
this.sessions.set(sessionId, 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();
|
||||
params[param].forEach((item: TransferQuickPickItems) => {
|
||||
handlesToItems.set(item.handle, item);
|
||||
});
|
||||
(input as any)[param] = params[param];
|
||||
} 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 { iconPath, tooltip, handle } = button;
|
||||
if ('id' in iconPath) {
|
||||
return {
|
||||
iconClass: ThemeIcon.asClassName(iconPath),
|
||||
tooltip,
|
||||
handle
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
iconPath: {
|
||||
dark: URI.revive(iconPath.dark),
|
||||
light: iconPath.light && URI.revive(iconPath.light)
|
||||
},
|
||||
tooltip,
|
||||
handle
|
||||
};
|
||||
}
|
||||
});
|
||||
} else {
|
||||
(input as any)[param] = params[param];
|
||||
}
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$dispose(sessionId: number): Promise<void> {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
session.input.dispose();
|
||||
this.sessions.delete(sessionId);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, IExtHostContext, ExtHostExtensionServiceShape } from '../common/extHost.protocol';
|
||||
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
|
||||
@extHostCustomer
|
||||
export class MainThreadRemoteConnectionData extends Disposable {
|
||||
|
||||
private readonly _proxy: ExtHostExtensionServiceShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IWorkbenchEnvironmentService protected readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostExtensionService);
|
||||
|
||||
const remoteAuthority = this._environmentService.remoteAuthority;
|
||||
if (remoteAuthority) {
|
||||
this._register(remoteAuthorityResolverService.onDidChangeConnectionData(() => {
|
||||
const connectionData = remoteAuthorityResolverService.getConnectionData(remoteAuthority);
|
||||
if (connectionData) {
|
||||
this._proxy.$updateRemoteConnectionData(connectionData);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
440
lib/vscode/src/vs/workbench/api/browser/mainThreadSCM.ts
Normal file
440
lib/vscode/src/vs/workbench/api/browser/mainThreadSCM.ts
Normal file
@@ -0,0 +1,440 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IDisposable, DisposableStore, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ISplice, Sequence } from 'vs/base/common/sequence';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
class MainThreadSCMResourceGroup implements ISCMResourceGroup {
|
||||
|
||||
readonly elements: ISCMResource[] = [];
|
||||
|
||||
private readonly _onDidSplice = new Emitter<ISplice<ISCMResource>>();
|
||||
readonly onDidSplice = this._onDidSplice.event;
|
||||
|
||||
get hideWhenEmpty(): boolean { return !!this.features.hideWhenEmpty; }
|
||||
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
private readonly sourceControlHandle: number,
|
||||
private readonly handle: number,
|
||||
public provider: ISCMProvider,
|
||||
public features: SCMGroupFeatures,
|
||||
public label: string,
|
||||
public id: string
|
||||
) { }
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: 4,
|
||||
sourceControlHandle: this.sourceControlHandle,
|
||||
groupHandle: this.handle
|
||||
};
|
||||
}
|
||||
|
||||
splice(start: number, deleteCount: number, toInsert: ISCMResource[]) {
|
||||
this.elements.splice(start, deleteCount, ...toInsert);
|
||||
this._onDidSplice.fire({ start, deleteCount, toInsert });
|
||||
}
|
||||
|
||||
$updateGroup(features: SCMGroupFeatures): void {
|
||||
this.features = { ...this.features, ...features };
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
$updateGroupLabel(label: string): void {
|
||||
this.label = label;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadSCMResource implements ISCMResource {
|
||||
|
||||
constructor(
|
||||
private readonly proxy: ExtHostSCMShape,
|
||||
private readonly sourceControlHandle: number,
|
||||
private readonly groupHandle: number,
|
||||
private readonly handle: number,
|
||||
readonly sourceUri: URI,
|
||||
readonly resourceGroup: ISCMResourceGroup,
|
||||
readonly decorations: ISCMResourceDecorations,
|
||||
readonly contextValue: string | undefined
|
||||
) { }
|
||||
|
||||
open(preserveFocus: boolean): Promise<void> {
|
||||
return this.proxy.$executeResourceCommand(this.sourceControlHandle, this.groupHandle, this.handle, preserveFocus);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: 3,
|
||||
sourceControlHandle: this.sourceControlHandle,
|
||||
groupHandle: this.groupHandle,
|
||||
handle: this.handle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadSCMProvider implements ISCMProvider {
|
||||
|
||||
private static ID_HANDLE = 0;
|
||||
private _id = `scm${MainThreadSCMProvider.ID_HANDLE++}`;
|
||||
get id(): string { return this._id; }
|
||||
|
||||
readonly groups = new Sequence<MainThreadSCMResourceGroup>();
|
||||
private readonly _groupsByHandle: { [handle: number]: MainThreadSCMResourceGroup; } = Object.create(null);
|
||||
|
||||
// get groups(): ISequence<ISCMResourceGroup> {
|
||||
// return {
|
||||
// elements: this._groups,
|
||||
// onDidSplice: this._onDidSplice.event
|
||||
// };
|
||||
|
||||
// // return this._groups
|
||||
// // .filter(g => g.resources.elements.length > 0 || !g.features.hideWhenEmpty);
|
||||
// }
|
||||
|
||||
private readonly _onDidChangeResources = new Emitter<void>();
|
||||
readonly onDidChangeResources: Event<void> = this._onDidChangeResources.event;
|
||||
|
||||
private features: SCMProviderFeatures = {};
|
||||
|
||||
get handle(): number { return this._handle; }
|
||||
get label(): string { return this._label; }
|
||||
get rootUri(): URI | undefined { return this._rootUri; }
|
||||
get contextValue(): string { return this._contextValue; }
|
||||
|
||||
get commitTemplate(): string { return this.features.commitTemplate || ''; }
|
||||
get acceptInputCommand(): Command | undefined { return this.features.acceptInputCommand; }
|
||||
get statusBarCommands(): Command[] | undefined { return this.features.statusBarCommands; }
|
||||
get count(): number | undefined { return this.features.count; }
|
||||
|
||||
private readonly _onDidChangeCommitTemplate = new Emitter<string>();
|
||||
readonly onDidChangeCommitTemplate: Event<string> = this._onDidChangeCommitTemplate.event;
|
||||
|
||||
private readonly _onDidChangeStatusBarCommands = new Emitter<Command[]>();
|
||||
get onDidChangeStatusBarCommands(): Event<Command[]> { return this._onDidChangeStatusBarCommands.event; }
|
||||
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
constructor(
|
||||
private readonly proxy: ExtHostSCMShape,
|
||||
private readonly _handle: number,
|
||||
private readonly _contextValue: string,
|
||||
private readonly _label: string,
|
||||
private readonly _rootUri: URI | undefined
|
||||
) { }
|
||||
|
||||
$updateSourceControl(features: SCMProviderFeatures): void {
|
||||
this.features = { ...this.features, ...features };
|
||||
this._onDidChange.fire();
|
||||
|
||||
if (typeof features.commitTemplate !== 'undefined') {
|
||||
this._onDidChangeCommitTemplate.fire(this.commitTemplate!);
|
||||
}
|
||||
|
||||
if (typeof features.statusBarCommands !== 'undefined') {
|
||||
this._onDidChangeStatusBarCommands.fire(this.statusBarCommands!);
|
||||
}
|
||||
}
|
||||
|
||||
$registerGroups(_groups: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][]): void {
|
||||
const groups = _groups.map(([handle, id, label, features]) => {
|
||||
const group = new MainThreadSCMResourceGroup(
|
||||
this.handle,
|
||||
handle,
|
||||
this,
|
||||
features,
|
||||
label,
|
||||
id
|
||||
);
|
||||
|
||||
this._groupsByHandle[handle] = group;
|
||||
return group;
|
||||
});
|
||||
|
||||
this.groups.splice(this.groups.elements.length, 0, groups);
|
||||
}
|
||||
|
||||
$updateGroup(handle: number, features: SCMGroupFeatures): 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 [groupHandle, groupSlices] of 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 [start, deleteCount, rawResources] of groupSlices) {
|
||||
const resources = rawResources.map(rawResource => {
|
||||
const [handle, sourceUri, icons, tooltip, strikeThrough, faded, contextValue] = rawResource;
|
||||
const icon = icons[0];
|
||||
const iconDark = icons[1] || icon;
|
||||
const decorations = {
|
||||
icon: icon ? URI.revive(icon) : undefined,
|
||||
iconDark: iconDark ? URI.revive(iconDark) : undefined,
|
||||
tooltip,
|
||||
strikeThrough,
|
||||
faded
|
||||
};
|
||||
|
||||
return new MainThreadSCMResource(
|
||||
this.proxy,
|
||||
this.handle,
|
||||
groupHandle,
|
||||
handle,
|
||||
URI.revive(sourceUri),
|
||||
group,
|
||||
decorations,
|
||||
contextValue || undefined
|
||||
);
|
||||
});
|
||||
|
||||
group.splice(start, deleteCount, resources);
|
||||
}
|
||||
}
|
||||
|
||||
this._onDidChangeResources.fire();
|
||||
}
|
||||
|
||||
$unregisterGroup(handle: number): void {
|
||||
const group = this._groupsByHandle[handle];
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this._groupsByHandle[handle];
|
||||
this.groups.splice(this.groups.elements.indexOf(group), 1);
|
||||
}
|
||||
|
||||
async getOriginalResource(uri: URI): Promise<URI | null> {
|
||||
if (!this.features.hasQuickDiffProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = await this.proxy.$provideOriginalResource(this.handle, uri, CancellationToken.None);
|
||||
return result && URI.revive(result);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return {
|
||||
$mid: 5,
|
||||
handle: this.handle
|
||||
};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadSCM)
|
||||
export class MainThreadSCM implements MainThreadSCMShape {
|
||||
|
||||
private readonly _proxy: ExtHostSCMShape;
|
||||
private _repositories = new Map<number, ISCMRepository>();
|
||||
private _repositoryDisposables = new Map<number, IDisposable>();
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ISCMService private readonly scmService: ISCMService,
|
||||
@ISCMViewService private readonly scmViewService: ISCMViewService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSCM);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._repositories.forEach(r => r.dispose());
|
||||
this._repositories.clear();
|
||||
|
||||
this._repositoryDisposables.forEach(d => d.dispose());
|
||||
this._repositoryDisposables.clear();
|
||||
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
$registerSourceControl(handle: number, id: string, label: string, rootUri: UriComponents | undefined): void {
|
||||
const provider = new MainThreadSCMProvider(this._proxy, handle, id, label, rootUri ? URI.revive(rootUri) : undefined);
|
||||
const repository = this.scmService.registerSCMProvider(provider);
|
||||
this._repositories.set(handle, repository);
|
||||
|
||||
const disposable = combinedDisposable(
|
||||
Event.filter(this.scmViewService.onDidFocusRepository, r => r === repository)(_ => this._proxy.$setSelectedSourceControl(handle)),
|
||||
repository.input.onDidChange(({ value }) => this._proxy.$onInputBoxValueChange(handle, value))
|
||||
);
|
||||
|
||||
if (this.scmViewService.focusedRepository === repository) {
|
||||
setTimeout(() => this._proxy.$setSelectedSourceControl(handle), 0);
|
||||
}
|
||||
|
||||
if (repository.input.value) {
|
||||
setTimeout(() => this._proxy.$onInputBoxValueChange(handle, repository.input.value), 0);
|
||||
}
|
||||
|
||||
this._repositoryDisposables.set(handle, disposable);
|
||||
}
|
||||
|
||||
$updateSourceControl(handle: number, features: SCMProviderFeatures): void {
|
||||
const repository = this._repositories.get(handle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$updateSourceControl(features);
|
||||
}
|
||||
|
||||
$unregisterSourceControl(handle: number): 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: [number /*handle*/, string /*id*/, string /*label*/, SCMGroupFeatures][], splices: SCMRawResourceSplices[]): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$registerGroups(groups);
|
||||
provider.$spliceGroupResourceStates(splices);
|
||||
}
|
||||
|
||||
$updateGroup(sourceControlHandle: number, groupHandle: number, features: SCMGroupFeatures): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
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 MainThreadSCMProvider;
|
||||
provider.$updateGroupLabel(groupHandle, label);
|
||||
}
|
||||
|
||||
$spliceResourceStates(sourceControlHandle: number, splices: SCMRawResourceSplices[]): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$spliceGroupResourceStates(splices);
|
||||
}
|
||||
|
||||
$unregisterGroup(sourceControlHandle: number, handle: number): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = repository.provider as MainThreadSCMProvider;
|
||||
provider.$unregisterGroup(handle);
|
||||
}
|
||||
|
||||
$setInputBoxValue(sourceControlHandle: number, value: string): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.input.setValue(value, false);
|
||||
}
|
||||
|
||||
$setInputBoxPlaceholder(sourceControlHandle: number, placeholder: string): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.input.placeholder = placeholder;
|
||||
}
|
||||
|
||||
$setInputBoxVisibility(sourceControlHandle: number, visible: boolean): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.input.visible = visible;
|
||||
}
|
||||
|
||||
$setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): void {
|
||||
const repository = this._repositories.get(sourceControlHandle);
|
||||
|
||||
if (!repository) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
repository.input.validateInput = async (value, pos): Promise<IInputValidation | undefined> => {
|
||||
const result = await this._proxy.$validateInput(sourceControlHandle, value, pos);
|
||||
return result && { message: result[0], type: result[1] };
|
||||
};
|
||||
} else {
|
||||
repository.input.validateInput = async () => undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IProgressStep, IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ITextFileSaveParticipant, ITextFileService, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import { ExtHostContext, ExtHostDocumentSaveParticipantShape, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
class ExtHostSaveParticipant implements ITextFileSaveParticipant {
|
||||
|
||||
private readonly _proxy: ExtHostDocumentSaveParticipantShape;
|
||||
|
||||
constructor(extHostContext: IExtHostContext) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentSaveParticipant);
|
||||
}
|
||||
|
||||
async participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason; }, _progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
|
||||
|
||||
if (!editorModel.textEditorModel || !shouldSynchronizeModel(editorModel.textEditorModel)) {
|
||||
// the model never made it to the extension
|
||||
// host meaning we cannot participate in its save
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
|
||||
token.onCancellationRequested(() => reject(canceled()));
|
||||
|
||||
setTimeout(
|
||||
() => reject(new Error(localize('timeout.onWillSave', "Aborted onWillSaveTextDocument-event after 1750ms"))),
|
||||
1750
|
||||
);
|
||||
this._proxy.$participateInSave(editorModel.resource, env.reason).then(values => {
|
||||
if (!values.every(success => success)) {
|
||||
return Promise.reject(new Error('listener failed'));
|
||||
}
|
||||
return undefined;
|
||||
}).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
|
||||
@extHostCustomer
|
||||
export class SaveParticipant {
|
||||
|
||||
private _saveParticipantDisposable: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@ITextFileService private readonly _textFileService: ITextFileService
|
||||
) {
|
||||
this._saveParticipantDisposable = this._textFileService.files.addSaveParticipant(instantiationService.createInstance(ExtHostSaveParticipant, extHostContext));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._saveParticipantDisposable.dispose();
|
||||
}
|
||||
}
|
||||
173
lib/vscode/src/vs/workbench/api/browser/mainThreadSearch.ts
Normal file
173
lib/vscode/src/vs/workbench/api/browser/mainThreadSearch.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search';
|
||||
import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../common/extHost.protocol';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadSearch)
|
||||
export class MainThreadSearch implements MainThreadSearchShape {
|
||||
|
||||
private readonly _proxy: ExtHostSearchShape;
|
||||
private readonly _searchProvider = new Map<number, RemoteSearchProvider>();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ISearchService private readonly _searchService: ISearchService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSearch);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._searchProvider.forEach(value => value.dispose());
|
||||
this._searchProvider.clear();
|
||||
}
|
||||
|
||||
$registerTextSearchProvider(handle: number, scheme: string): void {
|
||||
this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.text, scheme, handle, this._proxy));
|
||||
}
|
||||
|
||||
$registerFileSearchProvider(handle: number, scheme: string): void {
|
||||
this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.file, scheme, handle, this._proxy));
|
||||
}
|
||||
|
||||
$unregisterProvider(handle: number): void {
|
||||
dispose(this._searchProvider.get(handle));
|
||||
this._searchProvider.delete(handle);
|
||||
}
|
||||
|
||||
$handleFileMatch(handle: number, session: number, data: UriComponents[]): void {
|
||||
const provider = this._searchProvider.get(handle);
|
||||
if (!provider) {
|
||||
throw new Error('Got result for unknown provider');
|
||||
}
|
||||
|
||||
provider.handleFindMatch(session, data);
|
||||
}
|
||||
|
||||
$handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void {
|
||||
const provider = this._searchProvider.get(handle);
|
||||
if (!provider) {
|
||||
throw new Error('Got result for unknown provider');
|
||||
}
|
||||
|
||||
provider.handleFindMatch(session, data);
|
||||
}
|
||||
|
||||
$handleTelemetry(eventName: string, data: any): void {
|
||||
this._telemetryService.publicLog(eventName, data);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchOperation {
|
||||
|
||||
private static _idPool = 0;
|
||||
|
||||
constructor(
|
||||
readonly progress?: (match: IFileMatch) => any,
|
||||
readonly id: number = ++SearchOperation._idPool,
|
||||
readonly matches = new Map<string, IFileMatch>()
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
addMatch(match: IFileMatch): void {
|
||||
const existingMatch = this.matches.get(match.resource.toString());
|
||||
if (existingMatch) {
|
||||
// TODO@rob clean up text/file result types
|
||||
// If a file search returns the same file twice, we would enter this branch.
|
||||
// It's possible that could happen, #90813
|
||||
if (existingMatch.results && match.results) {
|
||||
existingMatch.results.push(...match.results);
|
||||
}
|
||||
} else {
|
||||
this.matches.set(match.resource.toString(), match);
|
||||
}
|
||||
|
||||
if (this.progress) {
|
||||
this.progress(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class RemoteSearchProvider implements ISearchResultProvider, IDisposable {
|
||||
|
||||
private readonly _registrations = new DisposableStore();
|
||||
private readonly _searches = new Map<number, SearchOperation>();
|
||||
|
||||
constructor(
|
||||
searchService: ISearchService,
|
||||
type: SearchProviderType,
|
||||
private readonly _scheme: string,
|
||||
private readonly _handle: number,
|
||||
private readonly _proxy: ExtHostSearchShape
|
||||
) {
|
||||
this._registrations.add(searchService.registerSearchResultProvider(this._scheme, type, this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._registrations.dispose();
|
||||
}
|
||||
|
||||
fileSearch(query: IFileQuery, token: CancellationToken = CancellationToken.None): Promise<ISearchComplete> {
|
||||
return this.doSearch(query, undefined, token);
|
||||
}
|
||||
|
||||
textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise<ISearchComplete> {
|
||||
return this.doSearch(query, onProgress, token);
|
||||
}
|
||||
|
||||
doSearch(query: ITextQuery | IFileQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise<ISearchComplete> {
|
||||
if (!query.folderQueries.length) {
|
||||
throw new Error('Empty folderQueries');
|
||||
}
|
||||
|
||||
const search = new SearchOperation(onProgress);
|
||||
this._searches.set(search.id, search);
|
||||
|
||||
const searchP = query.type === QueryType.File
|
||||
? this._proxy.$provideFileSearchResults(this._handle, search.id, query, token)
|
||||
: this._proxy.$provideTextSearchResults(this._handle, search.id, query, token);
|
||||
|
||||
return Promise.resolve(searchP).then((result: ISearchCompleteStats) => {
|
||||
this._searches.delete(search.id);
|
||||
return { results: Array.from(search.matches.values()), stats: result.stats, limitHit: result.limitHit };
|
||||
}, err => {
|
||||
this._searches.delete(search.id);
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
clearCache(cacheKey: string): Promise<void> {
|
||||
return Promise.resolve(this._proxy.$clearCache(cacheKey));
|
||||
}
|
||||
|
||||
handleFindMatch(session: number, dataOrUri: Array<UriComponents | IRawFileMatch2>): void {
|
||||
const searchOp = this._searches.get(session);
|
||||
|
||||
if (!searchOp) {
|
||||
// ignore...
|
||||
return;
|
||||
}
|
||||
|
||||
dataOrUri.forEach(result => {
|
||||
if ((<IRawFileMatch2>result).results) {
|
||||
searchOp.addMatch({
|
||||
resource: URI.revive((<IRawFileMatch2>result).resource),
|
||||
results: (<IRawFileMatch2>result).results
|
||||
});
|
||||
} else {
|
||||
searchOp.addMatch({
|
||||
resource: URI.revive(<UriComponents>result)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStatusbarService, StatusbarAlignment as MainThreadStatusBarAlignment, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar';
|
||||
import { MainThreadStatusBarShape, MainContext, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { ThemeColor } from 'vs/platform/theme/common/themeService';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { dispose } from 'vs/base/common/lifecycle';
|
||||
import { Command } from 'vs/editor/common/modes';
|
||||
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadStatusBar)
|
||||
export class MainThreadStatusBar implements MainThreadStatusBarShape {
|
||||
|
||||
private readonly entries: Map<number, { accessor: IStatusbarEntryAccessor, alignment: MainThreadStatusBarAlignment, priority: number }> = new Map();
|
||||
static readonly CODICON_REGEXP = /\$\((.*?)\)/g;
|
||||
|
||||
constructor(
|
||||
_extHostContext: IExtHostContext,
|
||||
@IStatusbarService private readonly statusbarService: IStatusbarService
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
this.entries.forEach(entry => entry.accessor.dispose());
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
$setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void {
|
||||
// if there are icons in the text use the tooltip for the aria label
|
||||
let ariaLabel: string;
|
||||
let role: string | undefined = undefined;
|
||||
if (accessibilityInformation) {
|
||||
ariaLabel = accessibilityInformation.label;
|
||||
role = accessibilityInformation.role;
|
||||
} else {
|
||||
ariaLabel = text ? text.replace(MainThreadStatusBar.CODICON_REGEXP, (_match, codiconName) => codiconName) : '';
|
||||
}
|
||||
const entry: IStatusbarEntry = { text, tooltip, command, color, ariaLabel, role };
|
||||
|
||||
if (typeof priority === 'undefined') {
|
||||
priority = 0;
|
||||
}
|
||||
|
||||
// Reset existing entry if alignment or priority changed
|
||||
let existingEntry = this.entries.get(id);
|
||||
if (existingEntry && (existingEntry.alignment !== alignment || existingEntry.priority !== priority)) {
|
||||
dispose(existingEntry.accessor);
|
||||
this.entries.delete(id);
|
||||
existingEntry = undefined;
|
||||
}
|
||||
|
||||
// Create new entry if not existing
|
||||
if (!existingEntry) {
|
||||
this.entries.set(id, { accessor: this.statusbarService.addEntry(entry, statusId, statusName, alignment, priority), alignment, priority });
|
||||
}
|
||||
|
||||
// Otherwise update
|
||||
else {
|
||||
existingEntry.accessor.update(entry);
|
||||
}
|
||||
}
|
||||
|
||||
$dispose(id: number) {
|
||||
const entry = this.entries.get(id);
|
||||
if (entry) {
|
||||
dispose(entry.accessor);
|
||||
this.entries.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts
Normal file
79
lib/vscode/src/vs/workbench/api/browser/mainThreadStorage.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { MainThreadStorageShape, MainContext, IExtHostContext, ExtHostStorageShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionIdWithVersion, IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadStorage)
|
||||
export class MainThreadStorage implements MainThreadStorageShape {
|
||||
|
||||
private readonly _storageService: IStorageService;
|
||||
private readonly _storageKeysSyncRegistryService: IStorageKeysSyncRegistryService;
|
||||
private readonly _proxy: ExtHostStorageShape;
|
||||
private readonly _storageListener: IDisposable;
|
||||
private readonly _sharedStorageKeysToWatch: Map<string, boolean> = new Map<string, boolean>();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
) {
|
||||
this._storageService = storageService;
|
||||
this._storageKeysSyncRegistryService = storageKeysSyncRegistryService;
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostStorage);
|
||||
|
||||
this._storageListener = this._storageService.onDidChangeStorage(e => {
|
||||
const shared = e.scope === StorageScope.GLOBAL;
|
||||
if (shared && this._sharedStorageKeysToWatch.has(e.key)) {
|
||||
try {
|
||||
this._proxy.$acceptValue(shared, e.key, this._getValue(shared, e.key));
|
||||
} catch (error) {
|
||||
// ignore parsing errors that can happen
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._storageListener.dispose();
|
||||
}
|
||||
|
||||
$getValue<T>(shared: boolean, key: string): Promise<T | undefined> {
|
||||
if (shared) {
|
||||
this._sharedStorageKeysToWatch.set(key, true);
|
||||
}
|
||||
try {
|
||||
return Promise.resolve(this._getValue<T>(shared, key));
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
private _getValue<T>(shared: boolean, key: string): T | undefined {
|
||||
const jsonValue = this._storageService.get(key, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
|
||||
if (!jsonValue) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(jsonValue);
|
||||
}
|
||||
|
||||
$setValue(shared: boolean, key: string, value: object): Promise<void> {
|
||||
let jsonValue: string;
|
||||
try {
|
||||
jsonValue = JSON.stringify(value);
|
||||
this._storageService.store(key, jsonValue, shared ? StorageScope.GLOBAL : StorageScope.WORKSPACE);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$registerExtensionStorageKeysToSync(extension: IExtensionIdWithVersion, keys: string[]): void {
|
||||
this._storageKeysSyncRegistryService.registerExtensionStorageKeys(extension, keys);
|
||||
}
|
||||
}
|
||||
712
lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts
Normal file
712
lib/vscode/src/vs/workbench/api/browser/mainThreadTask.ts
Normal file
@@ -0,0 +1,712 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import * as Types from 'vs/base/common/types';
|
||||
import * as Platform from 'vs/base/common/platform';
|
||||
import { IStringDictionary, forEach } from 'vs/base/common/collections';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
|
||||
import {
|
||||
ContributedTask, ConfiguringTask, KeyedTaskIdentifier, TaskExecution, Task, TaskEvent, TaskEventKind,
|
||||
PresentationOptions, CommandOptions, CommandConfiguration, RuntimeType, CustomTask, TaskScope, TaskSource,
|
||||
TaskSourceKind, ExtensionTaskSource, RunOptions, TaskSet, TaskDefinition
|
||||
} from 'vs/workbench/contrib/tasks/common/tasks';
|
||||
|
||||
|
||||
import { ResolveSet, ResolvedVariables } from 'vs/workbench/contrib/tasks/common/taskSystem';
|
||||
import { ITaskService, TaskFilter, ITaskProvider } from 'vs/workbench/contrib/tasks/common/taskService';
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, MainThreadTaskShape, ExtHostTaskShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import {
|
||||
TaskDefinitionDTO, TaskExecutionDTO, ProcessExecutionOptionsDTO, TaskPresentationOptionsDTO,
|
||||
ProcessExecutionDTO, ShellExecutionDTO, ShellExecutionOptionsDTO, CustomExecutionDTO, TaskDTO, TaskSourceDTO, TaskHandleDTO, TaskFilterDTO, TaskProcessStartedDTO, TaskProcessEndedDTO, TaskSystemInfoDTO,
|
||||
RunOptionsDTO
|
||||
} from 'vs/workbench/api/common/shared/tasks';
|
||||
import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver';
|
||||
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
namespace TaskExecutionDTO {
|
||||
export function from(value: TaskExecution): TaskExecutionDTO {
|
||||
return {
|
||||
id: value.id,
|
||||
task: TaskDTO.from(value.task)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskProcessStartedDTO {
|
||||
export function from(value: TaskExecution, processId: number): TaskProcessStartedDTO {
|
||||
return {
|
||||
id: value.id,
|
||||
processId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskProcessEndedDTO {
|
||||
export function from(value: TaskExecution, exitCode: number): TaskProcessEndedDTO {
|
||||
return {
|
||||
id: value.id,
|
||||
exitCode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskDefinitionDTO {
|
||||
export function from(value: KeyedTaskIdentifier): TaskDefinitionDTO {
|
||||
const result = Object.assign(Object.create(null), value);
|
||||
delete result._key;
|
||||
return result;
|
||||
}
|
||||
export function to(value: TaskDefinitionDTO, executeOnly: boolean): KeyedTaskIdentifier | undefined {
|
||||
let result = TaskDefinition.createTaskIdentifier(value, console);
|
||||
if (result === undefined && executeOnly) {
|
||||
result = {
|
||||
_key: generateUuid(),
|
||||
type: '$executeOnly'
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskPresentationOptionsDTO {
|
||||
export function from(value: PresentationOptions | undefined): TaskPresentationOptionsDTO | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.assign(Object.create(null), value);
|
||||
}
|
||||
export function to(value: TaskPresentationOptionsDTO | undefined): PresentationOptions {
|
||||
if (value === undefined || value === null) {
|
||||
return PresentationOptions.defaults;
|
||||
}
|
||||
return Object.assign(Object.create(null), PresentationOptions.defaults, value);
|
||||
}
|
||||
}
|
||||
|
||||
namespace RunOptionsDTO {
|
||||
export function from(value: RunOptions): RunOptionsDTO | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.assign(Object.create(null), value);
|
||||
}
|
||||
export function to(value: RunOptionsDTO | undefined): RunOptions {
|
||||
if (value === undefined || value === null) {
|
||||
return RunOptions.defaults;
|
||||
}
|
||||
return Object.assign(Object.create(null), RunOptions.defaults, value);
|
||||
}
|
||||
}
|
||||
|
||||
namespace ProcessExecutionOptionsDTO {
|
||||
export function from(value: CommandOptions): ProcessExecutionOptionsDTO | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
cwd: value.cwd,
|
||||
env: value.env
|
||||
};
|
||||
}
|
||||
export function to(value: ProcessExecutionOptionsDTO | undefined): CommandOptions {
|
||||
if (value === undefined || value === null) {
|
||||
return CommandOptions.defaults;
|
||||
}
|
||||
return {
|
||||
cwd: value.cwd || CommandOptions.defaults.cwd,
|
||||
env: value.env
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace ProcessExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ProcessExecutionDTO {
|
||||
const candidate = value as ProcessExecutionDTO;
|
||||
return candidate && !!candidate.process;
|
||||
}
|
||||
export function from(value: CommandConfiguration): ProcessExecutionDTO {
|
||||
const process: string = Types.isString(value.name) ? value.name : value.name!.value;
|
||||
const args: string[] = value.args ? value.args.map(value => Types.isString(value) ? value : value.value) : [];
|
||||
const result: ProcessExecutionDTO = {
|
||||
process: process,
|
||||
args: args
|
||||
};
|
||||
if (value.options) {
|
||||
result.options = ProcessExecutionOptionsDTO.from(value.options);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function to(value: ProcessExecutionDTO): CommandConfiguration {
|
||||
const result: CommandConfiguration = {
|
||||
runtime: RuntimeType.Process,
|
||||
name: value.process,
|
||||
args: value.args,
|
||||
presentation: undefined
|
||||
};
|
||||
result.options = ProcessExecutionOptionsDTO.to(value.options);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ShellExecutionOptionsDTO {
|
||||
export function from(value: CommandOptions): ShellExecutionOptionsDTO | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
const result: ShellExecutionOptionsDTO = {
|
||||
cwd: value.cwd || CommandOptions.defaults.cwd,
|
||||
env: value.env
|
||||
};
|
||||
if (value.shell) {
|
||||
result.executable = value.shell.executable;
|
||||
result.shellArgs = value.shell.args;
|
||||
result.shellQuoting = value.shell.quoting;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function to(value: ShellExecutionOptionsDTO): CommandOptions | undefined {
|
||||
if (value === undefined || value === null) {
|
||||
return undefined;
|
||||
}
|
||||
const result: CommandOptions = {
|
||||
cwd: value.cwd,
|
||||
env: value.env
|
||||
};
|
||||
if (value.executable) {
|
||||
result.shell = {
|
||||
executable: value.executable
|
||||
};
|
||||
if (value.shellArgs) {
|
||||
result.shell.args = value.shellArgs;
|
||||
}
|
||||
if (value.shellQuoting) {
|
||||
result.shell.quoting = value.shellQuoting;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace ShellExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is ShellExecutionDTO {
|
||||
const candidate = value as ShellExecutionDTO;
|
||||
return candidate && (!!candidate.commandLine || !!candidate.command);
|
||||
}
|
||||
export function from(value: CommandConfiguration): ShellExecutionDTO {
|
||||
const result: ShellExecutionDTO = {};
|
||||
if (value.name && Types.isString(value.name) && (value.args === undefined || value.args === null || value.args.length === 0)) {
|
||||
result.commandLine = value.name;
|
||||
} else {
|
||||
result.command = value.name;
|
||||
result.args = value.args;
|
||||
}
|
||||
if (value.options) {
|
||||
result.options = ShellExecutionOptionsDTO.from(value.options);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function to(value: ShellExecutionDTO): CommandConfiguration {
|
||||
const result: CommandConfiguration = {
|
||||
runtime: RuntimeType.Shell,
|
||||
name: value.commandLine ? value.commandLine : value.command,
|
||||
args: value.args,
|
||||
presentation: undefined
|
||||
};
|
||||
if (value.options) {
|
||||
result.options = ShellExecutionOptionsDTO.to(value.options);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace CustomExecutionDTO {
|
||||
export function is(value: ShellExecutionDTO | ProcessExecutionDTO | CustomExecutionDTO): value is CustomExecutionDTO {
|
||||
const candidate = value as CustomExecutionDTO;
|
||||
return candidate && candidate.customExecution === 'customExecution';
|
||||
}
|
||||
|
||||
export function from(value: CommandConfiguration): CustomExecutionDTO {
|
||||
return {
|
||||
customExecution: 'customExecution'
|
||||
};
|
||||
}
|
||||
|
||||
export function to(value: CustomExecutionDTO): CommandConfiguration {
|
||||
return {
|
||||
runtime: RuntimeType.CustomExecution,
|
||||
presentation: undefined
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskSourceDTO {
|
||||
export function from(value: TaskSource): TaskSourceDTO {
|
||||
const result: TaskSourceDTO = {
|
||||
label: value.label
|
||||
};
|
||||
if (value.kind === TaskSourceKind.Extension) {
|
||||
result.extensionId = value.extension;
|
||||
if (value.workspaceFolder) {
|
||||
result.scope = value.workspaceFolder.uri;
|
||||
} else {
|
||||
result.scope = value.scope;
|
||||
}
|
||||
} else if (value.kind === TaskSourceKind.Workspace) {
|
||||
result.extensionId = '$core';
|
||||
result.scope = value.config.workspaceFolder ? value.config.workspaceFolder.uri : TaskScope.Global;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
export function to(value: TaskSourceDTO, workspace: IWorkspaceContextService): ExtensionTaskSource {
|
||||
let scope: TaskScope;
|
||||
let workspaceFolder: IWorkspaceFolder | undefined;
|
||||
if ((value.scope === undefined) || ((typeof value.scope === 'number') && (value.scope !== TaskScope.Global))) {
|
||||
if (workspace.getWorkspace().folders.length === 0) {
|
||||
scope = TaskScope.Global;
|
||||
workspaceFolder = undefined;
|
||||
} else {
|
||||
scope = TaskScope.Folder;
|
||||
workspaceFolder = workspace.getWorkspace().folders[0];
|
||||
}
|
||||
} else if (typeof value.scope === 'number') {
|
||||
scope = value.scope;
|
||||
} else {
|
||||
scope = TaskScope.Folder;
|
||||
workspaceFolder = Types.withNullAsUndefined(workspace.getWorkspaceFolder(URI.revive(value.scope)));
|
||||
}
|
||||
const result: ExtensionTaskSource = {
|
||||
kind: TaskSourceKind.Extension,
|
||||
label: value.label,
|
||||
extension: value.extensionId,
|
||||
scope,
|
||||
workspaceFolder
|
||||
};
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskHandleDTO {
|
||||
export function is(value: any): value is TaskHandleDTO {
|
||||
const candidate: TaskHandleDTO = value;
|
||||
return candidate && Types.isString(candidate.id) && !!candidate.workspaceFolder;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskDTO {
|
||||
export function from(task: Task | ConfiguringTask): TaskDTO | undefined {
|
||||
if (task === undefined || task === null || (!CustomTask.is(task) && !ContributedTask.is(task) && !ConfiguringTask.is(task))) {
|
||||
return undefined;
|
||||
}
|
||||
const result: TaskDTO = {
|
||||
_id: task._id,
|
||||
name: task.configurationProperties.name,
|
||||
definition: TaskDefinitionDTO.from(task.getDefinition(true)),
|
||||
source: TaskSourceDTO.from(task._source),
|
||||
execution: undefined,
|
||||
presentationOptions: !ConfiguringTask.is(task) && task.command ? TaskPresentationOptionsDTO.from(task.command.presentation) : undefined,
|
||||
isBackground: task.configurationProperties.isBackground,
|
||||
problemMatchers: [],
|
||||
hasDefinedMatchers: ContributedTask.is(task) ? task.hasDefinedMatchers : false,
|
||||
runOptions: RunOptionsDTO.from(task.runOptions),
|
||||
};
|
||||
if (task.configurationProperties.group) {
|
||||
result.group = task.configurationProperties.group;
|
||||
}
|
||||
if (task.configurationProperties.detail) {
|
||||
result.detail = task.configurationProperties.detail;
|
||||
}
|
||||
if (!ConfiguringTask.is(task) && task.command) {
|
||||
switch (task.command.runtime) {
|
||||
case RuntimeType.Process: result.execution = ProcessExecutionDTO.from(task.command); break;
|
||||
case RuntimeType.Shell: result.execution = ShellExecutionDTO.from(task.command); break;
|
||||
case RuntimeType.CustomExecution: result.execution = CustomExecutionDTO.from(task.command); break;
|
||||
}
|
||||
}
|
||||
if (task.configurationProperties.problemMatchers) {
|
||||
for (let matcher of task.configurationProperties.problemMatchers) {
|
||||
if (Types.isString(matcher)) {
|
||||
result.problemMatchers.push(matcher);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function to(task: TaskDTO | undefined, workspace: IWorkspaceContextService, executeOnly: boolean): ContributedTask | undefined {
|
||||
if (!task || (typeof task.name !== 'string')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let command: CommandConfiguration | undefined;
|
||||
if (task.execution) {
|
||||
if (ShellExecutionDTO.is(task.execution)) {
|
||||
command = ShellExecutionDTO.to(task.execution);
|
||||
} else if (ProcessExecutionDTO.is(task.execution)) {
|
||||
command = ProcessExecutionDTO.to(task.execution);
|
||||
} else if (CustomExecutionDTO.is(task.execution)) {
|
||||
command = CustomExecutionDTO.to(task.execution);
|
||||
}
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
return undefined;
|
||||
}
|
||||
command.presentation = TaskPresentationOptionsDTO.to(task.presentationOptions);
|
||||
const source = TaskSourceDTO.to(task.source, workspace);
|
||||
|
||||
const label = nls.localize('task.label', '{0}: {1}', source.label, task.name);
|
||||
const definition = TaskDefinitionDTO.to(task.definition, executeOnly)!;
|
||||
const id = (CustomExecutionDTO.is(task.execution!) && task._id) ? task._id : `${task.source.extensionId}.${definition._key}`;
|
||||
const result: ContributedTask = new ContributedTask(
|
||||
id, // uuidMap.getUUID(identifier)
|
||||
source,
|
||||
label,
|
||||
definition.type,
|
||||
definition,
|
||||
command,
|
||||
task.hasDefinedMatchers,
|
||||
RunOptionsDTO.to(task.runOptions),
|
||||
{
|
||||
name: task.name,
|
||||
identifier: label,
|
||||
group: task.group,
|
||||
isBackground: !!task.isBackground,
|
||||
problemMatchers: task.problemMatchers.slice(),
|
||||
detail: task.detail
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TaskFilterDTO {
|
||||
export function from(value: TaskFilter): TaskFilterDTO {
|
||||
return value;
|
||||
}
|
||||
export function to(value: TaskFilterDTO | undefined): TaskFilter | undefined {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTask)
|
||||
export class MainThreadTask implements MainThreadTaskShape {
|
||||
|
||||
private readonly _extHostContext: IExtHostContext | undefined;
|
||||
private readonly _proxy: ExtHostTaskShape;
|
||||
private readonly _providers: Map<number, { disposable: IDisposable, provider: ITaskProvider }>;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ITaskService private readonly _taskService: ITaskService,
|
||||
@IWorkspaceContextService private readonly _workspaceContextServer: IWorkspaceContextService,
|
||||
@IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask);
|
||||
this._providers = new Map();
|
||||
this._taskService.onDidStateChange(async (event: TaskEvent) => {
|
||||
const task = event.__task!;
|
||||
if (event.kind === TaskEventKind.Start) {
|
||||
const execution = TaskExecutionDTO.from(task.getTaskExecution());
|
||||
let resolvedDefinition: TaskDefinitionDTO = execution.task!.definition;
|
||||
if (execution.task?.execution && CustomExecutionDTO.is(execution.task.execution) && event.resolvedVariables) {
|
||||
const dictionary: IStringDictionary<string> = {};
|
||||
Array.from(event.resolvedVariables.entries()).forEach(entry => dictionary[entry[0]] = entry[1]);
|
||||
resolvedDefinition = await this._configurationResolverService.resolveAny(task.getWorkspaceFolder(),
|
||||
execution.task.definition, dictionary);
|
||||
}
|
||||
this._proxy.$onDidStartTask(execution, event.terminalId!, resolvedDefinition);
|
||||
} else if (event.kind === TaskEventKind.ProcessStarted) {
|
||||
this._proxy.$onDidStartTaskProcess(TaskProcessStartedDTO.from(task.getTaskExecution(), event.processId!));
|
||||
} else if (event.kind === TaskEventKind.ProcessEnded) {
|
||||
this._proxy.$onDidEndTaskProcess(TaskProcessEndedDTO.from(task.getTaskExecution(), event.exitCode!));
|
||||
} else if (event.kind === TaskEventKind.End) {
|
||||
this._proxy.$OnDidEndTask(TaskExecutionDTO.from(task.getTaskExecution()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._providers.forEach((value) => {
|
||||
value.disposable.dispose();
|
||||
});
|
||||
this._providers.clear();
|
||||
}
|
||||
|
||||
$createTaskId(taskDTO: TaskDTO): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let task = TaskDTO.to(taskDTO, this._workspaceContextServer, true);
|
||||
if (task) {
|
||||
resolve(task._id);
|
||||
} else {
|
||||
reject(new Error('Task could not be created from DTO'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public $registerTaskProvider(handle: number, type: string): Promise<void> {
|
||||
const provider: ITaskProvider = {
|
||||
provideTasks: (validTypes: IStringDictionary<boolean>) => {
|
||||
return Promise.resolve(this._proxy.$provideTasks(handle, validTypes)).then((value) => {
|
||||
const tasks: Task[] = [];
|
||||
for (let dto of value.tasks) {
|
||||
const task = TaskDTO.to(dto, this._workspaceContextServer, true);
|
||||
if (task) {
|
||||
tasks.push(task);
|
||||
} else {
|
||||
console.error(`Task System: can not convert task: ${JSON.stringify(dto.definition, undefined, 0)}. Task will be dropped`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
tasks,
|
||||
extension: value.extension
|
||||
} as TaskSet;
|
||||
});
|
||||
},
|
||||
resolveTask: (task: ConfiguringTask) => {
|
||||
const dto = TaskDTO.from(task);
|
||||
|
||||
if (dto) {
|
||||
dto.name = ((dto.name === undefined) ? '' : dto.name); // Using an empty name causes the name to default to the one given by the provider.
|
||||
return Promise.resolve(this._proxy.$resolveTask(handle, dto)).then(resolvedTask => {
|
||||
if (resolvedTask) {
|
||||
return TaskDTO.to(resolvedTask, this._workspaceContextServer, true);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return Promise.resolve<ContributedTask | undefined>(undefined);
|
||||
}
|
||||
};
|
||||
const disposable = this._taskService.registerTaskProvider(provider, type);
|
||||
this._providers.set(handle, { disposable, provider });
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public $unregisterTaskProvider(handle: number): Promise<void> {
|
||||
const provider = this._providers.get(handle);
|
||||
if (provider) {
|
||||
provider.disposable.dispose();
|
||||
this._providers.delete(handle);
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
public $fetchTasks(filter?: TaskFilterDTO): Promise<TaskDTO[]> {
|
||||
return this._taskService.tasks(TaskFilterDTO.to(filter)).then((tasks) => {
|
||||
const result: TaskDTO[] = [];
|
||||
for (let task of tasks) {
|
||||
const item = TaskDTO.from(task);
|
||||
if (item) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private getWorkspace(value: UriComponents | string): string | IWorkspace | IWorkspaceFolder | null {
|
||||
let workspace;
|
||||
if (typeof value === 'string') {
|
||||
workspace = value;
|
||||
} else {
|
||||
const workspaceObject = this._workspaceContextServer.getWorkspace();
|
||||
const uri = URI.revive(value);
|
||||
if (workspaceObject.configuration?.toString() === uri.toString()) {
|
||||
workspace = workspaceObject;
|
||||
} else {
|
||||
workspace = this._workspaceContextServer.getWorkspaceFolder(uri);
|
||||
}
|
||||
}
|
||||
return workspace;
|
||||
}
|
||||
|
||||
public async $getTaskExecution(value: TaskHandleDTO | TaskDTO): Promise<TaskExecutionDTO> {
|
||||
if (TaskHandleDTO.is(value)) {
|
||||
const workspace = this.getWorkspace(value.workspaceFolder);
|
||||
if (workspace) {
|
||||
const task = await this._taskService.getTask(workspace, value.id, true);
|
||||
if (task) {
|
||||
return {
|
||||
id: task._id,
|
||||
task: TaskDTO.from(task)
|
||||
};
|
||||
}
|
||||
throw new Error('Task not found');
|
||||
} else {
|
||||
throw new Error('No workspace folder');
|
||||
}
|
||||
} else {
|
||||
const task = TaskDTO.to(value, this._workspaceContextServer, true)!;
|
||||
return {
|
||||
id: task._id,
|
||||
task: TaskDTO.from(task)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Passing in a TaskHandleDTO will cause the task to get re-resolved, which is important for tasks are coming from the core,
|
||||
// such as those gotten from a fetchTasks, since they can have missing configuration properties.
|
||||
public $executeTask(value: TaskHandleDTO | TaskDTO): Promise<TaskExecutionDTO> {
|
||||
return new Promise<TaskExecutionDTO>((resolve, reject) => {
|
||||
if (TaskHandleDTO.is(value)) {
|
||||
const workspace = this.getWorkspace(value.workspaceFolder);
|
||||
if (workspace) {
|
||||
this._taskService.getTask(workspace, value.id, true).then((task: Task | undefined) => {
|
||||
if (!task) {
|
||||
reject(new Error('Task not found'));
|
||||
} else {
|
||||
this._taskService.run(task).then(undefined, reason => {
|
||||
// eat the error, it has already been surfaced to the user and we don't care about it here
|
||||
});
|
||||
const result: TaskExecutionDTO = {
|
||||
id: value.id,
|
||||
task: TaskDTO.from(task)
|
||||
};
|
||||
resolve(result);
|
||||
}
|
||||
}, (_error) => {
|
||||
reject(new Error('Task not found'));
|
||||
});
|
||||
} else {
|
||||
reject(new Error('No workspace folder'));
|
||||
}
|
||||
} else {
|
||||
const task = TaskDTO.to(value, this._workspaceContextServer, true)!;
|
||||
this._taskService.run(task).then(undefined, reason => {
|
||||
// eat the error, it has already been surfaced to the user and we don't care about it here
|
||||
});
|
||||
const result: TaskExecutionDTO = {
|
||||
id: task._id,
|
||||
task: TaskDTO.from(task)
|
||||
};
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public $customExecutionComplete(id: string, result?: number): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._taskService.getActiveTasks().then((tasks) => {
|
||||
for (let task of tasks) {
|
||||
if (id === task._id) {
|
||||
this._taskService.extensionCallbackTaskComplete(task, result).then((value) => {
|
||||
resolve(undefined);
|
||||
}, (error) => {
|
||||
reject(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
reject(new Error('Task to mark as complete not found'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public $terminateTask(id: string): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this._taskService.getActiveTasks().then((tasks) => {
|
||||
for (let task of tasks) {
|
||||
if (id === task._id) {
|
||||
this._taskService.terminate(task).then((value) => {
|
||||
resolve(undefined);
|
||||
}, (error) => {
|
||||
reject(undefined);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
reject(new Error('Task to terminate not found'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public $registerTaskSystem(key: string, info: TaskSystemInfoDTO): void {
|
||||
let platform: Platform.Platform;
|
||||
switch (info.platform) {
|
||||
case 'Web':
|
||||
platform = Platform.Platform.Web;
|
||||
break;
|
||||
case 'win32':
|
||||
platform = Platform.Platform.Windows;
|
||||
break;
|
||||
case 'darwin':
|
||||
platform = Platform.Platform.Mac;
|
||||
break;
|
||||
case 'linux':
|
||||
platform = Platform.Platform.Linux;
|
||||
break;
|
||||
default:
|
||||
platform = Platform.platform;
|
||||
}
|
||||
this._taskService.registerTaskSystem(key, {
|
||||
platform: platform,
|
||||
uriProvider: (path: string): URI => {
|
||||
return URI.parse(`${info.scheme}://${info.authority}${path}`);
|
||||
},
|
||||
context: this._extHostContext,
|
||||
resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise<ResolvedVariables | undefined> => {
|
||||
const vars: string[] = [];
|
||||
toResolve.variables.forEach(item => vars.push(item));
|
||||
return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => {
|
||||
const partiallyResolvedVars = new Array<string>();
|
||||
forEach(values.variables, (entry) => {
|
||||
partiallyResolvedVars.push(entry.value);
|
||||
});
|
||||
return new Promise<ResolvedVariables | undefined>((resolve, reject) => {
|
||||
this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks', undefined, target).then(resolvedVars => {
|
||||
if (!resolvedVars) {
|
||||
resolve(undefined);
|
||||
}
|
||||
|
||||
const result: ResolvedVariables = {
|
||||
process: undefined,
|
||||
variables: new Map<string, string>()
|
||||
};
|
||||
for (let i = 0; i < partiallyResolvedVars.length; i++) {
|
||||
const variableName = vars[i].substring(2, vars[i].length - 1);
|
||||
if (resolvedVars && values.variables[vars[i]] === vars[i]) {
|
||||
const resolved = resolvedVars.get(variableName);
|
||||
if (typeof resolved === 'string') {
|
||||
result.variables.set(variableName, resolved);
|
||||
}
|
||||
} else {
|
||||
result.variables.set(variableName, partiallyResolvedVars[i]);
|
||||
}
|
||||
}
|
||||
if (Types.isString(values.process)) {
|
||||
result.process = values.process;
|
||||
}
|
||||
resolve(result);
|
||||
}, reason => {
|
||||
reject(reason);
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
getDefaultShellAndArgs: (): Promise<{ shell: string, args: string[] | string | undefined }> => {
|
||||
return Promise.resolve(this._proxy.$getDefaultShellAndArgs());
|
||||
},
|
||||
findExecutable: (command: string, cwd?: string, paths?: string[]): Promise<string | undefined> => {
|
||||
return this._proxy.$findExecutable(command, cwd, paths);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async $registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): Promise<void> {
|
||||
return this._taskService.registerSupportedExecutions(custom, shell, process);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { MainThreadTelemetryShape, MainContext, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTelemetry)
|
||||
export class MainThreadTelemetry implements MainThreadTelemetryShape {
|
||||
|
||||
private static readonly _name = 'pluginHostTelemetry';
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
//
|
||||
}
|
||||
|
||||
$publicLog(eventName: string, data: any = Object.create(null)): void {
|
||||
// __GDPR__COMMON__ "pluginHostTelemetry" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
|
||||
data[MainThreadTelemetry._name] = true;
|
||||
this._telemetryService.publicLog(eventName, data);
|
||||
}
|
||||
|
||||
$publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data: StrictPropertyCheck<T, E>): void {
|
||||
this.$publicLog(eventName, data as any);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,430 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal';
|
||||
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { StopWatch } from 'vs/base/common/stopwatch';
|
||||
import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITerminalExternalLinkProvider, ITerminalLink } from 'vs/workbench/contrib/terminal/browser/terminal';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
|
||||
import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
|
||||
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
|
||||
|
||||
private _proxy: ExtHostTerminalServiceShape;
|
||||
private _remoteAuthority: string | null;
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
|
||||
private _dataEventTracker: TerminalDataEventTracker | undefined;
|
||||
/**
|
||||
* A single shared terminal link provider for the exthost. When an ext registers a link
|
||||
* provider, this is registered with the terminal on the renderer side and all links are
|
||||
* provided through this, even from multiple ext link providers. Xterm should remove lower
|
||||
* priority intersecting links itself.
|
||||
*/
|
||||
private _linkProvider: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ITerminalService private readonly _terminalService: ITerminalService,
|
||||
@ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService,
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
|
||||
this._remoteAuthority = extHostContext.remoteAuthority;
|
||||
|
||||
// ITerminalService listeners
|
||||
this._toDispose.add(_terminalService.onInstanceCreated((instance) => {
|
||||
// Delay this message so the TerminalInstance constructor has a chance to finish and
|
||||
// return the ID normally to the extension host. The ID that is passed here will be
|
||||
// used to register non-extension API terminals in the extension host.
|
||||
setTimeout(() => {
|
||||
this._onTerminalOpened(instance);
|
||||
this._onInstanceDimensionsChanged(instance);
|
||||
}, EXT_HOST_CREATION_DELAY);
|
||||
}));
|
||||
|
||||
this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceProcessIdReady(instance => this._onTerminalProcessIdReady(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceDimensionsChanged(instance => this._onInstanceDimensionsChanged(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceMaximumDimensionsChanged(instance => this._onInstanceMaximumDimensionsChanged(instance)));
|
||||
this._toDispose.add(_terminalService.onInstanceRequestSpawnExtHostProcess(request => this._onRequestSpawnExtHostProcess(request)));
|
||||
this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e)));
|
||||
this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.id : null)));
|
||||
this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.id, instance.title)));
|
||||
this._toDispose.add(_terminalService.configHelper.onWorkspacePermissionsChanged(isAllowed => this._onWorkspacePermissionsChanged(isAllowed)));
|
||||
this._toDispose.add(_terminalService.onRequestAvailableShells(e => this._onRequestAvailableShells(e)));
|
||||
|
||||
// ITerminalInstanceService listeners
|
||||
if (terminalInstanceService.onRequestDefaultShellAndArgs) {
|
||||
this._toDispose.add(terminalInstanceService.onRequestDefaultShellAndArgs(e => this._onRequestDefaultShellAndArgs(e)));
|
||||
}
|
||||
|
||||
// Set initial ext host state
|
||||
this._terminalService.terminalInstances.forEach(t => {
|
||||
this._onTerminalOpened(t);
|
||||
t.processReady.then(() => this._onTerminalProcessIdReady(t));
|
||||
});
|
||||
const activeInstance = this._terminalService.getActiveInstance();
|
||||
if (activeInstance) {
|
||||
this._proxy.$acceptActiveTerminalChanged(activeInstance.id);
|
||||
}
|
||||
if (this._environmentVariableService.collections.size > 0) {
|
||||
const collectionAsArray = [...this._environmentVariableService.collections.entries()];
|
||||
const serializedCollections: [string, ISerializableEnvironmentVariableCollection][] = collectionAsArray.map(e => {
|
||||
return [e[0], serializeEnvironmentVariableCollection(e[1].map)];
|
||||
});
|
||||
this._proxy.$initEnvironmentVariableCollections(serializedCollections);
|
||||
}
|
||||
|
||||
this._terminalService.extHostReady(extHostContext.remoteAuthority!); // TODO@Tyriar: remove null assertion
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toDispose.dispose();
|
||||
this._linkProvider?.dispose();
|
||||
|
||||
// TODO@Daniel: Should all the previously created terminals be disposed
|
||||
// when the extension host process goes down ?
|
||||
}
|
||||
|
||||
public $createTerminal(launchConfig: TerminalLaunchConfig): Promise<{ id: number, name: string }> {
|
||||
const shellLaunchConfig: IShellLaunchConfig = {
|
||||
name: launchConfig.name,
|
||||
executable: launchConfig.shellPath,
|
||||
args: launchConfig.shellArgs,
|
||||
cwd: typeof launchConfig.cwd === 'string' ? launchConfig.cwd : URI.revive(launchConfig.cwd),
|
||||
waitOnExit: launchConfig.waitOnExit,
|
||||
ignoreConfigurationCwd: true,
|
||||
env: launchConfig.env,
|
||||
strictEnv: launchConfig.strictEnv,
|
||||
hideFromUser: launchConfig.hideFromUser,
|
||||
isExtensionTerminal: launchConfig.isExtensionTerminal,
|
||||
isFeatureTerminal: launchConfig.isFeatureTerminal
|
||||
};
|
||||
const terminal = this._terminalService.createTerminal(shellLaunchConfig);
|
||||
return Promise.resolve({
|
||||
id: terminal.id,
|
||||
name: terminal.title
|
||||
});
|
||||
}
|
||||
|
||||
public $show(terminalId: number, preserveFocus: boolean): void {
|
||||
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
|
||||
if (terminalInstance) {
|
||||
this._terminalService.setActiveInstance(terminalInstance);
|
||||
this._terminalService.showPanel(!preserveFocus);
|
||||
}
|
||||
}
|
||||
|
||||
public $hide(terminalId: number): void {
|
||||
const instance = this._terminalService.getActiveInstance();
|
||||
if (instance && instance.id === terminalId) {
|
||||
this._terminalService.hidePanel();
|
||||
}
|
||||
}
|
||||
|
||||
public $dispose(terminalId: number): void {
|
||||
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
|
||||
if (terminalInstance) {
|
||||
terminalInstance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public $sendText(terminalId: number, text: string, addNewLine: boolean): void {
|
||||
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
|
||||
if (terminalInstance) {
|
||||
terminalInstance.sendText(text, addNewLine);
|
||||
}
|
||||
}
|
||||
|
||||
public $startSendingDataEvents(): void {
|
||||
if (!this._dataEventTracker) {
|
||||
this._dataEventTracker = this._instantiationService.createInstance(TerminalDataEventTracker, (id, data) => {
|
||||
this._onTerminalData(id, data);
|
||||
});
|
||||
// Send initial events if they exist
|
||||
this._terminalService.terminalInstances.forEach(t => {
|
||||
t.initialDataEvents?.forEach(d => this._onTerminalData(t.id, d));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public $stopSendingDataEvents(): void {
|
||||
if (this._dataEventTracker) {
|
||||
this._dataEventTracker.dispose();
|
||||
this._dataEventTracker = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public $startLinkProvider(): void {
|
||||
this._linkProvider?.dispose();
|
||||
this._linkProvider = this._terminalService.registerLinkProvider(new ExtensionTerminalLinkProvider(this._proxy));
|
||||
}
|
||||
|
||||
public $stopLinkProvider(): void {
|
||||
this._linkProvider?.dispose();
|
||||
this._linkProvider = undefined;
|
||||
}
|
||||
|
||||
public $registerProcessSupport(isSupported: boolean): void {
|
||||
this._terminalService.registerProcessSupport(isSupported);
|
||||
}
|
||||
|
||||
private _onActiveTerminalChanged(terminalId: number | null): void {
|
||||
this._proxy.$acceptActiveTerminalChanged(terminalId);
|
||||
}
|
||||
|
||||
private _onTerminalData(terminalId: number, data: string): void {
|
||||
this._proxy.$acceptTerminalProcessData(terminalId, data);
|
||||
}
|
||||
|
||||
private _onTitleChanged(terminalId: number, name: string): void {
|
||||
this._proxy.$acceptTerminalTitleChange(terminalId, name);
|
||||
}
|
||||
|
||||
private _onWorkspacePermissionsChanged(isAllowed: boolean): void {
|
||||
this._proxy.$acceptWorkspacePermissionsChanged(isAllowed);
|
||||
}
|
||||
|
||||
private _onTerminalDisposed(terminalInstance: ITerminalInstance): void {
|
||||
this._proxy.$acceptTerminalClosed(terminalInstance.id, terminalInstance.exitCode);
|
||||
}
|
||||
|
||||
private _onTerminalOpened(terminalInstance: ITerminalInstance): void {
|
||||
const shellLaunchConfigDto: IShellLaunchConfigDto = {
|
||||
name: terminalInstance.shellLaunchConfig.name,
|
||||
executable: terminalInstance.shellLaunchConfig.executable,
|
||||
args: terminalInstance.shellLaunchConfig.args,
|
||||
cwd: terminalInstance.shellLaunchConfig.cwd,
|
||||
env: terminalInstance.shellLaunchConfig.env,
|
||||
hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser
|
||||
};
|
||||
if (terminalInstance.title) {
|
||||
this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto);
|
||||
} else {
|
||||
terminalInstance.waitForTitle().then(title => {
|
||||
this._proxy.$acceptTerminalOpened(terminalInstance.id, title, shellLaunchConfigDto);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void {
|
||||
if (terminalInstance.processId === undefined) {
|
||||
return;
|
||||
}
|
||||
this._proxy.$acceptTerminalProcessId(terminalInstance.id, terminalInstance.processId);
|
||||
}
|
||||
|
||||
private _onInstanceDimensionsChanged(instance: ITerminalInstance): void {
|
||||
this._proxy.$acceptTerminalDimensions(instance.id, instance.cols, instance.rows);
|
||||
}
|
||||
|
||||
private _onInstanceMaximumDimensionsChanged(instance: ITerminalInstance): void {
|
||||
this._proxy.$acceptTerminalMaximumDimensions(instance.id, instance.maxCols, instance.maxRows);
|
||||
}
|
||||
|
||||
private _onRequestSpawnExtHostProcess(request: ISpawnExtHostProcessRequest): void {
|
||||
// Only allow processes on remote ext hosts
|
||||
if (!this._remoteAuthority) {
|
||||
return;
|
||||
}
|
||||
|
||||
const proxy = request.proxy;
|
||||
this._terminalProcessProxies.set(proxy.terminalId, proxy);
|
||||
const shellLaunchConfigDto: IShellLaunchConfigDto = {
|
||||
name: request.shellLaunchConfig.name,
|
||||
executable: request.shellLaunchConfig.executable,
|
||||
args: request.shellLaunchConfig.args,
|
||||
cwd: request.shellLaunchConfig.cwd,
|
||||
env: request.shellLaunchConfig.env
|
||||
};
|
||||
|
||||
this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request });
|
||||
this._proxy.$spawnExtHostProcess(
|
||||
proxy.terminalId,
|
||||
shellLaunchConfigDto,
|
||||
request.activeWorkspaceRootUri,
|
||||
request.cols,
|
||||
request.rows,
|
||||
request.isWorkspaceShellAllowed
|
||||
).then(request.callback);
|
||||
|
||||
proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data));
|
||||
proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows));
|
||||
proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate));
|
||||
proxy.onRequestCwd(() => this._proxy.$acceptProcessRequestCwd(proxy.terminalId));
|
||||
proxy.onRequestInitialCwd(() => this._proxy.$acceptProcessRequestInitialCwd(proxy.terminalId));
|
||||
proxy.onRequestLatency(() => this._onRequestLatency(proxy.terminalId));
|
||||
}
|
||||
|
||||
private _onRequestStartExtensionTerminal(request: IStartExtensionTerminalRequest): void {
|
||||
const proxy = request.proxy;
|
||||
this._terminalProcessProxies.set(proxy.terminalId, proxy);
|
||||
|
||||
// Note that onReisze is not being listened to here as it needs to fire when max dimensions
|
||||
// change, excluding the dimension override
|
||||
const initialDimensions: ITerminalDimensionsDto | undefined = request.cols && request.rows ? {
|
||||
columns: request.cols,
|
||||
rows: request.rows
|
||||
} : undefined;
|
||||
|
||||
this._proxy.$startExtensionTerminal(
|
||||
proxy.terminalId,
|
||||
initialDimensions
|
||||
).then(request.callback);
|
||||
|
||||
proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data));
|
||||
proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate));
|
||||
proxy.onRequestCwd(() => this._proxy.$acceptProcessRequestCwd(proxy.terminalId));
|
||||
proxy.onRequestInitialCwd(() => this._proxy.$acceptProcessRequestInitialCwd(proxy.terminalId));
|
||||
proxy.onRequestLatency(() => this._onRequestLatency(proxy.terminalId));
|
||||
}
|
||||
|
||||
public $sendProcessTitle(terminalId: number, title: string): void {
|
||||
this._getTerminalProcess(terminalId).emitTitle(title);
|
||||
}
|
||||
|
||||
public $sendProcessData(terminalId: number, data: string): void {
|
||||
this._getTerminalProcess(terminalId).emitData(data);
|
||||
}
|
||||
|
||||
public $sendProcessReady(terminalId: number, pid: number, cwd: string): void {
|
||||
this._getTerminalProcess(terminalId).emitReady(pid, cwd);
|
||||
}
|
||||
|
||||
public $sendProcessExit(terminalId: number, exitCode: number | undefined): void {
|
||||
this._getTerminalProcess(terminalId).emitExit(exitCode);
|
||||
this._terminalProcessProxies.delete(terminalId);
|
||||
}
|
||||
|
||||
public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void {
|
||||
this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions);
|
||||
}
|
||||
|
||||
public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void {
|
||||
this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd);
|
||||
}
|
||||
|
||||
public $sendProcessCwd(terminalId: number, cwd: string): void {
|
||||
this._getTerminalProcess(terminalId).emitCwd(cwd);
|
||||
}
|
||||
|
||||
public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void {
|
||||
const instance = this._terminalService.getInstanceFromId(terminalId);
|
||||
if (instance) {
|
||||
this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig);
|
||||
}
|
||||
}
|
||||
|
||||
private async _onRequestLatency(terminalId: number): Promise<void> {
|
||||
const COUNT = 2;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
const sw = StopWatch.create(true);
|
||||
await this._proxy.$acceptProcessRequestLatency(terminalId);
|
||||
sw.stop();
|
||||
sum += sw.elapsed();
|
||||
}
|
||||
this._getTerminalProcess(terminalId).emitLatency(sum / COUNT);
|
||||
}
|
||||
|
||||
private _isPrimaryExtHost(): boolean {
|
||||
// The "primary" ext host is the remote ext host if there is one, otherwise the local
|
||||
const conn = this._remoteAgentService.getConnection();
|
||||
if (conn) {
|
||||
return this._remoteAuthority === conn.remoteAuthority;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async _onRequestAvailableShells(req: IAvailableShellsRequest): Promise<void> {
|
||||
if (this._isPrimaryExtHost()) {
|
||||
req.callback(await this._proxy.$getAvailableShells());
|
||||
}
|
||||
}
|
||||
|
||||
private async _onRequestDefaultShellAndArgs(req: IDefaultShellAndArgsRequest): Promise<void> {
|
||||
if (this._isPrimaryExtHost()) {
|
||||
const res = await this._proxy.$getDefaultShellAndArgs(req.useAutomationShell);
|
||||
req.callback(res.shell, res.args);
|
||||
}
|
||||
}
|
||||
|
||||
private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy {
|
||||
const terminal = this._terminalProcessProxies.get(terminalId);
|
||||
if (!terminal) {
|
||||
throw new Error(`Unknown terminal: ${terminalId}`);
|
||||
}
|
||||
return terminal;
|
||||
}
|
||||
|
||||
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void {
|
||||
if (collection) {
|
||||
const translatedCollection = {
|
||||
persistent,
|
||||
map: deserializeEnvironmentVariableCollection(collection)
|
||||
};
|
||||
this._environmentVariableService.set(extensionIdentifier, translatedCollection);
|
||||
} else {
|
||||
this._environmentVariableService.delete(extensionIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates temporary tracking of data events from terminal instances, once disposed all
|
||||
* listeners are removed.
|
||||
*/
|
||||
class TerminalDataEventTracker extends Disposable {
|
||||
private readonly _bufferer: TerminalDataBufferer;
|
||||
|
||||
constructor(
|
||||
private readonly _callback: (id: number, data: string) => void,
|
||||
@ITerminalService private readonly _terminalService: ITerminalService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this._bufferer = new TerminalDataBufferer(this._callback));
|
||||
|
||||
this._terminalService.terminalInstances.forEach(instance => this._registerInstance(instance));
|
||||
this._register(this._terminalService.onInstanceCreated(instance => this._registerInstance(instance)));
|
||||
this._register(this._terminalService.onInstanceDisposed(instance => this._bufferer.stopBuffering(instance.id)));
|
||||
}
|
||||
|
||||
private _registerInstance(instance: ITerminalInstance): void {
|
||||
// Buffer data events to reduce the amount of messages going to the extension host
|
||||
this._register(this._bufferer.startBuffering(instance.id, instance.onData));
|
||||
}
|
||||
}
|
||||
|
||||
class ExtensionTerminalLinkProvider implements ITerminalExternalLinkProvider {
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostTerminalServiceShape
|
||||
) {
|
||||
}
|
||||
|
||||
async provideLinks(instance: ITerminalInstance, line: string): Promise<ITerminalLink[] | undefined> {
|
||||
const proxy = this._proxy;
|
||||
const extHostLinks = await proxy.$provideLinks(instance.id, line);
|
||||
return extHostLinks.map(dto => ({
|
||||
id: dto.id,
|
||||
startIndex: dto.startIndex,
|
||||
length: dto.length,
|
||||
label: dto.label,
|
||||
activate: () => proxy.$activateLink(instance.id, dto.id)
|
||||
}));
|
||||
}
|
||||
}
|
||||
34
lib/vscode/src/vs/workbench/api/browser/mainThreadTheming.ts
Normal file
34
lib/vscode/src/vs/workbench/api/browser/mainThreadTheming.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MainContext, IExtHostContext, ExtHostThemingShape, ExtHostContext, MainThreadThemingShape } from '../common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTheming)
|
||||
export class MainThreadTheming implements MainThreadThemingShape {
|
||||
|
||||
private readonly _themeService: IThemeService;
|
||||
private readonly _proxy: ExtHostThemingShape;
|
||||
private readonly _themeChangeListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
this._themeService = themeService;
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTheming);
|
||||
|
||||
this._themeChangeListener = this._themeService.onDidColorThemeChange(e => {
|
||||
this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type);
|
||||
});
|
||||
this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._themeChangeListener.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { MainContext, MainThreadTimelineShape, IExtHostContext, ExtHostTimelineShape, ExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, ITimelineService, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTimeline)
|
||||
export class MainThreadTimeline implements MainThreadTimelineShape {
|
||||
private readonly _proxy: ExtHostTimelineShape;
|
||||
private readonly _providerEmitters = new Map<string, Emitter<TimelineChangeEvent>>();
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@ITimelineService private readonly _timelineService: ITimelineService
|
||||
) {
|
||||
this._proxy = context.getProxy(ExtHostContext.ExtHostTimeline);
|
||||
}
|
||||
|
||||
$registerTimelineProvider(provider: TimelineProviderDescriptor): void {
|
||||
this.logService.trace(`MainThreadTimeline#registerTimelineProvider: id=${provider.id}`);
|
||||
|
||||
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, token: CancellationToken, internalOptions?: InternalTimelineOptions) {
|
||||
return proxy.$getTimeline(provider.id, uri, options, token, internalOptions);
|
||||
},
|
||||
dispose() {
|
||||
emitters.delete(provider.id);
|
||||
onDidChange?.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$unregisterTimelineProvider(id: string): void {
|
||||
this.logService.trace(`MainThreadTimeline#unregisterTimelineProvider: id=${id}`);
|
||||
|
||||
this._timelineService.unregisterTimelineProvider(id);
|
||||
}
|
||||
|
||||
$emitTimelineChangeEvent(e: TimelineChangeEvent): void {
|
||||
this.logService.trace(`MainThreadTimeline#emitChangeEvent: id=${e.id}, uri=${e.uri?.toString(true)}`);
|
||||
|
||||
const emitter = this._providerEmitters.get(e.id!);
|
||||
emitter?.fire(e);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
244
lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts
Normal file
244
lib/vscode/src/vs/workbench/api/browser/mainThreadTreeViews.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ExtHostContext, MainThreadTreeViewsShape, ExtHostTreeViewsShape, MainContext, IExtHostContext } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ITreeViewDataProvider, ITreeItem, IViewsService, ITreeView, IViewsRegistry, ITreeViewDescriptor, IRevealOptions, Extensions, ResolvableTreeItem } from 'vs/workbench/common/views';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { isUndefinedOrNull, isNumber } from 'vs/base/common/types';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTreeViews)
|
||||
export class MainThreadTreeViews extends Disposable implements MainThreadTreeViewsShape {
|
||||
|
||||
private readonly _proxy: ExtHostTreeViewsShape;
|
||||
private readonly _dataProviders: Map<string, TreeViewDataProvider> = new Map<string, TreeViewDataProvider>();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IExtensionService private readonly extensionService: IExtensionService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTreeViews);
|
||||
}
|
||||
|
||||
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean, canSelectMany: boolean }): void {
|
||||
this.logService.trace('MainThreadTreeViews#$registerTreeViewDataProvider', treeViewId, options);
|
||||
|
||||
this.extensionService.whenInstalledExtensionsRegistered().then(() => {
|
||||
const dataProvider = new TreeViewDataProvider(treeViewId, this._proxy, this.notificationService);
|
||||
this._dataProviders.set(treeViewId, dataProvider);
|
||||
const viewer = this.getTreeView(treeViewId);
|
||||
if (viewer) {
|
||||
// Order is important here. The internal tree isn't created until the dataProvider is set.
|
||||
// Set all other properties first!
|
||||
viewer.showCollapseAllAction = !!options.showCollapseAll;
|
||||
viewer.canSelectMany = !!options.canSelectMany;
|
||||
viewer.dataProvider = dataProvider;
|
||||
this.registerListeners(treeViewId, viewer);
|
||||
this._proxy.$setVisible(treeViewId, viewer.visible);
|
||||
} else {
|
||||
this.notificationService.error('No view is registered with id: ' + treeViewId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$reveal(treeViewId: string, item: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise<void> {
|
||||
this.logService.trace('MainThreadTreeViews#$reveal', treeViewId, item, parentChain, options);
|
||||
|
||||
return this.viewsService.openView(treeViewId, options.focus)
|
||||
.then(() => {
|
||||
const viewer = this.getTreeView(treeViewId);
|
||||
if (viewer) {
|
||||
return this.reveal(viewer, this._dataProviders.get(treeViewId)!, item, parentChain, options);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
$refresh(treeViewId: string, itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): Promise<void> {
|
||||
this.logService.trace('MainThreadTreeViews#$refresh', treeViewId, itemsToRefreshByHandle);
|
||||
|
||||
const viewer = this.getTreeView(treeViewId);
|
||||
const dataProvider = this._dataProviders.get(treeViewId);
|
||||
if (viewer && dataProvider) {
|
||||
const itemsToRefresh = dataProvider.getItemsToRefresh(itemsToRefreshByHandle);
|
||||
return viewer.refresh(itemsToRefresh.length ? itemsToRefresh : undefined);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
$setMessage(treeViewId: string, message: string): void {
|
||||
this.logService.trace('MainThreadTreeViews#$setMessage', treeViewId, message);
|
||||
|
||||
const viewer = this.getTreeView(treeViewId);
|
||||
if (viewer) {
|
||||
viewer.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
$setTitle(treeViewId: string, title: string, description: string | undefined): void {
|
||||
this.logService.trace('MainThreadTreeViews#$setTitle', treeViewId, title, description);
|
||||
|
||||
const viewer = this.getTreeView(treeViewId);
|
||||
if (viewer) {
|
||||
viewer.title = title;
|
||||
viewer.description = description;
|
||||
}
|
||||
}
|
||||
|
||||
private async reveal(treeView: ITreeView, dataProvider: TreeViewDataProvider, itemIn: ITreeItem, parentChain: ITreeItem[], options: IRevealOptions): Promise<void> {
|
||||
options = options ? options : { select: false, focus: false };
|
||||
const select = isUndefinedOrNull(options.select) ? false : options.select;
|
||||
const focus = isUndefinedOrNull(options.focus) ? false : options.focus;
|
||||
let expand = Math.min(isNumber(options.expand) ? options.expand : options.expand === true ? 1 : 0, 3);
|
||||
|
||||
if (dataProvider.isEmpty()) {
|
||||
// Refresh if empty
|
||||
await treeView.refresh();
|
||||
}
|
||||
for (const parent of parentChain) {
|
||||
const parentItem = dataProvider.getItem(parent.handle);
|
||||
if (parentItem) {
|
||||
await treeView.expand(parentItem);
|
||||
}
|
||||
}
|
||||
const item = dataProvider.getItem(itemIn.handle);
|
||||
if (item) {
|
||||
await treeView.reveal(item);
|
||||
if (select) {
|
||||
treeView.setSelection([item]);
|
||||
}
|
||||
if (focus) {
|
||||
treeView.setFocus(item);
|
||||
}
|
||||
let itemsToExpand = [item];
|
||||
for (; itemsToExpand.length > 0 && expand > 0; expand--) {
|
||||
await treeView.expand(itemsToExpand);
|
||||
itemsToExpand = itemsToExpand.reduce((result, itemValue) => {
|
||||
const item = dataProvider.getItem(itemValue.handle);
|
||||
if (item && item.children && item.children.length) {
|
||||
result.push(...item.children);
|
||||
}
|
||||
return result;
|
||||
}, [] as ITreeItem[]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerListeners(treeViewId: string, treeView: ITreeView): void {
|
||||
this._register(treeView.onDidExpandItem(item => this._proxy.$setExpanded(treeViewId, item.handle, true)));
|
||||
this._register(treeView.onDidCollapseItem(item => this._proxy.$setExpanded(treeViewId, item.handle, false)));
|
||||
this._register(treeView.onDidChangeSelection(items => this._proxy.$setSelection(treeViewId, items.map(({ handle }) => handle))));
|
||||
this._register(treeView.onDidChangeVisibility(isVisible => this._proxy.$setVisible(treeViewId, isVisible)));
|
||||
}
|
||||
|
||||
private getTreeView(treeViewId: string): ITreeView | null {
|
||||
const viewDescriptor: ITreeViewDescriptor = <ITreeViewDescriptor>Registry.as<IViewsRegistry>(Extensions.ViewsRegistry).getView(treeViewId);
|
||||
return viewDescriptor ? viewDescriptor.treeView : null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._dataProviders.forEach((dataProvider, treeViewId) => {
|
||||
const treeView = this.getTreeView(treeViewId);
|
||||
if (treeView) {
|
||||
treeView.dataProvider = undefined;
|
||||
}
|
||||
});
|
||||
this._dataProviders.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
type TreeItemHandle = string;
|
||||
|
||||
class TreeViewDataProvider implements ITreeViewDataProvider {
|
||||
|
||||
private readonly itemsMap: Map<TreeItemHandle, ITreeItem> = new Map<TreeItemHandle, ITreeItem>();
|
||||
private hasResolve: Promise<boolean>;
|
||||
|
||||
constructor(private readonly treeViewId: string,
|
||||
private readonly _proxy: ExtHostTreeViewsShape,
|
||||
private readonly notificationService: INotificationService
|
||||
) {
|
||||
this.hasResolve = this._proxy.$hasResolve(this.treeViewId);
|
||||
}
|
||||
|
||||
getChildren(treeItem?: ITreeItem): Promise<ITreeItem[]> {
|
||||
return Promise.resolve(this._proxy.$getChildren(this.treeViewId, treeItem ? treeItem.handle : undefined)
|
||||
.then(
|
||||
children => this.postGetChildren(children),
|
||||
err => {
|
||||
this.notificationService.error(err);
|
||||
return [];
|
||||
}));
|
||||
}
|
||||
|
||||
getItemsToRefresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeItem }): ITreeItem[] {
|
||||
const itemsToRefresh: ITreeItem[] = [];
|
||||
if (itemsToRefreshByHandle) {
|
||||
for (const treeItemHandle of Object.keys(itemsToRefreshByHandle)) {
|
||||
const currentTreeItem = this.getItem(treeItemHandle);
|
||||
if (currentTreeItem) { // Refresh only if the item exists
|
||||
const treeItem = itemsToRefreshByHandle[treeItemHandle];
|
||||
// Update the current item with refreshed item
|
||||
this.updateTreeItem(currentTreeItem, treeItem);
|
||||
if (treeItemHandle === treeItem.handle) {
|
||||
itemsToRefresh.push(currentTreeItem);
|
||||
} else {
|
||||
// Update maps when handle is changed and refresh parent
|
||||
this.itemsMap.delete(treeItemHandle);
|
||||
this.itemsMap.set(currentTreeItem.handle, currentTreeItem);
|
||||
const parent = treeItem.parentHandle ? this.itemsMap.get(treeItem.parentHandle) : null;
|
||||
if (parent) {
|
||||
itemsToRefresh.push(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return itemsToRefresh;
|
||||
}
|
||||
|
||||
getItem(treeItemHandle: string): ITreeItem | undefined {
|
||||
return this.itemsMap.get(treeItemHandle);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.itemsMap.size === 0;
|
||||
}
|
||||
|
||||
private async postGetChildren(elements: ITreeItem[]): Promise<ResolvableTreeItem[]> {
|
||||
const result: ResolvableTreeItem[] = [];
|
||||
const hasResolve = await this.hasResolve;
|
||||
if (elements) {
|
||||
for (const element of elements) {
|
||||
const resolvable = new ResolvableTreeItem(element, hasResolve ? () => {
|
||||
return this._proxy.$resolve(this.treeViewId, element.handle);
|
||||
} : undefined);
|
||||
this.itemsMap.set(element.handle, resolvable);
|
||||
result.push(resolvable);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private updateTreeItem(current: ITreeItem, treeItem: ITreeItem): void {
|
||||
treeItem.children = treeItem.children ? treeItem.children : undefined;
|
||||
if (current) {
|
||||
const properties = distinct([...Object.keys(current), ...Object.keys(treeItem)]);
|
||||
for (const property of properties) {
|
||||
(<any>current)[property] = (<any>treeItem)[property];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IRemoteExplorerService, MakeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
|
||||
import { ITunnelProvider, ITunnelService, TunnelOptions } from 'vs/platform/remote/common/tunnel';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTunnelService)
|
||||
export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape {
|
||||
private readonly _proxy: ExtHostTunnelServiceShape;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
|
||||
@ITunnelService private readonly tunnelService: ITunnelService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService);
|
||||
this._register(tunnelService.onTunnelOpened(() => this._proxy.$onDidTunnelsChange()));
|
||||
this._register(tunnelService.onTunnelClosed(() => this._proxy.$onDidTunnelsChange()));
|
||||
}
|
||||
|
||||
async $openTunnel(tunnelOptions: TunnelOptions): Promise<TunnelDto | undefined> {
|
||||
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label);
|
||||
if (tunnel) {
|
||||
return TunnelDto.fromServiceTunnel(tunnel);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async $closeTunnel(remote: { host: string, port: number }): Promise<void> {
|
||||
return this.remoteExplorerService.close(remote);
|
||||
}
|
||||
|
||||
async $getTunnels(): Promise<TunnelDescription[]> {
|
||||
return (await this.tunnelService.tunnels).map(tunnel => {
|
||||
return {
|
||||
remoteAddress: { port: tunnel.tunnelRemotePort, host: tunnel.tunnelRemoteHost },
|
||||
localAddress: tunnel.localAddress
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async $registerCandidateFinder(): Promise<void> {
|
||||
this.remoteExplorerService.registerCandidateFinder(() => this._proxy.$findCandidatePorts());
|
||||
}
|
||||
|
||||
async $tunnelServiceReady(): Promise<void> {
|
||||
return this.remoteExplorerService.restore();
|
||||
}
|
||||
|
||||
async $setTunnelProvider(): Promise<void> {
|
||||
const tunnelProvider: ITunnelProvider = {
|
||||
forwardPort: (tunnelOptions: TunnelOptions) => {
|
||||
const forward = this._proxy.$forwardPort(tunnelOptions);
|
||||
if (forward) {
|
||||
return forward.then(tunnel => {
|
||||
return {
|
||||
tunnelRemotePort: tunnel.remoteAddress.port,
|
||||
tunnelRemoteHost: tunnel.remoteAddress.host,
|
||||
localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : MakeAddress(tunnel.localAddress.host, tunnel.localAddress.port),
|
||||
tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined,
|
||||
dispose: (silent: boolean) => {
|
||||
if (!silent) {
|
||||
this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port });
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
this.tunnelService.setTunnelProvider(tunnelProvider);
|
||||
}
|
||||
|
||||
async $setCandidateFilter(): Promise<void> {
|
||||
this._register(this.remoteExplorerService.setCandidateFilter(async (candidates: { host: string, port: number, detail: string }[]): Promise<{ host: string, port: number, detail: string }[]> => {
|
||||
const filters: boolean[] = await this._proxy.$filterCandidates(candidates);
|
||||
const filteredCandidates: { host: string, port: number, detail: string }[] = [];
|
||||
if (filters.length !== candidates.length) {
|
||||
return candidates;
|
||||
}
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
if (filters[i]) {
|
||||
filteredCandidates.push(candidates[i]);
|
||||
}
|
||||
}
|
||||
return filteredCandidates;
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
}
|
||||
79
lib/vscode/src/vs/workbench/api/browser/mainThreadUrls.ts
Normal file
79
lib/vscode/src/vs/workbench/api/browser/mainThreadUrls.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ExtHostContext, IExtHostContext, MainContext, MainThreadUrlsShape, ExtHostUrlsShape } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostNamedCustomer } from '../common/extHostCustomers';
|
||||
import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IExtensionUrlHandler } from 'vs/workbench/services/extensions/browser/extensionUrlHandler';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
class ExtensionUrlHandler implements IURLHandler {
|
||||
|
||||
constructor(
|
||||
private readonly proxy: ExtHostUrlsShape,
|
||||
private readonly handle: number,
|
||||
readonly extensionId: ExtensionIdentifier
|
||||
) { }
|
||||
|
||||
handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
|
||||
if (!ExtensionIdentifier.equals(this.extensionId, uri.authority)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return Promise.resolve(this.proxy.$handleExternalUri(this.handle, uri)).then(() => true);
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadUrls)
|
||||
export class MainThreadUrls implements MainThreadUrlsShape {
|
||||
|
||||
private readonly proxy: ExtHostUrlsShape;
|
||||
private handlers = new Map<number, { extensionId: ExtensionIdentifier, disposable: IDisposable }>();
|
||||
|
||||
constructor(
|
||||
context: IExtHostContext,
|
||||
@IURLService private readonly urlService: IURLService,
|
||||
@IExtensionUrlHandler private readonly extensionUrlHandler: IExtensionUrlHandler
|
||||
) {
|
||||
this.proxy = context.getProxy(ExtHostContext.ExtHostUrls);
|
||||
}
|
||||
|
||||
$registerUriHandler(handle: number, extensionId: ExtensionIdentifier): Promise<void> {
|
||||
const handler = new ExtensionUrlHandler(this.proxy, handle, extensionId);
|
||||
const disposable = this.urlService.registerHandler(handler);
|
||||
|
||||
this.handlers.set(handle, { extensionId, disposable });
|
||||
this.extensionUrlHandler.registerExtensionHandler(extensionId, handler);
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
$unregisterUriHandler(handle: number): Promise<void> {
|
||||
const tuple = this.handlers.get(handle);
|
||||
|
||||
if (!tuple) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
const { extensionId, disposable } = tuple;
|
||||
|
||||
this.extensionUrlHandler.unregisterExtensionHandler(extensionId);
|
||||
this.handlers.delete(handle);
|
||||
disposable.dispose();
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
async $createAppUri(uri: UriComponents): Promise<URI> {
|
||||
return this.urlService.create(uri);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.handlers.forEach(({ disposable }) => disposable.dispose());
|
||||
this.handlers.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { MainThreadCustomEditors } from 'vs/workbench/api/browser/mainThreadCustomEditors';
|
||||
import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels';
|
||||
import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews';
|
||||
import { MainThreadWebviewsViews } from 'vs/workbench/api/browser/mainThreadWebviewViews';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { extHostCustomer } from '../common/extHostCustomers';
|
||||
|
||||
@extHostCustomer
|
||||
export class MainThreadWebviewManager extends Disposable {
|
||||
constructor(
|
||||
context: extHostProtocol.IExtHostContext,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const webviews = this._register(instantiationService.createInstance(MainThreadWebviews, context));
|
||||
context.set(extHostProtocol.MainContext.MainThreadWebviews, webviews);
|
||||
|
||||
const webviewPanels = this._register(instantiationService.createInstance(MainThreadWebviewPanels, context, webviews));
|
||||
context.set(extHostProtocol.MainContext.MainThreadWebviewPanels, webviewPanels);
|
||||
|
||||
const customEditors = this._register(instantiationService.createInstance(MainThreadCustomEditors, context, webviews, webviewPanels));
|
||||
context.set(extHostProtocol.MainContext.MainThreadCustomEditors, customEditors);
|
||||
|
||||
const webviewViews = this._register(instantiationService.createInstance(MainThreadWebviewsViews, context, webviews));
|
||||
context.set(extHostProtocol.MainContext.MainThreadWebviewViews, webviewViews);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { MainThreadWebviews, reviveWebviewExtension, reviveWebviewOptions } from 'vs/workbench/api/browser/mainThreadWebviews';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor';
|
||||
import { IEditorInput } from 'vs/workbench/common/editor';
|
||||
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
|
||||
import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput';
|
||||
import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService';
|
||||
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
|
||||
/**
|
||||
* Bi-directional map between webview handles and inputs.
|
||||
*/
|
||||
class WebviewInputStore {
|
||||
private readonly _handlesToInputs = new Map<string, WebviewInput>();
|
||||
private readonly _inputsToHandles = new Map<WebviewInput, string>();
|
||||
|
||||
public add(handle: string, input: WebviewInput): void {
|
||||
this._handlesToInputs.set(handle, input);
|
||||
this._inputsToHandles.set(input, handle);
|
||||
}
|
||||
|
||||
public getHandleForInput(input: WebviewInput): string | undefined {
|
||||
return this._inputsToHandles.get(input);
|
||||
}
|
||||
|
||||
public getInputForHandle(handle: string): WebviewInput | undefined {
|
||||
return this._handlesToInputs.get(handle);
|
||||
}
|
||||
|
||||
public delete(handle: string): void {
|
||||
const input = this.getInputForHandle(handle);
|
||||
this._handlesToInputs.delete(handle);
|
||||
if (input) {
|
||||
this._inputsToHandles.delete(input);
|
||||
}
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this._handlesToInputs.size;
|
||||
}
|
||||
|
||||
[Symbol.iterator](): Iterator<WebviewInput> {
|
||||
return this._handlesToInputs.values();
|
||||
}
|
||||
}
|
||||
|
||||
class WebviewViewTypeTransformer {
|
||||
public constructor(
|
||||
public readonly prefix: string,
|
||||
) { }
|
||||
|
||||
public fromExternal(viewType: string): string {
|
||||
return this.prefix + viewType;
|
||||
}
|
||||
|
||||
public toExternal(viewType: string): string | undefined {
|
||||
return viewType.startsWith(this.prefix)
|
||||
? viewType.substr(this.prefix.length)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadWebviewPanels extends Disposable implements extHostProtocol.MainThreadWebviewPanelsShape {
|
||||
|
||||
private readonly webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-');
|
||||
|
||||
private readonly _proxy: extHostProtocol.ExtHostWebviewPanelsShape;
|
||||
|
||||
private readonly _webviewInputs = new WebviewInputStore();
|
||||
|
||||
private readonly _editorProviders = new Map<string, IDisposable>();
|
||||
private readonly _webviewFromDiffEditorHandles = new Set<string>();
|
||||
|
||||
private readonly _revivers = new Map<string, IDisposable>();
|
||||
|
||||
constructor(
|
||||
context: extHostProtocol.IExtHostContext,
|
||||
private readonly _mainThreadWebviews: MainThreadWebviews,
|
||||
@IExtensionService extensionService: IExtensionService,
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@ITelemetryService private readonly _telemetryService: ITelemetryService,
|
||||
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewPanels);
|
||||
|
||||
this._register(_editorService.onDidActiveEditorChange(() => {
|
||||
const activeInput = this._editorService.activeEditor;
|
||||
if (activeInput instanceof DiffEditorInput && activeInput.primary instanceof WebviewInput && activeInput.secondary instanceof WebviewInput) {
|
||||
this.registerWebviewFromDiffEditorListeners(activeInput);
|
||||
}
|
||||
|
||||
this.updateWebviewViewStates(activeInput);
|
||||
}));
|
||||
|
||||
this._register(_editorService.onDidVisibleEditorsChange(() => {
|
||||
this.updateWebviewViewStates(this._editorService.activeEditor);
|
||||
}));
|
||||
|
||||
// This reviver's only job is to activate extensions.
|
||||
// This should trigger the real reviver to be registered from the extension host side.
|
||||
this._register(_webviewWorkbenchService.registerResolver({
|
||||
canResolve: (webview: WebviewInput) => {
|
||||
const viewType = this.webviewPanelViewType.toExternal(webview.viewType);
|
||||
if (typeof viewType === 'string') {
|
||||
extensionService.activateByEvent(`onWebviewPanel:${viewType}`);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
resolveWebview: () => { throw new Error('not implemented'); }
|
||||
}));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
dispose(this._editorProviders.values());
|
||||
this._editorProviders.clear();
|
||||
}
|
||||
|
||||
public get webviewInputs(): Iterable<WebviewInput> { return this._webviewInputs; }
|
||||
|
||||
public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void {
|
||||
this._webviewInputs.add(handle, input);
|
||||
this._mainThreadWebviews.addWebview(handle, input.webview);
|
||||
|
||||
input.webview.onDidDispose(() => {
|
||||
this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => {
|
||||
this._webviewInputs.delete(handle);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public $createWebviewPanel(
|
||||
extensionData: extHostProtocol.WebviewExtensionDescription,
|
||||
handle: extHostProtocol.WebviewHandle,
|
||||
viewType: string,
|
||||
title: string,
|
||||
showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; },
|
||||
options: WebviewInputOptions
|
||||
): void {
|
||||
const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null);
|
||||
if (showOptions) {
|
||||
mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus;
|
||||
mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn);
|
||||
}
|
||||
|
||||
const extension = reviveWebviewExtension(extensionData);
|
||||
|
||||
const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension);
|
||||
this.addWebviewInput(handle, webview);
|
||||
|
||||
/* __GDPR__
|
||||
"webviews:createWebviewPanel" : {
|
||||
"extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
||||
}
|
||||
*/
|
||||
this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value });
|
||||
}
|
||||
|
||||
public $disposeWebview(handle: extHostProtocol.WebviewHandle): void {
|
||||
const webview = this.getWebviewInput(handle);
|
||||
webview.dispose();
|
||||
}
|
||||
|
||||
public $setTitle(handle: extHostProtocol.WebviewHandle, value: string): void {
|
||||
const webview = this.getWebviewInput(handle);
|
||||
webview.setName(value);
|
||||
}
|
||||
|
||||
|
||||
public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
|
||||
const webview = this.getWebviewInput(handle);
|
||||
webview.iconPath = reviveWebviewIcon(value);
|
||||
}
|
||||
|
||||
public $reveal(handle: extHostProtocol.WebviewHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void {
|
||||
const webview = this.getWebviewInput(handle);
|
||||
if (webview.isDisposed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0);
|
||||
if (targetGroup) {
|
||||
this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus);
|
||||
}
|
||||
}
|
||||
|
||||
public $registerSerializer(viewType: string)
|
||||
: void {
|
||||
if (this._revivers.has(viewType)) {
|
||||
throw new Error(`Reviver for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({
|
||||
canResolve: (webviewInput) => {
|
||||
return webviewInput.viewType === this.webviewPanelViewType.fromExternal(viewType);
|
||||
},
|
||||
resolveWebview: async (webviewInput): Promise<void> => {
|
||||
const viewType = this.webviewPanelViewType.toExternal(webviewInput.viewType);
|
||||
if (!viewType) {
|
||||
webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const handle = webviewInput.id;
|
||||
|
||||
this.addWebviewInput(handle, webviewInput);
|
||||
|
||||
let state = undefined;
|
||||
if (webviewInput.webview.state) {
|
||||
try {
|
||||
state = JSON.parse(webviewInput.webview.state);
|
||||
} catch (e) {
|
||||
console.error('Could not load webview state', e, webviewInput.webview.state);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(viewType);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public $unregisterSerializer(viewType: string): void {
|
||||
const reviver = this._revivers.get(viewType);
|
||||
if (!reviver) {
|
||||
throw new Error(`No reviver for ${viewType} registered`);
|
||||
}
|
||||
|
||||
reviver.dispose();
|
||||
this._revivers.delete(viewType);
|
||||
}
|
||||
|
||||
private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void {
|
||||
const primary = diffEditorInput.primary as WebviewInput;
|
||||
const secondary = diffEditorInput.secondary as WebviewInput;
|
||||
|
||||
if (this._webviewFromDiffEditorHandles.has(primary.id) || this._webviewFromDiffEditorHandles.has(secondary.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._webviewFromDiffEditorHandles.add(primary.id);
|
||||
this._webviewFromDiffEditorHandles.add(secondary.id);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
disposables.add(primary.webview.onDidFocus(() => this.updateWebviewViewStates(primary)));
|
||||
disposables.add(secondary.webview.onDidFocus(() => this.updateWebviewViewStates(secondary)));
|
||||
disposables.add(diffEditorInput.onDispose(() => {
|
||||
this._webviewFromDiffEditorHandles.delete(primary.id);
|
||||
this._webviewFromDiffEditorHandles.delete(secondary.id);
|
||||
dispose(disposables);
|
||||
}));
|
||||
}
|
||||
|
||||
private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) {
|
||||
if (!this._webviewInputs.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewStates: extHostProtocol.WebviewPanelViewStateData = {};
|
||||
|
||||
const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => {
|
||||
if (!(editorInput instanceof WebviewInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorInput.updateGroup(group.id);
|
||||
|
||||
const handle = this._webviewInputs.getHandleForInput(editorInput);
|
||||
if (handle) {
|
||||
viewStates[handle] = {
|
||||
visible: topLevelInput === group.activeEditor,
|
||||
active: editorInput === activeEditorInput,
|
||||
position: editorGroupToViewColumn(this._editorGroupService, group.id),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
for (const group of this._editorGroupService.groups) {
|
||||
for (const input of group.editors) {
|
||||
if (input instanceof DiffEditorInput) {
|
||||
updateViewStatesForInput(group, input, input.primary);
|
||||
updateViewStatesForInput(group, input, input.secondary);
|
||||
} else {
|
||||
updateViewStatesForInput(group, input, input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(viewStates).length) {
|
||||
this._proxy.$onDidChangeWebviewPanelViewStates(viewStates);
|
||||
}
|
||||
}
|
||||
|
||||
private getWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput {
|
||||
const webview = this.tryGetWebviewInput(handle);
|
||||
if (!webview) {
|
||||
throw new Error(`Unknown webview handle:${handle}`);
|
||||
}
|
||||
return webview;
|
||||
}
|
||||
|
||||
private tryGetWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput | undefined {
|
||||
return this._webviewInputs.getInputForHandle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function reviveWebviewIcon(
|
||||
value: { light: UriComponents, dark: UriComponents; } | undefined
|
||||
): WebviewIcons | undefined {
|
||||
return value
|
||||
? { light: URI.revive(value.light), dark: URI.revive(value.dark) }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService';
|
||||
|
||||
|
||||
export class MainThreadWebviewsViews extends Disposable implements extHostProtocol.MainThreadWebviewViewsShape {
|
||||
|
||||
private readonly _proxy: extHostProtocol.ExtHostWebviewViewsShape;
|
||||
|
||||
private readonly _webviewViews = new Map<string, WebviewView>();
|
||||
private readonly _webviewViewProviders = new Map<string, IDisposable>();
|
||||
|
||||
constructor(
|
||||
context: extHostProtocol.IExtHostContext,
|
||||
private readonly mainThreadWebviews: MainThreadWebviews,
|
||||
@IWebviewViewService private readonly _webviewViewService: IWebviewViewService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
|
||||
dispose(this._webviewViewProviders.values());
|
||||
this._webviewViewProviders.clear();
|
||||
|
||||
dispose(this._webviewViews.values());
|
||||
}
|
||||
|
||||
public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void {
|
||||
const webviewView = this.getWebviewView(handle);
|
||||
webviewView.title = value;
|
||||
}
|
||||
|
||||
public $setWebviewViewDescription(handle: extHostProtocol.WebviewHandle, value: string | undefined): void {
|
||||
const webviewView = this.getWebviewView(handle);
|
||||
webviewView.description = value;
|
||||
}
|
||||
|
||||
public $show(handle: extHostProtocol.WebviewHandle, preserveFocus: boolean): void {
|
||||
const webviewView = this.getWebviewView(handle);
|
||||
webviewView.show(preserveFocus);
|
||||
}
|
||||
|
||||
public $registerWebviewViewProvider(
|
||||
extensionData: extHostProtocol.WebviewExtensionDescription,
|
||||
viewType: string,
|
||||
options?: { retainContextWhenHidden?: boolean }
|
||||
): void {
|
||||
if (this._webviewViewProviders.has(viewType)) {
|
||||
throw new Error(`View provider for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
const extension = reviveWebviewExtension(extensionData);
|
||||
|
||||
const registration = this._webviewViewService.register(viewType, {
|
||||
resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => {
|
||||
const handle = webviewView.webview.id;
|
||||
|
||||
this._webviewViews.set(handle, webviewView);
|
||||
this.mainThreadWebviews.addWebview(handle, webviewView.webview);
|
||||
|
||||
let state = undefined;
|
||||
if (webviewView.webview.state) {
|
||||
try {
|
||||
state = JSON.parse(webviewView.webview.state);
|
||||
} catch (e) {
|
||||
console.error('Could not load webview state', e, webviewView.webview.state);
|
||||
}
|
||||
}
|
||||
|
||||
webviewView.webview.extension = extension;
|
||||
|
||||
if (options) {
|
||||
webviewView.webview.options = options;
|
||||
}
|
||||
|
||||
webviewView.onDidChangeVisibility(visible => {
|
||||
this._proxy.$onDidChangeWebviewViewVisibility(handle, visible);
|
||||
});
|
||||
|
||||
webviewView.onDispose(() => {
|
||||
this._proxy.$disposeWebviewView(handle);
|
||||
this._webviewViews.delete(handle);
|
||||
});
|
||||
|
||||
try {
|
||||
await this._proxy.$resolveWebviewView(handle, viewType, webviewView.title, state, cancellation);
|
||||
} catch (error) {
|
||||
onUnexpectedError(error);
|
||||
webviewView.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._webviewViewProviders.set(viewType, registration);
|
||||
}
|
||||
|
||||
public $unregisterWebviewViewProvider(viewType: string): void {
|
||||
const provider = this._webviewViewProviders.get(viewType);
|
||||
if (!provider) {
|
||||
throw new Error(`No view provider for ${viewType} registered`);
|
||||
}
|
||||
|
||||
provider.dispose();
|
||||
this._webviewViewProviders.delete(viewType);
|
||||
}
|
||||
|
||||
private getWebviewView(handle: string): WebviewView {
|
||||
const webviewView = this._webviewViews.get(handle);
|
||||
if (!webviewView) {
|
||||
throw new Error('unknown webview view');
|
||||
}
|
||||
return webviewView;
|
||||
}
|
||||
}
|
||||
|
||||
125
lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts
Normal file
125
lib/vscode/src/vs/workbench/api/browser/mainThreadWebviews.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IWebviewOptions } from 'vs/editor/common/modes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { Webview, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview';
|
||||
import { WebviewInputOptions } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService';
|
||||
|
||||
export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape {
|
||||
|
||||
private static readonly standardSupportedLinkSchemes = new Set([
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
Schemas.mailto,
|
||||
Schemas.vscode,
|
||||
'vscode-insider',
|
||||
]);
|
||||
|
||||
private readonly _proxy: extHostProtocol.ExtHostWebviewsShape;
|
||||
|
||||
private readonly _webviews = new Map<string, Webview>();
|
||||
|
||||
constructor(
|
||||
context: extHostProtocol.IExtHostContext,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews);
|
||||
}
|
||||
|
||||
public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void {
|
||||
this._webviews.set(handle, webview);
|
||||
this.hookupWebviewEventDelegate(handle, webview);
|
||||
}
|
||||
|
||||
public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void {
|
||||
const webview = this.getWebview(handle);
|
||||
webview.html = value;
|
||||
}
|
||||
|
||||
public $setOptions(handle: extHostProtocol.WebviewHandle, options: IWebviewOptions): void {
|
||||
const webview = this.getWebview(handle);
|
||||
webview.contentOptions = reviveWebviewOptions(options);
|
||||
}
|
||||
|
||||
public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise<boolean> {
|
||||
const webview = this.getWebview(handle);
|
||||
webview.postMessage(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri)));
|
||||
disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); }));
|
||||
disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value)));
|
||||
|
||||
disposables.add(webview.onDidDispose(() => {
|
||||
disposables.dispose();
|
||||
this._webviews.delete(handle);
|
||||
}));
|
||||
}
|
||||
|
||||
private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void {
|
||||
const webview = this.getWebview(handle);
|
||||
if (this.isSupportedLink(webview, URI.parse(link))) {
|
||||
this._openerService.open(link, { fromUserGesture: true });
|
||||
}
|
||||
}
|
||||
|
||||
private isSupportedLink(webview: Webview, link: URI): boolean {
|
||||
if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) {
|
||||
return true;
|
||||
}
|
||||
if (!isWeb && this._productService.urlProtocol === link.scheme) {
|
||||
return true;
|
||||
}
|
||||
return !!webview.contentOptions.enableCommandUris && link.scheme === Schemas.command;
|
||||
}
|
||||
|
||||
private getWebview(handle: extHostProtocol.WebviewHandle): Webview {
|
||||
const webview = this._webviews.get(handle);
|
||||
if (!webview) {
|
||||
throw new Error(`Unknown webview handle:${handle}`);
|
||||
}
|
||||
return webview;
|
||||
}
|
||||
|
||||
public getWebviewResolvedFailedContent(viewType: string) {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none';">
|
||||
</head>
|
||||
<body>${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))}</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
export function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription {
|
||||
return { id: extensionData.id, location: URI.revive(extensionData.location) };
|
||||
}
|
||||
|
||||
export function reviveWebviewOptions(options: IWebviewOptions): WebviewInputOptions {
|
||||
return {
|
||||
...options,
|
||||
allowScripts: options.enableScripts,
|
||||
localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined,
|
||||
};
|
||||
}
|
||||
63
lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts
Normal file
63
lib/vscode/src/vs/workbench/api/browser/mainThreadWindow.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, ExtHostWindowShape, IExtHostContext, IOpenUriOptions, MainContext, MainThreadWindowShape } from '../common/extHost.protocol';
|
||||
import { IHostService } from 'vs/workbench/services/host/browser/host';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadWindow)
|
||||
export class MainThreadWindow implements MainThreadWindowShape {
|
||||
|
||||
private readonly proxy: ExtHostWindowShape;
|
||||
private readonly disposables = new DisposableStore();
|
||||
private readonly resolved = new Map<number, IDisposable>();
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
) {
|
||||
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostWindow);
|
||||
|
||||
Event.latch(hostService.onDidChangeFocus)
|
||||
(this.proxy.$onDidChangeWindowFocus, this.proxy, this.disposables);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables.dispose();
|
||||
|
||||
for (const value of this.resolved.values()) {
|
||||
value.dispose();
|
||||
}
|
||||
this.resolved.clear();
|
||||
}
|
||||
|
||||
$getWindowVisibility(): Promise<boolean> {
|
||||
return Promise.resolve(this.hostService.hasFocus);
|
||||
}
|
||||
|
||||
async $openUri(uriComponents: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise<boolean> {
|
||||
const uri = URI.from(uriComponents);
|
||||
let target: URI | string;
|
||||
if (uriString && URI.parse(uriString).toString() === uri.toString()) {
|
||||
// called with string and no transformation happened -> keep string
|
||||
target = uriString;
|
||||
} else {
|
||||
// called with URI or transformed -> use uri
|
||||
target = uri;
|
||||
}
|
||||
return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling });
|
||||
}
|
||||
|
||||
async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise<UriComponents> {
|
||||
const uri = URI.revive(uriComponents);
|
||||
const result = await this.openerService.resolveExternalUri(uri, options);
|
||||
return result.resolved;
|
||||
}
|
||||
}
|
||||
205
lib/vscode/src/vs/workbench/api/browser/mainThreadWorkspace.ts
Normal file
205
lib/vscode/src/vs/workbench/api/browser/mainThreadWorkspace.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isNative } from 'vs/base/common/platform';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
|
||||
import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
|
||||
import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadWorkspace)
|
||||
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _activeCancelTokens: { [id: number]: CancellationTokenSource } = Object.create(null);
|
||||
private readonly _proxy: ExtHostWorkspaceShape;
|
||||
private readonly _queryBuilder = this._instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@ISearchService private readonly _searchService: ISearchService,
|
||||
@IWorkspaceContextService private readonly _contextService: IWorkspaceContextService,
|
||||
@IEditorService private readonly _editorService: IEditorService,
|
||||
@IWorkspaceEditingService private readonly _workspaceEditingService: IWorkspaceEditingService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IRequestService private readonly _requestService: IRequestService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostWorkspace);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
// The workspace file is provided be a unknown file system provider. It might come
|
||||
// from the extension host. So initialize now knowing that `rootPath` is undefined.
|
||||
if (workspace.configuration && !isNative && !fileService.canHandleResource(workspace.configuration)) {
|
||||
this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace));
|
||||
} else {
|
||||
this._contextService.getCompleteWorkspace().then(workspace => this._proxy.$initializeWorkspace(this.getWorkspaceData(workspace)));
|
||||
}
|
||||
this._contextService.onDidChangeWorkspaceFolders(this._onDidChangeWorkspace, this, this._toDispose);
|
||||
this._contextService.onDidChangeWorkbenchState(this._onDidChangeWorkspace, this, this._toDispose);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._toDispose.dispose();
|
||||
|
||||
for (let requestId in this._activeCancelTokens) {
|
||||
const tokenSource = this._activeCancelTokens[requestId];
|
||||
tokenSource.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// --- workspace ---
|
||||
|
||||
$updateWorkspaceFolders(extensionName: string, index: number, deleteCount: number, foldersToAdd: { uri: UriComponents, name?: string }[]): Promise<void> {
|
||||
const workspaceFoldersToAdd = foldersToAdd.map(f => ({ uri: URI.revive(f.uri), name: f.name }));
|
||||
|
||||
// Indicate in status message
|
||||
this._notificationService.status(this.getStatusMessage(extensionName, workspaceFoldersToAdd.length, deleteCount), { hideAfter: 10 * 1000 /* 10s */ });
|
||||
|
||||
return this._workspaceEditingService.updateFolders(index, deleteCount, workspaceFoldersToAdd, true);
|
||||
}
|
||||
|
||||
private getStatusMessage(extensionName: string, addCount: number, removeCount: number): string {
|
||||
let message: string;
|
||||
|
||||
const wantsToAdd = addCount > 0;
|
||||
const wantsToDelete = removeCount > 0;
|
||||
|
||||
// Add Folders
|
||||
if (wantsToAdd && !wantsToDelete) {
|
||||
if (addCount === 1) {
|
||||
message = localize('folderStatusMessageAddSingleFolder', "Extension '{0}' added 1 folder to the workspace", extensionName);
|
||||
} else {
|
||||
message = localize('folderStatusMessageAddMultipleFolders', "Extension '{0}' added {1} folders to the workspace", extensionName, addCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Folders
|
||||
else if (wantsToDelete && !wantsToAdd) {
|
||||
if (removeCount === 1) {
|
||||
message = localize('folderStatusMessageRemoveSingleFolder', "Extension '{0}' removed 1 folder from the workspace", extensionName);
|
||||
} else {
|
||||
message = localize('folderStatusMessageRemoveMultipleFolders', "Extension '{0}' removed {1} folders from the workspace", extensionName, removeCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Change Folders
|
||||
else {
|
||||
message = localize('folderStatusChangeFolder', "Extension '{0}' changed folders of the workspace", extensionName);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private _onDidChangeWorkspace(): void {
|
||||
this._proxy.$acceptWorkspaceData(this.getWorkspaceData(this._contextService.getWorkspace()));
|
||||
}
|
||||
|
||||
private getWorkspaceData(workspace: IWorkspace): IWorkspaceData | null {
|
||||
if (this._contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
configuration: workspace.configuration || undefined,
|
||||
isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false,
|
||||
folders: workspace.folders,
|
||||
id: workspace.id,
|
||||
name: this._labelService.getWorkspaceLabel(workspace)
|
||||
};
|
||||
}
|
||||
|
||||
// --- search ---
|
||||
|
||||
$startFileSearch(includePattern: string | null, _includeFolder: UriComponents | null, excludePatternOrDisregardExcludes: string | false | null, maxResults: number | null, token: CancellationToken): Promise<UriComponents[] | null> {
|
||||
const includeFolder = URI.revive(_includeFolder);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
if (!workspace.folders.length) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const query = this._queryBuilder.file(
|
||||
includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders,
|
||||
{
|
||||
maxResults: withNullAsUndefined(maxResults),
|
||||
disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined,
|
||||
disregardSearchExcludeSettings: true,
|
||||
disregardIgnoreFiles: true,
|
||||
includePattern: withNullAsUndefined(includePattern),
|
||||
excludePattern: typeof excludePatternOrDisregardExcludes === 'string' ? excludePatternOrDisregardExcludes : undefined,
|
||||
_reason: 'startFileSearch'
|
||||
});
|
||||
|
||||
return this._searchService.fileSearch(query, token).then(result => {
|
||||
return result.results.map(m => m.resource);
|
||||
}, err => {
|
||||
if (!isPromiseCanceledError(err)) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
$startTextSearch(pattern: IPatternInfo, _folder: UriComponents | null, options: ITextQueryBuilderOptions, requestId: number, token: CancellationToken): Promise<ITextSearchComplete | null> {
|
||||
const folder = URI.revive(_folder);
|
||||
const workspace = this._contextService.getWorkspace();
|
||||
const folders = folder ? [folder] : workspace.folders.map(folder => folder.uri);
|
||||
|
||||
const query = this._queryBuilder.text(pattern, folders, options);
|
||||
query._reason = 'startTextSearch';
|
||||
|
||||
const onProgress = (p: ISearchProgressItem) => {
|
||||
if ((<IFileMatch>p).results) {
|
||||
this._proxy.$handleTextSearchResult(<IFileMatch>p, requestId);
|
||||
}
|
||||
};
|
||||
|
||||
const search = this._searchService.textSearch(query, token, onProgress).then(
|
||||
result => {
|
||||
return { limitHit: result.limitHit };
|
||||
},
|
||||
err => {
|
||||
if (!isPromiseCanceledError(err)) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
$checkExists(folders: readonly UriComponents[], includes: string[], token: CancellationToken): Promise<boolean> {
|
||||
return this._instantiationService.invokeFunction((accessor) => checkGlobFileExists(accessor, folders, includes, token));
|
||||
}
|
||||
|
||||
// --- save & edit resources ---
|
||||
|
||||
$saveAll(includeUntitled?: boolean): Promise<boolean> {
|
||||
return this._editorService.saveAll({ includeUntitled });
|
||||
}
|
||||
|
||||
$resolveProxy(url: string): Promise<string | undefined> {
|
||||
return this._requestService.resolveProxy(url);
|
||||
}
|
||||
}
|
||||
604
lib/vscode/src/vs/workbench/api/browser/viewsExtensionPoint.ts
Normal file
604
lib/vscode/src/vs/workbench/api/browser/viewsExtensionPoint.ts
Normal file
@@ -0,0 +1,604 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { forEach } from 'vs/base/common/collections';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation } from 'vs/workbench/common/views';
|
||||
import { TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { coalesce, } from 'vs/base/common/arrays';
|
||||
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
|
||||
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files';
|
||||
import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm';
|
||||
import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug';
|
||||
import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet';
|
||||
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
|
||||
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
|
||||
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions';
|
||||
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
|
||||
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
|
||||
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView';
|
||||
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
|
||||
|
||||
export interface IUserFriendlyViewsContainerDescriptor {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const viewsContainerSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: localize({ key: 'vscode.extension.contributes.views.containers.id', comment: ['Contribution refers to those that an extension contributes to VS Code through an extension/contribution point. '] }, "Unique id used to identify the container in which views can be contributed using 'views' contribution point"),
|
||||
type: 'string',
|
||||
pattern: '^[a-zA-Z0-9_-]+$'
|
||||
},
|
||||
title: {
|
||||
description: localize('vscode.extension.contributes.views.containers.title', 'Human readable string used to render the container'),
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
description: localize('vscode.extension.contributes.views.containers.icon', "Path to the container icon. Icons are 24x24 centered on a 50x40 block and have a fill color of 'rgb(215, 218, 224)' or '#d7dae0'. It is recommended that icons be in SVG, though any image file type is accepted."),
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
required: ['id', 'title', 'icon']
|
||||
};
|
||||
|
||||
export const viewsContainersContribution: IJSONSchema = {
|
||||
description: localize('vscode.extension.contributes.viewsContainers', 'Contributes views containers to the editor'),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'activitybar': {
|
||||
description: localize('views.container.activitybar', "Contribute views containers to Activity Bar"),
|
||||
type: 'array',
|
||||
items: viewsContainerSchema
|
||||
},
|
||||
'panel': {
|
||||
description: localize('views.container.panel', "Contribute views containers to Panel"),
|
||||
type: 'array',
|
||||
items: viewsContainerSchema
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
enum ViewType {
|
||||
Tree = 'tree',
|
||||
Webview = 'webview'
|
||||
}
|
||||
|
||||
|
||||
interface IUserFriendlyViewDescriptor {
|
||||
type?: ViewType;
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
when?: string;
|
||||
|
||||
icon?: string;
|
||||
contextualTitle?: string;
|
||||
visibility?: string;
|
||||
|
||||
// From 'remoteViewDescriptor' type
|
||||
group?: string;
|
||||
remoteName?: string | string[];
|
||||
}
|
||||
|
||||
enum InitialVisibility {
|
||||
Visible = 'visible',
|
||||
Hidden = 'hidden',
|
||||
Collapsed = 'collapsed'
|
||||
}
|
||||
|
||||
const viewDescriptor: IJSONSchema = {
|
||||
type: 'object',
|
||||
required: ['id', 'name'],
|
||||
defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }],
|
||||
properties: {
|
||||
type: {
|
||||
markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."),
|
||||
type: 'string',
|
||||
enum: [
|
||||
'tree',
|
||||
'webview',
|
||||
],
|
||||
markdownEnumDescriptions: [
|
||||
localize('vscode.extension.contributes.view.tree', "The view is backed by a `TreeView` created by `createTreeView`."),
|
||||
localize('vscode.extension.contributes.view.webview', "The view is backed by a `WebviewView` registered by `registerWebviewViewProvider`."),
|
||||
]
|
||||
},
|
||||
id: {
|
||||
markdownDescription: localize('vscode.extension.contributes.view.id', 'Identifier of the view. This should be unique across all views. It is recommended to include your extension id as part of the view id. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'),
|
||||
type: 'string'
|
||||
},
|
||||
name: {
|
||||
description: localize('vscode.extension.contributes.view.name', 'The human-readable name of the view. Will be shown'),
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'),
|
||||
type: 'string'
|
||||
},
|
||||
icon: {
|
||||
description: localize('vscode.extension.contributes.view.icon', "Path to the view icon. View icons are displayed when the name of the view cannot be shown. It is recommended that icons be in SVG, though any image file type is accepted."),
|
||||
type: 'string'
|
||||
},
|
||||
contextualTitle: {
|
||||
description: localize('vscode.extension.contributes.view.contextualTitle', "Human-readable context for when the view is moved out of its original location. By default, the view's container name will be used. Will be shown"),
|
||||
type: 'string'
|
||||
},
|
||||
visibility: {
|
||||
description: localize('vscode.extension.contributes.view.initialState', "Initial state of the view when the extension is first installed. Once the user has changed the view state by collapsing, moving, or hiding the view, the initial state will not be used again."),
|
||||
type: 'string',
|
||||
enum: [
|
||||
'visible',
|
||||
'hidden',
|
||||
'collapsed'
|
||||
],
|
||||
default: 'visible',
|
||||
enumDescriptions: [
|
||||
localize('vscode.extension.contributes.view.initialState.visible', "The default initial state for the view. In most containers the view will be expanded, however; some built-in containers (explorer, scm, and debug) show all contributed views collapsed regardless of the `visibility`."),
|
||||
localize('vscode.extension.contributes.view.initialState.hidden', "The view will not be shown in the view container, but will be discoverable through the views menu and other view entry points and can be un-hidden by the user."),
|
||||
localize('vscode.extension.contributes.view.initialState.collapsed', "The view will show in the view container, but will be collapsed.")
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remoteViewDescriptor: IJSONSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
description: localize('vscode.extension.contributes.view.id', 'Identifier of the view. This should be unique across all views. It is recommended to include your extension id as part of the view id. Use this to register a data provider through `vscode.window.registerTreeDataProviderForView` API. Also to trigger activating your extension by registering `onView:${id}` event to `activationEvents`.'),
|
||||
type: 'string'
|
||||
},
|
||||
name: {
|
||||
description: localize('vscode.extension.contributes.view.name', 'The human-readable name of the view. Will be shown'),
|
||||
type: 'string'
|
||||
},
|
||||
when: {
|
||||
description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'),
|
||||
type: 'string'
|
||||
},
|
||||
group: {
|
||||
description: localize('vscode.extension.contributes.view.group', 'Nested group in the viewlet'),
|
||||
type: 'string'
|
||||
},
|
||||
remoteName: {
|
||||
description: localize('vscode.extension.contributes.view.remoteName', 'The name of the remote type associated with this view'),
|
||||
type: ['string', 'array'],
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const viewsContribution: IJSONSchema = {
|
||||
description: localize('vscode.extension.contributes.views', "Contributes views to the editor"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'explorer': {
|
||||
description: localize('views.explorer', "Contributes views to Explorer container in the Activity bar"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
},
|
||||
'debug': {
|
||||
description: localize('views.debug', "Contributes views to Debug container in the Activity bar"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
},
|
||||
'scm': {
|
||||
description: localize('views.scm', "Contributes views to SCM container in the Activity bar"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
},
|
||||
'test': {
|
||||
description: localize('views.test', "Contributes views to Test container in the Activity bar"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
},
|
||||
'remote': {
|
||||
description: localize('views.remote', "Contributes views to Remote container in the Activity bar. To contribute to this container, enableProposedApi needs to be turned on"),
|
||||
type: 'array',
|
||||
items: remoteViewDescriptor,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
additionalProperties: {
|
||||
description: localize('views.contributed', "Contributes views to contributed views container"),
|
||||
type: 'array',
|
||||
items: viewDescriptor,
|
||||
default: []
|
||||
}
|
||||
};
|
||||
|
||||
export interface ICustomTreeViewDescriptor extends ITreeViewDescriptor {
|
||||
readonly extensionId: ExtensionIdentifier;
|
||||
readonly originalContainerId: string;
|
||||
}
|
||||
|
||||
export interface ICustomWebviewViewDescriptor extends IViewDescriptor {
|
||||
readonly extensionId: ExtensionIdentifier;
|
||||
readonly originalContainerId: string;
|
||||
}
|
||||
|
||||
export type ICustomViewDescriptor = ICustomTreeViewDescriptor | ICustomWebviewViewDescriptor;
|
||||
|
||||
type ViewContainerExtensionPointType = { [loc: string]: IUserFriendlyViewsContainerDescriptor[] };
|
||||
const viewsContainersExtensionPoint: IExtensionPoint<ViewContainerExtensionPointType> = ExtensionsRegistry.registerExtensionPoint<ViewContainerExtensionPointType>({
|
||||
extensionPoint: 'viewsContainers',
|
||||
jsonSchema: viewsContainersContribution
|
||||
});
|
||||
|
||||
type ViewExtensionPointType = { [loc: string]: IUserFriendlyViewDescriptor[] };
|
||||
const viewsExtensionPoint: IExtensionPoint<ViewExtensionPointType> = ExtensionsRegistry.registerExtensionPoint<ViewExtensionPointType>({
|
||||
extensionPoint: 'views',
|
||||
deps: [viewsContainersExtensionPoint],
|
||||
jsonSchema: viewsContribution
|
||||
});
|
||||
|
||||
const TEST_VIEW_CONTAINER_ORDER = 6;
|
||||
class ViewsExtensionHandler implements IWorkbenchContribution {
|
||||
|
||||
private viewContainersRegistry: IViewContainersRegistry;
|
||||
private viewsRegistry: IViewsRegistry;
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
this.viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
this.viewsRegistry = Registry.as<IViewsRegistry>(ViewContainerExtensions.ViewsRegistry);
|
||||
this.handleAndRegisterCustomViewContainers();
|
||||
this.handleAndRegisterCustomViews();
|
||||
}
|
||||
|
||||
private handleAndRegisterCustomViewContainers() {
|
||||
this.registerTestViewContainer();
|
||||
viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => {
|
||||
if (removed.length) {
|
||||
this.removeCustomViewContainers(removed);
|
||||
}
|
||||
if (added.length) {
|
||||
this.addCustomViewContainers(added, this.viewContainersRegistry.all);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser<ViewContainerExtensionPointType>[], existingViewContainers: ViewContainer[]): void {
|
||||
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1;
|
||||
let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1;
|
||||
for (let { value, collector, description } of extensionPoints) {
|
||||
forEach(value, entry => {
|
||||
if (!this.isValidViewsContainer(entry.value, collector)) {
|
||||
return;
|
||||
}
|
||||
switch (entry.key) {
|
||||
case 'activitybar':
|
||||
activityBarOrder = this.registerCustomViewContainers(entry.value, description, activityBarOrder, existingViewContainers, ViewContainerLocation.Sidebar);
|
||||
break;
|
||||
case 'panel':
|
||||
panelOrder = this.registerCustomViewContainers(entry.value, description, panelOrder, existingViewContainers, ViewContainerLocation.Panel);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private removeCustomViewContainers(extensionPoints: readonly IExtensionPointUser<ViewContainerExtensionPointType>[]): void {
|
||||
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
|
||||
const removedExtensions: Set<string> = extensionPoints.reduce((result, e) => { result.add(ExtensionIdentifier.toKey(e.description.identifier)); return result; }, new Set<string>());
|
||||
for (const viewContainer of viewContainersRegistry.all) {
|
||||
if (viewContainer.extensionId && removedExtensions.has(ExtensionIdentifier.toKey(viewContainer.extensionId))) {
|
||||
// move only those views that do not belong to the removed extension
|
||||
const views = this.viewsRegistry.getViews(viewContainer).filter(view => !removedExtensions.has(ExtensionIdentifier.toKey((view as ICustomViewDescriptor).extensionId)));
|
||||
if (views.length) {
|
||||
this.viewsRegistry.moveViews(views, this.getDefaultViewContainer());
|
||||
}
|
||||
this.deregisterCustomViewContainer(viewContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private registerTestViewContainer(): void {
|
||||
const title = localize('test', "Test");
|
||||
const icon = Codicon.beaker.classNames;
|
||||
|
||||
this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar);
|
||||
}
|
||||
|
||||
private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean {
|
||||
if (!Array.isArray(viewsContainersDescriptors)) {
|
||||
collector.error(localize('viewcontainer requirearray', "views containers must be an array"));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let descriptor of viewsContainersDescriptors) {
|
||||
if (typeof descriptor.id !== 'string') {
|
||||
collector.error(localize('requireidstring', "property `{0}` is mandatory and must be of type `string`. Only alphanumeric characters, '_', and '-' are allowed.", 'id'));
|
||||
return false;
|
||||
}
|
||||
if (!(/^[a-z0-9_-]+$/i.test(descriptor.id))) {
|
||||
collector.error(localize('requireidstring', "property `{0}` is mandatory and must be of type `string`. Only alphanumeric characters, '_', and '-' are allowed.", 'id'));
|
||||
return false;
|
||||
}
|
||||
if (typeof descriptor.title !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'title'));
|
||||
return false;
|
||||
}
|
||||
if (typeof descriptor.icon !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'icon'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private registerCustomViewContainers(containers: IUserFriendlyViewsContainerDescriptor[], extension: IExtensionDescription, order: number, existingViewContainers: ViewContainer[], location: ViewContainerLocation): number {
|
||||
containers.forEach(descriptor => {
|
||||
const icon = resources.joinPath(extension.extensionLocation, descriptor.icon);
|
||||
const id = `workbench.view.extension.${descriptor.id}`;
|
||||
const viewContainer = this.registerCustomViewContainer(id, descriptor.title, icon, order++, extension.identifier, location);
|
||||
|
||||
// Move those views that belongs to this container
|
||||
if (existingViewContainers.length) {
|
||||
const viewsToMove: IViewDescriptor[] = [];
|
||||
for (const existingViewContainer of existingViewContainers) {
|
||||
if (viewContainer !== existingViewContainer) {
|
||||
viewsToMove.push(...this.viewsRegistry.getViews(existingViewContainer).filter(view => (view as ICustomViewDescriptor).originalContainerId === descriptor.id));
|
||||
}
|
||||
}
|
||||
if (viewsToMove.length) {
|
||||
this.viewsRegistry.moveViews(viewsToMove, viewContainer);
|
||||
}
|
||||
}
|
||||
});
|
||||
return order;
|
||||
}
|
||||
|
||||
private registerCustomViewContainer(id: string, title: string, icon: URI | string, order: number, extensionId: ExtensionIdentifier | undefined, location: ViewContainerLocation): ViewContainer {
|
||||
let viewContainer = this.viewContainersRegistry.get(id);
|
||||
|
||||
if (!viewContainer) {
|
||||
|
||||
viewContainer = this.viewContainersRegistry.registerViewContainer({
|
||||
id,
|
||||
name: title, extensionId,
|
||||
ctorDescriptor: new SyncDescriptor(
|
||||
ViewPaneContainer,
|
||||
[id, { mergeViewWithContainerWhenSingleView: true }]
|
||||
),
|
||||
hideIfEmpty: true,
|
||||
order,
|
||||
icon,
|
||||
}, location);
|
||||
|
||||
// Register Action to Open Viewlet
|
||||
class OpenCustomViewletAction extends ShowViewletAction {
|
||||
constructor(
|
||||
id: string, label: string,
|
||||
@IViewletService viewletService: IViewletService,
|
||||
@IEditorGroupsService editorGroupService: IEditorGroupsService,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
|
||||
) {
|
||||
super(id, label, id, viewletService, editorGroupService, layoutService);
|
||||
}
|
||||
}
|
||||
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
|
||||
registry.registerWorkbenchAction(
|
||||
SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)),
|
||||
`View: Show ${title}`,
|
||||
CATEGORIES.View.value
|
||||
);
|
||||
}
|
||||
|
||||
return viewContainer;
|
||||
}
|
||||
|
||||
private deregisterCustomViewContainer(viewContainer: ViewContainer): void {
|
||||
this.viewContainersRegistry.deregisterViewContainer(viewContainer);
|
||||
Registry.as<ViewletRegistry>(ViewletExtensions.Viewlets).deregisterViewlet(viewContainer.id);
|
||||
}
|
||||
|
||||
private handleAndRegisterCustomViews() {
|
||||
viewsExtensionPoint.setHandler((extensions, { added, removed }) => {
|
||||
if (removed.length) {
|
||||
this.removeViews(removed);
|
||||
}
|
||||
if (added.length) {
|
||||
this.addViews(added);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addViews(extensions: readonly IExtensionPointUser<ViewExtensionPointType>[]): void {
|
||||
const viewIds: Set<string> = new Set<string>();
|
||||
const allViewDescriptors: { views: IViewDescriptor[], viewContainer: ViewContainer }[] = [];
|
||||
|
||||
for (const extension of extensions) {
|
||||
const { value, collector } = extension;
|
||||
|
||||
forEach(value, entry => {
|
||||
if (!this.isValidViewDescriptors(entry.value, collector)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.key === 'remote' && !extension.description.enableProposedApi) {
|
||||
collector.warn(localize('ViewContainerRequiresProposedAPI', "View container '{0}' requires 'enableProposedApi' turned on to be added to 'Remote'.", entry.key));
|
||||
return;
|
||||
}
|
||||
|
||||
const viewContainer = this.getViewContainer(entry.key);
|
||||
if (!viewContainer) {
|
||||
collector.warn(localize('ViewContainerDoesnotExist', "View container '{0}' does not exist and all views registered to it will be added to 'Explorer'.", entry.key));
|
||||
}
|
||||
const container = viewContainer || this.getDefaultViewContainer();
|
||||
const viewDescriptors = coalesce(entry.value.map((item, index) => {
|
||||
// validate
|
||||
if (viewIds.has(item.id)) {
|
||||
collector.error(localize('duplicateView1', "Cannot register multiple views with same id `{0}`", item.id));
|
||||
return null;
|
||||
}
|
||||
if (this.viewsRegistry.getView(item.id) !== null) {
|
||||
collector.error(localize('duplicateView2', "A view with id `{0}` is already registered.", item.id));
|
||||
return null;
|
||||
}
|
||||
|
||||
const order = ExtensionIdentifier.equals(extension.description.identifier, container.extensionId)
|
||||
? index + 1
|
||||
: container.viewOrderDelegate
|
||||
? container.viewOrderDelegate.getOrder(item.group)
|
||||
: undefined;
|
||||
|
||||
const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined;
|
||||
const initialVisibility = this.convertInitialVisibility(item.visibility);
|
||||
|
||||
const type = this.getViewType(item.type);
|
||||
if (!type) {
|
||||
collector.error(localize('unknownViewType', "Unknown view type `{0}`.", item.type));
|
||||
return null;
|
||||
}
|
||||
|
||||
const viewDescriptor = <ICustomTreeViewDescriptor>{
|
||||
type: type,
|
||||
ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane),
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
when: ContextKeyExpr.deserialize(item.when),
|
||||
containerIcon: icon || viewContainer?.icon,
|
||||
containerTitle: item.contextualTitle || viewContainer?.name,
|
||||
canToggleVisibility: true,
|
||||
canMoveView: viewContainer?.id !== REMOTE,
|
||||
treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined,
|
||||
collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed,
|
||||
order: order,
|
||||
extensionId: extension.description.identifier,
|
||||
originalContainerId: entry.key,
|
||||
group: item.group,
|
||||
remoteAuthority: item.remoteName || (<any>item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated
|
||||
hideByDefault: initialVisibility === InitialVisibility.Hidden
|
||||
};
|
||||
|
||||
|
||||
viewIds.add(viewDescriptor.id);
|
||||
return viewDescriptor;
|
||||
}));
|
||||
|
||||
allViewDescriptors.push({ viewContainer: container, views: viewDescriptors });
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
this.viewsRegistry.registerViews2(allViewDescriptors);
|
||||
}
|
||||
|
||||
private getViewType(type: string | undefined): ViewType | undefined {
|
||||
if (type === ViewType.Webview) {
|
||||
return ViewType.Webview;
|
||||
}
|
||||
if (!type || type === ViewType.Tree) {
|
||||
return ViewType.Tree;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private getDefaultViewContainer(): ViewContainer {
|
||||
return this.viewContainersRegistry.get(EXPLORER)!;
|
||||
}
|
||||
|
||||
private removeViews(extensions: readonly IExtensionPointUser<ViewExtensionPointType>[]): void {
|
||||
const removedExtensions: Set<string> = extensions.reduce((result, e) => { result.add(ExtensionIdentifier.toKey(e.description.identifier)); return result; }, new Set<string>());
|
||||
for (const viewContainer of this.viewContainersRegistry.all) {
|
||||
const removedViews = this.viewsRegistry.getViews(viewContainer).filter(v => (v as ICustomViewDescriptor).extensionId && removedExtensions.has(ExtensionIdentifier.toKey((v as ICustomViewDescriptor).extensionId)));
|
||||
if (removedViews.length) {
|
||||
this.viewsRegistry.deregisterViews(removedViews, viewContainer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private convertInitialVisibility(value: any): InitialVisibility | undefined {
|
||||
if (Object.values(InitialVisibility).includes(value)) {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isValidViewDescriptors(viewDescriptors: IUserFriendlyViewDescriptor[], collector: ExtensionMessageCollector): boolean {
|
||||
if (!Array.isArray(viewDescriptors)) {
|
||||
collector.error(localize('requirearray', "views must be an array"));
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let descriptor of viewDescriptors) {
|
||||
if (typeof descriptor.id !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'id'));
|
||||
return false;
|
||||
}
|
||||
if (typeof descriptor.name !== 'string') {
|
||||
collector.error(localize('requirestring', "property `{0}` is mandatory and must be of type `string`", 'name'));
|
||||
return false;
|
||||
}
|
||||
if (descriptor.when && typeof descriptor.when !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when'));
|
||||
return false;
|
||||
}
|
||||
if (descriptor.icon && typeof descriptor.icon !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon'));
|
||||
return false;
|
||||
}
|
||||
if (descriptor.contextualTitle && typeof descriptor.contextualTitle !== 'string') {
|
||||
collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'contextualTitle'));
|
||||
return false;
|
||||
}
|
||||
if (descriptor.visibility && !this.convertInitialVisibility(descriptor.visibility)) {
|
||||
collector.error(localize('optenum', "property `{0}` can be omitted or must be one of {1}", 'visibility', Object.values(InitialVisibility).join(', ')));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private getViewContainer(value: string): ViewContainer | undefined {
|
||||
switch (value) {
|
||||
case 'explorer': return this.viewContainersRegistry.get(EXPLORER);
|
||||
case 'debug': return this.viewContainersRegistry.get(DEBUG);
|
||||
case 'scm': return this.viewContainersRegistry.get(SCM);
|
||||
case 'remote': return this.viewContainersRegistry.get(REMOTE);
|
||||
default: return this.viewContainersRegistry.get(`workbench.view.extension.${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
private showCollapsed(container: ViewContainer): boolean {
|
||||
switch (container.id) {
|
||||
case EXPLORER:
|
||||
case SCM:
|
||||
case DEBUG:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
|
||||
workbenchRegistry.registerWorkbenchContribution(ViewsExtensionHandler, LifecyclePhase.Starting);
|
||||
Reference in New Issue
Block a user