From edc710011400f9be78ee2253a41a9f9f71f81425 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 26 Mar 2020 15:40:03 -0700 Subject: [PATCH] Apply multiple extension mutators, share code with ext host --- .../api/browser/mainThreadTerminalService.ts | 10 +- .../workbench/api/common/extHost.protocol.ts | 10 +- .../api/common/extHostTerminalService.ts | 20 +-- .../api/node/extHostTerminalService.ts | 28 +--- .../browser/terminalProcessManager.ts | 69 ++++---- .../terminal/common/environmentVariable.ts | 45 ++++- .../common/environmentVariableCollection.ts | 154 +++++++++++++----- .../common/environmentVariableService.ts | 60 +++---- .../common/environmentVariableShared.ts | 18 ++ .../environmentVariableCollection.test.ts | 124 ++++++-------- .../common/environmentVariableService.test.ts | 71 ++++---- .../common/environmentVariableShared.test.ts | 38 +++++ 12 files changed, 375 insertions(+), 272 deletions(-) create mode 100644 src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts create mode 100644 src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 21589ab69f3..9ca6d26134c 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -5,7 +5,7 @@ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto, IEnvironmentVariableCollectionDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { URI } from 'vs/base/common/uri'; import { StopWatch } from 'vs/base/common/stopwatch'; @@ -13,8 +13,8 @@ import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITermina import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering'; -import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { EnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; @extHostNamedCustomer(MainContext.MainThreadTerminalService) export class MainThreadTerminalService implements MainThreadTerminalServiceShape { @@ -350,9 +350,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape return terminal; } - $setEnvironmentVariableCollection(extensionIdentifier: string, collection: IEnvironmentVariableCollectionDto | undefined): void { + $setEnvironmentVariableCollection(extensionIdentifier: string, collection: ISerializableEnvironmentVariableCollection | undefined): void { if (collection) { - const translatedCollection = new EnvironmentVariableCollection(collection.variables, collection.values, collection.types); + const translatedCollection = deserializeEnvironmentVariableCollection(collection); this._environmentVariableService.set(extensionIdentifier, translatedCollection); } else { this._environmentVariableService.delete(extensionIdentifier); diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8e6fa94038e..0b56ebb0e8a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -54,7 +54,7 @@ import { revive } from 'vs/base/common/marshalling'; import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; -import { EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -427,12 +427,6 @@ export interface TerminalLaunchConfig { isExtensionTerminal?: boolean; } -export interface IEnvironmentVariableCollectionDto { - variables: string[]; - values: string[]; - types: EnvironmentVariableMutatorType[]; -} - export interface MainThreadTerminalServiceShape extends IDisposable { $createTerminal(config: TerminalLaunchConfig): Promise<{ id: number, name: string; }>; $dispose(terminalId: number): void; @@ -443,7 +437,7 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $stopSendingDataEvents(): void; $startHandlingLinks(): void; $stopHandlingLinks(): void; - $setEnvironmentVariableCollection(extensionIdentifier: string, collection: IEnvironmentVariableCollectionDto | undefined): void; + $setEnvironmentVariableCollection(extensionIdentifier: string, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process $sendProcessTitle(terminalId: number, title: string): void; diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 1a99a6b6476..54dd2c878d9 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -648,50 +648,50 @@ export class EnvironmentVariableMutator implements vscode.EnvironmentVariableMut } export class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollection { - private _entries: Map = new Map(); + public entries: Map = new Map(); protected readonly _onDidChangeCollection: Emitter = new Emitter(); get onDidChangeCollection(): Event { return this._onDidChangeCollection && this._onDidChangeCollection.event; } get size(): number { - return this._entries.size; + return this.entries.size; } replace(variable: string, value: string): void { - this._entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Replace)); + this.entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Replace)); this._onDidChangeCollection.fire(); } append(variable: string, value: string): void { - this._entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Append)); + this.entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Append)); this._onDidChangeCollection.fire(); } prepend(variable: string, value: string): void { - this._entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Prepend)); + this.entries.set(variable, new EnvironmentVariableMutator(value, EnvironmentVariableMutatorType.Prepend)); this._onDidChangeCollection.fire(); } get(variable: string): EnvironmentVariableMutator | undefined { - return this._entries.get(variable); + return this.entries.get(variable); } forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => any, thisArg?: any): void { - this._entries.forEach((value, key) => callback(key, value, this)); + this.entries.forEach((value, key) => callback(key, value, this)); } delete(variable: string): void { - this._entries.delete(variable); + this.entries.delete(variable); this._onDidChangeCollection.fire(); } clear(): void { - this._entries.clear(); + this.entries.clear(); this._onDidChangeCollection.fire(); } dispose(): void { - this._entries.clear(); + this.entries.clear(); this._onDidChangeCollection.fire(); } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 31fe513969e..65a29485705 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -9,7 +9,7 @@ import * as os from 'os'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, IEnvironmentVariableCollectionDto } from 'vs/workbench/api/common/extHost.protocol'; +import { IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostConfiguration, ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; import { ILogService } from 'vs/platform/log/common/log'; import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -23,8 +23,8 @@ import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/term import { BaseExtHostTerminalService, ExtHostTerminal, EnvironmentVariableCollection } from 'vs/workbench/api/common/extHostTerminalService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { EnvironmentVariableMutatorType } from 'vs/workbench/api/common/extHostTypes'; import { dispose } from 'vs/base/common/lifecycle'; +import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; export class ExtHostTerminalService extends BaseExtHostTerminalService { @@ -254,27 +254,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { } private _syncEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { - this._proxy.$setEnvironmentVariableCollection(extensionIdentifier, this._serializeEnvironmentVariableCollection(collection)); - } - - private _serializeEnvironmentVariableCollection(collection: EnvironmentVariableCollection): IEnvironmentVariableCollectionDto | undefined { - if (collection.size === 0) { - return undefined; - } - - - const variables: string[] = []; - const values: string[] = []; - const types: EnvironmentVariableMutatorType[] = []; - collection.forEach((variable, mutator) => { - variables.push(variable); - values.push(mutator.value); - types.push(mutator.type); - }); - return { - variables, - values, - types - }; + const serialized = serializeEnvironmentVariableCollection(collection.entries); + this._proxy.$setEnvironmentVariableCollection(extensionIdentifier, serialized.length === 0 ? undefined : serialized); } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 4a99d29c50e..2a11d92c192 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { env as processEnv } from 'vs/base/common/process'; @@ -24,8 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Disposable } from 'vs/base/common/lifecycle'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IEnvironmentVariableService, IEnvironmentVariableCollection, EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; -import { INotificationService, Severity, IPromptChoice } from 'vs/platform/notification/common/notification'; +import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -62,7 +60,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce private _latency: number = -1; private _latencyLastMeasured: number = 0; private _initialCwd: string | undefined; - private _extEnvironmentVariableCollection: IEnvironmentVariableCollection | undefined; + private _extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined; private readonly _onProcessReady = this._register(new Emitter()); public get onProcessReady(): Event { return this._onProcessReady.event; } @@ -92,8 +90,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @IProductService private readonly _productService: IProductService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, - @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, - @INotificationService private readonly _notificationService: INotificationService + @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService ) { super(); this.ptyProcessReady = new Promise(c => { @@ -316,35 +313,35 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._onProcessExit.fire(exitCode); } - private _onEnvironmentVariableCollectionChange(newCollection: IEnvironmentVariableCollection): void { - const newAdditions = this._extEnvironmentVariableCollection!.getNewAdditions(newCollection); - if (newAdditions === undefined) { - return; - } - const promptChoices: IPromptChoice[] = [ - { - label: nls.localize('apply', "Apply"), - run: () => { - let text = ''; - newAdditions.forEach((mutator, variable) => { - // TODO: Support other common shells - // TODO: Escape the new values properly - switch (mutator.type) { - case EnvironmentVariableMutatorType.Append: - text += `export ${variable}="$${variable}${mutator.value}"\n`; - break; - case EnvironmentVariableMutatorType.Prepend: - text += `export ${variable}="${mutator.value}$${variable}"\n`; - break; - case EnvironmentVariableMutatorType.Replace: - text += `export ${variable}="${mutator.value}"\n`; - break; - } - }); - this.write(text); - } - } as IPromptChoice - ]; - this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send commands to set the variables in the terminal? Note if you have an application open in the terminal this may not work."), promptChoices); + private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void { + // const newAdditions = this._extEnvironmentVariableCollection!.getNewAdditions(newCollection); + // if (newAdditions === undefined) { + // return; + // } + // const promptChoices: IPromptChoice[] = [ + // { + // label: nls.localize('apply', "Apply"), + // run: () => { + // let text = ''; + // newAdditions.forEach((mutator, variable) => { + // // TODO: Support other common shells + // // TODO: Escape the new values properly + // switch (mutator.type) { + // case EnvironmentVariableMutatorType.Append: + // text += `export ${variable}="$${variable}${mutator.value}"\n`; + // break; + // case EnvironmentVariableMutatorType.Prepend: + // text += `export ${variable}="${mutator.value}$${variable}"\n`; + // break; + // case EnvironmentVariableMutatorType.Replace: + // text += `export ${variable}="${mutator.value}"\n`; + // break; + // } + // }); + // this.write(text); + // } + // } as IPromptChoice + // ]; + // this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send commands to set the variables in the terminal? Note if you have an application open in the terminal this may not work."), promptChoices); } } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index a10b86bd847..97d43dd5a6e 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -20,18 +20,21 @@ export interface IEnvironmentVariableMutator { readonly type: EnvironmentVariableMutatorType; } -export interface IEnvironmentVariableCollection { - /** - * All entries in the collection - */ - readonly entries: ReadonlyMap; +export type IEnvironmentVariableCollection = ReadonlyMap; + +/** + * Represents an environment variable collection that results from merging several collections + * together. + */ +export interface IMergedEnvironmentVariableCollection { + readonly entries: ReadonlyMap; /** * Get's additions when compared to another collection. This only gets additions rather than * doing a full diff because we can only reliably add entries to an environment, not remove * them. */ - getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined; + // getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined; /** * Applies this collection to a process environment. @@ -39,6 +42,25 @@ export interface IEnvironmentVariableCollection { applyToProcessEnvironment(env: IProcessEnvironment): void; } +// export interface IEnvironmentVariableCollection { +// /** +// * All entries in the collection +// */ +// readonly entries: ReadonlyMap; + +// /** +// * Get's additions when compared to another collection. This only gets additions rather than +// * doing a full diff because we can only reliably add entries to an environment, not remove +// * them. +// */ +// getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined; + +// /** +// * Applies this collection to a process environment. +// */ +// applyToProcessEnvironment(env: IProcessEnvironment): void; +// } + /** * Tracks and persists environment variable collections as defined by extensions. */ @@ -49,13 +71,13 @@ export interface IEnvironmentVariableService { * Gets a single collection constructed by merging all environment variable collections into * one. */ - readonly mergedCollection: IEnvironmentVariableCollection; + readonly mergedCollection: IMergedEnvironmentVariableCollection; /** * An event that is fired when an extension's environment variable collection changes, the event * provides the new merged collection. */ - onDidChangeCollections: Event; + onDidChangeCollections: Event; /** * Sets an extension's environment variable collection. @@ -67,3 +89,10 @@ export interface IEnvironmentVariableService { */ delete(extensionIdentifier: string): void; } + +/** + * First: Variable + * Second: Value + * Third: Type + */ +export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][]; diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts index 8363da11573..fdf72ee1226 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableCollection.ts @@ -3,53 +3,129 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableCollection, IEnvironmentVariableMutator, EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { IEnvironmentVariableCollection, IEnvironmentVariableMutator, EnvironmentVariableMutatorType, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IProcessEnvironment } from 'vs/base/common/platform'; -export class EnvironmentVariableCollection implements IEnvironmentVariableCollection { - readonly entries: Map; - - constructor( - variables?: string[], - values?: string[], - types?: EnvironmentVariableMutatorType[] - ) { - this.entries = new Map(); - if (variables && values && types) { - if (variables.length !== values.length || variables.length !== types.length) { - throw new Error('Cannot create environment collection from arrays of differing length'); - } - for (let i = 0; i < variables.length; i++) { - this.entries.set(variables[i], { value: values[i], type: types[i] }); - } - } - } +export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { + readonly entries: Map = new Map(); - // TODO: Consider doing a full diff, just marking the environment as stale with no action available? - getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined { - const result = new Map(); - other.entries.forEach((newMutator, variable) => { - const currentMutator = this.entries.get(variable); - if (currentMutator?.type !== newMutator.type || currentMutator.value !== newMutator.value) { - result.set(variable, newMutator); + constructor(...extensionCollections: IEnvironmentVariableCollection[]) { + extensionCollections.forEach(collection => { + const it = collection.entries(); + let next = it.next(); + while (!next.done) { + const variable = next.value[0]; + let entry = this.entries.get(variable); + if (!entry) { + entry = []; + this.entries.set(variable, entry); + } + + // If the first item in the entry is replace ignore any other entries as they would + // just get replaced by this one. + if (entry.length > 0 && entry[0].type === EnvironmentVariableMutatorType.Replace) { + next = it.next(); + continue; + } + + // Mutators get applied in the reverse order than they are created + entry.unshift(next.value[1]); + + next = it.next(); } }); - return result.size === 0 ? undefined : result; } + // TODO: Consider doing a full diff, just marking the environment as stale with no action available? + // getNewAdditions(other: IMergedEnvironmentVariableCollection): ReadonlyMap | undefined { + // const result = new Map(); + // const it = other.entries.entries(); + // let next = it.next(); + // while (!next.done) { + // // TODO: convert to support multiple w/ iterator + // const variable = next.value[0]; + // const currentMutators = this.entries.get(variable); + // const newMutators = next.value[1]; + // newMutators.forEach(newMutator => { + + // }); + // } + + + + // // other.entries.forEach((newMutator, variable) => { + + // // const currentMutator = this.entries.get(variable); + // // if (currentMutator?.type !== newMutator.type || currentMutator.value !== newMutator.value) { + // // result.set(variable, newMutator); + // // } + // // }); + // return result.size === 0 ? undefined : result; + // } + applyToProcessEnvironment(env: IProcessEnvironment): void { - this.entries.forEach((mutator, variable) => { - switch (mutator.type) { - case EnvironmentVariableMutatorType.Append: - env[variable] = (env[variable] || '') + mutator.value; - break; - case EnvironmentVariableMutatorType.Prepend: - env[variable] = mutator.value + (env[variable] || ''); - break; - case EnvironmentVariableMutatorType.Replace: - env[variable] = mutator.value; - break; - } + this.entries.forEach((mutators, variable) => { + mutators.forEach(mutator => { + switch (mutator.type) { + case EnvironmentVariableMutatorType.Append: + env[variable] = (env[variable] || '') + mutator.value; + break; + case EnvironmentVariableMutatorType.Prepend: + env[variable] = mutator.value + (env[variable] || ''); + break; + case EnvironmentVariableMutatorType.Replace: + env[variable] = mutator.value; + break; + } + }); }); } } + +// export class EnvironmentVariableCollection implements IEnvironmentVariableCollection { +// readonly entries: Map; + +// constructor( +// variables?: string[], +// values?: string[], +// types?: EnvironmentVariableMutatorType[] +// ) { +// this.entries = new Map(); +// if (variables && values && types) { +// if (variables.length !== values.length || variables.length !== types.length) { +// throw new Error('Cannot create environment collection from arrays of differing length'); +// } +// for (let i = 0; i < variables.length; i++) { +// this.entries.set(variables[i], { value: values[i], type: types[i] }); +// } +// } +// } + +// // TODO: Consider doing a full diff, just marking the environment as stale with no action available? +// getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined { +// const result = new Map(); +// other.entries.forEach((newMutator, variable) => { +// const currentMutator = this.entries.get(variable); +// if (currentMutator?.type !== newMutator.type || currentMutator.value !== newMutator.value) { +// result.set(variable, newMutator); +// } +// }); +// return result.size === 0 ? undefined : result; +// } + +// applyToProcessEnvironment(env: IProcessEnvironment): void { +// this.entries.forEach((mutator, variable) => { +// switch (mutator.type) { +// case EnvironmentVariableMutatorType.Append: +// env[variable] = (env[variable] || '') + mutator.value; +// break; +// case EnvironmentVariableMutatorType.Prepend: +// env[variable] = mutator.value + (env[variable] || ''); +// break; +// case EnvironmentVariableMutatorType.Replace: +// env[variable] = mutator.value; +// break; +// } +// }); +// } +// } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index ddb1bc53a62..275977eaebf 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -3,21 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEnvironmentVariableService, IEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { IEnvironmentVariableService, IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { Event, Emitter } from 'vs/base/common/event'; import { debounce, throttle } from 'vs/base/common/decorators'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { EnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; const ENVIRONMENT_VARIABLE_COLLECTIONS_KEY = 'terminal.integrated.environmentVariableCollections'; -interface ISerializableEnvironmentVariableCollection { - variables: string[]; - values: string[]; - types: number[]; -} - interface ISerializableExtensionEnvironmentVariableCollection { extensionIdentifier: string, collection: ISerializableEnvironmentVariableCollection @@ -30,10 +25,10 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { _serviceBrand: undefined; private _collections: Map = new Map(); - private _mergedCollection: IEnvironmentVariableCollection; + private _mergedCollection: IMergedEnvironmentVariableCollection; - private readonly _onDidChangeCollections = new Emitter(); - get onDidChangeCollections(): Event { return this._onDidChangeCollections.event; } + private readonly _onDidChangeCollections = new Emitter(); + get onDidChangeCollections(): Event { return this._onDidChangeCollections.event; } constructor( @IExtensionService private _extensionService: IExtensionService, @@ -42,10 +37,7 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { const serializedPersistedCollections = this._storageService.get(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY, StorageScope.WORKSPACE); if (serializedPersistedCollections) { const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = JSON.parse(serializedPersistedCollections); - collectionsJson.forEach(c => { - const extCollection = new EnvironmentVariableCollection(c.collection.variables, c.collection.values, c.collection.types); - this._collections.set(c.extensionIdentifier, extCollection); - }); + collectionsJson.forEach(c => this._collections.set(c.extensionIdentifier, deserializeEnvironmentVariableCollection(c.collection))); console.log('serialized from previous session', this._collections); // Asynchronously invalidate collections where extensions have been uninstalled, this is @@ -59,7 +51,7 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { this._extensionService.onDidChangeExtensions(() => this._invalidateExtensionCollections()); } - get mergedCollection(): IEnvironmentVariableCollection { + get mergedCollection(): IMergedEnvironmentVariableCollection { return this._mergedCollection; } @@ -106,17 +98,21 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { this._onDidChangeCollections.fire(this._mergedCollection); } - private _resolveMergedCollection(): IEnvironmentVariableCollection { - // TODO: Currently this will replace any entry but it's more complex; we need to apply multiple PATH transformations for example - const result = new EnvironmentVariableCollection(); - this._collections.forEach(collection => { - collection.entries.forEach((mutator, variable) => { - if (!result.entries.has(variable)) { - result.entries.set(variable, mutator); - } - }); - }); - return result; + private _resolveMergedCollection(): IMergedEnvironmentVariableCollection { + return new MergedEnvironmentVariableCollection(...[...this._collections.values()]); + // const result = new EnvironmentVariableCollection(); + // this._collections.forEach(collection => { + // const it = collection.entries(); + // let next = it.next(); + // while (!next.done) { + // const variable = next.value[0]; + // if (!result.entries.has(variable)) { + // result.entries.set(variable, next.value[1]); + // } + // next = it.next(); + // } + // }); + // return result; } private async _invalidateExtensionCollections(): Promise { @@ -138,13 +134,3 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { } } } - -function serializeEnvironmentVariableCollection(collection: IEnvironmentVariableCollection): ISerializableEnvironmentVariableCollection { - const entries = [...collection.entries.entries()]; - const result: ISerializableEnvironmentVariableCollection = { - variables: entries.map(e => e[0]), - values: entries.map(e => e[1].value), - types: entries.map(e => e[1].type), - }; - return result; -} diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts new file mode 100644 index 00000000000..47634653bef --- /dev/null +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableShared.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEnvironmentVariableCollection, IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; + +// This file is shared between the renderer and extension host + +export function serializeEnvironmentVariableCollection(collection: IEnvironmentVariableCollection): ISerializableEnvironmentVariableCollection { + return [...collection.entries()]; +} + +export function deserializeEnvironmentVariableCollection( + serializedCollection: ISerializableEnvironmentVariableCollection +): IEnvironmentVariableCollection { + return new Map(serializedCollection); +} diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts index 8b9c96abd87..e2d9b3ec15c 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -3,80 +3,61 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; -import { strictEqual, deepStrictEqual, throws } from 'assert'; +import { deepStrictEqual } from 'assert'; import { EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; +import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; -suite('EnvironmentVariable - EnvironmentVariableCollection', () => { - test('should construct correctly with no arguments', () => { - const c = new EnvironmentVariableCollection(); - strictEqual(c.entries.size, 0); - }); - - test('should construct correctly with 3 arguments', () => { - const c = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); - const keys = [...c.entries.keys()]; - deepStrictEqual(keys, ['A', 'B', 'C']); - deepStrictEqual(c.entries.get('A'), { value: 'a', type: EnvironmentVariableMutatorType.Replace }); - deepStrictEqual(c.entries.get('B'), { value: 'b', type: EnvironmentVariableMutatorType.Append }); - deepStrictEqual(c.entries.get('C'), { value: 'c', type: EnvironmentVariableMutatorType.Prepend }); - }); +suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { + // test('getAdditions should return undefined when there are no new additions', () => { + // const c1 = deserializeEnvironmentVariableCollection({ + // variables: ['A', 'B', 'C'], + // values: ['a', 'b', 'c'], + // types: [1, 2, 3] + // } + // ); + // const c2 = deserializeEnvironmentVariableCollection({ + // variables: ['A', 'B', 'C'], + // values: ['a', 'b', 'c'], + // types: [1, 2, 3] + // } + // ); + // const newAdditions = c1.getNewAdditions(c2); + // strictEqual(newAdditions, undefined); + // }); - test('should throw when ctor arguments have differing length', () => { - throws(() => new EnvironmentVariableCollection(['A'], ['a'], [])); - throws(() => new EnvironmentVariableCollection([], ['a'], [1])); - throws(() => new EnvironmentVariableCollection(['A'], [], [])); - }); - - test('getNewAdditions should return undefined when there are no new additions', () => { - const c1 = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); - const c2 = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); - const newAdditions = c1.getNewAdditions(c2); - strictEqual(newAdditions, undefined); - }); - - test('getNewAdditions should return only new additions in another collection', () => { - const c1 = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); - const c2 = new EnvironmentVariableCollection( - ['B', 'D', 'C'], - ['b', 'd', 'c'], - [2, 1, 3] - ); - const newAdditions = c1.getNewAdditions(c2)!; - const keys = [...newAdditions.keys()]; - deepStrictEqual(keys, ['D']); - deepStrictEqual(newAdditions.get('D'), { value: 'd', type: EnvironmentVariableMutatorType.Replace }); - }); + // test('getNewAdditions should return only new additions in another collection', () => { + // const c1 = deserializeEnvironmentVariableCollection({ + // variables: ['A', 'B', 'C'], + // values: ['a', 'b', 'c'], + // types: [1, 2, 3] + // } + // ); + // const c2 = deserializeEnvironmentVariableCollection({ + // variables: ['B', 'D', 'C'], + // values: ['b', 'd', 'c'], + // types: [2, 1, 3] + // } + // ); + // const newAdditions = c1.getNewAdditions(c2)!; + // const keys = [...newAdditions.keys()]; + // deepStrictEqual(keys, ['D']); + // deepStrictEqual(newAdditions.get('D'), { value: 'd', type: EnvironmentVariableMutatorType.Replace }); + // }); test('applyToProcessEnvironment should apply the collection to an environment', () => { - const c = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); + const merged = new MergedEnvironmentVariableCollection(deserializeEnvironmentVariableCollection([ + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ])); const env: IProcessEnvironment = { A: 'foo', B: 'bar', C: 'baz' }; - c.applyToProcessEnvironment(env); + merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'barb', @@ -85,20 +66,17 @@ suite('EnvironmentVariable - EnvironmentVariableCollection', () => { }); test('applyToProcessEnvironment should apply the collection to environment entries with no values', () => { - const c = new EnvironmentVariableCollection( - ['A', 'B', 'C'], - ['a', 'b', 'c'], - [1, 2, 3] - ); - const env: IProcessEnvironment = { - }; - c.applyToProcessEnvironment(env); + const merged = new MergedEnvironmentVariableCollection(deserializeEnvironmentVariableCollection([ + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ])); + const env: IProcessEnvironment = {}; + merged.applyToProcessEnvironment(env); deepStrictEqual(env, { A: 'a', B: 'b', C: 'c' }); }); - - // TODO: Implement and test multiple mutators applying to one variable }); diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts index 509b750e750..1077b22781e 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -6,8 +6,7 @@ import { deepStrictEqual } from 'assert'; import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService'; -import { EnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection'; -import { EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -34,66 +33,74 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { instantiationService.stub(IStorageService, storageService); instantiationService.stub(IExtensionService, TestExtensionService); instantiationService.stub(IExtensionService, 'onDidChangeExtensions', changeExtensionsEvent.event); + instantiationService.stub(IExtensionService, 'getExtensions', [ + { identifier: { value: 'ext1' } }, + { identifier: { value: 'ext2' } }, + { identifier: { value: 'ext3' } } + ]); environmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService); }); test('should persist collections to the storage service and be able to restore from them', () => { - const collection = new EnvironmentVariableCollection(); - collection.entries.set('A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }); - collection.entries.set('B', { value: 'b', type: EnvironmentVariableMutatorType.Append }); - collection.entries.set('C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }); - environmentVariableService.set('ext', collection); + const collection = new Map(); + collection.set('A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }); + collection.set('B', { value: 'b', type: EnvironmentVariableMutatorType.Append }); + collection.set('C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }); + environmentVariableService.set('ext1', collection); deepStrictEqual([...environmentVariableService.mergedCollection.entries.entries()], [ - ['A', { type: EnvironmentVariableMutatorType.Replace, value: 'a' }], - ['B', { type: EnvironmentVariableMutatorType.Append, value: 'b' }], - ['C', { type: EnvironmentVariableMutatorType.Prepend, value: 'c' }] + ['A', [{ type: EnvironmentVariableMutatorType.Replace, value: 'a' }]], + ['B', [{ type: EnvironmentVariableMutatorType.Append, value: 'b' }]], + ['C', [{ type: EnvironmentVariableMutatorType.Prepend, value: 'c' }]] ]); // Persist with old service, create a new service with the same storage service to verify restore environmentVariableService.persistCollections(); const service2: TestEnvironmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService); deepStrictEqual([...service2.mergedCollection.entries.entries()], [ - ['A', { type: EnvironmentVariableMutatorType.Replace, value: 'a' }], - ['B', { type: EnvironmentVariableMutatorType.Append, value: 'b' }], - ['C', { type: EnvironmentVariableMutatorType.Prepend, value: 'c' }] + ['A', [{ type: EnvironmentVariableMutatorType.Replace, value: 'a' }]], + ['B', [{ type: EnvironmentVariableMutatorType.Append, value: 'b' }]], + ['C', [{ type: EnvironmentVariableMutatorType.Prepend, value: 'c' }]] ]); }); - suite('Merged collection', () => { + suite('mergedCollection', () => { test('should overwrite any other variable with the first extension that replaces', () => { - const collection1 = new EnvironmentVariableCollection(); - const collection2 = new EnvironmentVariableCollection(); - const collection3 = new EnvironmentVariableCollection(); - collection1.entries.set('A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }); - collection1.entries.set('B', { value: 'b1', type: EnvironmentVariableMutatorType.Replace }); - collection2.entries.set('A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }); - collection2.entries.set('B', { value: 'b2', type: EnvironmentVariableMutatorType.Append }); - collection3.entries.set('A', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend }); - collection3.entries.set('B', { value: 'b3', type: EnvironmentVariableMutatorType.Replace }); + const collection1 = new Map(); + const collection2 = new Map(); + const collection3 = new Map(); + collection1.set('A', { value: 'a1', type: EnvironmentVariableMutatorType.Append }); + collection1.set('B', { value: 'b1', type: EnvironmentVariableMutatorType.Replace }); + collection2.set('A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }); + collection2.set('B', { value: 'b2', type: EnvironmentVariableMutatorType.Append }); + collection3.set('A', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend }); + collection3.set('B', { value: 'b3', type: EnvironmentVariableMutatorType.Replace }); environmentVariableService.set('ext1', collection1); environmentVariableService.set('ext2', collection2); environmentVariableService.set('ext3', collection3); deepStrictEqual([...environmentVariableService.mergedCollection.entries.entries()], [ - ['A', { type: EnvironmentVariableMutatorType.Replace, value: 'a1' }], - ['B', { type: EnvironmentVariableMutatorType.Replace, value: 'b1' }] + ['A', [ + { type: EnvironmentVariableMutatorType.Replace, value: 'a2' }, + { type: EnvironmentVariableMutatorType.Append, value: 'a1' } + ]], + ['B', [{ type: EnvironmentVariableMutatorType.Replace, value: 'b1' }]] ]); }); test('should correctly apply the environment values from multiple extension contributions in the correct order', () => { - const collection1 = new EnvironmentVariableCollection(); - const collection2 = new EnvironmentVariableCollection(); - const collection3 = new EnvironmentVariableCollection(); - collection1.entries.set('PATH', { value: ':a1', type: EnvironmentVariableMutatorType.Append }); - collection2.entries.set('PATH', { value: 'a2:', type: EnvironmentVariableMutatorType.Prepend }); - collection3.entries.set('PATH', { value: 'a3', type: EnvironmentVariableMutatorType.Replace }); + const collection1 = new Map(); + const collection2 = new Map(); + const collection3 = new Map(); + collection1.set('A', { value: ':a1', type: EnvironmentVariableMutatorType.Append }); + collection2.set('A', { value: 'a2:', type: EnvironmentVariableMutatorType.Prepend }); + collection3.set('A', { value: 'a3', type: EnvironmentVariableMutatorType.Replace }); environmentVariableService.set('ext1', collection1); environmentVariableService.set('ext2', collection2); environmentVariableService.set('ext3', collection3); // The entries should be ordered in the order they are applied deepStrictEqual([...environmentVariableService.mergedCollection.entries.entries()], [ - ['PATH', [ + ['A', [ { type: EnvironmentVariableMutatorType.Replace, value: 'a3' }, { type: EnvironmentVariableMutatorType.Prepend, value: 'a2:' }, { type: EnvironmentVariableMutatorType.Append, value: ':a1' } diff --git a/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts b/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts new file mode 100644 index 00000000000..1f4e518c58d --- /dev/null +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual } from 'assert'; +import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared'; +import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable'; + +suite('EnvironmentVariable - deserializeEnvironmentVariableCollection', () => { + test('should construct correctly with 3 arguments', () => { + const c = deserializeEnvironmentVariableCollection([ + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ]); + const keys = [...c.keys()]; + deepStrictEqual(keys, ['A', 'B', 'C']); + deepStrictEqual(c.get('A'), { value: 'a', type: EnvironmentVariableMutatorType.Replace }); + deepStrictEqual(c.get('B'), { value: 'b', type: EnvironmentVariableMutatorType.Append }); + deepStrictEqual(c.get('C'), { value: 'c', type: EnvironmentVariableMutatorType.Prepend }); + }); +}); + +suite('EnvironmentVariable - serializeEnvironmentVariableCollection', () => { + test('should correctly serialize the object', () => { + const collection = new Map(); + deepStrictEqual(serializeEnvironmentVariableCollection(collection), []); + collection.set('A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }); + collection.set('B', { value: 'b', type: EnvironmentVariableMutatorType.Append }); + collection.set('C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }); + deepStrictEqual(serializeEnvironmentVariableCollection(collection), [ + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ]); + }); +}); -- GitLab