diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index b76e348e02e..36c1e66c852 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -850,17 +850,17 @@ import { assertNoRpc, poll } from '../utils'; collection.prepend('C', '~c2~'); // Verify get - deepStrictEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }); - deepStrictEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append }); - deepStrictEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }); + deepStrictEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, scope: undefined }); + deepStrictEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append, scope: undefined }); + deepStrictEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, scope: undefined }); // Verify forEach const entries: [string, EnvironmentVariableMutator][] = []; collection.forEach((v, m) => entries.push([v, m])); deepStrictEqual(entries, [ - ['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append }], - ['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }] + ['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace, scope: undefined }], + ['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append, scope: undefined }], + ['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend, scope: undefined }] ]); }); }); diff --git a/src/vs/platform/terminal/common/environmentVariable.ts b/src/vs/platform/terminal/common/environmentVariable.ts index f61231e92d1..f207a85a987 100644 --- a/src/vs/platform/terminal/common/environmentVariable.ts +++ b/src/vs/platform/terminal/common/environmentVariable.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IWorkspaceFolderData } from 'vs/platform/workspace/common/workspace'; export enum EnvironmentVariableMutatorType { Replace = 1, @@ -16,11 +17,17 @@ export enum EnvironmentVariableMutatorType { // // TODO: Do we need a both? // } export interface IEnvironmentVariableMutator { + readonly variable: string; readonly value: string; readonly type: EnvironmentVariableMutatorType; + readonly scope?: EnvironmentVariableScope; // readonly timing?: EnvironmentVariableMutatorTiming; } +export type EnvironmentVariableScope = { + workspaceFolder?: IWorkspaceFolderData; +}; + export interface IEnvironmentVariableCollection { readonly map: ReadonlyMap; } @@ -49,18 +56,21 @@ type VariableResolver = (str: string) => Promise; */ export interface IMergedEnvironmentVariableCollection { readonly collections: ReadonlyMap; - readonly map: ReadonlyMap; - + /** + * Gets the variable map for a given scope. + * @param scope The scope to get the variable map for. If undefined, the global scope is used. + */ + getVariableMap(scope: EnvironmentVariableScope | undefined): Map; /** * Applies this collection to a process environment. * @param variableResolver An optional function to use to resolve variables within the * environment values. */ - applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise; + applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise; /** * Generates a diff of this collection against another. Returns undefined if the collections are * the same. */ - diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined; + diff(other: IMergedEnvironmentVariableCollection, scope: EnvironmentVariableScope | undefined): IMergedEnvironmentVariableCollectionDiff | undefined; } diff --git a/src/vs/platform/terminal/common/environmentVariableCollection.ts b/src/vs/platform/terminal/common/environmentVariableCollection.ts index 29fb4b721fb..c9858f8a494 100644 --- a/src/vs/platform/terminal/common/environmentVariableCollection.ts +++ b/src/vs/platform/terminal/common/environmentVariableCollection.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; -import { EnvironmentVariableMutatorType, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/platform/terminal/common/environmentVariable'; +import { EnvironmentVariableMutatorType, EnvironmentVariableScope, IEnvironmentVariableCollection, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/platform/terminal/common/environmentVariable'; type VariableResolver = (str: string) => Promise; @@ -15,20 +15,21 @@ type VariableResolver = (str: string) => Promise; // ]); export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection { - readonly map: Map = new Map(); + private readonly map: Map = new Map(); constructor( - readonly collections: ReadonlyMap + readonly collections: ReadonlyMap, ) { collections.forEach((collection, extensionIdentifier) => { const it = collection.map.entries(); let next = it.next(); while (!next.done) { - const variable = next.value[0]; - let entry = this.map.get(variable); + const mutator = next.value[1]; + const key = next.value[0]; + let entry = this.map.get(key); if (!entry) { entry = []; - this.map.set(variable, entry); + this.map.set(key, entry); } // If the first item in the entry is replace ignore any other entries as they would @@ -38,26 +39,31 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa continue; } - // Mutators get applied in the reverse order than they are created - const mutator = next.value[1]; - entry.unshift({ + const extensionMutator = { extensionIdentifier, value: mutator.value, - type: mutator.type - }); + type: mutator.type, + scope: mutator.scope, + variable: mutator.variable + }; + if (!extensionMutator.scope) { + delete extensionMutator.scope; // Convenient for tests + } + // Mutators get applied in the reverse order than they are created + entry.unshift(extensionMutator); next = it.next(); } }); } - async applyToProcessEnvironment(env: IProcessEnvironment, variableResolver?: VariableResolver): Promise { + async applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise { let lowerToActualVariableNames: { [lowerKey: string]: string | undefined } | undefined; if (isWindows) { lowerToActualVariableNames = {}; Object.keys(env).forEach(e => lowerToActualVariableNames![e.toLowerCase()] = e); } - for (const [variable, mutators] of this.map) { + for (const [variable, mutators] of this.getVariableMap(scope)) { const actualVariable = isWindows ? lowerToActualVariableNames![variable.toLowerCase()] || variable : variable; for (const mutator of mutators) { const value = variableResolver ? await variableResolver(mutator.value) : mutator.value; @@ -81,14 +87,14 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa } } - diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff | undefined { + diff(other: IMergedEnvironmentVariableCollection, scope: EnvironmentVariableScope | undefined): IMergedEnvironmentVariableCollectionDiff | undefined { const added: Map = new Map(); const changed: Map = new Map(); const removed: Map = new Map(); // Find added - other.map.forEach((otherMutators, variable) => { - const currentMutators = this.map.get(variable); + other.getVariableMap(scope).forEach((otherMutators, variable) => { + const currentMutators = this.getVariableMap(scope).get(variable); const result = getMissingMutatorsFromArray(otherMutators, currentMutators); if (result) { added.set(variable, result); @@ -96,8 +102,8 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa }); // Find removed - this.map.forEach((currentMutators, variable) => { - const otherMutators = other.map.get(variable); + this.getVariableMap(scope).forEach((currentMutators, variable) => { + const otherMutators = other.getVariableMap(scope).get(variable); const result = getMissingMutatorsFromArray(currentMutators, otherMutators); if (result) { removed.set(variable, result); @@ -105,8 +111,8 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa }); // Find changed - this.map.forEach((currentMutators, variable) => { - const otherMutators = other.map.get(variable); + this.getVariableMap(scope).forEach((currentMutators, variable) => { + const otherMutators = other.getVariableMap(scope).get(variable); const result = getChangedMutatorsFromArray(currentMutators, otherMutators); if (result) { changed.set(variable, result); @@ -119,6 +125,33 @@ export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVa return { added, changed, removed }; } + + getVariableMap(scope: EnvironmentVariableScope | undefined): Map { + const result = new Map(); + this.map.forEach((mutators, _key) => { + const filteredMutators = mutators.filter(m => filterScope(m, scope)); + if (filteredMutators.length > 0) { + // All of these mutators are for the same variable because they are in the same scope, hence choose anyone to form a key. + result.set(filteredMutators[0].variable, filteredMutators); + } + }); + return result; + } +} + +function filterScope( + mutator: IExtensionOwnedEnvironmentVariableMutator, + scope: EnvironmentVariableScope | undefined +): boolean { + if (!mutator.scope) { + return true; + } + // If a mutator is scoped to a workspace folder, only apply it if the workspace + // folder matches. + if (mutator.scope.workspaceFolder && scope?.workspaceFolder && mutator.scope.workspaceFolder.index === scope.workspaceFolder.index) { + return true; + } + return false; } function getMissingMutatorsFromArray( @@ -162,7 +195,7 @@ function getChangedMutatorsFromArray( const result: IExtensionOwnedEnvironmentVariableMutator[] = []; current.forEach(mutator => { const otherMutator = otherMutatorExtensions.get(mutator.extensionIdentifier); - if (otherMutator && (mutator.type !== otherMutator.type || mutator.value !== otherMutator.value)) { + if (otherMutator && (mutator.type !== otherMutator.type || mutator.value !== otherMutator.value || mutator.scope?.workspaceFolder?.index !== otherMutator.scope?.workspaceFolder?.index)) { // Return the new result, not the old one result.push(otherMutator); } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 16c4d7dee9f..3b63a459209 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -12,6 +12,7 @@ import { IGetTerminalLayoutInfoArgs, IProcessDetails, ISetTerminalLayoutInfoArgs import { ThemeIcon } from 'vs/base/common/themables'; import { ISerializableEnvironmentVariableCollections } from 'vs/platform/terminal/common/environmentVariable'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; export const terminalTabFocusContextKey = new RawContextKey('terminalTabFocusMode', false, true); @@ -600,6 +601,7 @@ export interface ITerminalProcessOptions { }; windowsEnableConpty: boolean; environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined; + workspaceFolder: IWorkspaceFolder | undefined; } export interface ITerminalEnvironment { diff --git a/src/vs/platform/terminal/node/terminalEnvironment.ts b/src/vs/platform/terminal/node/terminalEnvironment.ts index 10278268668..f973081f9d6 100644 --- a/src/vs/platform/terminal/node/terminalEnvironment.ts +++ b/src/vs/platform/terminal/node/terminalEnvironment.ts @@ -259,7 +259,7 @@ function addEnvMixinPathPrefix(options: ITerminalProcessOptions, envMixin: IProc const merged = new MergedEnvironmentVariableCollection(deserialized); // Get all prepend PATH entries - const pathEntry = merged.map.get('PATH'); + const pathEntry = merged.getVariableMap({ workspaceFolder: options.workspaceFolder }).get('PATH'); const prependToPath: string[] = []; if (pathEntry) { for (const mutator of pathEntry) { diff --git a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts index 101a48aeed2..b25548fbfc1 100644 --- a/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/platform/terminal/test/node/terminalEnvironment.test.ts @@ -11,9 +11,9 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ITerminalProcessOptions } from 'vs/platform/terminal/common/terminal'; import { getShellIntegrationInjection, getWindowsBuildNumber, IShellIntegrationConfigInjection } from 'vs/platform/terminal/node/terminalEnvironment'; -const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false }, windowsEnableConpty: true, environmentVariableCollections: undefined }; -const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false }, windowsEnableConpty: true, environmentVariableCollections: undefined }; -const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false }, windowsEnableConpty: false, environmentVariableCollections: undefined }; +const enabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const disabledProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: false, suggestEnabled: false }, windowsEnableConpty: true, environmentVariableCollections: undefined, workspaceFolder: undefined }; +const winptyProcessOptions: ITerminalProcessOptions = { shellIntegration: { enabled: true, suggestEnabled: false }, windowsEnableConpty: false, environmentVariableCollections: undefined, workspaceFolder: undefined }; const pwshExe = process.platform === 'win32' ? 'pwsh.exe' : 'pwsh'; const repoRoot = process.platform === 'win32' ? process.cwd()[0].toLowerCase() + process.cwd().substring(1) : process.cwd(); const logService = new NullLogService(); diff --git a/src/vs/server/node/remoteTerminalChannel.ts b/src/vs/server/node/remoteTerminalChannel.ts index 753ca363fff..fddf20d605d 100644 --- a/src/vs/server/node/remoteTerminalChannel.ts +++ b/src/vs/server/node/remoteTerminalChannel.ts @@ -31,6 +31,7 @@ import { IServerEnvironmentService } from 'vs/server/node/serverEnvironmentServi import { IProductService } from 'vs/platform/product/common/productService'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { withNullAsUndefined } from 'vs/base/common/types'; class CustomVariableResolver extends AbstractVariableResolverService { constructor( @@ -237,7 +238,8 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel< } const envVariableCollections = new Map(entries); const mergedCollection = new MergedEnvironmentVariableCollection(envVariableCollections); - await mergedCollection.applyToProcessEnvironment(env, variableResolver); + const workspaceFolder = activeWorkspaceFolder ? withNullAsUndefined(activeWorkspaceFolder) : undefined; + await mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } // Fork the process and listen for messages diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index e9f3d90d605..522db605583 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -17,7 +17,7 @@ import { NotSupportedError } from 'vs/base/common/errors'; import { serializeEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableShared'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { generateUuid } from 'vs/base/common/uuid'; -import { ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; import { ICreateContributedTerminalProfileOptions, IProcessReadyEvent, IShellLaunchConfigDto, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, TerminalIcon, TerminalLocation, IProcessProperty, ProcessPropertyType, IProcessPropertyMap } from 'vs/platform/terminal/common/terminal'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; import { ThemeColor } from 'vs/base/common/themables'; @@ -867,7 +867,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I } class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollection { - readonly map: Map = new Map(); + readonly map: Map = new Map(); private _persistent: boolean = true; public get persistent(): boolean { return this._persistent; } @@ -889,45 +889,79 @@ class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollect return this.map.size; } - replace(variable: string, value: string): void { - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace }); + replace(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace, scope }); } - append(variable: string, value: string): void { - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append }); + append(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append, scope }); } - prepend(variable: string, value: string): void { - this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend }); + prepend(variable: string, value: string, scope?: vscode.EnvironmentVariableScope): void { + this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend, scope }); } private _setIfDiffers(variable: string, mutator: vscode.EnvironmentVariableMutator): void { - const current = this.map.get(variable); - if (!current || current.value !== mutator.value || current.type !== mutator.type) { - this.map.set(variable, mutator); + if (!mutator.scope) { + delete (mutator as any).scope; // Convenient for tests + } + const key = this.getKey(variable, mutator.scope); + const current = this.map.get(key); + if (!current || current.value !== mutator.value || current.type !== mutator.type || current.scope?.workspaceFolder?.index !== mutator.scope?.workspaceFolder?.index) { + const key = this.getKey(variable, mutator.scope); + const value: IEnvironmentVariableMutator = { variable, ...mutator }; + this.map.set(key, value); this._onDidChangeCollection.fire(); } } - get(variable: string): vscode.EnvironmentVariableMutator | undefined { - return this.map.get(variable); + get(variable: string, scope?: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableMutator | undefined { + const key = this.getKey(variable, scope); + const value = this.map.get(key); + return value ? convertMutator(value) : undefined; + } + + private getKey(variable: string, scope: vscode.EnvironmentVariableScope | undefined) { + const workspaceKey = this.getWorkspaceKey(scope?.workspaceFolder); + return workspaceKey ? `${variable}:::${workspaceKey}` : variable; + } + + private getWorkspaceKey(workspaceFolder: vscode.WorkspaceFolder | undefined): string | undefined { + return workspaceFolder ? workspaceFolder.uri.toString() : undefined; } forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => any, thisArg?: any): void { - this.map.forEach((value, key) => callback.call(thisArg, key, value, this)); + this.map.forEach((value, _) => callback.call(thisArg, value.variable, convertMutator(value), this)); } [Symbol.iterator](): IterableIterator<[variable: string, mutator: vscode.EnvironmentVariableMutator]> { - return this.map.entries(); + const map: Map = new Map(); + this.map.forEach((mutator, _key) => { + if (mutator.scope) { + // Scoped mutators are not supported via this iterator, as it returns variable as the key which is supposed to be unique. + return; + } + map.set(mutator.variable, convertMutator(mutator)); + }); + return map.entries(); } - delete(variable: string): void { - this.map.delete(variable); + delete(variable: string, scope?: vscode.EnvironmentVariableScope): void { + const key = this.getKey(variable, scope); + this.map.delete(key); this._onDidChangeCollection.fire(); } - clear(): void { - this.map.clear(); + clear(scope?: vscode.EnvironmentVariableScope): void { + if (scope?.workspaceFolder) { + for (const [key, mutator] of this.map) { + if (mutator.scope?.workspaceFolder?.index === scope.workspaceFolder.index) { + this.map.delete(key); + } + } + } else { + this.map.clear(); + } this._onDidChangeCollection.fire(); } } @@ -966,3 +1000,10 @@ function asTerminalIcon(iconPath?: vscode.Uri | { light: vscode.Uri; dark: vscod function asTerminalColor(color?: vscode.ThemeColor): ThemeColor | undefined { return ThemeColor.isThemeColor(color) ? color as ThemeColor : undefined; } + +function convertMutator(mutator: IEnvironmentVariableMutator): vscode.EnvironmentVariableMutator { + const newMutator = { ...mutator }; + newMutator.scope = newMutator.scope ?? undefined; + delete (newMutator as any).variable; + return newMutator as vscode.EnvironmentVariableMutator; +} diff --git a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts index 6d3ae4e867f..66e0ad68d4b 100644 --- a/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts +++ b/src/vs/workbench/contrib/terminal/browser/environmentVariableInfo.ts @@ -8,7 +8,7 @@ import { ITerminalStatus, ITerminalStatusHoverAction, TerminalCommandId } from ' import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { localize } from 'vs/nls'; import { Codicon } from 'vs/base/common/codicons'; -import { IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/platform/terminal/common/environmentVariable'; +import { EnvironmentVariableScope, IExtensionOwnedEnvironmentVariableMutator, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff } from 'vs/platform/terminal/common/environmentVariable'; import { TerminalStatus } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import Severity from 'vs/base/common/severity'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -68,9 +68,9 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl ) { } - private _getInfo(): string { + private _getInfo(scope: EnvironmentVariableScope | undefined): string { const extSet: Set = new Set(); - addExtensionIdentifiers(extSet, this._collection.map.values()); + addExtensionIdentifiers(extSet, this._collection.getVariableMap(scope).values()); let message = localize('extensionEnvironmentContributionInfoActive', "The following extensions have contributed to this terminal's environment:"); message += '\n'; @@ -80,20 +80,20 @@ export class EnvironmentVariableInfoChangesActive implements IEnvironmentVariabl return message; } - private _getActions(): ITerminalStatusHoverAction[] { + private _getActions(scope: EnvironmentVariableScope | undefined): ITerminalStatusHoverAction[] { return [{ label: localize('showEnvironmentContributions', "Show environment contributions"), - run: () => this._commandService.executeCommand(TerminalCommandId.ShowEnvironmentContributions), + run: () => this._commandService.executeCommand(TerminalCommandId.ShowEnvironmentContributions, scope), commandId: TerminalCommandId.ShowEnvironmentContributions }]; } - getStatus(): ITerminalStatus { + getStatus(scope: EnvironmentVariableScope | undefined): ITerminalStatus { return { id: TerminalStatus.EnvironmentVariableInfoChangesActive, severity: Severity.Info, - tooltip: this._getInfo(), - hoverActions: this._getActions() + tooltip: this._getInfo(scope), + hoverActions: this._getActions(scope) }; } } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index ef24696f885..06b11d86c13 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -89,6 +89,7 @@ import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme import { TerminalExtensionsRegistry } from 'vs/workbench/contrib/terminal/browser/terminalExtensions'; import { ResolvedKeybinding } from 'vs/base/common/keybindings'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; +import { getWorkspaceForTerminal } from 'vs/workbench/services/configurationResolver/common/terminalResolver'; const enum Constants { /** @@ -2162,9 +2163,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.relaunch(); return; } - // Re-create statuses - this.statusList.add(info.getStatus()); + const workspaceFolder = getWorkspaceForTerminal(this.shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); + this.statusList.add(info.getStatus({ workspaceFolder })); } async toggleEscapeSequenceLogging(): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 9cb4d00bd79..8b45521762b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -7,7 +7,6 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { formatMessageForTerminal } from 'vs/platform/terminal/common/terminalStrings'; @@ -22,7 +21,7 @@ import { NaiveCwdDetectionCapability } from 'vs/platform/terminal/common/capabil import { TerminalCapabilityStore } from 'vs/platform/terminal/common/capabilities/terminalCapabilityStore'; import { FlowControlConstants, IProcessDataEvent, IProcessProperty, IProcessPropertyMap, IProcessReadyEvent, IReconnectionProperties, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalEnvironment, ITerminalLaunchError, ITerminalProcessOptions, ProcessPropertyType, TerminalSettingId } from 'vs/platform/terminal/common/terminal'; import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EnvironmentVariableInfoChangesActive, EnvironmentVariableInfoStale } from 'vs/workbench/contrib/terminal/browser/environmentVariableInfo'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEnvironmentVariableInfo, IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -39,6 +38,7 @@ import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; import Severity from 'vs/base/common/severity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { getWorkspaceForTerminal } from 'vs/workbench/services/configurationResolver/common/terminalResolver'; const enum ProcessConstants { /** @@ -116,6 +116,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce readonly onProcessExit = this._onProcessExit.event; private readonly _onRestoreCommands = this._register(new Emitter()); readonly onRestoreCommands = this._onRestoreCommands.event; + private _cwdWorkspaceFolder: IWorkspaceFolder | undefined; get persistentProcessId(): number | undefined { return this._process?.id; } get shouldPersist(): boolean { return !!this.reconnectionProperties || (this._process ? this._process.shouldPersist : false); } @@ -146,7 +147,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @INotificationService private readonly _notificationService: INotificationService ) { super(); - + this._cwdWorkspaceFolder = getWorkspaceForTerminal(cwd, this._workspaceContextService, this._historyService); this.ptyProcessReady = this._createPtyProcessReadyPromise(); this.getLatency(); this._ackDataBufferer = new AckDataBufferer(e => this._process?.acknowledgeDataEvent(e)); @@ -239,9 +240,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this.backend = backend; // Create variable resolver - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); - const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - const variableResolver = terminalEnvironment.createVariableResolver(lastActiveWorkspace, await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority), this._configurationResolverService); + const variableResolver = terminalEnvironment.createVariableResolver(this._cwdWorkspaceFolder, await this._terminalProfileResolverService.getEnvironment(this.remoteAuthority), this._configurationResolverService); // resolvedUserHome is needed here as remote resolvers can launch local terminals before // they're connected to the remote. @@ -282,7 +281,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce suggestEnabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationSuggestEnabled), }, windowsEnableConpty: this._configHelper.config.windowsEnableConpty, - environmentVariableCollections: this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined + environmentVariableCollections: this._extEnvironmentVariableCollection?.collections ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined, + workspaceFolder: this._cwdWorkspaceFolder, }; try { newProcess = await backend.createProcess( @@ -409,6 +409,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // Fetch any extension environment additions and apply them private async _resolveEnvironment(backend: ITerminalBackend, variableResolver: terminalEnvironment.VariableResolver | undefined, shellLaunchConfig: IShellLaunchConfig): Promise { + const workspaceFolder = getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); this._configHelper.showRecommendations(shellLaunchConfig); @@ -431,8 +432,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce // info widget. While technically these could differ due to the slight change of a race // condition, the chance is minimal plus the impact on the user is also not that great // if it happens - it's not worth adding plumbing to sync back the resolved collection. - await this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, variableResolver); - if (this._extEnvironmentVariableCollection.map.size > 0) { + await this._extEnvironmentVariableCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); + if (this._extEnvironmentVariableCollection.getVariableMap({ workspaceFolder }).size) { this.environmentVariableInfo = this._instantiationService.createInstance(EnvironmentVariableInfoChangesActive, this._extEnvironmentVariableCollection); this._onEnvironmentVariableInfoChange.fire(this.environmentVariableInfo); } @@ -471,7 +472,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce suggestEnabled: this._configurationService.getValue(TerminalSettingId.ShellIntegrationSuggestEnabled), }, windowsEnableConpty: this._configHelper.config.windowsEnableConpty, - environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined + environmentVariableCollections: this._extEnvironmentVariableCollection ? serializeEnvironmentVariableCollections(this._extEnvironmentVariableCollection.collections) : undefined, + workspaceFolder: this._cwdWorkspaceFolder, }; const shouldPersist = ((this._configurationService.getValue(TaskSettingId.Reconnection) && shellLaunchConfig.reconnectionProperties) || !shellLaunchConfig.isFeatureTerminal) && this._configHelper.config.enablePersistentSessions && !shellLaunchConfig.isTransient; return await backend.createProcess(shellLaunchConfig, initialCwd, cols, rows, this._configHelper.config.unicodeVersion, env, options, shouldPersist); @@ -647,7 +649,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void { - const diff = this._extEnvironmentVariableCollection!.diff(newCollection); + const diff = this._extEnvironmentVariableCollection!.diff(newCollection, { workspaceFolder: this._cwdWorkspaceFolder }); if (diff === undefined) { // If there are no longer differences, remove the stale info indicator if (this.environmentVariableInfo instanceof EnvironmentVariableInfoStale) { diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index c29bfb1b688..6d879f6d20e 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -5,7 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { EnvironmentVariableScope, IEnvironmentVariableCollection, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; import { ITerminalStatus } from 'vs/workbench/contrib/terminal/common/terminal'; export const IEnvironmentVariableService = createDecorator('environmentVariableService'); @@ -51,5 +51,5 @@ export interface IEnvironmentVariableCollectionWithPersistence extends IEnvironm export interface IEnvironmentVariableInfo { readonly requiresAction: boolean; - getStatus(): ITerminalStatus; + getStatus(scope: EnvironmentVariableScope | undefined): ITerminalStatus; } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index 9f68db1d984..dbd39153b54 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -34,6 +34,7 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { @IExtensionService private readonly _extensionService: IExtensionService, @IStorageService private readonly _storageService: IStorageService ) { + this._storageService.remove(TerminalStorageKeys.DeprecatedEnvironmentVariableCollections, StorageScope.WORKSPACE); const serializedPersistedCollections = this._storageService.get(TerminalStorageKeys.EnvironmentVariableCollections, StorageScope.WORKSPACE); if (serializedPersistedCollections) { const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = JSON.parse(serializedPersistedCollections); diff --git a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts index 30f88b0cfa0..521a2cde049 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts @@ -8,7 +8,8 @@ export const enum TerminalStorageKeys { SuggestedRendererType = 'terminal.integrated.suggestedRendererType', TabsListWidthHorizontal = 'tabs-list-width-horizontal', TabsListWidthVertical = 'tabs-list-width-vertical', - EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections', + DeprecatedEnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections', + EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollectionsV2', TerminalBufferState = 'terminal.integrated.bufferState', TerminalLayoutInfo = 'terminal.integrated.layoutInfo', PinnedRecentCommandsPrefix = 'terminal.pinnedRecentCommands', diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts index d3a4b2402e6..cf8cda99141 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalBackend.ts @@ -30,6 +30,7 @@ import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/termi import { IProductService } from 'vs/platform/product/common/productService'; import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { BaseTerminalBackend } from 'vs/workbench/contrib/terminal/browser/baseTerminalBackend'; +import { getWorkspaceForTerminal } from 'vs/workbench/services/configurationResolver/common/terminalResolver'; export class LocalTerminalBackendContribution implements IWorkbenchContribution { constructor( @@ -262,7 +263,8 @@ class LocalTerminalBackend extends BaseTerminalBackend implements ITerminalBacke const baseEnv = await (shellLaunchConfig.useShellEnvironment ? this.getShellEnvironment() : this.getEnvironment()); const env = await terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configurationService.getValue(TerminalSettingId.DetectLocale), baseEnv); if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { - await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, variableResolver); + const workspaceFolder = getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService); + await this._environmentVariableService.mergedCollection.applyToProcessEnvironment(env, { workspaceFolder }, variableResolver); } return env; } 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 3ecd9b962e7..528fb863631 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableCollection.test.ts @@ -8,6 +8,7 @@ import { EnvironmentVariableMutatorType } from 'vs/platform/terminal/common/envi import { IProcessEnvironment, isWindows } from 'vs/base/common/platform'; import { MergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableCollection'; import { deserializeEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariableShared'; +import { URI } from 'vs/base/common/uri'; suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { suite('ctor', () => { @@ -15,31 +16,31 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }], ['ext2', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }], ['ext3', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }], ['ext4', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a4', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a4', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }] ])); - deepStrictEqual([...merged.map.entries()], [ + deepStrictEqual([...merged.getVariableMap(undefined).entries()], [ ['A', [ - { extensionIdentifier: 'ext4', type: EnvironmentVariableMutatorType.Append, value: 'a4' }, - { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Prepend, value: 'a3' }, - { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2' }, - { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'a1' } + { extensionIdentifier: 'ext4', type: EnvironmentVariableMutatorType.Append, value: 'a4', variable: 'A' }, + { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Prepend, value: 'a3', variable: 'A' }, + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2', variable: 'A' }, + { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'a1', variable: 'A' } ]] ]); }); @@ -48,33 +49,101 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }], ['ext2', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }], ['ext3', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a3', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }], ['ext4', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a4', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a4', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }] ])); - deepStrictEqual([...merged.map.entries()], [ + deepStrictEqual([...merged.getVariableMap(undefined).entries()], [ ['A', [ - { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3' }, - { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2' }, - { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'a1' } + { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3', variable: 'A' }, + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2', variable: 'A' }, + { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'a1', variable: 'A' } ]] ], 'The ext4 entry should be removed as it comes after a Replace'); }); + + test('Appropriate workspace scoped entries are returned when querying for a particular workspace folder', () => { + const scope1 = { workspaceFolder: { uri: URI.file('workspace1'), name: 'workspace1', index: 0 } }; + const scope2 = { workspaceFolder: { uri: URI.file('workspace2'), name: 'workspace2', index: 3 } }; + const merged = new MergedEnvironmentVariableCollection(new Map([ + ['ext1', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, scope: scope1, variable: 'A' }] + ]) + }], + ['ext2', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] + ]) + }], + ['ext3', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend, scope: scope2, variable: 'A' }] + ]) + }], + ['ext4', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a4', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] + ]) + }] + ])); + deepStrictEqual([...merged.getVariableMap(scope2).entries()], [ + ['A', [ + { extensionIdentifier: 'ext4', type: EnvironmentVariableMutatorType.Append, value: 'a4', variable: 'A' }, + { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Prepend, value: 'a3', scope: scope2, variable: 'A' }, + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2', variable: 'A' }, + ]] + ]); + }); + + test('Workspace scoped entries are not included when looking for global entries', () => { + const scope1 = { workspaceFolder: { uri: URI.file('workspace1'), name: 'workspace1', index: 0 } }; + const scope2 = { workspaceFolder: { uri: URI.file('workspace2'), name: 'workspace2', index: 3 } }; + const merged = new MergedEnvironmentVariableCollection(new Map([ + ['ext1', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, scope: scope1, variable: 'A' }] + ]) + }], + ['ext2', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] + ]) + }], + ['ext3', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend, scope: scope2, variable: 'A' }] + ]) + }], + ['ext4', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a4', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] + ]) + }] + ])); + deepStrictEqual([...merged.getVariableMap(undefined).entries()], [ + ['A', [ + { extensionIdentifier: 'ext4', type: EnvironmentVariableMutatorType.Append, value: 'a4', variable: 'A' }, + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2', variable: 'A' }, + ]] + ]); + }); + }); suite('applyToProcessEnvironment', () => { @@ -82,9 +151,9 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], - ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }] ]) }] ])); @@ -93,7 +162,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'bar', C: 'baz' }; - await merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env, undefined); deepStrictEqual(env, { A: 'a', B: 'barb', @@ -101,18 +170,43 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { }); }); + test('should apply the appropriate workspace scoped entries to an environment', async () => { + const scope1 = { workspaceFolder: { uri: URI.file('workspace1'), name: 'workspace1', index: 0 } }; + const scope2 = { workspaceFolder: { uri: URI.file('workspace2'), name: 'workspace2', index: 3 } }; + const merged = new MergedEnvironmentVariableCollection(new Map([ + ['ext', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, scope: scope1, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, scope: scope2, variable: 'B' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }] + ]) + }] + ])); + const env: IProcessEnvironment = { + A: 'foo', + B: 'bar', + C: 'baz' + }; + await merged.applyToProcessEnvironment(env, scope1); + deepStrictEqual(env, { + A: 'a', + B: 'bar', // This is not changed because the scope does not match + C: 'cbaz' + }); + }); + test('should apply the collection to environment entries with no values', async () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], - ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }] ]) }] ])); const env: IProcessEnvironment = {}; - await merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env, undefined); deepStrictEqual(env, { A: 'a', B: 'b', @@ -124,9 +218,9 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged = new MergedEnvironmentVariableCollection(new Map([ ['ext', { map: deserializeEnvironmentVariableCollection([ - ['a', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['b', { value: 'b', type: EnvironmentVariableMutatorType.Append }], - ['c', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'a' }], + ['b', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'b' }], + ['c', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'c' }] ]) }] ])); @@ -135,7 +229,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { B: 'B', C: 'C' }; - await merged.applyToProcessEnvironment(env); + await merged.applyToProcessEnvironment(env, undefined); if (isWindows) { deepStrictEqual(env, { A: 'a', @@ -160,18 +254,18 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); - const diff = merged1.diff(merged2); + const diff = merged1.diff(merged2, undefined); strictEqual(diff, undefined); }); test('should generate added diffs from when the first entry is added', () => { @@ -179,16 +273,16 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; strictEqual(diff.changed.size, 0); strictEqual(diff.removed.size, 0); const entries = [...diff.added.entries()]; deepStrictEqual(entries, [ - ['A', [{ extensionIdentifier: 'ext1', value: 'a', type: EnvironmentVariableMutatorType.Replace }]] + ['A', [{ extensionIdentifier: 'ext1', value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }]] ]); }); @@ -196,24 +290,24 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; strictEqual(diff.changed.size, 0); strictEqual(diff.removed.size, 0); const entries = [...diff.added.entries()]; deepStrictEqual(entries, [ - ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Append }]] + ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }]] ]); }); @@ -221,7 +315,7 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }] ])); @@ -229,36 +323,36 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext2', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }], ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; strictEqual(diff.changed.size, 0); strictEqual(diff.removed.size, 0); deepStrictEqual([...diff.added.entries()], [ - ['A', [{ extensionIdentifier: 'ext2', value: 'a2', type: EnvironmentVariableMutatorType.Append }]] + ['A', [{ extensionIdentifier: 'ext2', value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }]] ]); const merged3 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }] ]) }], // This entry should get removed ['ext2', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }] ])); - const diff2 = merged1.diff(merged3)!; + const diff2 = merged1.diff(merged3, undefined)!; strictEqual(diff2.changed.size, 0); strictEqual(diff2.removed.size, 0); deepStrictEqual([...diff.added.entries()], [...diff2.added.entries()], 'Swapping the order of the entries in the other collection should yield the same result'); @@ -268,24 +362,24 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); const merged4 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }], // This entry should get removed as it comes after a replace ['ext2', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Append, variable: 'A' }] ]) }] ])); - const diff = merged1.diff(merged4); + const diff = merged1.diff(merged4, undefined); strictEqual(diff, undefined, 'Replace should ignore any entries after it'); }); @@ -293,23 +387,23 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace, variable: 'B' }] ]) }] ])); const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; strictEqual(diff.changed.size, 0); strictEqual(diff.added.size, 0); deepStrictEqual([...diff.removed.entries()], [ - ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Replace }]] + ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Replace, variable: 'B' }]] ]); }); @@ -317,25 +411,25 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace, variable: 'B' }] ]) }] ])); const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; strictEqual(diff.added.size, 0); strictEqual(diff.removed.size, 0); deepStrictEqual([...diff.changed.entries()], [ - ['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace }]], - ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Append }]] + ['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }]], + ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }]] ]); }); @@ -343,28 +437,57 @@ suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => { const merged1 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Prepend }] + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Prepend, variable: 'B' }] ]) }] ])); const merged2 = new MergedEnvironmentVariableCollection(new Map([ ['ext1', { map: deserializeEnvironmentVariableCollection([ - ['A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }], - ['C', { value: 'c', type: EnvironmentVariableMutatorType.Append }] + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Append, variable: 'C' }] ]) }] ])); - const diff = merged1.diff(merged2)!; + const diff = merged1.diff(merged2, undefined)!; deepStrictEqual([...diff.added.entries()], [ - ['C', [{ extensionIdentifier: 'ext1', value: 'c', type: EnvironmentVariableMutatorType.Append }]], + ['C', [{ extensionIdentifier: 'ext1', value: 'c', type: EnvironmentVariableMutatorType.Append, variable: 'C' }]], ]); deepStrictEqual([...diff.removed.entries()], [ - ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Prepend }]] + ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Prepend, variable: 'B' }]] ]); deepStrictEqual([...diff.changed.entries()], [ - ['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace }]] + ['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }]] + ]); + }); + + test('should only generate workspace specific diffs', () => { + const scope1 = { workspaceFolder: { uri: URI.file('workspace1'), name: 'workspace1', index: 0 } }; + const scope2 = { workspaceFolder: { uri: URI.file('workspace2'), name: 'workspace2', index: 3 } }; + const merged1 = new MergedEnvironmentVariableCollection(new Map([ + ['ext1', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Replace, scope: scope1, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Prepend, variable: 'B' }] + ]) + }] + ])); + const merged2 = new MergedEnvironmentVariableCollection(new Map([ + ['ext1', { + map: deserializeEnvironmentVariableCollection([ + ['A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Replace, scope: scope1, variable: 'A' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Append, scope: scope2, variable: 'C' }] + ]) + }] + ])); + const diff = merged1.diff(merged2, scope1)!; + strictEqual(diff.added.size, 0); + deepStrictEqual([...diff.removed.entries()], [ + ['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Prepend, variable: 'B' }]] + ]); + deepStrictEqual([...diff.changed.entries()], [ + ['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace, scope: scope1, variable: 'A' }]] ]); }); }); 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 e3ac05202f8..c1651e86f9e 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { deepStrictEqual } from 'assert'; -import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestExtensionService, TestHistoryService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService'; import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/platform/terminal/common/environmentVariable'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -12,6 +12,8 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Emitter } from 'vs/base/common/event'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { URI } from 'vs/base/common/uri'; class TestEnvironmentVariableService extends EnvironmentVariableService { persistCollections(): void { this._persistCollections(); } @@ -22,6 +24,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { let instantiationService: TestInstantiationService; let environmentVariableService: TestEnvironmentVariableService; let storageService: TestStorageService; + let historyService: TestHistoryService; let changeExtensionsEvent: Emitter; setup(() => { @@ -30,6 +33,7 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { instantiationService = new TestInstantiationService(); instantiationService.stub(IExtensionService, TestExtensionService); storageService = new TestStorageService(); + historyService = new TestHistoryService(); instantiationService.stub(IStorageService, storageService); instantiationService.stub(IExtensionService, TestExtensionService); instantiationService.stub(IExtensionService, 'onDidChangeExtensions', changeExtensionsEvent.event); @@ -38,29 +42,30 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { { identifier: { value: 'ext2' } }, { identifier: { value: 'ext3' } } ]); + instantiationService.stub(IHistoryService, historyService); environmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService); }); test('should persist collections to the storage service and be able to restore from them', () => { 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 }); + collection.set('A-key', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }); + collection.set('B-key', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }); + collection.set('C-key', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }); environmentVariableService.set('ext1', { map: collection, persistent: true }); - deepStrictEqual([...environmentVariableService.mergedCollection.map.entries()], [ - ['A', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'a' }]], - ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'b' }]], - ['C', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'c' }]] + deepStrictEqual([...environmentVariableService.mergedCollection.getVariableMap(undefined).entries()], [ + ['A', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'a', variable: 'A' }]], + ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'b', variable: 'B' }]], + ['C', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'c', variable: '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.map.entries()], [ - ['A', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'a' }]], - ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'b' }]], - ['C', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'c' }]] + deepStrictEqual([...service2.mergedCollection.getVariableMap(undefined).entries()], [ + ['A', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'a', variable: 'A' }]], + ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'b', variable: 'B' }]], + ['C', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'c', variable: 'C' }]] ]); }); @@ -69,21 +74,21 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { 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 }); + collection1.set('A-key', { value: 'a1', type: EnvironmentVariableMutatorType.Append, variable: 'A' }); + collection1.set('B-key', { value: 'b1', type: EnvironmentVariableMutatorType.Replace, variable: 'B' }); + collection2.set('A-key', { value: 'a2', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }); + collection2.set('B-key', { value: 'b2', type: EnvironmentVariableMutatorType.Append, variable: 'B' }); + collection3.set('A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }); + collection3.set('B-key', { value: 'b3', type: EnvironmentVariableMutatorType.Replace, variable: 'B' }); environmentVariableService.set('ext1', { map: collection1, persistent: true }); environmentVariableService.set('ext2', { map: collection2, persistent: true }); environmentVariableService.set('ext3', { map: collection3, persistent: true }); - deepStrictEqual([...environmentVariableService.mergedCollection.map.entries()], [ + deepStrictEqual([...environmentVariableService.mergedCollection.getVariableMap(undefined).entries()], [ ['A', [ - { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Replace, value: 'a2' }, - { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'a1' } + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Replace, value: 'a2', variable: 'A' }, + { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'a1', variable: 'A' } ]], - ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'b1' }]] + ['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'b1', variable: 'B' }]] ]); }); @@ -91,26 +96,53 @@ suite('EnvironmentVariable - EnvironmentVariableService', () => { 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 }); + collection1.set('A-key', { value: ':a1', type: EnvironmentVariableMutatorType.Append, variable: 'A' }); + collection2.set('A-key', { value: 'a2:', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }); + collection3.set('A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }); environmentVariableService.set('ext1', { map: collection1, persistent: true }); environmentVariableService.set('ext2', { map: collection2, persistent: true }); environmentVariableService.set('ext3', { map: collection3, persistent: true }); // The entries should be ordered in the order they are applied - deepStrictEqual([...environmentVariableService.mergedCollection.map.entries()], [ + deepStrictEqual([...environmentVariableService.mergedCollection.getVariableMap(undefined).entries()], [ ['A', [ - { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3' }, - { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Prepend, value: 'a2:' }, - { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: ':a1' } + { extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3', variable: 'A' }, + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Prepend, value: 'a2:', variable: 'A' }, + { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: ':a1', variable: 'A' } ]] ]); // Verify the entries get applied to the environment as expected const env: IProcessEnvironment = { A: 'foo' }; - await environmentVariableService.mergedCollection.applyToProcessEnvironment(env); + await environmentVariableService.mergedCollection.applyToProcessEnvironment(env, undefined); deepStrictEqual(env, { A: 'a2:a3:a1' }); }); + + test('should correctly apply the workspace specific environment values from multiple extension contributions in the correct order', async () => { + const scope1 = { workspaceFolder: { uri: URI.file('workspace1'), name: 'workspace1', index: 0 } }; + const scope2 = { workspaceFolder: { uri: URI.file('workspace2'), name: 'workspace2', index: 3 } }; + const collection1 = new Map(); + const collection2 = new Map(); + const collection3 = new Map(); + collection1.set('A-key', { value: ':a1', type: EnvironmentVariableMutatorType.Append, scope: scope1, variable: 'A' }); + collection2.set('A-key', { value: 'a2:', type: EnvironmentVariableMutatorType.Prepend, variable: 'A' }); + collection3.set('A-key', { value: 'a3', type: EnvironmentVariableMutatorType.Replace, scope: scope2, variable: 'A' }); + environmentVariableService.set('ext1', { map: collection1, persistent: true }); + environmentVariableService.set('ext2', { map: collection2, persistent: true }); + environmentVariableService.set('ext3', { map: collection3, persistent: true }); + + // The entries should be ordered in the order they are applied + deepStrictEqual([...environmentVariableService.mergedCollection.getVariableMap(scope1).entries()], [ + ['A', [ + { extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Prepend, value: 'a2:', variable: 'A' }, + { extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: ':a1', scope: scope1, variable: 'A' } + ]] + ]); + + // Verify the entries get applied to the environment as expected + const env: IProcessEnvironment = { A: 'foo' }; + await environmentVariableService.mergedCollection.applyToProcessEnvironment(env, scope1); + deepStrictEqual(env, { A: 'a2:foo: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 index cb96b0b8bb6..d3fb2177556 100644 --- a/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/environmentVariableShared.test.ts @@ -10,15 +10,15 @@ import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/ 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 }] + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }] ]); 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 }); + deepStrictEqual(c.get('A'), { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }); + deepStrictEqual(c.get('B'), { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }); + deepStrictEqual(c.get('C'), { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }); }); }); @@ -26,13 +26,13 @@ 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 }); + collection.set('A', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }); + collection.set('B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }); + collection.set('C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }); deepStrictEqual(serializeEnvironmentVariableCollection(collection), [ - ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }], - ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }], - ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }] + ['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace, variable: 'A' }], + ['B', { value: 'b', type: EnvironmentVariableMutatorType.Append, variable: 'B' }], + ['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend, variable: 'C' }] ]); }); }); diff --git a/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts b/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts index 89767e2bbdc..fadbd79ae29 100644 --- a/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/environmentChanges/browser/terminal.environmentChanges.contribution.ts @@ -6,7 +6,7 @@ import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; -import { EnvironmentVariableMutatorType, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; +import { EnvironmentVariableMutatorType, EnvironmentVariableScope, IEnvironmentVariableMutator, IMergedEnvironmentVariableCollection } from 'vs/platform/terminal/common/environmentVariable'; import { registerActiveInstanceAction } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -16,7 +16,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic registerActiveInstanceAction({ id: TerminalCommandId.ShowEnvironmentContributions, title: { value: localize('workbench.action.terminal.showEnvironmentContributions', "Show Environment Contributions"), original: 'Show Environment Contributions' }, - run: async (activeInstance, c, accessor) => { + run: async (activeInstance, c, accessor, scope) => { const collection = activeInstance.extEnvironmentVariableCollection; if (collection) { const editorService = accessor.get(IEditorService); @@ -24,7 +24,7 @@ registerActiveInstanceAction({ resource: URI.from({ scheme: Schemas.untitled }), - contents: describeEnvironmentChanges(collection), + contents: describeEnvironmentChanges(collection, scope as EnvironmentVariableScope | undefined), languageId: 'markdown' }); } @@ -32,18 +32,35 @@ registerActiveInstanceAction({ }); -function describeEnvironmentChanges(collection: IMergedEnvironmentVariableCollection): string { +function describeEnvironmentChanges(collection: IMergedEnvironmentVariableCollection, scope: EnvironmentVariableScope | undefined): string { let content = `# ${localize('envChanges', 'Terminal Environment Changes')}`; for (const [ext, coll] of collection.collections) { content += `\n\n## ${localize('extension', 'Extension: {0}', ext)}`; content += '\n'; - for (const [variable, mutator] of coll.map.entries()) { - content += `\n- \`${mutatorTypeLabel(mutator.type, mutator.value, variable)}\``; + for (const [_, mutator] of coll.map.entries()) { + if (filterScope(mutator, scope) === false) { + continue; + } + content += `\n- \`${mutatorTypeLabel(mutator.type, mutator.value, mutator.variable)}\``; } } return content; } +function filterScope( + mutator: IEnvironmentVariableMutator, + scope: EnvironmentVariableScope | undefined +): boolean { + if (!mutator.scope) { + return true; + } + // Only mutators which are applicable on the relevant workspace should be shown. + if (mutator.scope.workspaceFolder && scope?.workspaceFolder && mutator.scope.workspaceFolder.index === scope.workspaceFolder.index) { + return true; + } + return false; +} + function mutatorTypeLabel(type: EnvironmentVariableMutatorType, value: string, variable: string): string { switch (type) { case EnvironmentVariableMutatorType.Prepend: return `${variable}=${value}\${env:${variable}}`; diff --git a/src/vs/workbench/services/configurationResolver/common/terminalResolver.ts b/src/vs/workbench/services/configurationResolver/common/terminalResolver.ts new file mode 100644 index 00000000000..f6f11802790 --- /dev/null +++ b/src/vs/workbench/services/configurationResolver/common/terminalResolver.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { withNullAsUndefined } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; + +export function getWorkspaceForTerminal(cwd: URI | string | undefined, workspaceContextService: IWorkspaceContextService, historyService: IHistoryService): IWorkspaceFolder | undefined { + const cwdUri = typeof cwd === 'string' ? URI.parse(cwd) : cwd; + let workspaceFolder = cwdUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(cwdUri)) : undefined; + if (!workspaceFolder) { + // fallback to last active workspace if cwd is not available or it is not in workspace + // TOOD: last active workspace is known to be unreliable, we should remove this fallback eventually + const activeWorkspaceRootUri = historyService.getLastActiveWorkspaceRoot(); + workspaceFolder = activeWorkspaceRootUri ? withNullAsUndefined(workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; + } + return workspaceFolder; +} diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 8739a178f4e..4af35783b55 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -31,6 +31,7 @@ export const allApiProposals = Object.freeze({ dropMetadata: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.dropMetadata.d.ts', editSessionIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', + envCollectionWorkspace: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts', envShellEvent: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.envShellEvent.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index e1398daa91d..d1e44f9fbce 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -18,12 +18,15 @@ import { NullExtensionService } from 'vs/workbench/services/extensions/common/ex import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation, IStoredFileWorkingCopySaveParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; -import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { ISaveOptions, IRevertOptions, SaveReason, GroupIdentifier } from 'vs/workbench/common/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import product from 'vs/platform/product/common/product'; import { IActivity, IActivityService } from 'vs/workbench/services/activity/common/activity'; import { IStoredFileWorkingCopySaveEvent } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy'; import { AbstractLoggerService, ILogger, LogLevel, NullLogger } from 'vs/platform/log/common/log'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { EditorInput } from 'vs/workbench/common/editor/editorInput'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; export class TestLoggerService extends AbstractLoggerService { constructor(logsHome?: URI) { @@ -140,6 +143,27 @@ export class TestStorageService extends InMemoryStorageService { } } +export class TestHistoryService implements IHistoryService { + + declare readonly _serviceBrand: undefined; + + constructor(private root?: URI) { } + + async reopenLastClosedEditor(): Promise { } + async goForward(): Promise { } + async goBack(): Promise { } + async goPrevious(): Promise { } + async goLast(): Promise { } + removeFromHistory(_input: EditorInput | IResourceEditorInput): void { } + clear(): void { } + clearRecentlyOpened(): void { } + getHistory(): readonly (EditorInput | IResourceEditorInput)[] { return []; } + async openNextRecentlyUsedEditor(group?: GroupIdentifier): Promise { } + async openPreviouslyUsedEditor(group?: GroupIdentifier): Promise { } + getLastActiveWorkspaceRoot(_schemeFilter: string): URI | undefined { return this.root; } + getLastActiveFile(_schemeFilter: string): URI | undefined { return undefined; } +} + export class TestWorkingCopy extends Disposable implements IWorkingCopy { private readonly _onDidChangeDirty = this._register(new Emitter()); diff --git a/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts new file mode 100644 index 00000000000..202782232fb --- /dev/null +++ b/src/vscode-dts/vscode.proposed.envCollectionWorkspace.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/171173 + + export interface EnvironmentVariableMutator { + readonly type: EnvironmentVariableMutatorType; + readonly value: string; + readonly scope: EnvironmentVariableScope | undefined; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + replace(variable: string, value: string, scope?: EnvironmentVariableScope): void; + append(variable: string, value: string, scope?: EnvironmentVariableScope): void; + prepend(variable: string, value: string, scope?: EnvironmentVariableScope): void; + get(variable: string, scope?: EnvironmentVariableScope): EnvironmentVariableMutator | undefined; + delete(variable: string, scope?: EnvironmentVariableScope): void; + clear(scope?: EnvironmentVariableScope): void; + } + + export type EnvironmentVariableScope = { + /** + * The workspace folder to which this collection applies to. If unspecified, collection applies to all workspace folders. + */ + workspaceFolder?: WorkspaceFolder; + }; +}