diff --git a/src/typings/iconv-lite.d.ts b/src/typings/iconv-lite.d.ts
index 84c54e320cf372098f7e807547a8a9030d76ba6e..5ad19bb95b7c3d70185e9f498d5d31ed577fad58 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 4177f3ffab5c597dbb34156badb87cb2e318e779..1d134c6576791fb41cf20b6907b2a8c3c6535b98 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 b36dd2e9b22ff6eb5f1d2d5d9dc0b498ca12c71c..9b540108284e7316164c5ac01214d179ffbe5853 100644
--- a/src/vs/base/node/extfs.ts
+++ b/src/vs/base/node/extfs.ts
@@ -14,7 +14,6 @@ import * as fs from 'fs';
import * as paths from 'path';
import { TPromise } from 'vs/base/common/winjs.base';
import { nfcall } from 'vs/base/common/async';
-import { Readable } from 'stream';
const loop = flow.loop;
@@ -321,17 +320,17 @@ export function mv(source: string, target: string, callback: (error: Error) => v
}
let canFlush = true;
-export function writeFileAndFlush(path: string, data: string | NodeBuffer | Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void {
+export function writeFileAndFlush(path: string, data: string | NodeBuffer | NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void {
options = ensureOptions(options);
- if (data instanceof Readable) {
- doWriteFileStreamAndFlush(path, data, options, callback);
- } else {
+ if (typeof data === 'string' || Buffer.isBuffer(data)) {
doWriteFileAndFlush(path, data, options, callback);
+ } else {
+ doWriteFileStreamAndFlush(path, data, options, callback);
}
}
-function doWriteFileStreamAndFlush(path: string, reader: Readable, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void {
+function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: { mode?: number; flag?: string; }, callback: (error?: Error) => void): void {
// finish only once
let finished = false;
diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts
index ba1ab12a7ca659cf323793aa877b6ff7da47538e..9ebc819fd023676909051fe1f0b38c69bc23715f 100644
--- a/src/vs/base/node/pfs.ts
+++ b/src/vs/base/node/pfs.ts
@@ -13,7 +13,6 @@ import * as fs from 'fs';
import * as os from 'os';
import * as platform from 'vs/base/common/platform';
import { once } from 'vs/base/common/event';
-import { Readable } from 'stream';
export function readdir(path: string): TPromise {
return nfcall(extfs.readdir, path);
@@ -102,7 +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: Readable, 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 2e3e8b848ae87c91f8182d424ec6aadabd5a8f95..686934d9e0159932bdc296692821c3b916370dbe 100644
--- a/src/vs/base/test/node/extfs/extfs.test.ts
+++ b/src/vs/base/test/node/extfs/extfs.test.ts
@@ -50,7 +50,8 @@ function toReadable(value: string, throwError?: boolean): Readable {
if (!res) {
this.push(null);
}
- }
+ },
+ encoding: 'utf8'
});
}
diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts
index af89c431ab68705e9bd8de7648db91fb110f10f3..0fe4bf63718e240562aea045fff2030c7c8a6dc4 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 524ffbdf8d6ecb5a54014aa5fcc977c2854e3b3a..29550c7197f8383662779d154c88b385c9ef3545 100644
--- a/src/vs/workbench/common/editor/textEditorModel.ts
+++ b/src/vs/workbench/common/editor/textEditorModel.ts
@@ -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.
@@ -142,6 +143,15 @@ export abstract class BaseTextEditorModel extends EditorModel implements ITextEd
return null;
}
+ public createSnapshot(): ITextSnapshot {
+ const model = this.textEditorModel;
+ if (model) {
+ return model.createSnapshot(true /* Preserve BOM */);
+ }
+
+ return null;
+ }
+
public isResolved(): boolean {
return !!this.textEditorModelHandle;
}
diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts
index 5eeae64bf3a08803af96ea722a029a3a8165cc50..e97cc0ffd1d127e8f85df65ecde490f79b5a7c27 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 2bf1ba014e189d13be0b98688a5004876d6fda7f..1fb80d99042486b6de9b81c1ef34d18764475e78 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 b1475b44eb9b9508c480b5a2f9263e0d991c5ec5..d55169d2af085fe187c102ae3b065b3d658ae51f 100644
--- a/src/vs/workbench/services/files/node/fileService.ts
+++ b/src/vs/workbench/services/files/node/fileService.ts
@@ -11,7 +11,7 @@ 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, snapshotToString } 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 +41,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,15 +506,16 @@ 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);
+ // We can currently only write strings elevated, so we need to convert snapshots properly
+ return this.doUpdateContentElevated(resource, typeof value === 'string' ? value : snapshotToString(value), options);
}
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 +581,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,6 +610,31 @@ export class FileService implements IFileService {
});
}
+ 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, options: IUpdateContentOptions = Object.create(null)): TPromise {
const absolutePath = this.toAbsolutePath(resource);
diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
index fc18f6c565646dc4e76d38a6dfd8a1679f5c6237..beb2ea20d74809308449a4431b862fe2d65bb5f0 100644
--- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
+++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts
@@ -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/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts
index e45bc1a4b106b763a0376ce937ebb7221be3ebc2..e6f1b33310f38f0ba7ffc65b64ee0a7a21bfdfea 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';
@@ -755,7 +755,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,