Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement scope API on env var collections #12999

Merged
merged 2 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- [vscode] evolve proposed API for documentPaste (stubbed) [#13010](https://github.com/eclipse-theia/theia/pull/13010) - contributed on behalf of STMicroelectronics
- [vscode] evolve proposed API for terminalQuickFixProvider [#13006](https://github.com/eclipse-theia/theia/pull/13006) - contributed on behalf of STMicroelectronics
- [vscode] added 'provideDocumentRangesFormattingEdits' in the `DocumentRangeFormattingEditProvider` VS Code API [#13020](https://github.com/eclipse-theia/theia/pull/13020) - contributed on behalf of STMicroelectronics

- [vscode] Implement scope API on env var collections [#12999](https://github.com/eclipse-theia/theia/pull/12999) - contributed on behalf of STMicroelectronics

<a name="breaking_changes_1.43.0">[Breaking Changes:](#breaking_changes_1.43.0)</a>

Expand Down
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
Loading