提交 6c331386 编写于 作者: B Benjamin Pasero

text files - make save dialog reusable

上级 2c7656f0
......@@ -232,7 +232,7 @@ export interface IFileDialogService {
/**
* Shows a save file dialog and save the file at the chosen file URI.
*/
pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined>;
pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
/**
* Shows a save file dialog and returns the chosen file URI.
......
......@@ -163,15 +163,12 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput {
return await super.resolve();
}
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise<URI | undefined> {
private async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
// Help user to find a name for the file by opening it first
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } });
return this.fileDialogService.pickFileToSave({
availableFileSystems,
defaultUri
});
return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems);
}
public handleMove(groupId: GroupIdentifier, uri: URI, options?: ITextEditorOptions): IEditorInput | undefined {
......
......@@ -157,7 +157,7 @@ export class SearchEditorInput extends EditorInput {
async save(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise<boolean> {
if (this.resource.scheme === 'search-editor') {
const path = await this.promptForPath(this.resource, this.suggestFileName());
const path = await this.promptForPath(this.resource, this.suggestFileName(), options?.availableFileSystems);
if (path) {
if (await this.textFileService.saveAs(this.resource, path, options)) {
this.setDirty(false);
......@@ -179,13 +179,10 @@ export class SearchEditorInput extends EditorInput {
// Brining this over from textFileService because it only suggests for untitled scheme.
// In the future I may just use the untitled scheme. I dont get particular benefit from using search-editor...
private async promptForPath(resource: URI, defaultUri: URI): Promise<URI | undefined> {
private async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
// Help user to find a name for the file by opening it first
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } });
return this.fileDialogService.pickFileToSave({
defaultUri,
title: localize('saveAsTitle', "Save As"),
});
return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems);
}
getTypeId(): string {
......
......@@ -21,6 +21,9 @@ import { IFileService } from 'vs/platform/files/common/files';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import Severity from 'vs/base/common/severity';
import { coalesce } from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
import { IModeService } from 'vs/editor/common/services/modeService';
export abstract class AbstractFileDialogService implements IFileDialogService {
......@@ -35,7 +38,8 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
@IConfigurationService protected readonly configurationService: IConfigurationService,
@IFileService protected readonly fileService: IFileService,
@IOpenerService protected readonly openerService: IOpenerService,
@IDialogService private readonly dialogService: IDialogService
@IDialogService private readonly dialogService: IDialogService,
@IModeService private readonly modeService: IModeService
) { }
defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
......@@ -222,7 +226,56 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined>;
abstract showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
abstract showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
abstract pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined>;
protected getPickFileToSaveDialogOptions(defaultUri: URI, availableFileSystems?: string[]): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As"),
availableFileSystems,
};
interface IFilter { name: string; extensions: string[]; }
// Build the file filter by using our known languages
const ext: string | undefined = defaultUri ? resources.extname(defaultUri) : undefined;
let matchingFilter: IFilter | undefined;
const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => {
const extensions = this.modeService.getExtensions(languageName);
if (!extensions || !extensions.length) {
return null;
}
const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) };
if (ext && extensions.indexOf(ext) >= 0) {
matchingFilter = filter;
return null; // matching filter will be added last to the top
}
return filter;
}));
// Filters are a bit weird on Windows, based on having a match or not:
// Match: we put the matching filter first so that it shows up selected and the all files last
// No match: we put the all files filter first
const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
if (matchingFilter) {
filters.unshift(matchingFilter);
filters.unshift(allFilesFilter);
} else {
filters.unshift(allFilesFilter);
}
// Allow to save file without extension
filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });
options.filters = filters;
return options;
}
}
......@@ -51,9 +51,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
return this.pickWorkspaceAndOpenSimplified(schema, options);
}
async pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined> {
const schema = this.getFileSystemSchema(options);
return this.pickFileToSaveSimplified(schema, options);
async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems });
return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems));
}
async showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined> {
......
......@@ -19,6 +19,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService';
import { Schemas } from 'vs/base/common/network';
import { IModeService } from 'vs/editor/common/services/modeService';
export class FileDialogService extends AbstractFileDialogService implements IFileDialogService {
......@@ -34,9 +35,10 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
@IFileService fileService: IFileService,
@IOpenerService openerService: IOpenerService,
@IElectronService private readonly electronService: IElectronService,
@IDialogService dialogService: IDialogService
@IDialogService dialogService: IDialogService,
@IModeService modeService: IModeService
) {
super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService);
super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService, modeService);
}
private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions {
......@@ -107,8 +109,9 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
return this.electronService.pickWorkspaceAndOpen(this.toNativeOpenDialogOptions(options));
}
async pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined> {
const schema = this.getFileSystemSchema(options);
async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems });
const options = this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems);
if (this.shouldUseSimplified(schema).useSimplified) {
return this.pickFileToSaveSimplified(schema, options);
} else {
......
......@@ -6,7 +6,6 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import { Emitter, AsyncEmitter } from 'vs/base/common/event';
import * as platform from 'vs/base/common/platform';
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModel, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
......@@ -22,12 +21,9 @@ import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
import { IModeService } from 'vs/editor/common/services/modeService';
import { isEqualOrParent, isEqual, joinPath, dirname, basename, toLocalResource } from 'vs/base/common/resources';
import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { coalesce } from 'vs/base/common/arrays';
import { trim } from 'vs/base/common/strings';
import { VSBuffer } from 'vs/base/common/buffer';
import { ITextSnapshot, ITextModel } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
......@@ -68,7 +64,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@ILifecycleService protected readonly lifecycleService: ILifecycleService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
@IHistoryService private readonly historyService: IHistoryService,
......@@ -311,7 +306,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Otherwise ask user
else {
targetUri = await this.promptForPath(resource, this.suggestFileName(resource));
targetUri = await this.promptForPath(resource, this.suggestFilePath(resource));
}
// Save as if target provided
......@@ -338,65 +333,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return false;
}
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: readonly string[]): Promise<URI | undefined> {
protected async promptForPath(resource: URI, defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
// Help user to find a name for the file by opening it first
await this.editorService.openEditor({ resource, options: { revealIfOpened: true, preserveFocus: true } });
return this.fileDialogService.pickFileToSave(this.getSaveDialogOptions(defaultUri, availableFileSystems));
}
private getSaveDialogOptions(defaultUri: URI, availableFileSystems?: readonly string[]): ISaveDialogOptions {
const options: ISaveDialogOptions = {
defaultUri,
title: nls.localize('saveAsTitle', "Save As"),
availableFileSystems,
};
// Filters are only enabled on Windows where they work properly
if (!platform.isWindows) {
return options;
}
interface IFilter { name: string; extensions: string[]; }
// Build the file filter by using our known languages
const ext: string | undefined = defaultUri ? extname(defaultUri) : undefined;
let matchingFilter: IFilter | undefined;
const filters: IFilter[] = coalesce(this.modeService.getRegisteredLanguageNames().map(languageName => {
const extensions = this.modeService.getExtensions(languageName);
if (!extensions || !extensions.length) {
return null;
}
const filter: IFilter = { name: languageName, extensions: extensions.slice(0, 10).map(e => trim(e, '.')) };
if (ext && extensions.indexOf(ext) >= 0) {
matchingFilter = filter;
return null; // matching filter will be added last to the top
}
return filter;
}));
// Filters are a bit weird on Windows, based on having a match or not:
// Match: we put the matching filter first so that it shows up selected and the all files last
// No match: we put the all files filter first
const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
if (matchingFilter) {
filters.unshift(matchingFilter);
filters.unshift(allFilesFilter);
} else {
filters.unshift(allFilesFilter);
}
// Allow to save file without extension
filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });
options.filters = filters;
return options;
return this.fileDialogService.pickFileToSave(defaultUri, availableFileSystems);
}
private getFileModels(arg1?: URI | URI[]): ITextFileEditorModel[] {
......@@ -422,7 +364,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
if (!target) {
let dialogPath = source;
if (source.scheme === Schemas.untitled) {
dialogPath = this.suggestFileName(source);
dialogPath = this.suggestFilePath(source);
}
target = await this.promptForPath(source, dialogPath, options ? options.availableFileSystems : undefined);
......@@ -598,7 +540,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return (await this.dialogService.confirm(confirm)).confirmed;
}
private suggestFileName(untitledResource: URI): URI {
private suggestFilePath(untitledResource: URI): URI {
const untitledFileName = this.untitled.get(untitledResource)?.suggestFileName() ?? basename(untitledResource);
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
......
......@@ -30,7 +30,6 @@ import { nodeReadableToString, streamToNodeReadable, nodeStreamToVSBufferReadabl
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
......@@ -47,7 +46,6 @@ export class NativeTextFileService extends AbstractTextFileService {
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IHistoryService historyService: IHistoryService,
......@@ -59,7 +57,7 @@ export class NativeTextFileService extends AbstractTextFileService {
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService,
@ITextModelService textModelService: ITextModelService
) {
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService);
super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService, textModelService);
}
private _encoding: EncodingOracle | undefined;
......
......@@ -198,7 +198,6 @@ export class TestTextFileService extends NativeTextFileService {
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IHistoryService historyService: IHistoryService,
......@@ -215,7 +214,6 @@ export class TestTextFileService extends NativeTextFileService {
untitledTextEditorService,
lifecycleService,
instantiationService,
modeService,
modelService,
environmentService,
historyService,
......@@ -413,7 +411,7 @@ export class TestFileDialogService implements IFileDialogService {
pickWorkspaceAndOpen(_options: IPickAndOpenOptions): Promise<any> {
return Promise.resolve(0);
}
pickFileToSave(_options: ISaveDialogOptions): Promise<URI | undefined> {
pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise<URI | undefined> {
return Promise.resolve(undefined);
}
showSaveDialog(_options: ISaveDialogOptions): Promise<URI | undefined> {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册