diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 9323f3be841..31fe513969e 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -261,6 +261,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { if (collection.size === 0) { return undefined; } + + const variables: string[] = []; const values: string[] = []; const types: EnvironmentVariableMutatorType[] = []; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index 31b9c3790bb..4a99d29c50e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -345,6 +345,6 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } } as IPromptChoice ]; - this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send export commands to the terminal?"), promptChoices); + this._notificationService.prompt(Severity.Info, nls.localize('environmentchange', "An extension wants to change the terminal environment, do you want to send commands to set the variables in the terminal? Note if you have an application open in the terminal this may not work."), promptChoices); } } diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts index a5227baf178..23007f95db8 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariable.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariable.ts @@ -22,8 +22,6 @@ export interface IEnvironmentVariableMutator { export interface IEnvironmentVariableCollection { readonly entries: ReadonlyMap; - // TODO: Remove equals? - // equals(other: IEnvironmentVariableCollection): boolean; /** * Get's additions when compared to another collection. This only gets additions rather than @@ -32,7 +30,15 @@ export interface IEnvironmentVariableCollection { */ getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined; + /** + * Applies this collection to a process environment. + */ applyToProcessEnvironment(env: IProcessEnvironment): void; + + /** + * Gets a serializable view of the collection. + */ + // serialize(): [string, IEnvironmentVariableMutator][]; } /** diff --git a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts index 1567e5118e3..ca5b4fccc21 100644 --- a/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts +++ b/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts @@ -7,6 +7,20 @@ import { IEnvironmentVariableService, IEnvironmentVariableCollection, IEnvironme import { Event, Emitter } from 'vs/base/common/event'; import { debounce } from 'vs/base/common/decorators'; import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +const ENVIRONMENT_VARIABLE_COLLECTIONS_KEY = 'terminal.integrated.environmentVariableCollections'; + +interface ISerializableEnvironmentVariableCollection { + variables: string[]; + values: string[]; + types: number[]; +} +interface ISerializableExtensionEnvironmentVariableCollection { + extensionIdentifier: string, + collection: ISerializableEnvironmentVariableCollection +} export class EnvironmentVariableCollection implements IEnvironmentVariableCollection { readonly entries: Map; @@ -27,21 +41,6 @@ export class EnvironmentVariableCollection implements IEnvironmentVariableCollec } } - // TODO: Remove? Does diff do the job? - // equals(other: IEnvironmentVariableCollection): boolean { - // if (this.entries.size !== other.entries.size) { - // return false; - // } - // let result = true; - // this.entries.forEach((mutator, variable) => { - // const otherMutator = other.entries.get(variable); - // if (otherMutator !== mutator) { - // result = false; - // } - // }); - // return result; - // } - // TODO: Consider doing a full diff, just marking the environment as stale with no action available? getNewAdditions(other: IEnvironmentVariableCollection): ReadonlyMap | undefined { const result = new Map(); @@ -78,15 +77,36 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { _serviceBrand: undefined; private _collections: Map = new Map(); - private _mergedCollection: IEnvironmentVariableCollection = new EnvironmentVariableCollection(); + private _mergedCollection: IEnvironmentVariableCollection; - // TODO: Generate a summary of changes inside the terminal component as it needs to be done per-terminal compared to what it started with private readonly _onDidChangeCollections = new Emitter(); get onDidChangeCollections(): Event { return this._onDidChangeCollections.event; } - constructor() { - // TODO: Load in persisted collections - // TODO: Fire an event when collections have changed that the terminal component can listen to + constructor( + @IExtensionService private _extensionService: IExtensionService, + @IStorageService private readonly _storageService: IStorageService + ) { + const serializedPersistedCollections = this._storageService.get(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY, StorageScope.WORKSPACE); + if (serializedPersistedCollections) { + // TODO: Load in persisted collections + const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = JSON.parse(serializedPersistedCollections); + + + collectionsJson.forEach(c => { + const extCollection = new EnvironmentVariableCollection(c.collection.variables, c.collection.values, c.collection.types); + this._collections.set(c.extensionIdentifier, extCollection); + }); + console.log('serialized from previous session', this._collections); + + // 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()); } get mergedCollection(): IEnvironmentVariableCollection { @@ -95,16 +115,34 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { set(extensionIdentifier: string, collection: IEnvironmentVariableCollection): void { this._collections.set(extensionIdentifier, collection); - this._mergedCollection = this._resolveMergedCollection(); - this._notifyCollectionUpdates(); + this._updateCollections(); } delete(extensionIdentifier: string): void { this._collections.delete(extensionIdentifier); + this._updateCollections(); + } + + private _updateCollections(): void { + this._persistCollections(); this._mergedCollection = this._resolveMergedCollection(); this._notifyCollectionUpdates(); } + @debounce(1000) + private _persistCollections(): void { + const keys = [...this._collections.keys()]; + const collectionsJson: ISerializableExtensionEnvironmentVariableCollection[] = keys.map(extensionIdentifier => { + return { + extensionIdentifier, + collection: serializeEnvironmentVariableCollection(this._collections.get(extensionIdentifier)!) + }; + }); + const stringifiedJson = JSON.stringify(collectionsJson); + console.log('storing', stringifiedJson, collectionsJson); + this._storageService.store(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY, stringifiedJson, StorageScope.WORKSPACE); + } + @debounce(1000) private _notifyCollectionUpdates(): void { this._onDidChangeCollections.fire(this._mergedCollection); @@ -121,4 +159,34 @@ export class EnvironmentVariableService implements IEnvironmentVariableService { }); return result; } + + private async _invalidateExtensionCollections(): Promise { + console.log('checking extensions'); + 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) { + console.log('invalidated ' + extensionIdentifier); + this._collections.delete(extensionIdentifier); + changes = true; + } + }); + if (changes) { + this._updateCollections(); + } + } +} + + +function serializeEnvironmentVariableCollection(collection: IEnvironmentVariableCollection): ISerializableEnvironmentVariableCollection { + const entries = [...collection.entries.entries()]; + const result: ISerializableEnvironmentVariableCollection = { + variables: entries.map(e => e[0]), + values: entries.map(e => e[1].value), + types: entries.map(e => e[1].type), + }; + return result; }