Skip to content

Commit

Permalink
Allow to save with UTF-8 + BOM encoding (fixes #751)
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Mar 3, 2016
1 parent 2ddf83a commit c8dbbef
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 15 deletions.
8 changes: 8 additions & 0 deletions src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,12 +418,20 @@ export const AutoSaveConfiguration = {
ON_FOCUS_CHANGE: 'onFocusChange'
};

export const BOMConfiguration = {
PRESERVE: 'preserve',
REMOVE: 'remove',
INSERT: 'insert'
};

export interface IFilesConfiguration {
files: {
exclude: glob.IExpression;
encoding: string;
trimTrailingWhitespace: boolean;
autoSave: string;
autoSaveDelay: number;
eol: string;
bom: string;
};
}
8 changes: 7 additions & 1 deletion src/vs/workbench/parts/files/browser/files.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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': [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/vs/workbench/services/files/node/fileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface IFileServiceOptions {
tmpDir?: string;
errorLogger?: (msg: string) => void;
encoding?: string;
bom?: string;
encodingOverride?: IEncodingOverride[];
watcherIgnoredPatterns?: string[];
disableWatcher?: boolean;
Expand Down Expand Up @@ -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
Expand Down
91 changes: 80 additions & 11 deletions src/vs/workbench/services/files/test/node/fileService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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 => {
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand All @@ -434,7 +434,7 @@ suite('FileService', () => {
}, 100);
});

test('options', function(done: () => void) {
test('options - encoding', function(done: () => void) {

// setup
let _id = uuid.generateUuid();
Expand Down Expand Up @@ -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();
});
});
});
});
});
});
});
});
});
});
});
});

0 comments on commit c8dbbef

Please sign in to comment.