@sussudio/platform
Version:
Internal APIs for VS Code's service injection the base services.
221 lines (220 loc) • 8.39 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 };