From 66b2e1816e680dd921d3169822a7443b0b2c1629 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 16 Apr 2019 08:51:33 +0200 Subject: [PATCH] files2 - first cut ITextFileService.read() --- src/vs/editor/common/model/textModel.ts | 26 +++++--- src/vs/platform/files/common/files.ts | 4 +- .../workbench/contrib/files/common/files.ts | 2 +- .../common/walkThroughContentProvider.ts | 4 +- .../textfile/common/textFileEditorModel.ts | 2 +- .../textfile/common/textFileService.ts | 30 ++++++++- .../services/textfile/common/textfiles.ts | 5 ++ .../services/textfile/node/textFileService.ts | 6 +- .../textfile/test/fixtures/binary.txt | Bin 0 -> 274 bytes .../textfile/test/textFileService.io.test.ts | 58 +++++++++++++++--- .../workbench/test/workbenchTestServices.ts | 4 ++ 11 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 src/vs/workbench/services/textfile/test/fixtures/binary.txt diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index c9e1c4b1652..3d8e5a4f8c4 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -33,6 +33,7 @@ import { BracketsUtils, RichEditBracket, RichEditBrackets } from 'vs/editor/comm import { IStringStream, ITextSnapshot } from 'vs/platform/files/common/files'; import { ITheme, ThemeColor } from 'vs/platform/theme/common/themeService'; import { withUndefinedAsNull } from 'vs/base/common/types'; +import { VSBufferReadableStream, VSBuffer } from 'vs/base/common/buffer'; const CHEAP_TOKENIZATION_LENGTH_LIMIT = 2048; @@ -46,30 +47,41 @@ export function createTextBufferFactory(text: string): model.ITextBufferFactory return builder.finish(); } -export function createTextBufferFactoryFromStream(stream: IStringStream, filter?: (chunk: string) => string): Promise { - return new Promise((c, e) => { +export function createTextBufferFactoryFromStream(stream: IStringStream, filter?: (chunk: string) => string, validator?: (chunk: string) => Error | undefined): Promise; +export function createTextBufferFactoryFromStream(stream: VSBufferReadableStream, filter?: (chunk: VSBuffer) => VSBuffer, validator?: (chunk: VSBuffer) => Error | undefined): Promise; +export function createTextBufferFactoryFromStream(stream: IStringStream | VSBufferReadableStream, filter?: (chunk: any) => string | VSBuffer, validator?: (chunk: any) => Error | undefined): Promise { + return new Promise((resolve, reject) => { + const builder = createTextBufferBuilder(); + let done = false; - let builder = createTextBufferBuilder(); - stream.on('data', (chunk) => { + stream.on('data', (chunk: string | VSBuffer) => { + if (validator) { + const error = validator(chunk); + if (error) { + done = true; + reject(error); + } + } + if (filter) { chunk = filter(chunk); } - builder.acceptChunk(chunk); + builder.acceptChunk((typeof chunk === 'string') ? chunk : chunk.toString()); }); stream.on('error', (error) => { if (!done) { done = true; - e(error); + reject(error); } }); stream.on('end', () => { if (!done) { done = true; - c(builder.finish()); + resolve(builder.finish()); } }); }); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ac9ae20ce8a..9921ff0844e 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -656,8 +656,8 @@ export interface IContentData { * A Stream emitting strings. */ export interface IStringStream { - on(event: 'data', callback: (chunk: string) => void): void; - on(event: 'error', callback: (err: any) => void): void; + on(event: 'data', callback: (data: string) => void): void; + on(event: 'error', callback: (err: Error) => void): void; on(event: 'end', callback: () => void): void; on(event: string, callback: any): void; } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index ebf1358a266..04dcc62f436 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -176,7 +176,7 @@ export class FileOnDiskContentProvider implements ITextModelContentProvider { private resolveEditorModel(resource: URI, createAsNeeded: boolean = true): Promise { const savedFileResource = toLocalResource(resource, this.environmentService.configuration.remoteAuthority); - return this.textFileService.read(savedFileResource).then(content => { + return this.textFileService.legacyRead(savedFileResource).then(content => { let codeEditorModel = this.modelService.getModel(resource); if (codeEditorModel) { this.modelService.updateModel(codeEditorModel, content.value); diff --git a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts index 7b2f981e34d..1b81fe0f909 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/common/walkThroughContentProvider.ts @@ -35,7 +35,7 @@ export class WalkThroughContentProvider implements ITextModelContentProvider, IW reject(err); } }); - }) : this.textFileService.read(URI.file(resource.fsPath)).then(content => content.value)); + }) : this.textFileService.legacyRead(URI.file(resource.fsPath)).then(content => content.value)); return content.then(content => { let codeEditorModel = this.modelService.getModel(resource); if (!codeEditorModel) { @@ -61,7 +61,7 @@ export class WalkThroughSnippetContentProvider implements ITextModelContentProvi } public provideTextContent(resource: URI): Promise { - return this.textFileService.read(URI.file(resource.fsPath)).then(content => { + return this.textFileService.legacyRead(URI.file(resource.fsPath)).then(content => { let codeEditorModel = this.modelService.getModel(resource); if (!codeEditorModel) { const j = parseInt(resource.fragment); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index dc3b2670e48..73aabdad94d 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -306,7 +306,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Resolve Content try { - const content = await this.textFileService.read(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }); + const content = await this.textFileService.legacyRead(this.resource, { acceptTextOnly: !allowBinary, etag, encoding: this.preferredEncoding }); // Clear orphaned state when loading was successful this.setOrphaned(false); diff --git a/src/vs/workbench/services/textfile/common/textFileService.ts b/src/vs/workbench/services/textfile/common/textFileService.ts index 73db260a201..276f819f362 100644 --- a/src/vs/workbench/services/textfile/common/textFileService.ts +++ b/src/vs/workbench/services/textfile/common/textFileService.ts @@ -37,6 +37,7 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { coalesce } from 'vs/base/common/arrays'; import { trim } from 'vs/base/common/strings'; +import { VSBuffer } from 'vs/base/common/buffer'; /** * The workbench file service implementation implements the raw file service spec and adds additional methods on top. @@ -366,9 +367,36 @@ export abstract class TextFileService extends Disposable implements ITextFileSer //#endregion - //#region primitives (resolve, create, move, delete, update) + //#region primitives (read, create, move, delete, update) async read(resource: URI, options?: IReadTextFileOptions): Promise { + const stream = await this.fileService.readFileStream(resource, options); + + // in case of acceptTextOnly: true, we check the first + // chunk for possibly being binary by looking for 0-bytes + let checkedForBinary = false; + const throwOnBinary = (data: VSBuffer): Error | undefined => { + if (!checkedForBinary) { + checkedForBinary = true; + + for (let i = 0; i < data.byteLength && i < 512; i++) { + if (data.readUint8(i) === 0) { + throw new FileOperationError(nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), FileOperationResult.FILE_IS_BINARY, options); + } + } + } + + return undefined; + }; + + return { + ...stream, + encoding: 'utf8', + value: await createTextBufferFactoryFromStream(stream.value, undefined, options && options.acceptTextOnly ? throwOnBinary : undefined) + }; + } + + async legacyRead(resource: URI, options?: IReadTextFileOptions): Promise { const streamContent = await this.fileService.resolveStreamContent(resource, options); const value = await createTextBufferFactoryFromStream(streamContent.value); diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index e4bf6e09f4b..c1612b66775 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -100,6 +100,11 @@ export interface ITextFileService extends IDisposable { */ create(resource: URI, contents?: string | ITextSnapshot, options?: { overwrite?: boolean }): Promise; + /** + * @deprecated use read() instead + */ + legacyRead(resource: URI, options?: IReadTextFileOptions): Promise; + /** * Read the contents of a file identified by the resource. */ diff --git a/src/vs/workbench/services/textfile/node/textFileService.ts b/src/vs/workbench/services/textfile/node/textFileService.ts index 7d334df669e..0fef9b8bb4e 100644 --- a/src/vs/workbench/services/textfile/node/textFileService.ts +++ b/src/vs/workbench/services/textfile/node/textFileService.ts @@ -6,7 +6,7 @@ import { tmpdir } from 'os'; import { localize } from 'vs/nls'; import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, ITextFileContent } from 'vs/workbench/services/textfile/common/textfiles'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { URI } from 'vs/base/common/uri'; import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, IResourceEncoding, IReadTextFileOptions, stringToSnapshot, ICreateFileOptions, FileOperationError, FileOperationResult, IResourceEncodings } from 'vs/platform/files/common/files'; @@ -37,6 +37,10 @@ export class NodeTextFileService extends TextFileService { return this._encoding; } + async read(resource: URI, options?: IReadTextFileOptions): Promise { + return super.read(resource, options); + } + protected async doCreate(resource: URI, value?: string, options?: ICreateFileOptions): Promise { // check for encoding diff --git a/src/vs/workbench/services/textfile/test/fixtures/binary.txt b/src/vs/workbench/services/textfile/test/fixtures/binary.txt new file mode 100644 index 0000000000000000000000000000000000000000..fc30693d792253bf83b60e6f9bac20311570a186 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^oNn{1`ISV`@iy0XB4ude`@%$AjKtcBs*NBqf{L-T2REFS;{F0JNg)$>O13e=> zBSSL<4QEY-kc|A?#9{@f#M0cvygUV6g^ZGt0xNy}Vz6qxl+?0f-TXYgywnn%7SXFf zBSSo0978H@y*=+J$iTqEq;UDB{Up_Iw;Y)-?SJ%)JW!Uhl4UK2&W{}$K=T { const detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, encoding); - const resolved = await service.read(resource); + const resolved = await service.legacyRead(resource); assert.equal(resolved.encoding, encoding); assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), expectedContent); @@ -273,18 +274,18 @@ suite('Files - TextFileService i/o', () => { }); async function testEncodingKeepsData(resource: URI, encoding: string, expected: string) { - let resolved = await service.read(resource, { encoding }); + let resolved = await service.legacyRead(resource, { encoding }); const content = snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)); assert.equal(content, expected); await service.write(resource, content, { encoding }); - resolved = await service.read(resource, { encoding }); + resolved = await service.legacyRead(resource, { encoding }); assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); await service.write(resource, TextModel.createFromString(content).createSnapshot(), { encoding }); - resolved = await service.read(resource, { encoding }); + resolved = await service.legacyRead(resource, { encoding }); assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.CRLF).createSnapshot(false)), content); } @@ -295,7 +296,7 @@ suite('Files - TextFileService i/o', () => { await service.write(resource, content); - const resolved = await service.read(resource); + const resolved = await service.legacyRead(resource); assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), content); }); @@ -306,14 +307,14 @@ suite('Files - TextFileService i/o', () => { await service.write(resource, TextModel.createFromString(content).createSnapshot()); - const resolved = await service.read(resource); + const resolved = await service.legacyRead(resource); assert.equal(snapshotToString(resolved.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).createSnapshot(false)), content); }); test('write - encoding preserved (UTF 16 LE) - content as string', async () => { const resource = URI.file(join(testDir, 'some_utf16le.css')); - const resolved = await service.read(resource); + const resolved = await service.legacyRead(resource); assert.equal(resolved.encoding, UTF16le); await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, 'Hello\nWorld', 'Hello\nWorld'); @@ -322,7 +323,7 @@ suite('Files - TextFileService i/o', () => { test('write - encoding preserved (UTF 16 LE) - content as snapshot', async () => { const resource = URI.file(join(testDir, 'some_utf16le.css')); - const resolved = await service.read(resource); + const resolved = await service.legacyRead(resource); assert.equal(resolved.encoding, UTF16le); await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld'); @@ -412,4 +413,41 @@ suite('Files - TextFileService i/o', () => { let detectedEncoding = await detectEncodingByBOM(resource.fsPath); assert.equal(detectedEncoding, UTF8); }); + + test('read - small text', async () => { + const resource = URI.file(join(testDir, 'small.txt')); + + await testReadFile(resource); + }); + + test('read - large text', async () => { + const resource = URI.file(join(testDir, 'lorem.txt')); + + await testReadFile(resource); + }); + + async function testReadFile(resource: URI): Promise { + const result = await service.read(resource); + assert.equal(result.name, basename(resource.fsPath)); + assert.equal(result.size, statSync(resource.fsPath).size); + + assert.equal(snapshotToString(result.value.create(DefaultEndOfLine.LF).createSnapshot(false)), readFileSync(resource.fsPath)); + } + + test('read - FILE_IS_BINARY', async () => { + const resource = URI.file(join(testDir, 'binary.txt')); + + let error: FileOperationError | undefined = undefined; + try { + await service.read(resource, { acceptTextOnly: true }); + } catch (err) { + error = err; + } + + assert.ok(error); + assert.equal(error!.fileOperationResult, FileOperationResult.FILE_IS_BINARY); + + const result = await service.read(URI.file(join(testDir, 'small.txt')), { acceptTextOnly: true }); + assert.equal(result.name, 'small.txt'); + }); }); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 8af586c0384..bc7bf1bd868 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -235,6 +235,10 @@ export class TestTextFileService extends BrowserTextFileService { this.resolveTextContentError = error; } + public legacyRead(resource: URI, options?: IReadTextFileOptions): Promise { + return this.read(resource, options); + } + public read(resource: URI, options?: IReadTextFileOptions): Promise { if (this.resolveTextContentError) { const error = this.resolveTextContentError; -- GitLab