From 694a599fe00ec580784fa8ef60e40e3d6dc4abc1 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 19 Oct 2017 22:23:19 +0200 Subject: [PATCH] Fix #33323 --- .../standalone/browser/simpleServices.ts | 8 + src/vs/platform/workspace/common/workspace.ts | 41 +- .../electron-browser/mainThreadFileSystem.ts | 6 +- .../browser/actions/workspaceActions.ts | 8 +- src/vs/workbench/electron-browser/window.ts | 2 +- .../files/browser/views/explorerViewer.ts | 2 +- .../configuration/node/configuration.ts | 286 ++++++++++++++ .../node/configurationService.ts | 372 +++++------------- .../workspace/common/workspaceEditing.ts | 11 - .../workspace/node/workspaceEditingService.ts | 95 +---- .../workbench/test/workbenchTestServices.ts | 8 + 11 files changed, 445 insertions(+), 394 deletions(-) create mode 100644 src/vs/workbench/services/configuration/node/configuration.ts diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 88a323b05be..5bfb927109a 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -598,4 +598,12 @@ export class SimpleWorkspaceContextService implements IWorkspaceContextService { public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { return true; } + + public addFolders(foldersToAdd: URI[]): TPromise { + return TPromise.as(void 0); + } + + public removeFolders(foldersToRemove: URI[]): TPromise { + return TPromise.as(void 0); + } } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 04e273ff14c..78875f25837 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -13,6 +13,7 @@ import Event from 'vs/base/common/event'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier, IStoredWorkspaceFolder, isRawFileWorkspaceFolder, isRawUriWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { coalesce, distinct } from 'vs/base/common/arrays'; import { isLinux } from 'vs/base/common/platform'; +import { TPromise } from 'vs/base/common/winjs.base'; export const IWorkspaceContextService = createDecorator('contextService'); @@ -31,21 +32,6 @@ export interface IWorkspaceFoldersChangeEvent { export interface IWorkspaceContextService { _serviceBrand: any; - /** - * Provides access to the workspace object the platform is running with. This may be null if the workbench was opened - * without workspace (empty); - */ - getWorkspace(): IWorkspace; - - /** - * Return the state of the workbench. - * - * WorkbenchState.EMPTY - if the workbench was opened with empty window or file - * WorkbenchState.FOLDER - if the workbench was opened with a folder - * WorkbenchState.WORKSPACE - if the workbench was opened with a workspace - */ - getWorkbenchState(): WorkbenchState; - /** * An event which fires on workbench state changes. */ @@ -61,6 +47,31 @@ export interface IWorkspaceContextService { */ onDidChangeWorkspaceFolders: Event; + /** + * Provides access to the workspace object the platform is running with. This may be null if the workbench was opened + * without workspace (empty); + */ + getWorkspace(): IWorkspace; + + /** + * add folders to the existing workspace + */ + addFolders(folders: URI[]): TPromise; + + /** + * remove folders from the existing workspace + */ + removeFolders(folders: URI[]): TPromise; + + /** + * Return the state of the workbench. + * + * WorkbenchState.EMPTY - if the workbench was opened with empty window or file + * WorkbenchState.FOLDER - if the workbench was opened with a folder + * WorkbenchState.WORKSPACE - if the workbench was opened with a workspace + */ + getWorkbenchState(): WorkbenchState; + /** * Returns the folder for the given resource from the workspace. * Can be null if there is no workspace or the resource is not inside the workspace. diff --git a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts index 5b6ea8b6f3a..a786896b834 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadFileSystem.ts @@ -12,8 +12,8 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import Event, { Emitter } from 'vs/base/common/event'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { IProgress } from 'vs/platform/progress/common/progress'; -import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ISearchResultProvider, ISearchQuery, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchService } from 'vs/platform/search/common/search'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; @extHostNamedCustomer(MainContext.MainThreadFileSystem) export class MainThreadFileSystem implements MainThreadFileSystemShape { @@ -26,7 +26,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { extHostContext: IExtHostContext, @IFileService private readonly _fileService: IFileService, @ISearchService private readonly _searchService: ISearchService, - @IWorkspaceEditingService private readonly _workspaceEditService: IWorkspaceEditingService + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService ) { this._proxy = extHostContext.get(ExtHostContext.ExtHostFileSystem); } @@ -45,7 +45,7 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape { } $onDidAddFileSystemRoot(uri: URI): void { - this._workspaceEditService.addFolders([uri]); + this._workspaceContextService.addFolders([uri]); } $onFileSystemChange(handle: number, changes: IFileChange[]): void { diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index ea253e70218..50a08486f98 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -119,7 +119,7 @@ export class AddRootFolderAction extends BaseWorkspacesAction { return TPromise.as(null); } - addFoldersPromise = this.workspaceEditingService.addFolders(folders.map(folder => URI.file(folder))); + addFoldersPromise = this.contextService.addFolders(folders.map(folder => URI.file(folder))); } // Empty or Folder @@ -163,7 +163,7 @@ export class GlobalRemoveRootFolderAction extends BaseWorkspacesAction { } // Workspace: remove folder - return this.workspaceEditingService.removeFolders([folder.uri]).then(() => true); + return this.contextService.removeFolders([folder.uri]).then(() => true); } return true; @@ -216,13 +216,13 @@ export class RemoveRootFolderAction extends Action { private rootUri: URI, id: string, label: string, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + @IWorkspaceContextService private contextService: IWorkspaceContextService ) { super(id, label); } public run(): TPromise { - return this.workspaceEditingService.removeFolders([this.rootUri]); + return this.contextService.removeFolders([this.rootUri]); } } diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 5049e7db79e..aeefa814e9f 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -400,7 +400,7 @@ export class ElectronWindow extends Themable { // Workspace: just add to workspace config if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - this.workspaceEditingService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError); + this.contextService.addFolders(foldersToAdd).done(null, errors.onUnexpectedError); } // Single folder or no workspace: create workspace and open diff --git a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts index 21617036d53..8c02ed1ac07 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerViewer.ts @@ -910,7 +910,7 @@ export class FileDragAndDrop extends SimpleFileResourceDragAndDrop { const folders = result.filter(result => result.stat.isDirectory).map(result => result.stat.resource); if (folders.length > 0) { if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - return this.workspaceEditingService.addFolders(folders); + return this.contextService.addFolders(folders); } // If we are in single-folder context, ask for confirmation to create a workspace diff --git a/src/vs/workbench/services/configuration/node/configuration.ts b/src/vs/workbench/services/configuration/node/configuration.ts new file mode 100644 index 00000000000..23a4fa18b23 --- /dev/null +++ b/src/vs/workbench/services/configuration/node/configuration.ts @@ -0,0 +1,286 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import URI from 'vs/base/common/uri'; +import * as paths from 'vs/base/common/paths'; +import { TPromise } from 'vs/base/common/winjs.base'; +import Event, { Emitter } from 'vs/base/common/event'; +import { readFile } from 'vs/base/node/pfs'; +import * as errors from 'vs/base/common/errors'; +import * as collections from 'vs/base/common/collections'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { RunOnceScheduler } from 'vs/base/common/async'; +import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; +import { isLinux } from 'vs/base/common/platform'; +import { ConfigWatcher } from 'vs/base/node/config'; +import { CustomConfigurationModel, ConfigurationModel } from 'vs/platform/configuration/common/configurationModels'; +import { WorkspaceConfigurationModel, ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel } from 'vs/workbench/services/configuration/common/configurationModels'; +import { WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; +import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IStoredWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; +import * as extfs from 'vs/base/node/extfs'; +import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; + +// node.hs helper functions + +interface IStat { + resource: URI; + isDirectory?: boolean; + children?: { resource: URI; }[]; +} + +interface IContent { + resource: URI; + value: string; +} + +function resolveContents(resources: URI[]): TPromise { + const contents: IContent[] = []; + + return TPromise.join(resources.map(resource => { + return resolveContent(resource).then(content => { + contents.push(content); + }); + })).then(() => contents); +} + +function resolveContent(resource: URI): TPromise { + return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() })); +} + +function resolveStat(resource: URI): TPromise { + return new TPromise((c, e) => { + extfs.readdir(resource.fsPath, (error, children) => { + if (error) { + if ((error).code === 'ENOTDIR') { + c({ resource }); + } else { + e(error); + } + } else { + c({ + resource, + isDirectory: true, + children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; }) + }); + } + }); + }); +} + +export class WorkspaceConfiguration extends Disposable { + + private _workspaceConfigPath: URI; + private _workspaceConfigurationWatcher: ConfigWatcher; + private _workspaceConfigurationWatcherDisposables: IDisposable[] = []; + + private _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; + + load(workspaceConfigPath: URI): TPromise { + if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) { + return this.reload(); + } + + this._workspaceConfigPath = workspaceConfigPath; + + this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables); + return new TPromise((c, e) => { + this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, { + changeBufferDelay: 300, + onError: error => errors.onUnexpectedError(error), + defaultConfig: new WorkspaceConfigurationModel(JSON.stringify({ folders: [] } as IStoredWorkspace, null, '\t'), this._workspaceConfigPath.fsPath), + parse: (content: string, parseErrors: any[]) => { + const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath); + parseErrors = [...workspaceConfigurationModel.errors]; + return workspaceConfigurationModel; + }, initCallback: () => c(null) + }); + this._workspaceConfigurationWatcherDisposables.push(this._workspaceConfigurationWatcher); + this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables); + }); + } + + private get workspaceConfigurationModel(): WorkspaceConfigurationModel { + return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel(); + } + + reload(): TPromise { + return new TPromise(c => this._workspaceConfigurationWatcher.reload(() => c(null))); + } + + getFolders(): IStoredWorkspaceFolder[] { + return this.workspaceConfigurationModel.folders; + } + + setFolders(folders: IStoredWorkspaceFolder[], jsonEditingService: JSONEditingService): TPromise { + return jsonEditingService.write(this._workspaceConfigPath, { key: 'folders', value: folders }, true) + .then(() => this.reload()); + } + + getConfiguration(): ConfigurationModel { + return this.workspaceConfigurationModel.workspaceConfiguration; + } + + dispose(): void { + dispose(this._workspaceConfigurationWatcherDisposables); + super.dispose(); + } +} + +export class FolderConfiguration extends Disposable { + + private static RELOAD_CONFIGURATION_DELAY = 50; + + private bulkFetchFromWorkspacePromise: TPromise; + private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; + + private reloadConfigurationScheduler: RunOnceScheduler; + private reloadConfigurationEventEmitter: Emitter = new Emitter(); + + constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) { + super(); + + this.workspaceFilePathToConfiguration = Object.create(null); + this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configuration => this.reloadConfigurationEventEmitter.fire(configuration), errors.onUnexpectedError), FolderConfiguration.RELOAD_CONFIGURATION_DELAY)); + } + + loadConfiguration(): TPromise { + // Load workspace locals + return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => { + // Consolidate (support *.json files in the workspace settings folder) + const workspaceSettingsConfig = workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new FolderSettingsModel(null); + const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => workspaceConfigFiles[key]); + return new FolderConfigurationModel(workspaceSettingsConfig, otherConfigModels, this.scope); + }); + } + + private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel }> { + // once: when invoked for the first time we fetch json files that contribute settings + if (!this.bulkFetchFromWorkspacePromise) { + this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => { + if (!stat.isDirectory) { + return TPromise.as([]); + } + + return resolveContents(stat.children.filter(stat => { + const isJson = paths.extname(stat.resource.fsPath) === '.json'; + if (!isJson) { + return false; // only JSON files + } + + return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files + }).map(stat => stat.resource)); + }, err => [] /* never fail this call */) + .then((contents: IContent[]) => { + contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content))); + }, errors.onUnexpectedError); + } + + // on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this + // happens whenever a config file changes, is deleted, or added + return this.bulkFetchFromWorkspacePromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration)); + } + + public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise { + const events = event.changes; + let affectedByChanges = false; + + // Find changes that affect workspace configuration files + for (let i = 0, len = events.length; i < len; i++) { + const resource = events[i].resource; + const isJson = paths.extname(resource.fsPath) === '.json'; + const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath)); + if (!isJson && !isDeletedSettingsFolder) { + continue; // only JSON files or the actual settings folder + } + + const workspacePath = this.toFolderRelativePath(resource); + if (!workspacePath) { + continue; // event is not inside workspace + } + + // Handle case where ".vscode" got deleted + if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) { + this.workspaceFilePathToConfiguration = Object.create(null); + affectedByChanges = true; + } + + // only valid workspace config files + if (!this.isWorkspaceConfigurationFile(workspacePath)) { + continue; + } + + // insert 'fetch-promises' for add and update events and + // remove promises for delete events + switch (events[i].type) { + case FileChangeType.DELETED: + affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath); + break; + case FileChangeType.UPDATED: + case FileChangeType.ADDED: + this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError); + affectedByChanges = true; + } + } + + if (!affectedByChanges) { + return TPromise.as(null); + } + + return new TPromise((c, e) => { + let disposable = this.reloadConfigurationEventEmitter.event(configuration => { + disposable.dispose(); + c(configuration); + }); + // trigger reload of the configuration if we are affected by changes + if (!this.reloadConfigurationScheduler.isScheduled()) { + this.reloadConfigurationScheduler.schedule(); + } + }); + } + + private createConfigModel(content: IContent): ConfigurationModel { + const path = this.toFolderRelativePath(content.resource); + if (path === WORKSPACE_CONFIG_DEFAULT_PATH) { + return new FolderSettingsModel(content.value, content.resource.toString()); + } else { + const matches = /\/([^\.]*)*\.json/.exec(path); + if (matches && matches[1]) { + return new ScopedConfigurationModel(content.value, content.resource.toString(), matches[1]); + } + } + + return new CustomConfigurationModel(null); + } + + private isWorkspaceConfigurationFile(folderRelativePath: string): boolean { + return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY], WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY]].some(p => p === folderRelativePath); + } + + private toResource(folderRelativePath: string): URI { + if (typeof folderRelativePath === 'string') { + return URI.file(paths.join(this.folder.fsPath, folderRelativePath)); + } + + return null; + } + + private toFolderRelativePath(resource: URI, toOSPath?: boolean): string { + if (this.contains(resource)) { + return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath); + } + + return null; + } + + private contains(resource: URI): boolean { + if (resource) { + return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */); + } + + return false; + } +} \ No newline at end of file diff --git a/src/vs/workbench/services/configuration/node/configurationService.ts b/src/vs/workbench/services/configuration/node/configurationService.ts index edd6058936e..e56f27c9ca5 100644 --- a/src/vs/workbench/services/configuration/node/configurationService.ts +++ b/src/vs/workbench/services/configuration/node/configurationService.ts @@ -7,31 +7,28 @@ import URI from 'vs/base/common/uri'; import * as paths from 'vs/base/common/paths'; import { TPromise } from 'vs/base/common/winjs.base'; +import { dirname } from 'path'; import * as assert from 'vs/base/common/assert'; import Event, { Emitter } from 'vs/base/common/event'; import { StrictResourceMap } from 'vs/base/common/map'; -import * as errors from 'vs/base/common/errors'; import { equals } from 'vs/base/common/objects'; -import * as collections from 'vs/base/common/collections'; -import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler } from 'vs/base/common/async'; -import { readFile, stat, writeFile } from 'vs/base/node/pfs'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Queue } from 'vs/base/common/async'; +import { stat, writeFile } from 'vs/base/node/pfs'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; -import * as extfs from 'vs/base/node/extfs'; import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; -import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileChangesEvent } from 'vs/platform/files/common/files'; import { isLinux } from 'vs/base/common/platform'; -import { ConfigWatcher } from 'vs/base/node/config'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { CustomConfigurationModel, ConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; +import { ConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData } from 'vs/platform/configuration/common/configuration'; -import { WorkspaceConfigurationModel, ScopedConfigurationModel, FolderConfigurationModel, FolderSettingsModel, Configuration, WorkspaceConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; -import { IWorkspaceConfigurationService, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, WORKSPACE_STANDALONE_CONFIGURATIONS, WORKSPACE_CONFIG_DEFAULT_PATH, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; +import { FolderConfigurationModel, Configuration, WorkspaceConfigurationChangeEvent } from 'vs/workbench/services/configuration/common/configurationModels'; +import { IWorkspaceConfigurationService, WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { ConfigurationService as GlobalConfigurationService } from 'vs/platform/configuration/node/configurationService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationNode, IConfigurationRegistry, Extensions, ConfigurationScope, settingsSchema, resourceSettingsSchema } from 'vs/platform/configuration/common/configurationRegistry'; import { createHash } from 'crypto'; -import { getWorkspaceLabel, IWorkspacesService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { getWorkspaceLabel, IWorkspacesService, IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { IWindowConfiguration } from 'vs/platform/windows/common/windows'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -39,17 +36,10 @@ import product from 'vs/platform/node/product'; import pkg from 'vs/platform/node/package'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ConfigurationEditingService } from 'vs/workbench/services/configuration/node/configurationEditingService'; - -interface IStat { - resource: URI; - isDirectory?: boolean; - children?: { resource: URI; }[]; -} - -interface IContent { - resource: URI; - value: string; -} +import { WorkspaceConfiguration, FolderConfiguration } from 'vs/workbench/services/configuration/node/configuration'; +import { JSONEditingService } from 'vs/workbench/services/configuration/node/jsonEditingService'; +import { Schemas } from 'vs/base/common/network'; +import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; export class WorkspaceService extends Disposable implements IWorkspaceConfigurationService, IWorkspaceContextService { @@ -61,6 +51,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private workspaceConfiguration: WorkspaceConfiguration; private cachedFolderConfigs: StrictResourceMap; + private workspaceEditingQueue: Queue; + protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); public readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; @@ -74,6 +66,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat public readonly onDidChangeWorkbenchState: Event = this._onDidChangeWorkbenchState.event; private configurationEditingService: ConfigurationEditingService; + private jsonEditingService: JSONEditingService; constructor(private environmentService: IEnvironmentService, private workspacesService: IWorkspacesService, private workspaceSettingsRootFolder: string = WORKSPACE_CONFIG_FOLDER_DEFAULT_NAME) { super(); @@ -84,6 +77,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat this.baseConfigurationService = this._register(new GlobalConfigurationService(environmentService)); this._register(this.baseConfigurationService.onDidChangeConfiguration(e => this.onBaseConfigurationChanged(e))); this._register(Registry.as(Extensions.Configuration).onDidRegisterConfiguration(e => this.registerConfigurationSchemas())); + + this.workspaceEditingQueue = new Queue(); } // Workspace Context Service Impl @@ -111,6 +106,16 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return this.workspace.getFolder(resource); } + public addFolders(foldersToAdd: URI[]): TPromise { + assert.ok(this.jsonEditingService, 'Workbench is not initialized yet'); + return this.workspaceEditingQueue.queue(() => this.doAddFolders(foldersToAdd)); + } + + public removeFolders(foldersToRemove: URI[]): TPromise { + assert.ok(this.jsonEditingService, 'Workbench is not initialized yet'); + return this.workspaceEditingQueue.queue(() => this.doRemoveFolders(foldersToRemove)); + } + public isInsideWorkspace(resource: URI): boolean { return !!this.getWorkspaceFolder(resource); } @@ -125,6 +130,79 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat return false; } + private doAddFolders(foldersToAdd: URI[]): TPromise { + if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) { + return TPromise.as(void 0); // we need a workspace to begin with + } + + const currentWorkspaceFolders = this.getWorkspace().folders; + const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri); + const currentStoredFolders = currentWorkspaceFolders.map(folder => folder.raw); + + const storedFoldersToAdd: IStoredWorkspaceFolder[] = []; + + const workspaceConfigFolder = dirname(this.getWorkspace().configuration.fsPath); + + foldersToAdd.forEach(folderToAdd => { + if (this.contains(currentWorkspaceFolderUris, folderToAdd)) { + return; // already existing + } + + // File resource: use "path" property + if (folderToAdd.scheme === Schemas.file) { + storedFoldersToAdd.push({ + path: massageFolderPathForWorkspace(folderToAdd.fsPath, workspaceConfigFolder, currentStoredFolders) + }); + } + + // Any other resource: use "uri" property + else { + storedFoldersToAdd.push({ + uri: folderToAdd.toString(true) + }); + } + }); + + if (storedFoldersToAdd.length > 0) { + return this.workspaceConfiguration.setFolders([...currentStoredFolders, ...storedFoldersToAdd], this.jsonEditingService); + } + + return TPromise.as(void 0); + } + + private doRemoveFolders(foldersToRemove: URI[]): TPromise { + if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) { + return TPromise.as(void 0); // we need a workspace to begin with + } + + const currentWorkspaceFolders = this.getWorkspace().folders; + const currentStoredFolders = currentWorkspaceFolders.map(folder => folder.raw); + + const newStoredFolders: IStoredWorkspaceFolder[] = currentStoredFolders.filter((folder, index) => { + if (!isStoredWorkspaceFolder(folder)) { + return true; // keep entries which are unrelated + } + + return !this.contains(foldersToRemove, currentWorkspaceFolders[index].uri); // keep entries which are unrelated + }); + + if (newStoredFolders.length !== currentStoredFolders.length) { + return this.workspaceConfiguration.setFolders(newStoredFolders, this.jsonEditingService); + } + + return TPromise.as(void 0); + } + + private contains(resources: URI[], toCheck: URI): boolean { + return resources.some(resource => { + if (isLinux) { + return resource.toString() === toCheck.toString(); + } + + return resource.toString().toLowerCase() === toCheck.toString().toLowerCase(); + }); + } + // Workspace Configuration Service Impl getConfigurationData(): IConfigurationData { @@ -198,6 +276,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat setInstantiationService(instantiationService: IInstantiationService): void { this.configurationEditingService = instantiationService.createInstance(ConfigurationEditingService); + this.jsonEditingService = instantiationService.createInstance(JSONEditingService); } handleWorkspaceFileEvents(event: FileChangesEvent): TPromise { @@ -226,8 +305,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat const workspaceConfigPath = URI.file(workspaceIdentifier.configPath); return this.workspaceConfiguration.load(workspaceConfigPath) .then(() => { - const workspaceConfigurationModel = this.workspaceConfiguration.workspaceConfigurationModel; - const workspaceFolders = toWorkspaceFolders(workspaceConfigurationModel.folders, URI.file(paths.dirname(workspaceConfigPath.fsPath))); + const workspaceFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(paths.dirname(workspaceConfigPath.fsPath))); const workspaceId = workspaceIdentifier.id; const workspaceName = getWorkspaceLabel({ id: workspaceId, configPath: workspaceConfigPath.fsPath }, this.environmentService); return new Workspace(workspaceId, workspaceName, workspaceFolders, workspaceConfigPath); @@ -341,7 +419,7 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat case WorkbenchState.FOLDER: return folderConfigurations[0]; case WorkbenchState.WORKSPACE: - return this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration; + return this.workspaceConfiguration.getConfiguration(); default: return new ConfigurationModel(); } @@ -378,8 +456,8 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat private onWorkspaceConfigurationChanged(): TPromise { if (this.workspace && this.workspace.configuration && this._configuration) { - const workspaceConfigurationChangeEvent = this._configuration.updateWorkspaceConfiguration(this.workspaceConfiguration.workspaceConfigurationModel.workspaceConfiguration); - let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.workspaceConfigurationModel.folders, URI.file(paths.dirname(this.workspace.configuration.fsPath))); + const workspaceConfigurationChangeEvent = this._configuration.updateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration()); + let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), URI.file(paths.dirname(this.workspace.configuration.fsPath))); const changes = this.compareFolders(this.workspace.folders, configuredFolders); if (changes.added.length || changes.removed.length || changes.changed.length) { this.workspace.folders = configuredFolders; @@ -557,244 +635,6 @@ export class WorkspaceService extends Disposable implements IWorkspaceConfigurat } } -class WorkspaceConfiguration extends Disposable { - - private _workspaceConfigPath: URI; - private _workspaceConfigurationWatcher: ConfigWatcher; - private _workspaceConfigurationWatcherDisposables: IDisposable[] = []; - - private _onDidUpdateConfiguration: Emitter = this._register(new Emitter()); - public readonly onDidUpdateConfiguration: Event = this._onDidUpdateConfiguration.event; - - load(workspaceConfigPath: URI): TPromise { - if (this._workspaceConfigPath && this._workspaceConfigPath.fsPath === workspaceConfigPath.fsPath) { - return this.reload(); - } - - this._workspaceConfigPath = workspaceConfigPath; - - this._workspaceConfigurationWatcherDisposables = dispose(this._workspaceConfigurationWatcherDisposables); - return new TPromise((c, e) => { - this._workspaceConfigurationWatcher = new ConfigWatcher(this._workspaceConfigPath.fsPath, { - changeBufferDelay: 300, - onError: error => errors.onUnexpectedError(error), - defaultConfig: new WorkspaceConfigurationModel(JSON.stringify({ folders: [] } as IStoredWorkspace, null, '\t'), this._workspaceConfigPath.fsPath), - parse: (content: string, parseErrors: any[]) => { - const workspaceConfigurationModel = new WorkspaceConfigurationModel(content, this._workspaceConfigPath.fsPath); - parseErrors = [...workspaceConfigurationModel.errors]; - return workspaceConfigurationModel; - }, initCallback: () => c(null) - }); - this._workspaceConfigurationWatcherDisposables.push(this._workspaceConfigurationWatcher); - this._workspaceConfigurationWatcher.onDidUpdateConfiguration(() => this._onDidUpdateConfiguration.fire(), this, this._workspaceConfigurationWatcherDisposables); - }); - } - - get workspaceConfigurationModel(): WorkspaceConfigurationModel { - return this._workspaceConfigurationWatcher ? this._workspaceConfigurationWatcher.getConfig() : new WorkspaceConfigurationModel(); - } - - reload(): TPromise { - return new TPromise(c => this._workspaceConfigurationWatcher.reload(() => c(null))); - } - - dispose(): void { - dispose(this._workspaceConfigurationWatcherDisposables); - super.dispose(); - } -} - -class FolderConfiguration extends Disposable { - - private static RELOAD_CONFIGURATION_DELAY = 50; - - private bulkFetchFromWorkspacePromise: TPromise; - private workspaceFilePathToConfiguration: { [relativeWorkspacePath: string]: TPromise }; - - private reloadConfigurationScheduler: RunOnceScheduler; - private reloadConfigurationEventEmitter: Emitter = new Emitter(); - - constructor(private folder: URI, private configFolderRelativePath: string, private scope: ConfigurationScope) { - super(); - - this.workspaceFilePathToConfiguration = Object.create(null); - this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.loadConfiguration().then(configuration => this.reloadConfigurationEventEmitter.fire(configuration), errors.onUnexpectedError), FolderConfiguration.RELOAD_CONFIGURATION_DELAY)); - } - - loadConfiguration(): TPromise { - // Load workspace locals - return this.loadWorkspaceConfigFiles().then(workspaceConfigFiles => { - // Consolidate (support *.json files in the workspace settings folder) - const workspaceSettingsConfig = workspaceConfigFiles[WORKSPACE_CONFIG_DEFAULT_PATH] || new FolderSettingsModel(null); - const otherConfigModels = Object.keys(workspaceConfigFiles).filter(key => key !== WORKSPACE_CONFIG_DEFAULT_PATH).map(key => workspaceConfigFiles[key]); - return new FolderConfigurationModel(workspaceSettingsConfig, otherConfigModels, this.scope); - }); - } - - private loadWorkspaceConfigFiles(): TPromise<{ [relativeWorkspacePath: string]: ConfigurationModel }> { - // once: when invoked for the first time we fetch json files that contribute settings - if (!this.bulkFetchFromWorkspacePromise) { - this.bulkFetchFromWorkspacePromise = resolveStat(this.toResource(this.configFolderRelativePath)).then(stat => { - if (!stat.isDirectory) { - return TPromise.as([]); - } - - return resolveContents(stat.children.filter(stat => { - const isJson = paths.extname(stat.resource.fsPath) === '.json'; - if (!isJson) { - return false; // only JSON files - } - - return this.isWorkspaceConfigurationFile(this.toFolderRelativePath(stat.resource)); // only workspace config files - }).map(stat => stat.resource)); - }, err => [] /* never fail this call */) - .then((contents: IContent[]) => { - contents.forEach(content => this.workspaceFilePathToConfiguration[this.toFolderRelativePath(content.resource)] = TPromise.as(this.createConfigModel(content))); - }, errors.onUnexpectedError); - } - - // on change: join on *all* configuration file promises so that we can merge them into a single configuration object. this - // happens whenever a config file changes, is deleted, or added - return this.bulkFetchFromWorkspacePromise.then(() => TPromise.join(this.workspaceFilePathToConfiguration)); - } - - public handleWorkspaceFileEvents(event: FileChangesEvent): TPromise { - const events = event.changes; - let affectedByChanges = false; - - // Find changes that affect workspace configuration files - for (let i = 0, len = events.length; i < len; i++) { - const resource = events[i].resource; - const isJson = paths.extname(resource.fsPath) === '.json'; - const isDeletedSettingsFolder = (events[i].type === FileChangeType.DELETED && paths.isEqual(paths.basename(resource.fsPath), this.configFolderRelativePath)); - if (!isJson && !isDeletedSettingsFolder) { - continue; // only JSON files or the actual settings folder - } - - const workspacePath = this.toFolderRelativePath(resource); - if (!workspacePath) { - continue; // event is not inside workspace - } - - // Handle case where ".vscode" got deleted - if (workspacePath === this.configFolderRelativePath && events[i].type === FileChangeType.DELETED) { - this.workspaceFilePathToConfiguration = Object.create(null); - affectedByChanges = true; - } - - // only valid workspace config files - if (!this.isWorkspaceConfigurationFile(workspacePath)) { - continue; - } - - // insert 'fetch-promises' for add and update events and - // remove promises for delete events - switch (events[i].type) { - case FileChangeType.DELETED: - affectedByChanges = collections.remove(this.workspaceFilePathToConfiguration, workspacePath); - break; - case FileChangeType.UPDATED: - case FileChangeType.ADDED: - this.workspaceFilePathToConfiguration[workspacePath] = resolveContent(resource).then(content => this.createConfigModel(content), errors.onUnexpectedError); - affectedByChanges = true; - } - } - - if (!affectedByChanges) { - return TPromise.as(null); - } - - return new TPromise((c, e) => { - let disposable = this.reloadConfigurationEventEmitter.event(configuration => { - disposable.dispose(); - c(configuration); - }); - // trigger reload of the configuration if we are affected by changes - if (!this.reloadConfigurationScheduler.isScheduled()) { - this.reloadConfigurationScheduler.schedule(); - } - }); - } - - private createConfigModel(content: IContent): ConfigurationModel { - const path = this.toFolderRelativePath(content.resource); - if (path === WORKSPACE_CONFIG_DEFAULT_PATH) { - return new FolderSettingsModel(content.value, content.resource.toString()); - } else { - const matches = /\/([^\.]*)*\.json/.exec(path); - if (matches && matches[1]) { - return new ScopedConfigurationModel(content.value, content.resource.toString(), matches[1]); - } - } - - return new CustomConfigurationModel(null); - } - - private isWorkspaceConfigurationFile(folderRelativePath: string): boolean { - return [WORKSPACE_CONFIG_DEFAULT_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY], WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY]].some(p => p === folderRelativePath); - } - - private toResource(folderRelativePath: string): URI { - if (typeof folderRelativePath === 'string') { - return URI.file(paths.join(this.folder.fsPath, folderRelativePath)); - } - - return null; - } - - private toFolderRelativePath(resource: URI, toOSPath?: boolean): string { - if (this.contains(resource)) { - return paths.normalize(paths.relative(this.folder.fsPath, resource.fsPath), toOSPath); - } - - return null; - } - - private contains(resource: URI): boolean { - if (resource) { - return paths.isEqualOrParent(resource.fsPath, this.folder.fsPath, !isLinux /* ignorecase */); - } - - return false; - } -} - -// node.hs helper functions - -function resolveContents(resources: URI[]): TPromise { - const contents: IContent[] = []; - - return TPromise.join(resources.map(resource => { - return resolveContent(resource).then(content => { - contents.push(content); - }); - })).then(() => contents); -} - -function resolveContent(resource: URI): TPromise { - return readFile(resource.fsPath).then(contents => ({ resource, value: contents.toString() })); -} - -function resolveStat(resource: URI): TPromise { - return new TPromise((c, e) => { - extfs.readdir(resource.fsPath, (error, children) => { - if (error) { - if ((error).code === 'ENOTDIR') { - c({ resource }); - } else { - e(error); - } - } else { - c({ - resource, - isDirectory: true, - children: children.map(child => { return { resource: URI.file(paths.join(resource.fsPath, child)) }; }) - }); - } - }); - }); -} - interface IExportedConfigurationNode { name: string; description: string; diff --git a/src/vs/workbench/services/workspace/common/workspaceEditing.ts b/src/vs/workbench/services/workspace/common/workspaceEditing.ts index 56f6b473db2..8f5e6cd634d 100644 --- a/src/vs/workbench/services/workspace/common/workspaceEditing.ts +++ b/src/vs/workbench/services/workspace/common/workspaceEditing.ts @@ -5,7 +5,6 @@ 'use strict'; import { TPromise } from 'vs/base/common/winjs.base'; -import URI from 'vs/base/common/uri'; import { createDecorator, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -15,16 +14,6 @@ export interface IWorkspaceEditingService { _serviceBrand: ServiceIdentifier; - /** - * add folders to the existing workspace - */ - addFolders(folders: URI[]): TPromise; - - /** - * remove folders from the existing workspace - */ - removeFolders(folders: URI[]): TPromise; - /** * creates a new workspace with the provided folders and opens it. if path is provided * the workspace will be saved into that location. diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index dfe723df71c..f5711bdfb21 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -9,14 +9,10 @@ import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IWindowsService, IWindowService, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IWindowService, IEnterWorkspaceResult } from 'vs/platform/windows/common/windows'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; -import { IWorkspacesService, IStoredWorkspaceFolder, IWorkspaceIdentifier, isStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; -import { dirname } from 'path'; +import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; -import { isLinux } from 'vs/base/common/platform'; import { WorkspaceService } from 'vs/workbench/services/configuration/node/configurationService'; import { migrateStorageToMultiRootWorkspace } from 'vs/platform/storage/common/migration'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -26,7 +22,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionService } from 'vs/platform/extensions/common/extensions'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileService'; -import { Schemas } from 'vs/base/common/network'; export class WorkspaceEditingService implements IWorkspaceEditingService { @@ -35,10 +30,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { constructor( @IJSONEditingService private jsonEditingService: IJSONEditingService, @IWorkspaceContextService private contextService: WorkspaceService, - @IEnvironmentService private environmentService: IEnvironmentService, - @IWindowsService private windowsService: IWindowsService, @IWindowService private windowService: IWindowService, - @IWorkspacesService private workspacesService: IWorkspacesService, @IWorkspaceConfigurationService private workspaceConfigurationService: IWorkspaceConfigurationService, @IStorageService private storageService: IStorageService, @IExtensionService private extensionService: IExtensionService, @@ -46,89 +38,6 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { ) { } - public addFolders(foldersToAdd: URI[]): TPromise { - if (!this.isSupported()) { - return TPromise.as(void 0); // we need a workspace to begin with - } - - const currentWorkspaceFolders = this.contextService.getWorkspace().folders; - const currentWorkspaceFolderUris = currentWorkspaceFolders.map(folder => folder.uri); - const currentStoredFolders = currentWorkspaceFolders.map(folder => folder.raw); - - const storedFoldersToAdd: IStoredWorkspaceFolder[] = []; - - const workspaceConfigFolder = dirname(this.contextService.getWorkspace().configuration.fsPath); - - foldersToAdd.forEach(folderToAdd => { - if (this.contains(currentWorkspaceFolderUris, folderToAdd)) { - return; // already existing - } - - // File resource: use "path" property - if (folderToAdd.scheme === Schemas.file) { - storedFoldersToAdd.push({ - path: massageFolderPathForWorkspace(folderToAdd.fsPath, workspaceConfigFolder, currentStoredFolders) - }); - } - - // Any other resource: use "uri" property - else { - storedFoldersToAdd.push({ - uri: folderToAdd.toString(true) - }); - } - }); - - if (storedFoldersToAdd.length > 0) { - return this.doSetFolders([...currentStoredFolders, ...storedFoldersToAdd]); - } - - return TPromise.as(void 0); - } - - public removeFolders(foldersToRemove: URI[]): TPromise { - if (!this.isSupported()) { - return TPromise.as(void 0); // we need a workspace to begin with - } - - const currentWorkspaceFolders = this.contextService.getWorkspace().folders; - const currentStoredFolders = currentWorkspaceFolders.map(folder => folder.raw); - - const newStoredFolders: IStoredWorkspaceFolder[] = currentStoredFolders.filter((folder, index) => { - if (!isStoredWorkspaceFolder(folder)) { - return true; // keep entries which are unrelated - } - - return !this.contains(foldersToRemove, currentWorkspaceFolders[index].uri); // keep entries which are unrelated - }); - - if (newStoredFolders.length !== currentStoredFolders.length) { - return this.doSetFolders(newStoredFolders); - } - - return TPromise.as(void 0); - } - - private doSetFolders(folders: IStoredWorkspaceFolder[]): TPromise { - const workspace = this.contextService.getWorkspace(); - - return this.jsonEditingService.write(workspace.configuration, { key: 'folders', value: folders }, true); - } - - private isSupported(): boolean { - return this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; // we need a multi folder workspace to begin with; - } - - private contains(resources: URI[], toCheck: URI): boolean { - return resources.some(resource => { - if (isLinux) { - return resource.toString() === toCheck.toString(); - } - - return resource.toString().toLowerCase() === toCheck.toString().toLowerCase(); - }); - } - public createAndEnterWorkspace(folderPaths?: string[], path?: string): TPromise { return this.doEnterWorkspace(() => this.windowService.createAndEnterWorkspace(folderPaths, path)); } diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index 0340fff3501..6c20a16bf44 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -118,6 +118,14 @@ export class TestContextService implements IWorkspaceContextService { return this.workspace; } + public addFolders(foldersToAdd: URI[]): TPromise { + return TPromise.as(void 0); + } + + public removeFolders(foldersToRemove: URI[]): TPromise { + return TPromise.as(void 0); + } + public getWorkspaceFolder(resource: URI): IWorkspaceFolder { return this.isInsideWorkspace(resource) ? this.workspace.folders[0] : null; }