diff --git a/src/vs/platform/dialogs/electron-main/dialogMainService.ts b/src/vs/platform/dialogs/electron-main/dialogMainService.ts index 809e255872419..321d52b2152a5 100644 --- a/src/vs/platform/dialogs/electron-main/dialogMainService.ts +++ b/src/vs/platform/dialogs/electron-main/dialogMainService.ts @@ -16,6 +16,8 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; +import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { hash } from 'vs/base/common/hash'; export const IDialogMainService = createDecorator('dialogMainService'); @@ -48,14 +50,13 @@ export class DialogMainService implements IDialogMainService { private static readonly workingDirPickerStorageKey = 'pickerWorkingDir'; - private readonly mapWindowToDialogQueue: Map>; - private readonly noWindowDialogQueue: Queue; + private readonly windowDialogLocks = new Map>(); + private readonly windowDialogQueues = new Map>(); + private readonly noWindowDialogueQueue = new Queue(); constructor( @IStateService private readonly stateService: IStateService ) { - this.mapWindowToDialogQueue = new Map>(); - this.noWindowDialogQueue = new Queue(); } pickFileFolder(options: INativeOpenDialogOptions, window?: BrowserWindow): Promise { @@ -123,22 +124,25 @@ export class DialogMainService implements IDialogMainService { return; } - private getDialogQueue(window?: BrowserWindow): Queue { - if (!window) { - return this.noWindowDialogQueue; - } + private getWindowDialogQueue(window?: BrowserWindow): Queue { - let windowDialogQueue = this.mapWindowToDialogQueue.get(window.id); - if (!windowDialogQueue) { - windowDialogQueue = new Queue(); - this.mapWindowToDialogQueue.set(window.id, windowDialogQueue); - } + // Queue message box requests per window so that one can show + // after the other. + if (window) { + let windowDialogQueue = this.windowDialogQueues.get(window.id); + if (!windowDialogQueue) { + windowDialogQueue = new Queue(); + this.windowDialogQueues.set(window.id, windowDialogQueue); + } - return windowDialogQueue; + return windowDialogQueue; + } else { + return this.noWindowDialogueQueue; + } } showMessageBox(options: MessageBoxOptions, window?: BrowserWindow): Promise { - return this.getDialogQueue(window).queue(async () => { + return this.getWindowDialogQueue(window).queue(async () => { if (window) { return dialog.showMessageBox(window, options); } @@ -147,7 +151,7 @@ export class DialogMainService implements IDialogMainService { }); } - showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { + async showSaveDialog(options: SaveDialogOptions, window?: BrowserWindow): Promise { function normalizePath(path: string | undefined): string | undefined { if (path && isMacintosh) { @@ -157,21 +161,31 @@ export class DialogMainService implements IDialogMainService { return path; } - return this.getDialogQueue(window).queue(async () => { - let result: SaveDialogReturnValue; - if (window) { - result = await dialog.showSaveDialog(window, options); - } else { - result = await dialog.showSaveDialog(options); - } + // prevent duplicates of the same dialog queueing at the same time + const fileDialogLock = this.acquireFileDialogLock(options, window); + if (!fileDialogLock) { + throw new Error('A file save dialog is already showing for the window with the same configuration'); + } - result.filePath = normalizePath(result.filePath); + try { + return await this.getWindowDialogQueue(window).queue(async () => { + let result: SaveDialogReturnValue; + if (window) { + result = await dialog.showSaveDialog(window, options); + } else { + result = await dialog.showSaveDialog(options); + } - return result; - }); + result.filePath = normalizePath(result.filePath); + + return result; + }); + } finally { + dispose(fileDialogLock); + } } - showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { + async showOpenDialog(options: OpenDialogOptions, window?: BrowserWindow): Promise { function normalizePaths(paths: string[]): string[] { if (paths && paths.length > 0 && isMacintosh) { @@ -181,27 +195,74 @@ export class DialogMainService implements IDialogMainService { return paths; } - return this.getDialogQueue(window).queue(async () => { + // Ensure the path exists (if provided) + if (options.defaultPath) { + const pathExists = await exists(options.defaultPath); + if (!pathExists) { + options.defaultPath = undefined; + } + } + + // prevent duplicates of the same dialog queueing at the same time + const fileDialogLock = this.acquireFileDialogLock(options, window); + if (!fileDialogLock) { + throw new Error('A file open dialog is already showing for the window with the same configuration'); + } - // Ensure the path exists (if provided) - if (options.defaultPath) { - const pathExists = await exists(options.defaultPath); - if (!pathExists) { - options.defaultPath = undefined; + try { + return await this.getWindowDialogQueue(window).queue(async () => { + let result: OpenDialogReturnValue; + if (window) { + result = await dialog.showOpenDialog(window, options); + } else { + result = await dialog.showOpenDialog(options); } - } - // Show dialog - let result: OpenDialogReturnValue; - if (window) { - result = await dialog.showOpenDialog(window, options); - } else { - result = await dialog.showOpenDialog(options); - } + result.filePaths = normalizePaths(result.filePaths); + + return result; + }); + } finally { + dispose(fileDialogLock); + } + } + + private acquireFileDialogLock(options: SaveDialogOptions | OpenDialogOptions, window?: BrowserWindow): IDisposable | undefined { + + // if no window is provided, allow as many dialogs as + // needed since we consider them not modal per window + if (!window) { + return Disposable.None; + } + + // if a window is provided, only allow a single dialog + // at the same time because dialogs are modal and we + // do not want to open one dialog after the other + // (https://github.com/microsoft/vscode/issues/114432) + let windowDialogLocks = this.windowDialogLocks.get(window.id); + + // figure out if a dialog with these options is already + // showing by hashing the options + const optionsHash = hash(options); + if (windowDialogLocks?.has(optionsHash)) { + return undefined; + } - result.filePaths = normalizePaths(result.filePaths); + if (!windowDialogLocks) { + windowDialogLocks = new Set(); + this.windowDialogLocks.set(window.id, windowDialogLocks); + } + + windowDialogLocks.add(optionsHash); - return result; + return toDisposable(() => { + const windowDialogLocks = this.windowDialogLocks.get(window.id); + windowDialogLocks?.delete(optionsHash); + + // if the window has no more dialog locks, delete it from the set of locks + if (windowDialogLocks?.size === 0) { + this.windowDialogLocks.delete(window.id); + } }); } }