From 572072403a5eecb5c769a30964d134e4b1b93d78 Mon Sep 17 00:00:00 2001 From: Johannes Faltermeier Date: Fri, 25 Aug 2023 12:18:54 +0200 Subject: [PATCH] [vscode] Support EnvironmentVariableCollection description #12696 (#12838) * add new field to EnvironmentVariableCollection and implement interface * add description to SerializableExtensionEnvironmentVariableCollection and use as DTO in $setEnvironmentVariableCollection * add fromMarkdownOrString method to converter * allow widgets to customize the enhanced preview node * implement enhanced preview for terminal widget Contributed on behalf of STMicroelectronics Signed-off-by: Johannes Faltermeier --- packages/core/src/browser/shell/tab-bars.ts | 9 ++- .../widgets/enhanced-preview-widget.ts | 27 ++++++++ .../plugin-ext/src/common/plugin-api-rpc.ts | 4 +- .../src/main/browser/terminal-main.ts | 10 +-- .../plugin-ext/src/plugin/terminal-ext.ts | 14 +++- .../plugin-ext/src/plugin/type-converters.ts | 10 +++ packages/plugin/src/theia.d.ts | 6 ++ .../src/browser/base/terminal-widget.ts | 4 ++ .../browser/terminal-frontend-contribution.ts | 2 +- .../src/browser/terminal-widget-impl.ts | 66 ++++++++++++++++++- .../src/common/base-terminal-protocol.ts | 8 ++- .../terminal/src/node/base-terminal-server.ts | 20 +++++- 12 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 packages/core/src/browser/widgets/enhanced-preview-widget.ts diff --git a/packages/core/src/browser/shell/tab-bars.ts b/packages/core/src/browser/shell/tab-bars.ts index 6d02fa6cdb84b..c43f42c73bbcf 100644 --- a/packages/core/src/browser/shell/tab-bars.ts +++ b/packages/core/src/browser/shell/tab-bars.ts @@ -38,6 +38,7 @@ import { Root, createRoot } from 'react-dom/client'; import { SelectComponent } from '../widgets/select-component'; import { createElement } from 'react'; import { PreviewableWidget } from '../widgets/previewable-widget'; +import { EnhancedPreviewWidget } from '../widgets/enhanced-preview-widget'; /** The class name added to hidden content nodes, which are required to render vertical side bars. */ const HIDDEN_CONTENT_CLASS = 'theia-TabBar-hidden-content'; @@ -504,7 +505,13 @@ export class TabBarRenderer extends TabBar.Renderer { labelElement.classList.add('theia-horizontal-tabBar-hover-title'); labelElement.textContent = title.label; hoverBox.append(labelElement); - if (title.caption) { + const widget = title.owner; + if (EnhancedPreviewWidget.is(widget)) { + const enhancedPreviewNode = widget.getEnhancedPreviewNode(); + if (enhancedPreviewNode) { + hoverBox.appendChild(enhancedPreviewNode); + } + } else if (title.caption) { const captionElement = document.createElement('p'); captionElement.classList.add('theia-horizontal-tabBar-hover-caption'); captionElement.textContent = title.caption; diff --git a/packages/core/src/browser/widgets/enhanced-preview-widget.ts b/packages/core/src/browser/widgets/enhanced-preview-widget.ts new file mode 100644 index 0000000000000..923394da08035 --- /dev/null +++ b/packages/core/src/browser/widgets/enhanced-preview-widget.ts @@ -0,0 +1,27 @@ +// ***************************************************************************** +// 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 { isFunction, isObject } from '../../common'; + +export interface EnhancedPreviewWidget { + getEnhancedPreviewNode(): Node | undefined; +} + +export namespace EnhancedPreviewWidget { + export function is(arg: unknown): arg is EnhancedPreviewWidget { + return isObject(arg) && isFunction(arg.getEnhancedPreviewNode); + } +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 394f38a174da6..f6273b5eddc58 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -111,7 +111,7 @@ import type { TimelineChangeEvent, TimelineProviderDescriptor } from '@theia/timeline/lib/common/timeline-model'; -import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol'; +import { SerializableEnvironmentVariableCollection, SerializableExtensionEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-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'; @@ -405,7 +405,7 @@ export interface TerminalServiceMain { */ $disposeByTerminalId(id: number, waitOnExit?: boolean | string): void; - $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void; + $setEnvironmentVariableCollection(persistent: boolean, collection: SerializableExtensionEnvironmentVariableCollection): 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..b68d5a800881f 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -22,7 +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 } from '@theia/terminal/lib/common/base-terminal-protocol'; +import { SerializableEnvironmentVariableCollection, SerializableExtensionEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol'; import { 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'; @@ -75,11 +75,11 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin return this.extProxy.$startProfile(id, CancellationToken.None); } - $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void { - if (collection) { - this.shellTerminalServer.setCollection(extensionIdentifier, persistent, collection); + $setEnvironmentVariableCollection(persistent: boolean, collection: SerializableExtensionEnvironmentVariableCollection): void { + if (collection.collection) { + this.shellTerminalServer.setCollection(collection.extensionIdentifier, persistent, collection.collection, collection.description); } else { - this.shellTerminalServer.deleteCollection(extensionIdentifier); + this.shellTerminalServer.deleteCollection(collection.extensionIdentifier); } } diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index f062d28b97a7c..0cdfaeb886d4b 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -20,6 +20,7 @@ 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'; @@ -313,7 +314,11 @@ 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(collection.persistent, { + extensionIdentifier, + collection: serialized.length === 0 ? undefined : serialized, + description: Converter.fromMarkdownOrString(collection.description) + }); } private setEnvironmentVariableCollection(extensionIdentifier: string, collection: EnvironmentVariableCollection): void { @@ -339,8 +344,15 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { export class EnvironmentVariableCollection implements theia.EnvironmentVariableCollection { readonly map: Map = new Map(); + private _description?: string | theia.MarkdownString; private _persistent: boolean = true; + public get description(): string | theia.MarkdownString | undefined { return this._description; } + public set description(value: string | theia.MarkdownString | undefined) { + this._description = value; + this.onDidChangeCollectionEmitter.fire(); + } + public get persistent(): boolean { return this._persistent; } public set persistent(value: boolean) { this._persistent = value; diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index f3c123fb0cec7..797758b79c3cd 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -201,6 +201,16 @@ export function fromMarkdown(markup: theia.MarkdownString | theia.MarkedString): } } +export function fromMarkdownOrString(value: string | theia.MarkdownString | undefined): string | MarkdownStringDTO | undefined { + if (value === undefined) { + return undefined; + } else if (typeof value === 'string') { + return value; + } else { + return fromMarkdown(value); + } +} + export function toMarkdown(value: MarkdownStringDTO): PluginMarkdownStringImpl { const implemented = new PluginMarkdownStringImpl(value.value, value.supportThemeIcons); implemented.isTrusted = value.isTrusted; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index c55570f398331..6bb9ec8aa30ee 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3585,6 +3585,12 @@ export module '@theia/plugin' { * A collection of mutations that an extension can apply to a process environment. */ export interface EnvironmentVariableCollection { + + /** + * A description for the environment variable collection, this will be used to describe the changes in the UI. + */ + description: string | MarkdownString | undefined; + /** * Whether the collection should be cached for the workspace and applied to the terminal * across window reloads. When true the collection will be active immediately such when the diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index f5a5497fdc84c..b47b322299030 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/lib/common/markdown-rendering/markdown-string'; export interface TerminalDimensions { cols: number; @@ -58,6 +59,9 @@ export abstract class TerminalWidget extends BaseWidget { */ abstract processInfo: Promise; + /** The ids of extensions contributing to the environment of this terminal mapped to the provided description for their changes. */ + abstract envVarCollectionDescriptionsByExtension: 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..5916ab41f399e 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -250,7 +250,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 ? c.collection : [], c.description)); } }); }); diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 76eddef0ba81c..1508c92a12bbe 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -43,6 +43,9 @@ 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, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; +import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-preview-widget'; +import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; @@ -57,7 +60,7 @@ export interface TerminalContribution { } @injectable() -export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget { +export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget, EnhancedPreviewWidget { readonly isExtractable: boolean = true; secondaryWindow: Window | undefined; location: TerminalLocationOptions; @@ -81,6 +84,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected lastMousePosition: { x: number, y: number } | undefined; protected isAttachedCloseListener: boolean = false; protected shown = false; + protected enhancedPreviewNode: Node | undefined; override lastCwd = new URI(); @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -98,6 +102,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget @inject(TerminalThemeService) protected readonly themeService: TerminalThemeService; @inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; + @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory; + + protected _markdownRenderer: MarkdownRenderer | undefined; + protected get markdownRenderer(): MarkdownRenderer { + this._markdownRenderer ||= this.markdownRendererFactory(); + return this._markdownRenderer; + } protected readonly onDidOpenEmitter = new Emitter(); readonly onDidOpen: Event = this.onDidOpenEmitter.event; @@ -426,6 +437,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget return this.shellTerminalServer.getProcessInfo(this.terminalId); } + get envVarCollectionDescriptionsByExtension(): Promise> { + if (!IBaseTerminalServer.validateId(this.terminalId)) { + return Promise.reject(new Error('terminal is not started')); + } + return this.shellTerminalServer.getEnvVarCollectionDescriptionsByExtension(this.terminalId); + } + get terminalId(): number { return this._terminalId; } @@ -762,6 +780,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget if (this.exitStatus) { this.onTermDidClose.fire(this); } + if (this.enhancedPreviewNode) { + // don't use preview node anymore. rendered markdown will be disposed on super call + this.enhancedPreviewNode = undefined; + } super.dispose(); } @@ -867,4 +889,46 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget private disableEnterWhenAttachCloseListener(): boolean { return this.isAttachedCloseListener; } + + getEnhancedPreviewNode(): Node | undefined { + if (this.enhancedPreviewNode) { + return this.enhancedPreviewNode; + } + + this.enhancedPreviewNode = document.createElement('div'); + + Promise.all([this.envVarCollectionDescriptionsByExtension, this.processId, this.processInfo]) + .then((values: [Map, number, TerminalProcessInfo]) => { + const extensions = values[0]; + const processId = values[1]; + const processInfo = values[2]; + + const markdown = new MarkdownStringImpl(); + markdown.appendMarkdown('Process ID: ' + processId + '\\\n'); + markdown.appendMarkdown('Command line: ' + + processInfo.executable + + ' ' + + processInfo.arguments.join(' ') + + '\n\n---\n\n'); + markdown.appendMarkdown('The following extensions have contributed to this terminal\'s environment:\n'); + extensions.forEach((value, key) => { + if (value === undefined) { + markdown.appendMarkdown('* ' + key + '\n'); + } else if (typeof value === 'string') { + markdown.appendMarkdown('* ' + key + ': ' + value + '\n'); + } else { + markdown.appendMarkdown('* ' + key + ': ' + value.value + '\n'); + } + }); + + const enhancedPreviewNode = this.enhancedPreviewNode; + if (!this.isDisposed && enhancedPreviewNode) { + const result = this.markdownRenderer.render(markdown); + this.toDispose.push(result); + enhancedPreviewNode.appendChild(result.element); + } + }); + + return this.enhancedPreviewNode; + } } diff --git a/packages/terminal/src/common/base-terminal-protocol.ts b/packages/terminal/src/common/base-terminal-protocol.ts index c597b97bf856a..0655bd42dd94b 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; + getEnvVarCollectionDescriptionsByExtension(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. */ @@ -157,6 +159,7 @@ export interface EnvironmentVariableCollection { export interface EnvironmentVariableCollectionWithPersistence extends EnvironmentVariableCollection { readonly persistent: boolean; + readonly description: string | MarkdownString | undefined; } export enum EnvironmentVariableMutatorType { @@ -189,7 +192,8 @@ export interface MergedEnvironmentVariableCollection { export interface SerializableExtensionEnvironmentVariableCollection { extensionIdentifier: string, - collection: SerializableEnvironmentVariableCollection + collection: SerializableEnvironmentVariableCollection | undefined, + description: string | MarkdownString | undefined } export type SerializableEnvironmentVariableCollection = [string, EnvironmentVariableMutator][]; diff --git a/packages/terminal/src/node/base-terminal-server.ts b/packages/terminal/src/node/base-terminal-server.ts index ae86faf5fcec5..d36b153649a53 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 { @@ -102,6 +103,18 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { }; } + async getEnvVarCollectionDescriptionsByExtension(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)) { @@ -189,8 +202,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(); } @@ -211,7 +224,8 @@ export abstract class BaseTerminalServer implements IBaseTerminalServer { if (collection.persistent) { collectionsJson.push({ extensionIdentifier, - collection: [...this.collections.get(extensionIdentifier)!.map.entries()] + collection: [...this.collections.get(extensionIdentifier)!.map.entries()], + description: collection.description }); } });