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