mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-26 01:25:38 -05:00
Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2
This commit is contained in:
@@ -7,7 +7,7 @@ import { Disposable, IDisposable } 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 } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { IAuthenticationService, AllowedExtension, readAllowedExtensions } 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';
|
||||
@@ -18,43 +18,56 @@ 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';
|
||||
|
||||
interface AllowedExtension {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const accountUsages = new Map<string, { [accountName: string]: string[] }>();
|
||||
import { fromNow } from 'vs/base/common/date';
|
||||
|
||||
const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git'];
|
||||
|
||||
function addAccountUsage(providerId: string, accountName: string, extensionOrFeatureName: string) {
|
||||
const providerAccountUsage = accountUsages.get(providerId);
|
||||
if (!providerAccountUsage) {
|
||||
accountUsages.set(providerId, { [accountName]: [extensionOrFeatureName] });
|
||||
} else {
|
||||
if (providerAccountUsage[accountName]) {
|
||||
if (!providerAccountUsage[accountName].includes(extensionOrFeatureName)) {
|
||||
providerAccountUsage[accountName].push(extensionOrFeatureName);
|
||||
}
|
||||
} else {
|
||||
providerAccountUsage[accountName] = [extensionOrFeatureName];
|
||||
}
|
||||
|
||||
accountUsages.set(providerId, providerAccountUsage);
|
||||
}
|
||||
interface IAccountUsage {
|
||||
extensionId: string;
|
||||
extensionName: string;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] {
|
||||
let trustedExtensions: AllowedExtension[] = [];
|
||||
try {
|
||||
const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL);
|
||||
if (trustedExtensionSrc) {
|
||||
trustedExtensions = JSON.parse(trustedExtensionSrc);
|
||||
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
|
||||
}
|
||||
} catch (err) { }
|
||||
}
|
||||
|
||||
return trustedExtensions;
|
||||
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 {
|
||||
@@ -66,8 +79,10 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
private readonly _proxy: ExtHostAuthenticationShape,
|
||||
public readonly id: string,
|
||||
public readonly displayName: string,
|
||||
public readonly supportsMultipleAccounts: boolean,
|
||||
private readonly notificationService: INotificationService,
|
||||
private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService
|
||||
private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
private readonly storageService: IStorageService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -81,12 +96,17 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
}
|
||||
|
||||
private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) {
|
||||
const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>();
|
||||
const quickPick = quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>();
|
||||
quickPick.canSelectMany = true;
|
||||
const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName);
|
||||
const usages = readAccountUsages(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('accountLastUsedDate', "Last used this account {0}", fromNow(usage.lastUsed, true))
|
||||
: nls.localize('notUsed', "Has not used this account"),
|
||||
extension
|
||||
};
|
||||
});
|
||||
@@ -110,24 +130,6 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
quickPick.show();
|
||||
}
|
||||
|
||||
private showUsage(quickInputService: IQuickInputService, accountName: string) {
|
||||
const quickPick = quickInputService.createQuickPick();
|
||||
const providerUsage = accountUsages.get(this.id);
|
||||
const accountUsage = (providerUsage || {})[accountName] || [];
|
||||
|
||||
quickPick.items = accountUsage.map(extensionOrFeature => {
|
||||
return {
|
||||
label: extensionOrFeature
|
||||
};
|
||||
});
|
||||
|
||||
quickPick.onDidHide(() => {
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
quickPick.show();
|
||||
}
|
||||
|
||||
private async registerCommandsAndContextMenuItems(): Promise<void> {
|
||||
const sessions = await this._proxy.$getSessions(this.id);
|
||||
sessions.forEach(session => this.registerSession(session));
|
||||
@@ -163,10 +165,9 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
|
||||
const quickPick = quickInputService.createQuickPick();
|
||||
const showUsage = nls.localize('showUsage', "Show Extensions and Features Using This Account");
|
||||
const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions");
|
||||
const signOut = nls.localize('signOut', "Sign Out");
|
||||
const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]);
|
||||
const items = ([{ label: manage }, { label: signOut }]);
|
||||
|
||||
quickPick.items = items;
|
||||
|
||||
@@ -180,10 +181,6 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName);
|
||||
}
|
||||
|
||||
if (selected.label === showUsage) {
|
||||
this.showUsage(quickInputService, session.account.displayName);
|
||||
}
|
||||
|
||||
quickPick.dispose();
|
||||
});
|
||||
|
||||
@@ -199,39 +196,24 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
}
|
||||
|
||||
async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise<void> {
|
||||
const providerUsage = accountUsages.get(this.id);
|
||||
const accountUsage = (providerUsage || {})[session.account.displayName] || [];
|
||||
const accountUsages = readAccountUsages(this.storageService, this.id, session.account.displayName);
|
||||
const sessionsForAccount = this._accounts.get(session.account.displayName);
|
||||
|
||||
// Skip dialog if nothing is using the account
|
||||
if (!accountUsage.length) {
|
||||
accountUsages.set(this.id, { [session.account.displayName]: [] });
|
||||
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await dialogService.confirm({
|
||||
title: nls.localize('signOutConfirm', "Sign out of {0}", session.account.displayName),
|
||||
message: nls.localize('signOutMessage', "The account {0} is currently used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsage.join('\n'))
|
||||
message: accountUsages.length
|
||||
? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n'))
|
||||
: nls.localize('signOutMessageSimple', "Sign out of {0}?", session.account.displayName)
|
||||
});
|
||||
|
||||
if (result.confirmed) {
|
||||
accountUsages.set(this.id, { [session.account.displayName]: [] });
|
||||
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
|
||||
removeAccountUsage(this.storageService, this.id, session.account.displayName);
|
||||
}
|
||||
}
|
||||
|
||||
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
|
||||
return (await this._proxy.$getSessions(this.id)).map(session => {
|
||||
return {
|
||||
id: session.id,
|
||||
account: session.account,
|
||||
getAccessToken: () => {
|
||||
addAccountUsage(this.id, session.account.displayName, nls.localize('sync', "Preferences Sync"));
|
||||
return this._proxy.$getSessionAccessToken(this.id, session.id);
|
||||
}
|
||||
};
|
||||
});
|
||||
return this._proxy.$getSessions(this.id);
|
||||
}
|
||||
|
||||
async updateSessionItems(event: modes.AuthenticationSessionsChangeEvent): Promise<void> {
|
||||
@@ -262,13 +244,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
|
||||
}
|
||||
|
||||
login(scopes: string[]): Promise<modes.AuthenticationSession> {
|
||||
return this._proxy.$login(this.id, scopes).then(session => {
|
||||
return {
|
||||
id: session.id,
|
||||
account: session.account,
|
||||
getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id)
|
||||
};
|
||||
});
|
||||
return this._proxy.$login(this.id, scopes);
|
||||
}
|
||||
|
||||
async logout(sessionId: string): Promise<void> {
|
||||
@@ -294,14 +270,31 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService
|
||||
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
|
||||
@IQuickInputService private readonly quickInputService: IQuickInputService
|
||||
) {
|
||||
super();
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
|
||||
|
||||
this._register(this.authenticationService.onDidChangeSessions(e => {
|
||||
this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.event);
|
||||
}));
|
||||
|
||||
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(providerId => {
|
||||
this._proxy.$onDidChangeAuthenticationProviders([providerId], []);
|
||||
}));
|
||||
|
||||
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(providerId => {
|
||||
this._proxy.$onDidChangeAuthenticationProviders([], [providerId]);
|
||||
}));
|
||||
}
|
||||
|
||||
async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService, this.storageKeysSyncRegistryService);
|
||||
$getProviderIds(): Promise<string[]> {
|
||||
return Promise.resolve(this.authenticationService.getProviderIds());
|
||||
}
|
||||
|
||||
async $registerAuthenticationProvider(id: string, displayName: string, supportsMultipleAccounts: boolean): Promise<void> {
|
||||
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService);
|
||||
await provider.initialize();
|
||||
this.authenticationService.registerAuthenticationProvider(id, provider);
|
||||
}
|
||||
@@ -310,21 +303,142 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
||||
this.authenticationService.unregisterAuthenticationProvider(id);
|
||||
}
|
||||
|
||||
$onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
|
||||
$sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
|
||||
this.authenticationService.sessionsUpdate(id, event);
|
||||
}
|
||||
|
||||
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
|
||||
addAccountUsage(providerId, accountName, extensionName);
|
||||
$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.sort().join(' ') === orderedScopes);
|
||||
const displayName = this.authenticationService.getDisplayName(providerId);
|
||||
|
||||
if (sessions.length) {
|
||||
if (!this.authenticationService.supportsMultipleAccounts(providerId)) {
|
||||
const session = sessions[0];
|
||||
const allowed = await this.$getSessionsPrompt(providerId, session.account.displayName, displayName, 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, displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference);
|
||||
return sessions.find(session => session.id === selected.id);
|
||||
} else {
|
||||
if (options.createIfNone) {
|
||||
const isAllowed = await this.$loginPrompt(displayName, extensionName);
|
||||
if (!isAllowed) {
|
||||
throw new Error('User did not consent to login.');
|
||||
}
|
||||
|
||||
const session = await this.authenticationService.login(providerId, scopes);
|
||||
await this.$setTrustedExtension(providerId, session.account.displayName, extensionId, extensionName);
|
||||
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.displayName, 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.displayName,
|
||||
session
|
||||
};
|
||||
});
|
||||
|
||||
items.push({
|
||||
label: nls.localize('useOtherAccount', "Sign in to another account")
|
||||
});
|
||||
|
||||
quickPick.items = items;
|
||||
quickPick.title = nls.localize('selectAccount', "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.displayName;
|
||||
|
||||
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();
|
||||
if (remoteConnection && remoteConnection.remoteAuthority && remoteConnection.remoteAuthority.startsWith('vsonline') && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) {
|
||||
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -339,6 +453,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -94,8 +94,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
|
||||
}, {
|
||||
allowScripts: options.enableScripts,
|
||||
localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined
|
||||
});
|
||||
webview.extension = { id: extensionId, location: URI.revive(extensionLocation) };
|
||||
}, { id: extensionId, location: URI.revive(extensionLocation) });
|
||||
|
||||
const webviewZone = new EditorWebviewZone(editor, line, height, webview);
|
||||
|
||||
@@ -128,7 +127,10 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape {
|
||||
|
||||
$setOptions(handle: number, options: modes.IWebviewOptions): void {
|
||||
const inset = this.getInset(handle);
|
||||
inset.webview.contentOptions = options;
|
||||
inset.webview.contentOptions = {
|
||||
...options,
|
||||
localResourceRoots: options.localResourceRoots?.map(components => URI.from(components)),
|
||||
};
|
||||
}
|
||||
|
||||
async $postMessage(handle: number, value: any): Promise<boolean> {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {
|
||||
@@ -15,15 +16,15 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape {
|
||||
private readonly _activeOwners = new Set<string>();
|
||||
|
||||
private readonly _proxy: ExtHostDiagnosticsShape;
|
||||
private readonly _markerService: IMarkerService;
|
||||
private readonly _markerListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@IMarkerService markerService: IMarkerService
|
||||
@IMarkerService private readonly _markerService: IMarkerService,
|
||||
@IUriIdentityService private readonly _uriIdentService: IUriIdentityService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDiagnostics);
|
||||
this._markerService = markerService;
|
||||
|
||||
this._markerListener = this._markerService.onMarkerChanged(this._forwardMarkers, this);
|
||||
}
|
||||
|
||||
@@ -59,7 +60,7 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape {
|
||||
}
|
||||
}
|
||||
}
|
||||
this._markerService.changeOne(owner, URI.revive(uri), markers);
|
||||
this._markerService.changeOne(owner, this._uriIdentService.asCanonicalUri(URI.revive(uri)), markers);
|
||||
}
|
||||
this._activeOwners.add(owner);
|
||||
}
|
||||
|
||||
@@ -10,17 +10,19 @@ 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 } from 'vs/platform/files/common/files';
|
||||
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 } from 'vs/base/common/resources';
|
||||
import { toLocalResource, isEqualOrParent, extUri } from 'vs/base/common/resources';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
|
||||
|
||||
export class BoundModelReferenceCollection {
|
||||
|
||||
private _data = new Array<{ length: number, dispose(): void }>();
|
||||
private _data = new Array<{ uri: URI, length: number, dispose(): void }>();
|
||||
private _length = 0;
|
||||
|
||||
constructor(
|
||||
@@ -34,10 +36,18 @@ export class BoundModelReferenceCollection {
|
||||
this._data = dispose(this._data);
|
||||
}
|
||||
|
||||
add(ref: IReference<ITextEditorModel>): void {
|
||||
remove(uri: URI): void {
|
||||
for (const entry of [...this._data] /* copy array because dispose will modify it */) {
|
||||
if (isEqualOrParent(entry.uri, uri)) {
|
||||
entry.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(uri: URI, ref: IReference<ITextEditorModel>): void {
|
||||
const length = ref.object.textEditorModel.getValueLength();
|
||||
let handle: any;
|
||||
let entry: { length: number, dispose(): void };
|
||||
let entry: { uri: URI, length: number, dispose(): void };
|
||||
const dispose = () => {
|
||||
const idx = this._data.indexOf(entry);
|
||||
if (idx >= 0) {
|
||||
@@ -48,7 +58,7 @@ export class BoundModelReferenceCollection {
|
||||
}
|
||||
};
|
||||
handle = setTimeout(dispose, this._maxAge);
|
||||
entry = { length, dispose };
|
||||
entry = { uri, length, dispose };
|
||||
|
||||
this._data.push(entry);
|
||||
this._length += length;
|
||||
@@ -69,12 +79,13 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
|
||||
private readonly _textFileService: ITextFileService;
|
||||
private readonly _fileService: IFileService;
|
||||
private readonly _environmentService: IWorkbenchEnvironmentService;
|
||||
private readonly _uriIdentityService: IUriIdentityService;
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _modelToDisposeMap: { [modelUrl: string]: IDisposable; };
|
||||
private readonly _proxy: ExtHostDocumentsShape;
|
||||
private readonly _modelIsSynced = new Set<string>();
|
||||
private _modelReferenceCollection = new BoundModelReferenceCollection();
|
||||
private readonly _modelReferenceCollection = new BoundModelReferenceCollection();
|
||||
|
||||
constructor(
|
||||
documentsAndEditors: MainThreadDocumentsAndEditors,
|
||||
@@ -83,13 +94,16 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IFileService fileService: IFileService,
|
||||
@ITextModelService textModelResolverService: ITextModelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService
|
||||
) {
|
||||
this._modelService = modelService;
|
||||
this._textModelResolverService = textModelResolverService;
|
||||
this._textFileService = textFileService;
|
||||
this._fileService = fileService;
|
||||
this._environmentService = environmentService;
|
||||
this._uriIdentityService = uriIdentityService;
|
||||
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments);
|
||||
|
||||
@@ -109,6 +123,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
|
||||
}
|
||||
}));
|
||||
|
||||
this._toDispose.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => {
|
||||
if (e.source && (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE)) {
|
||||
this._modelReferenceCollection.remove(e.source);
|
||||
}
|
||||
}));
|
||||
|
||||
this._modelToDisposeMap = Object.create(null);
|
||||
}
|
||||
|
||||
@@ -163,33 +183,37 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
|
||||
return this._textFileService.save(URI.revive(uri)).then(target => !!target);
|
||||
}
|
||||
|
||||
$tryOpenDocument(_uri: UriComponents): Promise<any> {
|
||||
const uri = URI.revive(_uri);
|
||||
if (!uri.scheme || !(uri.fsPath || uri.authority)) {
|
||||
$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.`));
|
||||
}
|
||||
|
||||
let promise: Promise<boolean>;
|
||||
switch (uri.scheme) {
|
||||
const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri);
|
||||
|
||||
let promise: Promise<URI>;
|
||||
switch (canonicalUri.scheme) {
|
||||
case Schemas.untitled:
|
||||
promise = this._handleUntitledScheme(uri);
|
||||
promise = this._handleUntitledScheme(canonicalUri);
|
||||
break;
|
||||
case Schemas.file:
|
||||
default:
|
||||
promise = this._handleAsResourceInput(uri);
|
||||
promise = this._handleAsResourceInput(canonicalUri);
|
||||
break;
|
||||
}
|
||||
|
||||
return promise.then(success => {
|
||||
if (!success) {
|
||||
return Promise.reject(new Error('cannot open ' + uri.toString()));
|
||||
} else if (!this._modelIsSynced.has(uri.toString())) {
|
||||
return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: Files above 50MB cannot be synchronized with extensions.'));
|
||||
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 undefined;
|
||||
return canonicalUri;
|
||||
}
|
||||
}, err => {
|
||||
return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: ' + toErrorMessage(err)));
|
||||
return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -197,21 +221,20 @@ export class MainThreadDocuments implements MainThreadDocumentsShape {
|
||||
return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined);
|
||||
}
|
||||
|
||||
private _handleAsResourceInput(uri: URI): Promise<boolean> {
|
||||
private _handleAsResourceInput(uri: URI): Promise<URI> {
|
||||
return this._textModelResolverService.createModelReference(uri).then(ref => {
|
||||
this._modelReferenceCollection.add(ref);
|
||||
const result = !!ref.object;
|
||||
return result;
|
||||
this._modelReferenceCollection.add(uri, ref);
|
||||
return ref.object.textEditorModel.uri;
|
||||
});
|
||||
}
|
||||
|
||||
private _handleUntitledScheme(uri: URI): Promise<boolean> {
|
||||
private _handleUntitledScheme(uri: URI): Promise<URI> {
|
||||
const asLocalUri = toLocalResource(uri, this._environmentService.configuration.remoteAuthority);
|
||||
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).then(resource => !!resource);
|
||||
return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor
|
||||
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';
|
||||
|
||||
namespace delta {
|
||||
|
||||
@@ -326,11 +328,13 @@ export class MainThreadDocumentsAndEditors {
|
||||
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
|
||||
@IBulkEditService bulkEditService: IBulkEditService,
|
||||
@IPanelService panelService: IPanelService,
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
|
||||
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
|
||||
@IUriIdentityService uriIdentityService: IUriIdentityService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors);
|
||||
|
||||
const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService));
|
||||
const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService));
|
||||
extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments);
|
||||
|
||||
const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService));
|
||||
|
||||
@@ -8,12 +8,9 @@ import { FileChangeType, IFileService, FileOperation } from 'vs/platform/files/c
|
||||
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol';
|
||||
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { localize } from 'vs/nls';
|
||||
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
|
||||
@extHostCustomer
|
||||
@@ -25,9 +22,6 @@ export class MainThreadFileSystemEventService {
|
||||
extHostContext: IExtHostContext,
|
||||
@IFileService fileService: IFileService,
|
||||
@ITextFileService textFileService: ITextFileService,
|
||||
@IProgressService progressService: IProgressService,
|
||||
@IConfigurationService configService: IConfigurationService,
|
||||
@ILogService logService: ILogService,
|
||||
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService
|
||||
) {
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ 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 {
|
||||
@@ -40,4 +43,19 @@ export class MainThreadLanguages implements MainThreadLanguagesShape {
|
||||
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))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol';
|
||||
import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService';
|
||||
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
|
||||
import { IRelativePattern } from 'vs/base/common/glob';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export class MainThreadNotebookDocument extends Disposable {
|
||||
private _textModel: NotebookTextModel;
|
||||
@@ -27,37 +29,103 @@ export class MainThreadNotebookDocument extends Disposable {
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
public handle: number,
|
||||
public viewType: string,
|
||||
public uri: URI
|
||||
public uri: URI,
|
||||
readonly notebookService: INotebookService
|
||||
) {
|
||||
super();
|
||||
this._textModel = new NotebookTextModel(handle, viewType, uri);
|
||||
this._register(this._textModel.onDidModelChange(e => {
|
||||
this._register(this._textModel.onDidModelChangeProxy(e => {
|
||||
this._proxy.$acceptModelChanged(this.uri, e);
|
||||
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null });
|
||||
}));
|
||||
this._register(this._textModel.onDidSelectionChange(e => {
|
||||
const selectionsChange = e ? { selections: e } : null;
|
||||
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange });
|
||||
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange, metadata: null });
|
||||
}));
|
||||
}
|
||||
|
||||
applyEdit(modelVersionId: number, edits: ICellEditOperation[]): boolean {
|
||||
return this._textModel.applyEdit(modelVersionId, edits);
|
||||
async applyEdit(modelVersionId: number, edits: ICellEditOperation[]): Promise<boolean> {
|
||||
await this.notebookService.transformEditsOutputs(this.textModel, edits);
|
||||
return this._textModel.$applyEdit(modelVersionId, edits);
|
||||
}
|
||||
|
||||
updateRenderers(renderers: number[]) {
|
||||
this._textModel.updateRenderers(renderers);
|
||||
async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) {
|
||||
await this.notebookService.transformSpliceOutputs(this.textModel, splices);
|
||||
this._textModel.$spliceNotebookCellOutputs(cellHandle, splices);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._textModel.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentAndEditorState {
|
||||
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!.textModel!.selections });
|
||||
}
|
||||
|
||||
return {
|
||||
addedDocuments: [],
|
||||
addedEditors: apiEditors
|
||||
};
|
||||
}
|
||||
// const documentDelta = delta.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.textModel!.selections
|
||||
}));
|
||||
|
||||
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;
|
||||
|
||||
return {
|
||||
addedEditors: addedAPIEditors,
|
||||
removedEditors: removedAPIEditors,
|
||||
newActiveEditor: newActiveEditor
|
||||
};
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly documents: Set<URI>,
|
||||
readonly textEditors: Map<string, IEditor>,
|
||||
readonly activeEditor: string | null | undefined,
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadNotebook)
|
||||
export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape {
|
||||
private readonly _notebookProviders = new Map<string, MainThreadNotebookController>();
|
||||
private readonly _notebookKernels = new Map<string, MainThreadNotebookKernel>();
|
||||
private readonly _notebookRenderers = new Map<string, MainThreadNotebookRenderer>();
|
||||
private readonly _proxy: ExtHostNotebookShape;
|
||||
private _toDisposeOnEditorRemove = new Map<string, IDisposable>();
|
||||
private _currentState?: DocumentAndEditorState;
|
||||
|
||||
constructor(
|
||||
extHostContext: IExtHostContext,
|
||||
@@ -83,12 +151,30 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
|
||||
}
|
||||
|
||||
registerListeners() {
|
||||
this._notebookService.listNotebookEditors().forEach((e) => {
|
||||
this._addNotebookEditor(e);
|
||||
});
|
||||
|
||||
this._register(this._notebookService.onDidChangeActiveEditor(e => {
|
||||
this._proxy.$acceptDocumentAndEditorsDelta({
|
||||
newActiveEditor: e.uri
|
||||
newActiveEditor: e
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onDidChangeVisibleEditors(e => {
|
||||
this._proxy.$acceptDocumentAndEditorsDelta({
|
||||
visibleEditors: e
|
||||
});
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onNotebookEditorAdd(editor => {
|
||||
this._addNotebookEditor(editor);
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onNotebookEditorRemove(editor => {
|
||||
this._removeNotebookEditor(editor);
|
||||
}));
|
||||
|
||||
const updateOrder = () => {
|
||||
let userOrder = this.configurationService.getValue<string[]>('notebook.displayOrder');
|
||||
this._proxy.$acceptDisplayOrder({
|
||||
@@ -108,29 +194,120 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
|
||||
this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => {
|
||||
updateOrder();
|
||||
}));
|
||||
|
||||
const activeEditorPane = this.editorService.activeEditorPane as any | undefined;
|
||||
const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined;
|
||||
this._updateState(notebookEditor);
|
||||
}
|
||||
|
||||
async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise<void> {
|
||||
this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri)));
|
||||
async addNotebookDocument(data: INotebookModelAddedData) {
|
||||
await this._proxy.$acceptDocumentAndEditorsDelta({
|
||||
addedDocuments: [data]
|
||||
});
|
||||
}
|
||||
|
||||
async $unregisterNotebookRenderer(handle: number): Promise<void> {
|
||||
this._notebookService.unregisterNotebookRenderer(handle);
|
||||
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);
|
||||
}
|
||||
|
||||
async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise<void> {
|
||||
let controller = new MainThreadNotebookController(this._proxy, this, viewType);
|
||||
private _removeNotebookEditor(e: IEditor) {
|
||||
const sub = this._toDisposeOnEditorRemove.get(e.getId());
|
||||
if (sub) {
|
||||
this._toDisposeOnEditorRemove.delete(e.getId());
|
||||
sub.dispose();
|
||||
this._updateState();
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateState(focusedNotebookEditor?: IEditor) {
|
||||
const documents = new Set<URI>();
|
||||
this._notebookService.listNotebookDocuments().forEach(document => {
|
||||
documents.add(document.uri);
|
||||
});
|
||||
|
||||
const editors = new Map<string, IEditor>();
|
||||
let activeEditor: string | null = null;
|
||||
|
||||
for (const editor of this._notebookService.listNotebookEditors()) {
|
||||
if (editor.hasModel()) {
|
||||
editors.set(editor.getId(), editor);
|
||||
if (editor.hasFocus()) {
|
||||
activeEditor = editor.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!activeEditor && focusedNotebookEditor) {
|
||||
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);
|
||||
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._proxy.$acceptDocumentAndEditorsDelta(delta);
|
||||
// }
|
||||
}
|
||||
|
||||
async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise<void> {
|
||||
const renderer = new MainThreadNotebookRenderer(this._proxy, type, extension.id, URI.revive(extension.location), selectors, preloads.map(uri => URI.revive(uri)));
|
||||
this._notebookRenderers.set(type, renderer);
|
||||
this._notebookService.registerNotebookRenderer(type, renderer);
|
||||
}
|
||||
|
||||
async $unregisterNotebookRenderer(id: string): Promise<void> {
|
||||
this._notebookService.unregisterNotebookRenderer(id);
|
||||
}
|
||||
|
||||
async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, kernel: INotebookKernelInfoDto | undefined): Promise<void> {
|
||||
let controller = new MainThreadNotebookController(this._proxy, this, viewType, kernel, this._notebookService);
|
||||
this._notebookProviders.set(viewType, controller);
|
||||
this._notebookService.registerNotebookController(viewType, extension, controller);
|
||||
return;
|
||||
}
|
||||
|
||||
async $onNotebookChange(viewType: string, uri: UriComponents): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
if (controller) {
|
||||
controller.handleNotebookChange(uri);
|
||||
}
|
||||
}
|
||||
|
||||
async $unregisterNotebookProvider(viewType: string): Promise<void> {
|
||||
this._notebookProviders.delete(viewType);
|
||||
this._notebookService.unregisterNotebookProvider(viewType);
|
||||
return;
|
||||
}
|
||||
|
||||
async $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise<void> {
|
||||
const kernel = new MainThreadNotebookKernel(this._proxy, id, label, selectors, extension.id, URI.revive(extension.location), preloads.map(preload => URI.revive(preload)));
|
||||
this._notebookKernels.set(id, kernel);
|
||||
this._notebookService.registerNotebookKernel(kernel);
|
||||
return;
|
||||
}
|
||||
|
||||
async $unregisterNotebookKernel(id: string): Promise<void> {
|
||||
this._notebookKernels.delete(id);
|
||||
this._notebookService.unregisterNotebookKernel(id);
|
||||
return;
|
||||
}
|
||||
|
||||
async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
|
||||
@@ -157,18 +334,18 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
|
||||
|
||||
async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void> {
|
||||
let controller = this._notebookProviders.get(viewType);
|
||||
controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers);
|
||||
await controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers);
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$executeNotebook(viewType, uri, undefined, token);
|
||||
async executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$executeNotebook(viewType, uri, undefined, useAttachedKernel, token);
|
||||
}
|
||||
|
||||
async $postMessage(handle: number, value: any): Promise<boolean> {
|
||||
|
||||
const activeEditorPane = this.editorService.activeEditorPane as any | undefined;
|
||||
if (activeEditorPane?.isNotebookEditor) {
|
||||
const notebookEditor = (activeEditorPane as INotebookEditor);
|
||||
const notebookEditor = (activeEditorPane.getControl() as INotebookEditor);
|
||||
|
||||
if (notebookEditor.viewModel?.handle === handle) {
|
||||
notebookEditor.postMessage(value);
|
||||
@@ -187,11 +364,14 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
private _mainThreadNotebook: MainThreadNotebooks,
|
||||
private _viewType: string
|
||||
private _viewType: string,
|
||||
readonly kernel: INotebookKernelInfoDto | undefined,
|
||||
readonly notebookService: INotebookService,
|
||||
|
||||
) {
|
||||
}
|
||||
|
||||
async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise<NotebookTextModel | undefined> {
|
||||
async createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string): Promise<NotebookTextModel | undefined> {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(uri).toString());
|
||||
|
||||
if (mainthreadNotebook) {
|
||||
@@ -203,7 +383,7 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
|
||||
mainthreadNotebook.textModel.languages = data.languages;
|
||||
mainthreadNotebook.textModel.metadata = data.metadata;
|
||||
mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [
|
||||
mainthreadNotebook.textModel.$applyEdit(mainthreadNotebook.textModel.versionId, [
|
||||
{ editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 },
|
||||
{ editType: CellEditType.Insert, index: 0, cells: data.cells }
|
||||
]);
|
||||
@@ -211,10 +391,43 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
return mainthreadNotebook.textModel;
|
||||
}
|
||||
|
||||
let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri);
|
||||
await this.createNotebookDocument(document);
|
||||
let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri, this.notebookService);
|
||||
this._mapping.set(document.uri.toString(), document);
|
||||
|
||||
if (backup) {
|
||||
// trigger events
|
||||
document.textModel.metadata = backup.metadata;
|
||||
document.textModel.languages = backup.languages;
|
||||
|
||||
document.textModel.$applyEdit(document.textModel.versionId, [
|
||||
{
|
||||
editType: CellEditType.Insert,
|
||||
index: 0,
|
||||
cells: backup.cells || []
|
||||
}
|
||||
]);
|
||||
|
||||
await this._mainThreadNotebook.addNotebookDocument({
|
||||
viewType: document.viewType,
|
||||
handle: document.handle,
|
||||
uri: document.uri,
|
||||
metadata: document.textModel.metadata,
|
||||
versionId: document.textModel.versionId,
|
||||
cells: document.textModel.cells.map(cell => ({
|
||||
handle: cell.handle,
|
||||
uri: cell.uri,
|
||||
source: cell.textBuffer.getLinesContent(),
|
||||
language: cell.language,
|
||||
cellKind: cell.cellKind,
|
||||
outputs: cell.outputs,
|
||||
metadata: cell.metadata
|
||||
})),
|
||||
attachedEditor: editorId ? {
|
||||
id: editorId,
|
||||
selections: document.textModel.selections
|
||||
} : undefined
|
||||
});
|
||||
|
||||
if (forBackup) {
|
||||
return document.textModel;
|
||||
}
|
||||
|
||||
@@ -226,7 +439,36 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
|
||||
document.textModel.languages = data.languages;
|
||||
document.textModel.metadata = data.metadata;
|
||||
document.textModel.initialize(data!.cells);
|
||||
|
||||
if (data.cells.length) {
|
||||
document.textModel.initialize(data!.cells);
|
||||
} else {
|
||||
const mainCell = document.textModel.createCellTextModel([''], document.textModel.languages.length ? document.textModel.languages[0] : '', CellKind.Code, [], undefined);
|
||||
document.textModel.insertTemplateCell(mainCell);
|
||||
}
|
||||
|
||||
await this._mainThreadNotebook.addNotebookDocument({
|
||||
viewType: document.viewType,
|
||||
handle: document.handle,
|
||||
uri: document.uri,
|
||||
metadata: document.textModel.metadata,
|
||||
versionId: document.textModel.versionId,
|
||||
cells: document.textModel.cells.map(cell => ({
|
||||
handle: cell.handle,
|
||||
uri: cell.uri,
|
||||
source: cell.textBuffer.getLinesContent(),
|
||||
language: cell.language,
|
||||
cellKind: cell.cellKind,
|
||||
outputs: cell.outputs,
|
||||
metadata: cell.metadata
|
||||
})),
|
||||
attachedEditor: editorId ? {
|
||||
id: editorId,
|
||||
selections: document.textModel.selections
|
||||
} : undefined
|
||||
});
|
||||
|
||||
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: null, metadata: document.textModel.metadata });
|
||||
|
||||
return document.textModel;
|
||||
}
|
||||
@@ -235,37 +477,23 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
|
||||
|
||||
if (mainthreadNotebook) {
|
||||
mainthreadNotebook.updateRenderers(renderers);
|
||||
return mainthreadNotebook.applyEdit(modelVersionId, edits);
|
||||
return await mainthreadNotebook.applyEdit(modelVersionId, edits);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): void {
|
||||
async spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise<void> {
|
||||
let mainthreadNotebook = this._mapping.get(URI.from(resource).toString());
|
||||
mainthreadNotebook?.textModel.updateRenderers(renderers);
|
||||
mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices);
|
||||
await mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices);
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise<void> {
|
||||
this._mainThreadNotebook.executeNotebook(viewType, uri, token);
|
||||
async executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise<void> {
|
||||
return this._mainThreadNotebook.executeNotebook(viewType, uri, useAttachedKernel, token);
|
||||
}
|
||||
|
||||
onDidReceiveMessage(uri: UriComponents, message: any): void {
|
||||
this._proxy.$onDidReceiveMessage(uri, message);
|
||||
}
|
||||
|
||||
async createNotebookDocument(document: MainThreadNotebookDocument): Promise<void> {
|
||||
this._mapping.set(document.uri.toString(), document);
|
||||
|
||||
await this._proxy.$acceptDocumentAndEditorsDelta({
|
||||
addedDocuments: [{
|
||||
viewType: document.viewType,
|
||||
handle: document.handle,
|
||||
uri: document.uri
|
||||
}]
|
||||
});
|
||||
onDidReceiveMessage(editorId: string, message: any): void {
|
||||
this._proxy.$onDidReceiveMessage(editorId, message);
|
||||
}
|
||||
|
||||
async removeNotebookDocument(notebook: INotebookTextModel): Promise<void> {
|
||||
@@ -282,6 +510,11 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
|
||||
// Methods for ExtHost
|
||||
|
||||
handleNotebookChange(resource: UriComponents) {
|
||||
let document = this._mapping.get(URI.from(resource).toString());
|
||||
document?.textModel.handleUnknownChange();
|
||||
}
|
||||
|
||||
updateLanguages(resource: UriComponents, languages: string[]) {
|
||||
let document = this._mapping.get(URI.from(resource).toString());
|
||||
document?.textModel.updateLanguages(languages);
|
||||
@@ -297,13 +530,8 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
document?.textModel.updateNotebookCellMetadata(handle, metadata);
|
||||
}
|
||||
|
||||
updateNotebookRenderers(resource: UriComponents, renderers: number[]): void {
|
||||
let document = this._mapping.get(URI.from(resource).toString());
|
||||
document?.textModel.updateRenderers(renderers);
|
||||
}
|
||||
|
||||
async executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$executeNotebook(this._viewType, uri, handle, token);
|
||||
async executeNotebookCell(uri: URI, handle: number, useAttachedKernel: boolean, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$executeNotebook(this._viewType, uri, handle, useAttachedKernel, token);
|
||||
}
|
||||
|
||||
async save(uri: URI, token: CancellationToken): Promise<boolean> {
|
||||
@@ -315,3 +543,41 @@ export class MainThreadNotebookController implements IMainNotebookController {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadNotebookKernel implements INotebookKernelInfo {
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
readonly id: string,
|
||||
readonly label: string,
|
||||
readonly selectors: (string | IRelativePattern)[],
|
||||
readonly extension: ExtensionIdentifier,
|
||||
readonly extensionLocation: URI,
|
||||
readonly preloads: URI[]
|
||||
) {
|
||||
}
|
||||
|
||||
async executeNotebook(viewType: string, uri: URI, handle: number | undefined, token: CancellationToken): Promise<void> {
|
||||
return this._proxy.$executeNotebook2(this.id, viewType, uri, handle, token);
|
||||
}
|
||||
}
|
||||
|
||||
export class MainThreadNotebookRenderer implements INotebookRendererInfo {
|
||||
constructor(
|
||||
private readonly _proxy: ExtHostNotebookShape,
|
||||
readonly id: string,
|
||||
readonly extensionId: ExtensionIdentifier,
|
||||
readonly extensionLocation: URI,
|
||||
readonly selectors: INotebookMimeTypeSelector,
|
||||
readonly preloads: URI[]
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
render(uri: URI, request: IOutputRenderRequest<UriComponents>): Promise<IOutputRenderResponse<UriComponents> | undefined> {
|
||||
return this._proxy.$renderOutputs(uri, this.id, request);
|
||||
}
|
||||
|
||||
render2<T>(uri: URI, request: IOutputRenderRequest<T>): Promise<IOutputRenderResponse<T> | undefined> {
|
||||
return this._proxy.$renderOutputs2(uri, this.id, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 {
|
||||
@@ -25,9 +26,14 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape {
|
||||
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): void {
|
||||
$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
|
||||
const ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text;
|
||||
let ariaLabel: string;
|
||||
if (accessibilityInformation) {
|
||||
ariaLabel = accessibilityInformation.label;
|
||||
} else {
|
||||
ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text;
|
||||
}
|
||||
const entry: IStatusbarEntry = { text, tooltip, command, color, ariaLabel };
|
||||
|
||||
if (typeof priority === 'undefined') {
|
||||
|
||||
@@ -25,6 +25,7 @@ export class MainThreadTheming implements MainThreadThemingShape {
|
||||
this._themeChangeListener = this._themeService.onDidColorThemeChange(e => {
|
||||
this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type);
|
||||
});
|
||||
this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -325,13 +325,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
|
||||
throw new Error(`Provider for ${viewType} already registered`);
|
||||
}
|
||||
|
||||
this._customEditorService.registerCustomEditorCapabilities(viewType, {
|
||||
supportsMultipleEditorsPerDocument
|
||||
});
|
||||
|
||||
const extension = reviveWebviewExtension(extensionData);
|
||||
|
||||
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;
|
||||
@@ -360,6 +361,17 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
|
||||
}
|
||||
|
||||
webviewInput.webview.onDispose(() => {
|
||||
// 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();
|
||||
});
|
||||
|
||||
@@ -649,10 +661,11 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
||||
) {
|
||||
super();
|
||||
|
||||
this._fromBackup = fromBackup;
|
||||
|
||||
if (_editable) {
|
||||
this._register(workingCopyService.registerWorkingCopy(this));
|
||||
}
|
||||
this._fromBackup = fromBackup;
|
||||
}
|
||||
|
||||
get editorResource() {
|
||||
@@ -710,7 +723,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
||||
//#endregion
|
||||
|
||||
public isReadonly() {
|
||||
return this._editable;
|
||||
return !this._editable;
|
||||
}
|
||||
|
||||
public get viewType() {
|
||||
|
||||
@@ -80,6 +80,9 @@ interface IUserFriendlyViewDescriptor {
|
||||
name: string;
|
||||
when?: string;
|
||||
|
||||
icon?: string;
|
||||
contextualTitle?: string;
|
||||
|
||||
// From 'remoteViewDescriptor' type
|
||||
group?: string;
|
||||
remoteName?: string | string[];
|
||||
@@ -100,6 +103,14 @@ const viewDescriptor: IJSONSchema = {
|
||||
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'
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -406,12 +417,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
|
||||
? container.viewOrderDelegate.getOrder(item.group)
|
||||
: undefined;
|
||||
|
||||
const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined;
|
||||
const viewDescriptor = <ICustomViewDescriptor>{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
ctorDescriptor: new SyncDescriptor(TreeViewPane),
|
||||
when: ContextKeyExpr.deserialize(item.when),
|
||||
containerIcon: viewContainer?.icon,
|
||||
containerIcon: icon || viewContainer?.icon,
|
||||
containerTitle: item.contextualTitle || viewContainer?.name,
|
||||
canToggleVisibility: true,
|
||||
canMoveView: true,
|
||||
treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name),
|
||||
@@ -468,6 +481,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user