diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index ba53fb83171..d54a646484a 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -10,6 +10,7 @@ import { IRevertOptions, SaveSourceRegistry } from 'vs/workbench/common/editor'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, IFileStreamContent } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; +import { extname as pathExtname } from 'vs/base/common/path'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IUntitledTextEditorService, IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; @@ -43,6 +44,7 @@ import { Emitter } from 'vs/base/common/event'; import { Codicon } from 'vs/base/common/codicons'; import { listErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { firstOrDefault } from 'vs/base/common/arrays'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -576,19 +578,16 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } // Untitled without associated file path: use name - // of untitled model if it is a valid path name, - // otherwise fallback to `basename`. - let untitledName = model.name; - if (!(await this.pathService.hasValidBasename(joinPath(defaultFilePath, untitledName), untitledName))) { - untitledName = basename(resource); - } + // of untitled model if it is a valid path name and + // figure out the file extension from the mode if any. - // Add language file extension if specified - const languageId = model.getLanguageId(); - if (languageId && languageId !== PLAINTEXT_LANGUAGE_ID) { - suggestedFilename = this.suggestFilename(languageId, untitledName); - } else { - suggestedFilename = untitledName; + if (await this.pathService.hasValidBasename(joinPath(defaultFilePath, model.name), model.name)) { + const languageId = model.getLanguageId(); + if (languageId && languageId !== PLAINTEXT_LANGUAGE_ID) { + suggestedFilename = this.suggestFilename(languageId, model.name); + } else { + suggestedFilename = model.name; + } } } } @@ -606,18 +605,31 @@ export abstract class AbstractTextFileService extends Disposable implements ITex suggestFilename(languageId: string, untitledName: string) { const languageName = this.languageService.getLanguageName(languageId); if (!languageName) { - return untitledName; + return untitledName; // unknown language, so we cannot suggest a better name } - const extension = this.languageService.getExtensions(languageId)[0]; - if (extension) { - if (!untitledName.endsWith(extension)) { - return untitledName + extension; + const untitledExtension = pathExtname(untitledName); + + const extensions = this.languageService.getExtensions(languageId); + if (extensions.includes(untitledExtension)) { + return untitledName; // preserve extension if it is compatible with the mode + } + + const primaryExtension = firstOrDefault(extensions); + if (primaryExtension) { + if (untitledExtension) { + return `${untitledName.substring(0, untitledName.indexOf(untitledExtension))}${primaryExtension}`; } + + return `${untitledName}${primaryExtension}`; } - const filename = this.languageService.getFilenames(languageId)[0]; - return filename || untitledName; + const filenames = this.languageService.getFilenames(languageId); + if (filenames.includes(untitledName)) { + return untitledName; // preserve name if it is compatible with the mode + } + + return firstOrDefault(filenames) ?? untitledName; } //#endregion 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 ad1be2b0dad..45bd9ba71fd 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileService.test.ts @@ -160,6 +160,28 @@ suite('Files - TextFileService', () => { registration.dispose(); }); + test('Filename Suggestion - Preserve extension if it matchers', () => { + const registration = accessor.languageService.registerLanguage({ + id: 'plumbus2', + extensions: ['.shleem', '.gazorpazorp'], + }); + + const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1.gazorpazorp'); + assert.strictEqual(suggested, 'Untitled-1.gazorpazorp'); + registration.dispose(); + }); + + test('Filename Suggestion - Rewrite extension according to language', () => { + const registration = accessor.languageService.registerLanguage({ + id: 'plumbus2', + extensions: ['.shleem', '.gazorpazorp'], + }); + + const suggested = accessor.textFileService.suggestFilename('plumbus2', 'Untitled-1.foobar'); + assert.strictEqual(suggested, 'Untitled-1.shleem'); + registration.dispose(); + }); + test('Filename Suggestion - Suggest filename if there are no extensions', () => { const registration = accessor.languageService.registerLanguage({ id: 'plumbus2', @@ -170,4 +192,26 @@ suite('Files - TextFileService', () => { assert.strictEqual(suggested, 'plumbus'); registration.dispose(); }); + + test('Filename Suggestion - Preserve filename if it matches', () => { + const registration = accessor.languageService.registerLanguage({ + id: 'plumbus2', + filenames: ['plumbus', 'shleem', 'gazorpazorp'] + }); + + const suggested = accessor.textFileService.suggestFilename('plumbus2', 'gazorpazorp'); + assert.strictEqual(suggested, 'gazorpazorp'); + registration.dispose(); + }); + + test('Filename Suggestion - Rewrites filename according to language', () => { + const registration = accessor.languageService.registerLanguage({ + id: 'plumbus2', + filenames: ['plumbus', 'shleem', 'gazorpazorp'] + }); + + const suggested = accessor.textFileService.suggestFilename('plumbus2', 'foobar'); + assert.strictEqual(suggested, 'plumbus'); + registration.dispose(); + }); });