From 1d143981b45110432de649a8f2d8c1436c107d2c Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 11 Apr 2018 12:06:50 +0200 Subject: [PATCH] Merge fileServices in /node and /electron-browser (fixes #38776) --- src/vs/platform/files/common/files.ts | 5 - .../extensionsTipsService.test.ts | 6 +- .../relauncher.contribution.ts | 8 + .../backupFileService.test.ts | 6 +- .../configurationEditingService.test.ts | 6 +- .../configurationService.test.ts | 12 +- .../files/electron-browser/fileService.ts | 1354 +++++++++++++++-- .../electron-browser/remoteFileService.ts | 6 +- .../services/files/node/fileService.ts | 1288 ---------------- .../fileService.test.ts | 54 +- .../fixtures/resolver/examples/company.js | 0 .../fixtures/resolver/examples/conway.js | 0 .../fixtures/resolver/examples/employee.js | 0 .../fixtures/resolver/examples/small.js | 0 .../fixtures/resolver/index.html | 0 .../fixtures/resolver/other/deep/company.js | 0 .../fixtures/resolver/other/deep/conway.js | 0 .../fixtures/resolver/other/deep/employee.js | 0 .../fixtures/resolver/other/deep/small.js | 0 .../fixtures/resolver/site.css | 0 .../fixtures/service/binary.txt | Bin .../fixtures/service/deep/company.js | 0 .../fixtures/service/deep/conway.js | 0 .../fixtures/service/deep/employee.js | 0 .../fixtures/service/deep/small.js | 0 .../fixtures/service/index.html | 0 .../fixtures/service/lorem.txt | 0 .../fixtures/service/small.txt | 0 .../fixtures/service/small_umlaut.txt | 0 .../fixtures/service/some_utf16le.css | Bin .../fixtures/service/some_utf8_bom.txt | 0 .../resolver.test.ts | 4 +- .../test/{node => electron-browser}/utils.ts | 0 .../watcher.test.ts | 0 .../keybindingEditing.test.ts | 15 +- .../workbench/test/workbenchTestServices.ts | 3 - 36 files changed, 1306 insertions(+), 1461 deletions(-) rename src/vs/workbench/services/backup/test/{node => electron-browser}/backupFileService.test.ts (98%) rename src/vs/workbench/services/configuration/test/{node => electron-browser}/configurationEditingService.test.ts (98%) rename src/vs/workbench/services/configuration/test/{node => electron-browser}/configurationService.test.ts (98%) delete mode 100644 src/vs/workbench/services/files/node/fileService.ts rename src/vs/workbench/services/files/test/{node => electron-browser}/fileService.test.ts (95%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/examples/company.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/examples/conway.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/examples/employee.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/examples/small.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/index.html (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/other/deep/company.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/other/deep/conway.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/other/deep/employee.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/other/deep/small.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/resolver/site.css (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/binary.txt (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/deep/company.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/deep/conway.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/deep/employee.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/deep/small.js (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/index.html (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/lorem.txt (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/small.txt (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/small_umlaut.txt (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/some_utf16le.css (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/fixtures/service/some_utf8_bom.txt (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/resolver.test.ts (97%) rename src/vs/workbench/services/files/test/{node => electron-browser}/utils.ts (100%) rename src/vs/workbench/services/files/test/{node => electron-browser}/watcher.test.ts (100%) rename src/vs/workbench/services/keybinding/test/{node => electron-browser}/keybindingEditing.test.ts (95%) diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 38114652f14..8efe3094e5d 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -140,11 +140,6 @@ export interface IFileService { */ unwatchFileChanges(resource: URI): void; - /** - * Configures the file service with the provided options. - */ - updateOptions(options: object): void; - /** * Returns the preferred encoding to use for a given resource. */ diff --git a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts index 98b711a62e8..3d75afd31e8 100644 --- a/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/parts/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -23,12 +23,12 @@ import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestTextResourceConfigurationService, TestContextService, TestLifecycleService, TestEnvironmentService, TestNotificationService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextResourceConfigurationService, TestContextService, TestLifecycleService, TestEnvironmentService, TestNotificationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import URI from 'vs/base/common/uri'; import { testWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import * as extfs from 'vs/base/node/extfs'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IPager } from 'vs/base/common/paging'; @@ -265,7 +265,7 @@ suite('ExtensionsTipsService Test', () => { const myWorkspace = testWorkspace(URI.from({ scheme: 'file', path: folderDir })); workspaceService = new TestContextService(myWorkspace); instantiationService.stub(IWorkspaceContextService, workspaceService); - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); }); } diff --git a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts index cc4f495a0d1..e1b621a5d90 100644 --- a/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts +++ b/src/vs/workbench/parts/relauncher/electron-browser/relauncher.contribution.ts @@ -26,6 +26,7 @@ interface IConfiguration extends IWindowsConfiguration { telemetry: { enableCrashReporter: boolean }; keyboard: { touchbar: { enabled: boolean } }; workbench: { tree: { horizontalScrolling: boolean } }; + files: { useExperimentalFileWatcher: boolean }; } export class SettingsChangeRelauncher implements IWorkbenchContribution { @@ -40,6 +41,7 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { private touchbarEnabled: boolean; private treeHorizontalScrolling: boolean; private windowsSmoothScrollingWorkaround: boolean; + private experimentalFileWatcher: boolean; private firstFolderResource: URI; private extensionHostRestarter: RunOnceScheduler; @@ -103,6 +105,12 @@ export class SettingsChangeRelauncher implements IWorkbenchContribution { changed = true; } + // Experimental File Watcher + if (config.files && typeof config.files.useExperimentalFileWatcher === 'boolean' && config.files.useExperimentalFileWatcher !== this.experimentalFileWatcher) { + this.experimentalFileWatcher = config.files.useExperimentalFileWatcher; + changed = true; + } + // macOS: Touchbar config if (isMacintosh && config.keyboard && config.keyboard.touchbar && typeof config.keyboard.touchbar.enabled === 'boolean' && config.keyboard.touchbar.enabled !== this.touchbarEnabled) { this.touchbarEnabled = config.keyboard.touchbar.enabled; diff --git a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts similarity index 98% rename from src/vs/workbench/services/backup/test/node/backupFileService.test.ts rename to src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index aa59da270d7..fc79fe68eeb 100644 --- a/src/vs/workbench/services/backup/test/node/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -14,9 +14,9 @@ import * as path from 'path'; import * as pfs from 'vs/base/node/pfs'; import Uri from 'vs/base/common/uri'; import { BackupFileService, BackupFilesModel } from 'vs/workbench/services/backup/node/backupFileService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { TextModel, createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestEnvironmentService, TestNotificationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { DefaultEndOfLine } from 'vs/editor/common/model'; @@ -38,7 +38,7 @@ const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', crypto.cre class TestBackupFileService extends BackupFileService { constructor(workspace: Uri, backupHome: string, workspacesJsonPath: string) { - const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true }); + const fileService = new FileService(new TestContextService(new Workspace(workspace.fsPath, workspace.fsPath, toWorkspaceFolders([{ path: workspace.fsPath }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); super(workspaceBackupPath, fileService); } diff --git a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts similarity index 98% rename from src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts rename to src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index 0712f46945d..4aa21047fda 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -18,11 +18,11 @@ import { parseArgs } from 'vs/platform/environment/node/argv'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestTextResourceConfigurationService, workbenchInstantiationService, TestLifecycleService, TestEnvironmentService, TestStorageService, TestNotificationService } from 'vs/workbench/test/workbenchTestServices'; import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode } from 'vs/workbench/services/configuration/node/configurationEditingService'; import { IFileService } from 'vs/platform/files/common/files'; import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; @@ -104,7 +104,7 @@ suite('ConfigurationEditingService', () => { instantiationService.stub(IWorkspaceContextService, workspaceService); return workspaceService.initialize(noWorkspace ? {} as IWindowConfiguration : workspaceDir).then(() => { instantiationService.stub(IConfigurationService, workspaceService); - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(ICommandService, CommandService); diff --git a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts similarity index 98% rename from src/vs/workbench/services/configuration/test/node/configurationService.test.ts rename to src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 651ce72f7c6..ac651e2834e 100644 --- a/src/vs/workbench/services/configuration/test/node/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -24,8 +24,8 @@ import { ConfigurationEditingErrorCode } from 'vs/workbench/services/configurati import { FileChangeType, FileChangesEvent, IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { ConfigurationTarget, IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { workbenchInstantiationService, TestTextResourceConfigurationService, TestTextFileService, TestLifecycleService, TestEnvironmentService, TestStorageService, TestNotificationService } from 'vs/workbench/test/workbenchTestServices'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -151,7 +151,7 @@ suite('WorkspaceContextService - Workspace', () => { return workspaceService.initialize({ id: configPath, configPath }).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); workspaceService.setInstantiationService(instantiationService); @@ -409,7 +409,7 @@ suite('WorkspaceService - Initialization', () => { instantiationService.stub(IEnvironmentService, environmentService); return workspaceService.initialize({}).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); workspaceService.setInstantiationService(instantiationService); @@ -667,7 +667,7 @@ suite('WorkspaceConfigurationService - Folder', () => { instantiationService.stub(IEnvironmentService, environmentService); return workspaceService.initialize(folderDir).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); workspaceService.setInstantiationService(instantiationService); @@ -980,7 +980,7 @@ suite('WorkspaceConfigurationService - Multiroot', () => { return workspaceService.initialize({ id: configPath, configPath }).then(() => { - instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService(workspaceService, TestEnvironmentService, new TestTextResourceConfigurationService(), workspaceService, new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true })); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); workspaceService.setInstantiationService(instantiationService); diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 7ea4e454d3c..7e459ad3dea 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -2,110 +2,221 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + 'use strict'; -import * as nls from 'vs/nls'; +import * as paths from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import * as assert from 'assert'; +import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot, IFilesConfiguration } from 'vs/platform/files/common/files'; +import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/files'; +import { isEqualOrParent } from 'vs/base/common/paths'; +import { ResourceMap } from 'vs/base/common/map'; +import * as arrays from 'vs/base/common/arrays'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -import * as paths from 'vs/base/common/paths'; -import * as encoding from 'vs/base/node/encoding'; -import * as errors from 'vs/base/common/errors'; +import * as objects from 'vs/base/common/objects'; +import * as extfs from 'vs/base/node/extfs'; +import { nfcall, ThrottledDelayer } from 'vs/base/common/async'; import uri from 'vs/base/common/uri'; -import { FileOperation, FileOperationEvent, IFileService, IFilesConfiguration, IResolveFileOptions, IFileStat, IResolveFileResult, IContent, IStreamContent, IImportResult, IResolveContentOptions, IUpdateContentOptions, FileChangesEvent, ICreateFileOptions, ITextSnapshot } from 'vs/platform/files/common/files'; -import { FileService as NodeFileService, IFileServiceOptions, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; -import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import * as nls from 'vs/nls'; +import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; +import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import * as pfs from 'vs/base/node/pfs'; +import * as encoding from 'vs/base/node/encoding'; +import * as flow from 'vs/base/node/flow'; +import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService'; +import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService'; +import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; import { Event, Emitter } from 'vs/base/common/event'; -import { shell } from 'electron'; +import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; -import product from 'vs/platform/node/product'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { Readable } from 'stream'; import { Schemas } from 'vs/base/common/network'; -import { Severity, INotificationService } from 'vs/platform/notification/common/notification'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import product from 'vs/platform/node/product'; import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; +import { shell } from 'electron'; + +export interface IEncodingOverride { + parent?: uri; + extension?: string; + encoding: string; +} + +export interface IFileServiceTestOptions { + tmpDir?: string; + disableWatcher?: boolean; + encodingOverride?: IEncodingOverride[]; +} + +function etag(stat: fs.Stats): string; +function etag(size: number, mtime: number): string; +function etag(arg1: any, arg2?: any): string { + let size: number; + let mtime: number; + if (typeof arg2 === 'number') { + size = arg1; + mtime = arg2; + } else { + size = (arg1).size; + mtime = (arg1).mtime.getTime(); + } + + return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`; +} + +class BufferPool { + + static _64K = new BufferPool(64 * 1024, 5); + + constructor( + readonly bufferSize: number, + private readonly _capacity: number, + private readonly _free: Buffer[] = [], + ) { + // + } + + acquire(): Buffer { + if (this._free.length === 0) { + return Buffer.allocUnsafe(this.bufferSize); + } else { + return this._free.shift(); + } + } + + release(buf: Buffer): void { + if (this._free.length <= this._capacity) { + this._free.push(buf); + } + } +} export class FileService implements IFileService { public _serviceBrand: any; - // If we run with .NET framework < 4.5, we need to detect this error to inform the user + private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) + private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) + private static readonly NET_VERSION_ERROR = 'System.MissingMethodException'; private static readonly NET_VERSION_ERROR_IGNORE_KEY = 'ignoreNetVersionError'; private static readonly ENOSPC_ERROR = 'ENOSPC'; private static readonly ENOSPC_ERROR_IGNORE_KEY = 'ignoreEnospcError'; - private raw: NodeFileService; + protected readonly _onFileChanges: Emitter; + protected readonly _onAfterOperation: Emitter; + + private tmpPath: string; - private toUnbind: IDisposable[]; + private toDispose: IDisposable[]; - protected _onFileChanges: Emitter; - protected _onAfterOperation: Emitter; + private activeWorkspaceFileChangeWatcher: IDisposable; + private activeFileChangesWatchers: ResourceMap; + private fileChangesWatchDelayer: ThrottledDelayer; + private undeliveredRawFileChangesEvents: IRawFileChange[]; + + private useExperimentalFileWatcher: boolean; + private watcherIgnoredPatterns: string[]; + + private encodingOverride: IEncodingOverride[]; constructor( - @IConfigurationService private configurationService: IConfigurationService, - @IWorkspaceContextService private contextService: IWorkspaceContextService, - @IEnvironmentService private environmentService: IEnvironmentService, - @ILifecycleService private lifecycleService: ILifecycleService, - @INotificationService private notificationService: INotificationService, - @IStorageService private storageService: IStorageService, - @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService + private contextService: IWorkspaceContextService, + private environmentService: IEnvironmentService, + private textResourceConfigurationService: ITextResourceConfigurationService, + private configurationService: IConfigurationService, + private lifecycleService: ILifecycleService, + private storageService: IStorageService, + private notificationService: INotificationService, + private options: IFileServiceTestOptions = Object.create(null) ) { - this.toUnbind = []; + this.toDispose = []; + this.tmpPath = this.options.tmpDir || os.tmpdir(); this._onFileChanges = new Emitter(); - this.toUnbind.push(this._onFileChanges); + this.toDispose.push(this._onFileChanges); this._onAfterOperation = new Emitter(); - this.toUnbind.push(this._onAfterOperation); + this.toDispose.push(this._onAfterOperation); + + this.activeFileChangesWatchers = new ResourceMap(); + this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); + this.undeliveredRawFileChangesEvents = []; const configuration = this.configurationService.getValue(); - let watcherIgnoredPatterns: string[] = []; + this.watcherIgnoredPatterns = []; if (configuration.files && configuration.files.watcherExclude) { - watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); - } - - // build config - const fileServiceConfig: IFileServiceOptions = { - errorLogger: (msg: string) => this.onFileServiceError(msg), - encodingOverride: this.getEncodingOverrides(), - watcherIgnoredPatterns, - verboseLogging: environmentService.verbose, - useExperimentalFileWatcher: configuration.files.useExperimentalFileWatcher, - elevationSupport: { - cliPath: this.environmentService.cliPath, - promptTitle: this.environmentService.appNameLong, - promptIcnsPath: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0 - } - }; + this.watcherIgnoredPatterns = Object.keys(configuration.files.watcherExclude).filter(k => !!configuration.files.watcherExclude[k]); + } - // create service - this.raw = new NodeFileService(contextService, environmentService, textResourceConfigurationService, configurationService, lifecycleService, fileServiceConfig); + this.useExperimentalFileWatcher = configuration.files && configuration.files.useExperimentalFileWatcher; + + this.encodingOverride = this.options.encodingOverride || this.getEncodingOverrides(); - // Listeners this.registerListeners(); } - public get onFileChanges(): Event { - return this._onFileChanges.event; + private registerListeners(): void { + + // Wait until we are fully running before starting file watchers + this.lifecycleService.when(LifecyclePhase.Running).then(() => { + this.setupFileWatching(); + }); + + // Workbench State Change + this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => { + if (this.lifecycleService.phase >= LifecyclePhase.Running) { + this.setupFileWatching(); + } + })); + + // Workspace Folder Change + this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => { + this.encodingOverride = this.getEncodingOverrides(); + })); + + // Lifecycle + this.lifecycleService.onShutdown(this.dispose, this); } - public get onAfterOperation(): Event { - return this._onAfterOperation.event; + private getEncodingOverrides(): IEncodingOverride[] { + const encodingOverride: IEncodingOverride[] = []; + + // Global settings + encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); + + // Workspace files + encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 }); + + // Folder Settings + this.contextService.getWorkspace().folders.forEach(folder => { + encodingOverride.push({ parent: uri.file(paths.join(folder.uri.fsPath, '.vscode')), encoding: encoding.UTF8 }); + }); + + return encodingOverride; } - private onFileServiceError(error: string | Error): void { + private handleError(error: string | Error): void { const msg = error ? error.toString() : void 0; if (!msg) { return; } // Forward to unexpected error handler - errors.onUnexpectedError(msg); + onUnexpectedError(msg); // Detect if we run < .NET Framework 4.5 (TODO@ben remove with new watcher impl) if (msg.indexOf(FileService.NET_VERSION_ERROR) >= 0 && !this.storageService.getBoolean(FileService.NET_VERSION_ERROR_IGNORE_KEY, StorageScope.WORKSPACE)) { @@ -142,95 +253,761 @@ export class FileService implements IFileService { } } - private registerListeners(): void { - - // File events - this.toUnbind.push(this.raw.onFileChanges(e => this._onFileChanges.fire(e))); - this.toUnbind.push(this.raw.onAfterOperation(e => this._onAfterOperation.fire(e))); - - // Config changes - this.toUnbind.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationChange(e))); - - // Root changes - this.toUnbind.push(this.contextService.onDidChangeWorkspaceFolders(() => this.onDidChangeWorkspaceFolders())); + public get onFileChanges(): Event { + return this._onFileChanges.event; + } - // Lifecycle - this.lifecycleService.onShutdown(this.dispose, this); + public get onAfterOperation(): Event { + return this._onAfterOperation.event; } - private onDidChangeWorkspaceFolders(): void { - this.updateOptions({ encodingOverride: this.getEncodingOverrides() }); + public updateOptions(options: IFileServiceTestOptions): void { + if (options) { + objects.mixin(this.options, options); // overwrite current options + } } - private getEncodingOverrides(): IEncodingOverride[] { - const encodingOverride: IEncodingOverride[] = []; + private setupFileWatching(): void { - // Global settings - encodingOverride.push({ parent: uri.file(this.environmentService.appSettingsHome), encoding: encoding.UTF8 }); + // dispose old if any + if (this.activeWorkspaceFileChangeWatcher) { + this.activeWorkspaceFileChangeWatcher.dispose(); + } - // Workspace files - encodingOverride.push({ extension: WORKSPACE_EXTENSION, encoding: encoding.UTF8 }); + // Return if not aplicable + const workbenchState = this.contextService.getWorkbenchState(); + if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) { + return; + } - // Folder Settings - this.contextService.getWorkspace().folders.forEach(folder => { - encodingOverride.push({ parent: uri.file(paths.join(folder.uri.fsPath, '.vscode')), encoding: encoding.UTF8 }); - }); + // new watcher: use it if setting tells us so or we run in multi-root environment + if (this.useExperimentalFileWatcher || workbenchState === WorkbenchState.WORKSPACE) { + this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupNsfwWorkspaceWatching().startWatching()); + } - return encodingOverride; + // old watcher + else { + if (isWindows) { + this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupWin32WorkspaceWatching().startWatching()); + } else { + this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupUnixWorkspaceWatching().startWatching()); + } + } } - private onConfigurationChange(event: IConfigurationChangeEvent): void { - if (event.affectsConfiguration('files.useExperimentalFileWatcher')) { - this.updateOptions({ useExperimentalFileWatcher: this.configurationService.getValue('files.useExperimentalFileWatcher') }); - } + private setupWin32WorkspaceWatching(): WindowsWatcherService { + return new WindowsWatcherService(this.contextService, this.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); + } + + private setupUnixWorkspaceWatching(): UnixWatcherService { + return new UnixWatcherService(this.contextService, this.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); } - public updateOptions(options: object): void { - this.raw.updateOptions(options); + private setupNsfwWorkspaceWatching(): NsfwWatcherService { + return new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), err => this.handleError(err), this.environmentService.verbose); } public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { - return this.raw.resolveFile(resource, options); + return this.resolve(resource, options); } public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { - return this.raw.resolveFiles(toResolve); + return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options) + .then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false })))); } public existsFile(resource: uri): TPromise { - return this.raw.existsFile(resource); + return this.resolveFile(resource).then(() => true, () => false); } public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.raw.resolveContent(resource, options); + return this.resolveStreamContent(resource, options).then(streamContent => { + return new TPromise((resolve, reject) => { + + const result: IContent = { + resource: streamContent.resource, + name: streamContent.name, + mtime: streamContent.mtime, + etag: streamContent.etag, + encoding: streamContent.encoding, + value: '' + }; + + streamContent.value.on('data', chunk => result.value += chunk); + streamContent.value.on('error', err => reject(err)); + streamContent.value.on('end', _ => resolve(result)); + + return result; + }); + }); } public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.raw.resolveStreamContent(resource, options); + + // Guard early against attempts to resolve an invalid file path + if (resource.scheme !== Schemas.file || !resource.fsPath) { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), + FileOperationResult.FILE_INVALID_PATH, + options + )); + } + + const result: IStreamContent = { + resource: void 0, + name: void 0, + mtime: void 0, + etag: void 0, + encoding: void 0, + value: void 0 + }; + + const contentResolverTokenSource = new CancellationTokenSource(); + + const onStatError = (error: Error) => { + + // error: stop reading the file the stat and content resolve call + // usually race, mostly likely the stat call will win and cancel + // the content call + contentResolverTokenSource.cancel(); + + // forward error + return TPromise.wrapError(error); + }; + + const statsPromise = this.resolveFile(resource).then(stat => { + result.resource = stat.resource; + result.name = stat.name; + result.mtime = stat.mtime; + result.etag = stat.etag; + + // Return early if resource is a directory + if (stat.isDirectory) { + return onStatError(new FileOperationError( + nls.localize('fileIsDirectoryError', "File is directory"), + FileOperationResult.FILE_IS_DIRECTORY, + options + )); + } + + // Return early if file not modified since + if (options && options.etag && options.etag === stat.etag) { + return onStatError(new FileOperationError( + nls.localize('fileNotModifiedError', "File not modified since"), + FileOperationResult.FILE_NOT_MODIFIED_SINCE, + options + )); + } + + // Return early if file is too large to load + if (typeof stat.size === 'number') { + if (stat.size > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { + return onStatError(new FileOperationError( + nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), + FileOperationResult.FILE_EXCEED_MEMORY_LIMIT + )); + } + + if (stat.size > MAX_FILE_SIZE) { + return onStatError(new FileOperationError( + nls.localize('fileTooLargeError', "File too large to open"), + FileOperationResult.FILE_TOO_LARGE + )); + } + } + + return void 0; + }, err => { + + // Wrap file not found errors + if (err.code === 'ENOENT') { + return onStatError(new FileOperationError( + nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + FileOperationResult.FILE_NOT_FOUND, + options + )); + } + + return onStatError(err); + }); + + let completePromise: Thenable; + + // await the stat iff we already have an etag so that we compare the + // etag from the stat before we actually read the file again. + if (options && options.etag) { + completePromise = statsPromise.then(() => { + return this.fillInContents(result, resource, options, contentResolverTokenSource.token); // Waterfall -> only now resolve the contents + }); + } + + // a fresh load without a previous etag which means we can resolve the file stat + // and the content at the same time, avoiding the waterfall. + else { + completePromise = Promise.all([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]); + } + + return TPromise.wrap(completePromise).then(() => { + contentResolverTokenSource.dispose(); + + return result; + }); } - public updateContent(resource: uri, value: string | ITextSnapshot, options?: IUpdateContentOptions): TPromise { - return this.raw.updateContent(resource, value, options); + private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { + return this.resolveFileData(resource, options, token).then(data => { + content.encoding = data.encoding; + content.value = data.stream; + }); } - public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.raw.moveFile(source, target, overwrite); + private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { + + const chunkBuffer = BufferPool._64K.acquire(); + + const result: IContentData = { + encoding: void 0, + stream: void 0 + }; + + return new Promise((resolve, reject) => { + fs.open(this.toAbsolutePath(resource), 'r', (err, fd) => { + if (err) { + if (err.code === 'ENOENT') { + // Wrap file not found errors + err = new FileOperationError( + nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), + FileOperationResult.FILE_NOT_FOUND, + options + ); + } + + return reject(err); + } + + let decoder: NodeJS.ReadWriteStream; + let totalBytesRead = 0; + + const finish = (err?: any) => { + + if (err) { + if (err.code === 'EISDIR') { + // Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it) + err = new FileOperationError( + nls.localize('fileIsDirectoryError', "File is directory"), + FileOperationResult.FILE_IS_DIRECTORY, + options + ); + } + if (decoder) { + // If the decoder already started, we have to emit the error through it as + // event because the promise is already resolved! + decoder.emit('error', err); + } else { + reject(err); + } + } + if (decoder) { + decoder.end(); + } + + // return the shared buffer + BufferPool._64K.release(chunkBuffer); + + if (fd) { + fs.close(fd, err => { + if (err) { + this.handleError(`resolveFileData#close(): ${err.toString()}`); + } + }); + } + }; + + const handleChunk = (bytesRead: number) => { + if (token.isCancellationRequested) { + // cancellation -> finish + finish(new Error('cancelled')); + } else if (bytesRead === 0) { + // no more data -> finish + finish(); + } else if (bytesRead < chunkBuffer.length) { + // write the sub-part of data we received -> repeat + decoder.write(chunkBuffer.slice(0, bytesRead), readChunk); + } else { + // write all data we received -> repeat + decoder.write(chunkBuffer, readChunk); + } + }; + + let currentPosition: number = (options && options.position) || null; + + const readChunk = () => { + fs.read(fd, chunkBuffer, 0, chunkBuffer.length, currentPosition, (err, bytesRead) => { + totalBytesRead += bytesRead; + + if (typeof currentPosition === 'number') { + // if we received a position argument as option we need to ensure that + // we advance the position by the number of bytesread + currentPosition += bytesRead; + } + + if (totalBytesRead > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { + finish(new FileOperationError( + nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), + FileOperationResult.FILE_EXCEED_MEMORY_LIMIT + )); + } + + if (totalBytesRead > MAX_FILE_SIZE) { + // stop when reading too much + finish(new FileOperationError( + nls.localize('fileTooLargeError', "File too large to open"), + FileOperationResult.FILE_TOO_LARGE, + options + )); + } else if (err) { + // some error happened + finish(err); + + } else if (decoder) { + // pass on to decoder + handleChunk(bytesRead); + + } else { + // when receiving the first chunk of data we need to create the + // decoding stream which is then used to drive the string stream. + TPromise.as(encoding.detectEncodingFromBuffer( + { buffer: chunkBuffer, bytesRead }, + options && options.autoGuessEncoding || this.configuredAutoGuessEncoding(resource) + )).then(detected => { + + if (options && options.acceptTextOnly && detected.seemsBinary) { + // Return error early if client only accepts text and this is not text + finish(new FileOperationError( + nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), + FileOperationResult.FILE_IS_BINARY, + options + )); + + } else { + result.encoding = this.getEncoding(resource, this.getPeferredEncoding(resource, options, detected)); + result.stream = decoder = encoding.decodeStream(result.encoding); + resolve(result); + handleChunk(bytesRead); + } + + }).then(void 0, err => { + // failed to get encoding + finish(err); + }); + } + }); + }; + + // start reading + readChunk(); + }); + }); } - public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.raw.copyFile(source, target, overwrite); + public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + if (options.writeElevated) { + return this.doUpdateContentElevated(resource, value, options); + } + + return this.doUpdateContent(resource, value, options); } - public createFile(resource: uri, content?: string, options?: ICreateFileOptions): TPromise { - return this.raw.createFile(resource, content, options); + private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + // 1.) check file for writing + return this.checkFileBeforeWriting(absolutePath, options).then(exists => { + let createParentsPromise: TPromise; + if (exists) { + createParentsPromise = TPromise.as(null); + } else { + createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath)); + } + + // 2.) create parents as needed + return createParentsPromise.then(() => { + const encodingToWrite = this.getEncoding(resource, options.encoding); + let addBomPromise: TPromise = TPromise.as(false); + + // UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM + if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) { + addBomPromise = TPromise.as(true); + } + + // Existing UTF-8 file: check for options regarding BOM + else if (exists && encodingToWrite === encoding.UTF8) { + if (options.overwriteEncoding) { + addBomPromise = TPromise.as(false); // if we are to overwrite the encoding, we do not preserve it if found + } else { + addBomPromise = encoding.detectEncodingByBOM(absolutePath).then(enc => enc === encoding.UTF8); // otherwise preserve it if found + } + } + + // 3.) check to add UTF BOM + return addBomPromise.then(addBom => { + + // 4.) set contents and resolve + return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite).then(void 0, error => { + if (!exists || error.code !== 'EPERM' || !isWindows) { + return TPromise.wrapError(error); + } + + // On Windows and if the file exists with an EPERM error, we try 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) + + // 5.) truncate + return pfs.truncate(absolutePath, 0).then(() => { + + // 6.) set contents (this time with r+ mode) and resolve again + return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }); + }); + }); + }); + }); + }).then(null, error => { + if (error.code === 'EACCES' || error.code === 'EPERM') { + return TPromise.wrapError(new FileOperationError( + nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), + FileOperationResult.FILE_PERMISSION_DENIED, + options + )); + } + + return TPromise.wrapError(error); + }); + } + + private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { + let writeFilePromise: TPromise; + + // Configure encoding related options as needed + const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null); + if (addBOM || encodingToWrite !== encoding.UTF8) { + writeFileOptions.encoding = { + charset: encodingToWrite, + addBOM + }; + } + + if (typeof value === 'string') { + writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions); + } else { + writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value), writeFileOptions); + } + + // set contents + return writeFilePromise.then(() => { + + // resolve + return this.resolve(resource); + }); + } + + private snapshotToReadableStream(snapshot: ITextSnapshot): NodeJS.ReadableStream { + return new Readable({ + read: function () { + try { + let chunk: string; + let canPush = true; + + // Push all chunks as long as we can push and as long as + // the underlying snapshot returns strings to us + while (canPush && typeof (chunk = snapshot.read()) === 'string') { + canPush = this.push(chunk); + } + + // Signal EOS by pushing NULL + if (typeof chunk !== 'string') { + this.push(null); + } + } catch (error) { + this.emit('error', error); + } + }, + encoding: encoding.UTF8 // very important, so that strings are passed around and not buffers! + }); + } + + private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + // 1.) check file 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: IUpdateContentOptions = objects.assign(Object.create(null), options); + writeOptions.writeElevated = false; + writeOptions.encoding = this.getEncoding(resource, options.encoding); + + // 2.) write to a temporary file to be able to copy over later + const tmpPath = paths.join(this.tmpPath, `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 TPromise((c, e) => { + const promptOptions = { + name: this.environmentService.appNameLong.replace('-', ''), + icns: (isMacintosh && this.environmentService.isBuilt) ? paths.join(paths.dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : void 0 + }; + + 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) { + e(error || stderr); + } else { + c(void 0); + } + }); + }); + }).then(() => { + + // 3.) delete temp file + return pfs.del(tmpPath, this.tmpPath).then(() => { + + // 4.) resolve again + return this.resolve(resource); + }); + }); + }); + }).then(null, error => { + if (this.environmentService.verbose) { + this.handleError(`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 TPromise.wrapError(error); + }); + } + + public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + let checkFilePromise: TPromise; + if (options.overwrite) { + checkFilePromise = TPromise.as(false); + } else { + checkFilePromise = pfs.exists(absolutePath); + } + + // Check file exists + return checkFilePromise.then(exists => { + if (exists && !options.overwrite) { + return TPromise.wrapError(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; + }); + }); } public createFolder(resource: uri): TPromise { - return this.raw.createFolder(resource); + + // 1.) Create folder + const absolutePath = this.toAbsolutePath(resource); + return pfs.mkdirp(absolutePath).then(() => { + + // 2.) Resolve + return this.resolve(resource).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); + + return result; + }); + }); + } + + private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): TPromise { + return pfs.exists(absolutePath).then(exists => { + if (exists) { + return pfs.stat(absolutePath).then(stat => { + if (stat.isDirectory()) { + return TPromise.wrapError(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 TPromise.wrapError(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(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(options); + } + + return exists; + }); + }); + } + + return TPromise.as(exists); + }); + } + + return TPromise.as(exists); + }); + } + + private readOnlyError(options: IUpdateContentOptions): TPromise { + return TPromise.wrapError(new FileOperationError( + nls.localize('fileReadOnlyError', "File is Read Only"), + FileOperationResult.FILE_READ_ONLY, + options + )); } public rename(resource: uri, newName: string): TPromise { - return this.raw.rename(resource, newName); + const newPath = paths.join(paths.dirname(resource.fsPath), newName); + + return this.moveFile(resource, uri.file(newPath)); + } + + public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { + return this.moveOrCopyFile(source, target, false, overwrite); + } + + public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { + return this.moveOrCopyFile(source, target, true, overwrite); + } + + private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): TPromise { + const sourcePath = this.toAbsolutePath(source); + const targetPath = this.toAbsolutePath(target); + + // 1.) move / copy + return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => { + + // 2.) resolve + return this.resolve(target).then(result => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result)); + + return result; + }); + }); + } + + private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): TPromise { + + // 1.) validate operation + if (isParent(targetPath, sourcePath, !isLinux)) { + return TPromise.wrapError(new Error('Unable to move/copy when source path is parent of target path')); + } + + // 2.) check if target exists + return pfs.exists(targetPath).then(exists => { + const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase(); + const isSameFile = sourcePath === targetPath; + + // Return early with conflict if target exists and we are not told to overwrite + if (exists && !isCaseRename && !overwrite) { + return TPromise.wrapError(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT)); + } + + // 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file + let deleteTargetPromise = TPromise.wrap(void 0); + if (exists && !isCaseRename) { + if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) { + return TPromise.wrapError(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! + } + + deleteTargetPromise = this.del(uri.file(targetPath)); + } + + return deleteTargetPromise.then(() => { + + // 4.) make sure parents exists + return pfs.mkdirp(paths.dirname(targetPath)).then(() => { + + // 4.) copy/move + if (isSameFile) { + return TPromise.wrap(null); + } else if (keepCopy) { + return nfcall(extfs.copy, sourcePath, targetPath); + } else { + return nfcall(extfs.mv, sourcePath, targetPath); + } + }).then(() => exists); + }); + }); + } + + public importFile(source: uri, targetFolder: uri): TPromise { + const sourcePath = this.toAbsolutePath(source); + const targetResource = uri.file(paths.join(targetFolder.fsPath, paths.basename(source.fsPath))); + const targetPath = this.toAbsolutePath(targetResource); + + // 1.) resolve + return pfs.stat(sourcePath).then(stat => { + if (stat.isDirectory()) { + return TPromise.wrapError(new Error(nls.localize('foldersCopyError', "Folders cannot be copied into the workspace. Please select individual files to copy them."))); // for now we do not allow to import a folder into a workspace + } + + // 2.) copy + return this.doMoveOrCopyFile(sourcePath, targetPath, true, true).then(exists => { + + // 3.) resolve + return this.resolve(targetResource).then(stat => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.IMPORT, stat)); + + return { isNew: !exists, stat }; + }); + }); + }); } public del(resource: uri, useTrash?: boolean): TPromise { @@ -238,7 +1015,7 @@ export class FileService implements IFileService { return this.doMoveItemToTrash(resource); } - return this.raw.del(resource); + return this.doDelete(resource); } private doMoveItemToTrash(resource: uri): TPromise { @@ -253,44 +1030,367 @@ export class FileService implements IFileService { return TPromise.as(null); } - public importFile(source: uri, targetFolder: uri): TPromise { - return this.raw.importFile(source, targetFolder).then((result) => { - return { - isNew: result && result.isNew, - stat: result && result.stat - }; + private doDelete(resource: uri): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + return pfs.del(absolutePath, this.tmpPath).then(() => { + + // Events + this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); }); } - public watchFileChanges(resource: uri): void { - if (!resource) { - return; + // Helpers + + private toAbsolutePath(arg1: uri | IFileStat): string { + let resource: uri; + if (arg1 instanceof uri) { + resource = arg1; + } else { + resource = (arg1).resource; } - if (resource.scheme !== Schemas.file) { - return; // only support files + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); + + return paths.normalize(resource.fsPath); + } + + private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): TPromise { + return this.toStatResolver(resource) + .then(model => model.resolve(options)); + } + + private toStatResolver(resource: uri): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => { + return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.environmentService.verbose ? err => this.handleError(err) : void 0); + }); + } + + private getPeferredEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string { + let preferredEncoding: string; + if (options && options.encoding) { + if (detected.encoding === encoding.UTF8 && options.encoding === encoding.UTF8) { + preferredEncoding = encoding.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 + } + } else if (detected.encoding) { + if (detected.encoding === encoding.UTF8) { + preferredEncoding = encoding.UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM + } else { + preferredEncoding = detected.encoding; + } + } else if (this.configuredEncoding(resource) === encoding.UTF8_with_bom) { + preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then } + return preferredEncoding; + } - // return early if the resource is inside the workspace for which we have another watcher in place - if (this.contextService.isInsideWorkspace(resource)) { - return; + public getEncoding(resource: uri, preferredEncoding?: string): string { + let fileEncoding: string; + + const override = this.getEncodingOverride(resource); + if (override) { + fileEncoding = override; + } else if (preferredEncoding) { + fileEncoding = preferredEncoding; + } else { + fileEncoding = this.configuredEncoding(resource); + } + + if (!fileEncoding || !encoding.encodingExists(fileEncoding)) { + fileEncoding = encoding.UTF8; // the default is UTF 8 } - this.raw.watchFileChanges(resource); + return fileEncoding; } - public unwatchFileChanges(resource: uri): void { - this.raw.unwatchFileChanges(resource); + private configuredAutoGuessEncoding(resource: uri): boolean { + return this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'); } - public getEncoding(resource: uri, preferredEncoding?: string): string { - return this.raw.getEncoding(resource, preferredEncoding); + private configuredEncoding(resource: uri): string { + return this.textResourceConfigurationService.getValue(resource, 'files.encoding'); + } + + private getEncodingOverride(resource: uri): string { + if (resource && this.encodingOverride && this.encodingOverride.length) { + for (let i = 0; i < this.encodingOverride.length; i++) { + const override = this.encodingOverride[i]; + + // check if the resource is child of encoding override path + if (override.parent && isParent(resource.fsPath, override.parent.fsPath, !isLinux /* ignorecase */)) { + return override.encoding; + } + + // check if the resource extension is equal to encoding override + if (override.extension && paths.extname(resource.fsPath) === `.${override.extension}`) { + return override.encoding; + } + } + } + + return null; + } + + public watchFileChanges(resource: uri): void { + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); + + // Create or get watcher for provided path + let watcher = this.activeFileChangesWatchers.get(resource); + if (!watcher) { + const fsPath = resource.fsPath; + const fsName = paths.basename(resource.fsPath); + + watcher = extfs.watch(fsPath, (eventType: string, filename: string) => { + const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); + + // The file was either deleted or renamed. Many tools apply changes to files in an + // atomic way ("Atomic Save") by first renaming the file to a temporary name and then + // renaming it back to the original name. Our watcher will detect this as a rename + // and then stops to work on Mac and Linux because the watcher is applied to the + // inode and not the name. The fix is to detect this case and trying to watch the file + // again after a certain delay. + // In addition, we send out a delete event if after a timeout we detect that the file + // does indeed not exist anymore. + if (renamedOrDeleted) { + + // Very important to dispose the watcher which now points to a stale inode + this.unwatchFileChanges(resource); + + // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") + setTimeout(() => { + this.existsFile(resource).done(exists => { + + // File still exists, so reapply the watcher + if (exists) { + this.watchFileChanges(resource); + } + + // File seems to be really gone, so emit a deleted event + else { + this.onRawFileChange({ + type: FileChangeType.DELETED, + path: fsPath + }); + } + }); + }, FileService.FS_REWATCH_DELAY); + } + + // Handle raw file change + this.onRawFileChange({ + type: FileChangeType.UPDATED, + path: fsPath + }); + }, (error: string) => this.handleError(error)); + + if (watcher) { + this.activeFileChangesWatchers.set(resource, watcher); + } + } + } + + private onRawFileChange(event: IRawFileChange): void { + + // add to bucket of undelivered events + this.undeliveredRawFileChangesEvents.push(event); + + if (this.environmentService.verbose) { + console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); + } + + // handle emit through delayer to accommodate for bulk changes + this.fileChangesWatchDelayer.trigger(() => { + const buffer = this.undeliveredRawFileChangesEvents; + this.undeliveredRawFileChangesEvents = []; + + // Normalize + const normalizedEvents = normalize(buffer); + + // Logging + if (this.environmentService.verbose) { + normalizedEvents.forEach(r => { + console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); + }); + } + + // Emit + this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); + + return TPromise.as(null); + }); + } + + public unwatchFileChanges(resource: uri): void { + const watcher = this.activeFileChangesWatchers.get(resource); + if (watcher) { + watcher.close(); + this.activeFileChangesWatchers.delete(resource); + } } public dispose(): void { - this.toUnbind = dispose(this.toUnbind); + this.toDispose = dispose(this.toDispose); + + if (this.activeWorkspaceFileChangeWatcher) { + this.activeWorkspaceFileChangeWatcher.dispose(); + this.activeWorkspaceFileChangeWatcher = null; + } + + this.activeFileChangesWatchers.forEach(watcher => watcher.close()); + this.activeFileChangesWatchers.clear(); + } +} + +export class StatResolver { + private name: string; + private etag: string; + + constructor( + private resource: uri, + private isSymbolicLink: boolean, + private isDirectory: boolean, + private mtime: number, + private size: number, + private errorLogger?: (error: Error | string) => void + ) { + assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); + + this.name = getBaseLabel(resource); + this.etag = etag(size, mtime); + } + + public resolve(options: IResolveFileOptions): TPromise { + + // General Data + const fileStat: IFileStat = { + resource: this.resource, + isDirectory: this.isDirectory, + isSymbolicLink: this.isSymbolicLink, + name: this.name, + etag: this.etag, + size: this.size, + mtime: this.mtime + }; + + // File Specific Data + if (!this.isDirectory) { + return TPromise.as(fileStat); + } - // Dispose service - this.raw.dispose(); + // Directory Specific Data + else { + + // Convert the paths from options.resolveTo to absolute paths + let absoluteTargetPaths: string[] = null; + if (options && options.resolveTo) { + absoluteTargetPaths = []; + options.resolveTo.forEach(resource => { + absoluteTargetPaths.push(resource.fsPath); + }); + } + + return new TPromise((c, e) => { + + // Load children + this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, options && options.resolveSingleChildDescendants, children => { + children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child) + fileStat.children = children || []; + + c(fileStat); + }); + }); + } + } + + private resolveChildren(absolutePath: string, absoluteTargetPaths: string[], resolveSingleChildDescendants: boolean, callback: (children: IFileStat[]) => void): void { + extfs.readdir(absolutePath, (error: Error, files: string[]) => { + if (error) { + if (this.errorLogger) { + this.errorLogger(error); + } + + return callback(null); // return - we might not have permissions to read the folder + } + + // for each file in the folder + flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => { + const fileResource = uri.file(paths.resolve(absolutePath, file)); + let fileStat: fs.Stats; + let isSymbolicLink = false; + const $this = this; + + flow.sequence( + function onError(error: Error): void { + if ($this.errorLogger) { + $this.errorLogger(error); + } + + clb(null, null); // return - we might not have permissions to read the folder or stat the file + }, + + function stat(this: any): void { + extfs.statLink(fileResource.fsPath, this); + }, + + function countChildren(this: any, statAndLink: extfs.IStatAndLink): void { + fileStat = statAndLink.stat; + isSymbolicLink = statAndLink.isSymbolicLink; + + if (fileStat.isDirectory()) { + extfs.readdir(fileResource.fsPath, (error, result) => { + this(null, result ? result.length : 0); + }); + } else { + this(null, 0); + } + }, + + function resolve(childCount: number): void { + const childStat: IFileStat = { + resource: fileResource, + isDirectory: fileStat.isDirectory(), + isSymbolicLink, + name: file, + mtime: fileStat.mtime.getTime(), + etag: etag(fileStat), + size: fileStat.size + }; + + // Return early for files + if (!fileStat.isDirectory()) { + return clb(null, childStat); + } + + // Handle Folder + let resolveFolderChildren = false; + if (files.length === 1 && resolveSingleChildDescendants) { + resolveFolderChildren = true; + } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) { + resolveFolderChildren = true; + } + + // Continue resolving children based on condition + if (resolveFolderChildren) { + $this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => { + children = arrays.coalesce(children); // we don't want those null children + childStat.children = children || []; + + clb(null, childStat); + }); + } + + // Otherwise return result + else { + clb(null, childStat); + } + }); + }, (errors, result) => { + callback(result); + }); + }); } } diff --git a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts index 8a4a0c41cc7..2137a762b6b 100644 --- a/src/vs/workbench/services/files/electron-browser/remoteFileService.ts +++ b/src/vs/workbench/services/files/electron-browser/remoteFileService.ts @@ -84,13 +84,13 @@ export class RemoteFileService extends FileService { @ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService, ) { super( - configurationService, contextService, environmentService, + textResourceConfigurationService, + configurationService, lifecycleService, - notificationService, _storageService, - textResourceConfigurationService, + notificationService ); this._supportedSchemes = JSON.parse(this._storageService.get('remote_schemes', undefined, '[]')); diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts deleted file mode 100644 index c73b17c1d36..00000000000 --- a/src/vs/workbench/services/files/node/fileService.ts +++ /dev/null @@ -1,1288 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as paths from 'path'; -import * as fs from 'fs'; -import * as os from 'os'; -import * as crypto from 'crypto'; -import * as assert from 'assert'; -import { isParent, FileOperation, FileOperationEvent, IContent, IFileService, IResolveFileOptions, IResolveFileResult, IResolveContentOptions, IFileStat, IStreamContent, FileOperationError, FileOperationResult, IUpdateContentOptions, FileChangeType, IImportResult, FileChangesEvent, ICreateFileOptions, IContentData, ITextSnapshot } from 'vs/platform/files/common/files'; -import { MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/platform/files/node/files'; -import { isEqualOrParent } from 'vs/base/common/paths'; -import { ResourceMap } from 'vs/base/common/map'; -import * as arrays from 'vs/base/common/arrays'; -import { TPromise } from 'vs/base/common/winjs.base'; -import * as objects from 'vs/base/common/objects'; -import * as extfs from 'vs/base/node/extfs'; -import { nfcall, ThrottledDelayer } from 'vs/base/common/async'; -import uri from 'vs/base/common/uri'; -import * as nls from 'vs/nls'; -import { isWindows, isLinux } from 'vs/base/common/platform'; -import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import * as pfs from 'vs/base/node/pfs'; -import * as encoding from 'vs/base/node/encoding'; -import * as flow from 'vs/base/node/flow'; -import { FileWatcher as UnixWatcherService } from 'vs/workbench/services/files/node/watcher/unix/watcherService'; -import { FileWatcher as WindowsWatcherService } from 'vs/workbench/services/files/node/watcher/win32/watcherService'; -import { toFileChangesEvent, normalize, IRawFileChange } from 'vs/workbench/services/files/node/watcher/common'; -import { Event, Emitter } from 'vs/base/common/event'; -import { FileWatcher as NsfwWatcherService } from 'vs/workbench/services/files/node/watcher/nsfw/watcherService'; -import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { getBaseLabel } from 'vs/base/common/labels'; -import { Readable } from 'stream'; -import { Schemas } from 'vs/base/common/network'; - -export interface IEncodingOverride { - parent?: uri; - extension?: string; - encoding: string; -} - -export interface IFileServiceOptions { - tmpDir?: string; - errorLogger?: (msg: string) => void; - encodingOverride?: IEncodingOverride[]; - watcherIgnoredPatterns?: string[]; - disableWatcher?: boolean; - verboseLogging?: boolean; - useExperimentalFileWatcher?: boolean; - writeElevated?: (source: string, target: string) => TPromise; - elevationSupport?: { - cliPath: string; - promptTitle: string; - promptIcnsPath?: string; - }; -} - -function etag(stat: fs.Stats): string; -function etag(size: number, mtime: number): string; -function etag(arg1: any, arg2?: any): string { - let size: number; - let mtime: number; - if (typeof arg2 === 'number') { - size = arg1; - mtime = arg2; - } else { - size = (arg1).size; - mtime = (arg1).mtime.getTime(); - } - - return `"${crypto.createHash('sha1').update(String(size) + String(mtime)).digest('hex')}"`; -} - -class BufferPool { - - static _64K = new BufferPool(64 * 1024, 5); - - constructor( - readonly bufferSize: number, - private readonly _capacity: number, - private readonly _free: Buffer[] = [], - ) { - // - } - - acquire(): Buffer { - if (this._free.length === 0) { - return Buffer.allocUnsafe(this.bufferSize); - } else { - return this._free.shift(); - } - } - - release(buf: Buffer): void { - if (this._free.length <= this._capacity) { - this._free.push(buf); - } - } -} - -export class FileService implements IFileService { - - public _serviceBrand: any; - - private static readonly FS_EVENT_DELAY = 50; // aggregate and only emit events when changes have stopped for this duration (in ms) - private static readonly FS_REWATCH_DELAY = 300; // delay to rewatch a file that was renamed or deleted (in ms) - - private tmpPath: string; - private options: IFileServiceOptions; - - private readonly _onFileChanges: Emitter; - private readonly _onAfterOperation: Emitter; - - private toDispose: IDisposable[]; - - private activeFileChangesWatchers: ResourceMap; - private fileChangesWatchDelayer: ThrottledDelayer; - private undeliveredRawFileChangesEvents: IRawFileChange[]; - - private activeWorkspaceFileChangeWatcher: IDisposable; - - constructor( - private contextService: IWorkspaceContextService, - private environmentService: IEnvironmentService, - private textResourceConfigurationService: ITextResourceConfigurationService, - private configurationService: IConfigurationService, - private lifecycleService: ILifecycleService, - options: IFileServiceOptions - ) { - this.toDispose = []; - this.options = options || Object.create(null); - this.tmpPath = this.options.tmpDir || os.tmpdir(); - - this._onFileChanges = new Emitter(); - this.toDispose.push(this._onFileChanges); - - this._onAfterOperation = new Emitter(); - this.toDispose.push(this._onAfterOperation); - - if (!this.options.errorLogger) { - this.options.errorLogger = console.error; - } - - this.activeFileChangesWatchers = new ResourceMap(); - this.fileChangesWatchDelayer = new ThrottledDelayer(FileService.FS_EVENT_DELAY); - this.undeliveredRawFileChangesEvents = []; - - lifecycleService.when(LifecyclePhase.Running).then(() => { - this.setupFileWatching(); // wait until we are fully running before starting file watchers - }); - - this.registerListeners(); - } - - private registerListeners(): void { - this.toDispose.push(this.contextService.onDidChangeWorkbenchState(() => { - if (this.lifecycleService.phase >= LifecyclePhase.Running) { - this.setupFileWatching(); - } - })); - } - - public get onFileChanges(): Event { - return this._onFileChanges.event; - } - - public get onAfterOperation(): Event { - return this._onAfterOperation.event; - } - - public updateOptions(options: IFileServiceOptions): void { - if (options) { - objects.mixin(this.options, options); // overwrite current options - } - } - - private setupFileWatching(): void { - - // dispose old if any - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - } - - // Return if not aplicable - const workbenchState = this.contextService.getWorkbenchState(); - if (workbenchState === WorkbenchState.EMPTY || this.options.disableWatcher) { - return; - } - - // new watcher: use it if setting tells us so or we run in multi-root environment - if (this.options.useExperimentalFileWatcher || workbenchState === WorkbenchState.WORKSPACE) { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupNsfwWorkspaceWatching().startWatching()); - } - - // old watcher - else { - if (isWindows) { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupWin32WorkspaceWatching().startWatching()); - } else { - this.activeWorkspaceFileChangeWatcher = toDisposable(this.setupUnixWorkspaceWatching().startWatching()); - } - } - } - - private setupWin32WorkspaceWatching(): WindowsWatcherService { - return new WindowsWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - private setupUnixWorkspaceWatching(): UnixWatcherService { - return new UnixWatcherService(this.contextService, this.options.watcherIgnoredPatterns, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - private setupNsfwWorkspaceWatching(): NsfwWatcherService { - return new NsfwWatcherService(this.contextService, this.configurationService, e => this._onFileChanges.fire(e), this.options.errorLogger, this.options.verboseLogging); - } - - public resolveFile(resource: uri, options?: IResolveFileOptions): TPromise { - return this.resolve(resource, options); - } - - public resolveFiles(toResolve: { resource: uri, options?: IResolveFileOptions }[]): TPromise { - return TPromise.join(toResolve.map(resourceAndOptions => this.resolve(resourceAndOptions.resource, resourceAndOptions.options) - .then(stat => ({ stat, success: true }), error => ({ stat: void 0, success: false })))); - } - - public existsFile(resource: uri): TPromise { - return this.resolveFile(resource).then(() => true, () => false); - } - - public resolveContent(resource: uri, options?: IResolveContentOptions): TPromise { - return this.resolveStreamContent(resource, options).then(streamContent => { - return new TPromise((resolve, reject) => { - - const result: IContent = { - resource: streamContent.resource, - name: streamContent.name, - mtime: streamContent.mtime, - etag: streamContent.etag, - encoding: streamContent.encoding, - value: '' - }; - - streamContent.value.on('data', chunk => result.value += chunk); - streamContent.value.on('error', err => reject(err)); - streamContent.value.on('end', _ => resolve(result)); - - return result; - }); - }); - } - - public resolveStreamContent(resource: uri, options?: IResolveContentOptions): TPromise { - - // Guard early against attempts to resolve an invalid file path - if (resource.scheme !== Schemas.file || !resource.fsPath) { - return TPromise.wrapError(new FileOperationError( - nls.localize('fileInvalidPath', "Invalid file resource ({0})", resource.toString(true)), - FileOperationResult.FILE_INVALID_PATH, - options - )); - } - - const result: IStreamContent = { - resource: void 0, - name: void 0, - mtime: void 0, - etag: void 0, - encoding: void 0, - value: void 0 - }; - - const contentResolverTokenSource = new CancellationTokenSource(); - - const onStatError = (error: Error) => { - - // error: stop reading the file the stat and content resolve call - // usually race, mostly likely the stat call will win and cancel - // the content call - contentResolverTokenSource.cancel(); - - // forward error - return TPromise.wrapError(error); - }; - - const statsPromise = this.resolveFile(resource).then(stat => { - result.resource = stat.resource; - result.name = stat.name; - result.mtime = stat.mtime; - result.etag = stat.etag; - - // Return early if resource is a directory - if (stat.isDirectory) { - return onStatError(new FileOperationError( - nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY, - options - )); - } - - // Return early if file not modified since - if (options && options.etag && options.etag === stat.etag) { - return onStatError(new FileOperationError( - nls.localize('fileNotModifiedError', "File not modified since"), - FileOperationResult.FILE_NOT_MODIFIED_SINCE, - options - )); - } - - // Return early if file is too large to load - if (typeof stat.size === 'number') { - if (stat.size > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { - return onStatError(new FileOperationError( - nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), - FileOperationResult.FILE_EXCEED_MEMORY_LIMIT - )); - } - - if (stat.size > MAX_FILE_SIZE) { - return onStatError(new FileOperationError( - nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE - )); - } - } - - return void 0; - }, err => { - - // Wrap file not found errors - if (err.code === 'ENOENT') { - return onStatError(new FileOperationError( - nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND, - options - )); - } - - return onStatError(err); - }); - - let completePromise: Thenable; - - // await the stat iff we already have an etag so that we compare the - // etag from the stat before we actually read the file again. - if (options && options.etag) { - completePromise = statsPromise.then(() => { - return this.fillInContents(result, resource, options, contentResolverTokenSource.token); // Waterfall -> only now resolve the contents - }); - } - - // a fresh load without a previous etag which means we can resolve the file stat - // and the content at the same time, avoiding the waterfall. - else { - completePromise = Promise.all([statsPromise, this.fillInContents(result, resource, options, contentResolverTokenSource.token)]); - } - - return TPromise.wrap(completePromise).then(() => { - contentResolverTokenSource.dispose(); - - return result; - }); - } - - private fillInContents(content: IStreamContent, resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { - return this.resolveFileData(resource, options, token).then(data => { - content.encoding = data.encoding; - content.value = data.stream; - }); - } - - private resolveFileData(resource: uri, options: IResolveContentOptions, token: CancellationToken): Thenable { - - const chunkBuffer = BufferPool._64K.acquire(); - - const result: IContentData = { - encoding: void 0, - stream: void 0 - }; - - return new Promise((resolve, reject) => { - fs.open(this.toAbsolutePath(resource), 'r', (err, fd) => { - if (err) { - if (err.code === 'ENOENT') { - // Wrap file not found errors - err = new FileOperationError( - nls.localize('fileNotFoundError', "File not found ({0})", resource.toString(true)), - FileOperationResult.FILE_NOT_FOUND, - options - ); - } - - return reject(err); - } - - let decoder: NodeJS.ReadWriteStream; - let totalBytesRead = 0; - - const finish = (err?: any) => { - - if (err) { - if (err.code === 'EISDIR') { - // Wrap EISDIR errors (fs.open on a directory works, but you cannot read from it) - err = new FileOperationError( - nls.localize('fileIsDirectoryError', "File is directory"), - FileOperationResult.FILE_IS_DIRECTORY, - options - ); - } - if (decoder) { - // If the decoder already started, we have to emit the error through it as - // event because the promise is already resolved! - decoder.emit('error', err); - } else { - reject(err); - } - } - if (decoder) { - decoder.end(); - } - - // return the shared buffer - BufferPool._64K.release(chunkBuffer); - - if (fd) { - fs.close(fd, err => { - if (err) { - this.options.errorLogger(`resolveFileData#close(): ${err.toString()}`); - } - }); - } - }; - - const handleChunk = (bytesRead: number) => { - if (token.isCancellationRequested) { - // cancellation -> finish - finish(new Error('cancelled')); - } else if (bytesRead === 0) { - // no more data -> finish - finish(); - } else if (bytesRead < chunkBuffer.length) { - // write the sub-part of data we received -> repeat - decoder.write(chunkBuffer.slice(0, bytesRead), readChunk); - } else { - // write all data we received -> repeat - decoder.write(chunkBuffer, readChunk); - } - }; - - let currentPosition: number = (options && options.position) || null; - - const readChunk = () => { - fs.read(fd, chunkBuffer, 0, chunkBuffer.length, currentPosition, (err, bytesRead) => { - totalBytesRead += bytesRead; - - if (typeof currentPosition === 'number') { - // if we received a position argument as option we need to ensure that - // we advance the position by the number of bytesread - currentPosition += bytesRead; - } - - if (totalBytesRead > Math.max(this.environmentService.args['max-memory'] * 1024 * 1024 || 0, MAX_HEAP_SIZE)) { - finish(new FileOperationError( - nls.localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart VS Code and allow it to use more memory"), - FileOperationResult.FILE_EXCEED_MEMORY_LIMIT - )); - } - - if (totalBytesRead > MAX_FILE_SIZE) { - // stop when reading too much - finish(new FileOperationError( - nls.localize('fileTooLargeError', "File too large to open"), - FileOperationResult.FILE_TOO_LARGE, - options - )); - } else if (err) { - // some error happened - finish(err); - - } else if (decoder) { - // pass on to decoder - handleChunk(bytesRead); - - } else { - // when receiving the first chunk of data we need to create the - // decoding stream which is then used to drive the string stream. - TPromise.as(encoding.detectEncodingFromBuffer( - { buffer: chunkBuffer, bytesRead }, - options && options.autoGuessEncoding || this.configuredAutoGuessEncoding(resource) - )).then(detected => { - - if (options && options.acceptTextOnly && detected.seemsBinary) { - // Return error early if client only accepts text and this is not text - finish(new FileOperationError( - nls.localize('fileBinaryError', "File seems to be binary and cannot be opened as text"), - FileOperationResult.FILE_IS_BINARY, - options - )); - - } else { - result.encoding = this.getEncoding(resource, this.getPeferredEncoding(resource, options, detected)); - result.stream = decoder = encoding.decodeStream(result.encoding); - resolve(result); - handleChunk(bytesRead); - } - - }).then(void 0, err => { - // failed to get encoding - finish(err); - }); - } - }); - }; - - // start reading - readChunk(); - }); - }); - } - - public updateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - if (this.options.elevationSupport && options.writeElevated) { - return this.doUpdateContentElevated(resource, value, options); - } - - return this.doUpdateContent(resource, value, options); - } - - private doUpdateContent(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file for writing - return this.checkFileBeforeWriting(absolutePath, options).then(exists => { - let createParentsPromise: TPromise; - if (exists) { - createParentsPromise = TPromise.as(null); - } else { - createParentsPromise = pfs.mkdirp(paths.dirname(absolutePath)); - } - - // 2.) create parents as needed - return createParentsPromise.then(() => { - const encodingToWrite = this.getEncoding(resource, options.encoding); - let addBomPromise: TPromise = TPromise.as(false); - - // UTF_16 BE and LE as well as UTF_8 with BOM always have a BOM - if (encodingToWrite === encoding.UTF16be || encodingToWrite === encoding.UTF16le || encodingToWrite === encoding.UTF8_with_bom) { - addBomPromise = TPromise.as(true); - } - - // Existing UTF-8 file: check for options regarding BOM - else if (exists && encodingToWrite === encoding.UTF8) { - if (options.overwriteEncoding) { - addBomPromise = TPromise.as(false); // if we are to overwrite the encoding, we do not preserve it if found - } else { - addBomPromise = encoding.detectEncodingByBOM(absolutePath).then(enc => enc === encoding.UTF8); // otherwise preserve it if found - } - } - - // 3.) check to add UTF BOM - return addBomPromise.then(addBom => { - - // 4.) set contents and resolve - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite).then(void 0, error => { - if (!exists || error.code !== 'EPERM' || !isWindows) { - return TPromise.wrapError(error); - } - - // On Windows and if the file exists with an EPERM error, we try 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) - - // 5.) truncate - return pfs.truncate(absolutePath, 0).then(() => { - - // 6.) set contents (this time with r+ mode) and resolve again - return this.doSetContentsAndResolve(resource, absolutePath, value, addBom, encodingToWrite, { flag: 'r+' }); - }); - }); - }); - }); - }).then(null, error => { - if (error.code === 'EACCES' || error.code === 'EPERM') { - return TPromise.wrapError(new FileOperationError( - nls.localize('filePermission', "Permission denied writing to file ({0})", resource.toString(true)), - FileOperationResult.FILE_PERMISSION_DENIED, - options - )); - } - - return TPromise.wrapError(error); - }); - } - - private doSetContentsAndResolve(resource: uri, absolutePath: string, value: string | ITextSnapshot, addBOM: boolean, encodingToWrite: string, options?: { mode?: number; flag?: string; }): TPromise { - let writeFilePromise: TPromise; - - // Configure encoding related options as needed - const writeFileOptions: extfs.IWriteFileOptions = options ? options : Object.create(null); - if (addBOM || encodingToWrite !== encoding.UTF8) { - writeFileOptions.encoding = { - charset: encodingToWrite, - addBOM - }; - } - - if (typeof value === 'string') { - writeFilePromise = pfs.writeFile(absolutePath, value, writeFileOptions); - } else { - writeFilePromise = pfs.writeFile(absolutePath, this.snapshotToReadableStream(value), writeFileOptions); - } - - // set contents - return writeFilePromise.then(() => { - - // resolve - return this.resolve(resource); - }); - } - - private snapshotToReadableStream(snapshot: ITextSnapshot): NodeJS.ReadableStream { - return new Readable({ - read: function () { - try { - let chunk: string; - let canPush = true; - - // Push all chunks as long as we can push and as long as - // the underlying snapshot returns strings to us - while (canPush && typeof (chunk = snapshot.read()) === 'string') { - canPush = this.push(chunk); - } - - // Signal EOS by pushing NULL - if (typeof chunk !== 'string') { - this.push(null); - } - } catch (error) { - this.emit('error', error); - } - }, - encoding: encoding.UTF8 // very important, so that strings are passed around and not buffers! - }); - } - - private doUpdateContentElevated(resource: uri, value: string | ITextSnapshot, options: IUpdateContentOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - // 1.) check file 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: IUpdateContentOptions = objects.assign(Object.create(null), options); - writeOptions.writeElevated = false; - writeOptions.encoding = this.getEncoding(resource, options.encoding); - - // 2.) write to a temporary file to be able to copy over later - const tmpPath = paths.join(this.tmpPath, `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 TPromise((c, e) => { - const promptOptions = { name: this.options.elevationSupport.promptTitle.replace('-', ''), icns: this.options.elevationSupport.promptIcnsPath }; - - const sudoCommand: string[] = [`"${this.options.elevationSupport.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) { - e(error || stderr); - } else { - c(void 0); - } - }); - }); - }).then(() => { - - // 3.) delete temp file - return pfs.del(tmpPath, this.tmpPath).then(() => { - - // 4.) resolve again - return this.resolve(resource); - }); - }); - }); - }).then(null, error => { - if (this.options.verboseLogging) { - this.options.errorLogger(`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 TPromise.wrapError(error); - }); - } - - public createFile(resource: uri, content: string = '', options: ICreateFileOptions = Object.create(null)): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - let checkFilePromise: TPromise; - if (options.overwrite) { - checkFilePromise = TPromise.as(false); - } else { - checkFilePromise = pfs.exists(absolutePath); - } - - // Check file exists - return checkFilePromise.then(exists => { - if (exists && !options.overwrite) { - return TPromise.wrapError(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; - }); - }); - } - - public createFolder(resource: uri): TPromise { - - // 1.) Create folder - const absolutePath = this.toAbsolutePath(resource); - return pfs.mkdirp(absolutePath).then(() => { - - // 2.) Resolve - return this.resolve(resource).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.CREATE, result)); - - return result; - }); - }); - } - - private checkFileBeforeWriting(absolutePath: string, options: IUpdateContentOptions = Object.create(null), ignoreReadonly?: boolean): TPromise { - return pfs.exists(absolutePath).then(exists => { - if (exists) { - return pfs.stat(absolutePath).then(stat => { - if (stat.isDirectory()) { - return TPromise.wrapError(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 TPromise.wrapError(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(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(options); - } - - return exists; - }); - }); - } - - return TPromise.as(exists); - }); - } - - return TPromise.as(exists); - }); - } - - private readOnlyError(options: IUpdateContentOptions): TPromise { - return TPromise.wrapError(new FileOperationError( - nls.localize('fileReadOnlyError', "File is Read Only"), - FileOperationResult.FILE_READ_ONLY, - options - )); - } - - public rename(resource: uri, newName: string): TPromise { - const newPath = paths.join(paths.dirname(resource.fsPath), newName); - - return this.moveFile(resource, uri.file(newPath)); - } - - public moveFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.moveOrCopyFile(source, target, false, overwrite); - } - - public copyFile(source: uri, target: uri, overwrite?: boolean): TPromise { - return this.moveOrCopyFile(source, target, true, overwrite); - } - - private moveOrCopyFile(source: uri, target: uri, keepCopy: boolean, overwrite: boolean): TPromise { - const sourcePath = this.toAbsolutePath(source); - const targetPath = this.toAbsolutePath(target); - - // 1.) move / copy - return this.doMoveOrCopyFile(sourcePath, targetPath, keepCopy, overwrite).then(() => { - - // 2.) resolve - return this.resolve(target).then(result => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(source, keepCopy ? FileOperation.COPY : FileOperation.MOVE, result)); - - return result; - }); - }); - } - - private doMoveOrCopyFile(sourcePath: string, targetPath: string, keepCopy: boolean, overwrite: boolean): TPromise { - - // 1.) validate operation - if (isParent(targetPath, sourcePath, !isLinux)) { - return TPromise.wrapError(new Error('Unable to move/copy when source path is parent of target path')); - } - - // 2.) check if target exists - return pfs.exists(targetPath).then(exists => { - const isCaseRename = sourcePath.toLowerCase() === targetPath.toLowerCase(); - const isSameFile = sourcePath === targetPath; - - // Return early with conflict if target exists and we are not told to overwrite - if (exists && !isCaseRename && !overwrite) { - return TPromise.wrapError(new FileOperationError(nls.localize('fileMoveConflict', "Unable to move/copy. File already exists at destination."), FileOperationResult.FILE_MOVE_CONFLICT)); - } - - // 3.) make sure target is deleted before we move/copy unless this is a case rename of the same file - let deleteTargetPromise = TPromise.wrap(void 0); - if (exists && !isCaseRename) { - if (isEqualOrParent(sourcePath, targetPath, !isLinux /* ignorecase */)) { - return TPromise.wrapError(new Error(nls.localize('unableToMoveCopyError', "Unable to move/copy. File would replace folder it is contained in."))); // catch this corner case! - } - - deleteTargetPromise = this.del(uri.file(targetPath)); - } - - return deleteTargetPromise.then(() => { - - // 4.) make sure parents exists - return pfs.mkdirp(paths.dirname(targetPath)).then(() => { - - // 4.) copy/move - if (isSameFile) { - return TPromise.wrap(null); - } else if (keepCopy) { - return nfcall(extfs.copy, sourcePath, targetPath); - } else { - return nfcall(extfs.mv, sourcePath, targetPath); - } - }).then(() => exists); - }); - }); - } - - public importFile(source: uri, targetFolder: uri): TPromise { - const sourcePath = this.toAbsolutePath(source); - const targetResource = uri.file(paths.join(targetFolder.fsPath, paths.basename(source.fsPath))); - const targetPath = this.toAbsolutePath(targetResource); - - // 1.) resolve - return pfs.stat(sourcePath).then(stat => { - if (stat.isDirectory()) { - return TPromise.wrapError(new Error(nls.localize('foldersCopyError', "Folders cannot be copied into the workspace. Please select individual files to copy them."))); // for now we do not allow to import a folder into a workspace - } - - // 2.) copy - return this.doMoveOrCopyFile(sourcePath, targetPath, true, true).then(exists => { - - // 3.) resolve - return this.resolve(targetResource).then(stat => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(source, FileOperation.IMPORT, stat)); - - return { isNew: !exists, stat: stat }; - }); - }); - }); - } - - public del(resource: uri): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - return pfs.del(absolutePath, this.tmpPath).then(() => { - - // Events - this._onAfterOperation.fire(new FileOperationEvent(resource, FileOperation.DELETE)); - }); - } - - // Helpers - - private toAbsolutePath(arg1: uri | IFileStat): string { - let resource: uri; - if (arg1 instanceof uri) { - resource = arg1; - } else { - resource = (arg1).resource; - } - - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); - - return paths.normalize(resource.fsPath); - } - - private resolve(resource: uri, options: IResolveFileOptions = Object.create(null)): TPromise { - return this.toStatResolver(resource) - .then(model => model.resolve(options)); - } - - private toStatResolver(resource: uri): TPromise { - const absolutePath = this.toAbsolutePath(resource); - - return pfs.statLink(absolutePath).then(({ isSymbolicLink, stat }) => { - return new StatResolver(resource, isSymbolicLink, stat.isDirectory(), stat.mtime.getTime(), stat.size, this.options.verboseLogging ? this.options.errorLogger : void 0); - }); - } - - private getPeferredEncoding(resource: uri, options: IResolveContentOptions, detected: encoding.IDetectedEncodingResult): string { - let preferredEncoding: string; - if (options && options.encoding) { - if (detected.encoding === encoding.UTF8 && options.encoding === encoding.UTF8) { - preferredEncoding = encoding.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 - } - } else if (detected.encoding) { - if (detected.encoding === encoding.UTF8) { - preferredEncoding = encoding.UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM - } else { - preferredEncoding = detected.encoding; - } - } else if (this.configuredEncoding(resource) === encoding.UTF8_with_bom) { - preferredEncoding = encoding.UTF8; // if we did not detect UTF 8 BOM before, this can only be UTF 8 then - } - return preferredEncoding; - } - - public getEncoding(resource: uri, preferredEncoding?: string): string { - let fileEncoding: string; - - const override = this.getEncodingOverride(resource); - if (override) { - fileEncoding = override; - } else if (preferredEncoding) { - fileEncoding = preferredEncoding; - } else { - fileEncoding = this.configuredEncoding(resource); - } - - if (!fileEncoding || !encoding.encodingExists(fileEncoding)) { - fileEncoding = encoding.UTF8; // the default is UTF 8 - } - - return fileEncoding; - } - - private configuredAutoGuessEncoding(resource: uri): boolean { - return this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'); - } - - private configuredEncoding(resource: uri): string { - return this.textResourceConfigurationService.getValue(resource, 'files.encoding'); - } - - private getEncodingOverride(resource: uri): string { - if (resource && this.options.encodingOverride && this.options.encodingOverride.length) { - for (let i = 0; i < this.options.encodingOverride.length; i++) { - const override = this.options.encodingOverride[i]; - - // check if the resource is child of encoding override path - if (override.parent && isParent(resource.fsPath, override.parent.fsPath, !isLinux /* ignorecase */)) { - return override.encoding; - } - - // check if the resource extension is equal to encoding override - if (override.extension && paths.extname(resource.fsPath) === `.${override.extension}`) { - return override.encoding; - } - } - } - - return null; - } - - public watchFileChanges(resource: uri): void { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource for watching: ${resource}`); - - // Create or get watcher for provided path - let watcher = this.activeFileChangesWatchers.get(resource); - if (!watcher) { - const fsPath = resource.fsPath; - const fsName = paths.basename(resource.fsPath); - - watcher = extfs.watch(fsPath, (eventType: string, filename: string) => { - const renamedOrDeleted = ((filename && filename !== fsName) || eventType === 'rename'); - - // The file was either deleted or renamed. Many tools apply changes to files in an - // atomic way ("Atomic Save") by first renaming the file to a temporary name and then - // renaming it back to the original name. Our watcher will detect this as a rename - // and then stops to work on Mac and Linux because the watcher is applied to the - // inode and not the name. The fix is to detect this case and trying to watch the file - // again after a certain delay. - // In addition, we send out a delete event if after a timeout we detect that the file - // does indeed not exist anymore. - if (renamedOrDeleted) { - - // Very important to dispose the watcher which now points to a stale inode - this.unwatchFileChanges(resource); - - // Wait a bit and try to install watcher again, assuming that the file was renamed quickly ("Atomic Save") - setTimeout(() => { - this.existsFile(resource).done(exists => { - - // File still exists, so reapply the watcher - if (exists) { - this.watchFileChanges(resource); - } - - // File seems to be really gone, so emit a deleted event - else { - this.onRawFileChange({ - type: FileChangeType.DELETED, - path: fsPath - }); - } - }); - }, FileService.FS_REWATCH_DELAY); - } - - // Handle raw file change - this.onRawFileChange({ - type: FileChangeType.UPDATED, - path: fsPath - }); - }, (error: string) => this.options.errorLogger(error)); - - if (watcher) { - this.activeFileChangesWatchers.set(resource, watcher); - } - } - } - - private onRawFileChange(event: IRawFileChange): void { - - // add to bucket of undelivered events - this.undeliveredRawFileChangesEvents.push(event); - - if (this.options.verboseLogging) { - console.log('%c[node.js Watcher]%c', 'color: green', 'color: black', event.type === FileChangeType.ADDED ? '[ADDED]' : event.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', event.path); - } - - // handle emit through delayer to accommodate for bulk changes - this.fileChangesWatchDelayer.trigger(() => { - const buffer = this.undeliveredRawFileChangesEvents; - this.undeliveredRawFileChangesEvents = []; - - // Normalize - const normalizedEvents = normalize(buffer); - - // Logging - if (this.options.verboseLogging) { - normalizedEvents.forEach(r => { - console.log('%c[node.js Watcher]%c >> normalized', 'color: green', 'color: black', r.type === FileChangeType.ADDED ? '[ADDED]' : r.type === FileChangeType.DELETED ? '[DELETED]' : '[CHANGED]', r.path); - }); - } - - // Emit - this._onFileChanges.fire(toFileChangesEvent(normalizedEvents)); - - return TPromise.as(null); - }); - } - - public unwatchFileChanges(resource: uri): void { - const watcher = this.activeFileChangesWatchers.get(resource); - if (watcher) { - watcher.close(); - this.activeFileChangesWatchers.delete(resource); - } - } - - public dispose(): void { - this.toDispose = dispose(this.toDispose); - - if (this.activeWorkspaceFileChangeWatcher) { - this.activeWorkspaceFileChangeWatcher.dispose(); - this.activeWorkspaceFileChangeWatcher = null; - } - - this.activeFileChangesWatchers.forEach(watcher => watcher.close()); - this.activeFileChangesWatchers.clear(); - } -} - -export class StatResolver { - private name: string; - private etag: string; - - constructor( - private resource: uri, - private isSymbolicLink: boolean, - private isDirectory: boolean, - private mtime: number, - private size: number, - private errorLogger?: (error: Error | string) => void - ) { - assert.ok(resource && resource.scheme === Schemas.file, `Invalid resource: ${resource}`); - - this.name = getBaseLabel(resource); - this.etag = etag(size, mtime); - } - - public resolve(options: IResolveFileOptions): TPromise { - - // General Data - const fileStat: IFileStat = { - resource: this.resource, - isDirectory: this.isDirectory, - isSymbolicLink: this.isSymbolicLink, - name: this.name, - etag: this.etag, - size: this.size, - mtime: this.mtime - }; - - // File Specific Data - if (!this.isDirectory) { - return TPromise.as(fileStat); - } - - // Directory Specific Data - else { - - // Convert the paths from options.resolveTo to absolute paths - let absoluteTargetPaths: string[] = null; - if (options && options.resolveTo) { - absoluteTargetPaths = []; - options.resolveTo.forEach(resource => { - absoluteTargetPaths.push(resource.fsPath); - }); - } - - return new TPromise((c, e) => { - - // Load children - this.resolveChildren(this.resource.fsPath, absoluteTargetPaths, options && options.resolveSingleChildDescendants, children => { - children = arrays.coalesce(children); // we don't want those null children (could be permission denied when reading a child) - fileStat.children = children || []; - - c(fileStat); - }); - }); - } - } - - private resolveChildren(absolutePath: string, absoluteTargetPaths: string[], resolveSingleChildDescendants: boolean, callback: (children: IFileStat[]) => void): void { - extfs.readdir(absolutePath, (error: Error, files: string[]) => { - if (error) { - if (this.errorLogger) { - this.errorLogger(error); - } - - return callback(null); // return - we might not have permissions to read the folder - } - - // for each file in the folder - flow.parallel(files, (file: string, clb: (error: Error, children: IFileStat) => void) => { - const fileResource = uri.file(paths.resolve(absolutePath, file)); - let fileStat: fs.Stats; - let isSymbolicLink = false; - const $this = this; - - flow.sequence( - function onError(error: Error): void { - if ($this.errorLogger) { - $this.errorLogger(error); - } - - clb(null, null); // return - we might not have permissions to read the folder or stat the file - }, - - function stat(this: any): void { - extfs.statLink(fileResource.fsPath, this); - }, - - function countChildren(this: any, statAndLink: extfs.IStatAndLink): void { - fileStat = statAndLink.stat; - isSymbolicLink = statAndLink.isSymbolicLink; - - if (fileStat.isDirectory()) { - extfs.readdir(fileResource.fsPath, (error, result) => { - this(null, result ? result.length : 0); - }); - } else { - this(null, 0); - } - }, - - function resolve(childCount: number): void { - const childStat: IFileStat = { - resource: fileResource, - isDirectory: fileStat.isDirectory(), - isSymbolicLink, - name: file, - mtime: fileStat.mtime.getTime(), - etag: etag(fileStat), - size: fileStat.size - }; - - // Return early for files - if (!fileStat.isDirectory()) { - return clb(null, childStat); - } - - // Handle Folder - let resolveFolderChildren = false; - if (files.length === 1 && resolveSingleChildDescendants) { - resolveFolderChildren = true; - } else if (childCount > 0 && absoluteTargetPaths && absoluteTargetPaths.some(targetPath => isEqualOrParent(targetPath, fileResource.fsPath, !isLinux /* ignorecase */))) { - resolveFolderChildren = true; - } - - // Continue resolving children based on condition - if (resolveFolderChildren) { - $this.resolveChildren(fileResource.fsPath, absoluteTargetPaths, resolveSingleChildDescendants, children => { - children = arrays.coalesce(children); // we don't want those null children - childStat.children = children || []; - - clb(null, childStat); - }); - } - - // Otherwise return result - else { - clb(null, childStat); - } - }); - }, (errors, result) => { - callback(result); - }); - }); - } -} diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts similarity index 95% rename from src/vs/workbench/services/files/test/node/fileService.test.ts rename to src/vs/workbench/services/files/test/electron-browser/fileService.test.ts index fb5aceb59bd..959d774f9d9 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/fileService.test.ts @@ -11,14 +11,14 @@ import * as os from 'os'; import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; -import { FileService, IEncodingOverride } from 'vs/workbench/services/files/node/fileService'; +import { FileService, IEncodingOverride } from 'vs/workbench/services/files/electron-browser/fileService'; import { FileOperation, FileOperationEvent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import uri from 'vs/base/common/uri'; import * as uuid from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; import * as encodingLib from 'vs/base/node/encoding'; -import * as utils from 'vs/workbench/services/files/test/node/utils'; -import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService } from 'vs/workbench/test/workbenchTestServices'; +import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; +import { TestEnvironmentService, TestContextService, TestTextResourceConfigurationService, getRandomTestPath, TestLifecycleService, TestNotificationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TextModel } from 'vs/editor/common/model/textModel'; @@ -34,7 +34,7 @@ suite('FileService', () => { const sourceDir = require.toUrl('./fixtures/service'); return pfs.copy(sourceDir, testDir).then(() => { - service = new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { disableWatcher: true }); + service = new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), new TestStorageService(), new TestNotificationService(), { disableWatcher: true }); }); }); @@ -892,10 +892,18 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); - const _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, textResourceConfigurationService, configurationService, new TestLifecycleService(), { - encodingOverride, - disableWatcher: true - }); + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + textResourceConfigurationService, + configurationService, + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + encodingOverride, + disableWatcher: true + }); return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { assert.equal(c.encoding, 'windows1252'); @@ -929,10 +937,18 @@ suite('FileService', () => { const textResourceConfigurationService = new TestTextResourceConfigurationService(configurationService); - const _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, textResourceConfigurationService, configurationService, new TestLifecycleService(), { - encodingOverride, - disableWatcher: true - }); + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + textResourceConfigurationService, + configurationService, + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + encodingOverride, + disableWatcher: true + }); return _service.resolveContent(uri.file(path.join(testDir, 'index.html'))).then(c => { assert.equal(c.encoding, 'windows1252'); @@ -955,9 +971,17 @@ suite('FileService', () => { const _sourceDir = require.toUrl('./fixtures/service'); const resource = uri.file(path.join(testDir, 'index.html')); - const _service = new FileService(new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), new TestLifecycleService(), { - disableWatcher: true - }); + const _service = new FileService( + new TestContextService(new Workspace(_testDir, _testDir, toWorkspaceFolders([{ path: _testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + new TestConfigurationService(), + new TestLifecycleService(), + new TestStorageService(), + new TestNotificationService(), + { + disableWatcher: true + }); return pfs.copy(_sourceDir, _testDir).then(() => { return pfs.readFile(resource.fsPath).then(data => { diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/examples/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/examples/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/examples/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/index.html b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/index.html similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/index.html rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/index.html diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/other/deep/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/other/deep/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/resolver/site.css b/src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/site.css similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/resolver/site.css rename to src/vs/workbench/services/files/test/electron-browser/fixtures/resolver/site.css diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/binary.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/binary.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/binary.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/binary.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/company.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/company.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/company.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/company.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/conway.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/conway.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/conway.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/conway.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/employee.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/employee.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/employee.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/employee.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/deep/small.js b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/small.js similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/deep/small.js rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/deep/small.js diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/index.html b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/index.html similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/index.html rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/index.html diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/lorem.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/lorem.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/lorem.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/lorem.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/small.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/small.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/small.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/small.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/small_umlaut.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/small_umlaut.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/small_umlaut.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/small_umlaut.txt diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/some_utf16le.css b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf16le.css similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/some_utf16le.css rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf16le.css diff --git a/src/vs/workbench/services/files/test/node/fixtures/service/some_utf8_bom.txt b/src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf8_bom.txt similarity index 100% rename from src/vs/workbench/services/files/test/node/fixtures/service/some_utf8_bom.txt rename to src/vs/workbench/services/files/test/electron-browser/fixtures/service/some_utf8_bom.txt diff --git a/src/vs/workbench/services/files/test/node/resolver.test.ts b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts similarity index 97% rename from src/vs/workbench/services/files/test/node/resolver.test.ts rename to src/vs/workbench/services/files/test/electron-browser/resolver.test.ts index 230d31d4e0f..3a9c19ad869 100644 --- a/src/vs/workbench/services/files/test/node/resolver.test.ts +++ b/src/vs/workbench/services/files/test/electron-browser/resolver.test.ts @@ -9,10 +9,10 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assert from 'assert'; -import { StatResolver } from 'vs/workbench/services/files/node/fileService'; +import { StatResolver } from 'vs/workbench/services/files/electron-browser/fileService'; import uri from 'vs/base/common/uri'; import { isLinux } from 'vs/base/common/platform'; -import * as utils from 'vs/workbench/services/files/test/node/utils'; +import * as utils from 'vs/workbench/services/files/test/electron-browser/utils'; function create(relativePath: string): StatResolver { let basePath = require.toUrl('./fixtures/resolver'); diff --git a/src/vs/workbench/services/files/test/node/utils.ts b/src/vs/workbench/services/files/test/electron-browser/utils.ts similarity index 100% rename from src/vs/workbench/services/files/test/node/utils.ts rename to src/vs/workbench/services/files/test/electron-browser/utils.ts diff --git a/src/vs/workbench/services/files/test/node/watcher.test.ts b/src/vs/workbench/services/files/test/electron-browser/watcher.test.ts similarity index 100% rename from src/vs/workbench/services/files/test/node/watcher.test.ts rename to src/vs/workbench/services/files/test/electron-browser/watcher.test.ts diff --git a/src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts similarity index 95% rename from src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts rename to src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 9e7e5d447f1..5d78d2c0752 100644 --- a/src/vs/workbench/services/keybinding/test/node/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -16,11 +16,11 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { KeyCode, SimpleKeybinding, ChordKeybinding } from 'vs/base/common/keyCodes'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import * as extfs from 'vs/base/node/extfs'; -import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestTextFileService, TestEditorGroupService, TestLifecycleService, TestBackupFileService, TestContextService, TestTextResourceConfigurationService, TestHashService, TestEnvironmentService, TestStorageService, TestNotificationService } from 'vs/workbench/test/workbenchTestServices'; import { IWorkspaceContextService, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import * as uuid from 'vs/base/common/uuid'; import { ConfigurationService } from 'vs/platform/configuration/node/configurationService'; -import { FileService } from 'vs/workbench/services/files/node/fileService'; +import { FileService } from 'vs/workbench/services/files/electron-browser/fileService'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; @@ -80,7 +80,16 @@ suite('Keybindings Editing', () => { instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IModeService, ModeServiceImpl); instantiationService.stub(IModelService, instantiationService.createInstance(ModelServiceImpl)); - instantiationService.stub(IFileService, new FileService(new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), TestEnvironmentService, new TestTextResourceConfigurationService(), new TestConfigurationService(), lifecycleService, { disableWatcher: true })); + instantiationService.stub(IFileService, new FileService( + new TestContextService(new Workspace(testDir, testDir, toWorkspaceFolders([{ path: testDir }]))), + TestEnvironmentService, + new TestTextResourceConfigurationService(), + new TestConfigurationService(), + lifecycleService, + new TestStorageService(), + new TestNotificationService(), + { disableWatcher: true }) + ); instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService)); instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService)); instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 0ca51d6b653..1fea905ffe0 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -815,9 +815,6 @@ export class TestFileService implements IFileService { unwatchFileChanges(resource: URI): void { } - updateOptions(options: any): void { - } - getEncoding(resource: URI): string { return 'utf8'; } -- GitLab