diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 26c88f7e3b0..73c8e05549d 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -5,7 +5,6 @@ import { basename, posix, extname } from 'vs/base/common/path'; import { startsWithUTF8BOM } from 'vs/base/common/strings'; -import { coalesce } from 'vs/base/common/arrays'; import { match } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; @@ -247,34 +246,6 @@ export function isUnspecific(mime: string[] | string): boolean { return mime.length === 1 && isUnspecific(mime[0]); } -/** - * Returns a suggestion for the filename by the following logic: - * 1. If a relevant extension exists and is an actual filename extension (starting with a dot), suggest the prefix appended by the first one. - * 2. Otherwise, if there are other extensions, suggest the first one. - * 3. Otherwise, suggest the prefix. - */ -export function suggestFilename(mode: string | undefined, prefix: string): string { - const extensions = registeredAssociations - .filter(assoc => !assoc.userConfigured && assoc.extension && assoc.id === mode) - .map(assoc => assoc.extension); - - const extensionsWithDotFirst = coalesce(extensions) - .filter(assoc => assoc.startsWith('.')); - - if (extensionsWithDotFirst.length > 0) { - const candidateExtension = extensionsWithDotFirst[0]; - if (prefix.endsWith(candidateExtension)) { - // do not add the prefix if it already exists - // https://github.com/microsoft/vscode/issues/83603 - return prefix; - } - - return prefix + candidateExtension; - } - - return extensions[0] || prefix; -} - interface MapExtToMediaMimes { [index: string]: string; } diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index 3d163580a6d..a7ac5a177a6 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { guessMimeTypes, registerTextMime, suggestFilename } from 'vs/base/common/mime'; +import { guessMimeTypes, registerTextMime } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; suite('Mime', () => { @@ -126,53 +126,4 @@ suite('Mime', () => { assert.deepEqual(guessMimeTypes(URI.parse(`data:;label:something.data;description:data,`)), ['text/data', 'text/plain']); }); - - test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => { - const id = 'plumbus0'; - const mime = `text/${id}`; - for (let extension of ['one', 'two']) { - registerTextMime({ id, mime, extension }); - } - - let suggested = suggestFilename('shleem', 'Untitled-1'); - assert.equal(suggested, 'Untitled-1'); - }); - - test('Filename Suggestion - Suggest prefix with first extension that begins with a dot', () => { - const id = 'plumbus1'; - const mime = `text/${id}`; - for (let extension of ['plumbus', '.shleem', '.gazorpazorp']) { - registerTextMime({ id, mime, extension }); - } - - let suggested = suggestFilename('plumbus1', 'Untitled-1'); - assert.equal(suggested, 'Untitled-1.shleem'); - }); - - test('Filename Suggestion - Suggest first relevant extension when there are none that begin with a dot', () => { - const id = 'plumbus2'; - const mime = `text/${id}`; - for (let extension of ['plumbus', 'shleem', 'gazorpazorp']) { - registerTextMime({ id, mime, extension }); - } - - let suggested = suggestFilename('plumbus2', 'Untitled-1'); - assert.equal(suggested, 'plumbus'); - }); - - test('Filename Suggestion - Should ignore user-configured associations', () => { - registerTextMime({ id: 'plumbus3', mime: 'text/plumbus3', extension: 'plumbus', userConfigured: true }); - registerTextMime({ id: 'plumbus3', mime: 'text/plumbus3', extension: '.shleem', userConfigured: true }); - registerTextMime({ id: 'plumbus3', mime: 'text/plumbus3', extension: '.gazorpazorp', userConfigured: false }); - - let suggested = suggestFilename('plumbus3', 'Untitled-1'); - assert.equal(suggested, 'Untitled-1.gazorpazorp'); - - registerTextMime({ id: 'plumbus4', mime: 'text/plumbus4', extension: 'plumbus', userConfigured: true }); - registerTextMime({ id: 'plumbus4', mime: 'text/plumbus4', extension: '.shleem', userConfigured: true }); - registerTextMime({ id: 'plumbus4', mime: 'text/plumbus4', extension: '.gazorpazorp', userConfigured: true }); - - suggested = suggestFilename('plumbus4', 'Untitled-1'); - assert.equal(suggested, 'Untitled-1'); - }); }); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 096c5d06355..9fe141b27b7 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -28,7 +28,6 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { suggestFilename } from 'vs/base/common/mime'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { isValidBasename } from 'vs/base/common/extpath'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -38,6 +37,7 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, UTF8_BOM, detectEncodingByBOMFromBuffer, toEncodeReadable, toDecodeStream, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding'; import { consumeStream } from 'vs/base/common/stream'; +import { IModeService } from 'vs/editor/common/services/modeService'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -65,7 +65,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IPathService private readonly pathService: IPathService, @IWorkingCopyFileService private readonly workingCopyFileService: IWorkingCopyFileService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IModeService private readonly modeService: IModeService ) { super(); @@ -442,8 +443,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex // Add mode file extension if specified const mode = model.getMode(); - if (mode !== PLAINTEXT_MODE_ID) { - suggestedFilename = suggestFilename(mode, untitledName); + if (mode && mode !== PLAINTEXT_MODE_ID) { + suggestedFilename = this.suggestFilename(mode, untitledName); } else { suggestedFilename = untitledName; } @@ -460,6 +461,17 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return joinPath(this.fileDialogService.defaultFilePath() || (await this.pathService.userHome()), suggestedFilename); } + suggestFilename(mode: string, untitledName: string) { + const extension = this.modeService.getExtensions(mode)[0]; + if (extension) { + if (!untitledName.endsWith(extension)) { + return untitledName + extension; + } + } + const filename = this.modeService.getFilenames(mode)[0]; + return filename || untitledName; + } + //#endregion //#region revert diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 81eb8b7192f..05dfa205077 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -31,6 +31,7 @@ import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/commo import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { ILogService } from 'vs/platform/log/common/log'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IModeService } from 'vs/editor/common/services/modeService'; export class NativeTextFileService extends AbstractTextFileService { @@ -51,9 +52,10 @@ export class NativeTextFileService extends AbstractTextFileService { @IPathService pathService: IPathService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ILogService private readonly logService: ILogService, - @IUriIdentityService uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModeService modeService: IModeService ) { - super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, pathService, workingCopyFileService, uriIdentityService); + super(fileService, untitledTextEditorService, lifecycleService, instantiationService, modelService, environmentService, dialogService, fileDialogService, textResourceConfigurationService, filesConfigurationService, textModelService, codeEditorService, pathService, workingCopyFileService, uriIdentityService, modeService); } async read(resource: URI, options?: IReadTextFileOptions): Promise { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts index 7c9894cf985..8507b7f887d 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -9,6 +9,7 @@ import { toResource } from 'vs/base/test/common/utils'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { FileOperation } from 'vs/platform/files/common/files'; +import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; suite('Files - TextFileService', () => { @@ -132,4 +133,36 @@ suite('Files - TextFileService', () => { disposable1.dispose(); disposable2.dispose(); }); + + test('Filename Suggestion - Suggest prefix only when there are no relevant extensions', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus0', + extensions: ['.one', '.two'] + }); + + let suggested = accessor.textFileService.suggestFilename('shleem', 'Untitled-1'); + assert.equal(suggested, 'Untitled-1'); + }); + + test('Filename Suggestion - Suggest prefix with first extension', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus1', + extensions: ['.shleem', '.gazorpazorp'], + filenames: ['plumbus'] + }); + + let suggested = accessor.textFileService.suggestFilename('plumbus1', 'Untitled-1'); + assert.equal(suggested, 'Untitled-1.shleem'); + }); + + test('Filename Suggestion - Suggest filename if there are no extensions', () => { + ModesRegistry.registerLanguage({ + id: 'plumbus2', + filenames: ['plumbus', 'shleem', 'gazorpazorp'] + }); + + let suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1'); + assert.equal(suggested, 'plumbus'); + }); + }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index ffe65cfc0fa..2bade794d32 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -230,7 +230,8 @@ export class TestTextFileService extends BrowserTextFileService { @ICodeEditorService codeEditorService: ICodeEditorService, @IPathService pathService: IPathService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, - @IUriIdentityService uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModeService modeService: IModeService ) { super( fileService, @@ -247,7 +248,8 @@ export class TestTextFileService extends BrowserTextFileService { codeEditorService, pathService, workingCopyFileService, - uriIdentityService + uriIdentityService, + modeService ); } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 3f735b267d3..5e391bb92b9 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -41,6 +41,7 @@ import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; +import { IModeService } from 'vs/editor/common/services/modeService'; export const TestWindowConfiguration: INativeWindowConfiguration = { windowId: 0, @@ -78,7 +79,8 @@ export class TestTextFileService extends NativeTextFileService { @IPathService athService: IPathService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ILogService logService: ILogService, - @IUriIdentityService uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IModeService modeService: IModeService ) { super( fileService, @@ -97,7 +99,8 @@ export class TestTextFileService extends NativeTextFileService { athService, workingCopyFileService, logService, - uriIdentityService + uriIdentityService, + modeService ); }