未验证 提交 85753fd6 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #72132 from Microsoft/ben/files2-save

Rewrite file saving code to use file system provider
......@@ -14,6 +14,10 @@ export const UTF8_with_bom = 'utf8bom';
export const UTF16be = 'utf16be';
export const UTF16le = 'utf16le';
export const UTF16be_BOM = [0xFE, 0xFF];
export const UTF16le_BOM = [0xFF, 0xFE];
export const UTF8_BOM = [0xEF, 0xBB, 0xBF];
export interface IDecodeStreamOptions {
guessEncoding?: boolean;
minBytesRequiredForDetection?: number;
......@@ -150,12 +154,12 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | null, bytesRead:
const b1 = buffer.readUInt8(1);
// UTF-16 BE
if (b0 === 0xFE && b1 === 0xFF) {
if (b0 === UTF16be_BOM[0] && b1 === UTF16be_BOM[1]) {
return UTF16be;
}
// UTF-16 LE
if (b0 === 0xFF && b1 === 0xFE) {
if (b0 === UTF16le_BOM[0] && b1 === UTF16le_BOM[1]) {
return UTF16le;
}
......@@ -166,7 +170,7 @@ export function detectEncodingByBOMFromBuffer(buffer: Buffer | null, bytesRead:
const b2 = buffer.readUInt8(2);
// UTF-8
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
if (b0 === UTF8_BOM[0] && b1 === UTF8_BOM[1] && b2 === UTF8_BOM[2]) {
return UTF8;
}
......
......@@ -124,11 +124,6 @@ export interface IFileService {
*/
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent>;
/**
* @deprecated use writeFile instead
*/
updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata>;
/**
* Updates the content replacing its previous value.
*/
......@@ -148,18 +143,13 @@ export interface IFileService {
*/
copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
/**
* @deprecated use createFile2 instead
*/
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata>;
/**
* Creates a new file with the given path and optional contents. The returned promise
* will have the stat model object as a result.
*
* The optional parameter content can be used as value to fill into the new file.
*/
createFile2(resource: URI, bufferOrReadable?: VSBuffer | VSBufferReadable, options?: ICreateFileOptions): Promise<IFileStatWithMetadata>;
createFile(resource: URI, bufferOrReadable?: VSBuffer | VSBufferReadable, options?: ICreateFileOptions): Promise<IFileStatWithMetadata>;
/**
* Creates a new folder with the given path. The returned promise
......@@ -666,6 +656,7 @@ export interface ITextSnapshot {
*/
export function snapshotToString(snapshot: ITextSnapshot): string {
const chunks: string[] = [];
let chunk: string | null;
while (typeof (chunk = snapshot.read()) === 'string') {
chunks.push(chunk);
......@@ -674,6 +665,22 @@ export function snapshotToString(snapshot: ITextSnapshot): string {
return chunks.join('');
}
export function stringToSnapshot(value: string): ITextSnapshot {
let done = false;
return {
read(): string | null {
if (!done) {
done = true;
return value;
}
return null;
}
};
}
export class TextSnapshotReadable implements VSBufferReadable {
private preambleHandled: boolean;
......@@ -703,6 +710,22 @@ export class TextSnapshotReadable implements VSBufferReadable {
}
}
export function toBufferOrReadable(value: string): VSBuffer;
export function toBufferOrReadable(value: ITextSnapshot): VSBufferReadable;
export function toBufferOrReadable(value: string | ITextSnapshot): VSBuffer | VSBufferReadable;
export function toBufferOrReadable(value: string | ITextSnapshot | undefined): VSBuffer | VSBufferReadable | undefined;
export function toBufferOrReadable(value: string | ITextSnapshot | undefined): VSBuffer | VSBufferReadable | undefined {
if (typeof value === 'undefined') {
return undefined;
}
if (typeof value === 'string') {
return VSBuffer.fromString(value);
}
return new TextSnapshotReadable(value);
}
/**
* Streamable content and meta information of a file.
*/
......@@ -1158,8 +1181,4 @@ export interface ILegacyFileService extends IDisposable {
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent>;
resolveStreamContent(resource: URI, options?: IResolveContentOptions): Promise<IStreamContent>;
updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata>;
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata>;
}
\ No newline at end of file
......@@ -5,28 +5,21 @@
import * as paths from 'vs/base/common/path';
import * as fs from 'fs';
import * as os from 'os';
import * as assert from 'assert';
import { FileOperation, FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IWriteTextFileOptions, ICreateFileOptions, IContentData, ITextSnapshot, ILegacyFileService, IFileStatWithMetadata, IFileService, IFileSystemProvider, etag } from 'vs/platform/files/common/files';
import { FileOperationEvent, IContent, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IContentData, ILegacyFileService, IFileService, IFileSystemProvider } from 'vs/platform/files/common/files';
import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/fileConstants';
import * as objects from 'vs/base/common/objects';
import { timeout } from 'vs/base/common/async';
import { URI as uri } from 'vs/base/common/uri';
import * as nls from 'vs/nls';
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import * as pfs from 'vs/base/node/pfs';
import { detectEncodingFromBuffer, decodeStream, detectEncodingByBOM, UTF8 } from 'vs/base/node/encoding';
import { detectEncodingFromBuffer, decodeStream } from 'vs/base/node/encoding';
import { Event, Emitter } from 'vs/base/common/event';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { Schemas } from 'vs/base/common/network';
import { onUnexpectedError } from 'vs/base/common/errors';
import product from 'vs/platform/product/node/product';
import { IEncodingOverride, ResourceEncodings } from 'vs/workbench/services/files/node/encoding';
import { createReadableOfSnapshot } from 'vs/workbench/services/files/node/streams';
import { withUndefinedAsNull } from 'vs/base/common/types';
export interface IFileServiceTestOptions {
......@@ -380,290 +373,8 @@ export class LegacyFileService extends Disposable implements ILegacyFileService
//#endregion
//#region File Writing
updateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
if (options.writeElevated) {
return this.doUpdateContentElevated(resource, value, options);
}
return this.doUpdateContent(resource, value, options);
}
private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
const absolutePath = this.toAbsolutePath(resource);
// 1.) check file for writing
return this.checkFileBeforeWriting(absolutePath, options).then(exists => {
let createParentsPromise: Promise<any>;
if (exists) {
createParentsPromise = Promise.resolve();
} else {
createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath));
}
// 2.) create parents as needed
return createParentsPromise.then(() => {
const { encoding, hasBOM } = this._encoding.getWriteEncoding(resource, options.encoding);
let addBomPromise: Promise<boolean> = Promise.resolve(false);
// Some encodings come with a BOM automatically
if (hasBOM) {
addBomPromise = Promise.resolve(hasBOM);
}
// Existing UTF-8 file: check for options regarding BOM
else if (exists && encoding === UTF8) {
if (options.overwriteEncoding) {
addBomPromise = Promise.resolve(false); // if we are to overwrite the encoding, we do not preserve it if found
} else {
addBomPromise = detectEncodingByBOM(absolutePath).then(enc => enc === UTF8); // otherwise preserve it if found
}
}
// 3.) check to add UTF BOM
return addBomPromise.then(addBom => {
// 4.) set contents and resolve
if (!exists || !isWindows) {
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding);
}
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows
// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/Microsoft/vscode/issues/6363)
else {
// 4.) truncate
return pfs.truncate(absolutePath, 0).then(() => {
// 5.) set contents (with r+ mode) and resolve
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding, { flag: 'r+' }).then(undefined, error => {
if (this.environmentService.verbose) {
console.error(`Truncate succeeded, but save failed (${error}), retrying after 100ms`);
}
// We heard from one user that fs.truncate() succeeds, but the save fails (https://github.com/Microsoft/vscode/issues/61310)
// In that case, the file is now entirely empty and the contents are gone. This can happen if an external file watcher is
// installed that reacts on the truncate and keeps the file busy right after. Our workaround is to retry to save after a
// short timeout, assuming that the file is free to write then.
return timeout(100).then(() => this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding, { flag: 'r+' }));
});
}, error => {
if (this.environmentService.verbose) {
console.error(`Truncate failed (${error}), falling back to normal save`);
}
// we heard from users that fs.truncate() fails (https://github.com/Microsoft/vscode/issues/59561)
// in that case we simply save the file without truncating first (same as macOS and Linux)
return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encoding);
});
}
});
});
}).then(undefined, error => {
if (error.code === 'EACCES' || error.code === 'EPERM') {
return Promise.reject(new FileOperationError(
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
FileOperationResult.FILE_PERMISSION_DENIED,
options
));
}
return Promise.reject(error);
});
}
private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): Promise<IFileStat> {
// Configure encoding related options as needed
const writeFileOptions: pfs.IWriteFileOptions = options ? options : Object.create(null);
if (addBOM || encodingToWrite !== UTF8) {
writeFileOptions.encoding = {
charset: encodingToWrite,
addBOM
};
}
let writeFilePromise: Promise<void>;
if (typeof value === 'string') {
writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions);
} else {
writeFilePromise = pfs.writeFile(absolutePath, createReadableOfSnapshot(value), writeFileOptions);
}
// set contents
return writeFilePromise.then(() => {
// resolve
return this.fileService.resolve(resource);
});
}
private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IWriteTextFileOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
const absolutePath = this.toAbsolutePath(resource);
// 1.) check file for writing
return this.checkFileBeforeWriting(absolutePath, options, options.overwriteReadonly /* ignore readonly if we overwrite readonly, this is handled via sudo later */).then(exists => {
const writeOptions: IWriteTextFileOptions = objects.assign(Object.create(null), options);
writeOptions.writeElevated = false;
writeOptions.encoding = this._encoding.getWriteEncoding(resource, options.encoding).encoding;
// 2.) write to a temporary file to be able to copy over later
const tmpPath = paths.join(os.tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`);
return this.updateContent(uri.file(tmpPath), value, writeOptions).then(() => {
// 3.) invoke our CLI as super user
return import('sudo-prompt').then(sudoPrompt => {
return new Promise<void>((resolve, reject) => {
const promptOptions = {
name: this.environmentService.appNameLong.replace('-', ''),
icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined
};
const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`];
if (options.overwriteReadonly) {
sudoCommand.push('--file-chmod');
}
sudoCommand.push('--file-write', `"${tmpPath}"`, `"${absolutePath}"`);
sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error: string, stdout: string, stderr: string) => {
if (error || stderr) {
reject(error || stderr);
} else {
resolve(undefined);
}
});
});
}).then(() => {
// 3.) delete temp file
return pfs.rimraf(tmpPath, pfs.RimRafMode.MOVE).then(() => {
// 4.) resolve again
return this.fileService.resolve(resource);
});
});
});
}).then(undefined, error => {
if (this.environmentService.verbose) {
onUnexpectedError(`Unable to write to file '${resource.toString(true)}' as elevated user (${error})`);
}
if (!FileOperationError.isFileOperationError(error)) {
error = new FileOperationError(
nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)),
FileOperationResult.FILE_PERMISSION_DENIED,
options
);
}
return Promise.reject(error);
});
}
//#endregion
//#region Create File
createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): Promise<IFileStatWithMetadata> {
const absolutePath = this.toAbsolutePath(resource);
let checkFilePromise: Promise<boolean>;
if (options.overwrite) {
checkFilePromise = Promise.resolve(false);
} else {
checkFilePromise = pfs.exists(absolutePath);
}
// Check file exists
return checkFilePromise.then(exists => {
if (exists && !options.overwrite) {
return Promise.reject(new FileOperationError(
nls.localize('fileExists', "File to create already exists ({0})", resource.toString(true)),
FileOperationResult.FILE_MODIFIED_SINCE,
options
));
}
// Create file
return this.updateContent(resource, content).then(result => {
// Events
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result));
return result;
});
});
}
//#endregion
//#region Helpers
private checkFileBeforeWriting(absolutePath: string, options: IWriteTextFileOptions = Object.create(null), ignoreReadonly?: boolean): Promise<boolean /* exists */> {
return pfs.exists(absolutePath).then(exists => {
if (exists) {
return pfs.stat(absolutePath).then(stat => {
if (stat.isDirectory()) {
return Promise.reject(new Error('Expected file is actually a directory'));
}
// Dirty write prevention: if the file on disk has been changed and does not match our expected
// mtime and etag, we bail out to prevent dirty writing.
//
// First, we check for a mtime that is in the future before we do more checks. The assumption is
// that only the mtime is an indicator for a file that has changd on disk.
//
// Second, if the mtime has advanced, we compare the size of the file on disk with our previous
// one using the etag() function. Relying only on the mtime check has prooven to produce false
// positives due to file system weirdness (especially around remote file systems). As such, the
// check for size is a weaker check because it can return a false negative if the file has changed
// but to the same length. This is a compromise we take to avoid having to produce checksums of
// the file content for comparison which would be much slower to compute.
if (typeof options.mtime === 'number' && typeof options.etag === 'string' && options.mtime < stat.mtime.getTime() && options.etag !== etag(stat.size, options.mtime)) {
return Promise.reject(new FileOperationError(nls.localize('fileModifiedError', "File Modified Since"), FileOperationResult.FILE_MODIFIED_SINCE, options));
}
// Throw if file is readonly and we are not instructed to overwrite
if (!ignoreReadonly && !(stat.mode & 128) /* readonly */) {
if (!options.overwriteReadonly) {
return this.readOnlyError<boolean>(options);
}
// Try to change mode to writeable
let mode = stat.mode;
mode = mode | 128;
return pfs.chmod(absolutePath, mode).then(() => {
// Make sure to check the mode again, it could have failed
return pfs.stat(absolutePath).then(stat => {
if (!(stat.mode & 128) /* readonly */) {
return this.readOnlyError<boolean>(options);
}
return exists;
});
});
}
return exists;
});
}
return exists;
});
}
private readOnlyError<T>(options: IWriteTextFileOptions): Promise<T> {
return Promise.reject(new FileOperationError(
nls.localize('fileReadOnlyError', "File is Read Only"),
FileOperationResult.FILE_READ_ONLY,
options
));
}
private toAbsolutePath(arg1: uri | IFileStat): string {
let resource: uri;
if (arg1 instanceof uri) {
......
......@@ -7,28 +7,16 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import * as resources from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { IDecodeStreamOptions, toDecodeStream, encodeStream } from 'vs/base/node/encoding';
import { IDecodeStreamOptions, toDecodeStream } from 'vs/base/node/encoding';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { localize } from 'vs/nls';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FileWriteOptions, FileSystemProviderCapabilities, IContent, ICreateFileOptions, IFileSystemProvider, IResolveContentOptions, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ILegacyFileService, IFileService, toFileOperationResult, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { FileOperationError, FileOperationResult, IContent, IFileSystemProvider, IResolveContentOptions, IStreamContent, ILegacyFileService, IFileService } from 'vs/platform/files/common/files';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { createReadableOfProvider, createReadableOfSnapshot, createWritableOfProvider } from 'vs/workbench/services/files/node/streams';
import { createReadableOfProvider } from 'vs/workbench/services/files/node/streams';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
class StringSnapshot implements ITextSnapshot {
private _value: string | null;
constructor(value: string) {
this._value = value;
}
read(): string | null {
let ret = this._value;
this._value = null;
return ret;
}
}
export class LegacyRemoteFileService extends LegacyFileService {
private readonly _provider: Map<string, IFileSystemProvider>;
......@@ -163,63 +151,6 @@ export class LegacyRemoteFileService extends LegacyFileService {
// --- saving
private static _throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider {
if (provider.capabilities & FileSystemProviderCapabilities.Readonly) {
throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED);
}
return provider;
}
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
if (resource.scheme === Schemas.file) {
return super.createFile(resource, content, options);
} else {
return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return this.fileService.createFolder(resources.dirname(resource)).then(() => {
const { encoding } = this.encoding.getWriteEncoding(resource);
return this._writeFile(provider, resource, new StringSnapshot(content || ''), encoding, { create: true, overwrite: Boolean(options && options.overwrite) });
});
}).then(fileStat => {
this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, fileStat));
return fileStat;
}, err => {
const message = localize('err.create', "Failed to create file {0}", resource.toString(false));
const result = toFileOperationResult(err);
throw new FileOperationError(message, result, options);
});
}
}
updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
if (resource.scheme === Schemas.file) {
return super.updateContent(resource, value, options);
} else {
return this._withProvider(resource).then(LegacyRemoteFileService._throwIfFileSystemIsReadonly).then(provider => {
return this.fileService.createFolder(resources.dirname(resource)).then(() => {
const snapshot = typeof value === 'string' ? new StringSnapshot(value) : value;
return this._writeFile(provider, resource, snapshot, options && options.encoding, { create: true, overwrite: true });
});
});
}
}
private _writeFile(provider: IFileSystemProvider, resource: URI, snapshot: ITextSnapshot, preferredEncoding: string | undefined = undefined, options: FileWriteOptions): Promise<IFileStatWithMetadata> {
const readable = createReadableOfSnapshot(snapshot);
const { encoding, hasBOM } = this.encoding.getWriteEncoding(resource, preferredEncoding);
const encoder = encodeStream(encoding, { addBOM: hasBOM });
const target = createWritableOfProvider(provider, resource, options);
return new Promise((resolve, reject) => {
readable.pipe(encoder).pipe(target);
target.once('error', err => reject(err));
target.once('finish', (_: unknown) => resolve(undefined));
}).then(_ => {
return this.fileService.resolve(resource, { resolveMetadata: true }) as Promise<IFileStatWithMetadata>;
});
}
private static _asContent(content: IStreamContent): Promise<IContent> {
return new Promise<IContent>((resolve, reject) => {
let result: IContent = {
......
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
// import * as fs from 'fs';
import * as path from 'vs/base/common/path';
import * as os from 'os';
import * as assert from 'assert';
......@@ -17,7 +17,6 @@ import { TestEnvironmentService, TestContextService, TestTextResourceConfigurati
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
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';
import { IEncodingOverride } from 'vs/workbench/services/files/node/encoding';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
......@@ -53,153 +52,6 @@ suite('LegacyFileService', () => {
return pfs.rimraf(parentDir, pfs.RimRafMode.MOVE);
});
test('updateContent - use encoding (UTF 16 BE)', function () {
const resource = uri.file(path.join(testDir, 'small.txt'));
const encoding = 'utf16be';
return service.resolveContent(resource).then(c => {
c.encoding = encoding;
return service.updateContent(c.resource, c.value, { 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);
});
});
});
});
});
test('updateContent - use encoding (UTF 16 BE, ITextSnapShot)', function () {
const resource = uri.file(path.join(testDir, 'small.txt'));
const encoding = 'utf16be';
return service.resolveContent(resource).then(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();
});
});
});
});
});
test('updateContent - encoding preserved (UTF 16 LE)', function () {
const encoding = 'utf16le';
const resource = uri.file(path.join(testDir, 'some_utf16le.css'));
return service.resolveContent(resource).then(c => {
assert.equal(c.encoding, encoding);
c.value = 'Some updates';
return service.updateContent(c.resource, c.value, { 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);
});
});
});
});
});
test('updateContent - encoding preserved (UTF 16 LE, ITextSnapShot)', function () {
const encoding = 'utf16le';
const resource = uri.file(path.join(testDir, 'some_utf16le.css'));
return service.resolveContent(resource).then(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();
});
});
});
});
});
test('updateContent - UTF 8 BOMs', function () {
// setup
const _id = uuid.generateUuid();
const _testDir = path.join(parentDir, _id);
const _sourceDir = getPathFromAmdModule(require, './fixtures/service');
const resource = uri.file(path.join(testDir, 'index.html'));
const fileService = new FileService2(new NullLogService());
fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(new NullLogService()));
const _service = new LegacyFileService(
fileService,
new TestContextService(new Workspace(_testDir, toWorkspaceFolders([{ path: _testDir }]))),
TestEnvironmentService,
new TestTextResourceConfigurationService()
);
return pfs.copy(_sourceDir, _testDir).then(() => {
return pfs.readFile(resource.fsPath).then(data => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
const model = TextModel.createFromString('Hello Bom');
// Update content: UTF_8 => UTF_8_BOM
return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8_with_bom }).then(() => {
return pfs.readFile(resource.fsPath).then(data => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8);
// Update content: PRESERVE BOM when using UTF-8
model.setValue('Please stay Bom');
return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => {
return pfs.readFile(resource.fsPath).then(data => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), encodingLib.UTF8);
// Update content: REMOVE BOM
model.setValue('Go away Bom');
return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8, overwriteEncoding: true }).then(() => {
return pfs.readFile(resource.fsPath).then(data => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
// Update content: BOM comes not back
model.setValue('Do not come back Bom');
return _service.updateContent(resource, model.createSnapshot(), { encoding: encodingLib.UTF8 }).then(() => {
return pfs.readFile(resource.fsPath).then(data => {
assert.equal(encodingLib.detectEncodingByBOMFromBuffer(data, 512), null);
model.dispose();
_service.dispose();
});
});
});
});
});
});
});
});
});
});
});
test('resolveContent - large file', function () {
const resource = uri.file(path.join(testDir, 'lorem.txt'));
......@@ -263,17 +115,17 @@ suite('LegacyFileService', () => {
});
});
test('resolveContent - FILE_MODIFIED_SINCE', function () {
const resource = uri.file(path.join(testDir, 'index.html'));
// test('resolveContent - FILE_MODIFIED_SINCE', function () {
// const resource = uri.file(path.join(testDir, 'index.html'));
return service.resolveContent(resource).then(c => {
fs.writeFileSync(resource.fsPath, 'Updates Incoming!');
// return service.resolveContent(resource).then(c => {
// fs.writeFileSync(resource.fsPath, 'Updates Incoming!');
return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => {
assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
});
});
});
// return service.updateContent(resource, c.value, { etag: c.etag, mtime: c.mtime - 1000 }).then(undefined, (e: FileOperationError) => {
// assert.equal(e.fileOperationResult, FileOperationResult.FILE_MODIFIED_SINCE);
// });
// });
// });
test('resolveContent - encoding picked up', function () {
const resource = uri.file(path.join(testDir, 'index.html'));
......
......@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable, combinedDisposable, dispose } from 'vs/base/common/lifecycle';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IWriteTextFileOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files';
import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, ILegacyFileService, IWriteFileOptions } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
......@@ -15,6 +15,7 @@ import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays';
import { getBaseLabel } from 'vs/base/common/labels';
import { ILogService } from 'vs/platform/log/common/log';
import { VSBuffer, VSBufferReadable, readableToBuffer, bufferToReadable } from 'vs/base/common/buffer';
import { Queue } from 'vs/base/common/async';
export class FileService2 extends Disposable implements IFileService {
......@@ -295,7 +296,7 @@ export class FileService2 extends Disposable implements IFileService {
return this._legacy.encoding;
}
async createFile2(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
async createFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
// validate overwrite
const overwrite = !!(options && options.overwrite);
......@@ -370,10 +371,6 @@ export class FileService2 extends Disposable implements IFileService {
return this.resolve(resource, { resolveMetadata: true });
}
createFile(resource: URI, content?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
return this.joinOnLegacy.then(legacy => legacy.createFile(resource, content, options));
}
resolveContent(resource: URI, options?: IResolveContentOptions): Promise<IContent> {
return this.joinOnLegacy.then(legacy => legacy.resolveContent(resource, options));
}
......@@ -382,10 +379,6 @@ export class FileService2 extends Disposable implements IFileService {
return this.joinOnLegacy.then(legacy => legacy.resolveStreamContent(resource, options));
}
updateContent(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
return this.joinOnLegacy.then(legacy => legacy.updateContent(resource, value, options));
}
//#endregion
//#region Move/Copy/Delete/Create Folder
......@@ -672,12 +665,10 @@ export class FileService2 extends Disposable implements IFileService {
}
private toWatchKey(provider: IFileSystemProvider, resource: URI, options: IWatchOptions): string {
const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
return [
isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase(), // lowercase path is the provider is case insensitive
String(options.recursive), // use recursive: true | false as part of the key
options.excludes.join() // use excludes as part of the key
this.toMapKey(provider, resource), // lowercase path if the provider is case insensitive
String(options.recursive), // use recursive: true | false as part of the key
options.excludes.join() // use excludes as part of the key
].join();
}
......@@ -692,7 +683,39 @@ export class FileService2 extends Disposable implements IFileService {
//#region Helpers
private writeQueues: Map<string, Queue<void>> = new Map();
private ensureWriteQueue(provider: IFileSystemProvider, resource: URI): Queue<void> {
// ensure to never write to the same resource without finishing
// the one write. this ensures a write finishes consistently
// (even with error) before another write is done.
const queueKey = this.toMapKey(provider, resource);
let writeQueue = this.writeQueues.get(queueKey);
if (!writeQueue) {
writeQueue = new Queue<void>();
this.writeQueues.set(queueKey, writeQueue);
const onFinish = Event.once(writeQueue.onFinished);
onFinish(() => {
this.writeQueues.delete(queueKey);
dispose(writeQueue);
});
}
return writeQueue;
}
private toMapKey(provider: IFileSystemProvider, resource: URI): string {
const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
return isPathCaseSensitive ? resource.toString() : resource.toString().toLowerCase();
}
private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readable: VSBufferReadable): Promise<void> {
return this.ensureWriteQueue(provider, resource).queue(() => this.doWriteBufferedQueued(provider, resource, readable));
}
private async doWriteBufferedQueued(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, readable: VSBufferReadable): Promise<void> {
// open handle
const handle = await provider.open(resource, { create: true });
......@@ -734,6 +757,9 @@ export class FileService2 extends Disposable implements IFileService {
}
private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeBufferedQueued(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
let sourceHandle: number | undefined = undefined;
let targetHandle: number | undefined = undefined;
......@@ -780,6 +806,10 @@ export class FileService2 extends Disposable implements IFileService {
}
private async doPipeUnbufferedToBuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
return this.ensureWriteQueue(targetProvider, target).queue(() => this.doPipeUnbufferedToBufferedQueued(sourceProvider, source, targetProvider, target));
}
private async doPipeUnbufferedToBufferedQueued(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise<void> {
// Open handle
const targetHandle = await targetProvider.open(target, { create: true });
......
......@@ -3,14 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { mkdir, open, close, read, write } from 'fs';
import { mkdir, open, close, read, write, fdatasync } from 'fs';
import { promisify } from 'util';
import { IDisposable, Disposable, toDisposable, dispose } from 'vs/base/common/lifecycle';
import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, FileSystemProviderErrorCode, createFileSystemProviderError, FileSystemProviderError } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { Event, Emitter } from 'vs/base/common/event';
import { isLinux, isWindows } from 'vs/base/common/platform';
import { statLink, readdir, unlink, move, copy, readFile, writeFile, fileExists, truncate, rimraf, RimRafMode } from 'vs/base/node/pfs';
import { statLink, readdir, unlink, move, copy, readFile, writeFile, truncate, rimraf, RimRafMode, exists } from 'vs/base/node/pfs';
import { normalize, basename, dirname } from 'vs/base/common/path';
import { joinPath } from 'vs/base/common/resources';
import { isEqual } from 'vs/base/common/extpath';
......@@ -116,14 +116,14 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
const filePath = this.toFilePath(resource);
// Validate target
const exists = await fileExists(filePath);
if (exists && !opts.overwrite) {
const fileExists = await exists(filePath);
if (fileExists && !opts.overwrite) {
throw createFileSystemProviderError(new Error(localize('fileExists', "File already exists")), FileSystemProviderErrorCode.FileExists);
} else if (!exists && !opts.create) {
} else if (!fileExists && !opts.create) {
throw createFileSystemProviderError(new Error(localize('fileNotExists', "File does not exist")), FileSystemProviderErrorCode.FileNotFound);
}
if (exists && isWindows) {
if (fileExists && isWindows) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
......@@ -154,16 +154,36 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
}
}
private writeHandles: Set<number> = new Set();
private canFlush: boolean = true;
async open(resource: URI, opts: FileOpenOptions): Promise<number> {
try {
const filePath = this.toFilePath(resource);
let flags: string;
let flags: string | undefined = undefined;
if (opts.create) {
// we take this as a hint that the file is opened for writing
if (isWindows && await exists(filePath)) {
try {
// On Windows and if the file exists, we use a different strategy of saving the file
// by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows
// (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams
// (see https://github.com/Microsoft/vscode/issues/6363)
await truncate(filePath, 0);
// After a successful truncate() the flag can be set to 'r+' which will not truncate.
flags = 'r+';
} catch (error) {
this.logService.trace(error);
}
}
// we take opts.create as a hint that the file is opened for writing
// as such we use 'w' to truncate an existing or create the
// file otherwise. we do not allow reading.
flags = 'w';
if (!flags) {
flags = 'w';
}
} else {
// otherwise we assume the file is opened for reading
// as such we use 'r' to neither truncate, nor create
......@@ -171,7 +191,14 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
flags = 'r';
}
return await promisify(open)(filePath, flags);
const handle = await promisify(open)(filePath, flags);
// remember that this handle was used for writing
if (opts.create) {
this.writeHandles.add(handle);
}
return handle;
} catch (error) {
throw this.toFileSystemProviderError(error);
}
......@@ -179,6 +206,19 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
async close(fd: number): Promise<void> {
try {
// if a handle is closed that was used for writing, ensure
// to flush the contents to disk if possible.
if (this.writeHandles.delete(fd) && this.canFlush) {
try {
await promisify(fdatasync)(fd);
} catch (error) {
// In some exotic setups it is well possible that node fails to sync
// In that case we disable flushing and log the error to our logger
this.canFlush = false;
this.logService.error(error);
}
}
return await promisify(close)(fd);
} catch (error) {
throw this.toFileSystemProviderError(error);
......@@ -199,6 +239,13 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
}
async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
// we know at this point that the file to write to is truncated and thus empty
// if the write now fails, the file remains empty. as such we really try hard
// to ensure the write succeeds by retrying up to three times.
return retry(() => this.doWrite(fd, pos, data, offset, length), 100 /* ms delay */, 3 /* retries */);
}
private async doWrite(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
try {
const result = await promisify(write)(fd, data, offset, length, pos);
if (typeof result === 'number') {
......@@ -295,7 +342,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
const isCaseChange = isPathCaseSensitive ? false : isEqual(fromFilePath, toFilePath, true /* ignore case */);
// handle existing target (unless this is a case change)
if (!isCaseChange && await fileExists(toFilePath)) {
if (!isCaseChange && await exists(toFilePath)) {
if (!overwrite) {
throw createFileSystemProviderError(new Error('File at target already exists'), FileSystemProviderErrorCode.FileExists);
}
......@@ -440,7 +487,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro
code = FileSystemProviderErrorCode.FileExists;
break;
case 'EPERM':
case 'EACCESS':
case 'EACCES':
code = FileSystemProviderErrorCode.NoPermissions;
break;
default:
......
......@@ -800,13 +800,13 @@ suite('Disk File Service', () => {
}
});
test('createFile2', async () => {
test('createFile', async () => {
let event: FileOperationEvent;
disposables.push(service.onAfterOperation(e => event = e));
const contents = 'Hello World';
const resource = URI.file(join(testDir, 'test.txt'));
const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents));
const fileStat = await service.createFile(resource, VSBuffer.fromString(contents));
assert.equal(fileStat.name, 'test.txt');
assert.equal(existsSync(fileStat.resource.fsPath), true);
assert.equal(readFileSync(fileStat.resource.fsPath), contents);
......@@ -817,21 +817,21 @@ suite('Disk File Service', () => {
assert.equal(event!.target!.resource.fsPath, resource.fsPath);
});
test('createFile2 (does not overwrite by default)', async () => {
test('createFile (does not overwrite by default)', async () => {
const contents = 'Hello World';
const resource = URI.file(join(testDir, 'test.txt'));
writeFileSync(resource.fsPath, ''); // create file
try {
await service.createFile2(resource, VSBuffer.fromString(contents));
await service.createFile(resource, VSBuffer.fromString(contents));
}
catch (error) {
assert.ok(error);
}
});
test('createFile2 (allows to overwrite existing)', async () => {
test('createFile (allows to overwrite existing)', async () => {
let event: FileOperationEvent;
disposables.push(service.onAfterOperation(e => event = e));
......@@ -840,7 +840,7 @@ suite('Disk File Service', () => {
writeFileSync(resource.fsPath, ''); // create file
const fileStat = await service.createFile2(resource, VSBuffer.fromString(contents), { overwrite: true });
const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true });
assert.equal(fileStat.name, 'test.txt');
assert.equal(existsSync(fileStat.resource.fsPath), true);
assert.equal(readFileSync(fileStat.resource.fsPath), contents);
......@@ -875,6 +875,20 @@ suite('Disk File Service', () => {
assert.equal(readFileSync(resource.fsPath), newContent);
});
test('writeFile (large file) - multiple parallel writes queue up', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = readFileSync(resource.fsPath);
const newContent = content.toString() + content.toString();
await Promise.all(['0', '00', '000', '0000', '00000'].map(async offset => {
const fileStat = await service.writeFile(resource, VSBuffer.fromString(offset + newContent));
assert.equal(fileStat.name, 'lorem.txt');
}));
assert.equal(readFileSync(resource.fsPath).toString(), '00000' + newContent);
});
test('writeFile (readable)', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
......
......@@ -15,7 +15,7 @@ import { IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, I
import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IFileService, IResolveContentOptions, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, toBufferOrReadable, ICreateFileOptions } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
......@@ -69,7 +69,7 @@ export class TextFileService extends Disposable implements ITextFileService {
@IFileService protected readonly fileService: IFileService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService protected instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService,
......@@ -382,15 +382,14 @@ export class TextFileService extends Disposable implements ITextFileService {
};
}
async create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata> {
const existingModel = this.models.get(resource);
const stat = await this.fileService.createFile(resource, contents, options);
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
const stat = await this.doCreate(resource, value, options);
// If we had an existing model for the given resource, load
// it again to make sure it is up to date with the contents
// we just wrote into the underlying resource by calling
// revert()
const existingModel = this.models.get(resource);
if (existingModel && !existingModel.isDisposed()) {
await existingModel.revert();
}
......@@ -398,10 +397,12 @@ export class TextFileService extends Disposable implements ITextFileService {
return stat;
}
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
const stat = await this.fileService.updateContent(resource, value, options);
protected doCreate(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
return this.fileService.createFile(resource, toBufferOrReadable(value), options);
}
return stat;
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
return this.fileService.writeFile(resource, toBufferOrReadable(value), options);
}
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
......
......@@ -345,7 +345,7 @@ export interface ITextFileService extends IDisposable {
* Create a file. If the file exists it will be overwritten with the contents if
* the options enable to overwrite.
*/
create(resource: URI, contents?: string, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata>;
create(resource: URI, contents?: string | ITextSnapshot, options?: { overwrite?: boolean }): Promise<IFileStatWithMetadata>;
/**
* Resolve the contents of a file identified by the resource.
......
......@@ -4,19 +4,53 @@
*--------------------------------------------------------------------------------------------*/
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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { URI } from 'vs/base/common/uri';
import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { ITextSnapshot, IWriteTextFileOptions, IFileStatWithMetadata, IResourceEncoding, IResolveContentOptions, IFileService, stringToSnapshot, ICreateFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
import { exists, stat, chmod, rimraf } from 'vs/base/node/pfs';
import { join, dirname } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import product from 'vs/platform/product/node/product';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, IDetectedEncodingResult, detectEncodingByBOM, encodeStream, UTF8_BOM, UTF16be_BOM, UTF16le_BOM } from 'vs/base/node/encoding';
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { joinPath, extname, isEqualOrParent } from 'vs/base/common/resources';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBufferReadable, VSBuffer } from 'vs/base/common/buffer';
import { Readable } from 'stream';
import { isUndefinedOrNull } from 'vs/base/common/types';
export class NodeTextFileService extends TextFileService {
private _encoding: EncodingOracle;
protected get encoding(): EncodingOracle {
if (!this._encoding) {
this._encoding = this._register(this.instantiationService.createInstance(EncodingOracle));
}
return this._encoding;
}
protected async doCreate(resource: URI, value?: string, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
// check for encoding
const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource);
// return to parent when encoding is standard
if (encoding === UTF8 && !addBOM) {
return super.doCreate(resource, value, options);
}
// otherwise create with encoding
return this.fileService.createFile(resource, this.getEncodedReadable(value || '', encoding, addBOM), options);
}
async write(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
// check for overwriteReadonly property (only supported for local file://)
......@@ -36,14 +70,137 @@ export class NodeTextFileService extends TextFileService {
return this.writeElevated(resource, value, options);
}
return super.write(resource, value, options);
try {
// check for encoding
const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource, options);
// return to parent when encoding is standard
if (encoding === UTF8 && !addBOM) {
return await super.write(resource, value, options);
}
// otherwise save with encoding
else {
return await this.fileService.writeFile(resource, this.getEncodedReadable(value, encoding, addBOM), options);
}
} catch (error) {
// In case of permission denied, we need to check for readonly
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED) {
let isReadonly = false;
try {
const fileStat = await stat(resource.fsPath);
if (!(fileStat.mode & 128)) {
isReadonly = true;
}
} catch (error) {
// ignore - rethrow original error
}
if (isReadonly) {
throw new FileOperationError(localize('fileReadOnlyError', "File is Read Only"), FileOperationResult.FILE_READ_ONLY, options);
}
}
throw error;
}
}
private getEncodedReadable(value: string | ITextSnapshot, encoding: string, addBOM: boolean): VSBufferReadable {
const readable = this.toNodeReadable(value);
const encoder = encodeStream(encoding, { addBOM });
const encodedReadable = readable.pipe(encoder);
return this.toBufferReadable(encodedReadable, encoding, addBOM);
}
private toNodeReadable(value: string | ITextSnapshot): Readable {
let snapshot: ITextSnapshot;
if (typeof value === 'string') {
snapshot = stringToSnapshot(value);
} else {
snapshot = value;
}
return new Readable({
read: function () {
try {
let chunk: string | null = null;
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: UTF8 // very important, so that strings are passed around and not buffers!
});
}
private toBufferReadable(stream: NodeJS.ReadWriteStream, encoding: string, addBOM: boolean): VSBufferReadable {
let bytesRead = 0;
let done = false;
return {
read(): VSBuffer | null {
if (done) {
return null;
}
const res = stream.read();
if (isUndefinedOrNull(res)) {
done = true;
// If we are instructed to add a BOM but we detect that no
// bytes have been read, we must ensure to return the BOM
// ourselves so that we comply with the contract.
if (bytesRead === 0 && addBOM) {
switch (encoding) {
case UTF8:
case UTF8_with_bom:
return VSBuffer.wrap(Buffer.from(UTF8_BOM));
case UTF16be:
return VSBuffer.wrap(Buffer.from(UTF16be_BOM));
case UTF16le:
return VSBuffer.wrap(Buffer.from(UTF16le_BOM));
}
}
return null;
}
// Handle String
if (typeof res === 'string') {
bytesRead += res.length;
return VSBuffer.fromString(res);
}
// Handle Buffer
else {
bytesRead += res.byteLength;
return VSBuffer.wrap(res);
}
}
};
}
private async writeElevated(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
// write into a tmp file first
const tmpPath = join(tmpdir(), `code-elevated-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 6)}`);
await this.write(URI.file(tmpPath), value, { encoding: this.fileService.encoding.getWriteEncoding(resource, options ? options.encoding : undefined).encoding });
const { encoding, addBOM } = await this.encoding.getWriteEncoding(resource, options);
await this.write(URI.file(tmpPath), value, { encoding: encoding === UTF8 && addBOM ? UTF8_with_bom : encoding });
// sudo prompt copy
await this.sudoPromptCopy(tmpPath, resource.fsPath, options);
......@@ -83,4 +240,151 @@ export class NodeTextFileService extends TextFileService {
}
}
export interface IEncodingOverride {
parent?: URI;
extension?: string;
encoding: string;
}
export class EncodingOracle extends Disposable {
protected encodingOverrides: IEncodingOverride[];
constructor(
@ITextResourceConfigurationService private textResourceConfigurationService: ITextResourceConfigurationService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IWorkspaceContextService private contextService: IWorkspaceContextService,
@IFileService private fileService: IFileService
) {
super();
this.encodingOverrides = this.getDefaultEncodingOverrides();
this.registerListeners();
}
private registerListeners(): void {
// Workspace Folder Change
this._register(this.contextService.onDidChangeWorkspaceFolders(() => this.encodingOverrides = this.getDefaultEncodingOverrides()));
}
private getDefaultEncodingOverrides(): IEncodingOverride[] {
const defaultEncodingOverrides: IEncodingOverride[] = [];
// Global settings
defaultEncodingOverrides.push({ parent: URI.file(this.environmentService.appSettingsHome), encoding: UTF8 });
// Workspace files
defaultEncodingOverrides.push({ extension: WORKSPACE_EXTENSION, encoding: UTF8 });
// Folder Settings
this.contextService.getWorkspace().folders.forEach(folder => {
defaultEncodingOverrides.push({ parent: joinPath(folder.uri, '.vscode'), encoding: UTF8 });
});
return defaultEncodingOverrides;
}
async getWriteEncoding(resource: URI, options?: IWriteTextFileOptions): Promise<{ encoding: string, addBOM: boolean }> {
const { encoding, hasBOM } = this.doGetWriteEncoding(resource, options ? options.encoding : undefined);
// Some encodings come with a BOM automatically
if (hasBOM) {
return { encoding, addBOM: true };
}
// Existing UTF-8 file: check for options regarding BOM
if (encoding === UTF8 && await this.fileService.exists(resource)) {
// if we are to overwrite the encoding, we do not preserve it if found
if (options && options.overwriteEncoding) {
return { encoding, addBOM: false };
}
// otherwise preserve it if found
if (resource.scheme === Schemas.file && await detectEncodingByBOM(resource.fsPath) === UTF8) {
return { encoding, addBOM: true };
}
}
return { encoding, addBOM: false };
}
private doGetWriteEncoding(resource: URI, preferredEncoding?: string): IResourceEncoding {
const resourceEncoding = this.getEncodingForResource(resource, preferredEncoding);
return {
encoding: resourceEncoding,
hasBOM: resourceEncoding === UTF16be || resourceEncoding === UTF16le || resourceEncoding === UTF8_with_bom // enforce BOM for certain encodings
};
}
getReadEncoding(resource: URI, options: IResolveContentOptions | undefined, detected: IDetectedEncodingResult): string {
let preferredEncoding: string | undefined;
// Encoding passed in as option
if (options && options.encoding) {
if (detected.encoding === UTF8 && options.encoding === UTF8) {
preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
} else {
preferredEncoding = options.encoding; // give passed in encoding highest priority
}
}
// Encoding detected
else if (detected.encoding) {
if (detected.encoding === UTF8) {
preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM
} else {
preferredEncoding = detected.encoding;
}
}
// Encoding configured
else if (this.textResourceConfigurationService.getValue(resource, 'files.encoding') === UTF8_with_bom) {
preferredEncoding = UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then
}
return this.getEncodingForResource(resource, preferredEncoding);
}
private getEncodingForResource(resource: URI, preferredEncoding?: string): string {
let fileEncoding: string;
const override = this.getEncodingOverride(resource);
if (override) {
fileEncoding = override; // encoding override always wins
} else if (preferredEncoding) {
fileEncoding = preferredEncoding; // preferred encoding comes second
} else {
fileEncoding = this.textResourceConfigurationService.getValue(resource, 'files.encoding'); // and last we check for settings
}
if (!fileEncoding || !encodingExists(fileEncoding)) {
fileEncoding = UTF8; // the default is UTF 8
}
return fileEncoding;
}
private getEncodingOverride(resource: URI): string | undefined {
if (this.encodingOverrides && this.encodingOverrides.length) {
for (const override of this.encodingOverrides) {
// check if the resource is child of encoding override path
if (override.parent && isEqualOrParent(resource, override.parent, !isLinux /* ignorecase */)) {
return override.encoding;
}
// check if the resource extension is equal to encoding override
if (override.extension && extname(resource) === `.${override.extension}`) {
return override.encoding;
}
}
}
return undefined;
}
}
registerSingleton(ITextFileService, NodeTextFileService);
\ No newline at end of file
<!DOCTYPE html>
<html>
<head id='headID'>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Strada </title>
<link href="site.css" rel="stylesheet" type="text/css" />
<script src="jquery-1.4.1.js"></script>
<script src="../compiler/dtree.js" type="text/javascript"></script>
<script src="../compiler/typescript.js" type="text/javascript"></script>
<script type="text/javascript">
// Compile strada source into resulting javascript
function compile(prog, libText) {
var outfile = {
source: "",
Write: function (s) { this.source += s; },
WriteLine: function (s) { this.source += s + "\r"; },
}
var parseErrors = []
var compiler=new Tools.TypeScriptCompiler(outfile,true);
compiler.setErrorCallback(function(start,len, message) { parseErrors.push({start:start, len:len, message:message}); });
compiler.addUnit(libText,"lib.ts");
compiler.addUnit(prog,"input.ts");
compiler.typeCheck();
compiler.emit();
if(parseErrors.length > 0 ) {
//throw new Error(parseErrors);
}
while(outfile.source[0] == '/' && outfile.source[1] == '/' && outfile.source[2] == ' ') {
outfile.source = outfile.source.slice(outfile.source.indexOf('\r')+1);
}
var errorPrefix = "";
for(var i = 0;i<parseErrors.length;i++) {
errorPrefix += "// Error: (" + parseErrors[i].start + "," + parseErrors[i].len + ") " + parseErrors[i].message + "\r";
}
return errorPrefix + outfile.source;
}
</script>
<script type="text/javascript">
var libText = "";
$.get("../compiler/lib.ts", function(newLibText) {
libText = newLibText;
});
// execute the javascript in the compiledOutput pane
function execute() {
$('#compilation').text("Running...");
var txt = $('#compiledOutput').val();
var res;
try {
var ret = eval(txt);
res = "Ran successfully!";
} catch(e) {
res = "Exception thrown: " + e;
}
$('#compilation').text(String(res));
}
// recompile the stradaSrc and populate the compiledOutput pane
function srcUpdated() {
var newText = $('#stradaSrc').val();
var compiledSource;
try {
compiledSource = compile(newText, libText);
} catch (e) {
compiledSource = "//Parse error"
for(var i in e)
compiledSource += "\r// " + e[i];
}
$('#compiledOutput').val(compiledSource);
}
// Populate the stradaSrc pane with one of the built in samples
function exampleSelectionChanged() {
var examples = document.getElementById('examples');
var selectedExample = examples.options[examples.selectedIndex].value;
if (selectedExample != "") {
$.get('examples/' + selectedExample, function (srcText) {
$('#stradaSrc').val(srcText);
setTimeout(srcUpdated,100);
}, function (err) {
console.log(err);
});
}
}
</script>
</head>
<body>
<h1>TypeScript</h1>
<br />
<select id="examples" onchange='exampleSelectionChanged()'>
<option value="">Select...</option>
<option value="small.ts">Small</option>
<option value="employee.ts">Employees</option>
<option value="conway.ts">Conway Game of Life</option>
<option value="typescript.ts">TypeScript Compiler</option>
</select>
<div>
<textarea id='stradaSrc' rows='40' cols='80' onchange='srcUpdated()' onkeyup='srcUpdated()' spellcheck="false">
//Type your TypeScript here...
</textarea>
<textarea id='compiledOutput' rows='40' cols='80' spellcheck="false">
//Compiled code will show up here...
</textarea>
<br />
<button onclick='execute()'/>Run</button>
<div id='compilation'>Press 'run' to execute code...</div>
<div id='results'>...write your results into #results...</div>
</div>
<div id='bod' style='display:none'></div>
</body>
</html>
Small File with Ümlaut
\ No newline at end of file
B/*----------------------------------------------------------
This is some UTF 8 with BOM file.
\ No newline at end of file
......@@ -44,21 +44,21 @@ suite('Files - TextFileEditorModel', () => {
accessor.fileService.setContent(content);
});
test('Save', function () {
test('Save', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel!.setValue('bar');
assert.ok(getLastModifiedTime(model) <= Date.now());
await model.load();
return model.save().then(() => {
assert.ok(model.getLastSaveAttemptTime() <= Date.now());
assert.ok(!model.isDirty());
model.textEditorModel!.setValue('bar');
assert.ok(getLastModifiedTime(model) <= Date.now());
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
});
});
await model.save();
assert.ok(model.getLastSaveAttemptTime() <= Date.now());
assert.ok(!model.isDirty());
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
});
test('setEncoding - encode', function () {
......@@ -74,29 +74,26 @@ suite('Files - TextFileEditorModel', () => {
model.dispose();
});
test('setEncoding - decode', function () {
test('setEncoding - decode', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
model.setEncoding('utf16', EncodingMode.Decode);
return timeout(0).then(() => { // due to model updating async
assert.ok(model.isResolved()); // model got loaded due to decoding
model.dispose();
});
await timeout(0);
assert.ok(model.isResolved()); // model got loaded due to decoding
model.dispose();
});
test('disposes when underlying model is destroyed', function () {
test('disposes when underlying model is destroyed', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel!.dispose();
await model.load();
assert.ok(model.isDisposed());
});
model.textEditorModel!.dispose();
assert.ok(model.isDisposed());
});
test('Load does not trigger save', function () {
test('Load does not trigger save', async function () {
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index.txt'), 'utf8');
assert.ok(model.hasState(ModelState.SAVED));
......@@ -104,32 +101,26 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(e !== StateChange.DIRTY && e !== StateChange.SAVED);
});
return model.load().then(() => {
assert.ok(model.isResolved());
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
});
await model.load();
assert.ok(model.isResolved());
model.dispose();
assert.ok(!accessor.modelService.getModel(model.getResource()));
});
test('Load returns dirty model as long as model is dirty', function () {
test('Load returns dirty model as long as model is dirty', async function () {
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.ok(model.hasState(ModelState.DIRTY));
return model.load().then(() => {
assert.ok(model.isDirty());
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.ok(model.hasState(ModelState.DIRTY));
model.dispose();
});
});
await model.load();
assert.ok(model.isDirty());
model.dispose();
});
test('Revert', function () {
test('Revert', async function () {
let eventCounter = 0;
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
......@@ -140,22 +131,18 @@ suite('Files - TextFileEditorModel', () => {
}
});
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
return model.revert().then(() => {
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'Hello Html');
assert.equal(eventCounter, 1);
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
model.dispose();
});
});
await model.revert();
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'Hello Html');
assert.equal(eventCounter, 1);
model.dispose();
});
test('Revert (soft)', function () {
test('Revert (soft)', async function () {
let eventCounter = 0;
const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
......@@ -166,99 +153,88 @@ suite('Files - TextFileEditorModel', () => {
}
});
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(model.isDirty());
assert.ok(model.isDirty());
return model.revert(true /* soft revert */).then(() => {
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'foo');
assert.equal(eventCounter, 1);
model.dispose();
});
});
await model.revert(true /* soft revert */);
assert.ok(!model.isDirty());
assert.equal(model.textEditorModel!.getValue(), 'foo');
assert.equal(eventCounter, 1);
model.dispose();
});
test('Load and undo turns model dirty', function () {
test('Load and undo turns model dirty', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
accessor.fileService.setContent('Hello Change');
return model.load().then(() => {
model.textEditorModel!.undo();
await model.load();
accessor.fileService.setContent('Hello Change');
assert.ok(model.isDirty());
});
});
await model.load();
model.textEditorModel!.undo();
assert.ok(model.isDirty());
});
test('File not modified error is handled gracefully', function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
test('File not modified error is handled gracefully', async function () {
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
const mtime = getLastModifiedTime(model);
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE));
await model.load();
return model.load().then((model: TextFileEditorModel) => {
assert.ok(model);
assert.equal(getLastModifiedTime(model), mtime);
model.dispose();
});
});
const mtime = getLastModifiedTime(model);
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_MODIFIED_SINCE));
model = await model.load() as TextFileEditorModel;
assert.ok(model);
assert.equal(getLastModifiedTime(model), mtime);
model.dispose();
});
test('Load error is handled gracefully if model already exists', function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
test('Load error is handled gracefully if model already exists', async function () {
let model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
return model.load().then(() => {
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND));
await model.load();
accessor.textFileService.setResolveTextContentErrorOnce(new FileOperationError('error', FileOperationResult.FILE_NOT_FOUND));
return model.load().then((model: TextFileEditorModel) => {
assert.ok(model);
model.dispose();
});
});
model = await model.load() as TextFileEditorModel;
assert.ok(model);
model.dispose();
});
test('save() and isDirty() - proper with check for mtimes', function () {
test('save() and isDirty() - proper with check for mtimes', async function () {
const input1 = createFileInput(instantiationService, toResource.call(this, '/path/index_async2.txt'));
const input2 = createFileInput(instantiationService, toResource.call(this, '/path/index_async.txt'));
return input1.resolve().then((model1: TextFileEditorModel) => {
return input2.resolve().then((model2: TextFileEditorModel) => {
model1.textEditorModel!.setValue('foo');
const m1Mtime = model1.getStat().mtime;
const m2Mtime = model2.getStat().mtime;
assert.ok(m1Mtime > 0);
assert.ok(m2Mtime > 0);
assert.ok(accessor.textFileService.isDirty());
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
model2.textEditorModel!.setValue('foo');
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
return timeout(10).then(() => {
accessor.textFileService.saveAll().then(() => {
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(model1.getStat().mtime > m1Mtime);
assert.ok(model2.getStat().mtime > m2Mtime);
assert.ok(model1.getLastSaveAttemptTime() > m1Mtime);
assert.ok(model2.getLastSaveAttemptTime() > m2Mtime);
model1.dispose();
model2.dispose();
});
});
});
});
const model1 = await input1.resolve() as TextFileEditorModel;
const model2 = await input2.resolve() as TextFileEditorModel;
model1.textEditorModel!.setValue('foo');
const m1Mtime = model1.getStat().mtime;
const m2Mtime = model2.getStat().mtime;
assert.ok(m1Mtime > 0);
assert.ok(m2Mtime > 0);
assert.ok(accessor.textFileService.isDirty());
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
model2.textEditorModel!.setValue('foo');
assert.ok(accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
await timeout(10);
await accessor.textFileService.saveAll();
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async.txt')));
assert.ok(!accessor.textFileService.isDirty(toResource.call(this, '/path/index_async2.txt')));
assert.ok(model1.getStat().mtime > m1Mtime);
assert.ok(model2.getStat().mtime > m2Mtime);
assert.ok(model1.getLastSaveAttemptTime() > m1Mtime);
assert.ok(model2.getLastSaveAttemptTime() > m2Mtime);
model1.dispose();
model2.dispose();
});
test('Save Participant', function () {
test('Save Participant', async function () {
let eventCounter = 0;
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
......@@ -280,18 +256,15 @@ suite('Files - TextFileEditorModel', () => {
}
});
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
return model.save().then(() => {
model.dispose();
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(eventCounter, 2);
});
});
await model.save();
model.dispose();
assert.equal(eventCounter, 2);
});
test('Save Participant, async participant', function () {
test('Save Participant, async participant', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
......@@ -301,18 +274,16 @@ suite('Files - TextFileEditorModel', () => {
}
});
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
await model.load();
model.textEditorModel!.setValue('foo');
const now = Date.now();
return model.save().then(() => {
assert.ok(Date.now() - now >= 10);
model.dispose();
});
});
const now = Date.now();
await model.save();
assert.ok(Date.now() - now >= 10);
model.dispose();
});
test('Save Participant, bad participant', function () {
test('Save Participant, bad participant', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8');
TextFileEditorModel.setSaveParticipant({
......@@ -321,15 +292,14 @@ suite('Files - TextFileEditorModel', () => {
}
});
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
return model.save().then(() => {
model.dispose();
});
});
await model.load();
model.textEditorModel!.setValue('foo');
await model.save();
model.dispose();
});
test('SaveSequentializer - pending basics', function () {
test('SaveSequentializer - pending basics', async function () {
const sequentializer = new SaveSequentializer();
assert.ok(!sequentializer.hasPendingSave());
......@@ -337,27 +307,25 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(!sequentializer.pendingSave);
// pending removes itself after done
return sequentializer.setPending(1, Promise.resolve()).then(() => {
assert.ok(!sequentializer.hasPendingSave());
assert.ok(!sequentializer.hasPendingSave(1));
assert.ok(!sequentializer.pendingSave);
// pending removes itself after done (use timeout)
sequentializer.setPending(2, timeout(1));
assert.ok(sequentializer.hasPendingSave());
assert.ok(sequentializer.hasPendingSave(2));
assert.ok(!sequentializer.hasPendingSave(1));
assert.ok(sequentializer.pendingSave);
return timeout(2).then(() => {
assert.ok(!sequentializer.hasPendingSave());
assert.ok(!sequentializer.hasPendingSave(2));
assert.ok(!sequentializer.pendingSave);
});
});
await sequentializer.setPending(1, Promise.resolve());
assert.ok(!sequentializer.hasPendingSave());
assert.ok(!sequentializer.hasPendingSave(1));
assert.ok(!sequentializer.pendingSave);
// pending removes itself after done (use timeout)
sequentializer.setPending(2, timeout(1));
assert.ok(sequentializer.hasPendingSave());
assert.ok(sequentializer.hasPendingSave(2));
assert.ok(!sequentializer.hasPendingSave(1));
assert.ok(sequentializer.pendingSave);
await timeout(2);
assert.ok(!sequentializer.hasPendingSave());
assert.ok(!sequentializer.hasPendingSave(2));
assert.ok(!sequentializer.pendingSave);
});
test('SaveSequentializer - pending and next (finishes instantly)', function () {
test('SaveSequentializer - pending and next (finishes instantly)', async function () {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
......@@ -367,13 +335,12 @@ suite('Files - TextFileEditorModel', () => {
let nextDone = false;
const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; }));
return res.then(() => {
assert.ok(pendingDone);
assert.ok(nextDone);
});
await res;
assert.ok(pendingDone);
assert.ok(nextDone);
});
test('SaveSequentializer - pending and next (finishes after timeout)', function () {
test('SaveSequentializer - pending and next (finishes after timeout)', async function () {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
......@@ -383,13 +350,12 @@ suite('Files - TextFileEditorModel', () => {
let nextDone = false;
const res = sequentializer.setNext(() => timeout(1).then(() => { nextDone = true; return; }));
return res.then(() => {
assert.ok(pendingDone);
assert.ok(nextDone);
});
await res;
assert.ok(pendingDone);
assert.ok(nextDone);
});
test('SaveSequentializer - pending and multiple next (last one wins)', function () {
test('SaveSequentializer - pending and multiple next (last one wins)', async function () {
const sequentializer = new SaveSequentializer();
let pendingDone = false;
......@@ -405,11 +371,10 @@ suite('Files - TextFileEditorModel', () => {
let thirdDone = false;
let thirdRes = sequentializer.setNext(() => timeout(4).then(() => { thirdDone = true; return; }));
return Promise.all([firstRes, secondRes, thirdRes]).then(() => {
assert.ok(pendingDone);
assert.ok(!firstDone);
assert.ok(!secondDone);
assert.ok(thirdDone);
});
await Promise.all([firstRes, secondRes, thirdRes]);
assert.ok(pendingDone);
assert.ok(!firstDone);
assert.ok(!secondDone);
assert.ok(thirdDone);
});
});
......@@ -94,29 +94,24 @@ suite('Files - TextFileEditorModelManager', () => {
model3.dispose();
});
test('loadOrCreate', () => {
test('loadOrCreate', async () => {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource = URI.file('/test.html');
const encoding = 'utf8';
return manager.loadOrCreate(resource, { encoding }).then(model => {
assert.ok(model);
assert.equal(model.getEncoding(), encoding);
assert.equal(manager.get(resource), model);
const model = await manager.loadOrCreate(resource, { encoding });
assert.ok(model);
assert.equal(model.getEncoding(), encoding);
assert.equal(manager.get(resource), model);
return manager.loadOrCreate(resource, { encoding }).then(model2 => {
assert.equal(model2, model);
const model2 = await manager.loadOrCreate(resource, { encoding });
assert.equal(model2, model);
model.dispose();
model.dispose();
return manager.loadOrCreate(resource, { encoding }).then(model3 => {
assert.notEqual(model3, model2);
assert.equal(manager.get(resource), model3);
model3.dispose();
});
});
});
const model3 = await manager.loadOrCreate(resource, { encoding });
assert.notEqual(model3, model2);
assert.equal(manager.get(resource), model3);
model3.dispose();
});
test('removed from cache when model disposed', function () {
......@@ -139,7 +134,7 @@ suite('Files - TextFileEditorModelManager', () => {
model3.dispose();
});
test('events', function () {
test('events', async function () {
TextFileEditorModel.DEFAULT_CONTENT_CHANGE_BUFFER_DELAY = 0;
TextFileEditorModel.DEFAULT_ORPHANED_CHANGE_BUFFER_DELAY = 0;
......@@ -189,46 +184,37 @@ suite('Files - TextFileEditorModelManager', () => {
disposeCounter++;
});
return manager.loadOrCreate(resource1, { encoding: 'utf8' }).then(model1 => {
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }]));
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }]));
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
return model1.revert().then(() => {
model1.textEditorModel!.setValue('changed again');
return model1.save().then(() => {
model1.dispose();
model2.dispose();
assert.equal(disposeCounter, 2);
return model1.revert().then(() => { // should not trigger another event if disposed
assert.equal(dirtyCounter, 2);
assert.equal(revertedCounter, 1);
assert.equal(savedCounter, 1);
assert.equal(encodingCounter, 2);
// content change event if done async
return timeout(10).then(() => {
assert.equal(contentCounter, 2);
model1.dispose();
model2.dispose();
assert.ok(!accessor.modelService.getModel(resource1));
assert.ok(!accessor.modelService.getModel(resource2));
});
});
});
});
});
});
const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' });
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.DELETED }]));
accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource: resource1, type: FileChangeType.ADDED }]));
const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' });
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
await model1.revert();
model1.textEditorModel!.setValue('changed again');
await model1.save();
model1.dispose();
model2.dispose();
assert.equal(disposeCounter, 2);
await model1.revert();
assert.equal(dirtyCounter, 2);
assert.equal(revertedCounter, 1);
assert.equal(savedCounter, 1);
assert.equal(encodingCounter, 2);
await timeout(10);
assert.equal(contentCounter, 2);
model1.dispose();
model2.dispose();
assert.ok(!accessor.modelService.getModel(resource1));
assert.ok(!accessor.modelService.getModel(resource2));
});
test('events debounced', function () {
test('events debounced', async function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource1 = toResource.call(this, '/path/index.txt');
......@@ -255,69 +241,53 @@ suite('Files - TextFileEditorModelManager', () => {
assert.equal(e[0].resource.toString(), resource1.toString());
});
return manager.loadOrCreate(resource1, { encoding: 'utf8' }).then(model1 => {
return manager.loadOrCreate(resource2, { encoding: 'utf8' }).then(model2 => {
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
return model1.revert().then(() => {
model1.textEditorModel!.setValue('changed again');
return model1.save().then(() => {
model1.dispose();
model2.dispose();
return model1.revert().then(() => { // should not trigger another event if disposed
return timeout(20).then(() => {
assert.equal(dirtyCounter, 2);
assert.equal(revertedCounter, 1);
assert.equal(savedCounter, 1);
model1.dispose();
model2.dispose();
assert.ok(!accessor.modelService.getModel(resource1));
assert.ok(!accessor.modelService.getModel(resource2));
});
});
});
});
});
});
});
test('disposing model takes it out of the manager', function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource = toResource.call(this, '/path/index_something.txt');
const model1 = await manager.loadOrCreate(resource1, { encoding: 'utf8' });
const model2 = await manager.loadOrCreate(resource2, { encoding: 'utf8' });
model1.textEditorModel!.setValue('changed');
model1.updatePreferredEncoding('utf16');
return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => {
model.dispose();
await model1.revert();
model1.textEditorModel!.setValue('changed again');
assert.ok(!manager.get(resource));
assert.ok(!accessor.modelService.getModel(model.getResource()));
await model1.save();
model1.dispose();
model2.dispose();
manager.dispose();
});
await model1.revert();
await timeout(20);
assert.equal(dirtyCounter, 2);
assert.equal(revertedCounter, 1);
assert.equal(savedCounter, 1);
model1.dispose();
model2.dispose();
assert.ok(!accessor.modelService.getModel(resource1));
assert.ok(!accessor.modelService.getModel(resource2));
});
test('dispose prevents dirty model from getting disposed', function () {
test('disposing model takes it out of the manager', async function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
const resource = toResource.call(this, '/path/index_something.txt');
return manager.loadOrCreate(resource, { encoding: 'utf8' }).then(model => {
model.textEditorModel!.setValue('make dirty');
manager.disposeModel(model as TextFileEditorModel);
assert.ok(!model.isDisposed());
const model = await manager.loadOrCreate(resource, { encoding: 'utf8' });
model.dispose();
assert.ok(!manager.get(resource));
assert.ok(!accessor.modelService.getModel(model.getResource()));
manager.dispose();
});
model.revert(true);
test('dispose prevents dirty model from getting disposed', async function () {
const manager: TestTextFileEditorModelManager = instantiationService.createInstance(TestTextFileEditorModelManager);
manager.disposeModel(model as TextFileEditorModel);
assert.ok(model.isDisposed());
const resource = toResource.call(this, '/path/index_something.txt');
manager.dispose();
});
const model = await manager.loadOrCreate(resource, { encoding: 'utf8' });
model.textEditorModel!.setValue('make dirty');
manager.disposeModel((model as TextFileEditorModel));
assert.ok(!model.isDisposed());
model.revert(true);
manager.disposeModel((model as TextFileEditorModel));
assert.ok(model.isDisposed());
manager.dispose();
});
});
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { URI } from 'vs/base/common/uri';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { workbenchInstantiationService, TestLifecycleService, TestTextFileService, TestWindowsService, TestContextService, TestFileService, TestEnvironmentService, TestTextResourceConfigurationService } from 'vs/workbench/test/workbenchTestServices';
import { IWindowsService } from 'vs/platform/windows/common/windows';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IFileService, ITextSnapshot, snapshotToString, SUPPORTED_ENCODINGS } from 'vs/platform/files/common/files';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl';
import { Schemas } from 'vs/base/common/network';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { rimraf, RimRafMode, copy, readFile, exists } from 'vs/base/node/pfs';
import { dispose, IDisposable } from 'vs/base/common/lifecycle';
import { FileService2 } from 'vs/workbench/services/files2/common/fileService2';
import { NullLogService } from 'vs/platform/log/common/log';
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
import { tmpdir } from 'os';
import { DiskFileSystemProvider } from 'vs/workbench/services/files2/node/diskFileSystemProvider';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { detectEncodingByBOM, UTF16be, UTF16le, UTF8_with_bom, UTF8 } from 'vs/base/node/encoding';
import { NodeTextFileService, EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfile/node/textFileService';
import { LegacyFileService } from 'vs/workbench/services/files/node/fileService';
import { DefaultEndOfLine } from 'vs/editor/common/model';
import { TextModel } from 'vs/editor/common/model/textModel';
class ServiceAccessor {
constructor(
@ILifecycleService public lifecycleService: TestLifecycleService,
@ITextFileService public textFileService: TestTextFileService,
@IUntitledEditorService public untitledEditorService: IUntitledEditorService,
@IWindowsService public windowsService: TestWindowsService,
@IWorkspaceContextService public contextService: TestContextService,
@IModelService public modelService: ModelServiceImpl,
@IFileService public fileService: TestFileService
) {
}
}
class TestNodeTextFileService extends NodeTextFileService {
private _testEncoding: TestEncodingOracle;
protected get encoding(): TestEncodingOracle {
if (!this._testEncoding) {
this._testEncoding = this._register(this.instantiationService.createInstance(TestEncodingOracle));
}
return this._testEncoding;
}
}
class TestEncodingOracle extends EncodingOracle {
protected get encodingOverrides(): IEncodingOverride[] {
return [
{ extension: 'utf16le', encoding: UTF16le },
{ extension: 'utf16be', encoding: UTF16be },
{ extension: 'utf8bom', encoding: UTF8_with_bom }
];
}
protected set encodingOverrides(overrides: IEncodingOverride[]) { }
}
suite('Files - TextFileService i/o', () => {
const parentDir = getRandomTestPath(tmpdir(), 'vsctests', 'textfileservice');
let accessor: ServiceAccessor;
let disposables: IDisposable[] = [];
let service: ITextFileService;
let testDir: string;
setup(async () => {
const instantiationService = workbenchInstantiationService();
accessor = instantiationService.createInstance(ServiceAccessor);
const logService = new NullLogService();
const fileService = new FileService2(logService);
const fileProvider = new DiskFileSystemProvider(logService);
disposables.push(fileService.registerProvider(Schemas.file, fileProvider));
disposables.push(fileProvider);
fileService.setLegacyService(new LegacyFileService(
fileService,
accessor.contextService,
TestEnvironmentService,
new TestTextResourceConfigurationService()
));
const collection = new ServiceCollection();
collection.set(IFileService, fileService);
service = instantiationService.createChild(collection).createInstance(TestNodeTextFileService);
const id = generateUuid();
testDir = join(parentDir, id);
const sourceDir = getPathFromAmdModule(require, './fixtures');
await copy(sourceDir, testDir);
});
teardown(async () => {
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
(<TextFileEditorModelManager>accessor.textFileService.models).dispose();
accessor.untitledEditorService.revertAll();
disposables = dispose(disposables);
await rimraf(parentDir, RimRafMode.MOVE);
});
test('create - no encoding - content empty', async () => {
const resource = URI.file(join(testDir, 'small_new.txt'));
await service.create(resource);
assert.equal(await exists(resource.fsPath), true);
});
test('create - no encoding - content provided', async () => {
const resource = URI.file(join(testDir, 'small_new.txt'));
await service.create(resource, 'Hello World');
assert.equal(await exists(resource.fsPath), true);
assert.equal((await readFile(resource.fsPath)).toString(), 'Hello World');
});
test('create - UTF 16 LE - no content', async () => {
const resource = URI.file(join(testDir, 'small_new.utf16le'));
await service.create(resource);
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF16le);
});
test('create - UTF 16 LE - content provided', async () => {
const resource = URI.file(join(testDir, 'small_new.utf16le'));
await service.create(resource, 'Hello World');
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF16le);
});
test('create - UTF 16 BE - no content', async () => {
const resource = URI.file(join(testDir, 'small_new.utf16be'));
await service.create(resource);
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF16be);
});
test('create - UTF 16 BE - content provided', async () => {
const resource = URI.file(join(testDir, 'small_new.utf16be'));
await service.create(resource, 'Hello World');
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF16be);
});
test('create - UTF 8 BOM - no content', async () => {
const resource = URI.file(join(testDir, 'small_new.utf8bom'));
await service.create(resource);
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('create - UTF 8 BOM - content provided', async () => {
const resource = URI.file(join(testDir, 'small_new.utf8bom'));
await service.create(resource, 'Hello World');
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('create - UTF 8 BOM - empty content - snapshot', async () => {
const resource = URI.file(join(testDir, 'small_new.utf8bom'));
await service.create(resource, TextModel.createFromString('').createSnapshot());
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('create - UTF 8 BOM - content provided - snapshot', async () => {
const resource = URI.file(join(testDir, 'small_new.utf8bom'));
await service.create(resource, TextModel.createFromString('Hello World').createSnapshot());
assert.equal(await exists(resource.fsPath), true);
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('write - use encoding (UTF 16 BE) - small content as string', async () => {
await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, 'Hello\nWorld', 'Hello\nWorld');
});
test('write - use encoding (UTF 16 BE) - small content as snapshot', async () => {
await testEncoding(URI.file(join(testDir, 'small.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld');
});
test('write - use encoding (UTF 16 BE) - large content as string', async () => {
await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, 'Hello\nWorld', 'Hello\nWorld');
});
test('write - use encoding (UTF 16 BE) - large content as snapshot', async () => {
await testEncoding(URI.file(join(testDir, 'lorem.txt')), UTF16be, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld');
});
async function testEncoding(resource: URI, encoding: string, content: string | ITextSnapshot, expectedContent: string) {
await service.write(resource, content, { encoding });
const detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, encoding);
const resolved = await service.resolve(resource);
assert.equal(resolved.encoding, encoding);
assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.LF).createSnapshot(false)), expectedContent);
}
test('write - no encoding - content as string', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
const content = (await readFile(resource.fsPath)).toString();
await service.write(resource, content);
const resolved = await service.resolve(resource);
assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.LF).createSnapshot(false)), content);
});
test('write - no encoding - content as snapshot', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
const content = (await readFile(resource.fsPath)).toString();
await service.write(resource, TextModel.createFromString(content).createSnapshot());
const resolved = await service.resolve(resource);
assert.equal(snapshotToString(resolved.value.create(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.resolve(resource);
assert.equal(resolved.encoding, UTF16le);
await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, 'Hello\nWorld', 'Hello\nWorld');
});
test('write - encoding preserved (UTF 16 LE) - content as snapshot', async () => {
const resource = URI.file(join(testDir, 'some_utf16le.css'));
const resolved = await service.resolve(resource);
assert.equal(resolved.encoding, UTF16le);
await testEncoding(URI.file(join(testDir, 'some_utf16le.css')), UTF16le, TextModel.createFromString('Hello\nWorld').createSnapshot(), 'Hello\nWorld');
});
test('write - UTF8 variations - content as string', async () => {
const resource = URI.file(join(testDir, 'index.html'));
let detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
const content = (await readFile(resource.fsPath)).toString() + 'updates';
await service.write(resource, content, { encoding: UTF8_with_bom });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
// ensure BOM preserved
await service.write(resource, content, { encoding: UTF8 });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
// allow to remove BOM
await service.write(resource, content, { encoding: UTF8, overwriteEncoding: true });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
// BOM does not come back
await service.write(resource, content, { encoding: UTF8 });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
});
test('write - UTF8 variations - content as snapshot', async () => {
const resource = URI.file(join(testDir, 'index.html'));
let detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
const model = TextModel.createFromString((await readFile(resource.fsPath)).toString() + 'updates');
await service.write(resource, model.createSnapshot(), { encoding: UTF8_with_bom });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
// ensure BOM preserved
await service.write(resource, model.createSnapshot(), { encoding: UTF8 });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
// allow to remove BOM
await service.write(resource, model.createSnapshot(), { encoding: UTF8, overwriteEncoding: true });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
// BOM does not come back
await service.write(resource, model.createSnapshot(), { encoding: UTF8 });
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, null);
});
test('write - preserve UTF8 BOM - content as string', async () => {
const resource = URI.file(join(testDir, 'some_utf8_bom.txt'));
let detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
await service.write(resource, 'Hello World');
detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('write - CP1252 - content as string', async () => {
const resource = URI.file(join(testDir, 'small_umlaut.txt'));
const content = (await readFile(resource.fsPath)).toString();
await service.write(resource, content, { encoding: 'cp1252' });
const resolved = await service.resolve(resource, { encoding: 'cp1252' });
assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.LF).createSnapshot(false)), content);
});
test('write - all encodings - large content as snapshot', async () => {
const resource = URI.file(join(testDir, 'lorem.txt'));
const content = (await readFile(resource.fsPath)).toString();
for (const encoding of Object.keys(SUPPORTED_ENCODINGS)) {
if (encoding === 'utf8bom') {
continue; // this is the only encoding that is not standard, so skip it
}
await testEncoding2(resource, encoding, content);
}
});
test('write - all encodings - small content as snapshot', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
const content = (await readFile(resource.fsPath)).toString();
for (const encoding of Object.keys(SUPPORTED_ENCODINGS)) {
if (encoding === 'utf8bom') {
continue; // this is the only encoding that is not standard, so skip it
}
await testEncoding2(resource, encoding, content);
}
});
async function testEncoding2(resource: URI, encoding: string, content: string): Promise<void> {
await service.write(resource, content, { encoding });
const resolved = await service.resolve(resource, { encoding });
assert.equal(snapshotToString(resolved.value.create(DefaultEndOfLine.LF).createSnapshot(false)), content, 'Encoding used: ' + encoding);
}
test('write - ensure BOM in empty file - content as string', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
await service.write(resource, '', { encoding: UTF8_with_bom });
let detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
test('write - ensure BOM in empty file - content as snapshot', async () => {
const resource = URI.file(join(testDir, 'small.txt'));
await service.write(resource, TextModel.createFromString('').createSnapshot(), { encoding: UTF8_with_bom });
let detectedEncoding = await detectEncodingByBOM(resource.fsPath);
assert.equal(detectedEncoding, UTF8);
});
});
......@@ -15,7 +15,6 @@ import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textF
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ConfirmResult } from 'vs/workbench/common/editor';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { HotExitConfiguration, IFileService } from 'vs/platform/files/common/files';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace';
......@@ -83,26 +82,23 @@ suite('Files - TextFileService', () => {
}
});
test('confirm onWillShutdown - veto if user cancels', function () {
test('confirm onWillShutdown - veto if user cancels', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
service.setConfirmResult(ConfirmResult.CANCEL);
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
assert.ok(event.value);
});
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
assert.ok(event.value);
});
test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', function () {
test('confirm onWillShutdown - no veto and backups cleaned up if user does not want to save (hot.exit: off)', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
......@@ -110,30 +106,25 @@ suite('Files - TextFileService', () => {
service.setConfirmResult(ConfirmResult.DONT_SAVE);
service.onFilesConfigurationChange({ files: { hotExit: 'off' } });
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
const veto = event.value;
if (typeof veto === 'boolean') {
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
assert.ok(!veto);
let veto = event.value;
if (typeof veto === 'boolean') {
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
assert.ok(!veto);
return;
}
return undefined;
} else {
return veto.then(veto => {
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
assert.ok(!veto);
});
}
});
veto = await veto;
assert.ok(service.cleanupBackupsBeforeShutdownCalled);
assert.ok(!veto);
});
test('confirm onWillShutdown - save (hot.exit: off)', function () {
test('confirm onWillShutdown - save (hot.exit: off)', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
......@@ -141,66 +132,60 @@ suite('Files - TextFileService', () => {
service.setConfirmResult(ConfirmResult.SAVE);
service.onFilesConfigurationChange({ files: { hotExit: 'off' } });
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
accessor.lifecycleService.fireWillShutdown(event);
return (<Promise<boolean>>event.value).then(veto => {
assert.ok(!veto);
assert.ok(!model.isDirty());
});
});
const veto = await (<Promise<boolean>>event.value);
assert.ok(!veto);
assert.ok(!model.isDirty());
});
test('isDirty/getDirty - files and untitled', function () {
test('isDirty/getDirty - files and untitled', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
return model.load().then(() => {
assert.ok(!service.isDirty(model.getResource()));
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
assert.equal(service.getDirty().length, 1);
assert.equal(service.getDirty([model.getResource()])[0].toString(), model.getResource().toString());
await model.load();
const untitled = accessor.untitledEditorService.createOrGet();
return untitled.resolve().then((model: UntitledEditorModel) => {
assert.ok(!service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 1);
model.textEditorModel!.setValue('changed');
assert.ok(!service.isDirty(model.getResource()));
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 2);
assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString());
});
});
assert.ok(service.isDirty(model.getResource()));
assert.equal(service.getDirty().length, 1);
assert.equal(service.getDirty([model.getResource()])[0].toString(), model.getResource().toString());
const untitled = accessor.untitledEditorService.createOrGet();
const untitledModel = await untitled.resolve();
assert.ok(!service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 1);
untitledModel.textEditorModel!.setValue('changed');
assert.ok(service.isDirty(untitled.getResource()));
assert.equal(service.getDirty().length, 2);
assert.equal(service.getDirty([untitled.getResource()])[0].toString(), untitled.getResource().toString());
});
test('save - file', function () {
test('save - file', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.save(model.getResource()).then(res => {
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
});
});
const res = await service.save(model.getResource());
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
});
test('save - UNC path', function () {
test('save - UNC path', async function () {
const untitledUncUri = URI.from({ scheme: 'untitled', authority: 'server', path: '/share/path/file.txt' });
model = instantiationService.createInstance(TextFileEditorModel, untitledUncUri, 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
......@@ -213,97 +198,82 @@ suite('Files - TextFileService', () => {
sinon.stub(accessor.untitledEditorService, 'hasAssociatedFilePath', () => true);
sinon.stub(accessor.modelService, 'updateModel', () => { });
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
return accessor.textFileService.saveAll(true).then(res => {
assert.ok(loadOrCreateStub.calledOnce);
assert.equal(res.results.length, 1);
assert.ok(res.results[0].success);
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(res.results[0].target!.scheme, Schemas.file);
assert.equal(res.results[0].target!.authority, untitledUncUri.authority);
assert.equal(res.results[0].target!.path, untitledUncUri.path);
});
});
const res = await accessor.textFileService.saveAll(true);
assert.ok(loadOrCreateStub.calledOnce);
assert.equal(res.results.length, 1);
assert.ok(res.results[0].success);
assert.equal(res.results[0].target!.scheme, Schemas.file);
assert.equal(res.results[0].target!.authority, untitledUncUri.authority);
assert.equal(res.results[0].target!.path, untitledUncUri.path);
});
test('saveAll - file', function () {
test('saveAll - file', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
assert.ok(service.isDirty(model.getResource()));
return service.saveAll([model.getResource()]).then(res => {
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
assert.equal(res.results.length, 1);
assert.equal(res.results[0].source.toString(), model.getResource().toString());
});
});
const res = await service.saveAll([model.getResource()]);
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
assert.equal(res.results.length, 1);
assert.equal(res.results[0].source.toString(), model.getResource().toString());
});
test('saveAs - file', function () {
test('saveAs - file', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
service.setPromptPath(model.getResource());
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
await model.load();
model.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.saveAs(model.getResource()).then(res => {
assert.equal(res!.toString(), model.getResource().toString());
assert.ok(!service.isDirty(model.getResource()));
});
});
const res = await service.saveAs(model.getResource());
assert.equal(res!.toString(), model.getResource().toString());
assert.ok(!service.isDirty(model.getResource()));
});
test('revert - file', function () {
test('revert - file', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
service.setPromptPath(model.getResource());
return model.load().then(() => {
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
await model.load();
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.revert(model.getResource()).then(res => {
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
});
});
const res = await service.revert(model.getResource());
assert.ok(res);
assert.ok(!service.isDirty(model.getResource()));
});
test('delete - dirty file', function () {
test('delete - dirty file', async function () {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
const service = accessor.textFileService;
return model.load().then(() => {
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
await model.load();
model!.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(model.getResource()));
return service.delete(model.getResource()).then(() => {
assert.ok(!service.isDirty(model.getResource()));
});
});
await service.delete(model.getResource());
assert.ok(!service.isDirty(model.getResource()));
});
test('move - dirty file', function () {
test('move - dirty file', async function () {
let sourceModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
let targetModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file_target.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(sourceModel.getResource(), sourceModel);
......@@ -311,18 +281,14 @@ suite('Files - TextFileService', () => {
const service = accessor.textFileService;
return sourceModel.load().then(() => {
sourceModel.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(sourceModel.getResource()));
return service.move(sourceModel.getResource(), targetModel.getResource(), true).then(() => {
assert.ok(!service.isDirty(sourceModel.getResource()));
await sourceModel.load();
sourceModel.textEditorModel!.setValue('foo');
assert.ok(service.isDirty(sourceModel.getResource()));
sourceModel.dispose();
targetModel.dispose();
});
});
await service.move(sourceModel.getResource(), targetModel.getResource(), true);
assert.ok(!service.isDirty(sourceModel.getResource()));
sourceModel.dispose();
targetModel.dispose();
});
suite('Hot Exit', () => {
......@@ -428,7 +394,7 @@ suite('Files - TextFileService', () => {
});
});
function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise<void> {
async function hotExitTest(this: any, setting: string, shutdownReason: ShutdownReason, multipleWindows: boolean, workspace: true, shouldVeto: boolean): Promise<void> {
model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file.txt'), 'utf8');
(<TextFileEditorModelManager>accessor.textFileService.models).add(model.getResource(), model);
......@@ -446,21 +412,16 @@ suite('Files - TextFileService', () => {
// Set cancel to force a veto if hot exit does not trigger
service.setConfirmResult(ConfirmResult.CANCEL);
return model.load().then(() => {
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
event.reason = shutdownReason;
accessor.lifecycleService.fireWillShutdown(event);
await model.load();
model.textEditorModel!.setValue('foo');
assert.equal(service.getDirty().length, 1);
const event = new BeforeShutdownEventImpl();
event.reason = shutdownReason;
accessor.lifecycleService.fireWillShutdown(event);
return (<Promise<boolean>>event.value).then(veto => {
// When hot exit is set, backups should never be cleaned since the confirm result is cancel
assert.ok(!service.cleanupBackupsBeforeShutdownCalled);
assert.equal(veto, shouldVeto);
});
});
const veto = await (<Promise<boolean>>event.value);
assert.ok(!service.cleanupBackupsBeforeShutdownCalled); // When hot exit is set, backups should never be cleaned since the confirm result is cancel
assert.equal(veto, shouldVeto);
}
});
});
......@@ -26,7 +26,7 @@ import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchS
import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { TextFileService } from 'vs/workbench/services/textfile/common/textFileService';
import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IWriteTextFileOptions, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } from 'vs/platform/files/common/files';
import { FileOperationEvent, IFileService, IResolveContentOptions, FileOperationError, IFileStat, IResolveFileResult, FileChangesEvent, IResolveFileOptions, IContent, IStreamContent, ICreateFileOptions, ITextSnapshot, IResourceEncodings, IResourceEncoding, IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileStatWithMetadata, IResolveMetadataFileOptions, IWriteFileOptions } 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';
......@@ -82,7 +82,7 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { VSBuffer } from 'vs/base/common/buffer';
import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer';
export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput {
return instantiationService.createInstance(FileEditorInput, resource, undefined);
......@@ -984,7 +984,7 @@ export class TestFileService implements IFileService {
});
}
updateContent(resource: URI, _value: string | ITextSnapshot, _options?: IWriteTextFileOptions): Promise<IFileStatWithMetadata> {
writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
return timeout(0).then(() => ({
resource,
etag: 'index.txt',
......@@ -996,10 +996,6 @@ export class TestFileService implements IFileService {
}));
}
writeFile(resource: URI, bufferOrReadable: VSBuffer, options?: IWriteFileOptions): Promise<IFileStatWithMetadata> {
return this.updateContent(resource, bufferOrReadable.toString(), options);
}
move(_source: URI, _target: URI, _overwrite?: boolean): Promise<IFileStatWithMetadata> {
return Promise.resolve(null!);
}
......@@ -1008,11 +1004,7 @@ export class TestFileService implements IFileService {
throw new Error('not implemented');
}
createFile(_resource: URI, _content?: string, _options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
throw new Error('not implemented');
}
createFile2(_resource: URI, _content?: VSBuffer, _options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
createFile(_resource: URI, _content?: VSBuffer | VSBufferReadable, _options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
throw new Error('not implemented');
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册