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,