From 9ae76400b5c660fc54684da31f1dfd275f8bf497 Mon Sep 17 00:00:00 2001 From: Dennis Huebner Date: Thu, 27 Jun 2024 16:13:45 +0200 Subject: [PATCH] Codicon color and URI support for TerminalOptions (#13413) Co-authored-by: FernandoAscencio Co-authored-by: Mark Sujew --- packages/core/src/browser/style/tabs.css | 11 +++ .../core/src/common/quick-pick-service.ts | 3 - packages/monaco/src/browser/style/index.css | 4 ++ .../plugin-ext/src/common/plugin-api-rpc.ts | 22 +++--- .../src/main/browser/quick-open-main.ts | 68 +++++++------------ .../src/main/browser/terminal-main.ts | 32 +++++++-- .../plugin-ext/src/plugin/plugin-context.ts | 4 +- .../plugin-ext/src/plugin/plugin-icon-path.ts | 7 +- packages/plugin-ext/src/plugin/quick-open.ts | 41 +++-------- .../plugin-ext/src/plugin/terminal-ext.ts | 60 ++++++++-------- .../plugin-ext/src/plugin/tree/tree-views.ts | 2 +- .../plugin-ext/src/plugin/type-converters.ts | 16 ++--- packages/plugin-ext/src/plugin/types-impl.ts | 3 + packages/plugin/src/theia.d.ts | 4 +- .../src/browser/base/terminal-widget.ts | 7 +- .../browser/terminal-frontend-contribution.ts | 3 +- .../src/browser/terminal-widget-impl.ts | 46 ++++++++++--- 17 files changed, 176 insertions(+), 157 deletions(-) diff --git a/packages/core/src/browser/style/tabs.css b/packages/core/src/browser/style/tabs.css index 92c48350cd691..18db27f235de4 100644 --- a/packages/core/src/browser/style/tabs.css +++ b/packages/core/src/browser/style/tabs.css @@ -177,6 +177,17 @@ padding-right: 8px; } +.p-TabBar.theia-app-centers .p-TabBar-tabIcon[class*="plugin-icon-"], +.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon[class*="plugin-icon-"] { + background: none; + height: var(--theia-icon-size); +} + +.p-TabBar.theia-app-centers .p-TabBar-tabIcon[class*="plugin-icon-"]::before, +.p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon[class*="plugin-icon-"]::before { + display: inline-block; +} + /* codicons */ .p-TabBar.theia-app-centers .p-TabBar-tabIcon.codicon, .p-TabBar-tab.p-mod-drag-image .p-TabBar-tabIcon.codicon { diff --git a/packages/core/src/common/quick-pick-service.ts b/packages/core/src/common/quick-pick-service.ts index d37481d76d658..dd3b07e1f8e1f 100644 --- a/packages/core/src/common/quick-pick-service.ts +++ b/packages/core/src/common/quick-pick-service.ts @@ -18,7 +18,6 @@ import * as fuzzy from 'fuzzy'; import { Event } from './event'; import { KeySequence } from './keys'; import { CancellationToken } from './cancellation'; -import { URI as Uri } from 'vscode-uri'; export const quickPickServicePath = '/services/quickPick'; export const QuickPickService = Symbol('QuickPickService'); @@ -53,7 +52,6 @@ export interface QuickPickItem { description?: string; detail?: string; keySequence?: KeySequence; - iconPath?: { light?: Uri; dark: Uri }; iconClasses?: string[]; alwaysShow?: boolean; highlights?: QuickPickItemHighlights; @@ -94,7 +92,6 @@ export interface QuickPickValue extends QuickPickItem { } export interface QuickInputButton { - iconPath?: { light?: Uri; dark: Uri }; iconClass?: string; tooltip?: string; /** diff --git a/packages/monaco/src/browser/style/index.css b/packages/monaco/src/browser/style/index.css index 7977874af744d..d03b5b4043382 100644 --- a/packages/monaco/src/browser/style/index.css +++ b/packages/monaco/src/browser/style/index.css @@ -176,6 +176,10 @@ text-align: left; } +.quick-input-list .monaco-icon-label::before { + height: 22px; +} + .codicon-file.default-file-icon.file-icon { padding-left: 2px; height: 22px; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 83da0b1d101c1..0b081d3904639 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -339,7 +339,7 @@ export interface TerminalServiceMain { * Create new Terminal with Terminal options. * @param options - object with parameters to create new terminal. */ - $createTerminal(id: string, options: theia.TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise; + $createTerminal(id: string, options: TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise; /** * Send text to the terminal by id. @@ -469,6 +469,10 @@ export interface TerminalServiceMain { $unregisterTerminalObserver(id: string): unknown; } +export interface TerminalOptions extends theia.TerminalOptions { + iconUrl?: string | { light: string; dark: string } | ThemeIcon; +} + export interface AutoFocus { autoFocusFirstEntry?: boolean; // TODO @@ -524,10 +528,10 @@ export interface QuickOpenExt { $onDidChangeSelection(sessionId: number, handles: number[]): void; /* eslint-disable max-len */ - showQuickPick(itemsOrItemsPromise: Array | Promise>, options: theia.QuickPickOptions & { canPickMany: true; }, + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Array | Promise>, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Array | Promise>, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Array | Promise>, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; showInput(options?: theia.InputBoxOptions, token?: theia.CancellationToken): PromiseLike; // showWorkspaceFolderPick(options?: theia.WorkspaceFolderPickOptions, token?: theia.CancellationToken): Promise @@ -651,7 +655,7 @@ export interface TransferQuickPickItem { handle: number; kind: 'item' | 'separator', label: string; - iconPath?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + iconUrl?: string | { light: string; dark: string } | ThemeIcon; description?: string; detail?: string; picked?: boolean; @@ -675,7 +679,7 @@ export interface TransferQuickPickOptions { export interface TransferQuickInputButton { handle?: number; - readonly iconPath: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; + readonly iconUrl?: string | { light: string; dark: string } | ThemeIcon; readonly tooltip?: string | undefined; } @@ -1514,12 +1518,10 @@ export interface WorkspaceEditEntryMetadataDto { needsConfirmation: boolean; label: string; description?: string; - iconPath?: { - id: string; - } | { + iconPath?: ThemeIcon | { light: UriComponents; dark: UriComponents; - } | ThemeIcon; + }; } export interface WorkspaceFileEditDto { diff --git a/packages/plugin-ext/src/main/browser/quick-open-main.ts b/packages/plugin-ext/src/main/browser/quick-open-main.ts index 39398d457438b..4c06ebdc8aa5f 100644 --- a/packages/plugin-ext/src/main/browser/quick-open-main.ts +++ b/packages/plugin-ext/src/main/browser/quick-open-main.ts @@ -42,11 +42,8 @@ import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposa import { CancellationToken } from '@theia/core/lib/common/cancellation'; import { MonacoQuickInputService } from '@theia/monaco/lib/browser/monaco-quick-input-service'; import { QuickInputButtons } from '../../plugin/types-impl'; -import * as monaco from '@theia/monaco-editor-core'; -import { UriComponents } from '../../common/uri-components'; -import { URI } from '@theia/core/shared/vscode-uri'; import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; -import { isUriComponents } from '@theia/monaco-editor-core/esm/vs/base/common/uri'; +import { PluginSharedStyle } from './plugin-shared-style'; import { QuickPickSeparator } from '@theia/core'; export interface QuickInputSession { @@ -54,16 +51,12 @@ export interface QuickInputSession { handlesToItems: Map; } -interface IconPath { - dark: URI, - light?: URI -}; - export class QuickOpenMainImpl implements QuickOpenMain, Disposable { private quickInputService: QuickInputService; private proxy: QuickOpenExt; private delegate: MonacoQuickInputService; + private sharedStyle: PluginSharedStyle; private readonly items: Record ({ - ...this.normalizeIconPath(button.iconPath), + iconClass: this.toIconClass(button.iconUrl), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, } as QuickInputButton)); diff --git a/packages/plugin-ext/src/main/browser/terminal-main.ts b/packages/plugin-ext/src/main/browser/terminal-main.ts index 059b4d9394be8..2ac7779f8cee5 100644 --- a/packages/plugin-ext/src/main/browser/terminal-main.ts +++ b/packages/plugin-ext/src/main/browser/terminal-main.ts @@ -15,21 +15,22 @@ // ***************************************************************************** import { interfaces } from '@theia/core/shared/inversify'; -import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser'; -import { TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin'; +import { ApplicationShell, WidgetOpenerOptions, codicon } from '@theia/core/lib/browser'; +import { TerminalEditorLocationOptions } from '@theia/plugin'; import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; import { TerminalProfileService } from '@theia/terminal/lib/browser/terminal-profile-service'; import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service'; -import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; +import { TerminalServiceMain, TerminalServiceExt, MAIN_RPC_CONTEXT, TerminalOptions } from '../../common/plugin-api-rpc'; import { RPCProtocol } from '../../common/rpc-protocol'; import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; 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'; import { PluginTerminalRegistry } from './plugin-terminal-registry'; -import { CancellationToken } from '@theia/core'; +import { CancellationToken, isObject } from '@theia/core'; import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin'; +import { PluginSharedStyle } from './plugin-shared-style'; +import { ThemeIcon } from '@theia/core/lib/common/theme'; import debounce = require('@theia/core/shared/lodash.debounce'); interface TerminalObserverData { @@ -49,6 +50,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin private readonly hostedPluginSupport: HostedPluginSupport; private readonly shell: ApplicationShell; private readonly extProxy: TerminalServiceExt; + private readonly sharedStyle: PluginSharedStyle; private readonly shellTerminalServer: ShellTerminalServerProxy; private readonly terminalLinkProviders: string[] = []; @@ -60,6 +62,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin this.terminalProfileService = container.get(TerminalProfileService); this.pluginTerminalRegistry = container.get(PluginTerminalRegistry); this.hostedPluginSupport = container.get(HostedPluginSupport); + this.sharedStyle = container.get(PluginSharedStyle); this.shell = container.get(ApplicationShell); this.shellTerminalServer = container.get(ShellTerminalServerProxy); this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT); @@ -153,7 +156,7 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin const terminal = await this.terminals.newTerminal({ id, title: options.name, - iconClass: getIconClass(options), + iconClass: this.toIconClass(options), shellPath: options.shellPath, shellArgs: options.shellArgs, cwd: options.cwd ? new URI(options.cwd) : undefined, @@ -329,6 +332,23 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin })); } + protected toIconClass(options: TerminalOptions): string | ThemeIcon | undefined { + const iconColor = isObject<{ id: string }>(options.color) && typeof options.color.id === 'string' ? options.color.id : undefined; + let iconClass: string; + if (options.iconUrl) { + if (typeof options.iconUrl === 'object' && 'id' in options.iconUrl) { + iconClass = codicon(options.iconUrl.id); + } else { + const iconReference = this.sharedStyle.toIconClass(options.iconUrl); + this.toDispose.push(iconReference); + iconClass = iconReference.object.iconClass; + } + } else { + iconClass = codicon('terminal'); + } + return iconColor ? { id: iconClass, color: { id: iconColor } } : iconClass; + } + $unregisterTerminalObserver(id: string): void { const observer = this.observers.get(id); if (observer) { diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index c37330cbf90de..2dc28f72b501b 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -483,7 +483,7 @@ export function createAPIFactory( }, // eslint-disable-next-line @typescript-eslint/no-explicit-any showQuickPick(items: any, options?: theia.QuickPickOptions, token?: theia.CancellationToken): any { - return quickOpenExt.showQuickPick(items, options, token); + return quickOpenExt.showQuickPick(plugin, items, options, token); }, createQuickPick(): theia.QuickPick { return quickOpenExt.createQuickPick(plugin); @@ -564,7 +564,7 @@ export function createAPIFactory( createTerminal(nameOrOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions | theia.ExtensionTerminalOptions | (string | undefined), shellPath?: string, shellArgs?: string[] | string): theia.Terminal { - return terminalExt.createTerminal(nameOrOptions, shellPath, shellArgs); + return terminalExt.createTerminal(plugin, nameOrOptions, shellPath, shellArgs); }, onDidChangeTerminalState, onDidCloseTerminal, diff --git a/packages/plugin-ext/src/plugin/plugin-icon-path.ts b/packages/plugin-ext/src/plugin/plugin-icon-path.ts index 610e7dcc1b3af..54b7b034a2aac 100644 --- a/packages/plugin-ext/src/plugin/plugin-icon-path.ts +++ b/packages/plugin-ext/src/plugin/plugin-icon-path.ts @@ -24,8 +24,8 @@ export type PluginIconPath = string | URI | { dark: string | URI }; export namespace PluginIconPath { - export function toUrl(iconPath: PluginIconPath | undefined, plugin: Plugin): IconUrl | undefined { - if (!iconPath) { + export function toUrl(iconPath: unknown, plugin: Plugin): IconUrl | undefined { + if (!is(iconPath)) { return undefined; } if (typeof iconPath === 'object' && 'light' in iconPath) { @@ -36,6 +36,9 @@ export namespace PluginIconPath { } return asString(iconPath, plugin); } + export function is(item: unknown): item is PluginIconPath { + return typeof item === 'string' || item instanceof URI || typeof item === 'object' && !!item && 'light' in item && 'dark' in item; + } export function asString(arg: string | URI, plugin: Plugin): string { arg = arg instanceof URI && arg.scheme === 'file' ? arg.fsPath : arg; if (typeof arg !== 'string') { diff --git a/packages/plugin-ext/src/plugin/quick-open.ts b/packages/plugin-ext/src/plugin/quick-open.ts index 7ed2773f56849..c9f51aa6c3f60 100644 --- a/packages/plugin-ext/src/plugin/quick-open.ts +++ b/packages/plugin-ext/src/plugin/quick-open.ts @@ -30,8 +30,8 @@ import { convertToTransferQuickPickItems } from './type-converters'; import { PluginPackage } from '../common/plugin-protocol'; import { QuickInputButtonHandle } from '@theia/core/lib/browser'; import { MaybePromise } from '@theia/core/lib/common/types'; -import { ThemeIcon as MonacoThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; import { Severity } from '@theia/core/lib/common/severity'; +import { PluginIconPath } from './plugin-icon-path'; const canceledName = 'Canceled'; /** @@ -42,27 +42,6 @@ export function isPromiseCanceledError(error: any): boolean { return error instanceof Error && error.name === canceledName && error.message === canceledName; } -export function getIconUris(iconPath: theia.QuickInputButton['iconPath']): { dark: URI, light: URI } | { id: string } { - if (ThemeIcon.is(iconPath)) { - return { id: iconPath.id }; - } - const dark = getDarkIconUri(iconPath as URI | { light: URI; dark: URI; }); - const light = getLightIconUri(iconPath as URI | { light: URI; dark: URI; }); - // Tolerate strings: https://github.com/microsoft/vscode/issues/110432#issuecomment-726144556 - return { - dark: typeof dark === 'string' ? URI.file(dark) : dark, - light: typeof light === 'string' ? URI.file(light) : light - }; -} - -export function getLightIconUri(iconPath: URI | { light: URI; dark: URI; }): URI { - return typeof iconPath === 'object' && 'light' in iconPath ? iconPath.light : iconPath; -} - -export function getDarkIconUri(iconPath: URI | { light: URI; dark: URI; }): URI { - return typeof iconPath === 'object' && 'dark' in iconPath ? iconPath.dark : iconPath; -} - type Item = theia.QuickPickItem | string; export class QuickOpenExtImpl implements QuickOpenExt { @@ -77,10 +56,10 @@ export class QuickOpenExtImpl implements QuickOpenExt { } /* eslint-disable max-len */ - showQuickPick(itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; - showQuickPick(itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; - showQuickPick(itemsOrItemsPromise: Item[] | Promise, options?: theia.QuickPickOptions, token: theia.CancellationToken = CancellationToken.None): Promise { + showQuickPick(plugin: Plugin, itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options: theia.QuickPickOptions & { canPickMany: true; }, token?: theia.CancellationToken): Promise | undefined>; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: string[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: theia.QuickPickItem[] | Promise, options?: theia.QuickPickOptions, token?: theia.CancellationToken): Promise; + showQuickPick(plugin: Plugin, itemsOrItemsPromise: Item[] | Promise, options?: theia.QuickPickOptions, token: theia.CancellationToken = CancellationToken.None): Promise { this.onDidSelectItem = undefined; const itemsPromise = Promise.resolve(itemsOrItemsPromise); @@ -104,7 +83,7 @@ export class QuickOpenExtImpl implements QuickOpenExt { return undefined; } return itemsPromise.then(async items => { - const pickItems = convertToTransferQuickPickItems(items); + const pickItems = convertToTransferQuickPickItems(plugin, items); if (options && typeof options.onDidSelectItem === 'function') { this.onDidSelectItem = handle => { @@ -398,8 +377,7 @@ export class QuickInputExt implements theia.QuickInput { }); this.update({ buttons: buttons.map((button, i) => ({ - iconPath: getIconUris(button.iconPath), - iconClass: ThemeIcon.is(button.iconPath) ? MonacoThemeIcon.asClassName(button.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(button.iconPath, this.plugin) ?? ThemeIcon.get(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : i, })) @@ -640,15 +618,14 @@ export class QuickPickExt extends QuickInputExt i pickItems.push({ kind: 'item', label: item.label, - iconPath: item.iconPath ? getIconUris(item.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(item.iconPath, this.plugin) ?? ThemeIcon.get(item.iconPath), description: item.description, handle, detail: item.detail, picked: item.picked, alwaysShow: item.alwaysShow, buttons: item.buttons?.map((button, index) => ({ - iconPath: getIconUris(button.iconPath), - iconClass: ThemeIcon.is(button.iconPath) ? MonacoThemeIcon.asClassName(button.iconPath) : undefined, + iconUrl: PluginIconPath.toUrl(button.iconPath, this.plugin) ?? ThemeIcon.get(button.iconPath), tooltip: button.tooltip, handle: button === QuickInputButtons.Back ? -1 : index, })) diff --git a/packages/plugin-ext/src/plugin/terminal-ext.ts b/packages/plugin-ext/src/plugin/terminal-ext.ts index d23967b13ca77..d8e2a3f01bd70 100644 --- a/packages/plugin-ext/src/plugin/terminal-ext.ts +++ b/packages/plugin-ext/src/plugin/terminal-ext.ts @@ -13,10 +13,10 @@ // // 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 { inject, injectable } from '@theia/core/shared/inversify'; -import { Terminal, TerminalOptions, PseudoTerminalOptions, ExtensionTerminalOptions, TerminalState } from '@theia/plugin'; -import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; +import { TerminalServiceExt, TerminalServiceMain, PLUGIN_RPC_CONTEXT, Plugin, TerminalOptions } 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'; @@ -26,28 +26,13 @@ import * as Converter from './type-converters'; import { Disposable, EnvironmentVariableMutatorType, TerminalExitReason, ThemeIcon } from './types-impl'; 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/base/common/themables'; - -export function getIconUris(iconPath: theia.TerminalOptions['iconPath']): { id: string } | undefined { - if (ThemeIcon.is(iconPath)) { - return { id: iconPath.id }; - } - return undefined; -} - -export function getIconClass(options: theia.TerminalOptions | theia.ExtensionTerminalOptions): string | undefined { - const iconClass = getIconUris(options.iconPath); - if (iconClass) { - return MonacoThemeIcon.asClassName(iconClass); - } - return undefined; -} +import { PluginIconPath } from './plugin-icon-path'; /** * Provides high level terminal plugin api to use in the Theia plugins. * This service allow(with help proxy) create and use terminal emulator. */ - @injectable() +@injectable() export class TerminalServiceExtImpl implements TerminalServiceExt { private readonly proxy: TerminalServiceMain; @@ -59,17 +44,17 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { private readonly terminalLinkProviders = new Map(); private readonly terminalObservers = new Map(); private readonly terminalProfileProviders = new Map(); - private readonly onDidCloseTerminalEmitter = new Emitter(); - readonly onDidCloseTerminal: theia.Event = this.onDidCloseTerminalEmitter.event; + private readonly onDidCloseTerminalEmitter = new Emitter(); + readonly onDidCloseTerminal: theia.Event = this.onDidCloseTerminalEmitter.event; - private readonly onDidOpenTerminalEmitter = new Emitter(); - readonly onDidOpenTerminal: theia.Event = this.onDidOpenTerminalEmitter.event; + private readonly onDidOpenTerminalEmitter = new Emitter(); + readonly onDidOpenTerminal: theia.Event = this.onDidOpenTerminalEmitter.event; - private readonly onDidChangeActiveTerminalEmitter = new Emitter(); - readonly onDidChangeActiveTerminal: theia.Event = this.onDidChangeActiveTerminalEmitter.event; + private readonly onDidChangeActiveTerminalEmitter = new Emitter(); + readonly onDidChangeActiveTerminal: theia.Event = this.onDidChangeActiveTerminalEmitter.event; - private readonly onDidChangeTerminalStateEmitter = new Emitter(); - readonly onDidChangeTerminalState: theia.Event = this.onDidChangeTerminalStateEmitter.event; + private readonly onDidChangeTerminalStateEmitter = new Emitter(); + readonly onDidChangeTerminalState: theia.Event = this.onDidChangeTerminalStateEmitter.event; protected environmentVariableCollections: MultiKeyMap = new MultiKeyMap(2); @@ -97,9 +82,10 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } createTerminal( - nameOrOptions: TerminalOptions | PseudoTerminalOptions | ExtensionTerminalOptions | (string | undefined), + plugin: Plugin, + nameOrOptions: theia.TerminalOptions | theia.PseudoTerminalOptions | theia.ExtensionTerminalOptions | string | undefined, shellPath?: string, shellArgs?: string[] | string - ): Terminal { + ): theia.Terminal { const id = `plugin-terminal-${UUID.uuid4()}`; let options: TerminalOptions; let pseudoTerminal: theia.Pseudoterminal | undefined = undefined; @@ -122,7 +108,6 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } let parentId; - if (options.location && typeof options.location === 'object' && 'parentTerminal' in options.location) { const parentTerminal = options.location.parentTerminal; if (parentTerminal instanceof TerminalExtImpl) { @@ -135,6 +120,15 @@ export class TerminalServiceExtImpl implements TerminalServiceExt { } } + if (typeof nameOrOptions === 'object' && 'iconPath' in nameOrOptions) { + const iconPath = nameOrOptions.iconPath; + options.iconUrl = PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath); + } + + if (typeof nameOrOptions === 'object' && 'color' in nameOrOptions) { + options.color = nameOrOptions.color; + } + this.proxy.$createTerminal(id, options, parentId, !!pseudoTerminal); let creationOptions: theia.TerminalOptions | theia.ExtensionTerminalOptions = options; @@ -462,7 +456,7 @@ export class EnvironmentVariableCollectionImpl implements theia.GlobalEnvironmen } } -export class TerminalExtImpl implements Terminal { +export class TerminalExtImpl implements theia.Terminal { name: string; @@ -476,9 +470,9 @@ export class TerminalExtImpl implements Terminal { return this.deferredProcessId.promise; } - readonly creationOptions: Readonly; + readonly creationOptions: Readonly; - state: TerminalState = { isInteractedWith: false }; + state: theia.TerminalState = { isInteractedWith: false }; constructor(private readonly proxy: TerminalServiceMain, private readonly options: theia.TerminalOptions | theia.ExtensionTerminalOptions) { this.creationOptions = this.options; diff --git a/packages/plugin-ext/src/plugin/tree/tree-views.ts b/packages/plugin-ext/src/plugin/tree/tree-views.ts index d7a130c2a3273..5dec3ca816fdd 100644 --- a/packages/plugin-ext/src/plugin/tree/tree-views.ts +++ b/packages/plugin-ext/src/plugin/tree/tree-views.ts @@ -446,7 +446,7 @@ class TreeViewExtImpl implements Disposable { } else if (ThemeIcon.is(iconPath)) { themeIcon = iconPath; } else { - iconUrl = PluginIconPath.toUrl(iconPath, this.plugin); + iconUrl = PluginIconPath.toUrl(iconPath, this.plugin); } let checkboxInfo; diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index 86e70d7258510..557b412ed4e43 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -16,7 +16,7 @@ import * as theia from '@theia/plugin'; import * as lstypes from '@theia/core/shared/vscode-languageserver-protocol'; -import { InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, QuickPickItemKind, URI } from './types-impl'; +import { InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, QuickPickItemKind, ThemeIcon, URI } from './types-impl'; import * as rpc from '../common/plugin-api-rpc'; import { DecorationOptions, EditorPosition, Plugin, Position, WorkspaceTextEditDto, WorkspaceFileEditDto, Selection, TaskDto, WorkspaceEditDto @@ -35,7 +35,7 @@ import { CellRange, isTextStreamMime } from '@theia/notebook/lib/common'; import { MarkdownString as MarkdownStringDTO } from '@theia/core/lib/common/markdown-rendering'; import { TestItemDTO, TestMessageDTO } from '../common/test-types'; -import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/base/common/themables'; +import { PluginIconPath } from './plugin-icon-path'; const SIDE_GROUP = -2; const ACTIVE_GROUP = -1; @@ -1226,7 +1226,7 @@ export function convertIconPath(iconPath: types.URI | { light: types.URI; dark: dark: iconPath.dark.toJSON(), light: iconPath.light?.toJSON() }; - } else if (ThemeIcon.isThemeIcon(iconPath)) { + } else if (ThemeIcon.is(iconPath)) { return { id: iconPath.id, color: iconPath.color ? { id: iconPath.color.id } : undefined @@ -1236,19 +1236,19 @@ export function convertIconPath(iconPath: types.URI | { light: types.URI; dark: } } -export function convertQuickInputButton(button: theia.QuickInputButton, index: number): rpc.TransferQuickInputButton { +export function convertQuickInputButton(plugin: Plugin, button: theia.QuickInputButton, index: number): rpc.TransferQuickInputButton { const iconPath = convertIconPath(button.iconPath); if (!iconPath) { throw new Error(`Could not convert icon path: '${button.iconPath}'`); } return { handle: index, - iconPath: iconPath, + iconUrl: PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath), tooltip: button.tooltip }; } -export function convertToTransferQuickPickItems(items: (theia.QuickPickItem | string)[]): rpc.TransferQuickPickItem[] { +export function convertToTransferQuickPickItems(plugin: Plugin, items: (theia.QuickPickItem | string)[]): rpc.TransferQuickPickItem[] { return items.map((item, index) => { if (typeof item === 'string') { return { kind: 'item', label: item, handle: index }; @@ -1260,11 +1260,11 @@ export function convertToTransferQuickPickItems(items: (theia.QuickPickItem | st kind: 'item', label, description, - iconPath: convertIconPath(iconPath), + iconUrl: PluginIconPath.toUrl(iconPath, plugin) ?? ThemeIcon.get(iconPath), detail, picked, alwaysShow, - buttons: buttons ? buttons.map(convertQuickInputButton) : undefined, + buttons: buttons ? buttons.map((button, i) => convertQuickInputButton(plugin, button, i)) : undefined, handle: index, }; } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 2d96f2b75bc39..b0c4f0a8cd1a2 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -744,6 +744,9 @@ export namespace ThemeIcon { export function is(item: unknown): item is ThemeIcon { return isObject(item) && 'id' in item; } + export function get(item: unknown): ThemeIcon | undefined { + return is(item) ? item : undefined; + } } export enum TextEditorRevealType { diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 1767f570a9a8f..98a0f69b3ca0f 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -3172,7 +3172,7 @@ export module '@theia/plugin' { /** * The icon path or {@link ThemeIcon} for the terminal. */ - iconPath?: ThemeIcon; + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; /** * The icon {@link ThemeColor} for the terminal. @@ -3293,7 +3293,7 @@ export module '@theia/plugin' { /** * The icon path or {@link ThemeIcon} for the terminal. */ - iconPath?: ThemeIcon; + iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; /** * The icon {@link ThemeColor} for the terminal. diff --git a/packages/terminal/src/browser/base/terminal-widget.ts b/packages/terminal/src/browser/base/terminal-widget.ts index 8ccb29477e2dc..7c9443f94de7f 100644 --- a/packages/terminal/src/browser/base/terminal-widget.ts +++ b/packages/terminal/src/browser/base/terminal-widget.ts @@ -16,11 +16,12 @@ import { Event, ViewColumn } from '@theia/core'; import { BaseWidget } from '@theia/core/lib/browser'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering/markdown-string'; +import { ThemeIcon } from '@theia/core/lib/common/theme'; import { CommandLineOptions } from '@theia/process/lib/common/shell-command-builder'; 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; @@ -191,9 +192,9 @@ export interface TerminalWidgetOptions { readonly title?: string; /** - * icon class + * icon class with or without color modifier */ - readonly iconClass?: string; + readonly iconClass?: string | ThemeIcon; /** * Path to the executable shell. For example: `/bin/bash`, `bash`, `sh`. diff --git a/packages/terminal/src/browser/terminal-frontend-contribution.ts b/packages/terminal/src/browser/terminal-frontend-contribution.ts index caa5199954f1f..ccd9f9a7254da 100644 --- a/packages/terminal/src/browser/terminal-frontend-contribution.ts +++ b/packages/terminal/src/browser/terminal-frontend-contribution.ts @@ -312,7 +312,8 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu } else { this.contributedProfileStore.registerTerminalProfile('SHELL', new ShellTerminalProfile(this, { shellPath: await this.resolveShellPath('${SHELL}')!, - shellArgs: ['-l'] + shellArgs: ['-l'], + iconClass: 'codicon codicon-terminal' })); } diff --git a/packages/terminal/src/browser/terminal-widget-impl.ts b/packages/terminal/src/browser/terminal-widget-impl.ts index 42924cfdefc93..5b923a278ce1c 100644 --- a/packages/terminal/src/browser/terminal-widget-impl.ts +++ b/packages/terminal/src/browser/terminal-widget-impl.ts @@ -17,9 +17,10 @@ import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify'; -import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS } from '@theia/core'; +import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS, generateUuid } from '@theia/core'; import { - Widget, Message, StatefulWidget, isFirefox, MessageLoop, KeyCode, codicon, ExtractableWidget, ContextMenuRenderer + Widget, Message, StatefulWidget, isFirefox, MessageLoop, KeyCode, ExtractableWidget, ContextMenuRenderer, + DecorationStyle } from '@theia/core/lib/browser'; import { isOSX } from '@theia/core/lib/common'; import { WorkspaceService } from '@theia/workspace/lib/browser'; @@ -48,6 +49,7 @@ import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markd import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-preview-widget'; import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider'; +import { ColorRegistry } from '@theia/core/lib/browser/color-registry'; export const TERMINAL_WIDGET_FACTORY_ID = 'terminal'; @@ -104,6 +106,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget protected isAttachedCloseListener: boolean = false; protected shown = false; protected enhancedPreviewNode: Node | undefined; + protected styleElement: HTMLStyleElement | undefined; override lastCwd = new URI(); @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService; @@ -119,6 +122,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget @inject(TerminalSearchWidgetFactory) protected readonly terminalSearchBoxFactory: TerminalSearchWidgetFactory; @inject(TerminalCopyOnSelectionHandler) protected readonly copyOnSelectionHandler: TerminalCopyOnSelectionHandler; @inject(TerminalThemeService) protected readonly themeService: TerminalThemeService; + @inject(ColorRegistry) protected readonly colorRegistry: ColorRegistry; @inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder; @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer; @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory; @@ -163,12 +167,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget @postConstruct() protected init(): void { this.setTitle(this.options.title || TerminalWidgetImpl.LABEL); - - if (this.options.iconClass) { - this.title.iconClass = this.options.iconClass; - } else { - this.title.iconClass = codicon('terminal'); - } + this.setIconClass(); if (this.options.kind) { this.terminalKind = this.options.kind; @@ -213,7 +212,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.update(); })); - this.toDispose.push(this.themeService.onDidChange(() => this.term.options.theme = this.themeService.theme)); + this.toDispose.push(this.themeService.onDidChange(() => { + this.term.options.theme = this.themeService.theme; + this.setIconClass(); + })); this.attachCustomKeyEventHandler(); const titleChangeListenerDispose = this.term.onTitleChange((title: string) => { if (this.options.useServerTitle) { @@ -333,6 +335,31 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget this.term.options.fastScrollSensitivity = this.preferences.get('terminal.integrated.fastScrollSensitivity'); } + protected setIconClass(): void { + this.styleElement?.remove(); + if (this.options.iconClass) { + const iconClass = this.options.iconClass; + if (typeof iconClass === 'string') { + this.title.iconClass = iconClass; + } else { + const iconClasses: string[] = []; + iconClasses.push(iconClass.id); + if (iconClass.color) { + this.styleElement = DecorationStyle.createStyleElement(`${this.terminalId}-terminal-style`); + const classId = 'terminal-icon-' + generateUuid().replace(/-/g, ''); + const color = this.colorRegistry.getCurrentColor(iconClass.color.id); + this.styleElement.textContent = ` + .${classId}::before { + color: ${color}; + } + `; + iconClasses.push(classId); + } + this.title.iconClass = iconClasses.join(' '); + } + } + } + private setCursorBlink(blink: boolean): void { if (this.term.options.cursorBlink !== blink) { this.term.options.cursorBlink = blink; @@ -812,6 +839,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget // don't use preview node anymore. rendered markdown will be disposed on super call this.enhancedPreviewNode = undefined; } + this.styleElement?.remove(); super.dispose(); }