From c8dbbefc7267d04903345b51a62153ac30338eac Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Thu, 3 Mar 2016 09:01:43 +0100 Subject: [PATCH] Allow to save with UTF-8 + BOM encoding (fixes #751) --- src/vs/platform/files/common/files.ts | 8 ++ .../parts/files/browser/files.contribution.ts | 8 +- .../files/electron-browser/fileService.ts | 1 + .../services/files/node/fileService.ts | 13 ++- .../files/test/node/fileService.test.ts | 91 ++++++++++++++++--- 5 files changed, 106 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index d976fcce55000..815c801812ea0 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -418,6 +418,12 @@ export const AutoSaveConfiguration = { ON_FOCUS_CHANGE: 'onFocusChange' }; +export const BOMConfiguration = { + PRESERVE: 'preserve', + REMOVE: 'remove', + INSERT: 'insert' +}; + export interface IFilesConfiguration { files: { exclude: glob.IExpression; @@ -425,5 +431,7 @@ export interface IFilesConfiguration { trimTrailingWhitespace: boolean; autoSave: string; autoSaveDelay: number; + eol: string; + bom: string; }; } \ No newline at end of file diff --git a/src/vs/workbench/parts/files/browser/files.contribution.ts b/src/vs/workbench/parts/files/browser/files.contribution.ts index 411f7bc719f7d..34d6a40cc4b8f 100644 --- a/src/vs/workbench/parts/files/browser/files.contribution.ts +++ b/src/vs/workbench/parts/files/browser/files.contribution.ts @@ -22,7 +22,7 @@ import {IEditorRegistry, Extensions as EditorExtensions, IEditorInputFactory} fr import {EditorInput, IFileEditorInput} from 'vs/workbench/common/editor'; import {QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions} from 'vs/workbench/browser/quickopen'; import {FileEditorDescriptor} from 'vs/workbench/parts/files/browser/files'; -import {AutoSaveConfiguration} from 'vs/platform/files/common/files'; +import {AutoSaveConfiguration, BOMConfiguration} from 'vs/platform/files/common/files'; import {FILE_EDITOR_INPUT_ID, VIEWLET_ID} from 'vs/workbench/parts/files/common/files'; import {FileTracker} from 'vs/workbench/parts/files/browser/fileTracker'; import {SaveParticipant} from 'vs/workbench/parts/files/common/editors/saveParticipant'; @@ -201,6 +201,12 @@ configurationRegistry.registerConfiguration({ 'default': 'utf8', 'description': nls.localize('encoding', "The default character set encoding to use when reading and writing files."), }, + 'files.bom': { + 'type': 'string', + 'default': BOMConfiguration.PRESERVE, + 'enum': [BOMConfiguration.PRESERVE, BOMConfiguration.REMOVE, BOMConfiguration.INSERT], + 'description': nls.localize('bom', "Controls the BOM (Byte Order Mark) for UTF-8 files. By default, a UTF-8 BOM will be preserved if found but not inserted if not found. Set to \"{0}\" to remove the BOM if found or \"{1}\" to always insert a BOM.", BOMConfiguration.REMOVE, BOMConfiguration.INSERT) + }, 'files.eol': { 'type': 'string', 'enum': [ diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index ce1013cf8a9f6..371b26982ecc3 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -53,6 +53,7 @@ export class FileService implements files.IFileService { let fileServiceConfig: IFileServiceOptions = { errorLogger: (msg: string) => errors.onUnexpectedError(msg), encoding: configuration.files && configuration.files.encoding, + bom: configuration.files && configuration.files.bom, encodingOverride: encodingOverride, watcherIgnoredPatterns: doNotWatch, verboseLogging: this.contextService.getConfiguration().env.verboseLogging diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index 366ea85c9d4a9..46ca9a26d0a27 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -43,6 +43,7 @@ export interface IFileServiceOptions { tmpDir?: string; errorLogger?: (msg: string) => void; encoding?: string; + bom?: string; encodingOverride?: IEncodingOverride[]; watcherIgnoredPatterns?: string[]; disableWatcher?: boolean; @@ -234,9 +235,15 @@ export class FileService implements files.IFileService { addBomPromise = TPromise.as(true); } - // UTF8 only gets a BOM if the file had it alredy - else if (exists && encodingToWrite === encoding.UTF8) { - addBomPromise = nfcall(encoding.detectEncodingByBOM, absolutePath).then((enc) => enc === encoding.UTF8); // only for UTF8 we need to check if we have to preserve a BOM + // UTF8 only gets a BOM if the file had it already or we are configured to add BOMs + else if (encodingToWrite === encoding.UTF8) { + if (this.options.bom === files.BOMConfiguration.INSERT) { + addBomPromise = TPromise.as(true); + } else if (this.options.bom === files.BOMConfiguration.REMOVE) { + addBomPromise = TPromise.as(false); + } else if (exists) { + addBomPromise = nfcall(encoding.detectEncodingByBOM, absolutePath).then((enc) => enc === encoding.UTF8); // only for UTF8 we need to check if we have to preserve a BOM + } } // 3.) check to add UTF BOM diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index f08fe02b479cc..e23a0b0e9df94 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -11,7 +11,7 @@ import os = require('os'); import assert = require('assert'); import {FileService, IEncodingOverride} from 'vs/workbench/services/files/node/fileService'; -import {EventType, FileChangesEvent, FileOperationResult, IFileOperationResult} from 'vs/platform/files/common/files'; +import {EventType, FileChangesEvent, FileOperationResult, IFileOperationResult, BOMConfiguration} from 'vs/platform/files/common/files'; import {nfcall} from 'vs/base/common/async'; import uri from 'vs/base/common/uri'; import uuid = require('vs/base/common/uuid'); @@ -25,7 +25,7 @@ suite('FileService', () => { let parentDir = path.join(os.tmpdir(), 'vsctests', 'service'); let testDir: string; - setup(function (done) { + setup(function(done) { let id = uuid.generateUuid(); testDir = path.join(parentDir, id); let sourceDir = require.toUrl('./fixtures/service'); @@ -125,7 +125,7 @@ suite('FileService', () => { test('move - FILE_MOVE_CONFLICT', function(done: () => void) { service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => { - return service.moveFile(source.resource, uri.file(path.join(testDir, 'binary.txt'))).then(null, (e:IFileOperationResult) => { + return service.moveFile(source.resource, uri.file(path.join(testDir, 'binary.txt'))).then(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_MOVE_CONFLICT); done(); @@ -246,7 +246,7 @@ suite('FileService', () => { }); test('resolveFile', function(done: () => void) { - service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))]}).done(r => { + service.resolveFile(uri.file(testDir), { resolveTo: [uri.file(path.join(testDir, 'deep'))] }).done(r => { assert.equal(r.children.length, 6); let deep = utils.getByName(r, 'deep'); @@ -319,7 +319,7 @@ suite('FileService', () => { test('resolveContent - FILE_IS_BINARY', function(done: () => void) { let resource = uri.file(path.join(testDir, 'binary.txt')); - service.resolveContent(resource, { acceptTextOnly: true }).done(null, (e:IFileOperationResult) => { + service.resolveContent(resource, { acceptTextOnly: true }).done(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_BINARY); return service.resolveContent(uri.file(path.join(testDir, 'small.txt')), { acceptTextOnly: true }).then(r => { @@ -333,7 +333,7 @@ suite('FileService', () => { test('resolveContent - FILE_IS_DIRECTORY', function(done: () => void) { let resource = uri.file(path.join(testDir, 'deep')); - service.resolveContent(resource).done(null, (e:IFileOperationResult) => { + service.resolveContent(resource).done(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_IS_DIRECTORY); done(); @@ -343,7 +343,7 @@ suite('FileService', () => { test('resolveContent - FILE_NOT_FOUND', function(done: () => void) { let resource = uri.file(path.join(testDir, '404.html')); - service.resolveContent(resource).done(null, (e:IFileOperationResult) => { + service.resolveContent(resource).done(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_FOUND); done(); @@ -354,7 +354,7 @@ suite('FileService', () => { let resource = uri.file(path.join(testDir, 'index.html')); service.resolveContent(resource).done(c => { - return service.resolveContent(resource, { etag: c.etag }).then(null, (e:IFileOperationResult) => { + return service.resolveContent(resource, { etag: c.etag }).then(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_NOT_MODIFIED_SINCE); done(); @@ -368,7 +368,7 @@ suite('FileService', () => { service.resolveContent(resource).done(c => { fs.writeFileSync(resource.fsPath, 'Updates Incoming!'); - return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(null, (e:IFileOperationResult) => { + return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(null, (e: IFileOperationResult) => { assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE); done(); @@ -422,7 +422,7 @@ suite('FileService', () => { service.watchFileChanges(toWatch); - events.on(EventType.FILE_CHANGES, (e:FileChangesEvent) => { + events.on(EventType.FILE_CHANGES, (e: FileChangesEvent) => { assert.ok(e); service.unwatchFileChanges(toWatch); @@ -434,7 +434,7 @@ suite('FileService', () => { }, 100); }); - test('options', function(done: () => void) { + test('options - encoding', function(done: () => void) { // setup let _id = uuid.generateUuid(); @@ -467,4 +467,73 @@ suite('FileService', () => { }); }); }); + + test('options - bom', function(done: () => void) { + + // setup + let _id = uuid.generateUuid(); + let _testDir = path.join(parentDir, _id); + let _sourceDir = require.toUrl('./fixtures/service'); + let resource = uri.file(path.join(testDir, 'index.html')); + + extfs.copy(_sourceDir, _testDir, () => { + let _service = new FileService(_testDir, null, { + bom: BOMConfiguration.INSERT, + disableWatcher: true + }); + + fs.readFile(resource.fsPath, (error, data) => { + assert.equal(encoding.detectEncodingByBOMFromBuffer(data, 512), null); + + // Update content: BOM => INSERT + _service.updateContent(resource, 'Hello Bom').done(() => { + fs.readFile(resource.fsPath, (error, data) => { + assert.equal(encoding.detectEncodingByBOMFromBuffer(data, 512), encoding.UTF8); + + _service.dispose(); + _service = new FileService(_testDir, null, { + bom: BOMConfiguration.PRESERVE, + disableWatcher: true + }); + + // Update content: BOM => PRESERVE + _service.updateContent(resource, 'Hello Bom').done(() => { + fs.readFile(resource.fsPath, (error, data) => { + assert.equal(encoding.detectEncodingByBOMFromBuffer(data, 512), encoding.UTF8); + + _service.dispose(); + _service = new FileService(_testDir, null, { + bom: BOMConfiguration.REMOVE, + disableWatcher: true + }); + + // Update content: BOM => REMOVE + _service.updateContent(resource, 'Hello Bom').done(() => { + fs.readFile(resource.fsPath, (error, data) => { + assert.equal(encoding.detectEncodingByBOMFromBuffer(data, 512), null); + + _service.dispose(); + _service = new FileService(_testDir, null, { + bom: BOMConfiguration.PRESERVE, + disableWatcher: true + }); + + // Update content: BOM => PRESERVE + _service.updateContent(resource, 'Hello Bom').done(() => { + fs.readFile(resource.fsPath, (error, data) => { + assert.equal(encoding.detectEncodingByBOMFromBuffer(data, 512), null); + + _service.dispose(); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); }); \ No newline at end of file