Skip to content

Commit

Permalink
feat:support VSCode API: GlobalEnvironmentVariableCollectoin (#4171)
Browse files Browse the repository at this point in the history
* feat: add typings

* chore: fix typo

* feat: api support
  • Loading branch information
bk1012 authored Nov 20, 2024
1 parent 5849361 commit 5dccd6a
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 13 deletions.
123 changes: 123 additions & 0 deletions packages/core-common/src/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// *****************************************************************************
// Copyright (C) 2023 STMicroelectronics and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

/**
* A convenience class for managing a "map of maps" of arbitrary depth
*/
export class MultiKeyMap<K, V> {
private rootMap = new Map();

constructor(private readonly keyLength: number) {}

static create<S, T>(keyLength: number, data: [S[], T][]): MultiKeyMap<S, T> {
const result = new MultiKeyMap<S, T>(keyLength);
for (const entry of data) {
result.set(entry[0], entry[1]);
}
return result;
}

set(key: readonly K[], value: V): V | undefined {
if (this.keyLength !== key.length) {
throw new Error(`inappropriate key length: ${key.length}, should be ${this.keyLength}`);
}
let map = this.rootMap;
for (let i = 0; i < this.keyLength - 1; i++) {
let existing = map.get(key[i]);
if (!existing) {
existing = new Map();
map.set(key[i], existing);
}
map = existing;
}
const oldValue = map.get(key[this.keyLength - 1]);
map.set(key[this.keyLength - 1], value);
return oldValue;
}

get(key: readonly K[]): V | undefined {
if (this.keyLength !== key.length) {
throw new Error(`inappropriate key length: ${key.length}, should be ${this.keyLength}`);
}
let map = this.rootMap;
for (let i = 0; i < this.keyLength - 1; i++) {
map = map.get(key[i]);
if (!map) {
return undefined;
}
}
return map.get(key[this.keyLength - 1]);
}

/**
* Checks whether the given key is present in the map
* @param key the key to test. It can have a length < the key length
* @returns whether the key exists
*/
has(key: readonly K[]): boolean {
if (this.keyLength < key.length) {
throw new Error(`inappropriate key length: ${key.length}, should <= ${this.keyLength}`);
}
let map = this.rootMap;
for (let i = 0; i < key.length - 1; i++) {
map = map.get(key[i]);
if (!map) {
return false;
}
}
return map.has(key[key.length - 1]);
}

/**
* Deletes the value with the given key from the map
* @param key the key to remove. It can have a length < the key length
* @returns whether the key was present in the map
*/
delete(key: readonly K[]): boolean {
if (this.keyLength < key.length) {
throw new Error(`inappropriate key length: ${key.length}, should <= ${this.keyLength}`);
}
let map = this.rootMap;
for (let i = 0; i < this.keyLength - 1; i++) {
map = map.get(key[i]);
if (!map) {
return false;
}
}
return map.delete(key[key.length - 1]);
}

/**
* Iterates over all entries in the map. The ordering semantics are like iterating over a map of maps.
* @param handler Handler for each entry
*/
forEach(handler: (value: V, key: K[]) => void): void {
this.doForeach(handler, this.rootMap, []);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private doForeach(handler: (value: V, key: K[]) => void, currentMap: Map<any, any>, keys: K[]): void {
if (keys.length === this.keyLength - 1) {
currentMap.forEach((v, k) => {
handler(v, [...keys, k]);
});
} else {
currentMap.forEach((v, k) => {
this.doForeach(handler, v, [...keys, k]);
});
}
}
}
1 change: 1 addition & 0 deletions packages/core-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './application.lifecycle';
export * from './extension.schema';
export * from './ai-native';
export * from './remote-service';
export * from './collections';
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ describe('ext host terminal test', () => {
const mockExtension = {
id: 'test-terminal-env',
};
const collection = extHost.getEnviromentVariableCollection(mockExtension as unknown as IExtension);
const collection = extHost.getEnvironmentVariableCollection(mockExtension as unknown as IExtension);
// @ts-ignore
const mocksyncEnvironmentVariableCollection = jest.spyOn(extHost, 'syncEnvironmentVariableCollection');

Expand Down
2 changes: 2 additions & 0 deletions packages/extension/src/common/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ export interface IExtension extends IExtensionProps {
toJSON(): IExtensionProps;
}

export const NO_ROOT_URI = '<none>';

const VAR_REGEXP = /^\$\(([a-z.]+\/)?([a-z-]+)(~[a-z]+)?\)$/i;

export interface ContributesMap<T> {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/src/common/vscode/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,6 @@ export interface IExtHostTerminal {
$createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise<void>;

// #region
getEnviromentVariableCollection(extension: IExtensionProps): vscode.EnvironmentVariableCollection;
getEnvironmentVariableCollection(extension: IExtensionProps): vscode.GlobalEnvironmentVariableCollection;
// #endregion
}
4 changes: 2 additions & 2 deletions packages/extension/src/hosted/api/vscode/env/envApiFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export function createEnvApiFactory(
get onDidChangeLogLevel(): Event<LogLevel> {
return envHost.logLevelChangeEmitter.event;
},
get environmentVariableCollection(): vscode.EnvironmentVariableCollection {
return exthostTerminal.getEnviromentVariableCollection(extension);
get environmentVariableCollection(): vscode.GlobalEnvironmentVariableCollection {
return exthostTerminal.getEnvironmentVariableCollection(extension);
},
createTelemetryLogger(
sender: vscode.TelemetrySender,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class ExtensionContext implements vscode.ExtensionContext, IKTExtensionCo
}

get environmentVariableCollection() {
return this.exthostTerminalService?.getEnviromentVariableCollection(this.extensionDescription)!;
return this.exthostTerminalService?.getEnvironmentVariableCollection(this.extensionDescription)!;
}

get extension() {
Expand Down
30 changes: 22 additions & 8 deletions packages/extension/src/hosted/api/vscode/ext.host.terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Emitter,
Event,
IDisposable,
MultiKeyMap,
isUndefined,
uuid,
} from '@opensumi/ide-core-common';
Expand All @@ -30,7 +31,7 @@ import {
ISerializableEnvironmentVariableCollection,
} from '@opensumi/ide-terminal-next/lib/common/environmentVariable';

import { IExtension } from '../../../common';
import { IExtension, NO_ROOT_URI } from '../../../common';
import {
IExtHostTerminal,
IExtensionDescription,
Expand Down Expand Up @@ -63,7 +64,7 @@ export class ExtHostTerminal implements IExtHostTerminal {
private _defaultProfile: ITerminalProfile | undefined;
private _defaultAutomationProfile: ITerminalProfile | undefined;

private environmentVariableCollections: Map<string, EnvironmentVariableCollection> = new Map();
private environmentVariableCollections: MultiKeyMap<string, EnvironmentVariableCollection> = new MultiKeyMap(2);

private disposables: DisposableStore = new DisposableStore();

Expand Down Expand Up @@ -524,11 +525,18 @@ export class ExtHostTerminal implements IExtHostTerminal {
}
}

getEnviromentVariableCollection(extension: IExtension) {
let collection = this.environmentVariableCollections.get(extension.id);
getEnvironmentVariableCollection(extension: IExtension, rootUri: string = NO_ROOT_URI) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const that = this;
let collection = this.environmentVariableCollections.get([extension.id, rootUri]);
if (!collection) {
collection = new EnvironmentVariableCollection();
this._setEnvironmentVariableCollection(extension.id, collection);
collection = new (class extends EnvironmentVariableCollection {
override getScoped(scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection {
return that.getEnvironmentVariableCollection(extension, scope.workspaceFolder?.uri.toString());
}
})();

this._setEnvironmentVariableCollection(extension.id, rootUri, collection);
}
return collection;
}
Expand All @@ -547,9 +555,10 @@ export class ExtHostTerminal implements IExtHostTerminal {

private _setEnvironmentVariableCollection(
extensionIdentifier: string,
rootUri: string,
collection: EnvironmentVariableCollection,
): void {
this.environmentVariableCollections.set(extensionIdentifier, collection);
this.environmentVariableCollections.set([extensionIdentifier, rootUri], 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
Expand All @@ -565,7 +574,7 @@ export class ExtHostTerminal implements IExtHostTerminal {
* Some code copied and modified from
* https://github.com/microsoft/vscode/blob/1.55.0/src/vs/workbench/api/common/extHostTerminalService.ts#L696
*/
export class EnvironmentVariableCollection implements vscode.EnvironmentVariableCollection {
export class EnvironmentVariableCollection implements vscode.GlobalEnvironmentVariableCollection {
readonly map: Map<string, vscode.EnvironmentVariableMutator> = new Map();

protected readonly _onDidChangeCollection: Emitter<void> = new Emitter<void>();
Expand All @@ -588,6 +597,11 @@ export class EnvironmentVariableCollection implements vscode.EnvironmentVariable
this._onDidChangeCollection.fire();
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
getScoped(_scope: vscode.EnvironmentVariableScope): vscode.EnvironmentVariableCollection {
throw new Error('Cannot get scoped from a regular env var collection');
}

private _setIfDiffers(variable: string, mutator: vscode.EnvironmentVariableMutator): void {
const current = this.map.get(variable);
if (!current || current.value !== mutator.value || current.type !== mutator.type) {
Expand Down
33 changes: 33 additions & 0 deletions packages/types/vscode/typings/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3179,6 +3179,39 @@ declare module 'vscode' {
clear(): void;
}

/**
* A collection of mutations that an extension can apply to a process environment. Applies to all scopes.
*/
export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection {
/**
* Gets scope-specific environment variable collection for the extension. This enables alterations to
* terminal environment variables solely within the designated scope, and is applied in addition to (and
* after) the global collection.
*
* Each object obtained through this method is isolated and does not impact objects for other scopes,
* including the global collection.
*
* @param scope The scope to which the environment variable collection applies to.
*
* If a scope parameter is omitted, collection applicable to all relevant scopes for that parameter is
* returned. For instance, if the 'workspaceFolder' parameter is not specified, the collection that applies
* across all workspace folders will be returned.
*
* @returns Environment variable collection for the passed in scope.
*/
getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection;
}

/**
* The scope object to which the environment variable collection applies.
*/
export interface EnvironmentVariableScope {
/**
* Any specific workspace folder to get collection for.
*/
workspaceFolder?: WorkspaceFolder;
}

//#endregion

/**
Expand Down

0 comments on commit 5dccd6a

Please sign in to comment.