Path: blob/main/src/vs/workbench/services/dialogs/browser/fileDialogService.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 { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, FileFilter, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js';6import { URI } from '../../../../base/common/uri.js';7import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';8import { AbstractFileDialogService } from './abstractFileDialogService.js';9import { Schemas } from '../../../../base/common/network.js';10import { memoize } from '../../../../base/common/decorators.js';11import { HTMLFileSystemProvider } from '../../../../platform/files/browser/htmlFileSystemProvider.js';12import { localize } from '../../../../nls.js';13import { getMediaOrTextMime } from '../../../../base/common/mime.js';14import { basename } from '../../../../base/common/resources.js';15import { getActiveWindow, triggerDownload, triggerUpload } from '../../../../base/browser/dom.js';16import Severity from '../../../../base/common/severity.js';17import { VSBuffer } from '../../../../base/common/buffer.js';18import { extractFileListData } from '../../../../platform/dnd/browser/dnd.js';19import { Iterable } from '../../../../base/common/iterator.js';20import { WebFileSystemAccess } from '../../../../platform/files/browser/webFileSystemAccess.js';21import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';2223export class FileDialogService extends AbstractFileDialogService implements IFileDialogService {2425@memoize26private get fileSystemProvider(): HTMLFileSystemProvider {27return this.fileService.getProvider(Schemas.file) as HTMLFileSystemProvider;28}2930async pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void> {31const schema = this.getFileSystemSchema(options);3233if (!options.defaultUri) {34options.defaultUri = await this.defaultFilePath(schema);35}3637if (this.shouldUseSimplified(schema)) {38return super.pickFileFolderAndOpenSimplified(schema, options, false);39}4041throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead."));42}4344protected override addFileSchemaIfNeeded(schema: string, isFolder: boolean): string[] {45return (schema === Schemas.untitled) ? [Schemas.file]46: (((schema !== Schemas.file) && (!isFolder || (schema !== Schemas.vscodeRemote))) ? [schema, Schemas.file] : [schema]);47}4849async pickFileAndOpen(options: IPickAndOpenOptions): Promise<void> {50const schema = this.getFileSystemSchema(options);5152if (!options.defaultUri) {53options.defaultUri = await this.defaultFilePath(schema);54}5556if (this.shouldUseSimplified(schema)) {57return super.pickFileAndOpenSimplified(schema, options, false);58}5960const activeWindow = getActiveWindow();61if (!WebFileSystemAccess.supported(activeWindow)) {62return this.showUnsupportedBrowserWarning('open');63}6465let fileHandle: FileSystemHandle | undefined = undefined;66try {67([fileHandle] = await activeWindow.showOpenFilePicker({ multiple: false }));68} catch (error) {69return; // `showOpenFilePicker` will throw an error when the user cancels70}7172if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {73return;74}7576const uri = await this.fileSystemProvider.registerFileHandle(fileHandle);7778this.addFileToRecentlyOpened(uri);7980await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } });81}8283async pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void> {84const schema = this.getFileSystemSchema(options);8586if (!options.defaultUri) {87options.defaultUri = await this.defaultFolderPath(schema);88}8990if (this.shouldUseSimplified(schema)) {91return super.pickFolderAndOpenSimplified(schema, options);92}9394throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead."));95}9697async pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {98options.availableFileSystems = this.getWorkspaceAvailableFileSystems(options);99const schema = this.getFileSystemSchema(options);100101if (!options.defaultUri) {102options.defaultUri = await this.defaultWorkspacePath(schema);103}104105if (this.shouldUseSimplified(schema)) {106return super.pickWorkspaceAndOpenSimplified(schema, options);107}108109throw new Error(localize('pickWorkspaceAndOpen', "Can't open workspaces, try adding a folder to the workspace instead."));110}111112async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {113const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems });114115const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems);116if (this.shouldUseSimplified(schema)) {117return super.pickFileToSaveSimplified(schema, options);118}119120const activeWindow = getActiveWindow();121if (!WebFileSystemAccess.supported(activeWindow)) {122return this.showUnsupportedBrowserWarning('save');123}124125let fileHandle: FileSystemHandle | undefined = undefined;126const startIn = Iterable.first(this.fileSystemProvider.directories);127128try {129fileHandle = await activeWindow.showSaveFilePicker({ types: this.getFilePickerTypes(options.filters), ...{ suggestedName: basename(defaultUri), startIn } });130} catch (error) {131return; // `showSaveFilePicker` will throw an error when the user cancels132}133134if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {135return undefined;136}137138return this.fileSystemProvider.registerFileHandle(fileHandle);139}140141private getFilePickerTypes(filters?: FileFilter[]): FilePickerAcceptType[] | undefined {142return filters?.filter(filter => {143return !((filter.extensions.length === 1) && ((filter.extensions[0] === '*') || filter.extensions[0] === ''));144}).map(filter => {145const accept: Record<string, string[]> = {};146const extensions = filter.extensions.filter(ext => (ext.indexOf('-') < 0) && (ext.indexOf('*') < 0) && (ext.indexOf('_') < 0));147accept[getMediaOrTextMime(`fileName.${filter.extensions[0]}`) ?? 'text/plain'] = extensions.map(ext => ext.startsWith('.') ? ext : `.${ext}`);148return {149description: filter.name,150accept151};152});153}154155async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {156const schema = this.getFileSystemSchema(options);157158if (this.shouldUseSimplified(schema)) {159return super.showSaveDialogSimplified(schema, options);160}161162const activeWindow = getActiveWindow();163if (!WebFileSystemAccess.supported(activeWindow)) {164return this.showUnsupportedBrowserWarning('save');165}166167let fileHandle: FileSystemHandle | undefined = undefined;168const startIn = Iterable.first(this.fileSystemProvider.directories);169170try {171fileHandle = await activeWindow.showSaveFilePicker({ types: this.getFilePickerTypes(options.filters), ...options.defaultUri ? { suggestedName: basename(options.defaultUri) } : undefined, ...{ startIn } });172} catch (error) {173return undefined; // `showSaveFilePicker` will throw an error when the user cancels174}175176if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {177return undefined;178}179180return this.fileSystemProvider.registerFileHandle(fileHandle);181}182183async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {184const schema = this.getFileSystemSchema(options);185186if (this.shouldUseSimplified(schema)) {187return super.showOpenDialogSimplified(schema, options);188}189190const activeWindow = getActiveWindow();191if (!WebFileSystemAccess.supported(activeWindow)) {192return this.showUnsupportedBrowserWarning('open');193}194195let uri: URI | undefined;196const startIn = Iterable.first(this.fileSystemProvider.directories) ?? 'documents';197198try {199if (options.canSelectFiles) {200const handle = await activeWindow.showOpenFilePicker({ multiple: false, types: this.getFilePickerTypes(options.filters), ...{ startIn } });201if (handle.length === 1 && WebFileSystemAccess.isFileSystemFileHandle(handle[0])) {202uri = await this.fileSystemProvider.registerFileHandle(handle[0]);203}204} else {205const handle = await activeWindow.showDirectoryPicker({ ...{ startIn } });206uri = await this.fileSystemProvider.registerDirectoryHandle(handle);207}208} catch (error) {209// ignore - `showOpenFilePicker` / `showDirectoryPicker` will throw an error when the user cancels210}211212return uri ? [uri] : undefined;213}214215private async showUnsupportedBrowserWarning(context: 'save' | 'open'): Promise<undefined> {216217// When saving, try to just download the contents218// of the active text editor if any as a workaround219if (context === 'save') {220const activeCodeEditor = this.codeEditorService.getActiveCodeEditor();221if (!(activeCodeEditor instanceof EmbeddedCodeEditorWidget)) {222const activeTextModel = activeCodeEditor?.getModel();223if (activeTextModel) {224triggerDownload(VSBuffer.fromString(activeTextModel.getValue()).buffer, basename(activeTextModel.uri));225return;226}227}228}229230// Otherwise inform the user about options231232const buttons: IPromptButton<void>[] = [233{234label: localize({ key: 'openRemote', comment: ['&& denotes a mnemonic'] }, "&&Open Remote..."),235run: async () => { await this.commandService.executeCommand('workbench.action.remote.showMenu'); }236},237{238label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"),239run: async () => { await this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); }240}241];242if (context === 'open') {243buttons.push({244label: localize({ key: 'openFiles', comment: ['&& denotes a mnemonic'] }, "Open &&Files..."),245run: async () => {246const files = await triggerUpload();247if (files) {248const filesData = (await this.instantiationService.invokeFunction(accessor => extractFileListData(accessor, files))).filter(fileData => !fileData.isDirectory);249if (filesData.length > 0) {250this.editorService.openEditors(filesData.map(fileData => {251return {252resource: fileData.resource,253contents: fileData.contents?.toString(),254options: { pinned: true }255};256}));257}258}259}260});261}262263await this.dialogService.prompt({264type: Severity.Warning,265message: localize('unsupportedBrowserMessage', "Opening Local Folders is Unsupported"),266detail: localize('unsupportedBrowserDetail', "Your browser doesn't support opening local folders.\nYou can either open single files or open a remote repository."),267buttons268});269270return undefined;271}272273private shouldUseSimplified(scheme: string): boolean {274return ![Schemas.file, Schemas.vscodeUserData, Schemas.tmp].includes(scheme);275}276}277278registerSingleton(IFileDialogService, FileDialogService, InstantiationType.Delayed);279280281