Merge from vscode 2e5312cd61ff99c570299ecc122c52584265eda2

This commit is contained in:
ADS Merger
2020-04-23 02:50:35 +00:00
committed by Anthony Dresser
parent 3603f55d97
commit 7f1d8fc32f
659 changed files with 22709 additions and 12497 deletions

View File

@@ -12,31 +12,35 @@ import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContex
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 { MenuRegistry, MenuId, IMenuItem } from 'vs/platform/actions/common/actions';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
interface AuthDependent {
providerId: string;
label: string;
scopes: string[];
scopeDescriptions?: string;
}
const BUILT_IN_AUTH_DEPENDENTS: AuthDependent[] = [
{
providerId: 'microsoft',
label: 'Settings sync',
scopes: ['https://management.core.windows.net/.default', 'offline_access'],
scopeDescriptions: 'Read user email'
}
];
import { INotificationService } from 'vs/platform/notification/common/notification';
interface AllowedExtension {
id: string;
name: string;
}
const accountUsages = new Map<string, { [accountName: string]: string[] }>();
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);
}
}
function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] {
let trustedExtensions: AllowedExtension[] = [];
try {
@@ -53,17 +57,22 @@ export class MainThreadAuthenticationProvider extends Disposable {
private _sessionMenuItems = new Map<string, IDisposable[]>();
private _accounts = new Map<string, string[]>(); // Map account name to session ids
private _sessions = new Map<string, string>(); // Map account id to name
private _signInMenuItem: IMenuItem | undefined;
constructor(
private readonly _proxy: ExtHostAuthenticationShape,
public readonly id: string,
public readonly displayName: string,
public readonly dependents: AuthDependent[]
private readonly notificationService: INotificationService
) {
super();
}
this.registerCommandsAndContextMenuItems();
public async initialize(): Promise<void> {
return this.registerCommandsAndContextMenuItems();
}
public hasSessions(): boolean {
return !!this._sessions.size;
}
private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) {
@@ -96,50 +105,45 @@ 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);
if (this.dependents.length) {
this._register(CommandsRegistry.registerCommand({
id: `signIn${this.id}`,
handler: (accessor, args) => {
this.login(this.dependents.reduce((previous: string[], current) => previous.concat(current.scopes), []));
},
}));
this._signInMenuItem = {
group: '2_providers',
command: {
id: `signIn${this.id}`,
title: sessions.length
? nls.localize('addAnotherAccount', "Sign in to another {0} account", this.displayName)
: nls.localize('addAccount', "Sign in to {0}", this.displayName)
},
order: 3
};
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, this._signInMenuItem));
}
sessions.forEach(session => this.registerSession(session));
}
private registerSession(session: modes.AuthenticationSession) {
this._sessions.set(session.id, session.accountName);
this._sessions.set(session.id, session.account.displayName);
const existingSessionsForAccount = this._accounts.get(session.accountName);
const existingSessionsForAccount = this._accounts.get(session.account.displayName);
if (existingSessionsForAccount) {
this._accounts.set(session.accountName, existingSessionsForAccount.concat(session.id));
this._accounts.set(session.account.displayName, existingSessionsForAccount.concat(session.id));
return;
} else {
this._accounts.set(session.accountName, [session.id]);
this._accounts.set(session.account.displayName, [session.id]);
}
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '1_accounts',
command: {
id: `configureSessions${session.id}`,
title: session.accountName
title: `${session.account.displayName} (${this.displayName})`
},
order: 3
});
@@ -149,23 +153,28 @@ export class MainThreadAuthenticationProvider extends Disposable {
handler: (accessor, args) => {
const quickInputService = accessor.get(IQuickInputService);
const storageService = accessor.get(IStorageService);
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: manage }, { label: signOut }]);
const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]);
quickPick.items = items;
quickPick.onDidAccept(e => {
const selected = quickPick.selectedItems[0];
if (selected.label === signOut) {
const sessionsForAccount = this._accounts.get(session.accountName);
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
this.signOut(dialogService, session);
}
if (selected.label === manage) {
this.manageTrustedExtensions(quickInputService, storageService, session.accountName);
this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName);
}
if (selected.label === showUsage) {
this.showUsage(quickInputService, session.account.displayName);
}
quickPick.dispose();
@@ -179,15 +188,41 @@ export class MainThreadAuthenticationProvider extends Disposable {
},
});
this._sessionMenuItems.set(session.accountName, [menuItem, manageCommand]);
this._sessionMenuItems.set(session.account.displayName, [menuItem, manageCommand]);
}
async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise<void> {
const providerUsage = accountUsages.get(this.id);
const accountUsage = (providerUsage || {})[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'))
});
if (result.confirmed) {
accountUsages.set(this.id, { [session.account.displayName]: [] });
sessionsForAccount?.forEach(sessionId => this.logout(sessionId));
}
}
async getSessions(): Promise<ReadonlyArray<modes.AuthenticationSession>> {
return (await this._proxy.$getSessions(this.id)).map(session => {
return {
id: session.id,
accountName: session.accountName,
getAccessToken: () => this._proxy.$getSessionAccessToken(this.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);
}
};
});
}
@@ -200,6 +235,7 @@ export class MainThreadAuthenticationProvider extends Disposable {
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);
@@ -211,33 +247,26 @@ export class MainThreadAuthenticationProvider extends Disposable {
this._sessionMenuItems.delete(accountName);
}
this._accounts.delete(accountName);
if (this._signInMenuItem) {
this._signInMenuItem.command.title = nls.localize('addAccount', "Sign in to {0}", this.displayName);
}
}
}
});
addedSessions.forEach(session => this.registerSession(session));
if (addedSessions.length && this._signInMenuItem) {
this._signInMenuItem.command.title = nls.localize('addAnotherAccount', "Sign in to another {0} account", this.displayName);
}
}
login(scopes: string[]): Promise<modes.AuthenticationSession> {
return this._proxy.$login(this.id, scopes).then(session => {
return {
id: session.id,
accountName: session.accountName,
account: session.account,
getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id)
};
});
}
logout(sessionId: string): Promise<void> {
return this._proxy.$logout(this.id, sessionId);
async logout(sessionId: string): Promise<void> {
await this._proxy.$logout(this.id, sessionId);
this.notificationService.info(nls.localize('signedOut', "Successfully signed out."));
}
dispose(): void {
@@ -255,16 +284,16 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
extHostContext: IExtHostContext,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IDialogService private readonly dialogService: IDialogService,
@IStorageService private readonly storageService: IStorageService
@IStorageService private readonly storageService: IStorageService,
@INotificationService private readonly notificationService: INotificationService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
}
async $registerAuthenticationProvider(id: string, displayName: string): Promise<void> {
const dependentBuiltIns = BUILT_IN_AUTH_DEPENDENTS.filter(dependency => dependency.providerId === id);
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, dependentBuiltIns);
const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService);
await provider.initialize();
this.authenticationService.registerAuthenticationProvider(id, provider);
}
@@ -277,6 +306,8 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
}
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
addAccountUsage(providerId, accountName, extensionName);
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
const extensionData = allowList.find(extension => extension.id === extensionId);
if (extensionData) {
@@ -313,4 +344,12 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
return choice === 1;
}
async $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: 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);
}
}
}

View File

@@ -378,7 +378,7 @@ export class MainThreadComments extends Disposable implements MainThreadComments
this._commentService.registerCommentController(providerId, provider);
this._commentControllers.set(handle, provider);
const commentsPanelAlreadyConstructed = !!this._viewDescriptorService.getViewDescriptor(COMMENTS_VIEW_ID);
const commentsPanelAlreadyConstructed = !!this._viewDescriptorService.getViewDescriptorById(COMMENTS_VIEW_ID);
if (!commentsPanelAlreadyConstructed) {
this.registerView(commentsPanelAlreadyConstructed);
this.registerViewOpenedListener(commentsPanelAlreadyConstructed);
@@ -451,7 +451,8 @@ export class MainThreadComments extends Disposable implements MainThreadComments
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, COMMENTS_VIEW_TITLE, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [COMMENTS_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
storageId: COMMENTS_VIEW_TITLE,
hideIfEmpty: true,
order: 10,
}, ViewContainerLocation.Panel);

View File

@@ -15,6 +15,7 @@ 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 { DebugConfigurationProviderScope } from 'vs/workbench/api/common/extHostTypes';
@extHostNamedCustomer(MainContext.MainThreadDebugService)
export class MainThreadDebugService implements MainThreadDebugServiceShape, IDebugAdapterFactory {
@@ -154,10 +155,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
return Promise.resolve();
}
public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise<void> {
public $registerDebugConfigurationProvider(debugType: string, providerScope: DebugConfigurationProviderScope, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise<void> {
const provider = <IDebugConfigurationProvider>{
type: debugType
type: debugType,
scope: providerScope
};
if (hasProvide) {
provider.provideDebugConfigurations = (folder, token) => {
@@ -271,7 +273,6 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
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}`));
}

View File

@@ -24,6 +24,7 @@ import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContex
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 { DEFAULT_EDITOR_ID } from 'vs/workbench/contrib/files/common/files';
export class MainThreadTextEditors implements MainThreadTextEditorsShape {
@@ -293,6 +294,30 @@ CommandsRegistry.registerCommand('_workbench.open', function (accessor: Services
return openerService.open(resource).then(_ => undefined);
});
CommandsRegistry.registerCommand('_workbench.openWith', (accessor: ServicesAccessor, args: [URI, string, ITextEditorOptions | undefined, EditorViewColumn | undefined]) => {
const editorService = accessor.get(IEditorService);
const editorGroupService = accessor.get(IEditorGroupsService);
const [resource, id, options, position] = args;
const group = editorGroupService.getGroup(viewColumnToEditorGroup(editorGroupService, position)) ?? editorGroupService.activeGroup;
const textOptions = options ? { ...options, ignoreOverrides: true } : { ignoreOverrides: true };
const fileEditorInput = editorService.createEditorInput({ resource, forceFile: true });
if (id === DEFAULT_EDITOR_ID) {
return editorService.openEditor(fileEditorInput, textOptions, position);
}
const editors = editorService.getEditorOverrides(fileEditorInput, undefined, group);
for (const [handler, data] of editors) {
if (data.id === id) {
return handler.open(fileEditorInput, options, group, id);
}
}
return undefined;
});
CommandsRegistry.registerCommand('_workbench.diff', function (accessor: ServicesAccessor, args: [URI, URI, string, string, IEditorOptions, EditorViewColumn]) {
const editorService = accessor.get(IEditorService);

View File

@@ -405,8 +405,8 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
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],
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],
@@ -414,7 +414,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
filterText: data[ISuggestDataDtoField.filterText],
preselect: data[ISuggestDataDtoField.preselect],
insertText: typeof data.h === 'undefined' ? data[ISuggestDataDtoField.label] : data.h,
range: data[ISuggestDataDtoField.range] || defaultRange,
range: data[ISuggestDataDtoField.range] ?? defaultRange,
insertTextRules: data[ISuggestDataDtoField.insertTextRules],
commitCharacters: data[ISuggestDataDtoField.commitCharacters],
additionalTextEdits: data[ISuggestDataDtoField.additionalTextEdits],

View File

@@ -33,6 +33,10 @@ export class MainThreadNotebookDocument extends Disposable {
this._register(this._textModel.onDidModelChange(e => {
this._proxy.$acceptModelChanged(this.uri, e);
}));
this._register(this._textModel.onDidSelectionChange(e => {
const selectionsChange = e ? { selections: e } : null;
this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange });
}));
}
applyEdit(modelVersionId: number, edits: ICellEditOperation[]): boolean {

View File

@@ -96,7 +96,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape {
// ---- input
$input(options: IInputBoxOptions | undefined, validateInput: boolean, token: CancellationToken): Promise<string> {
$input(options: IInputBoxOptions | undefined, validateInput: boolean, token: CancellationToken): Promise<string | undefined> {
const inputOptions: IInputOptions = Object.create(null);
if (options) {

View File

@@ -71,8 +71,8 @@ class MainThreadSCMResource implements ISCMResource {
public decorations: ISCMResourceDecorations
) { }
open(): Promise<void> {
return this.proxy.$executeResourceCommand(this.sourceControlHandle, this.groupHandle, this.handle);
open(preserveFocus: boolean): Promise<void> {
return this.proxy.$executeResourceCommand(this.sourceControlHandle, this.groupHandle, this.handle, preserveFocus);
}
toJSON(): any {

View File

@@ -26,7 +26,9 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape {
}
$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 {
const entry: IStatusbarEntry = { text, tooltip, command, color };
// if there are icons in the text use the tooltip for the aria label
const ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text;
const entry: IStatusbarEntry = { text, tooltip, command, color, ariaLabel };
if (typeof priority === 'undefined') {
priority = 0;

View File

@@ -133,6 +133,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IBackupFileService private readonly _backupService: IBackupFileService,
) {
super();
@@ -151,7 +152,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
this.updateWebviewViewStates(this._editorService.activeEditor);
}));
// This reviver's only job is to activate webview panel extensions
// 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) => {
@@ -305,11 +306,11 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
}
public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void {
this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities);
this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true);
}
public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions): void {
this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {});
public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerResource: boolean): void {
this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {}, supportsMultipleEditorsPerResource);
}
private registerEditorProvider(
@@ -318,11 +319,16 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
viewType: string,
options: modes.IWebviewPanelOptions,
capabilities: extHostProtocol.CustomTextEditorCapabilities,
supportsMultipleEditorsPerResource: boolean,
): DisposableStore {
if (this._editorProviders.has(viewType)) {
throw new Error(`Provider for ${viewType} already registered`);
}
this._customEditorService.registerCustomEditorCapabilities(viewType, {
supportsMultipleEditorsPerResource
});
const extension = reviveWebviewExtension(extensionData);
const disposables = new DisposableStore();
@@ -341,7 +347,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
let modelRef: IReference<ICustomEditorModel>;
try {
modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, cancellation);
modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation);
} catch (error) {
onUnexpectedError(error);
webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType);
@@ -360,7 +366,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
if (capabilities.supportsMove) {
webviewInput.onMove(async (newResource: URI) => {
const oldModel = modelRef;
modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, CancellationToken.None);
modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None);
this._proxy.$onMoveCustomEditor(handle, newResource, viewType);
oldModel.dispose();
});
@@ -398,6 +404,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
modelType: ModelType,
resource: URI,
viewType: string,
options: { backupId?: string },
cancellation: CancellationToken,
): Promise<IReference<ICustomEditorModel>> {
const existingModel = this._customEditorService.models.tryRetain(resource, viewType);
@@ -405,26 +412,33 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
return existingModel;
}
const model = modelType === ModelType.Text
? CustomTextEditorModel.create(this._instantiationService, viewType, resource)
: MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, () => {
return Array.from(this._webviewInputs)
.filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[];
}, cancellation);
return this._customEditorService.models.add(resource, viewType, model);
switch (modelType) {
case ModelType.Text:
{
const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource);
return this._customEditorService.models.add(resource, viewType, model);
}
case ModelType.Custom:
{
const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, options, () => {
return Array.from(this._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 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');
}
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 hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) {
const disposables = new DisposableStore();
@@ -531,6 +545,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
return this._webviewInputs.getInputForHandle(handle);
}
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;
}
private static getWebviewResolvedFailedContent(viewType: string) {
return `<!DOCTYPE html>
<html>
@@ -577,7 +600,7 @@ namespace HotExitState {
readonly type = Type.Pending;
constructor(
public readonly operation: CancelablePromise<void>,
public readonly operation: CancelablePromise<string>,
) { }
}
@@ -588,58 +611,58 @@ namespace HotExitState {
class MainThreadCustomEditorModel extends Disposable implements ICustomEditorModel, IWorkingCopy {
private _hotExitState: HotExitState.State = HotExitState.Allowed;
private readonly _fromBackup: boolean = false;
private _currentEditIndex: number = -1;
private _savePoint: number = -1;
private readonly _edits: Array<number> = [];
private _fromBackup: boolean = false;
private _isDirtyFromContentChange = false;
private _ongoingSave?: CancelablePromise<void>;
public static async create(
instantiationService: IInstantiationService,
proxy: extHostProtocol.ExtHostWebviewsShape,
viewType: string,
resource: URI,
options: { backupId?: string },
getEditors: () => CustomEditorInput[],
cancellation: CancellationToken,
_backupFileService: IBackupFileService,
) {
const { editable } = await proxy.$createWebviewCustomEditorDocument(resource, viewType, cancellation);
const model = instantiationService.createInstance(MainThreadCustomEditorModel, proxy, viewType, resource, editable, getEditors);
await model.init();
return model;
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.ExtHostWebviewsShape,
private readonly _viewType: string,
private readonly _editorResource: URI,
fromBackup: boolean,
private readonly _editable: boolean,
private readonly _getEditors: () => CustomEditorInput[],
@IWorkingCopyService workingCopyService: IWorkingCopyService,
@ILabelService private readonly _labelService: ILabelService,
@IFileService private readonly _fileService: IFileService,
@IUndoRedoService private readonly _undoService: IUndoRedoService,
@IBackupFileService private readonly _backupFileService: IBackupFileService,
) {
super();
if (_editable) {
this._register(workingCopyService.registerWorkingCopy(this));
}
this._fromBackup = fromBackup;
}
get editorResource() {
return this._editorResource;
}
async init(): Promise<void> {
const backup = await this._backupFileService.resolve<CustomDocumentBackupData>(this.resource);
this._fromBackup = !!backup;
}
dispose() {
if (this._editable) {
this._undoService.removeElements(this._editorResource);
}
this._proxy.$disposeWebviewCustomEditorDocument(this._editorResource, this._viewType);
this._proxy.$disposeCustomDocument(this._editorResource, this._viewType);
super.dispose();
}
@@ -647,11 +670,15 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
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) {
return URI.from({
scheme: Schemas.vscodeCustomEditor,
authority: this._viewType,
path: this._editorResource.path,
query: JSON.stringify(this._editorResource.toJSON()),
authority: viewType,
path: resource.path,
query: JSON.stringify(resource.toJSON()),
});
}
@@ -664,6 +691,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
}
public isDirty(): boolean {
if (this._isDirtyFromContentChange) {
return true;
}
if (this._edits.length > 0) {
return this._savePoint !== this._currentEditIndex;
}
@@ -705,6 +735,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
});
}
public changeContent() {
this.change(() => {
this._isDirtyFromContentChange = true;
});
}
private async undo(): Promise<void> {
if (!this._editable) {
return;
@@ -719,15 +755,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
this.change(() => {
--this._currentEditIndex;
});
await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.getEditState());
}
private getEditState(): extHostProtocol.CustomDocumentEditState {
return {
allEdits: this._edits,
currentIndex: this._currentEditIndex,
saveIndex: this._savePoint,
};
await this._proxy.$undo(this._editorResource, this.viewType, undoneEdit, this.isDirty());
}
private async redo(): Promise<void> {
@@ -744,7 +772,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
this.change(() => {
++this._currentEditIndex;
});
await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.getEditState());
await this._proxy.$redo(this._editorResource, this.viewType, redoneEdit, this.isDirty());
}
private spliceEdits(editToInsert?: number) {
@@ -775,21 +803,13 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
return;
}
if (this._currentEditIndex === this._savePoint) {
if (this._currentEditIndex === this._savePoint && !this._isDirtyFromContentChange) {
return;
}
let editsToUndo: number[] = [];
let editsToRedo: number[] = [];
if (this._currentEditIndex >= this._savePoint) {
editsToUndo = this._edits.slice(this._savePoint, this._currentEditIndex).reverse();
} else if (this._currentEditIndex < this._savePoint) {
editsToRedo = this._edits.slice(this._currentEditIndex, this._savePoint);
}
this._proxy.$revert(this._editorResource, this.viewType, { undoneEdits: editsToUndo, redoneEdits: editsToRedo }, this.getEditState());
this._proxy.$revert(this._editorResource, this.viewType, CancellationToken.None);
this.change(() => {
this._isDirtyFromContentChange = false;
this._currentEditIndex = this._savePoint;
this.spliceEdits();
});
@@ -804,11 +824,23 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
return undefined;
}
// TODO: handle save untitled case
// TODO: handle cancellation
await createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
this._ongoingSave?.cancel();
this._ongoingSave = savePromise;
this.change(() => {
this._isDirtyFromContentChange = false;
this._savePoint = this._currentEditIndex;
});
try {
await savePromise;
} finally {
if (this._ongoingSave === savePromise) {
this._ongoingSave = undefined;
}
}
return this._editorResource;
}
@@ -838,6 +870,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
meta: {
viewType: this.viewType,
editorResource: this._editorResource,
backupId: '',
extension: primaryEditor.extension ? {
id: primaryEditor.extension.id.value,
location: primaryEditor.extension.location,
@@ -864,10 +897,11 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
this._hotExitState = pendingState;
try {
await pendingState.operation;
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;
}
} catch (e) {
// Make sure state has not changed in the meantime

View File

@@ -320,7 +320,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
name: title, extensionId,
ctorDescriptor: new SyncDescriptor(
ViewPaneContainer,
[id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true }]
[id, { mergeViewWithContainerWhenSingleView: true }]
),
hideIfEmpty: true,
order,