From 60625a5e4456b3451b5cfb23e23678a71256036e Mon Sep 17 00:00:00 2001 From: Johannes Faltermeier Date: Wed, 16 Aug 2023 15:11:40 +0200 Subject: [PATCH] [vscode] Support EnvironmentVariableCollection description #12696 * show information in UI Contributed on behalf of STMicroelectronics Signed-off-by: Johannes Faltermeier --- .../plugin-ext/src/common/plugin-api-rpc.ts | 3 +- .../src/main/browser/terminal-main.ts | 7 +- .../plugin-ext/src/plugin/terminal-ext.ts | 23 +++- .../src/browser/base/terminal-widget.ts | 4 + .../browser/terminal-frontend-contribution.ts | 18 ++- .../browser/terminal-info-toolbar-item.tsx | 112 ++++++++++++++++++ .../src/browser/terminal-widget-impl.ts | 8 ++ .../src/common/base-terminal-protocol.ts | 5 +- .../terminal/src/node/base-terminal-server.ts | 17 ++- 9 files changed, 183 insertions(+), 14 deletions(-) create mode 100644 packages/terminal/src/browser/terminal-info-toolbar-item.tsx diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 2857dbb4d106f..55a6e003e207f 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -404,7 +404,8 @@ export interface TerminalServiceMain { */ $disposeByTerminalId(id: number, waitOnExit?: boolean | string): void; - $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void; + $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined, + description: string | MarkdownString | undefined): void; /** * Set the terminal widget name. diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 61bb2912df614..02a3827087863 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -16,7 +16,7 @@ import { interfaces } from '@theia/core/shared/inversify'; import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin'; +import { MarkdownString, TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin'; import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; @@ -75,9 +75,10 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin return this.extProxy.$startProfile(id, CancellationToken.None); } - $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void { + $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined, + description: string | MarkdownString | undefined): void { if (collection) { - this.shellTerminalServer.setCollection(extensionIdentifier, persistent, collection); + this.shellTerminalServer.setCollection(extensionIdentifier, persistent, collection, description); } else { this.shellTerminalServer.deleteCollection(extensionIdentifier); } diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index 35738a63725b1..dce1898377100 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -14,16 +14,18 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { UUID } from '@theia/core/shared/@phosphor/coreutils'; -import { Terminal, TerminalOptions, PseudoTerminalOptions, ExtensionTerminalOptions, TerminalState, MarkdownString } from '@theia/plugin'; +import { Terminal, TerminalOptions, PseudoTerminalOptions, ExtensionTerminalOptions, TerminalState } from '@theia/plugin'; 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 { 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 { ProvidedTerminalLink } from '../common/plugin-api-rpc-model'; import { ThemeIcon as MonacoThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService'; +import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering'; export function getIconUris(iconPath: theia.TerminalOptions['iconPath']): { id: string } | undefined { if (ThemeIcon.is(iconPath)) { @@ -313,7 +315,18 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { private syncEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { const serialized = [...collection.map.entries()]; - this.proxy.$setEnvironmentVariableCollection(extensionIdentifier, collection.persistent, serialized.length === 0 ? undefined : serialized); + this.proxy.$setEnvironmentVariableCollection(extensionIdentifier, collection.persistent, serialized.length === 0 ? undefined : serialized, + this.descriptionToDTO(collection.description)); + } + + private descriptionToDTO(value: string | theia.MarkdownString | undefined): string | MarkdownStringDTO | undefined { + if (value === undefined) { + return undefined; + } else if (typeof value === 'string') { + return value; + } else { + return Converter.fromMarkdown(value); + } } private setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { @@ -339,11 +352,11 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { export class EnvironmentVariableCollection implements theia.EnvironmentVariableCollection { readonly map: Map = new Map(); - private _description?: string | MarkdownString; + private _description?: string | theia.MarkdownString; private _persistent: boolean = true; - public get description(): string | MarkdownString | undefined { return this._description; } - public set description(value: string | MarkdownString | undefined) { + public get description(): string | theia.MarkdownString | undefined { return this._description; } + public set description(value: string | theia.MarkdownString | undefined) { this._description = value; this.onDidChangeCollectionEmitter.fire(); } diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index f5a5497fdc84c..473c8abdaeb62 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -20,6 +20,7 @@ import { CommandLineOptions } from '@theia/process/lib/common/shell-command-buil import { TerminalSearchWidget } from '../search/terminal-search-widget'; import { TerminalProcessInfo, TerminalExitReason } from '../../common/base-terminal-protocol'; import URI from '@theia/core/lib/common/uri'; +import { MarkdownString } from '@theia/core/src/common/markdown-rendering/markdown-string'; export interface TerminalDimensions { cols: number; @@ -58,6 +59,9 @@ export abstract class TerminalWidget extends BaseWidget { */ abstract processInfo: Promise; + /** The extensions contributing to the environment of this terminal */ + abstract contributingExtensions: Promise>; + /** Terminal kind that indicates whether a terminal is created by a user or by some extension for a user */ abstract readonly kind: 'user' | string; diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index bd31ca3a32968..8f0758fd01a7d 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -32,7 +32,7 @@ import { import { ApplicationShell, KeybindingContribution, KeyCode, Key, WidgetManager, PreferenceService, KeybindingRegistry, LabelProvider, WidgetOpenerOptions, StorageService, QuickInputService, - codicon, CommonCommands, FrontendApplicationContribution, OnWillStopAction, Dialog, ConfirmDialog, FrontendApplication, PreferenceScope, Widget + codicon, CommonCommands, FrontendApplicationContribution, OnWillStopAction, Dialog, ConfirmDialog, FrontendApplication, PreferenceScope, Widget, HoverService } from '@theia/core/lib/browser'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions, TerminalWidgetImpl } from './terminal-widget-impl'; @@ -60,6 +60,8 @@ import { nls } from '@theia/core/lib/common/nls'; import { Profiles, TerminalPreferences } from './terminal-preferences'; import { ShellTerminalProfile } from './shell-terminal-profile'; import { VariableResolverService } from '@theia/variable-resolver/lib/browser'; +import { TerminalInfoToolbarItem } from './terminal-info-toolbar-item'; +import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; export namespace TerminalMenus { export const TERMINAL = [...MAIN_MENU_BAR, '7_terminal']; @@ -216,6 +218,17 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu @inject(TerminalPreferences) protected terminalPreferences: TerminalPreferences; + @inject(HoverService) + protected readonly hoverService: HoverService; + + @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory; + + protected _markdownRenderer: MarkdownRenderer | undefined; + protected get markdownRenderer(): MarkdownRenderer { + this._markdownRenderer ||= this.markdownRendererFactory(); + return this._markdownRenderer; + } + protected mergePreferencesPromise: Promise = Promise.resolve(); protected readonly onDidCreateTerminalEmitter = new Emitter(); @@ -250,7 +263,7 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu this.storageService.getData(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY).then(data => { if (data) { const collectionsJson: SerializableExtensionEnvironmentVariableCollection[] = JSON.parse(data); - collectionsJson.forEach(c => this.shellTerminalServer.setCollection(c.extensionIdentifier, true, c.collection)); + collectionsJson.forEach(c => this.shellTerminalServer.setCollection(c.extensionIdentifier, true, c.collection, undefined)); } }); }); @@ -731,6 +744,7 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu } registerToolbarItems(toolbar: TabBarToolbarRegistry): void { + toolbar.registerItem(new TerminalInfoToolbarItem(this.hoverService, () => this.currentTerminal, this.markdownRenderer)); toolbar.registerItem({ id: TerminalCommands.SPLIT.id, command: TerminalCommands.SPLIT.id, diff --git a/packages/terminal/src/browser/terminal-info-toolbar-item.tsx b/packages/terminal/src/browser/terminal-info-toolbar-item.tsx new file mode 100644 index 0000000000000..0bba23e802c65 --- /dev/null +++ b/packages/terminal/src/browser/terminal-info-toolbar-item.tsx @@ -0,0 +1,112 @@ +// ***************************************************************************** +// 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 +// ***************************************************************************** +import { ReactTabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { ReactNode } from '@theia/core/shared/react'; +import React = require('@theia/core/shared/react'); +import { HoverService } from '@theia/core/lib/browser'; +import { TerminalWidget } from './base/terminal-widget'; +import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; +import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; +import { DisposableCollection } from '@theia/core'; + +export class TerminalInfoToolbarItem implements ReactTabBarToolbarItem { + readonly id = 'terminal:info'; + + protected toDispose = new DisposableCollection(); + + constructor( + protected readonly hoverService: HoverService, + protected readonly terminal: () => TerminalWidget | undefined, + protected readonly markdownRenderer: MarkdownRenderer + ) {} + + render(): ReactNode { + return ( +
this.onMouseEnter(e)} + onMouseLeave={this.onMouseEnter} + >
+ ); + } + + protected async onMouseEnter( + event: React.MouseEvent + ): Promise { + const currentTarget = event.currentTarget; + const currentTerminal = this.terminal(); + if (currentTerminal) { + const extensions = await currentTerminal.contributingExtensions; + const processId = await currentTerminal.processId; + const processInfo = await currentTerminal.processInfo; + + const mainDiv = document.createElement('div'); + + const pid = document.createElement('div'); + pid.textContent = 'Process ID: ' + processId; + mainDiv.appendChild(pid); + + const commandLine = document.createElement('div'); + commandLine.textContent = + 'Command line: ' + + processInfo.executable + + ' ' + + processInfo.arguments.join(' '); + mainDiv.appendChild(commandLine); + + mainDiv.appendChild(document.createElement('hr')); + + const header = document.createElement('div'); + header.textContent = + 'The following extensions have contributed to this terminal\'s environment:'; + mainDiv.appendChild(header); + + const list = document.createElement('ul'); + mainDiv.appendChild(list); + + extensions.forEach((value, key) => { + const item = document.createElement('li'); + let markdown; + if (value === undefined) { + markdown = new MarkdownStringImpl(''); + markdown.appendText(key); + } else if (typeof value === 'string') { + markdown = new MarkdownStringImpl(''); + markdown.appendText(key + ': ' + value); + } else { + markdown = new MarkdownStringImpl('', value); + markdown.appendText(key + ': '); + markdown.appendMarkdown(value.value); + } + const result = this.markdownRenderer.render(markdown); + this.toDispose.push(result); + item.appendChild(result.element); + list.appendChild(item); + }); + + this.hoverService.requestHover({ + content: mainDiv, + target: currentTarget, + position: 'right', + }); + } + } + + protected async onMouseLeave(): Promise { + this.toDispose.dispose(); + } +} diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 0fb4695889033..72a47d30aa3dc 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -43,6 +43,7 @@ import { Key } from '@theia/core/lib/browser/keys'; import { nls } from '@theia/core/lib/common/nls'; import { TerminalMenus } from './terminal-frontend-contribution'; import debounce = require('p-debounce'); +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; @@ -422,6 +423,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget return this.shellTerminalServer.getProcessInfo(this.terminalId); } + get contributingExtensions(): Promise> { + if (!IBaseTerminalServer.validateId(this.terminalId)) { + return Promise.reject(new Error('terminal is not started')); + } + return this.shellTerminalServer.getContributingExtensions(this.terminalId); + } + get terminalId(): number { return this._terminalId; } diff --git a/packages/terminal/src/common/base-terminal-protocol.ts b/packages/terminal/src/common/base-terminal-protocol.ts index 23474b66e50f7..1b19753ac47c6 100644 --- a/packages/terminal/src/common/base-terminal-protocol.ts +++ b/packages/terminal/src/common/base-terminal-protocol.ts @@ -16,6 +16,7 @@ import { RpcServer } from '@theia/core/lib/common/messaging/proxy-factory'; import { Disposable } from '@theia/core'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; export interface TerminalProcessInfo { executable: string @@ -28,6 +29,7 @@ export interface IBaseTerminalServer extends RpcServer { create(IBaseTerminalServerOptions: object): Promise; getProcessId(id: number): Promise; getProcessInfo(id: number): Promise; + getContributingExtensions(id: number): Promise>; getCwdURI(id: number): Promise; resize(id: number, cols: number, rows: number): Promise; attach(id: number): Promise; @@ -48,7 +50,7 @@ export interface IBaseTerminalServer extends RpcServer { /** * Sets an extension's environment variable collection. */ - setCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection): void; + setCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection, description: string | MarkdownString | undefined): void; /** * Deletes an extension's environment variable collection. */ @@ -154,6 +156,7 @@ export interface EnvironmentVariableCollection { export interface EnvironmentVariableCollectionWithPersistence extends EnvironmentVariableCollection { readonly persistent: boolean; + readonly description: string | MarkdownString | undefined; } export enum EnvironmentVariableMutatorType { diff --git a/packages/terminal/src/node/base-terminal-server.ts b/packages/terminal/src/node/base-terminal-server.ts index 786ebb440f47e..e5fbdc1423a2b 100644 --- a/packages/terminal/src/node/base-terminal-server.ts +++ b/packages/terminal/src/node/base-terminal-server.ts @@ -33,6 +33,7 @@ import { } from '../common/base-terminal-protocol'; import { TerminalProcess, ProcessManager, TaskTerminalProcess } from '@theia/process/lib/node'; import { ShellProcess } from './shell-process'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; @injectable() export abstract class BaseTerminalServer implements IBaseTerminalServer { @@ -100,6 +101,18 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { }; } + async getContributingExtensions(id: number): Promise> { + const terminal = this.processManager.get(id); + if (!(terminal instanceof TerminalProcess)) { + throw new Error(`terminal "${id}" does not exist`); + } + const result = new Map(); + this.collections.forEach((value, key) => { + result.set(key, value.description); + }); + return result; + } + async getCwdURI(id: number): Promise { const terminal = this.processManager.get(id); if (!(terminal instanceof TerminalProcess)) { @@ -176,8 +189,8 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { *--------------------------------------------------------------------------------------------*/ // some code copied and modified from https://github.com/microsoft/vscode/blob/1.49.0/src/vs/workbench/contrib/terminal/common/environmentVariableService.ts - setCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection): void { - const translatedCollection = { persistent, map: new Map(collection) }; + setCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection, description: string | MarkdownString | undefined): void { + const translatedCollection = { persistent, description, map: new Map(collection) }; this.collections.set(extensionIdentifier, translatedCollection); this.updateCollections(); }