UNPKG

@sussudio/platform

Version:

Internal APIs for VS Code's service injection the base services.

221 lines (220 loc) 8.39 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); }; }; import { BrowserWindow, dialog } from 'electron'; import { Queue } from '@sussudio/base/common/async.mjs'; import { hash } from '@sussudio/base/common/hash.mjs'; import { mnemonicButtonLabel } from '@sussudio/base/common/labels.mjs'; import { Disposable, dispose, toDisposable } from '@sussudio/base/common/lifecycle.mjs'; import { normalizeNFC } from '@sussudio/base/common/normalization.mjs'; import { isMacintosh } from '@sussudio/base/common/platform.mjs'; import { withNullAsUndefined } from '@sussudio/base/common/types.mjs'; import { Promises } from '@sussudio/base/node/pfs.mjs'; import { localize } from 'vscode-nls.mjs'; import { createDecorator } from '../../instantiation/common/instantiation.mjs'; import { ILogService } from '../../log/common/log.mjs'; import { WORKSPACE_FILTER } from '../../workspace/common/workspace.mjs'; export const IDialogMainService = createDecorator('dialogMainService'); let DialogMainService = class DialogMainService { logService; windowFileDialogLocks = new Map(); windowDialogQueues = new Map(); noWindowDialogueQueue = new Queue(); constructor(logService) { this.logService = logService; } pickFileFolder(options, window) { return this.doPick({ ...options, pickFolders: true, pickFiles: true, title: localize('open', 'Open') }, window); } pickFolder(options, window) { return this.doPick({ ...options, pickFolders: true, title: localize('openFolder', 'Open Folder') }, window); } pickFile(options, window) { return this.doPick({ ...options, pickFiles: true, title: localize('openFile', 'Open File') }, window); } pickWorkspace(options, window) { const title = localize('openWorkspaceTitle', 'Open Workspace from File'); const buttonLabel = mnemonicButtonLabel( localize({ key: 'openWorkspace', comment: ['&& denotes a mnemonic'] }, '&&Open'), ); const filters = WORKSPACE_FILTER; return this.doPick({ ...options, pickFiles: true, title, filters, buttonLabel }, window); } async doPick(options, window) { // Ensure dialog options const dialogOptions = { title: options.title, buttonLabel: options.buttonLabel, filters: options.filters, defaultPath: options.defaultPath, }; // Ensure properties if (typeof options.pickFiles === 'boolean' || typeof options.pickFolders === 'boolean') { dialogOptions.properties = undefined; // let it override based on the booleans if (options.pickFiles && options.pickFolders) { dialogOptions.properties = ['multiSelections', 'openDirectory', 'openFile', 'createDirectory']; } } if (!dialogOptions.properties) { dialogOptions.properties = [ 'multiSelections', options.pickFolders ? 'openDirectory' : 'openFile', 'createDirectory', ]; } if (isMacintosh) { dialogOptions.properties.push('treatPackageAsDirectory'); // always drill into .app files } // Show Dialog const result = await this.showOpenDialog( dialogOptions, withNullAsUndefined(window || BrowserWindow.getFocusedWindow()), ); if (result && result.filePaths && result.filePaths.length > 0) { return result.filePaths; } return undefined; } getWindowDialogQueue(window) { // 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; } else { return this.noWindowDialogueQueue; } } showMessageBox(options, window) { return this.getWindowDialogQueue(window).queue(async () => { if (window) { return dialog.showMessageBox(window, options); } return dialog.showMessageBox(options); }); } async showSaveDialog(options, window) { // Prevent duplicates of the same dialog queueing at the same time const fileDialogLock = this.acquireFileDialogLock(options, window); if (!fileDialogLock) { this.logService.error( '[DialogMainService]: file save dialog is already or will be showing for the window with the same configuration', ); return { canceled: true }; } try { return await this.getWindowDialogQueue(window).queue(async () => { let result; if (window) { result = await dialog.showSaveDialog(window, options); } else { result = await dialog.showSaveDialog(options); } result.filePath = this.normalizePath(result.filePath); return result; }); } finally { dispose(fileDialogLock); } } normalizePath(path) { if (path && isMacintosh) { path = normalizeNFC(path); // macOS only: normalize paths to NFC form } return path; } normalizePaths(paths) { return paths.map((path) => this.normalizePath(path)); } async showOpenDialog(options, window) { // Ensure the path exists (if provided) if (options.defaultPath) { const pathExists = await Promises.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) { this.logService.error( '[DialogMainService]: file open dialog is already or will be showing for the window with the same configuration', ); return { canceled: true, filePaths: [] }; } try { return await this.getWindowDialogQueue(window).queue(async () => { let result; if (window) { result = await dialog.showOpenDialog(window, options); } else { result = await dialog.showOpenDialog(options); } result.filePaths = this.normalizePaths(result.filePaths); return result; }); } finally { dispose(fileDialogLock); } } acquireFileDialogLock(options, window) { // 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) // we figure this out by `hashing` the configuration // options for the dialog to prevent duplicates this.logService.trace('[DialogMainService]: request to acquire file dialog lock', options); let windowFileDialogLocks = this.windowFileDialogLocks.get(window.id); if (!windowFileDialogLocks) { windowFileDialogLocks = new Set(); this.windowFileDialogLocks.set(window.id, windowFileDialogLocks); } const optionsHash = hash(options); if (windowFileDialogLocks.has(optionsHash)) { return undefined; // prevent duplicates, return } this.logService.trace('[DialogMainService]: new file dialog lock created', options); windowFileDialogLocks.add(optionsHash); return toDisposable(() => { this.logService.trace('[DialogMainService]: file dialog lock disposed', options); windowFileDialogLocks?.delete(optionsHash); // If the window has no more dialog locks, delete it from the set of locks if (windowFileDialogLocks?.size === 0) { this.windowFileDialogLocks.delete(window.id); } }); } }; DialogMainService = __decorate([__param(0, ILogService)], DialogMainService); export { DialogMainService };