Skip to content

Commit

Permalink
Implement scope API on env var collections. Fixes #12940
Browse files Browse the repository at this point in the history
Contributed on behalf of STMicroelectronics

Signed-off-by: Thomas Mäder <t.s.maeder@gmail.com>
  • Loading branch information
tsmaeder committed Oct 24, 2023
1 parent a8c2a67 commit 480d1e2
Show file tree
Hide file tree
Showing 13 changed files with 406 additions and 278 deletions.
125 changes: 125 additions & 0 deletions packages/core/src/common/collections.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// *****************************************************************************
// 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 convencience 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(`innappropriate 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(`innappropriate 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(`innappropriate 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(`innappropriate 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 semantices 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]);
});

}
}
}
8 changes: 4 additions & 4 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ import type {
TimelineChangeEvent,
TimelineProviderDescriptor
} from '@theia/timeline/lib/common/timeline-model';
import { SerializableEnvironmentVariableCollection, SerializableExtensionEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { ThemeType } from '@theia/core/lib/common/theme';
import { Disposable } from '@theia/core/lib/common/disposable';
import { isString, isObject, PickOptions, QuickInputButtonHandle } from '@theia/core/lib/common';
Expand Down Expand Up @@ -287,10 +287,10 @@ export interface TerminalServiceExt {
$terminalSizeChanged(id: string, cols: number, rows: number): void;
$currentTerminalChanged(id: string | undefined): void;
$terminalStateChanged(id: string): void;
$initEnvironmentVariableCollections(collections: [string, SerializableEnvironmentVariableCollection][]): void;
$initEnvironmentVariableCollections(collections: [string, string, boolean, SerializableEnvironmentVariableCollection][]): void;
$provideTerminalLinks(line: string, terminalId: string, token: theia.CancellationToken): Promise<ProvidedTerminalLink[]>;
$handleTerminalLink(link: ProvidedTerminalLink): Promise<void>;
getEnvironmentVariableCollection(extensionIdentifier: string): theia.EnvironmentVariableCollection;
getEnvironmentVariableCollection(extensionIdentifier: string): theia.GlobalEnvironmentVariableCollection;
}
export interface OutputChannelRegistryExt {
createOutputChannel(name: string, pluginInfo: PluginInfo): theia.OutputChannel,
Expand Down Expand Up @@ -408,7 +408,7 @@ export interface TerminalServiceMain {
*/
$disposeByTerminalId(id: number, waitOnExit?: boolean | string): void;

$setEnvironmentVariableCollection(persistent: boolean, collection: SerializableExtensionEnvironmentVariableCollection): void;
$setEnvironmentVariableCollection(persistent: boolean, extensionIdentifier: string, rootUri: string, collection: SerializableEnvironmentVariableCollection): void;

/**
* Set the terminal widget name.
Expand Down
18 changes: 7 additions & 11 deletions packages/plugin-ext/src/main/browser/terminal-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-servi
import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
import { RPCProtocol } from '../../common/rpc-protocol';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { SerializableEnvironmentVariableCollection, SerializableExtensionEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { SerializableEnvironmentVariableCollection, ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { TerminalLink, TerminalLinkProvider } from '@theia/terminal/lib/browser/terminal-link-provider';
import { URI } from '@theia/core/lib/common/uri';
import { getIconClass } from '../../plugin/terminal-ext';
Expand Down Expand Up @@ -59,11 +58,8 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
}
this.toDispose.push(this.terminals.onDidChangeCurrentTerminal(() => this.updateCurrentTerminal()));
this.updateCurrentTerminal();
if (this.shellTerminalServer.collections.size > 0) {
const collectionAsArray = [...this.shellTerminalServer.collections.entries()];
const serializedCollections: [string, SerializableEnvironmentVariableCollection][] = collectionAsArray.map(e => [e[0], [...e[1].map.entries()]]);
this.extProxy.$initEnvironmentVariableCollections(serializedCollections);
}

this.shellTerminalServer.getEnvVarCollections().then(collections => this.extProxy.$initEnvironmentVariableCollections(collections));

this.pluginTerminalRegistry.startCallback = id => this.startProfile(id);

Expand All @@ -75,11 +71,11 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
return this.extProxy.$startProfile(id, CancellationToken.None);
}

$setEnvironmentVariableCollection(persistent: boolean, collection: SerializableExtensionEnvironmentVariableCollection): void {
if (collection.collection) {
this.shellTerminalServer.setCollection(collection.extensionIdentifier, persistent, collection.collection, collection.description);
$setEnvironmentVariableCollection(persistent: boolean, extensionIdentifier: string, rootUri: string, collection: SerializableEnvironmentVariableCollection): void {
if (collection) {
this.shellTerminalServer.setCollection(extensionIdentifier, rootUri, persistent, collection, collection.description);
} else {
this.shellTerminalServer.deleteCollection(collection.extensionIdentifier);
this.shellTerminalServer.deleteCollection(extensionIdentifier);
}
}

Expand Down
54 changes: 34 additions & 20 deletions packages/plugin-ext/src/plugin/terminal-ext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import { Terminal, TerminalOptions, PseudoTerminalOptions, ExtensionTerminalOpti
import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc';
import { RPCProtocol } from '../common/rpc-protocol';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { MultiKeyMap } from '@theia/core/lib/common/collections';
import { Deferred } from '@theia/core/lib/common/promise-util';
import * as theia from '@theia/plugin';
import * as Converter from './type-converters';
import { Disposable, EnvironmentVariableMutatorType, TerminalExitReason, ThemeIcon } from './types-impl';
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { NO_ROOT_URI, SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/shell-terminal-protocol';
import { ProvidedTerminalLink } from '../common/plugin-api-rpc-model';
import { ThemeIcon as MonacoThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';

Expand Down Expand Up @@ -68,7 +69,7 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {
private readonly onDidChangeTerminalStateEmitter = new Emitter<Terminal>();
readonly onDidChangeTerminalState: theia.Event<Terminal> = this.onDidChangeTerminalStateEmitter.event;

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

constructor(rpc: RPCProtocol) {
this.proxy = rpc.getProxy(PLUGIN_RPC_CONTEXT.TERMINAL_MAIN);
Expand Down Expand Up @@ -303,46 +304,53 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {
*--------------------------------------------------------------------------------------------*/
// some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.0/src/vs/workbench/api/common/extHostTerminalService.ts

getEnvironmentVariableCollection(extensionIdentifier: string): theia.EnvironmentVariableCollection {
let collection = this.environmentVariableCollections.get(extensionIdentifier);
getEnvironmentVariableCollection(extensionIdentifier: string, rootUri: string = NO_ROOT_URI): theia.GlobalEnvironmentVariableCollection {
const that = this;
let collection = this.environmentVariableCollections.get([extensionIdentifier, rootUri]);
if (!collection) {
collection = new EnvironmentVariableCollection();
this.setEnvironmentVariableCollection(extensionIdentifier, collection);
collection = new class extends EnvironmentVariableCollectionImpl {
override getScoped(scope: theia.EnvironmentVariableScope): theia.EnvironmentVariableCollection {
return that.getEnvironmentVariableCollection(extensionIdentifier, scope.workspaceFolder?.uri.toString());
}
}(true);
this.setEnvironmentVariableCollection(extensionIdentifier, rootUri, collection);
}
return collection;
}

private syncEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void {
private syncEnvironmentVariableCollection(extensionIdentifier: string, rootUri: string, collection: EnvironmentVariableCollectionImpl): void {
const serialized = [...collection.map.entries()];
this.proxy.$setEnvironmentVariableCollection(collection.persistent, {
extensionIdentifier,
collection: serialized.length === 0 ? undefined : serialized,
description: Converter.fromMarkdownOrString(collection.description)
});
this.proxy.$setEnvironmentVariableCollection(collection.persistent, extensionIdentifier,
rootUri,
{
mutators: serialized,
description: Converter.fromMarkdownOrString(collection.description)
});
}

private setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void {
this.environmentVariableCollections.set(extensionIdentifier, collection);
private setEnvironmentVariableCollection(pluginIdentifier: string, rootUri: string, collection: EnvironmentVariableCollectionImpl): void {
this.environmentVariableCollections.set([pluginIdentifier, 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
// result in more noise by sending multiple updates when called but collections are
// expected to be small.
this.syncEnvironmentVariableCollection(extensionIdentifier, collection);
this.syncEnvironmentVariableCollection(pluginIdentifier, rootUri, collection);
});
}

$initEnvironmentVariableCollections(collections: [string, SerializableEnvironmentVariableCollection][]): void {
$initEnvironmentVariableCollections(collections: [string, string, boolean, SerializableEnvironmentVariableCollection][]): void {
collections.forEach(entry => {
const extensionIdentifier = entry[0];
const collection = new EnvironmentVariableCollection(entry[1]);
this.setEnvironmentVariableCollection(extensionIdentifier, collection);
const rootUri = entry[1];
const collection = new EnvironmentVariableCollectionImpl(entry[2], entry[3]);
this.setEnvironmentVariableCollection(extensionIdentifier, rootUri, collection);
});
}

}

export class EnvironmentVariableCollection implements theia.EnvironmentVariableCollection {
export class EnvironmentVariableCollectionImpl implements theia.GlobalEnvironmentVariableCollection {
readonly map: Map<string, theia.EnvironmentVariableMutator> = new Map();
private _description?: string | theia.MarkdownString;
private _persistent: boolean = true;
Expand All @@ -363,9 +371,15 @@ export class EnvironmentVariableCollection implements theia.EnvironmentVariableC
onDidChangeCollection: Event<void> = this.onDidChangeCollectionEmitter.event;

constructor(
persistent: boolean,
serialized?: SerializableEnvironmentVariableCollection
) {
this.map = new Map(serialized);
this._persistent = persistent;
this.map = new Map(serialized?.mutators);
}

getScoped(scope: theia.EnvironmentVariableScope): theia.EnvironmentVariableCollection {
throw new Error('Cannot get scoped from a regular env var collection');
}

get size(): number {
Expand Down
35 changes: 34 additions & 1 deletion packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3692,6 +3692,39 @@ export module '@theia/plugin' {
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.
*
* @return 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;
}

/**
* The ExtensionMode is provided on the `ExtensionContext` and indicates the
* mode the specific extension is running in.
Expand Down Expand Up @@ -3879,7 +3912,7 @@ export module '@theia/plugin' {
* Gets the extension's environment variable collection for this workspace, enabling changes
* to be applied to terminal environment variables.
*/
readonly environmentVariableCollection: EnvironmentVariableCollection;
readonly environmentVariableCollection: GlobalEnvironmentVariableCollection;

/**
* Get the absolute path of a resource contained in the extension.
Expand Down
2 changes: 1 addition & 1 deletion packages/terminal/src/browser/base/terminal-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export abstract class TerminalWidget extends BaseWidget {
abstract processInfo: Promise<TerminalProcessInfo>;

/** The ids of extensions contributing to the environment of this terminal mapped to the provided description for their changes. */
abstract envVarCollectionDescriptionsByExtension: Promise<Map<string, string | MarkdownString | undefined>>;
abstract envVarCollectionDescriptionsByExtension: Promise<Map<string, (string | MarkdownString | undefined)[]>>;

/** Terminal kind that indicates whether a terminal is created by a user or by some extension for a user */
abstract readonly kind: 'user' | string;
Expand Down
Loading

0 comments on commit 480d1e2

Please sign in to comment.