Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/dialogs/browser/fileDialogService.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, FileFilter, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js';
7
import { URI } from '../../../../base/common/uri.js';
8
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
9
import { AbstractFileDialogService } from './abstractFileDialogService.js';
10
import { Schemas } from '../../../../base/common/network.js';
11
import { memoize } from '../../../../base/common/decorators.js';
12
import { HTMLFileSystemProvider } from '../../../../platform/files/browser/htmlFileSystemProvider.js';
13
import { localize } from '../../../../nls.js';
14
import { getMediaOrTextMime } from '../../../../base/common/mime.js';
15
import { basename } from '../../../../base/common/resources.js';
16
import { getActiveWindow, triggerDownload, triggerUpload } from '../../../../base/browser/dom.js';
17
import Severity from '../../../../base/common/severity.js';
18
import { VSBuffer } from '../../../../base/common/buffer.js';
19
import { extractFileListData } from '../../../../platform/dnd/browser/dnd.js';
20
import { Iterable } from '../../../../base/common/iterator.js';
21
import { WebFileSystemAccess } from '../../../../platform/files/browser/webFileSystemAccess.js';
22
import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js';
23
24
export class FileDialogService extends AbstractFileDialogService implements IFileDialogService {
25
26
@memoize
27
private get fileSystemProvider(): HTMLFileSystemProvider {
28
return this.fileService.getProvider(Schemas.file) as HTMLFileSystemProvider;
29
}
30
31
async pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void> {
32
const schema = this.getFileSystemSchema(options);
33
34
if (!options.defaultUri) {
35
options.defaultUri = await this.defaultFilePath(schema);
36
}
37
38
if (this.shouldUseSimplified(schema)) {
39
return super.pickFileFolderAndOpenSimplified(schema, options, false);
40
}
41
42
throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead."));
43
}
44
45
protected override addFileSchemaIfNeeded(schema: string, isFolder: boolean): string[] {
46
return (schema === Schemas.untitled) ? [Schemas.file]
47
: (((schema !== Schemas.file) && (!isFolder || (schema !== Schemas.vscodeRemote))) ? [schema, Schemas.file] : [schema]);
48
}
49
50
async pickFileAndOpen(options: IPickAndOpenOptions): Promise<void> {
51
const schema = this.getFileSystemSchema(options);
52
53
if (!options.defaultUri) {
54
options.defaultUri = await this.defaultFilePath(schema);
55
}
56
57
if (this.shouldUseSimplified(schema)) {
58
return super.pickFileAndOpenSimplified(schema, options, false);
59
}
60
61
const activeWindow = getActiveWindow();
62
if (!WebFileSystemAccess.supported(activeWindow)) {
63
return this.showUnsupportedBrowserWarning('open');
64
}
65
66
let fileHandle: FileSystemHandle | undefined = undefined;
67
try {
68
([fileHandle] = await activeWindow.showOpenFilePicker({ multiple: false }));
69
} catch (error) {
70
return; // `showOpenFilePicker` will throw an error when the user cancels
71
}
72
73
if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {
74
return;
75
}
76
77
const uri = await this.fileSystemProvider.registerFileHandle(fileHandle);
78
79
this.addFileToRecentlyOpened(uri);
80
81
await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } });
82
}
83
84
async pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void> {
85
const schema = this.getFileSystemSchema(options);
86
87
if (!options.defaultUri) {
88
options.defaultUri = await this.defaultFolderPath(schema);
89
}
90
91
if (this.shouldUseSimplified(schema)) {
92
return super.pickFolderAndOpenSimplified(schema, options);
93
}
94
95
throw new Error(localize('pickFolderAndOpen', "Can't open folders, try adding a folder to the workspace instead."));
96
}
97
98
async pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void> {
99
options.availableFileSystems = this.getWorkspaceAvailableFileSystems(options);
100
const schema = this.getFileSystemSchema(options);
101
102
if (!options.defaultUri) {
103
options.defaultUri = await this.defaultWorkspacePath(schema);
104
}
105
106
if (this.shouldUseSimplified(schema)) {
107
return super.pickWorkspaceAndOpenSimplified(schema, options);
108
}
109
110
throw new Error(localize('pickWorkspaceAndOpen', "Can't open workspaces, try adding a folder to the workspace instead."));
111
}
112
113
async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
114
const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems });
115
116
const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems);
117
if (this.shouldUseSimplified(schema)) {
118
return super.pickFileToSaveSimplified(schema, options);
119
}
120
121
const activeWindow = getActiveWindow();
122
if (!WebFileSystemAccess.supported(activeWindow)) {
123
return this.showUnsupportedBrowserWarning('save');
124
}
125
126
let fileHandle: FileSystemHandle | undefined = undefined;
127
const startIn = Iterable.first(this.fileSystemProvider.directories);
128
129
try {
130
fileHandle = await activeWindow.showSaveFilePicker({ types: this.getFilePickerTypes(options.filters), ...{ suggestedName: basename(defaultUri), startIn } });
131
} catch (error) {
132
return; // `showSaveFilePicker` will throw an error when the user cancels
133
}
134
135
if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {
136
return undefined;
137
}
138
139
return this.fileSystemProvider.registerFileHandle(fileHandle);
140
}
141
142
private getFilePickerTypes(filters?: FileFilter[]): FilePickerAcceptType[] | undefined {
143
return filters?.filter(filter => {
144
return !((filter.extensions.length === 1) && ((filter.extensions[0] === '*') || filter.extensions[0] === ''));
145
}).map(filter => {
146
const accept: Record<string, string[]> = {};
147
const extensions = filter.extensions.filter(ext => (ext.indexOf('-') < 0) && (ext.indexOf('*') < 0) && (ext.indexOf('_') < 0));
148
accept[getMediaOrTextMime(`fileName.${filter.extensions[0]}`) ?? 'text/plain'] = extensions.map(ext => ext.startsWith('.') ? ext : `.${ext}`);
149
return {
150
description: filter.name,
151
accept
152
};
153
});
154
}
155
156
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
157
const schema = this.getFileSystemSchema(options);
158
159
if (this.shouldUseSimplified(schema)) {
160
return super.showSaveDialogSimplified(schema, options);
161
}
162
163
const activeWindow = getActiveWindow();
164
if (!WebFileSystemAccess.supported(activeWindow)) {
165
return this.showUnsupportedBrowserWarning('save');
166
}
167
168
let fileHandle: FileSystemHandle | undefined = undefined;
169
const startIn = Iterable.first(this.fileSystemProvider.directories);
170
171
try {
172
fileHandle = await activeWindow.showSaveFilePicker({ types: this.getFilePickerTypes(options.filters), ...options.defaultUri ? { suggestedName: basename(options.defaultUri) } : undefined, ...{ startIn } });
173
} catch (error) {
174
return undefined; // `showSaveFilePicker` will throw an error when the user cancels
175
}
176
177
if (!WebFileSystemAccess.isFileSystemFileHandle(fileHandle)) {
178
return undefined;
179
}
180
181
return this.fileSystemProvider.registerFileHandle(fileHandle);
182
}
183
184
async showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined> {
185
const schema = this.getFileSystemSchema(options);
186
187
if (this.shouldUseSimplified(schema)) {
188
return super.showOpenDialogSimplified(schema, options);
189
}
190
191
const activeWindow = getActiveWindow();
192
if (!WebFileSystemAccess.supported(activeWindow)) {
193
return this.showUnsupportedBrowserWarning('open');
194
}
195
196
let uri: URI | undefined;
197
const startIn = Iterable.first(this.fileSystemProvider.directories) ?? 'documents';
198
199
try {
200
if (options.canSelectFiles) {
201
const handle = await activeWindow.showOpenFilePicker({ multiple: false, types: this.getFilePickerTypes(options.filters), ...{ startIn } });
202
if (handle.length === 1 && WebFileSystemAccess.isFileSystemFileHandle(handle[0])) {
203
uri = await this.fileSystemProvider.registerFileHandle(handle[0]);
204
}
205
} else {
206
const handle = await activeWindow.showDirectoryPicker({ ...{ startIn } });
207
uri = await this.fileSystemProvider.registerDirectoryHandle(handle);
208
}
209
} catch (error) {
210
// ignore - `showOpenFilePicker` / `showDirectoryPicker` will throw an error when the user cancels
211
}
212
213
return uri ? [uri] : undefined;
214
}
215
216
private async showUnsupportedBrowserWarning(context: 'save' | 'open'): Promise<undefined> {
217
218
// When saving, try to just download the contents
219
// of the active text editor if any as a workaround
220
if (context === 'save') {
221
const activeCodeEditor = this.codeEditorService.getActiveCodeEditor();
222
if (!(activeCodeEditor instanceof EmbeddedCodeEditorWidget)) {
223
const activeTextModel = activeCodeEditor?.getModel();
224
if (activeTextModel) {
225
triggerDownload(VSBuffer.fromString(activeTextModel.getValue()).buffer, basename(activeTextModel.uri));
226
return;
227
}
228
}
229
}
230
231
// Otherwise inform the user about options
232
233
const buttons: IPromptButton<void>[] = [
234
{
235
label: localize({ key: 'openRemote', comment: ['&& denotes a mnemonic'] }, "&&Open Remote..."),
236
run: async () => { await this.commandService.executeCommand('workbench.action.remote.showMenu'); }
237
},
238
{
239
label: localize({ key: 'learnMore', comment: ['&& denotes a mnemonic'] }, "&&Learn More"),
240
run: async () => { await this.openerService.open('https://aka.ms/VSCodeWebLocalFileSystemAccess'); }
241
}
242
];
243
if (context === 'open') {
244
buttons.push({
245
label: localize({ key: 'openFiles', comment: ['&& denotes a mnemonic'] }, "Open &&Files..."),
246
run: async () => {
247
const files = await triggerUpload();
248
if (files) {
249
const filesData = (await this.instantiationService.invokeFunction(accessor => extractFileListData(accessor, files))).filter(fileData => !fileData.isDirectory);
250
if (filesData.length > 0) {
251
this.editorService.openEditors(filesData.map(fileData => {
252
return {
253
resource: fileData.resource,
254
contents: fileData.contents?.toString(),
255
options: { pinned: true }
256
};
257
}));
258
}
259
}
260
}
261
});
262
}
263
264
await this.dialogService.prompt({
265
type: Severity.Warning,
266
message: localize('unsupportedBrowserMessage', "Opening Local Folders is Unsupported"),
267
detail: localize('unsupportedBrowserDetail', "Your browser doesn't support opening local folders.\nYou can either open single files or open a remote repository."),
268
buttons
269
});
270
271
return undefined;
272
}
273
274
private shouldUseSimplified(scheme: string): boolean {
275
return ![Schemas.file, Schemas.vscodeUserData, Schemas.tmp].includes(scheme);
276
}
277
}
278
279
registerSingleton(IFileDialogService, FileDialogService, InstantiationType.Delayed);
280
281