Merge pull request #179323 from microsoft/kartik/terminal

Add proposed API to specific workspace-specific environment variables via EnvironmentVariableCollection
This commit is contained in:
Kartik Raj
2023-04-20 15:00:29 -07:00
committed by GitHub
23 changed files with 545 additions and 201 deletions

View File

@@ -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 }]
]);
});
});

View File

@@ -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<string, IEnvironmentVariableMutator>;
}
@@ -49,18 +56,21 @@ type VariableResolver = (str: string) => Promise<string>;
*/
export interface IMergedEnvironmentVariableCollection {
readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>;
readonly map: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
/**
* 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<string, IExtensionOwnedEnvironmentVariableMutator[]>;
/**
* 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<void>;
applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise<void>;
/**
* 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;
}

View File

@@ -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<string>;
@@ -15,20 +15,21 @@ type VariableResolver = (str: string) => Promise<string>;
// ]);
export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection {
readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
private readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
constructor(
readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>
readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>,
) {
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<void> {
async applyToProcessEnvironment(env: IProcessEnvironment, scope: EnvironmentVariableScope | undefined, variableResolver?: VariableResolver): Promise<void> {
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<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
const changed: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
const removed: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = 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<string, IExtensionOwnedEnvironmentVariableMutator[]> {
const result = new Map<string, IExtensionOwnedEnvironmentVariableMutator[]>();
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);
}

View File

@@ -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<boolean>('terminalTabFocusMode', false, true);
@@ -600,6 +601,7 @@ export interface ITerminalProcessOptions {
};
windowsEnableConpty: boolean;
environmentVariableCollections: ISerializableEnvironmentVariableCollections | undefined;
workspaceFolder: IWorkspaceFolder | undefined;
}
export interface ITerminalEnvironment {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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<string, IEnvironmentVariableCollection>(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

View File

@@ -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<string, vscode.EnvironmentVariableMutator> = new Map();
readonly map: Map<string, IEnvironmentVariableMutator> = 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<string, vscode.EnvironmentVariableMutator> = 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;
}

View File

@@ -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<string> = 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)
};
}
}

View File

@@ -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<boolean> {

View File

@@ -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<ISerializedCommandDetectionCapability>());
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<IProcessEnvironment> {
const workspaceFolder = getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService);
const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux');
const envFromConfigValue = this._configurationService.getValue<ITerminalEnvironment | undefined>(`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) {

View File

@@ -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<IEnvironmentVariableService>('environmentVariableService');
@@ -51,5 +51,5 @@ export interface IEnvironmentVariableCollectionWithPersistence extends IEnvironm
export interface IEnvironmentVariableInfo {
readonly requiresAction: boolean;
getStatus(): ITerminalStatus;
getStatus(scope: EnvironmentVariableScope | undefined): ITerminalStatus;
}

View File

@@ -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);

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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' }]]
]);
});
});

View File

@@ -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<void>;
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<string, IEnvironmentVariableMutator>();
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<string, IEnvironmentVariableMutator>();
const collection2 = new Map<string, IEnvironmentVariableMutator>();
const collection3 = new Map<string, IEnvironmentVariableMutator>();
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<string, IEnvironmentVariableMutator>();
const collection2 = new Map<string, IEnvironmentVariableMutator>();
const collection3 = new Map<string, IEnvironmentVariableMutator>();
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<string, IEnvironmentVariableMutator>();
const collection2 = new Map<string, IEnvironmentVariableMutator>();
const collection3 = new Map<string, IEnvironmentVariableMutator>();
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' });
});
});
});

View File

@@ -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<string, IEnvironmentVariableMutator>();
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' }]
]);
});
});

View File

@@ -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}}`;

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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<void> { }
async goForward(): Promise<void> { }
async goBack(): Promise<void> { }
async goPrevious(): Promise<void> { }
async goLast(): Promise<void> { }
removeFromHistory(_input: EditorInput | IResourceEditorInput): void { }
clear(): void { }
clearRecentlyOpened(): void { }
getHistory(): readonly (EditorInput | IResourceEditorInput)[] { return []; }
async openNextRecentlyUsedEditor(group?: GroupIdentifier): Promise<void> { }
async openPreviouslyUsedEditor(group?: GroupIdentifier): Promise<void> { }
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<void>());

View File

@@ -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;
};
}