Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts
5237 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 * as resources from '../../../../base/common/resources.js';
8
import * as objects from '../../../../base/common/objects.js';
9
import { IFileService, IFileStat, FileKind, IFileStatWithPartialMetadata } from '../../../../platform/files/common/files.js';
10
import { IQuickInputService, IQuickPickItem, IQuickPick, ItemActivation } from '../../../../platform/quickinput/common/quickInput.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { isWindows, OperatingSystem } from '../../../../base/common/platform.js';
13
import { ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
14
import { ILabelService } from '../../../../platform/label/common/label.js';
15
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
16
import { INotificationService } from '../../../../platform/notification/common/notification.js';
17
import { IModelService } from '../../../../editor/common/services/model.js';
18
import { ILanguageService } from '../../../../editor/common/languages/language.js';
19
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
20
import { Schemas } from '../../../../base/common/network.js';
21
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
22
import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js';
23
import { IContextKeyService, IContextKey, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
24
import { equalsIgnoreCase, format, startsWithIgnoreCase } from '../../../../base/common/strings.js';
25
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
26
import { IRemoteAgentEnvironment } from '../../../../platform/remote/common/remoteAgentEnvironment.js';
27
import { isValidBasename } from '../../../../base/common/extpath.js';
28
import { Emitter } from '../../../../base/common/event.js';
29
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
30
import { createCancelablePromise, CancelablePromise } from '../../../../base/common/async.js';
31
import { CancellationToken } from '../../../../base/common/cancellation.js';
32
import { ICommandHandler } from '../../../../platform/commands/common/commands.js';
33
import { IEditorService } from '../../editor/common/editorService.js';
34
import { normalizeDriveLetter } from '../../../../base/common/labels.js';
35
import { SaveReason } from '../../../common/editor.js';
36
import { IPathService } from '../../path/common/pathService.js';
37
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
38
import { getActiveDocument } from '../../../../base/browser/dom.js';
39
import { Codicon } from '../../../../base/common/codicons.js';
40
import { ThemeIcon } from '../../../../base/common/themables.js';
41
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
42
43
export namespace OpenLocalFileCommand {
44
export const ID = 'workbench.action.files.openLocalFile';
45
export const LABEL = nls.localize('openLocalFile', "Open Local File...");
46
export function handler(): ICommandHandler {
47
return accessor => {
48
const dialogService = accessor.get(IFileDialogService);
49
return dialogService.pickFileAndOpen({ forceNewWindow: false, availableFileSystems: [Schemas.file] });
50
};
51
}
52
}
53
54
export namespace SaveLocalFileCommand {
55
export const ID = 'workbench.action.files.saveLocalFile';
56
export const LABEL = nls.localize('saveLocalFile', "Save Local File...");
57
export function handler(): ICommandHandler {
58
return accessor => {
59
const editorService = accessor.get(IEditorService);
60
const activeEditorPane = editorService.activeEditorPane;
61
if (activeEditorPane) {
62
return editorService.save({ groupId: activeEditorPane.group.id, editor: activeEditorPane.input }, { saveAs: true, availableFileSystems: [Schemas.file], reason: SaveReason.EXPLICIT });
63
}
64
65
return Promise.resolve(undefined);
66
};
67
}
68
}
69
70
export namespace OpenLocalFolderCommand {
71
export const ID = 'workbench.action.files.openLocalFolder';
72
export const LABEL = nls.localize('openLocalFolder', "Open Local Folder...");
73
export function handler(): ICommandHandler {
74
return accessor => {
75
const dialogService = accessor.get(IFileDialogService);
76
return dialogService.pickFolderAndOpen({ forceNewWindow: false, availableFileSystems: [Schemas.file] });
77
};
78
}
79
}
80
81
export namespace OpenLocalFileFolderCommand {
82
export const ID = 'workbench.action.files.openLocalFileFolder';
83
export const LABEL = nls.localize('openLocalFileFolder', "Open Local...");
84
export function handler(): ICommandHandler {
85
return accessor => {
86
const dialogService = accessor.get(IFileDialogService);
87
return dialogService.pickFileFolderAndOpen({ forceNewWindow: false, availableFileSystems: [Schemas.file] });
88
};
89
}
90
}
91
92
interface FileQuickPickItem extends IQuickPickItem {
93
uri: URI;
94
isFolder: boolean;
95
}
96
97
enum UpdateResult {
98
Updated,
99
UpdatedWithTrailing,
100
Updating,
101
NotUpdated,
102
InvalidPath
103
}
104
105
export const RemoteFileDialogContext = new RawContextKey<boolean>('remoteFileDialogVisible', false);
106
107
export interface ISimpleFileDialog extends IDisposable {
108
showOpenDialog(options: IOpenDialogOptions): Promise<URI | undefined>;
109
showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
110
}
111
112
export class SimpleFileDialog extends Disposable implements ISimpleFileDialog {
113
private options!: IOpenDialogOptions;
114
private currentFolder!: URI;
115
private filePickBox!: IQuickPick<FileQuickPickItem>;
116
private hidden: boolean = false;
117
private allowFileSelection: boolean = true;
118
private allowFolderSelection: boolean = false;
119
private remoteAuthority: string | undefined;
120
private requiresTrailing: boolean = false;
121
private trailing: string | undefined;
122
protected scheme: string;
123
private contextKey: IContextKey<boolean>;
124
private userEnteredPathSegment: string = '';
125
private autoCompletePathSegment: string = '';
126
private activeItem: FileQuickPickItem | undefined;
127
private userHome!: URI;
128
private trueHome!: URI;
129
private isWindows: boolean = false;
130
private badPath: string | undefined;
131
private remoteAgentEnvironment: IRemoteAgentEnvironment | null | undefined;
132
private separator: string = '/';
133
private readonly onBusyChangeEmitter = this._register(new Emitter<boolean>());
134
private updatingPromise: CancelablePromise<boolean> | undefined;
135
136
private _showDotFiles: boolean = true;
137
138
constructor(
139
@IFileService private readonly fileService: IFileService,
140
@IQuickInputService private readonly quickInputService: IQuickInputService,
141
@ILabelService private readonly labelService: ILabelService,
142
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
143
@INotificationService private readonly notificationService: INotificationService,
144
@IFileDialogService private readonly fileDialogService: IFileDialogService,
145
@IModelService private readonly modelService: IModelService,
146
@ILanguageService private readonly languageService: ILanguageService,
147
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
148
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
149
@IPathService protected readonly pathService: IPathService,
150
@IKeybindingService private readonly keybindingService: IKeybindingService,
151
@IContextKeyService contextKeyService: IContextKeyService,
152
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
153
@IStorageService private readonly storageService: IStorageService
154
) {
155
super();
156
this.remoteAuthority = this.environmentService.remoteAuthority;
157
this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService);
158
this.scheme = this.pathService.defaultUriScheme;
159
160
this.getShowDotFiles();
161
const disposableStore = this._register(new DisposableStore());
162
disposableStore.add(this.storageService.onDidChangeValue(StorageScope.WORKSPACE, 'remoteFileDialog.showDotFiles', disposableStore)(async _ => {
163
this.getShowDotFiles();
164
this.setButtons();
165
const startingValue = this.filePickBox.value;
166
const folderValue = this.pathFromUri(this.currentFolder, true);
167
this.filePickBox.value = folderValue;
168
await this.tryUpdateItems(folderValue, this.currentFolder, true);
169
this.filePickBox.value = startingValue;
170
}));
171
}
172
173
private setShowDotFiles(showDotFiles: boolean) {
174
this.storageService.store('remoteFileDialog.showDotFiles', showDotFiles, StorageScope.WORKSPACE, StorageTarget.USER);
175
}
176
177
private getShowDotFiles() {
178
this._showDotFiles = this.storageService.getBoolean('remoteFileDialog.showDotFiles', StorageScope.WORKSPACE, true);
179
}
180
181
set busy(busy: boolean) {
182
if (this.filePickBox.busy !== busy) {
183
this.filePickBox.busy = busy;
184
this.onBusyChangeEmitter.fire(busy);
185
}
186
}
187
188
get busy(): boolean {
189
return this.filePickBox.busy;
190
}
191
192
public async showOpenDialog(options: IOpenDialogOptions = {}): Promise<URI | undefined> {
193
this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri);
194
this.userHome = await this.getUserHome();
195
this.trueHome = await this.getUserHome(true);
196
const newOptions = this.getOptions(options);
197
if (!newOptions) {
198
return Promise.resolve(undefined);
199
}
200
this.options = newOptions;
201
return this.pickResource();
202
}
203
204
public async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
205
this.scheme = this.getScheme(options.availableFileSystems, options.defaultUri);
206
this.userHome = await this.getUserHome();
207
this.trueHome = await this.getUserHome(true);
208
this.requiresTrailing = true;
209
const newOptions = this.getOptions(options, true);
210
if (!newOptions) {
211
return Promise.resolve(undefined);
212
}
213
this.options = newOptions;
214
this.options.canSelectFolders = true;
215
this.options.canSelectFiles = true;
216
217
return new Promise<URI | undefined>((resolve) => {
218
this.pickResource(true).then(folderUri => {
219
resolve(folderUri);
220
});
221
});
222
}
223
224
private getOptions(options: ISaveDialogOptions | IOpenDialogOptions, isSave: boolean = false): IOpenDialogOptions | undefined {
225
let defaultUri: URI | undefined = undefined;
226
let filename: string | undefined = undefined;
227
if (options.defaultUri) {
228
defaultUri = (this.scheme === options.defaultUri.scheme) ? options.defaultUri : undefined;
229
filename = isSave ? resources.basename(options.defaultUri) : undefined;
230
}
231
if (!defaultUri) {
232
defaultUri = this.userHome;
233
if (filename) {
234
defaultUri = resources.joinPath(defaultUri, filename);
235
}
236
}
237
if ((this.scheme !== Schemas.file) && !this.fileService.hasProvider(defaultUri)) {
238
this.notificationService.info(nls.localize('remoteFileDialog.notConnectedToRemote', 'File system provider for {0} is not available.', defaultUri.toString()));
239
return undefined;
240
}
241
const newOptions: IOpenDialogOptions = objects.deepClone(options);
242
newOptions.defaultUri = defaultUri;
243
return newOptions;
244
}
245
246
private remoteUriFrom(path: string, hintUri?: URI): URI {
247
if (!path.startsWith('\\\\')) {
248
path = path.replace(/\\/g, '/');
249
}
250
const uri: URI = this.scheme === Schemas.file ? URI.file(path) : URI.from({ scheme: this.scheme, path, query: hintUri?.query, fragment: hintUri?.fragment });
251
// If the default scheme is file, then we don't care about the remote authority or the hint authority
252
const authority = (uri.scheme === Schemas.file) ? undefined : (this.remoteAuthority ?? hintUri?.authority);
253
return resources.toLocalResource(uri, authority,
254
// If there is a remote authority, then we should use the system's default URI as the local scheme.
255
// If there is *no* remote authority, then we should use the default scheme for this dialog as that is already local.
256
authority ? this.pathService.defaultUriScheme : uri.scheme);
257
}
258
259
private getScheme(available: readonly string[] | undefined, defaultUri: URI | undefined): string {
260
if (available && available.length > 0) {
261
if (defaultUri && (available.indexOf(defaultUri.scheme) >= 0)) {
262
return defaultUri.scheme;
263
}
264
return available[0];
265
} else if (defaultUri) {
266
return defaultUri.scheme;
267
}
268
return Schemas.file;
269
}
270
271
private async getRemoteAgentEnvironment(): Promise<IRemoteAgentEnvironment | null> {
272
if (this.remoteAgentEnvironment === undefined) {
273
this.remoteAgentEnvironment = await this.remoteAgentService.getEnvironment();
274
}
275
return this.remoteAgentEnvironment;
276
}
277
278
protected getUserHome(trueHome = false): Promise<URI> {
279
return trueHome
280
? this.pathService.userHome({ preferLocal: this.scheme === Schemas.file })
281
: this.fileDialogService.preferredHome(this.scheme);
282
}
283
284
private async pickResource(isSave: boolean = false): Promise<URI | undefined> {
285
this.allowFolderSelection = !!this.options.canSelectFolders;
286
this.allowFileSelection = !!this.options.canSelectFiles;
287
this.separator = this.labelService.getSeparator(this.scheme, this.remoteAuthority);
288
this.hidden = false;
289
this.isWindows = await this.checkIsWindowsOS();
290
let homedir: URI = this.options.defaultUri ? this.options.defaultUri : this.workspaceContextService.getWorkspace().folders[0].uri;
291
let stat: IFileStatWithPartialMetadata | undefined;
292
const ext: string = resources.extname(homedir);
293
if (this.options.defaultUri) {
294
try {
295
stat = await this.fileService.stat(this.options.defaultUri);
296
} catch (e) {
297
// The file or folder doesn't exist
298
}
299
if (!stat || !stat.isDirectory) {
300
homedir = resources.dirname(this.options.defaultUri);
301
this.trailing = resources.basename(this.options.defaultUri);
302
}
303
}
304
305
return new Promise<URI | undefined>((resolve) => {
306
this.filePickBox = this._register(this.quickInputService.createQuickPick<FileQuickPickItem>());
307
this.busy = true;
308
this.filePickBox.matchOnLabel = false;
309
this.filePickBox.sortByLabel = false;
310
this.filePickBox.ignoreFocusOut = true;
311
this.filePickBox.placeholder = nls.localize('remoteFileDialog.placeholder', "Folder path");
312
this.filePickBox.ok = true;
313
this.filePickBox.okLabel = typeof this.options.openLabel === 'string' ? this.options.openLabel : this.options.openLabel?.withoutMnemonic;
314
if ((this.scheme !== Schemas.file) && this.options && this.options.availableFileSystems && (this.options.availableFileSystems.length > 1) && (this.options.availableFileSystems.indexOf(Schemas.file) > -1)) {
315
this.filePickBox.customButton = true;
316
this.filePickBox.customLabel = nls.localize('remoteFileDialog.local', 'Show Local');
317
this.filePickBox.customButtonSecondary = true;
318
let action;
319
if (isSave) {
320
action = SaveLocalFileCommand;
321
} else {
322
action = this.allowFileSelection ? (this.allowFolderSelection ? OpenLocalFileFolderCommand : OpenLocalFileCommand) : OpenLocalFolderCommand;
323
}
324
const keybinding = this.keybindingService.lookupKeybinding(action.ID);
325
if (keybinding) {
326
const label = keybinding.getLabel();
327
if (label) {
328
this.filePickBox.customHover = format('{0} ({1})', action.LABEL, label);
329
}
330
}
331
}
332
333
this.setButtons();
334
this._register(this.filePickBox.onDidTriggerButton(e => {
335
this.setShowDotFiles(!this._showDotFiles);
336
}));
337
338
let isResolving: number = 0;
339
let isAcceptHandled = false;
340
this.currentFolder = resources.dirname(homedir);
341
this.userEnteredPathSegment = '';
342
this.autoCompletePathSegment = '';
343
344
this.filePickBox.title = this.options.title;
345
this.filePickBox.value = this.pathFromUri(this.currentFolder, true);
346
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
347
348
const doResolve = (uri: URI | undefined) => {
349
if (uri) {
350
uri = resources.addTrailingPathSeparator(uri, this.separator); // Ensures that c: is c:/ since this comes from user input and can be incorrect.
351
// To be consistent, we should never have a trailing path separator on directories (or anything else). Will not remove from c:/.
352
uri = resources.removeTrailingPathSeparator(uri);
353
}
354
resolve(uri);
355
this.contextKey.set(false);
356
this.dispose();
357
};
358
359
this._register(this.filePickBox.onDidCustom(() => {
360
if (isAcceptHandled || this.busy) {
361
return;
362
}
363
364
isAcceptHandled = true;
365
isResolving++;
366
if (this.options.availableFileSystems && (this.options.availableFileSystems.length > 1)) {
367
this.options.availableFileSystems = this.options.availableFileSystems.slice(1);
368
}
369
this.filePickBox.hide();
370
if (isSave) {
371
return this.fileDialogService.showSaveDialog(this.options).then(result => {
372
doResolve(result);
373
});
374
} else {
375
return this.fileDialogService.showOpenDialog(this.options).then(result => {
376
doResolve(result ? result[0] : undefined);
377
});
378
}
379
}));
380
381
const busyDisposable = this._register(new MutableDisposable());
382
const handleAccept = () => {
383
if (this.busy) {
384
// Save the accept until the file picker is not busy.
385
busyDisposable.value = this.onBusyChangeEmitter.event((busy: boolean) => {
386
if (!busy) {
387
handleAccept();
388
}
389
});
390
return;
391
} else if (isAcceptHandled) {
392
return;
393
}
394
395
isAcceptHandled = true;
396
isResolving++;
397
this.onDidAccept().then(resolveValue => {
398
if (resolveValue) {
399
this.filePickBox.hide();
400
doResolve(resolveValue);
401
} else if (this.hidden) {
402
doResolve(undefined);
403
} else {
404
isResolving--;
405
isAcceptHandled = false;
406
}
407
});
408
};
409
410
this._register(this.filePickBox.onDidAccept(_ => {
411
handleAccept();
412
}));
413
414
this._register(this.filePickBox.onDidChangeActive(i => {
415
isAcceptHandled = false;
416
// update input box to match the first selected item
417
if ((i.length === 1) && this.isSelectionChangeFromUser()) {
418
this.filePickBox.validationMessage = undefined;
419
const userPath = this.constructFullUserPath();
420
if (!equalsIgnoreCase(this.filePickBox.value.substring(0, userPath.length), userPath)) {
421
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
422
this.insertText(userPath, userPath);
423
}
424
this.setAutoComplete(userPath, this.userEnteredPathSegment, i[0], true);
425
}
426
}));
427
428
this._register(this.filePickBox.onDidChangeValue(async value => {
429
return this.handleValueChange(value);
430
}));
431
this._register(this.filePickBox.onDidHide(() => {
432
this.hidden = true;
433
if (isResolving === 0) {
434
doResolve(undefined);
435
}
436
}));
437
438
this.filePickBox.show();
439
this.contextKey.set(true);
440
this.updateItems(homedir, true, this.trailing).then(() => {
441
if (this.trailing) {
442
this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length];
443
} else {
444
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
445
}
446
this.busy = false;
447
});
448
});
449
}
450
451
public override dispose(): void {
452
super.dispose();
453
}
454
455
private async handleValueChange(value: string) {
456
try {
457
// onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything
458
if (this.isValueChangeFromUser()) {
459
// If the user has just entered more bad path, don't change anything
460
if (!equalsIgnoreCase(value, this.constructFullUserPath()) && (!this.isBadSubpath(value) || this.canTildaEscapeHatch(value))) {
461
this.filePickBox.validationMessage = undefined;
462
const filePickBoxUri = this.filePickBoxValue();
463
let updated: UpdateResult = UpdateResult.NotUpdated;
464
if (!resources.extUriIgnorePathCase.isEqual(this.currentFolder, filePickBoxUri)) {
465
updated = await this.tryUpdateItems(value, filePickBoxUri);
466
}
467
if ((updated === UpdateResult.NotUpdated) || (updated === UpdateResult.UpdatedWithTrailing)) {
468
this.setActiveItems(value);
469
}
470
} else {
471
this.filePickBox.activeItems = [];
472
this.userEnteredPathSegment = '';
473
}
474
}
475
} catch {
476
// Since any text can be entered in the input box, there is potential for error causing input. If this happens, do nothing.
477
}
478
}
479
480
private setButtons() {
481
this.filePickBox.buttons = [{
482
iconClass: this._showDotFiles ? ThemeIcon.asClassName(Codicon.eye) : ThemeIcon.asClassName(Codicon.eyeClosed),
483
tooltip: this._showDotFiles ? nls.localize('remoteFileDialog.hideDotFiles', "Hide dot files") : nls.localize('remoteFileDialog.showDotFiles', "Show dot files"),
484
alwaysVisible: true
485
}];
486
}
487
488
private isBadSubpath(value: string) {
489
return this.badPath && (value.length > this.badPath.length) && equalsIgnoreCase(value.substring(0, this.badPath.length), this.badPath);
490
}
491
492
private isValueChangeFromUser(): boolean {
493
if (equalsIgnoreCase(this.filePickBox.value, this.pathAppend(this.currentFolder, this.userEnteredPathSegment + this.autoCompletePathSegment))) {
494
return false;
495
}
496
return true;
497
}
498
499
private isSelectionChangeFromUser(): boolean {
500
if (this.activeItem === (this.filePickBox.activeItems ? this.filePickBox.activeItems[0] : undefined)) {
501
return false;
502
}
503
return true;
504
}
505
506
private constructFullUserPath(): string {
507
const currentFolderPath = this.pathFromUri(this.currentFolder);
508
if (equalsIgnoreCase(this.filePickBox.value.substr(0, this.userEnteredPathSegment.length), this.userEnteredPathSegment)) {
509
if (equalsIgnoreCase(this.filePickBox.value.substr(0, currentFolderPath.length), currentFolderPath)) {
510
return currentFolderPath;
511
} else {
512
return this.userEnteredPathSegment;
513
}
514
} else {
515
return this.pathAppend(this.currentFolder, this.userEnteredPathSegment);
516
}
517
}
518
519
private filePickBoxValue(): URI {
520
// The file pick box can't render everything, so we use the current folder to create the uri so that it is an existing path.
521
const directUri = this.remoteUriFrom(this.filePickBox.value.trimRight(), this.currentFolder);
522
const currentPath = this.pathFromUri(this.currentFolder);
523
if (equalsIgnoreCase(this.filePickBox.value, currentPath)) {
524
return this.currentFolder;
525
}
526
const currentDisplayUri = this.remoteUriFrom(currentPath, this.currentFolder);
527
const relativePath = resources.relativePath(currentDisplayUri, directUri);
528
const isSameRoot = (this.filePickBox.value.length > 1 && currentPath.length > 1) ? equalsIgnoreCase(this.filePickBox.value.substr(0, 2), currentPath.substr(0, 2)) : false;
529
if (relativePath && isSameRoot) {
530
let path = resources.joinPath(this.currentFolder, relativePath);
531
const directBasename = resources.basename(directUri);
532
if ((directBasename === '.') || (directBasename === '..')) {
533
path = this.remoteUriFrom(this.pathAppend(path, directBasename), this.currentFolder);
534
}
535
return resources.hasTrailingPathSeparator(directUri) ? resources.addTrailingPathSeparator(path) : path;
536
} else {
537
return directUri;
538
}
539
}
540
541
private async onDidAccept(): Promise<URI | undefined> {
542
this.busy = true;
543
if (!this.updatingPromise && this.filePickBox.activeItems.length === 1) {
544
const item = this.filePickBox.selectedItems[0];
545
if (item.isFolder) {
546
if (this.trailing) {
547
await this.updateItems(item.uri, true, this.trailing);
548
} else {
549
// When possible, cause the update to happen by modifying the input box.
550
// This allows all input box updates to happen first, and uses the same code path as the user typing.
551
const newPath = this.pathFromUri(item.uri);
552
if (startsWithIgnoreCase(newPath, this.filePickBox.value) && (equalsIgnoreCase(item.label, resources.basename(item.uri)))) {
553
this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder).length, this.filePickBox.value.length];
554
this.insertText(newPath, this.basenameWithTrailingSlash(item.uri));
555
} else if ((item.label === '..') && startsWithIgnoreCase(this.filePickBox.value, newPath)) {
556
this.filePickBox.valueSelection = [newPath.length, this.filePickBox.value.length];
557
this.insertText(newPath, '');
558
} else {
559
await this.updateItems(item.uri, true);
560
}
561
}
562
this.filePickBox.busy = false;
563
return;
564
}
565
} else if (!this.updatingPromise) {
566
// If the items have updated, don't try to resolve
567
if ((await this.tryUpdateItems(this.filePickBox.value, this.filePickBoxValue())) !== UpdateResult.NotUpdated) {
568
this.filePickBox.busy = false;
569
return;
570
}
571
}
572
573
let resolveValue: URI | undefined;
574
// Find resolve value
575
if (this.filePickBox.activeItems.length === 0) {
576
resolveValue = this.filePickBoxValue();
577
} else if (this.filePickBox.activeItems.length === 1) {
578
resolveValue = this.filePickBox.selectedItems[0].uri;
579
}
580
if (resolveValue) {
581
resolveValue = this.addPostfix(resolveValue);
582
}
583
if (await this.validate(resolveValue)) {
584
this.busy = false;
585
return resolveValue;
586
}
587
this.busy = false;
588
return undefined;
589
}
590
591
private root(value: URI) {
592
let lastDir = value;
593
let dir = resources.dirname(value);
594
while (!resources.isEqual(lastDir, dir)) {
595
lastDir = dir;
596
dir = resources.dirname(dir);
597
}
598
return dir;
599
}
600
601
private canTildaEscapeHatch(value: string): boolean {
602
return !!(value.endsWith('~') && this.isBadSubpath(value));
603
}
604
605
private tildaReplace(value: string): URI {
606
const home = this.trueHome;
607
if ((value.length > 0) && (value[0] === '~')) {
608
return resources.joinPath(home, value.substring(1));
609
} else if (this.canTildaEscapeHatch(value)) {
610
return home;
611
}
612
return this.remoteUriFrom(value);
613
}
614
615
private tryAddTrailingSeparatorToDirectory(uri: URI, stat: IFileStatWithPartialMetadata): URI {
616
if (stat.isDirectory) {
617
// At this point we know it's a directory and can add the trailing path separator
618
if (!this.endsWithSlash(uri.path)) {
619
return resources.addTrailingPathSeparator(uri);
620
}
621
}
622
return uri;
623
}
624
625
private async tryUpdateItems(value: string, valueUri: URI, reset: boolean = false): Promise<UpdateResult> {
626
if ((value.length > 0) && ((value[0] === '~') || this.canTildaEscapeHatch(value))) {
627
const newDir = this.tildaReplace(value);
628
return await this.updateItems(newDir, true) ? UpdateResult.UpdatedWithTrailing : UpdateResult.Updated;
629
} else if (value === '\\') {
630
valueUri = this.root(this.currentFolder);
631
value = this.pathFromUri(valueUri);
632
return await this.updateItems(valueUri, true) ? UpdateResult.UpdatedWithTrailing : UpdateResult.Updated;
633
} else {
634
const newFolderIsOldFolder = resources.extUriIgnorePathCase.isEqual(this.currentFolder, valueUri);
635
const newFolderIsSubFolder = resources.extUriIgnorePathCase.isEqual(this.currentFolder, resources.dirname(valueUri));
636
const newFolderIsParent = resources.extUriIgnorePathCase.isEqualOrParent(this.currentFolder, resources.dirname(valueUri));
637
const newFolderIsUnrelated = !newFolderIsParent && !newFolderIsSubFolder;
638
if ((!newFolderIsOldFolder && (this.endsWithSlash(value) || newFolderIsParent || newFolderIsUnrelated)) || reset) {
639
let stat: IFileStatWithPartialMetadata | undefined;
640
try {
641
stat = await this.fileService.stat(valueUri);
642
} catch (e) {
643
// do nothing
644
}
645
if (stat?.isDirectory && (resources.basename(valueUri) !== '.') && this.endsWithSlash(value)) {
646
valueUri = this.tryAddTrailingSeparatorToDirectory(valueUri, stat);
647
return await this.updateItems(valueUri) ? UpdateResult.UpdatedWithTrailing : UpdateResult.Updated;
648
} else if (this.endsWithSlash(value)) {
649
// The input box contains a path that doesn't exist on the system.
650
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.badPath', 'The path does not exist. Use ~ to go to your home directory.');
651
// Save this bad path. It can take too long to a stat on every user entered character, but once a user enters a bad path they are likely
652
// to keep typing more bad path. We can compare against this bad path and see if the user entered path starts with it.
653
this.badPath = value;
654
return UpdateResult.InvalidPath;
655
} else {
656
let inputUriDirname = resources.dirname(valueUri);
657
const currentFolderWithoutSep = resources.removeTrailingPathSeparator(resources.addTrailingPathSeparator(this.currentFolder));
658
const inputUriDirnameWithoutSep = resources.removeTrailingPathSeparator(resources.addTrailingPathSeparator(inputUriDirname));
659
if (!resources.extUriIgnorePathCase.isEqual(currentFolderWithoutSep, inputUriDirnameWithoutSep)
660
&& (!/^[a-zA-Z]:$/.test(this.filePickBox.value)
661
|| !equalsIgnoreCase(this.pathFromUri(this.currentFolder).substring(0, this.filePickBox.value.length), this.filePickBox.value))) {
662
let statWithoutTrailing: IFileStatWithPartialMetadata | undefined;
663
try {
664
statWithoutTrailing = await this.fileService.stat(inputUriDirname);
665
} catch (e) {
666
// do nothing
667
}
668
if (statWithoutTrailing?.isDirectory) {
669
this.badPath = undefined;
670
inputUriDirname = this.tryAddTrailingSeparatorToDirectory(inputUriDirname, statWithoutTrailing);
671
return await this.updateItems(inputUriDirname, false, resources.basename(valueUri)) ? UpdateResult.UpdatedWithTrailing : UpdateResult.Updated;
672
}
673
}
674
}
675
}
676
}
677
this.badPath = undefined;
678
return UpdateResult.NotUpdated;
679
}
680
681
private tryUpdateTrailing(value: URI) {
682
const ext = resources.extname(value);
683
if (this.trailing && ext) {
684
this.trailing = resources.basename(value);
685
}
686
}
687
688
private setActiveItems(value: string) {
689
value = this.pathFromUri(this.tildaReplace(value));
690
const asUri = this.remoteUriFrom(value);
691
const inputBasename = resources.basename(asUri);
692
const userPath = this.constructFullUserPath();
693
// Make sure that the folder whose children we are currently viewing matches the path in the input
694
const pathsEqual = equalsIgnoreCase(userPath, value.substring(0, userPath.length)) ||
695
equalsIgnoreCase(value, userPath.substring(0, value.length));
696
if (pathsEqual) {
697
let hasMatch = false;
698
for (let i = 0; i < this.filePickBox.items.length; i++) {
699
const item = <FileQuickPickItem>this.filePickBox.items[i];
700
if (this.setAutoComplete(value, inputBasename, item)) {
701
hasMatch = true;
702
break;
703
}
704
}
705
if (!hasMatch) {
706
const userBasename = inputBasename.length >= 2 ? userPath.substring(userPath.length - inputBasename.length + 2) : '';
707
this.userEnteredPathSegment = (userBasename === inputBasename) ? inputBasename : '';
708
this.autoCompletePathSegment = '';
709
this.filePickBox.activeItems = [];
710
this.tryUpdateTrailing(asUri);
711
}
712
} else {
713
this.userEnteredPathSegment = inputBasename;
714
this.autoCompletePathSegment = '';
715
this.filePickBox.activeItems = [];
716
this.tryUpdateTrailing(asUri);
717
}
718
}
719
720
private setAutoComplete(startingValue: string, startingBasename: string, quickPickItem: FileQuickPickItem, force: boolean = false): boolean {
721
if (this.busy) {
722
// We're in the middle of something else. Doing an auto complete now can result jumbled or incorrect autocompletes.
723
this.userEnteredPathSegment = startingBasename;
724
this.autoCompletePathSegment = '';
725
return false;
726
}
727
const itemBasename = quickPickItem.label;
728
// Either force the autocomplete, or the old value should be one smaller than the new value and match the new value.
729
if (itemBasename === '..') {
730
// Don't match on the up directory item ever.
731
this.userEnteredPathSegment = '';
732
this.autoCompletePathSegment = '';
733
this.activeItem = quickPickItem;
734
if (force) {
735
// clear any selected text
736
getActiveDocument().execCommand('insertText', false, '');
737
}
738
return false;
739
} else if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) {
740
this.userEnteredPathSegment = startingBasename;
741
this.activeItem = quickPickItem;
742
// Changing the active items will trigger the onDidActiveItemsChanged. Clear the autocomplete first, then set it after.
743
this.autoCompletePathSegment = '';
744
if (quickPickItem.isFolder || !this.trailing) {
745
this.filePickBox.activeItems = [quickPickItem];
746
} else {
747
this.filePickBox.activeItems = [];
748
}
749
return true;
750
} else if (force && (!equalsIgnoreCase(this.basenameWithTrailingSlash(quickPickItem.uri), (this.userEnteredPathSegment + this.autoCompletePathSegment)))) {
751
this.userEnteredPathSegment = '';
752
if (!this.accessibilityService.isScreenReaderOptimized()) {
753
this.autoCompletePathSegment = this.trimTrailingSlash(itemBasename);
754
}
755
this.activeItem = quickPickItem;
756
if (!this.accessibilityService.isScreenReaderOptimized()) {
757
this.filePickBox.valueSelection = [this.pathFromUri(this.currentFolder, true).length, this.filePickBox.value.length];
758
// use insert text to preserve undo buffer
759
this.insertText(this.pathAppend(this.currentFolder, this.autoCompletePathSegment), this.autoCompletePathSegment);
760
this.filePickBox.valueSelection = [this.filePickBox.value.length - this.autoCompletePathSegment.length, this.filePickBox.value.length];
761
}
762
return true;
763
} else {
764
this.userEnteredPathSegment = startingBasename;
765
this.autoCompletePathSegment = '';
766
return false;
767
}
768
}
769
770
private insertText(wholeValue: string, insertText: string) {
771
if (this.filePickBox.inputHasFocus()) {
772
getActiveDocument().execCommand('insertText', false, insertText);
773
if (this.filePickBox.value !== wholeValue) {
774
this.filePickBox.value = wholeValue;
775
this.handleValueChange(wholeValue);
776
}
777
} else {
778
this.filePickBox.value = wholeValue;
779
this.handleValueChange(wholeValue);
780
}
781
}
782
783
private addPostfix(uri: URI): URI {
784
let result = uri;
785
if (this.requiresTrailing && this.options.filters && this.options.filters.length > 0 && !resources.hasTrailingPathSeparator(uri)) {
786
// Make sure that the suffix is added. If the user deleted it, we automatically add it here
787
let hasExt: boolean = false;
788
const currentExt = resources.extname(uri).substr(1);
789
for (let i = 0; i < this.options.filters.length; i++) {
790
for (let j = 0; j < this.options.filters[i].extensions.length; j++) {
791
if ((this.options.filters[i].extensions[j] === '*') || (this.options.filters[i].extensions[j] === currentExt)) {
792
hasExt = true;
793
break;
794
}
795
}
796
if (hasExt) {
797
break;
798
}
799
}
800
if (!hasExt) {
801
result = resources.joinPath(resources.dirname(uri), resources.basename(uri) + '.' + this.options.filters[0].extensions[0]);
802
}
803
}
804
return result;
805
}
806
807
private trimTrailingSlash(path: string): string {
808
return ((path.length > 1) && this.endsWithSlash(path)) ? path.substr(0, path.length - 1) : path;
809
}
810
811
private yesNoPrompt(uri: URI, message: string): Promise<boolean> {
812
interface YesNoItem extends IQuickPickItem {
813
value: boolean;
814
}
815
const disposableStore = new DisposableStore();
816
const prompt = disposableStore.add(this.quickInputService.createQuickPick<YesNoItem>());
817
prompt.title = message;
818
prompt.ignoreFocusOut = true;
819
prompt.ok = true;
820
prompt.customButton = true;
821
prompt.customLabel = nls.localize('remoteFileDialog.cancel', 'Cancel');
822
prompt.customButtonSecondary = true;
823
prompt.value = this.pathFromUri(uri);
824
825
let isResolving = false;
826
return new Promise<boolean>(resolve => {
827
disposableStore.add(prompt.onDidAccept(() => {
828
isResolving = true;
829
prompt.hide();
830
resolve(true);
831
}));
832
disposableStore.add(prompt.onDidHide(() => {
833
if (!isResolving) {
834
resolve(false);
835
}
836
this.filePickBox.show();
837
this.hidden = false;
838
disposableStore.dispose();
839
}));
840
disposableStore.add(prompt.onDidChangeValue(() => {
841
prompt.hide();
842
}));
843
disposableStore.add(prompt.onDidCustom(() => {
844
prompt.hide();
845
}));
846
prompt.show();
847
});
848
}
849
850
private async validate(uri: URI | undefined): Promise<boolean> {
851
if (uri === undefined) {
852
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.invalidPath', 'Please enter a valid path.');
853
return Promise.resolve(false);
854
}
855
856
let stat: IFileStatWithPartialMetadata | undefined;
857
let statDirname: IFileStatWithPartialMetadata | undefined;
858
try {
859
statDirname = await this.fileService.stat(resources.dirname(uri));
860
stat = await this.fileService.stat(uri);
861
} catch (e) {
862
// do nothing
863
}
864
865
if (this.requiresTrailing) { // save
866
if (stat?.isDirectory) {
867
// Can't do this
868
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolder', 'The folder already exists. Please use a new file name.');
869
return Promise.resolve(false);
870
} else if (stat) {
871
// Replacing a file.
872
// Show a yes/no prompt
873
const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri));
874
return this.yesNoPrompt(uri, message);
875
} else if (!(isValidBasename(resources.basename(uri), this.isWindows))) {
876
// Filename not allowed
877
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.');
878
return Promise.resolve(false);
879
} else if (!statDirname) {
880
// Folder to save in doesn't exist
881
const message = nls.localize('remoteFileDialog.validateCreateDirectory', 'The folder {0} does not exist. Would you like to create it?', resources.basename(resources.dirname(uri)));
882
return this.yesNoPrompt(uri, message);
883
} else if (!statDirname.isDirectory) {
884
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.');
885
return Promise.resolve(false);
886
} else if (statDirname.readonly) {
887
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateReadonlyFolder', 'This folder cannot be used as a save destination. Please choose another folder');
888
return Promise.resolve(false);
889
}
890
} else { // open
891
if (!stat) {
892
// File or folder doesn't exist
893
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateNonexistentDir', 'Please enter a path that exists.');
894
return Promise.resolve(false);
895
} else if (uri.path === '/' && this.isWindows) {
896
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.windowsDriveLetter', 'Please start the path with a drive letter.');
897
return Promise.resolve(false);
898
} else if (stat.isDirectory && !this.allowFolderSelection) {
899
// Folder selected when folder selection not permitted
900
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFileOnly', 'Please select a file.');
901
return Promise.resolve(false);
902
} else if (!stat.isDirectory && !this.allowFileSelection) {
903
// File selected when file selection not permitted
904
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateFolderOnly', 'Please select a folder.');
905
return Promise.resolve(false);
906
}
907
}
908
return Promise.resolve(true);
909
}
910
911
// Returns true if there is a file at the end of the URI.
912
private async updateItems(newFolder: URI, force: boolean = false, trailing?: string): Promise<boolean> {
913
this.busy = true;
914
this.autoCompletePathSegment = '';
915
const wasDotDot = trailing === '..';
916
trailing = wasDotDot ? undefined : trailing;
917
const isSave = !!trailing;
918
let result = false;
919
920
const updatingPromise = createCancelablePromise(async token => {
921
let folderStat: IFileStat | undefined;
922
try {
923
folderStat = await this.fileService.resolve(newFolder);
924
if (!folderStat.isDirectory) {
925
trailing = resources.basename(newFolder);
926
newFolder = resources.dirname(newFolder);
927
folderStat = undefined;
928
result = true;
929
}
930
} catch (e) {
931
// The file/directory doesn't exist
932
}
933
const newValue = trailing ? this.pathAppend(newFolder, trailing) : this.pathFromUri(newFolder, true);
934
this.currentFolder = this.endsWithSlash(newFolder.path) ? newFolder : resources.addTrailingPathSeparator(newFolder, this.separator);
935
this.userEnteredPathSegment = trailing ? trailing : '';
936
937
return this.createItems(folderStat, this.currentFolder, token).then(items => {
938
if (token.isCancellationRequested) {
939
this.busy = false;
940
return false;
941
}
942
943
this.filePickBox.itemActivation = ItemActivation.NONE;
944
this.filePickBox.items = items;
945
946
// the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory.
947
if (!equalsIgnoreCase(this.filePickBox.value, newValue) && (force || wasDotDot)) {
948
this.filePickBox.valueSelection = [0, this.filePickBox.value.length];
949
this.insertText(newValue, newValue);
950
}
951
if (force && trailing && isSave) {
952
// Keep the cursor position in front of the save as name.
953
this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - trailing.length];
954
} else if (!trailing) {
955
// If there is trailing, we don't move the cursor. If there is no trailing, cursor goes at the end.
956
this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length];
957
}
958
this.busy = false;
959
this.updatingPromise = undefined;
960
return result;
961
});
962
});
963
964
if (this.updatingPromise !== undefined) {
965
this.updatingPromise.cancel();
966
}
967
this.updatingPromise = updatingPromise;
968
969
return updatingPromise;
970
}
971
972
private pathFromUri(uri: URI, endWithSeparator: boolean = false): string {
973
let result: string = normalizeDriveLetter(uri.fsPath, this.isWindows).replace(/\n/g, '');
974
if (this.separator === '/') {
975
result = result.replace(/\\/g, this.separator);
976
} else {
977
result = result.replace(/\//g, this.separator);
978
}
979
if (endWithSeparator && !this.endsWithSlash(result)) {
980
result = result + this.separator;
981
}
982
return result;
983
}
984
985
private pathAppend(uri: URI, additional: string): string {
986
if ((additional === '..') || (additional === '.')) {
987
const basePath = this.pathFromUri(uri, true);
988
return basePath + additional;
989
} else {
990
return this.pathFromUri(resources.joinPath(uri, additional));
991
}
992
}
993
994
private async checkIsWindowsOS(): Promise<boolean> {
995
let isWindowsOS = isWindows;
996
const env = await this.getRemoteAgentEnvironment();
997
if (env) {
998
isWindowsOS = env.os === OperatingSystem.Windows;
999
}
1000
return isWindowsOS;
1001
}
1002
1003
private endsWithSlash(s: string) {
1004
return /[\/\\]$/.test(s);
1005
}
1006
1007
private basenameWithTrailingSlash(fullPath: URI): string {
1008
const child = this.pathFromUri(fullPath, true);
1009
const parent = this.pathFromUri(resources.dirname(fullPath), true);
1010
return child.substring(parent.length);
1011
}
1012
1013
private async createBackItem(currFolder: URI): Promise<FileQuickPickItem | undefined> {
1014
const fileRepresentationCurr = this.currentFolder.with({ scheme: Schemas.file, authority: '' });
1015
const fileRepresentationParent = resources.dirname(fileRepresentationCurr);
1016
if (!resources.isEqual(fileRepresentationCurr, fileRepresentationParent)) {
1017
const parentFolder = resources.dirname(currFolder);
1018
if (await this.fileService.exists(parentFolder)) {
1019
return { label: '..', uri: resources.addTrailingPathSeparator(parentFolder, this.separator), isFolder: true };
1020
}
1021
}
1022
return undefined;
1023
}
1024
1025
private async createItems(folder: IFileStat | undefined, currentFolder: URI, token: CancellationToken): Promise<FileQuickPickItem[]> {
1026
const result: FileQuickPickItem[] = [];
1027
1028
const backDir = await this.createBackItem(currentFolder);
1029
try {
1030
if (!folder) {
1031
folder = await this.fileService.resolve(currentFolder);
1032
}
1033
const filteredChildren = this._showDotFiles ? folder.children : folder.children?.filter(child => !child.name.startsWith('.'));
1034
const items = filteredChildren ? await Promise.all(filteredChildren.map(child => this.createItem(child, currentFolder, token))) : [];
1035
for (const item of items) {
1036
if (item) {
1037
result.push(item);
1038
}
1039
}
1040
} catch (e) {
1041
// ignore
1042
console.log(e);
1043
}
1044
if (token.isCancellationRequested) {
1045
return [];
1046
}
1047
const sorted = result.sort((i1, i2) => {
1048
if (i1.isFolder !== i2.isFolder) {
1049
return i1.isFolder ? -1 : 1;
1050
}
1051
const trimmed1 = this.endsWithSlash(i1.label) ? i1.label.substr(0, i1.label.length - 1) : i1.label;
1052
const trimmed2 = this.endsWithSlash(i2.label) ? i2.label.substr(0, i2.label.length - 1) : i2.label;
1053
return trimmed1.localeCompare(trimmed2);
1054
});
1055
1056
if (backDir) {
1057
sorted.unshift(backDir);
1058
}
1059
return sorted;
1060
}
1061
1062
private filterFile(file: URI): boolean {
1063
if (this.options.filters) {
1064
for (let i = 0; i < this.options.filters.length; i++) {
1065
for (let j = 0; j < this.options.filters[i].extensions.length; j++) {
1066
const testExt = this.options.filters[i].extensions[j];
1067
if ((testExt === '*') || (file.path.endsWith('.' + testExt))) {
1068
return true;
1069
}
1070
}
1071
}
1072
return false;
1073
}
1074
return true;
1075
}
1076
1077
private async createItem(stat: IFileStat, parent: URI, token: CancellationToken): Promise<FileQuickPickItem | undefined> {
1078
if (token.isCancellationRequested) {
1079
return undefined;
1080
}
1081
let fullPath = resources.joinPath(parent, stat.name);
1082
if (stat.isDirectory) {
1083
const filename = resources.basename(fullPath);
1084
fullPath = resources.addTrailingPathSeparator(fullPath, this.separator);
1085
return { label: filename, uri: fullPath, isFolder: true, iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined, FileKind.FOLDER) };
1086
} else if (!stat.isDirectory && this.allowFileSelection && this.filterFile(fullPath)) {
1087
return { label: stat.name, uri: fullPath, isFolder: false, iconClasses: getIconClasses(this.modelService, this.languageService, fullPath || undefined) };
1088
}
1089
return undefined;
1090
}
1091
}
1092
1093