Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts
5240 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 * as nls from '../../../../nls.js';
7
import { IWindowOpenable, isWorkspaceToOpen, isFileToOpen } from '../../../../platform/window/common/window.js';
8
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from '../../../../platform/dialogs/common/dialogs.js';
9
import { isSavedWorkspace, isTemporaryWorkspace, IWorkspaceContextService, WorkbenchState, WORKSPACE_EXTENSION } from '../../../../platform/workspace/common/workspace.js';
10
import { IHistoryService } from '../../history/common/history.js';
11
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
12
import { URI } from '../../../../base/common/uri.js';
13
import * as resources from '../../../../base/common/resources.js';
14
import { isAbsolute as localPathIsAbsolute, normalize as localPathNormalize } from '../../../../base/common/path.js';
15
import { IInstantiationService, } from '../../../../platform/instantiation/common/instantiation.js';
16
import { ISimpleFileDialog, SimpleFileDialog } from './simpleFileDialog.js';
17
import { IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
18
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
19
import { IFileService } from '../../../../platform/files/common/files.js';
20
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
21
import { IHostService } from '../../host/browser/host.js';
22
import Severity from '../../../../base/common/severity.js';
23
import { coalesce, distinct } from '../../../../base/common/arrays.js';
24
import { trim } from '../../../../base/common/strings.js';
25
import { ILanguageService } from '../../../../editor/common/languages/language.js';
26
import { ILabelService } from '../../../../platform/label/common/label.js';
27
import { IPathService } from '../../path/common/pathService.js';
28
import { Schemas } from '../../../../base/common/network.js';
29
import { PLAINTEXT_EXTENSION } from '../../../../editor/common/languages/modesRegistry.js';
30
import { ICommandService } from '../../../../platform/commands/common/commands.js';
31
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
32
import { IEditorService } from '../../editor/common/editorService.js';
33
import { EditorOpenSource } from '../../../../platform/editor/common/editor.js';
34
import { ILogService } from '../../../../platform/log/common/log.js';
35
36
export abstract class AbstractFileDialogService implements IFileDialogService {
37
38
declare readonly _serviceBrand: undefined;
39
40
constructor(
41
@IHostService protected readonly hostService: IHostService,
42
@IWorkspaceContextService protected readonly contextService: IWorkspaceContextService,
43
@IHistoryService protected readonly historyService: IHistoryService,
44
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
45
@IInstantiationService protected readonly instantiationService: IInstantiationService,
46
@IConfigurationService protected readonly configurationService: IConfigurationService,
47
@IFileService protected readonly fileService: IFileService,
48
@IOpenerService protected readonly openerService: IOpenerService,
49
@IDialogService protected readonly dialogService: IDialogService,
50
@ILanguageService private readonly languageService: ILanguageService,
51
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
52
@ILabelService private readonly labelService: ILabelService,
53
@IPathService private readonly pathService: IPathService,
54
@ICommandService protected readonly commandService: ICommandService,
55
@IEditorService protected readonly editorService: IEditorService,
56
@ICodeEditorService protected readonly codeEditorService: ICodeEditorService,
57
@ILogService private readonly logService: ILogService
58
) { }
59
60
async defaultFilePath(schemeFilter = this.getSchemeFilterForWindow(), authorityFilter = this.getAuthorityFilterForWindow()): Promise<URI> {
61
62
// Check for last active file first...
63
let candidate = this.historyService.getLastActiveFile(schemeFilter, authorityFilter);
64
65
// ...then for last active file root
66
if (!candidate) {
67
candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter, authorityFilter);
68
if (candidate) {
69
this.logService.debug(`[FileDialogService] Default file path using last active workspace root: ${candidate}`);
70
}
71
} else {
72
this.logService.debug(`[FileDialogService] Default file path using parent of last active file: ${candidate}`);
73
candidate = resources.dirname(candidate);
74
}
75
76
if (!candidate) {
77
candidate = await this.preferredHome(schemeFilter);
78
this.logService.debug(`[FileDialogService] Default file path using preferred home: ${candidate}`);
79
}
80
81
return candidate;
82
}
83
84
async defaultFolderPath(schemeFilter = this.getSchemeFilterForWindow(), authorityFilter = this.getAuthorityFilterForWindow()): Promise<URI> {
85
86
// Check for last active file root first...
87
let candidate = this.historyService.getLastActiveWorkspaceRoot(schemeFilter, authorityFilter);
88
89
// ...then for last active file
90
if (!candidate) {
91
candidate = this.historyService.getLastActiveFile(schemeFilter, authorityFilter);
92
if (candidate) {
93
this.logService.debug(`[FileDialogService] Default folder path using parent of last active file: ${candidate}`);
94
}
95
} else {
96
this.logService.debug(`[FileDialogService] Default folder path using last active workspace root: ${candidate}`);
97
}
98
99
if (!candidate) {
100
const preferredHome = await this.preferredHome(schemeFilter);
101
this.logService.debug(`[FileDialogService] Default folder path using preferred home: ${preferredHome}`);
102
return preferredHome;
103
}
104
105
return resources.dirname(candidate);
106
}
107
108
async preferredHome(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {
109
const preferLocal = schemeFilter === Schemas.file;
110
const preferredHomeConfig = this.configurationService.inspect<string>('files.dialog.defaultPath');
111
const preferredHomeCandidate = preferLocal ? preferredHomeConfig.userLocalValue : preferredHomeConfig.userRemoteValue;
112
this.logService.debug(`[FileDialogService] Preferred home: preferLocal=${preferLocal}, userLocalValue=${preferredHomeConfig.userLocalValue}, userRemoteValue=${preferredHomeConfig.userRemoteValue}`);
113
if (preferredHomeCandidate) {
114
const isPreferredHomeCandidateAbsolute = preferLocal ? localPathIsAbsolute(preferredHomeCandidate) : (await this.pathService.path).isAbsolute(preferredHomeCandidate);
115
if (isPreferredHomeCandidateAbsolute) {
116
const preferredHomeNormalized = preferLocal ? localPathNormalize(preferredHomeCandidate) : (await this.pathService.path).normalize(preferredHomeCandidate);
117
const preferredHome = resources.toLocalResource(await this.pathService.fileURI(preferredHomeNormalized), this.environmentService.remoteAuthority, this.pathService.defaultUriScheme);
118
if (await this.fileService.exists(preferredHome)) {
119
this.logService.debug(`[FileDialogService] Preferred home using files.dialog.defaultPath setting: ${preferredHome}`);
120
return preferredHome;
121
}
122
this.logService.debug(`[FileDialogService] Preferred home files.dialog.defaultPath path does not exist: ${preferredHome}`);
123
} else {
124
this.logService.debug(`[FileDialogService] Preferred home files.dialog.defaultPath is not absolute: ${preferredHomeCandidate}`);
125
}
126
}
127
128
const userHome = this.pathService.userHome({ preferLocal });
129
this.logService.debug(`[FileDialogService] Preferred home using user home: ${userHome}`);
130
return userHome;
131
}
132
133
async defaultWorkspacePath(schemeFilter = this.getSchemeFilterForWindow()): Promise<URI> {
134
let defaultWorkspacePath: URI | undefined;
135
136
// Check for current workspace config file first...
137
if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
138
const configuration = this.contextService.getWorkspace().configuration;
139
if (configuration?.scheme === schemeFilter && isSavedWorkspace(configuration, this.environmentService) && !isTemporaryWorkspace(configuration)) {
140
defaultWorkspacePath = resources.dirname(configuration);
141
}
142
}
143
144
// ...then fallback to default file path
145
if (!defaultWorkspacePath) {
146
defaultWorkspacePath = await this.defaultFilePath(schemeFilter);
147
}
148
149
return defaultWorkspacePath;
150
}
151
152
async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {
153
if (this.skipDialogs()) {
154
this.logService.trace('FileDialogService: refused to show save confirmation dialog in tests.');
155
156
// no veto when we are in extension dev testing mode because we cannot assume we run interactive
157
return ConfirmResult.DONT_SAVE;
158
}
159
160
return this.doShowSaveConfirm(fileNamesOrResources);
161
}
162
163
private skipDialogs(): boolean {
164
if (this.environmentService.enableSmokeTestDriver) {
165
this.logService.warn('DialogService: Dialog requested during smoke test.');
166
}
167
// integration tests
168
return this.environmentService.isExtensionDevelopment && !!this.environmentService.extensionTestsLocationURI;
169
}
170
171
private async doShowSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {
172
if (fileNamesOrResources.length === 0) {
173
return ConfirmResult.DONT_SAVE;
174
}
175
176
let message: string;
177
let detail = nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.");
178
if (fileNamesOrResources.length === 1) {
179
message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNamesOrResources[0] === 'string' ? fileNamesOrResources[0] : resources.basename(fileNamesOrResources[0]));
180
} else {
181
message = nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length);
182
detail = getFileNamesMessage(fileNamesOrResources) + '\n' + detail;
183
}
184
185
const { result } = await this.dialogService.prompt<ConfirmResult>({
186
type: Severity.Warning,
187
message,
188
detail,
189
buttons: [
190
{
191
label: fileNamesOrResources.length > 1 ?
192
nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") :
193
nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
194
run: () => ConfirmResult.SAVE
195
},
196
{
197
label: nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
198
run: () => ConfirmResult.DONT_SAVE
199
}
200
],
201
cancelButton: {
202
run: () => ConfirmResult.CANCEL
203
}
204
});
205
206
return result;
207
}
208
209
protected addFileSchemaIfNeeded(schema: string, _isFolder?: boolean): string[] {
210
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
211
}
212
213
protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<void> {
214
const title = nls.localize('openFileOrFolder.title', 'Open File or Folder');
215
const availableFileSystems = this.addFileSchemaIfNeeded(schema);
216
217
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
218
219
if (uri) {
220
const stat = await this.fileService.stat(uri);
221
222
const toOpen: IWindowOpenable = stat.isDirectory ? { folderUri: uri } : { fileUri: uri };
223
if (!isWorkspaceToOpen(toOpen) && isFileToOpen(toOpen)) {
224
this.addFileToRecentlyOpened(toOpen.fileUri);
225
}
226
227
if (stat.isDirectory || options.forceNewWindow || preferNewWindow) {
228
await this.hostService.openWindow([toOpen], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
229
} else {
230
await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true });
231
}
232
}
233
}
234
235
protected async pickFileAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<void> {
236
const title = nls.localize('openFile.title', 'Open File');
237
const availableFileSystems = this.addFileSchemaIfNeeded(schema);
238
239
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
240
if (uri) {
241
this.addFileToRecentlyOpened(uri);
242
243
if (options.forceNewWindow || preferNewWindow) {
244
await this.hostService.openWindow([{ fileUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
245
} else {
246
await this.editorService.openEditors([{ resource: uri, options: { source: EditorOpenSource.USER, pinned: true } }], undefined, { validateTrust: true });
247
}
248
}
249
}
250
251
protected addFileToRecentlyOpened(uri: URI): void {
252
this.workspacesService.addRecentlyOpened([{ fileUri: uri, label: this.labelService.getUriLabel(uri, { appendWorkspaceSuffix: true }) }]);
253
}
254
255
protected async pickFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions): Promise<void> {
256
const title = nls.localize('openFolder.title', 'Open Folder');
257
const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);
258
259
const uri = await this.pickResource({ canSelectFiles: false, canSelectFolders: true, canSelectMany: false, defaultUri: options.defaultUri, title, availableFileSystems });
260
if (uri) {
261
return this.hostService.openWindow([{ folderUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
262
}
263
}
264
265
protected async pickWorkspaceAndOpenSimplified(schema: string, options: IPickAndOpenOptions): Promise<void> {
266
const title = nls.localize('openWorkspace.title', 'Open Workspace from File');
267
const filters: FileFilter[] = [{ name: nls.localize('filterName.workspace', 'Workspace'), extensions: [WORKSPACE_EXTENSION] }];
268
const availableFileSystems = this.addFileSchemaIfNeeded(schema, true);
269
270
const uri = await this.pickResource({ canSelectFiles: true, canSelectFolders: false, canSelectMany: false, defaultUri: options.defaultUri, title, filters, availableFileSystems });
271
if (uri) {
272
return this.hostService.openWindow([{ workspaceUri: uri }], { forceNewWindow: options.forceNewWindow, remoteAuthority: options.remoteAuthority });
273
}
274
}
275
276
protected async pickFileToSaveSimplified(schema: string, options: ISaveDialogOptions): Promise<URI | undefined> {
277
if (!options.availableFileSystems) {
278
options.availableFileSystems = this.addFileSchemaIfNeeded(schema);
279
}
280
281
options.title = nls.localize('saveFileAs.title', 'Save As');
282
const uri = await this.saveRemoteResource(options);
283
284
if (uri) {
285
this.addFileToRecentlyOpened(uri);
286
}
287
288
return uri;
289
}
290
291
protected async showSaveDialogSimplified(schema: string, options: ISaveDialogOptions): Promise<URI | undefined> {
292
if (!options.availableFileSystems) {
293
options.availableFileSystems = this.addFileSchemaIfNeeded(schema);
294
}
295
296
return this.saveRemoteResource(options);
297
}
298
299
protected async showOpenDialogSimplified(schema: string, options: IOpenDialogOptions): Promise<URI[] | undefined> {
300
if (!options.availableFileSystems) {
301
options.availableFileSystems = this.addFileSchemaIfNeeded(schema, options.canSelectFolders);
302
}
303
304
const uri = await this.pickResource(options);
305
306
return uri ? [uri] : undefined;
307
}
308
309
protected getSimpleFileDialog(): ISimpleFileDialog {
310
return this.instantiationService.createInstance(SimpleFileDialog);
311
}
312
313
private pickResource(options: IOpenDialogOptions): Promise<URI | undefined> {
314
return this.getSimpleFileDialog().showOpenDialog(options);
315
}
316
317
private saveRemoteResource(options: ISaveDialogOptions): Promise<URI | undefined> {
318
return this.getSimpleFileDialog().showSaveDialog(options);
319
}
320
321
private getSchemeFilterForWindow(defaultUriScheme?: string): string {
322
return defaultUriScheme ?? this.pathService.defaultUriScheme;
323
}
324
325
private getAuthorityFilterForWindow(): string | undefined {
326
return this.environmentService.remoteAuthority;
327
}
328
329
protected getFileSystemSchema(options: { availableFileSystems?: readonly string[]; defaultUri?: URI }): string {
330
return options.availableFileSystems?.[0] || this.getSchemeFilterForWindow(options.defaultUri?.scheme);
331
}
332
333
abstract pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
334
abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
335
abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
336
abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
337
protected getWorkspaceAvailableFileSystems(options: IPickAndOpenOptions): string[] {
338
if (options.availableFileSystems && (options.availableFileSystems.length > 0)) {
339
return options.availableFileSystems;
340
}
341
const availableFileSystems = [Schemas.file];
342
if (this.environmentService.remoteAuthority) {
343
availableFileSystems.unshift(Schemas.vscodeRemote);
344
}
345
return availableFileSystems;
346
}
347
abstract showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
348
abstract showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
349
350
abstract pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
351
352
protected getPickFileToSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {
353
const options: ISaveDialogOptions = {
354
defaultUri,
355
title: nls.localize('saveAsTitle', "Save As"),
356
availableFileSystems
357
};
358
359
interface IFilter { name: string; extensions: string[] }
360
361
// Build the file filter by using our known languages
362
const ext: string | undefined = defaultUri ? resources.extname(defaultUri) : undefined;
363
let matchingFilter: IFilter | undefined;
364
365
const registeredLanguageNames = this.languageService.getSortedRegisteredLanguageNames();
366
const registeredLanguageFilters: IFilter[] = coalesce(registeredLanguageNames.map(({ languageName, languageId }) => {
367
const extensions = this.languageService.getExtensions(languageId);
368
if (!extensions.length) {
369
return null;
370
}
371
372
const filter: IFilter = { name: languageName, extensions: distinct(extensions).slice(0, 10).map(e => trim(e, '.')) };
373
374
// https://github.com/microsoft/vscode/issues/115860
375
const extOrPlaintext = ext || PLAINTEXT_EXTENSION;
376
if (!matchingFilter && extensions.includes(extOrPlaintext)) {
377
matchingFilter = filter;
378
379
// The selected extension must be in the set of extensions that are in the filter list that is sent to the save dialog.
380
// If it isn't, add it manually. https://github.com/microsoft/vscode/issues/147657
381
const trimmedExt = trim(extOrPlaintext, '.');
382
if (!filter.extensions.includes(trimmedExt)) {
383
filter.extensions.unshift(trimmedExt);
384
}
385
386
return null; // first matching filter will be added to the top
387
}
388
389
return filter;
390
}));
391
392
// We have no matching filter, e.g. because the language
393
// is unknown. We still add the extension to the list of
394
// filters though so that it can be picked
395
// (https://github.com/microsoft/vscode/issues/96283)
396
if (!matchingFilter && ext) {
397
matchingFilter = { name: trim(ext, '.').toUpperCase(), extensions: [trim(ext, '.')] };
398
}
399
400
// Order of filters is
401
// - All Files (we MUST do this to fix macOS issue https://github.com/microsoft/vscode/issues/102713)
402
// - File Extension Match (if any)
403
// - All Languages
404
// - No Extension
405
options.filters = coalesce([
406
{ name: nls.localize('allFiles', "All Files"), extensions: ['*'] },
407
matchingFilter,
408
...registeredLanguageFilters,
409
{ name: nls.localize('noExt', "No Extension"), extensions: [''] }
410
]);
411
412
return options;
413
}
414
}
415
416