diff --git a/src/typings/iconv-lite.d.ts b/src/typings/iconv-lite.d.ts index 84c54e320cf37..5ad19bb95b7c3 100644 --- a/src/typings/iconv-lite.d.ts +++ b/src/typings/iconv-lite.d.ts @@ -6,13 +6,13 @@ /// declare module 'iconv-lite' { - export function decode(buffer: NodeBuffer, encoding: string, options?: any): string; + export function decode(buffer: NodeBuffer, encoding: string): string; - export function encode(content: string, encoding: string, options?: any): NodeBuffer; + export function encode(content: string, encoding: string, options?: { addBOM?: boolean }): NodeBuffer; export function encodingExists(encoding: string): boolean; export function decodeStream(encoding: string): NodeJS.ReadWriteStream; - export function encodeStream(encoding: string): NodeJS.ReadWriteStream; + export function encodeStream(encoding: string, options?: { addBOM?: boolean }): NodeJS.ReadWriteStream; } \ No newline at end of file diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 4177f3ffab5c5..1d134c6576791 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -28,11 +28,11 @@ export function bomLength(encoding: string): number { return 0; } -export function decode(buffer: NodeBuffer, encoding: string, options?: any): string { - return iconv.decode(buffer, toNodeEncoding(encoding), options); +export function decode(buffer: NodeBuffer, encoding: string): string { + return iconv.decode(buffer, toNodeEncoding(encoding)); } -export function encode(content: string, encoding: string, options?: any): NodeBuffer { +export function encode(content: string, encoding: string, options?: { addBOM?: boolean }): NodeBuffer { return iconv.encode(content, toNodeEncoding(encoding), options); } @@ -44,6 +44,10 @@ export function decodeStream(encoding: string): NodeJS.ReadWriteStream { return iconv.decodeStream(toNodeEncoding(encoding)); } +export function encodeStream(encoding: string, options?: { addBOM?: boolean }): NodeJS.ReadWriteStream { + return iconv.encodeStream(toNodeEncoding(encoding), options); +} + function toNodeEncoding(enc: string): string { if (enc === UTF8_with_bom) { return UTF8; // iconv does not distinguish UTF 8 with or without BOM, so we need to help it diff --git a/src/vs/base/node/extfs.ts b/src/vs/base/node/extfs.ts index b3a4121079efc..963a9a3905e28 100644 --- a/src/vs/base/node/extfs.ts +++ b/src/vs/base/node/extfs.ts @@ -54,7 +54,7 @@ export function copy(source: string, target: string, callback: (error: Error) => } if (!stat.isDirectory()) { - return pipeFs(source, target, stat.mode & 511, callback); + return doCopyFile(source, target, stat.mode & 511, callback); } if (copiedSources[source]) { @@ -75,6 +75,38 @@ export function copy(source: string, target: string, callback: (error: Error) => }); } +function doCopyFile(source: string, target: string, mode: number, callback: (error: Error) => void): void { + const reader = fs.createReadStream(source); + const writer = fs.createWriteStream(target, { mode }); + + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases, pass to callback + if (error) { + callback(error); + } + + // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 + else { + fs.chmod(target, mode, callback); + } + } + }; + + // handle errors properly + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // we are done (underlying fd has been closed) + writer.once('close', () => finish()); + + // start piping + reader.pipe(writer); +} + export function mkdirp(path: string, mode?: number): TPromise { const mkdir = () => nfcall(fs.mkdir, path, mode) .then(null, (err: NodeJS.ErrnoException) => { @@ -88,11 +120,12 @@ export function mkdirp(path: string, mode?: number): TPromise { return TPromise.wrapError(err); }); - // is root? + // stop at root if (path === paths.dirname(path)) { return TPromise.as(true); } + // recursively mkdir return mkdir().then(null, (err: NodeJS.ErrnoException) => { if (err.code === 'ENOENT') { return mkdirp(paths.dirname(path), mode).then(mkdir); @@ -102,40 +135,6 @@ export function mkdirp(path: string, mode?: number): TPromise { }); } -function pipeFs(source: string, target: string, mode: number, callback: (error: Error) => void): void { - let callbackHandled = false; - - const readStream = fs.createReadStream(source); - const writeStream = fs.createWriteStream(target, { mode: mode }); - - const onError = (error: Error) => { - if (!callbackHandled) { - callbackHandled = true; - callback(error); - } - }; - - readStream.on('error', onError); - writeStream.on('error', onError); - - readStream.on('end', () => { - (writeStream).end(() => { // In this case the write stream is known to have an end signature with callback - if (!callbackHandled) { - callbackHandled = true; - - fs.chmod(target, mode, callback); // we need to explicitly chmod because of https://github.com/nodejs/node/issues/1104 - } - }); - }); - - // In node 0.8 there is no easy way to find out when the pipe operation has finished. As such, we use the end property = false - // so that we are in charge of calling end() on the write stream and we will be notified when the write stream is really done. - // We can do this because file streams have an end() method that allows to pass in a callback. - // In node 0.10 there is an event 'finish' emitted from the write stream that can be used. See - // https://groups.google.com/forum/?fromgroups=#!topic/nodejs/YWQ1sRoXOdI - readStream.pipe(writeStream, { end: false }); -} - // Deletes the given path by first moving it out of the workspace. This has two benefits. For one, the operation can return fast because // after the rename, the contents are out of the workspace although not yet deleted. The greater benefit however is that this operation // will fail in case any file is used by another process. fs.unlink() in node will not bail if a file unlinked is used by another process. @@ -320,15 +319,101 @@ export function mv(source: string, target: string, callback: (error: Error) => v }); } +let canFlush = true; +export function writeFileAndFlush(path: string, data: string | NodeBuffer | NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { + options = ensureOptions(options); + + if (typeof data === 'string' || Buffer.isBuffer(data)) { + doWriteFileAndFlush(path, data, options, callback); + } else { + doWriteFileStreamAndFlush(path, data, options, callback); + } +} + +function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { + + // finish only once + let finished = false; + const finish = (error?: Error) => { + if (!finished) { + finished = true; + + // in error cases we need to manually close streams + // if the write stream was successfully opened + if (error) { + if (isOpen) { + writer.once('close', () => callback(error)); + writer.close(); + } else { + callback(error); + } + } + + // otherwise just return without error + else { + callback(); + } + } + }; + + // create writer to target. we set autoClose: false because we want to use the streams + // file descriptor to call fs.fdatasync to ensure the data is flushed to disk + const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false }); + + // Event: 'open' + // Purpose: save the fd for later use + // Notes: will not be called when there is an error opening the file descriptor! + let fd: number; + let isOpen: boolean; + writer.once('open', descriptor => { + fd = descriptor; + isOpen = true; + }); + + // Event: 'error' + // Purpose: to return the error to the outside and to close the write stream (does not happen automatically) + reader.once('error', error => finish(error)); + writer.once('error', error => finish(error)); + + // Event: 'finish' + // Purpose: use fs.fdatasync to flush the contents to disk + // Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close() + // because we have created the WriteStream with autoClose: false + writer.once('finish', () => { + + // flush to disk + if (canFlush && isOpen) { + fs.fdatasync(fd, (syncError: Error) => { + + // In some exotic setups it is well possible that node fails to sync + // In that case we disable flushing and warn to the console + if (syncError) { + console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); + canFlush = false; + } + + writer.close(); + }); + } else { + writer.close(); + } + }); + + // Event: 'close' + // Purpose: signal we are done to the outside + // Notes: event is called when the writer's filedescriptor is closed + writer.once('close', () => finish()); + + // start data piping + reader.pipe(writer); +} + // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk // We do this in cases where we want to make sure the data is really on disk and // not in some cache. // // See https://github.com/nodejs/node/blob/v5.10.0/lib/fs.js#L1194 -let canFlush = true; -export function writeFileAndFlush(path: string, data: string | NodeBuffer, options: { mode?: number; flag?: string; }, callback: (error: Error) => void): void { - options = ensureOptions(options); - +function doWriteFileAndFlush(path: string, data: string | NodeBuffer, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void { if (!canFlush) { return fs.writeFile(path, data, options, callback); } diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index b949384de1ed1..9ebc819fd0236 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -101,6 +101,7 @@ const writeFilePathQueue: { [path: string]: Queue } = Object.create(null); export function writeFile(path: string, data: string, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: NodeBuffer, options?: { mode?: number; flag?: string; }): TPromise; +export function writeFile(path: string, data: NodeJS.ReadableStream, options?: { mode?: number; flag?: string; }): TPromise; export function writeFile(path: string, data: any, options?: { mode?: number; flag?: string; }): TPromise { let queueKey = toQueueKey(path); diff --git a/src/vs/base/test/node/extfs/extfs.test.ts b/src/vs/base/test/node/extfs/extfs.test.ts index 06ecf0b8ba0b1..686934d9e0159 100644 --- a/src/vs/base/test/node/extfs/extfs.test.ts +++ b/src/vs/base/test/node/extfs/extfs.test.ts @@ -15,6 +15,7 @@ import uuid = require('vs/base/common/uuid'); import strings = require('vs/base/common/strings'); import extfs = require('vs/base/node/extfs'); import { onError } from 'vs/base/test/common/utils'; +import { Readable } from 'stream'; const ignore = () => { }; @@ -22,6 +23,38 @@ const mkdirp = (path: string, mode: number, callback: (error) => void) => { extfs.mkdirp(path, mode).done(() => callback(null), error => callback(error)); }; +const chunkSize = 64 * 1024; +const readError = 'Error while reading'; +function toReadable(value: string, throwError?: boolean): Readable { + const totalChunks = Math.ceil(value.length / chunkSize); + const stringChunks: string[] = []; + + for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) { + stringChunks[i] = value.substr(j, chunkSize); + } + + let counter = 0; + return new Readable({ + read: function () { + if (throwError) { + this.emit('error', new Error(readError)); + } + + let res: string; + let canPush = true; + while (canPush && (res = stringChunks[counter++])) { + canPush = this.push(res); + } + + // EOS + if (!res) { + this.push(null); + } + }, + encoding: 'utf8' + }); +} + suite('Extfs', () => { test('mkdirp', function (done: () => void) { @@ -174,7 +207,7 @@ suite('Extfs', () => { } }); - test('writeFileAndFlush', function (done: () => void) { + test('writeFileAndFlush (string)', function (done: () => void) { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); const newDir = path.join(parentDir, 'extfs', id); @@ -209,6 +242,192 @@ suite('Extfs', () => { }); }); + test('writeFileAndFlush (stream)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile), 'Hello World'); + + const largeString = (new Array(100 * 1024)).join('Large String\n'); + + extfs.writeFileAndFlush(testFile, toReadable(largeString), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile), largeString); + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + }); + + test('writeFileAndFlush (file stream)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = require.toUrl('./fixtures/index.html'); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null, error => { + if (error) { + return onError(error, done); + } + + assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString()); + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (string, error handling)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, 'Hello World', null, error => { + if (!error) { + return onError(new Error('Expected error for writing to readonly file'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (stream, error handling EISDIR)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (!error || (error).code !== 'EISDIR') { + return onError(new Error('Expected EISDIR error for writing to folder but got: ' + (error ? (error).code : 'no error')), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (stream, error handling READERROR)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + extfs.writeFileAndFlush(testFile, toReadable('Hello World', true /* throw error */), null, error => { + if (!error || error.message !== readError) { + return onError(new Error('Expected error for writing to folder'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('pasero writeFileAndFlush (stream, error handling EACCES)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.writeFileSync(testFile, ''); + fs.chmodSync(testFile, 33060); // make readonly + + extfs.writeFileAndFlush(testFile, toReadable('Hello World'), null, error => { + if (!error || !((error).code !== 'EACCES' || (error).code !== 'EPERM')) { + return onError(new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (error ? (error).code : 'no error')), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + + test('writeFileAndFlush (file stream, error handling)', function (done: () => void) { + const id = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id); + const sourceFile = require.toUrl('./fixtures/index.html'); + const newDir = path.join(parentDir, 'extfs', id); + const testFile = path.join(newDir, 'flushed.txt'); + + mkdirp(newDir, 493, error => { + if (error) { + return onError(error, done); + } + + assert.ok(fs.existsSync(newDir)); + + fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! + + extfs.writeFileAndFlush(testFile, fs.createReadStream(sourceFile), null, error => { + if (!error) { + return onError(new Error('Expected error for writing to folder'), done); + } + + extfs.del(parentDir, os.tmpdir(), done, ignore); + }); + }); + }); + test('writeFileAndFlushSync', function (done: () => void) { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 90e95ac7bb7f4..27c58c24d2e62 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -82,6 +82,17 @@ export function createTextBufferFactoryFromStream(stream: IStringStream): TPromi }); } +export function createTextBufferFactoryFromSnapshot(snapshot: ITextSnapshot): model.ITextBufferFactory { + let builder = createTextBufferBuilder(); + + let chunk: string; + while (typeof (chunk = snapshot.read()) === 'string') { + builder.acceptChunk(chunk); + } + + return builder.finish(); +} + export function createTextBuffer(value: string | model.ITextBufferFactory, defaultEOL: model.DefaultEndOfLine): model.ITextBuffer { const factory = (typeof value === 'string' ? createTextBufferFactory(value) : value); return factory.create(defaultEOL); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index af89c431ab687..0fe4bf63718e2 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -83,7 +83,7 @@ export interface IFileService { /** * Updates the content replacing its previous value. */ - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise; + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise; /** * Moves the file to a new path identified by the resource. @@ -468,6 +468,19 @@ export interface ITextSnapshot { read(): string; } +/** + * Helper method to convert a snapshot into its full string form. + */ +export function snapshotToString(snapshot: ITextSnapshot): string { + const chunks: string[] = []; + let chunk: string; + while (typeof (chunk = snapshot.read()) === 'string') { + chunks.push(chunk); + } + + return chunks.join(''); +} + /** * Streamable content and meta information of a file. */ diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 524ffbdf8d6ec..8c10065dfcc6a 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -5,7 +5,7 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import { EndOfLinePreference, ITextModel, ITextBufferFactory } from 'vs/editor/common/model'; +import { ITextModel, ITextBufferFactory } from 'vs/editor/common/model'; import { IMode } from 'vs/editor/common/modes'; import { EditorModel } from 'vs/workbench/common/editor'; import URI from 'vs/base/common/uri'; @@ -13,6 +13,7 @@ import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITextSnapshot } from 'vs/platform/files/common/files'; /** * The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated. @@ -90,7 +91,9 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd return this; } - protected getFirstLineText(value: string | ITextBufferFactory): string { + protected getFirstLineText(value: string | ITextBufferFactory | ITextSnapshot): string { + + // string if (typeof value === 'string') { const firstLineText = value.substr(0, 100); @@ -105,9 +108,17 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd } return firstLineText.substr(0, Math.min(crIndex, lfIndex)); - } else { - return value.getFirstLineText(100); } + + // text buffer factory + const textBufferFactory = value as ITextBufferFactory; + if (typeof textBufferFactory.getFirstLineText === 'function') { + return textBufferFactory.getFirstLineText(100); + } + + // text snapshot + const textSnapshot = value as ITextSnapshot; + return this.getFirstLineText(textSnapshot.read() || ''); } /** @@ -130,13 +141,10 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd this.modelService.updateModel(this.textEditorModel, newValue); } - /** - * Returns the textual value of this editor model or null if it has not yet been created. - */ - public getValue(): string { + public createSnapshot(): ITextSnapshot { const model = this.textEditorModel; if (model) { - return model.getValue(EndOfLinePreference.TextDefined, true /* Preserve BOM */); + return model.createSnapshot(true /* Preserve BOM */); } return null; diff --git a/src/vs/workbench/common/editor/untitledEditorModel.ts b/src/vs/workbench/common/editor/untitledEditorModel.ts index 9a817653721ae..c7dcee066bd45 100644 --- a/src/vs/workbench/common/editor/untitledEditorModel.ts +++ b/src/vs/workbench/common/editor/untitledEditorModel.ts @@ -10,7 +10,6 @@ import { IEncodingSupport } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import URI from 'vs/base/common/uri'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; -import { EndOfLinePreference } from 'vs/editor/common/model'; import { CONTENT_CHANGE_EVENT_BUFFER_DELAY } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -113,14 +112,6 @@ export class UntitledEditorModel extends BaseTextEditorModel implements IEncodin return this.versionId; } - public getValue(): string { - if (this.textEditorModel) { - return this.textEditorModel.getValue(EndOfLinePreference.TextDefined, true /* Preserve BOM */); - } - - return null; - } - public getModeId(): string { if (this.textEditorModel) { return this.textEditorModel.getLanguageIdentifier().language; diff --git a/src/vs/workbench/parts/backup/common/backupModelTracker.ts b/src/vs/workbench/parts/backup/common/backupModelTracker.ts index 7078c5cd4b565..1c61b9715e28e 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 e80a6804eb1f0..952968f7641f2 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/saveErrorHandler.ts b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts index 379a2a53d81bc..3a0864e2ea7d5 100644 --- a/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts +++ b/src/vs/workbench/parts/files/electron-browser/saveErrorHandler.ts @@ -31,6 +31,7 @@ import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEdi import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/parts/files/electron-browser/fileCommands'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext'; export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; @@ -262,7 +263,7 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: resolverService.createModelReference(resource).then(reference => { const model = reference.object as ITextFileEditorModel; - const localModelValue = model.getValue(); + const localModelSnapshot = model.createSnapshot(); clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions @@ -270,7 +271,7 @@ export const acceptLocalChangesCommand = (accessor: ServicesAccessor, resource: return model.revert().then(() => { // Restore user value (without loosing undo stack) - modelService.updateModel(model.textEditorModel, localModelValue); + modelService.updateModel(model.textEditorModel, createTextBufferFactoryFromSnapshot(localModelSnapshot)); // Trigger save return model.save().then(() => { 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 a812d54aaff8d..49550e507a130 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/parts/files/test/browser/fileEditorTracker.test.ts b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts index 4b9590f037f44..a76038659ec0b 100644 --- a/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts +++ b/src/vs/workbench/parts/files/test/browser/fileEditorTracker.test.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { EditorStacksModel } from 'vs/workbench/common/editor/editorStacksModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, FileChangesEvent, FileChangeType, IFileService, snapshotToString } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { once } from 'vs/base/common/event'; @@ -191,14 +191,14 @@ suite('Files - FileEditorTracker', () => { accessor.textFileService.models.loadOrCreate(resource).then((model: TextFileEditorModel) => { model.textEditorModel.setValue('Super Good'); - assert.equal(model.getValue(), 'Super Good'); + assert.equal(snapshotToString(model.createSnapshot()), 'Super Good'); model.save().then(() => { // change event (watcher) accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); - assert.equal(model.getValue(), 'Hello Html'); + assert.equal(snapshotToString(model.createSnapshot()), 'Hello Html'); tracker.dispose(); diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index c93549e85dd49..fca4c6b1a3678 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -24,6 +24,7 @@ import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IFileService } from 'vs/platform/files/common/files'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; const REPLACE_PREVIEW = 'replacePreview'; @@ -70,7 +71,7 @@ class ReplacePreviewModel extends Disposable { ref = this._register(ref); const sourceModel = ref.object.textEditorModel; const sourceModelModeId = sourceModel.getLanguageIdentifier().language; - const replacePreviewModel = this.modelService.createModel(sourceModel.getValue(), this.modeService.getOrCreateMode(sourceModelModeId), replacePreviewUri); + const replacePreviewModel = this.modelService.createModel(createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot()), this.modeService.getOrCreateMode(sourceModelModeId), replacePreviewUri); this._register(fileMatch.onChange(modelChange => this.update(sourceModel, replacePreviewModel, fileMatch, modelChange))); this._register(this.searchWorkbenchService.searchModel.onReplaceTermChanged(() => this.update(sourceModel, replacePreviewModel, fileMatch))); this._register(fileMatch.onDispose(() => replacePreviewModel.dispose())); // TODO@Sandeep we should not dispose a model directly but rather the reference (depends on https://github.com/Microsoft/vscode/issues/17073) diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts index 7a46ffa491e76..0b4d35f5d0cdb 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 ff3543726ce70..18c4e2a7b5073 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, 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'; @@ -28,6 +28,28 @@ export interface IBackupFilesModel { clear(): void; } +export class BackupSnapshot 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; + } +} + export class BackupFilesModel implements IBackupFilesModel { private cache: { [resource: string]: number /* version ID */ } = Object.create(null); @@ -149,7 +171,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 +186,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 BackupSnapshot(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 84ae8794ec775..2720754abbbf6 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/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 196825b2f11ac..e5fd893ab7363 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -19,6 +19,7 @@ import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorIn import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { ICloseEditorsFilter } from 'vs/workbench/browser/parts/editor/editorPart'; +import { snapshotToString } from 'vs/platform/files/common/files'; let activeEditor: BaseEditor = { getSelection: function () { @@ -163,7 +164,7 @@ suite('WorkbenchEditorService', () => { const untitledInput = openedEditorInput as UntitledEditorInput; untitledInput.resolve().then(model => { - assert.equal(model.getValue(), 'Hello Untitled'); + assert.equal(snapshotToString(model.createSnapshot()), 'Hello Untitled'); }); }); diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 5eeae64bf3a08..e97cc0ffd1d12 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -11,7 +11,7 @@ import paths = require('vs/base/common/paths'); import encoding = require('vs/base/node/encoding'); import errors = require('vs/base/common/errors'); import uri from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions } from 'vs/platform/files/common/files'; +import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @@ -181,7 +181,7 @@ export class FileService implements IFileService { return this.raw.resolveStreamContent(resource, options); } - public updateContent(resource: uri, value: string, options?: IUpdateContentOptions): TPromise { + public updateContent(resource: uri, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { return this.raw.updateContent(resource, value, options); } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 2bf1ba014e189..1fb80d9904248 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -6,7 +6,7 @@ import URI from 'vs/base/common/uri'; import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; -import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IContent, IStreamContent, IFileStat, IResolveContentOptions, IUpdateContentOptions, IResolveFileOptions, IResolveFileResult, FileOperationEvent, FileOperation, IFileSystemProvider, IStat, FileType, IImportResult, FileChangesEvent, ICreateFileOptions, FileOperationError, FileOperationResult, ITextSnapshot, snapshotToString } from 'vs/platform/files/common/files'; import { TPromise } from 'vs/base/common/winjs.base'; import { basename, join } from 'path'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -351,7 +351,7 @@ export class RemoteFileService extends FileService { } } - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { if (resource.scheme === Schemas.file) { return super.updateContent(resource, value, options); } else { @@ -361,9 +361,10 @@ export class RemoteFileService extends FileService { } } - private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string, options: IUpdateContentOptions): TPromise { + private _doUpdateContent(provider: IFileSystemProvider, resource: URI, content: string | ITextSnapshot, options: IUpdateContentOptions): TPromise { const encoding = this.getEncoding(resource, options.encoding); - return provider.write(resource, encode(content, encoding)).then(() => { + // TODO@Joh support streaming API for remote file system writes + return provider.write(resource, encode(typeof content === 'string' ? content : snapshotToString(content), encoding)).then(() => { return this.resolveFile(resource); }); } diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index b1475b44eb9b9..f443fec34a4e0 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -10,8 +10,7 @@ import fs = require('fs'); import os = require('os'); import crypto = require('crypto'); import assert = require('assert'); - -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData } from 'vs/platform/files/common/files'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot } from 'vs/platform/files/common/files'; import { MAX_FILE_SIZE } from 'vs/platform/files/node/files'; import { isEqualOrParent } from 'vs/base/common/paths'; import { ResourceMap } from 'vs/base/common/map'; @@ -41,6 +40,7 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { getBaseLabel } from 'vs/base/common/labels'; import { assign } from 'vs/base/common/objects'; +import { Readable } from 'stream'; export interface IEncodingOverride { resource: uri; @@ -505,7 +505,7 @@ export class FileService implements IFileService { }); } - public updateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { if (this.options.elevationSupport && options.writeElevated) { return this.doUpdateContentElevated(resource, value, options); } @@ -513,7 +513,7 @@ export class FileService implements IFileService { return this.doUpdateContent(resource, value, options); } - private doUpdateContent(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file @@ -579,18 +579,25 @@ export class FileService implements IFileService { }); } - private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { + private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { let writeFilePromise: TPromise; // Write fast if we do UTF 8 without BOM if (!addBOM && encodingToWrite === encoding.UTF8) { - writeFilePromise = pfs.writeFile(absolutePath, value, options); + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, value, options); + } else { + writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value), options); + } } // Otherwise use encoding lib else { - const encoded = encoding.encode(value, encodingToWrite, { addBOM }); - writeFilePromise = pfs.writeFile(absolutePath, encoded, options); + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, encoding.encode(value, encodingToWrite, { addBOM }), options); + } else { + writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value).pipe(encoding.encodeStream(encodingToWrite, { addBOM })), options); + } } // set contents @@ -601,7 +608,32 @@ export class FileService implements IFileService { }); } - private doUpdateContentElevated(resource: uri, value: string, options: IUpdateContentOptions = Object.create(null)): TPromise { + private snapshotToReadableStream(snapshot: ITextSnapshot): NodeJS.ReadableStream { + return new Readable({ + read: function () { + try { + let chunk: string; + let canPush = true; + + // Push all chunks as long as we can push and as long as + // the underlying snapshot returns strings to us + while (canPush && typeof (chunk = snapshot.read()) === 'string') { + canPush = this.push(chunk); + } + + // Signal EOS by pushing NULL + if (typeof chunk !== 'string') { + this.push(null); + } + } catch (error) { + this.emit('error', error); + } + }, + encoding: encoding.UTF8 // very important, so that strings are passed around and not buffers! + }); + } + + private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { const absolutePath = this.toAbsolutePath(resource); // 1.) check file 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 3a97a2a5cecac..378b1b91a08d1 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -22,6 +22,7 @@ import { onError } from 'vs/base/test/common/utils'; 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'; +import { TextModel } from 'vs/editor/common/model/textModel'; suite('FileService', () => { let service: FileService; @@ -581,6 +582,54 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent (ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'small.txt')); + + service.resolveContent(resource).done(c => { + assert.equal(c.value, 'Small File'); + + const model = TextModel.createFromString('Updates to the small file'); + + return service.updateContent(c.resource, model.createSnapshot()).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), 'Updates to the small file'); + + model.dispose(); + + done(); + }); + }, error => onError(error, done)); + }); + + test('updateContent (large file)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'lorem.txt')); + + service.resolveContent(resource).done(c => { + const newValue = c.value + c.value; + c.value = newValue; + + return service.updateContent(c.resource, c.value).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), newValue); + + done(); + }); + }, error => onError(error, done)); + }); + + test('updateContent (large file, ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'lorem.txt')); + + service.resolveContent(resource).done(c => { + const newValue = c.value + c.value; + const model = TextModel.createFromString(newValue); + + return service.updateContent(c.resource, model.createSnapshot()).then(c => { + assert.equal(fs.readFileSync(resource.fsPath), newValue); + + done(); + }); + }, error => onError(error, done)); + }); + test('updateContent - use encoding (UTF 16 BE)', function (done: () => void) { const resource = uri.file(path.join(testDir, 'small.txt')); const encoding = 'utf16be'; @@ -602,6 +651,31 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent - use encoding (UTF 16 BE, ITextSnapShot)', function (done: () => void) { + const resource = uri.file(path.join(testDir, 'small.txt')); + const encoding = 'utf16be'; + + service.resolveContent(resource).done(c => { + c.encoding = encoding; + + const model = TextModel.createFromString(c.value); + + return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { + return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { + assert.equal(enc, encodingLib.UTF16be); + + return service.resolveContent(resource).then(c => { + assert.equal(c.encoding, encoding); + + model.dispose(); + + done(); + }); + }); + }); + }, error => onError(error, done)); + }); + test('updateContent - encoding preserved (UTF 16 LE)', function (done: () => void) { const encoding = 'utf16le'; const resource = uri.file(path.join(testDir, 'some_utf16le.css')); @@ -625,6 +699,31 @@ suite('FileService', () => { }, error => onError(error, done)); }); + test('updateContent - encoding preserved (UTF 16 LE, ITextSnapShot)', function (done: () => void) { + const encoding = 'utf16le'; + const resource = uri.file(path.join(testDir, 'some_utf16le.css')); + + service.resolveContent(resource).done(c => { + assert.equal(c.encoding, encoding); + + const model = TextModel.createFromString('Some updates'); + + return service.updateContent(c.resource, model.createSnapshot(), { encoding: encoding }).then(c => { + return encodingLib.detectEncodingByBOM(c.resource.fsPath).then((enc) => { + assert.equal(enc, encodingLib.UTF16le); + + return service.resolveContent(resource).then(c => { + assert.equal(c.encoding, encoding); + + model.dispose(); + + done(); + }); + }); + }); + }, error => onError(error, done)); + }); + test('resolveContent - large file', function (done: () => void) { const resource = uri.file(path.join(testDir, 'lorem.txt')); @@ -846,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/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fc18f6c565646..88197be4a82f5 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -188,7 +188,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - const firstLineText = this.getFirstLineText(this.textEditorModel.getValue()); + const firstLineText = this.getFirstLineText(this.textEditorModel.createSnapshot()); const mode = this.getOrCreateMode(this.modeService, modeId, firstLineText); this.modelService.setMode(this.textEditorModel, mode); @@ -702,7 +702,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Save to Disk // mark the save operation as currently pending with the versionId (it might have changed from a save participant triggering) diag(`doSave(${versionId}) - before updateContent()`, this.resource, new Date()); - return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.getValue(), { + return this.saveSequentializer.setPending(newVersionId, this.fileService.updateContent(this.lastResolvedDiskStat.resource, this.createSnapshot(), { overwriteReadonly: options.overwriteReadonly, overwriteEncoding: options.overwriteEncoding, mtime: this.lastResolvedDiskStat.mtime, diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 6e5bf84bf4a63..42f1d46637001 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -32,6 +32,8 @@ import { Schemas } from 'vs/base/common/network'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IRevertOptions } from 'vs/platform/editor/common/editor'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; +import { IModelService } from 'vs/editor/common/services/modelService'; export interface IBackupResult { didBackup: boolean; @@ -73,7 +75,8 @@ export abstract class TextFileService implements ITextFileService { private backupFileService: IBackupFileService, private windowsService: IWindowsService, private historyService: IHistoryService, - contextKeyService: IContextKeyService + contextKeyService: IContextKeyService, + private modelService: IModelService ) { this.toUnbind = []; @@ -241,7 +244,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 +253,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); @@ -615,7 +618,7 @@ export abstract class TextFileService implements ITextFileService { // take over encoding and model value from source model targetModel.updatePreferredEncoding(sourceModel.getEncoding()); - targetModel.textEditorModel.setValue(sourceModel.getValue()); + this.modelService.updateModel(targetModel.textEditorModel, createTextBufferFactoryFromSnapshot(sourceModel.createSnapshot())); // save model return targetModel.save(options); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 4be8ffb329e92..eacc46d19705f 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'; @@ -200,7 +200,7 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport revert(soft?: boolean): TPromise; - getValue(): string; + createSnapshot(): ITextSnapshot; isDirty(): boolean; diff --git a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts index ecf4229c83b9a..1dcfc4c6334c0 100644 --- a/src/vs/workbench/services/textfile/electron-browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/textFileService.ts @@ -30,6 +30,7 @@ import { IWindowsService, IWindowService } from 'vs/platform/windows/common/wind import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IModelService } from 'vs/editor/common/services/modelService'; export class TextFileService extends AbstractTextFileService { @@ -41,6 +42,7 @@ export class TextFileService extends AbstractTextFileService { @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @IModeService private modeService: IModeService, + @IModelService modelService: IModelService, @IWindowService private windowService: IWindowService, @IEnvironmentService environmentService: IEnvironmentService, @IMessageService messageService: IMessageService, @@ -49,7 +51,7 @@ export class TextFileService extends AbstractTextFileService { @IHistoryService historyService: IHistoryService, @IContextKeyService contextKeyService: IContextKeyService ) { - super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService, historyService, contextKeyService); + super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, environmentService, backupFileService, windowsService, historyService, contextKeyService, modelService); } public resolveTextContent(resource: URI, options?: IResolveContentOptions): TPromise { diff --git a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts index 5e36065442d70..d747bb23b3f35 100644 --- a/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileEditorModel.test.ts @@ -14,7 +14,7 @@ import { ITextFileService, ModelState, StateChange } from 'vs/workbench/services import { workbenchInstantiationService, TestTextFileService, createFileInput, TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { onError, toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; -import { FileOperationResult, FileOperationError, IFileService } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, IFileService, snapshotToString } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; class ServiceAccessor { @@ -284,7 +284,7 @@ suite('Files - TextFileEditorModel', () => { model.onDidStateChange(e => { if (e === StateChange.SAVED) { - assert.equal(model.getValue(), 'bar'); + assert.equal(snapshotToString(model.createSnapshot()), 'bar'); assert.ok(!model.isDirty()); eventCounter++; } diff --git a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts index f5bde377342c7..3c904f154a425 100644 --- a/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts +++ b/src/vs/workbench/services/textmodelResolver/test/textModelResolverService.test.ts @@ -22,6 +22,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { once } from 'vs/base/common/event'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @@ -73,7 +74,7 @@ suite('Workbench - TextModelResolverService', () => { input.resolve().then(model => { assert.ok(model); - assert.equal((model as ResourceEditorModel).getValue(), 'Hello Test'); + assert.equal(snapshotToString((model as ResourceEditorModel).createSnapshot()), 'Hello Test'); let disposed = false; once(model.onDispose)(() => { diff --git a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts index 85c63eb3e1bbb..bed57cb0b054a 100644 --- a/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/common/editor/resourceEditorInput.test.ts @@ -13,6 +13,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @@ -39,7 +40,7 @@ suite('Workbench - ResourceEditorInput', () => { return input.resolve().then((model: ResourceEditorModel) => { assert.ok(model); - assert.equal(model.getValue(), 'function test() {}'); + assert.equal(snapshotToString(model.createSnapshot()), 'function test() {}'); }); }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/editor/untitledEditor.test.ts b/src/vs/workbench/test/common/editor/untitledEditor.test.ts index b7b8a04dd4a0a..8edf3bb24aae8 100644 --- a/src/vs/workbench/test/common/editor/untitledEditor.test.ts +++ b/src/vs/workbench/test/common/editor/untitledEditor.test.ts @@ -17,6 +17,7 @@ import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorMo import { IModeService } from 'vs/editor/common/services/modeService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; +import { snapshotToString } from 'vs/platform/files/common/files'; export class TestUntitledEditorService extends UntitledEditorService { @@ -142,7 +143,7 @@ suite('Workbench - Untitled Editor', () => { assert.ok(!model1.isDirty()); return service.loadOrCreate({ initialValue: 'Hello World' }).then(model2 => { - assert.equal(model2.getValue(), 'Hello World'); + assert.equal(snapshotToString(model2.createSnapshot()), 'Hello World'); const input = service.createOrGet(); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts index d6f3681d33193..d83503b679ec9 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadSaveParticipant.test.ts @@ -17,6 +17,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { ITextFileService, SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { snapshotToString } from 'vs/platform/files/common/files'; class ServiceAccessor { constructor( @ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService) { @@ -51,25 +52,25 @@ suite('MainThreadSaveParticipant', function () { let lineContent = ''; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // No new line if last line already empty lineContent = `Hello New Line${model.textEditorModel.getEOL()}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // New empty line added (single line) lineContent = 'Hello New Line'; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.equal(snapshotToString(model.createSnapshot()), `${lineContent}${model.textEditorModel.getEOL()}`); // New empty line added (multi line) lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`); + assert.equal(snapshotToString(model.createSnapshot()), `${lineContent}${model.textEditorModel.getEOL()}`); done(); }); @@ -91,25 +92,25 @@ suite('MainThreadSaveParticipant', function () { let lineContent = `${textContent}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // No new line removal if last line is single new line lineContent = `${textContent}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), lineContent); + assert.equal(snapshotToString(model.createSnapshot()), lineContent); // Remove new line (single line with two new lines) lineContent = `${textContent}${eol}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${textContent}${eol}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}`); // Remove new lines (multiple lines with multiple new lines) lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`; model.textEditorModel.setValue(lineContent); participant.participate(model, { reason: SaveReason.EXPLICIT }); - assert.equal(model.getValue(), `${textContent}${eol}${textContent}${eol}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}${eol}${textContent}${eol}`); done(); }); @@ -134,11 +135,11 @@ suite('MainThreadSaveParticipant', function () { model.textEditorModel.pushEditOperations([new Selection(1, 14, 1, 14)], textEdits, () => { return [new Selection(1, 15, 1, 15)]; }); // undo model.textEditorModel.undo(); - assert.equal(model.getValue(), `${textContent}`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}`); // trim final new lines should not mess the undo stack participant.participate(model, { reason: SaveReason.EXPLICIT }); model.textEditorModel.redo(); - assert.equal(model.getValue(), `${textContent}.`); + assert.equal(snapshotToString(model.createSnapshot()), `${textContent}.`); done(); }); }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index e45bc1a4b106b..e5db572cc9115 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -33,7 +33,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { IEditorGroupService, GroupArrangement, GroupOrientation, IEditorTabOptions, IMoveOptions } from 'vs/workbench/services/group/common/groupService'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions } from 'vs/platform/files/common/files'; +import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, IImportResult, FileChangesEvent, IResolveFileOptions, IContent, IUpdateContentOptions, IStreamContent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; @@ -180,9 +180,10 @@ export class TestTextFileService extends TextFileService { @IBackupFileService backupFileService: IBackupFileService, @IWindowsService windowsService: IWindowsService, @IHistoryService historyService: IHistoryService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IModelService modelService: IModelService ) { - super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, windowsService, historyService, contextKeyService); + super(lifecycleService, contextService, configurationService, fileService, untitledEditorService, instantiationService, messageService, TestEnvironmentService, backupFileService, windowsService, historyService, contextKeyService, modelService); } public setPromptPath(path: string): void { @@ -755,7 +756,7 @@ export class TestFileService implements IFileService { }); } - updateContent(resource: URI, value: string, options?: IUpdateContentOptions): TPromise { + updateContent(resource: URI, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { return TPromise.timeout(1).then(() => { return { resource, @@ -856,7 +857,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); }