Unexpected filenames proposed when saving untitled files (fix #150164) (#168278)

This commit is contained in:
Benjamin Pasero 2022-12-07 14:18:54 +01:00 committed by GitHub
parent 911e119dfc
commit f7c5c0dbab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 75 additions and 19 deletions

View File

@ -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

View File

@ -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();
});
});