Path: blob/main/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as nls from '../../../../nls.js';6import { IWindowOpenable, isWorkspaceToOpen, isFileToOpen } from '../../../../platform/window/common/window.js';7import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from '../../../../platform/dialogs/common/dialogs.js';8import { isSavedWorkspace, isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from '../../../../platform/workspace/common/workspace.js';9import { IHistoryService } from '../../history/common/history.js';10import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';11import { URI } from '../../../../base/common/uri.js';12import * as resources from '../../../../base/common/resources.js';13import { isAbsolute as localPathIsAbsolute, normalize as localPathNormalize } from '../../../../base/common/path.js';14import { IInstantiationService, } from '../../../../platform/instantiation/common/instantiation.js';15import { ISimpleFileDialog, SimpleFileDialog } from './simpleFileDialog.js';16import { IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';17import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';18import { IFileService } from '../../../../platform/files/common/files.js';19import { IOpenerService } from '../../../../platform/opener/common/opener.js';20import { IHostService } from '../../host/browser/host.js';21import Severity from '../../../../base/common/severity.js';22import { coalesce, distinct } from '../../../../base/common/arrays.js';23import { trim } from '../../../../base/common/strings.js';24import { ILanguageService } from '../../../../editor/common/languages/language.js';25import { ILabelService } from '../../../../platform/label/common/label.js';26import { IPathService } from '../../path/common/pathService.js';27import { Schemas } from '../../../../base/common/network.js';28import { PLAINTEXT_EXTENSION } from '../../../../editor/common/languages/modesRegistry.js';29import { ICommandService } from '../../../../platform/commands/common/commands.js';30import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';31import { IEditorService } from '../../editor/common/editorService.js';32import { EditorOpenSource } from '../../../../platform/editor/common/editor.js';33import { ILogService } from '../../../../platform/log/common/log.js';3435export abstract class AbstractFileDialogService implements IFileDialogService {3637declare readonly _serviceBrand: undefined;3839constructor(40@IHostService protected readonly hostService: IHostService,41@IWorkspaceContextService protected readonly contextService: IWorkspaceContextService,42@IHistoryService protected readonly historyService: IHistoryService,43@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,44@IInstantiationService protected readonly instantiationService: IInstantiationService,45@IConfigurationService protected readonly configurationService: IConfigurationService,46@IFileService protected readonly fileService: IFileService,47@IOpenerService protected readonly openerService: IOpenerService,48@IDialogService protected readonly dialogService: IDialogService,49@ILanguageService private readonly languageService: ILanguageService,50@IWorkspacesService private readonly workspacesService: IWorkspacesService,51@ILabelService private readonly labelService: ILabelService,52@IPathService private readonly pathService: IPathService,53@ICommandService protected readonly commandService: ICommandService,54@IEditorService protected readonly editorService: IEditorService,55@ICodeEditorService protected readonly codeEditorService: ICodeEditorService,56@ILogService private readonly logService: ILogService57) { }5859async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow(), authorityFilter = this.getAuthorityFilterForWindow()): Promise<URI> {6061// Check for last active file first...62let candidate = this.historyService.getLastActiveFile(schemeFilter, authorityFilter);6364// ...then for last active file root65if (!candidate) {66candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter, authorityFilter);67} else {68candidate = resources.dirname(candidate);69}7071if (!candidate) {72candidate = await this.preferredHome(schemeFilter);73}7475return candidate;76}7778async defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow(), authorityFilter = this.getAuthorityFilterForWindow()): Promise<URI> {7980// Check for last active file root first...81let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter, authorityFilter);8283// ...then for last active file84if (!candidate) {85candidate = this.historyService.getLastActiveFile(schemeFilter, authorityFilter);86}8788if (!candidate) {89return this.preferredHome(schemeFilter);90}9192return resources.dirname(candidate);93}9495async preferredHome(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {96const preferLocal = schemeFilter === Schemas.file;97const preferredHomeConfig = this.configurationService.inspect<string>('files.dialog.defaultPath');98const preferredHomeCandidate = preferLocal ? preferredHomeConfig.userLocalValue : preferredHomeConfig.userRemoteValue;99if (preferredHomeCandidate) {100const isPreferredHomeCandidateAbsolute = preferLocal ? localPathIsAbsolute(preferredHomeCandidate) : (await this.pathService.path).isAbsolute(preferredHomeCandidate);101if (isPreferredHomeCandidateAbsolute) {102const preferredHomeNormalized = preferLocal ? localPathNormalize(preferredHomeCandidate) : (await this.pathService.path).normalize(preferredHomeCandidate);103const preferredHome = resources.toLocalResource(await this.pathService.fileURI(preferredHomeNormalized), this.environmentService.remoteAuthority, this.pathService.defaultUriScheme);104if (await this.fileService.exists(preferredHome)) {105return preferredHome;106}107}108}109110return this.pathService.userHome({ preferLocal });111}112113async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {114let defaultWorkspacePath: URI | undefined;115116// Check for current workspace config file first...117if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {118const configuration = this.contextService.getWorkspace().configuration;119if (configuration?.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService) && !isTemporaryWorkspace(configuration)) {120defaultWorkspacePath = resources.dirname(configuration);121}122}123124// ...then fallback to default file path125if (!defaultWorkspacePath) {126defaultWorkspacePath = await this.defaultFilePath(schemeFilter);127}128129return defaultWorkspacePath;130}131132async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {133if (this.skipDialogs()) {134this.logService.trace('FileDialogService: refused to show save confirmation dialog in tests.');135136// no veto when we are in extension dev testing mode because we cannot assume we run interactive137return ConfirmResult.DONT_SAVE;138}139140return this.doShowSaveConfirm(fileNamesOrResources);141}142143private skipDialogs(): boolean {144if (this.environmentService.enableSmokeTestDriver) {145this.logService.warn('DialogService: Dialog requested during smoke test.');146}147// integration tests148return this.environmentService.isExtensionDevelopment && !!this.environmentService.extensionTestsLocationURI;149}150151private async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {152if (fileNamesOrResources.length === 0) {153return ConfirmResult.DONT_SAVE;154}155156let message: string;157let detail = nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.");158if (fileNamesOrResources.length === 1) {159message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNamesOrResources[0] === 'string' ? fileNamesOrResources[0] : resources.basename(fileNamesOrResources[0]));160} else {161message = nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length);162detail = getFileNamesMessage(fileNamesOrResources) + '\n' + detail;163}164165const { result } = await this.dialogService.prompt<ConfirmResult>({166type: Severity.Warning,167message,168detail,169buttons: [170{171label: fileNamesOrResources.length > 1 ?172nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") :173nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),174run: () => ConfirmResult.SAVE175},176{177label: nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),178run: () => ConfirmResult.DONT_SAVE179}180],181cancelButton: {182run: () => ConfirmResult.CANCEL183}184});185186return result;187}188189protected addFileSchemaIfNeeded(schema: string, _isFolder?: boolean): string[] {190return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);191}192193protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<void> {194const title = nls.localize('openFileOrFolder.title', 'Open File or Folder');195const availableFileSystems = this.addFileSchemaIfNeeded(schema);196197const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });198199if (uri) {200const stat = await this.fileService.stat(uri);201202const toOpen: IWindowOpenable = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };203if (!isWorkspaceToOpen(toOpen) && isFileToOpen(toOpen)) {204this.addFileToRecentlyOpened(toOpen.fileUri);205}206207if (stat.isDirectory || options.forceNewWindow || preferNewWindow) {208await this.hostService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });209} else {210await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true });211}212}213}214215protected async pickFileAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<void> {216const title = nls.localize('openFile.title', 'Open File');217const availableFileSystems = this.addFileSchemaIfNeeded(schema);218219const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });220if (uri) {221this.addFileToRecentlyOpened(uri);222223if (options.forceNewWindow || preferNewWindow) {224await this.hostService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });225} else {226await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true });227}228}229}230231protected addFileToRecentlyOpened(uri: URI): void {232this.workspacesService.addRecentlyOpened([{ fileUri: uri, label: this.labelService.getUriLabel(uri, { appendWorkspaceSuffix: true }) }]);233}234235protected async pickFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions): Promise<void> {236const title = nls.localize('openFolder.title', 'Open Folder');237const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);238239const uri = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });240if (uri) {241return this.hostService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });242}243}244245protected async pickWorkspaceAndOpenSimplified(schema: string, options: IPickAndOpenOptions): Promise<void> {246const title = nls.localize('openWorkspace.title', 'Open Workspace from File');247const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];248const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);249250const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });251if (uri) {252return this.hostService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });253}254}255256protected async pickFileToSaveSimplified(schema: string, options: ISaveDialogOptions): Promise<URI | undefined> {257if (!options.availableFileSystems) {258options.availableFileSystems = this.addFileSchemaIfNeeded(schema);259}260261options.title = nls.localize('saveFileAs.title', 'Save As');262const uri = await this.saveRemoteResource(options);263264if (uri) {265this.addFileToRecentlyOpened(uri);266}267268return uri;269}270271protected async showSaveDialogSimplified(schema: string, options: ISaveDialogOptions): Promise<URI | undefined> {272if (!options.availableFileSystems) {273options.availableFileSystems = this.addFileSchemaIfNeeded(schema);274}275276return this.saveRemoteResource(options);277}278279protected async showOpenDialogSimplified(schema: string, options: IOpenDialogOptions): Promise<URI[] | undefined> {280if (!options.availableFileSystems) {281options.availableFileSystems = this.addFileSchemaIfNeeded(schema, options.canSelectFolders);282}283284const uri = await this.pickResource(options);285286return uri ? [uri] : undefined;287}288289protected getSimpleFileDialog(): ISimpleFileDialog {290return this.instantiationService.createInstance(SimpleFileDialog);291}292293private pickResource(options: IOpenDialogOptions): Promise<URI | undefined> {294return this.getSimpleFileDialog().showOpenDialog(options);295}296297private saveRemoteResource(options: ISaveDialogOptions): Promise<URI | undefined> {298return this.getSimpleFileDialog().showSaveDialog(options);299}300301private getSchemeFilterForWindow(defaultUriScheme?: string): string {302return defaultUriScheme ?? this.pathService.defaultUriScheme;303}304305private getAuthorityFilterForWindow(): string | undefined {306return this.environmentService.remoteAuthority;307}308309protected getFileSystemSchema(options: { availableFileSystems?: readonly string[]; defaultUri?: URI }): string {310return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow(options.defaultUri?.scheme);311}312313abstract pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;314abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;315abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;316abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;317protected getWorkspaceAvailableFileSystems(options: IPickAndOpenOptions): string[] {318if (options.availableFileSystems && (options.availableFileSystems.length > 0)) {319return options.availableFileSystems;320}321const availableFileSystems = [Schemas.file];322if (this.environmentService.remoteAuthority) {323availableFileSystems.unshift(Schemas.vscodeRemote);324}325return availableFileSystems;326}327abstract showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;328abstract showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;329330abstract pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;331332protected getPickFileToSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {333const options: ISaveDialogOptions = {334defaultUri,335title: nls.localize('saveAsTitle', "Save As"),336availableFileSystems337};338339interface IFilter { name: string; extensions: string[] }340341// Build the file filter by using our known languages342const ext: string | undefined = defaultUri ? resources.extname(defaultUri) : undefined;343let matchingFilter: IFilter | undefined;344345const registeredLanguageNames = this.languageService.getSortedRegisteredLanguageNames();346const registeredLanguageFilters: IFilter[] = coalesce(registeredLanguageNames.map(({ languageName, languageId }) => {347const extensions = this.languageService.getExtensions(languageId);348if (!extensions.length) {349return null;350}351352const filter: IFilter = { name: languageName, extensions: distinct(extensions).slice(0, 10).map(e => trim(e, '.')) };353354// https://github.com/microsoft/vscode/issues/115860355const extOrPlaintext = ext || PLAINTEXT_EXTENSION;356if (!matchingFilter && extensions.includes(extOrPlaintext)) {357matchingFilter = filter;358359// The selected extension must be in the set of extensions that are in the filter list that is sent to the save dialog.360// If it isn't, add it manually. https://github.com/microsoft/vscode/issues/147657361const trimmedExt = trim(extOrPlaintext, '.');362if (!filter.extensions.includes(trimmedExt)) {363filter.extensions.unshift(trimmedExt);364}365366return null; // first matching filter will be added to the top367}368369return filter;370}));371372// We have no matching filter, e.g. because the language373// is unknown. We still add the extension to the list of374// filters though so that it can be picked375// (https://github.com/microsoft/vscode/issues/96283)376if (!matchingFilter && ext) {377matchingFilter = { name: trim(ext, '.').toUpperCase(), extensions: [trim(ext, '.')] };378}379380// Order of filters is381// - All Files (we MUST do this to fix macOS issue https://github.com/microsoft/vscode/issues/102713)382// - File Extension Match (if any)383// - All Languages384// - No Extension385options.filters = coalesce([386{ name: nls.localize('allFiles', "All Files"), extensions: ['*'] },387matchingFilter,388...registeredLanguageFilters,389{ name: nls.localize('noExt', "No Extension"), extensions: [''] }390]);391392return options;393}394}395396397