未验证 提交 3367ea60 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #93505 from microsoft/tyriar/46696_terminal_env

Allow extensions to contribute to terminal environments
......@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env } from 'vscode';
import { window, Pseudoterminal, EventEmitter, TerminalDimensions, workspace, ConfigurationTarget, Disposable, UIKind, env, EnvironmentVariableMutatorType, EnvironmentVariableMutator } from 'vscode';
import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert';
// TODO@Daniel flaky tests (https://github.com/microsoft/vscode/issues/92826)
......@@ -533,5 +533,203 @@ import { doesNotThrow, equal, ok, deepEqual, throws } from 'assert';
}
});
});
suite('getEnvironmentVariableCollection', () => {
test('should have collection variables apply to terminals immediately after setting', (done) => {
// Text to match on before passing the test
const expectedText = [
'~a2~',
'b1~b2~',
'~c2~c1'
];
disposables.push(window.onDidWriteTerminalData(e => {
try {
equal(terminal, e.terminal);
} catch (e) {
done(e);
}
console.log('Terminal data: ' + e.data);
// Multiple expected could show up in the same data event
while (expectedText.length > 0 && e.data.indexOf(expectedText[0]) >= 0) {
expectedText.shift();
// Check if all string are found, if so finish the test
if (expectedText.length === 0) {
disposables.push(window.onDidCloseTerminal(() => done()));
terminal.dispose();
}
}
}));
const collection = window.getEnvironmentVariableCollection();
disposables.push(collection);
collection.replace('A', '~a2~');
collection.append('B', '~b2~');
collection.prepend('C', '~c2~');
const terminal = window.createTerminal({
env: {
A: 'a1',
B: 'b1',
C: 'c1'
}
});
// Run both PowerShell and sh commands, errors don't matter we're just looking for
// the correct output
terminal.sendText('$env:A');
terminal.sendText('echo $A');
terminal.sendText('$env:B');
terminal.sendText('echo $B');
terminal.sendText('$env:C');
terminal.sendText('echo $C');
});
test('should have collection variables apply to environment variables that don\'t exist', (done) => {
// Text to match on before passing the test
const expectedText = [
'~a2~',
'~b2~',
'~c2~'
];
disposables.push(window.onDidWriteTerminalData(e => {
try {
equal(terminal, e.terminal);
} catch (e) {
done(e);
}
console.log('Terminal data: ' + e.data);
// Multiple expected could show up in the same data event
while (expectedText.length > 0 && e.data.indexOf(expectedText[0]) >= 0) {
expectedText.shift();
// Check if all string are found, if so finish the test
if (expectedText.length === 0) {
disposables.push(window.onDidCloseTerminal(() => done()));
terminal.dispose();
}
}
}));
const collection = window.getEnvironmentVariableCollection();
disposables.push(collection);
collection.replace('A', '~a2~');
collection.append('B', '~b2~');
collection.prepend('C', '~c2~');
const terminal = window.createTerminal({
env: {
A: null,
B: null,
C: null
}
});
// Run both PowerShell and sh commands, errors don't matter we're just looking for
// the correct output
terminal.sendText('$env:A');
terminal.sendText('echo $A');
terminal.sendText('$env:B');
terminal.sendText('echo $B');
terminal.sendText('$env:C');
terminal.sendText('echo $C');
});
test('should respect clearing entries', (done) => {
// Text to match on before passing the test
const expectedText = [
'~a1~',
'~b1~'
];
disposables.push(window.onDidWriteTerminalData(e => {
try {
equal(terminal, e.terminal);
} catch (e) {
done(e);
}
// Multiple expected could show up in the same data event
while (expectedText.length > 0 && e.data.indexOf(expectedText[0]) >= 0) {
expectedText.shift();
// Check if all string are found, if so finish the test
if (expectedText.length === 0) {
disposables.push(window.onDidCloseTerminal(() => done()));
terminal.dispose();
}
}
}));
const collection = window.getEnvironmentVariableCollection();
disposables.push(collection);
collection.replace('A', '~a2~');
collection.replace('B', '~a2~');
collection.clear();
const terminal = window.createTerminal({
env: {
A: '~a1~',
B: '~b1~'
}
});
// Run both PowerShell and sh commands, errors don't matter we're just looking for
// the correct output
terminal.sendText('$env:A');
terminal.sendText('echo $A');
terminal.sendText('$env:B');
terminal.sendText('echo $B');
});
test('should respect deleting entries', (done) => {
// Text to match on before passing the test
const expectedText = [
'~a1~',
'~b2~'
];
disposables.push(window.onDidWriteTerminalData(e => {
try {
equal(terminal, e.terminal);
} catch (e) {
done(e);
}
// Multiple expected could show up in the same data event
while (expectedText.length > 0 && e.data.indexOf(expectedText[0]) >= 0) {
expectedText.shift();
// Check if all string are found, if so finish the test
if (expectedText.length === 0) {
disposables.push(window.onDidCloseTerminal(() => done()));
terminal.dispose();
}
}
}));
const collection = window.getEnvironmentVariableCollection();
disposables.push(collection);
collection.replace('A', '~a2~');
collection.replace('B', '~b2~');
collection.delete('A');
const terminal = window.createTerminal({
env: {
A: '~a1~',
B: '~b2~'
}
});
// Run both PowerShell and sh commands, errors don't matter we're just looking for
// the correct output
terminal.sendText('$env:A');
terminal.sendText('echo $A');
terminal.sendText('$env:B');
terminal.sendText('echo $B');
});
test('get and forEach should work', () => {
const collection = window.getEnvironmentVariableCollection();
disposables.push(collection);
collection.replace('A', '~a2~');
collection.append('B', '~b2~');
collection.prepend('C', '~c2~');
// Verify get
deepEqual(collection.get('A'), { value: '~a2~', type: EnvironmentVariableMutatorType.Replace });
deepEqual(collection.get('B'), { value: '~b2~', type: EnvironmentVariableMutatorType.Append });
deepEqual(collection.get('C'), { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend });
// Verify forEach
const entries: [string, EnvironmentVariableMutator][] = [];
collection.forEach((v, m) => entries.push([v, m]));
deepEqual(entries, [
['A', { value: '~a2~', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: '~b2~', type: EnvironmentVariableMutatorType.Append }],
['C', { value: '~c2~', type: EnvironmentVariableMutatorType.Prepend }]
]);
});
});
});
});
......@@ -1093,7 +1093,7 @@ declare module 'vscode' {
readonly dimensions: TerminalDimensions;
}
namespace window {
export namespace window {
/**
* An event which fires when the [dimensions](#Terminal.dimensions) of the terminal change.
*/
......@@ -1114,19 +1114,125 @@ declare module 'vscode' {
//#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606
export namespace window {
/**
* Register a [TerminalLinkHandler](#TerminalLinkHandler) that can be used to intercept and
* handle links that are activated within terminals.
*/
export function registerTerminalLinkHandler(handler: TerminalLinkHandler): Disposable;
}
export interface TerminalLinkHandler {
/**
* @return true when the link was handled (and should not be considered by
* other providers including the default), false when the link was not handled.
* Handles a link that is activated within the terminal.
*
* @return Whether the link was handled, the link was handled this link will not be
* considered by any other extension or by the default built-in link handler.
*/
handleLink(terminal: Terminal, link: string): ProviderResult<boolean>;
}
//#endregion
//#region Contribute to terminal environment https://github.com/microsoft/vscode/issues/46696
export enum EnvironmentVariableMutatorType {
/**
* Replace the variable's existing value.
*/
Replace = 1,
/**
* Append to the end of the variable's existing value.
*/
Append = 2,
/**
* Prepend to the start of the variable's existing value.
*/
Prepend = 3
}
export interface EnvironmentVariableMutator {
/**
* The type of mutation that will occur to the variable.
*/
readonly type: EnvironmentVariableMutatorType;
/**
* The value to use for the variable.
*/
readonly value: string;
}
/**
* A collection of mutations that an extension can apply to a process environment.
*/
export interface EnvironmentVariableCollection {
/**
* Replace an environment variable with a value.
*
* Note that an extension can only make a single change to any one variable, so this will
* overwrite any previous calls to replace, append or prepend.
*/
replace(variable: string, value: string): void;
/**
* Append a value to an environment variable.
*
* Note that an extension can only make a single change to any one variable, so this will
* overwrite any previous calls to replace, append or prepend.
*/
append(variable: string, value: string): void;
/**
* Prepend a value to an environment variable.
*
* Note that an extension can only make a single change to any one variable, so this will
* overwrite any previous calls to replace, append or prepend.
*/
prepend(variable: string, value: string): void;
/**
* Gets the mutator that this collection applies to a variable, if any.
*/
get(variable: string): EnvironmentVariableMutator | undefined;
/**
* Iterate over each mutator in this collection.
*/
forEach(callback: (variable: string, mutator: EnvironmentVariableMutator, collection: EnvironmentVariableCollection) => any, thisArg?: any): void;
/**
* Deletes this collection's mutator for a variable.
*/
delete(variable: string): void;
/**
* Clears all mutators from this collection.
*/
clear(): void;
/**
* Disposes the collection, if the collection was persisted it will no longer be retained
* across reloads.
*/
dispose(): void;
}
export namespace window {
/**
* Creates or returns the extension's environment variable collection for this workspace,
* enabling changes to be applied to terminal environment variables.
*
* @param persistent Whether the collection should be cached for the workspace and applied
* to the terminal across window reloads. When true the collection will be active
* immediately such when the window reloads. Additionally, this API will return the cached
* version if it exists. The collection will be invalidated when the extension is
* uninstalled or when the collection is disposed. Defaults to false.
*/
export function getEnvironmentVariableCollection(persistent?: boolean): EnvironmentVariableCollection;
}
//#endregion
//#region Joh -> exclusive document filters
export interface DocumentFilter {
......
......@@ -13,6 +13,8 @@ import { ITerminalInstanceService, ITerminalService, ITerminalInstance, ITermina
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
......@@ -29,8 +31,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
extHostContext: IExtHostContext,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalInstanceService readonly terminalInstanceService: ITerminalInstanceService,
@IRemoteAgentService readonly _remoteAgentService: IRemoteAgentService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
this._remoteAuthority = extHostContext.remoteAuthority;
......@@ -71,6 +74,13 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
if (activeInstance) {
this._proxy.$acceptActiveTerminalChanged(activeInstance.id);
}
if (this._environmentVariableService.collections.size > 0) {
const collectionAsArray = [...this._environmentVariableService.collections.entries()];
const serializedCollections: [string, ISerializableEnvironmentVariableCollection][] = collectionAsArray.map(e => {
return [e[0], serializeEnvironmentVariableCollection(e[1].map)];
});
this._proxy.$initEnvironmentVariableCollections(serializedCollections);
}
this._terminalService.extHostReady(extHostContext.remoteAuthority);
}
......@@ -346,6 +356,18 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
return terminal;
}
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void {
if (collection) {
const translatedCollection = {
persistent,
map: deserializeEnvironmentVariableCollection(collection)
};
this._environmentVariableService.set(extensionIdentifier, translatedCollection);
} else {
this._environmentVariableService.delete(extensionIdentifier);
}
}
}
/**
......
......@@ -487,6 +487,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension);
return extHostTerminalService.onDidWriteTerminalData(listener, thisArg, disposables);
},
getEnvironmentVariableCollection(persistent?: boolean): vscode.EnvironmentVariableCollection {
checkProposedApiEnabled(extension);
return extHostTerminalService.getEnvironmentVariableCollection(extension, persistent);
},
get state() {
return extHostWindow.state;
},
......@@ -957,6 +961,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
DocumentLink: extHostTypes.DocumentLink,
DocumentSymbol: extHostTypes.DocumentSymbol,
EndOfLine: extHostTypes.EndOfLine,
EnvironmentVariableMutatorType: extHostTypes.EnvironmentVariableMutatorType,
EvaluatableExpression: extHostTypes.EvaluatableExpression,
EventEmitter: Emitter,
ExtensionKind: extHostTypes.ExtensionKind,
......
......@@ -54,6 +54,7 @@ import { revive } from 'vs/base/common/marshalling';
import { INotebookMimeTypeSelector, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { Dto } from 'vs/base/common/types';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
......@@ -437,6 +438,7 @@ export interface MainThreadTerminalServiceShape extends IDisposable {
$stopSendingDataEvents(): void;
$startHandlingLinks(): void;
$stopHandlingLinks(): void;
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void;
// Process
$sendProcessTitle(terminalId: number, title: string): void;
......@@ -1387,6 +1389,7 @@ export interface ExtHostTerminalServiceShape {
$getAvailableShells(): Promise<IShellDefinitionDto[]>;
$getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto>;
$handleLink(id: number, link: string): Promise<boolean>;
$initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void;
}
export interface ExtHostSCMShape {
......
......@@ -14,7 +14,9 @@ import { timeout } from 'vs/base/common/async';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Disposable as VSCodeDisposable } from './extHostTypes';
import { Disposable as VSCodeDisposable, EnvironmentVariableMutatorType } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
......@@ -35,7 +37,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void;
getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string;
getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string;
registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable
registerLinkHandler(handler: vscode.TerminalLinkHandler): vscode.Disposable;
getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection;
}
export const IExtHostTerminalService = createDecorator<IExtHostTerminalService>('IExtHostTerminalService');
......@@ -333,6 +336,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
public abstract $getAvailableShells(): Promise<IShellDefinitionDto[]>;
public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise<IShellAndArgsDto>;
public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void;
public abstract getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection;
public abstract $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void;
public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, options, options.name);
......@@ -637,6 +642,67 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
}
}
export class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollection {
readonly map: Map<string, vscode.EnvironmentVariableMutator> = new Map();
protected readonly _onDidChangeCollection: Emitter<void> = new Emitter<void>();
get onDidChangeCollection(): Event<void> { return this._onDidChangeCollection && this._onDidChangeCollection.event; }
constructor(
readonly persistent: boolean,
serialized?: ISerializableEnvironmentVariableCollection
) {
this.map = new Map(serialized);
}
get size(): number {
return this.map.size;
}
replace(variable: string, value: string): void {
this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Replace });
}
append(variable: string, value: string): void {
this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Append });
}
prepend(variable: string, value: string): void {
this._setIfDiffers(variable, { value, type: EnvironmentVariableMutatorType.Prepend });
}
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);
this._onDidChangeCollection.fire();
}
}
get(variable: string): vscode.EnvironmentVariableMutator | undefined {
return this.map.get(variable);
}
forEach(callback: (variable: string, mutator: vscode.EnvironmentVariableMutator, collection: vscode.EnvironmentVariableCollection) => any, thisArg?: any): void {
this.map.forEach((value, key) => callback(key, value, this));
}
delete(variable: string): void {
this.map.delete(variable);
this._onDidChangeCollection.fire();
}
clear(): void {
this.map.clear();
this._onDidChangeCollection.fire();
}
dispose(): void {
this.map.clear();
this._onDidChangeCollection.fire();
}
}
export class WorkerExtHostTerminalService extends BaseExtHostTerminalService {
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal {
throw new Error('Not implemented');
......@@ -669,4 +735,13 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService {
public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void {
// No-op for web worker ext host as workspace permissions aren't used
}
public getEnvironmentVariableCollection(extension: IExtensionDescription, persistent?: boolean): vscode.EnvironmentVariableCollection {
// This is not implemented so worker ext host extensions cannot influence terminal envs
throw new Error('Not implemented');
}
public $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void {
// No-op for web worker ext host as collections aren't used
}
}
......@@ -484,6 +484,12 @@ export enum EndOfLine {
CRLF = 2
}
export enum EnvironmentVariableMutatorType {
Replace = 1,
Append = 2,
Prepend = 3
}
@es5ClassCompat
export class TextEdit {
......
......@@ -20,14 +20,21 @@ import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostD
import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal';
import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment';
import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService';
import { BaseExtHostTerminalService, ExtHostTerminal, EnvironmentVariableCollection } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { dispose } from 'vs/base/common/lifecycle';
import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
export class ExtHostTerminalService extends BaseExtHostTerminalService {
private _variableResolver: ExtHostVariableResolverService | undefined;
private _lastActiveWorkspace: IWorkspaceFolder | undefined;
private _environmentVariableCollections: Map<string, EnvironmentVariableCollection> = new Map();
// TODO: Pull this from main side
private _isWorkspaceShellAllowed: boolean = false;
......@@ -191,6 +198,10 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
baseEnv
);
// Apply extension environment variable collections to the environment
const mergedCollection = new MergedEnvironmentVariableCollection(this._environmentVariableCollections);
mergedCollection.applyToProcessEnvironment(env);
this._proxy.$sendResolvedLaunchConfig(id, shellLaunchConfig);
// Fork the process and listen for messages
this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env);
......@@ -215,4 +226,50 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void {
this._isWorkspaceShellAllowed = isAllowed;
}
public getEnvironmentVariableCollection(extension: IExtensionDescription, persistent: boolean = false): vscode.EnvironmentVariableCollection {
let collection: EnvironmentVariableCollection | undefined;
if (persistent) {
// If persistent is specified, return the current collection if it exists
collection = this._environmentVariableCollections.get(extension.identifier.value);
// If persistence changed then create a new collection
if (collection && !collection.persistent) {
collection = undefined;
}
}
if (!collection) {
// If not persistent, clear out the current collection and create a new one
dispose(this._environmentVariableCollections.get(extension.identifier.value));
collection = new EnvironmentVariableCollection(persistent);
this._setEnvironmentVariableCollection(extension.identifier.value, collection);
}
return collection;
}
private _syncEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void {
const serialized = serializeEnvironmentVariableCollection(collection.map);
this._proxy.$setEnvironmentVariableCollection(extensionIdentifier, collection.persistent, serialized.length === 0 ? undefined : serialized);
}
public $initEnvironmentVariableCollections(collections: [string, ISerializableEnvironmentVariableCollection][]): void {
collections.forEach(entry => {
const extensionIdentifier = entry[0];
const collection = new EnvironmentVariableCollection(true, entry[1]);
this._setEnvironmentVariableCollection(extensionIdentifier, collection);
});
}
private _setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void {
this._environmentVariableCollections.set(extensionIdentifier, collection);
collection.onDidChangeCollection(() => {
// When any collection value changes send this immediately, this is done to ensure
// following calls to createTerminal will be created with the new environment. It will
// result in more noise by sending multiple updates when called but collections are
// expected to be small.
this._syncEnvironmentVariableCollection(extensionIdentifier, collection!);
});
}
}
......@@ -7,7 +7,7 @@ import * as assert from 'assert';
import * as sinon from 'sinon';
import { ExperimentActionType, ExperimentState, IExperiment, ExperimentService, getCurrentActivationRecord, currentSchemaVersion } from 'vs/workbench/contrib/experiments/common/experimentService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { TestLifecycleService, TestExtensionService } from 'vs/workbench/test/browser/workbenchTestServices';
import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices';
import {
IExtensionManagementService, DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, ILocalExtension
} from 'vs/platform/extensionManagement/common/extensionManagement';
......@@ -30,6 +30,7 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWillActivateEvent, IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { timeout } from 'vs/base/common/async';
import { TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices';
interface ExperimentSettings {
enabled?: boolean;
......
......@@ -23,6 +23,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Disposable } from 'vs/base/common/lifecycle';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
......@@ -59,6 +60,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
private _latency: number = -1;
private _latencyLastMeasured: number = 0;
private _initialCwd: string | undefined;
private _extEnvironmentVariableCollection: IMergedEnvironmentVariableCollection | undefined;
private readonly _onProcessReady = this._register(new Emitter<void>());
public get onProcessReady(): Event<void> { return this._onProcessReady.event; }
......@@ -87,7 +89,8 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@IProductService private readonly _productService: IProductService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService
) {
super();
this.ptyProcessReady = new Promise<void>(c => {
......@@ -230,6 +233,11 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
const baseEnv = this._configHelper.config.inheritEnv ? processEnv : await this._terminalInstanceService.getMainProcessParentEnv();
const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, lastActiveWorkspace, envFromConfigValue, this._configurationResolverService, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.detectLocale, baseEnv);
// Fetch any extension environment additions and apply them
this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection;
this._register(this._environmentVariableService.onDidChangeCollections(newCollection => this._onEnvironmentVariableCollectionChange(newCollection)));
this._extEnvironmentVariableCollection.applyToProcessEnvironment(env);
const useConpty = this._configHelper.config.windowsEnableConpty && !isScreenReaderModeEnabled;
return this._terminalInstanceService.createTerminalProcess(shellLaunchConfig, initialCwd, cols, rows, env, useConpty);
}
......@@ -304,4 +312,37 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce
this._onProcessExit.fire(exitCode);
}
private _onEnvironmentVariableCollectionChange(newCollection: IMergedEnvironmentVariableCollection): void {
// TODO: React to changes in environment variable collections
// const newAdditions = this._extEnvironmentVariableCollection!.getNewAdditions(newCollection);
// if (newAdditions === undefined) {
// return;
// }
// const promptChoices: IPromptChoice[] = [
// {
// label: nls.localize('apply', "Apply"),
// run: () => {
// let text = '';
// newAdditions.forEach((mutator, variable) => {
// // TODO: Support other common shells
// // TODO: Escape the new values properly
// switch (mutator.type) {
// case EnvironmentVariableMutatorType.Append:
// text += `export ${variable}="$${variable}${mutator.value}"\n`;
// break;
// case EnvironmentVariableMutatorType.Prepend:
// text += `export ${variable}="${mutator.value}$${variable}"\n`;
// break;
// case EnvironmentVariableMutatorType.Replace:
// text += `export ${variable}="${mutator.value}"\n`;
// break;
// }
// });
// this.write(text);
// }
// } as IPromptChoice
// ];
// this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send commands to set the variables in the terminal? Note if you have an application open in the terminal this may not work."), promptChoices);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
registerSingleton(IEnvironmentVariableService, EnvironmentVariableService, true);
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { IProcessEnvironment } from 'vs/base/common/platform';
export const IEnvironmentVariableService = createDecorator<IEnvironmentVariableService>('environmentVariableService');
export enum EnvironmentVariableMutatorType {
Replace = 1,
Append = 2,
Prepend = 3
}
export interface IEnvironmentVariableMutator {
readonly value: string;
readonly type: EnvironmentVariableMutatorType;
}
export interface IExtensionOwnedEnvironmentVariableMutator extends IEnvironmentVariableMutator {
readonly extensionIdentifier: string;
}
export interface IEnvironmentVariableCollection {
readonly map: ReadonlyMap<string, IEnvironmentVariableMutator>;
}
export interface IEnvironmentVariableCollectionWithPersistence extends IEnvironmentVariableCollection {
readonly persistent: boolean;
}
export interface IMergedEnvironmentVariableCollectionDiff {
added: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
changed: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
removed: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
}
/**
* Represents an environment variable collection that results from merging several collections
* together.
*/
export interface IMergedEnvironmentVariableCollection {
readonly map: ReadonlyMap<string, IExtensionOwnedEnvironmentVariableMutator[]>;
/**
* Applies this collection to a process environment.
*/
applyToProcessEnvironment(env: IProcessEnvironment): void;
/**
* Generates a diff of this connection against another.
*/
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff;
}
/**
* Tracks and persists environment variable collections as defined by extensions.
*/
export interface IEnvironmentVariableService {
_serviceBrand: undefined;
/**
* Gets a single collection constructed by merging all environment variable collections into
* one.
*/
readonly collections: ReadonlyMap<string, IEnvironmentVariableCollection>;
/**
* Gets a single collection constructed by merging all environment variable collections into
* one.
*/
readonly mergedCollection: IMergedEnvironmentVariableCollection;
/**
* An event that is fired when an extension's environment variable collection changes, the event
* provides the new merged collection.
*/
onDidChangeCollections: Event<IMergedEnvironmentVariableCollection>;
/**
* Sets an extension's environment variable collection.
*/
set(extensionIdentifier: string, collection: IEnvironmentVariableCollection): void;
/**
* Deletes an extension's environment variable collection.
*/
delete(extensionIdentifier: string): void;
}
/**
* First: Variable
* Second: Value
* Third: Type
*/
export type ISerializableEnvironmentVariableCollection = [string, IEnvironmentVariableMutator][];
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEnvironmentVariableCollection, EnvironmentVariableMutatorType, IMergedEnvironmentVariableCollection, IMergedEnvironmentVariableCollectionDiff, IExtensionOwnedEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IProcessEnvironment } from 'vs/base/common/platform';
export class MergedEnvironmentVariableCollection implements IMergedEnvironmentVariableCollection {
readonly map: Map<string, IExtensionOwnedEnvironmentVariableMutator[]> = new Map();
constructor(collections: Map<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);
if (!entry) {
entry = [];
this.map.set(variable, entry);
}
// If the first item in the entry is replace ignore any other entries as they would
// just get replaced by this one.
if (entry.length > 0 && entry[0].type === EnvironmentVariableMutatorType.Replace) {
next = it.next();
continue;
}
// Mutators get applied in the reverse order than they are created
const mutator = next.value[1];
entry.unshift({
extensionIdentifier,
value: mutator.value,
type: mutator.type
});
next = it.next();
}
});
}
applyToProcessEnvironment(env: IProcessEnvironment): void {
this.map.forEach((mutators, variable) => {
mutators.forEach(mutator => {
switch (mutator.type) {
case EnvironmentVariableMutatorType.Append:
env[variable] = (env[variable] || '') + mutator.value;
break;
case EnvironmentVariableMutatorType.Prepend:
env[variable] = mutator.value + (env[variable] || '');
break;
case EnvironmentVariableMutatorType.Replace:
env[variable] = mutator.value;
break;
}
});
});
}
diff(other: IMergedEnvironmentVariableCollection): IMergedEnvironmentVariableCollectionDiff {
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);
const result = getMissingMutatorsFromArray(otherMutators, currentMutators);
if (result) {
added.set(variable, result);
}
});
// Find removed
this.map.forEach((currentMutators, variable) => {
const otherMutators = other.map.get(variable);
const result = getMissingMutatorsFromArray(currentMutators, otherMutators);
if (result) {
removed.set(variable, result);
}
});
// Find changed
this.map.forEach((currentMutators, variable) => {
const otherMutators = other.map.get(variable);
const result = getChangedMutatorsFromArray(currentMutators, otherMutators);
if (result) {
changed.set(variable, result);
}
});
return { added, changed, removed };
}
}
function getMissingMutatorsFromArray(
current: IExtensionOwnedEnvironmentVariableMutator[],
other: IExtensionOwnedEnvironmentVariableMutator[] | undefined
): IExtensionOwnedEnvironmentVariableMutator[] | undefined {
// If it doesn't exist, all are removed
if (!other) {
return current;
}
// Create a map to help
const otherMutatorExtensions = new Set<string>();
other.forEach(m => otherMutatorExtensions.add(m.extensionIdentifier));
// Find entries removed from other
const result: IExtensionOwnedEnvironmentVariableMutator[] = [];
current.forEach(mutator => {
if (!otherMutatorExtensions.has(mutator.extensionIdentifier)) {
result.push(mutator);
}
});
return result.length === 0 ? undefined : result;
}
function getChangedMutatorsFromArray(
current: IExtensionOwnedEnvironmentVariableMutator[],
other: IExtensionOwnedEnvironmentVariableMutator[] | undefined
): IExtensionOwnedEnvironmentVariableMutator[] | undefined {
// If it doesn't exist, none are changed (they are removed)
if (!other) {
return undefined;
}
// Create a map to help
const otherMutatorExtensions = new Map<string, IExtensionOwnedEnvironmentVariableMutator>();
other.forEach(m => otherMutatorExtensions.set(m.extensionIdentifier, m));
// Find entries that exist in both but are not equal
const result: IExtensionOwnedEnvironmentVariableMutator[] = [];
current.forEach(mutator => {
const otherMutator = otherMutatorExtensions.get(mutator.extensionIdentifier);
if (otherMutator && (mutator.type !== otherMutator.type || mutator.value !== otherMutator.value)) {
// Return the new result, not the old one
result.push(otherMutator);
}
});
return result.length === 0 ? undefined : result;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEnvironmentVariableService, IMergedEnvironmentVariableCollection, ISerializableEnvironmentVariableCollection, IEnvironmentVariableCollectionWithPersistence } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { Event, Emitter } from 'vs/base/common/event';
import { debounce, throttle } from 'vs/base/common/decorators';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
const ENVIRONMENT_VARIABLE_COLLECTIONS_KEY = 'terminal.integrated.environmentVariableCollections';
interface ISerializableExtensionEnvironmentVariableCollection {
extensionIdentifier: string,
collection: ISerializableEnvironmentVariableCollection
}
/**
* Tracks and persists environment variable collections as defined by extensions.
*/
export class EnvironmentVariableService implements IEnvironmentVariableService {
_serviceBrand: undefined;
collections: Map<string, IEnvironmentVariableCollectionWithPersistence> = new Map();
mergedCollection: IMergedEnvironmentVariableCollection;
private readonly _onDidChangeCollections = new Emitter<IMergedEnvironmentVariableCollection>();
get onDidChangeCollections(): Event<IMergedEnvironmentVariableCollection> { return this._onDidChangeCollections.event; }
constructor(
@IExtensionService private readonly _extensionService: IExtensionService,
@IStorageService private readonly _storageService: IStorageService
) {
const serializedPersistedCollections = this._storageService.get(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY, StorageScope.WORKSPACE);
if (serializedPersistedCollections) {
const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = JSON.parse(serializedPersistedCollections);
collectionsJson.forEach(c => this.collections.set(c.extensionIdentifier, {
persistent: true,
map: deserializeEnvironmentVariableCollection(c.collection)
}));
// Asynchronously invalidate collections where extensions have been uninstalled, this is
// async to avoid making all functions on the service synchronous and because extensions
// being uninstalled is rare.
this._invalidateExtensionCollections();
}
this.mergedCollection = this._resolveMergedCollection();
// Listen for uninstalled/disabled extensions
this._extensionService.onDidChangeExtensions(() => this._invalidateExtensionCollections());
}
set(extensionIdentifier: string, collection: IEnvironmentVariableCollectionWithPersistence): void {
this.collections.set(extensionIdentifier, collection);
this._updateCollections();
}
delete(extensionIdentifier: string): void {
this.collections.delete(extensionIdentifier);
this._updateCollections();
}
private _updateCollections(): void {
this._persistCollectionsEventually();
this.mergedCollection = this._resolveMergedCollection();
this._notifyCollectionUpdatesEventually();
}
@throttle(1000)
private _persistCollectionsEventually(): void {
this._persistCollections();
}
protected _persistCollections(): void {
const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = [];
this.collections.forEach((collection, extensionIdentifier) => {
if (collection.persistent) {
collectionsJson.push({
extensionIdentifier,
collection: serializeEnvironmentVariableCollection(this.collections.get(extensionIdentifier)!.map)
});
}
});
const stringifiedJson = JSON.stringify(collectionsJson);
this._storageService.store(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY, stringifiedJson, StorageScope.WORKSPACE);
}
@debounce(1000)
private _notifyCollectionUpdatesEventually(): void {
this._notifyCollectionUpdates();
}
protected _notifyCollectionUpdates(): void {
this._onDidChangeCollections.fire(this.mergedCollection);
}
private _resolveMergedCollection(): IMergedEnvironmentVariableCollection {
return new MergedEnvironmentVariableCollection(this.collections);
}
private async _invalidateExtensionCollections(): Promise<void> {
await this._extensionService.whenInstalledExtensionsRegistered();
const registeredExtensions = await this._extensionService.getExtensions();
let changes = false;
this.collections.forEach((_, extensionIdentifier) => {
const isExtensionRegistered = registeredExtensions.some(r => r.identifier.value === extensionIdentifier);
if (!isExtensionRegistered) {
this.collections.delete(extensionIdentifier);
changes = true;
}
});
if (changes) {
this._updateCollections();
}
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEnvironmentVariableMutator, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
// This file is shared between the renderer and extension host
export function serializeEnvironmentVariableCollection(collection: ReadonlyMap<string, IEnvironmentVariableMutator>): ISerializableEnvironmentVariableCollection {
return [...collection.entries()];
}
export function deserializeEnvironmentVariableCollection(
serializedCollection: ISerializableEnvironmentVariableCollection
): Map<string, IEnvironmentVariableMutator> {
return new Map<string, IEnvironmentVariableMutator>(serializedCollection);
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual, strictEqual } from 'assert';
import { EnvironmentVariableMutatorType } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IProcessEnvironment } from 'vs/base/common/platform';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
import { deserializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
suite('EnvironmentVariable - MergedEnvironmentVariableCollection', () => {
suite('ctor', () => {
test('Should keep entries that come after a Prepend or Append type mutators', () => {
const merged = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }]
])
}],
['ext2', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }]
])
}],
['ext3', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a3', type: EnvironmentVariableMutatorType.Prepend }]
])
}],
['ext4', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a4', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
deepStrictEqual([...merged.map.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' }
]]
]);
});
test('Should remove entries that come after a Replace type mutator', () => {
const merged = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }]
])
}],
['ext2', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }]
])
}],
['ext3', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a3', type: EnvironmentVariableMutatorType.Replace }]
])
}],
['ext4', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a4', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
deepStrictEqual([...merged.map.entries()], [
['A', [
{ extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3' },
{ extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Append, value: 'a2' },
{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Prepend, value: 'a1' }
]]
], 'The ext4 entry should be removed as it comes after a Replace');
});
});
suite('applyToProcessEnvironment', () => {
test('should apply the collection to an environment', () => {
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 }]
])
}]
]));
const env: IProcessEnvironment = {
A: 'foo',
B: 'bar',
C: 'baz'
};
merged.applyToProcessEnvironment(env);
deepStrictEqual(env, {
A: 'a',
B: 'barb',
C: 'cbaz'
});
});
test('should apply the collection to environment entries with no values', () => {
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 }]
])
}]
]));
const env: IProcessEnvironment = {};
merged.applyToProcessEnvironment(env);
deepStrictEqual(env, {
A: 'a',
B: 'b',
C: 'c'
});
});
});
suite('diff', () => {
test('should generate added diffs from when the first entry is added', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const diff = merged1.diff(merged2);
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 }]]
]);
});
test('should generate added diffs from the same extension', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
const diff = merged1.diff(merged2);
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 }]]
]);
});
test('should generate added diffs from a different extension', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }]
])
}]
]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext2', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }]
])
}],
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }]
])
}]
]));
const diff = merged1.diff(merged2);
strictEqual(diff.changed.size, 0);
strictEqual(diff.removed.size, 0);
deepStrictEqual([...diff.added.entries()], [
['A', [{ extensionIdentifier: 'ext2', value: 'a2', type: EnvironmentVariableMutatorType.Append }]]
]);
const merged3 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Prepend }]
])
}],
// This entry should get removed
['ext2', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
const diff2 = merged1.diff(merged3);
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');
});
test('should remove entries in the diff that come after a Replce', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const merged4 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }]
])
}],
// This entry should get removed as it comes after a replace
['ext2', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
const diff = merged1.diff(merged4);
strictEqual(diff.changed.size, 0);
strictEqual(diff.removed.size, 0);
deepStrictEqual([...diff.added.entries()], [], 'Replace should ignore any entries after it');
});
test('should generate removed diffs', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const diff = merged1.diff(merged2);
strictEqual(diff.changed.size, 0);
strictEqual(diff.added.size, 0);
deepStrictEqual([...diff.removed.entries()], [
['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Replace }]]
]);
});
test('should generate changed diffs', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Replace }]
])
}]
]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
const diff = merged1.diff(merged2);
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 }]]
]);
});
test('should generate diffs with added, changed and removed', () => {
const merged1 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a1', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Prepend }]
])
}]
]));
const merged2 = new MergedEnvironmentVariableCollection(new Map([
['ext1', {
map: deserializeEnvironmentVariableCollection([
['A', { value: 'a2', type: EnvironmentVariableMutatorType.Replace }],
['C', { value: 'c', type: EnvironmentVariableMutatorType.Append }]
])
}]
]));
const diff = merged1.diff(merged2);
deepStrictEqual([...diff.added.entries()], [
['C', [{ extensionIdentifier: 'ext1', value: 'c', type: EnvironmentVariableMutatorType.Append }]],
]);
deepStrictEqual([...diff.removed.entries()], [
['B', [{ extensionIdentifier: 'ext1', value: 'b', type: EnvironmentVariableMutatorType.Prepend }]]
]);
deepStrictEqual([...diff.changed.entries()], [
['A', [{ extensionIdentifier: 'ext1', value: 'a2', type: EnvironmentVariableMutatorType.Replace }]]
]);
});
});
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService';
import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { Emitter } from 'vs/base/common/event';
import { IProcessEnvironment } from 'vs/base/common/platform';
class TestEnvironmentVariableService extends EnvironmentVariableService {
persistCollections(): void { this._persistCollections(); }
notifyCollectionUpdates(): void { this._notifyCollectionUpdates(); }
}
suite('EnvironmentVariable - EnvironmentVariableService', () => {
let instantiationService: TestInstantiationService;
let environmentVariableService: TestEnvironmentVariableService;
let storageService: TestStorageService;
let changeExtensionsEvent: Emitter<void>;
setup(() => {
changeExtensionsEvent = new Emitter<void>();
instantiationService = new TestInstantiationService();
instantiationService.stub(IExtensionService, TestExtensionService);
storageService = new TestStorageService();
instantiationService.stub(IStorageService, storageService);
instantiationService.stub(IExtensionService, TestExtensionService);
instantiationService.stub(IExtensionService, 'onDidChangeExtensions', changeExtensionsEvent.event);
instantiationService.stub(IExtensionService, 'getExtensions', [
{ identifier: { value: 'ext1' } },
{ identifier: { value: 'ext2' } },
{ identifier: { value: 'ext3' } }
]);
environmentVariableService = instantiationService.createInstance(TestEnvironmentVariableService);
});
test('should persist collections to the storage service and be able to restore from them', () => {
const collection = new 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 });
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' }]]
]);
// 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' }]]
]);
});
suite('mergedCollection', () => {
test('should overwrite any other variable with the first extension that replaces', () => {
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 });
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()], [
['A', [
{ extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Replace, value: 'a2' },
{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: 'a1' }
]],
['B', [{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Replace, value: 'b1' }]]
]);
});
test('should correctly apply the environment values from multiple extension contributions in the correct order', () => {
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 });
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()], [
['A', [
{ extensionIdentifier: 'ext3', type: EnvironmentVariableMutatorType.Replace, value: 'a3' },
{ extensionIdentifier: 'ext2', type: EnvironmentVariableMutatorType.Prepend, value: 'a2:' },
{ extensionIdentifier: 'ext1', type: EnvironmentVariableMutatorType.Append, value: ':a1' }
]]
]);
// Verify the entries get applied to the environment as expected
const env: IProcessEnvironment = { A: 'foo' };
environmentVariableService.mergedCollection.applyToProcessEnvironment(env);
deepStrictEqual(env, { A: 'a2:a3:a1' });
});
});
});
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { deepStrictEqual } from 'assert';
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { EnvironmentVariableMutatorType, IEnvironmentVariableMutator } from 'vs/workbench/contrib/terminal/common/environmentVariable';
suite('EnvironmentVariable - deserializeEnvironmentVariableCollection', () => {
test('should construct correctly with 3 arguments', () => {
const c = deserializeEnvironmentVariableCollection([
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }],
['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }]
]);
const keys = [...c.keys()];
deepStrictEqual(keys, ['A', 'B', 'C']);
deepStrictEqual(c.get('A'), { value: 'a', type: EnvironmentVariableMutatorType.Replace });
deepStrictEqual(c.get('B'), { value: 'b', type: EnvironmentVariableMutatorType.Append });
deepStrictEqual(c.get('C'), { value: 'c', type: EnvironmentVariableMutatorType.Prepend });
});
});
suite('EnvironmentVariable - serializeEnvironmentVariableCollection', () => {
test('should correctly serialize the object', () => {
const collection = new Map<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 });
deepStrictEqual(serializeEnvironmentVariableCollection(collection), [
['A', { value: 'a', type: EnvironmentVariableMutatorType.Replace }],
['B', { value: 'b', type: EnvironmentVariableMutatorType.Append }],
['C', { value: 'c', type: EnvironmentVariableMutatorType.Prepend }]
]);
});
});
......@@ -47,7 +47,7 @@ import { Range } from 'vs/editor/common/core/range';
import { IDialogService, IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService';
import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations';
import { IDisposable, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
......@@ -103,7 +103,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService';
import { IListService } from 'vs/platform/list/browser/listService';
import { win32, posix } from 'vs/base/common/path';
import { TestWorkingCopyService, TestContextService, TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices';
import { TestWorkingCopyService, TestContextService, TestStorageService, TestTextResourcePropertiesService, TestExtensionService } from 'vs/workbench/test/common/workbenchTestServices';
import { IViewsService, IView } from 'vs/workbench/common/views';
import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
......@@ -298,8 +298,6 @@ export class TestDecorationsService implements IDecorationsService {
getDecoration(_uri: URI, _includeChildren: boolean, _overwrite?: IDecorationData): IDecoration | undefined { return undefined; }
}
export class TestExtensionService extends NullExtensionService { }
export class TestMenuService implements IMenuService {
_serviceBrand: undefined;
......
......@@ -15,6 +15,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe
import { isLinux, isMacintosh } from 'vs/base/common/platform';
import { InMemoryStorageService, IWillSaveStateEvent } from 'vs/platform/storage/common/storage';
import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export class TestTextResourcePropertiesService implements ITextResourcePropertiesService {
......@@ -127,3 +128,5 @@ export function mock<T>(): Ctor<T> {
export interface Ctor<T> {
new(): T;
}
export class TestExtensionService extends NullExtensionService { }
......@@ -201,6 +201,7 @@ import 'vs/workbench/contrib/output/browser/output.contribution';
import 'vs/workbench/contrib/output/browser/outputView';
// Terminal
import 'vs/workbench/contrib/terminal/common/environmentVariable.contribution';
import 'vs/workbench/contrib/terminal/browser/terminal.contribution';
import 'vs/workbench/contrib/terminal/browser/terminalView';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册