diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0fe4bf63718e240562aea045fff2030c7c8a6dc4..9f313e5a55ae49574ce5c83b97fbfdbd918530ce 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -481,6 +481,32 @@ export function snapshotToString(snapshot: ITextSnapshot): string { return chunks.join(''); } +/** + * Helper that wraps around a ITextSnapshot and allows to have a + * preamble that the read() method will return first. + */ +export class BufferedTextSnapshot implements ITextSnapshot { + private preambleHandled: boolean; + + constructor(private snapshot: ITextSnapshot, private preamble: string) { + } + + public read(): string { + let value = this.snapshot.read(); + if (!this.preambleHandled) { + this.preambleHandled = true; + + if (typeof value === 'string') { + value = this.preamble + value; + } else { + value = this.preamble; + } + } + + return value; + } +} + /** * Streamable content and meta information of a file. */ diff --git a/src/vs/workbench/parts/backup/common/backupModelTracker.ts b/src/vs/workbench/parts/backup/common/backupModelTracker.ts index 7078c5cd4b5652b5b86ca873c12c98cae03fc249..1c61b9715e28e220990429a33e0a1a39de8cbc3e 100644 --- a/src/vs/workbench/parts/backup/common/backupModelTracker.ts +++ b/src/vs/workbench/parts/backup/common/backupModelTracker.ts @@ -72,14 +72,14 @@ export class BackupModelTracker implements IWorkbenchContribution { // Do not backup when auto save after delay is configured if (!this.configuredAutoSaveAfterDelay) { const model = this.textFileService.models.get(event.resource); - this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()).done(null, errors.onUnexpectedError); + this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()).done(null, errors.onUnexpectedError); } } } private onUntitledModelChanged(resource: Uri): void { if (this.untitledEditorService.isDirty(resource)) { - this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.getValue(), model.getVersionId())).done(null, errors.onUnexpectedError); + this.untitledEditorService.loadOrCreate({ resource }).then(model => this.backupFileService.backupResource(resource, model.createSnapshot(), model.getVersionId())).done(null, errors.onUnexpectedError); } else { this.discardBackup(resource); } diff --git a/src/vs/workbench/parts/files/electron-browser/fileActions.ts b/src/vs/workbench/parts/files/electron-browser/fileActions.ts index e80a6804eb1f0855018eaf8b3cab34aa4cdf8fa1..952968f7641f2d7c5412400ccc5443ccc446c463 100644 --- a/src/vs/workbench/parts/files/electron-browser/fileActions.ts +++ b/src/vs/workbench/parts/files/electron-browser/fileActions.ts @@ -318,7 +318,7 @@ class RenameFileAction extends BaseRenameAction { const model = this.textFileService.models.get(d); - return this.backupFileService.backupResource(renamed, model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(renamed, model.createSnapshot(), model.getVersionId()); })) // 2. soft revert all dirty since we have backed up their contents diff --git a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts index a812d54aaff8d949fd0596c29d1d07025de9c7ce..49550e507a130b5ec1eb509d6e2fdbd8eab45903 100644 --- a/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/electron-browser/views/explorerViewer.ts @@ -968,7 +968,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { const model = this.textFileService.models.get(d); - return this.backupFileService.backupResource(moved, model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(moved, model.createSnapshot(), model.getVersionId()); })) // 2. soft revert all dirty since we have backed up their contents diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 7a46ffa491e769ea7fc5976b543990d91d6f665b..0b4d35f5d0cdbe6b507399ca0c819c44ecc077f1 100644 --- a/src/vs/workbench/services/backup/common/backup.ts +++ b/src/vs/workbench/services/backup/common/backup.ts @@ -8,7 +8,7 @@ import Uri from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IResolveContentOptions, IUpdateContentOptions } from 'vs/platform/files/common/files'; +import { IResolveContentOptions, IUpdateContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { ITextBufferFactory } from 'vs/editor/common/model'; export const IBackupFileService = createDecorator('backupFileService'); @@ -52,10 +52,10 @@ export interface IBackupFileService { * Backs up a resource. * * @param resource The resource to back up. - * @param content The content of the resource. + * @param content The content of the resource as value or snapshot. * @param versionId The version id of the resource to backup. */ - backupResource(resource: Uri, content: string, versionId?: number): TPromise; + backupResource(resource: Uri, content: string | ITextSnapshot, versionId?: number): TPromise; /** * Gets a list of file backups for the current workspace. diff --git a/src/vs/workbench/services/backup/node/backupFileService.ts b/src/vs/workbench/services/backup/node/backupFileService.ts index ff3543726ce70e25eeb96506341e60e96e990d47..3ef38e18dc5cf7c112bbaf77b977a68d79476026 100644 --- a/src/vs/workbench/services/backup/node/backupFileService.ts +++ b/src/vs/workbench/services/backup/node/backupFileService.ts @@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs'; import Uri from 'vs/base/common/uri'; import { ResourceQueue } from 'vs/base/common/async'; import { IBackupFileService, BACKUP_FILE_UPDATE_OPTIONS } from 'vs/workbench/services/backup/common/backup'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, ITextSnapshot, BufferedTextSnapshot, IFileStat } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { readToMatchingString } from 'vs/base/node/stream'; import { Range } from 'vs/editor/common/core/range'; @@ -149,7 +149,7 @@ export class BackupFileService implements IBackupFileService { }); } - public backupResource(resource: Uri, content: string, versionId?: number): TPromise { + public backupResource(resource: Uri, content: string | ITextSnapshot, versionId?: number): TPromise { if (this.isShuttingDown) { return TPromise.as(void 0); } @@ -164,11 +164,21 @@ export class BackupFileService implements IBackupFileService { return void 0; // return early if backup version id matches requested one } - // Add metadata to top of file - content = `${resource.toString()}${BackupFileService.META_MARKER}${content}`; - return this.ioOperationQueues.queueFor(backupResource).queue(() => { - return this.fileService.updateContent(backupResource, content, BACKUP_FILE_UPDATE_OPTIONS).then(() => model.add(backupResource, versionId)); + const preamble = `${resource.toString()}${BackupFileService.META_MARKER}`; + + // Update content with value + let updateContentPromise: TPromise; + if (typeof content === 'string') { + updateContentPromise = this.fileService.updateContent(backupResource, `${preamble}${content}`, BACKUP_FILE_UPDATE_OPTIONS); + } + + // Update content with snapshot + else { + updateContentPromise = this.fileService.updateContent(backupResource, new BufferedTextSnapshot(content, preamble), BACKUP_FILE_UPDATE_OPTIONS); + } + + return updateContentPromise.then(() => model.add(backupResource, versionId)); }); }); } diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts index 84ae8794ec7755d261d39ad1a400adfc496f8f2b..2720754abbbf67b74304f132ee5d22bf78ffb07d 100644 --- a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/node/backupFileService.test.ts @@ -16,7 +16,7 @@ import pfs = require('vs/base/node/pfs'); import Uri from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService'; import { FileService } from 'vs/workbench/services/files/node/fileService'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactory, TextModel } from 'vs/editor/common/model/textModel'; import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -121,6 +121,56 @@ suite('BackupFileService', () => { done(); }); }); + + test('text file (ITextSnapshot)', function (done: () => void) { + const model = TextModel.createFromString('test'); + + service.backupResource(fooFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); + assert.equal(fs.existsSync(fooBackupPath), true); + assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\ntest`); + model.dispose(); + done(); + }); + }); + + test('untitled file (ITextSnapshot)', function (done: () => void) { + const model = TextModel.createFromString('test'); + + service.backupResource(untitledFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); + assert.equal(fs.existsSync(untitledBackupPath), true); + assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\ntest`); + model.dispose(); + done(); + }); + }); + + test('text file (large file, ITextSnapshot)', function (done: () => void) { + const largeString = (new Array(100 * 1024)).join('Large String\n'); + const model = TextModel.createFromString(largeString); + + service.backupResource(fooFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1); + assert.equal(fs.existsSync(fooBackupPath), true); + assert.equal(fs.readFileSync(fooBackupPath), `${fooFile.toString()}\n${largeString}`); + model.dispose(); + done(); + }); + }); + + test('untitled file (large file, ITextSnapshot)', function (done: () => void) { + const largeString = (new Array(100 * 1024)).join('Large String\n'); + const model = TextModel.createFromString(largeString); + + service.backupResource(untitledFile, model.createSnapshot()).then(() => { + assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1); + assert.equal(fs.existsSync(untitledBackupPath), true); + assert.equal(fs.readFileSync(untitledBackupPath), `${untitledFile.toString()}\n${largeString}`); + model.dispose(); + done(); + }); + }); }); suite('discardResourceBackup', () => { 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 863f43f8721293dadb884409c26dc69ee1947d17..378b1b91a08d1d881e8a85cf9a1307243ebc2b78 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -945,26 +945,32 @@ suite('FileService', () => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + const model = TextModel.createFromString('Hello Bom'); + // Update content: UTF_8 => UTF_8_BOM - _service.updateContent(resource, 'Hello Bom', { encoding: encodingLib.UTF8_with_bom }).done(() => { + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); // Update content: PRESERVE BOM when using UTF-8 - _service.updateContent(resource, 'Please stay Bom', { encoding: encodingLib.UTF8 }).done(() => { + model.setValue('Please stay Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8); // Update content: REMOVE BOM - _service.updateContent(resource, 'Go away Bom', { encoding: encodingLib.UTF8, overwriteEncoding: true }).done(() => { + model.setValue('Go away Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); // Update content: BOM comes not back - _service.updateContent(resource, 'Do not come back Bom', { encoding: encodingLib.UTF8 }).done(() => { + model.setValue('Do not come back Bom'); + _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).done(() => { fs.readFile(resource.fsPath, (error, data) => { assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null); + model.dispose(); _service.dispose(); done(); }); diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 6e5bf84bf4a63f2924a37e400c82f3e78402306d..a1ef38dd364e66c0d4c4e33214cb41c12d9b65ca 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -241,7 +241,7 @@ export abstract class TextFileService implements ITextFileService { private doBackupAll(dirtyFileModels: ITextFileEditorModel[], untitledResources: URI[]): TPromise { // Handle file resources first - return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()))).then(results => { + return TPromise.join(dirtyFileModels.map(model => this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()))).then(results => { // Handle untitled resources const untitledModelPromises = untitledResources @@ -250,7 +250,7 @@ export abstract class TextFileService implements ITextFileService { return TPromise.join(untitledModelPromises).then(untitledModels => { const untitledBackupPromises = untitledModels.map(model => { - return this.backupFileService.backupResource(model.getResource(), model.getValue(), model.getVersionId()); + return this.backupFileService.backupResource(model.getResource(), model.createSnapshot(), model.getVersionId()); }); return TPromise.join(untitledBackupPromises).then(() => void 0); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 4be8ffb329e92ce9e01a08520ad1f59af04fc91c..6000c9734e52e8721e6eae3f66b51130eb5a4030 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -9,7 +9,7 @@ import URI from 'vs/base/common/uri'; import Event from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IEncodingSupport, ConfirmResult } from 'vs/workbench/common/editor'; -import { IBaseStat, IResolveContentOptions } from 'vs/platform/files/common/files'; +import { IBaseStat, IResolveContentOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory } from 'vs/editor/common/model'; @@ -202,6 +202,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport getValue(): string; + createSnapshot(): ITextSnapshot; + isDirty(): boolean; isResolved(): boolean; diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e6f1b33310f38f0ba7ffc65b64ee0a7a21bfdfea..50f9c781ff8616fc56d1a62e27a356e8d6ae350c 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -856,7 +856,7 @@ export class TestBackupFileService implements IBackupFileService { return null; } - public backupResource(resource: URI, content: string): TPromise { + public backupResource(resource: URI, content: string | ITextSnapshot): TPromise { return TPromise.as(void 0); }