// ***************************************************************************** // Copyright (C) 2018 Red Hat, Inc. 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 * as theia from '@theia/plugin'; import { ThemeColor, StatusBarAlignment } from '../types-impl'; import { StatusBarMessageRegistryMain } from '../../common/plugin-api-rpc'; import { UUID } from '@theia/core/shared/@lumino/coreutils'; import { CommandRegistryImpl } from '../command-registry'; import { MarkdownString } from '../markdown-string'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; export class StatusBarItemImpl implements theia.StatusBarItem { /** Map from allowed background colors to corresponding foreground colors. */ private static BACKGROUND_COLORS = new Map([ ['statusBarItem.errorBackground', 'statusBarItem.errorForeground'], ['statusBarItem.warningBackground', 'statusBarItem.warningForeground'], ['statusBarItem.prominentBackground', 'statusBarItem.prominentForeground'], ['statusBarItem.remoteBackground', 'statusBarItem.remoteForeground'], ['statusBarItem.offlineBackground', 'statusBarItem.offlineForeground'] ]); private _id: string; private _alignment: StatusBarAlignment; private _priority: number; private _name: string | undefined; private _text: string; private _tooltip: string | theia.MarkdownString | undefined; private _tooltip2: string | theia.MarkdownString | undefined | ((token: theia.CancellationToken) => theia.ProviderResult); private _color: string | ThemeColor | undefined; private _backgroundColor: ThemeColor | undefined; private _command: string | theia.Command; private _accessibilityInformation: theia.AccessibilityInformation; private _isVisible: boolean; private _timeoutHandle: NodeJS.Timeout | undefined; _proxy: StatusBarMessageRegistryMain; constructor(proxy: StatusBarMessageRegistryMain, private readonly commandRegistry: CommandRegistryImpl, alignment: StatusBarAlignment = StatusBarAlignment.Left, priority: number = 0, id = StatusBarItemImpl.nextId(), private onDispose?: () => void) { this._proxy = proxy; this._alignment = alignment; this._priority = priority; this._id = id; } public get id(): string { return this._id; } public get alignment(): theia.StatusBarAlignment { return this._alignment; } public get priority(): number { return this._priority; } public get name(): string | undefined { return this._name; } public get text(): string { return this._text; } public get tooltip(): string | theia.MarkdownString | undefined { return this._tooltip; } public get tooltip2(): string | theia.MarkdownString | undefined | ((token: theia.CancellationToken) => theia.ProviderResult) { if (typeof this._tooltip2 === 'function') { const getTooltip = this._tooltip2.bind(this); return (token: theia.CancellationToken) => Promise.resolve(getTooltip(token)).then(res => { this.processTooltip(res); return res; }); } return this._tooltip2; } public get color(): string | ThemeColor | undefined { return this._color; } public get backgroundColor(): ThemeColor | undefined { return this._backgroundColor; } public get command(): string | theia.Command { return this._command; } public get accessibilityInformation(): theia.AccessibilityInformation { return this._accessibilityInformation; } public set name(name: string | undefined) { this._name = name; this.update(); } public set text(text: string) { this._text = text; this.update(); } public set tooltip2(tooltip: string | theia.MarkdownString | ((token: theia.CancellationToken) => theia.ProviderResult) | undefined) { this.processTooltip(tooltip); this._tooltip2 = tooltip; this.update(); } public set tooltip(tooltip: string | theia.MarkdownString | undefined) { this.processTooltip(tooltip); this._tooltip = tooltip; this.update(); } public set color(color: string | ThemeColor | undefined) { this._color = color; this.update(); } public set backgroundColor(backgroundColor: ThemeColor | undefined) { if (backgroundColor && StatusBarItemImpl.BACKGROUND_COLORS.has(backgroundColor.id)) { this._backgroundColor = backgroundColor; } else { this._backgroundColor = undefined; } this.update(); } public set command(command: string | theia.Command) { this._command = command; this.update(); } public set accessibilityInformation(information: theia.AccessibilityInformation) { this._accessibilityInformation = information; this.update(); } public show(): void { this._isVisible = true; this.update(); } public hide(): void { if (this._timeoutHandle) { clearTimeout(this._timeoutHandle); } this._proxy.$dispose(this.id); this._isVisible = false; } private processTooltip(tooltip: string | theia.MarkdownString | ((token: theia.CancellationToken) => theia.ProviderResult) | undefined): void { if (!MarkdownString.isMarkdownString(tooltip)) { return; } const content = tooltip.value; // Find all command links in the markdown content const regex = /\[([^\]]+)\]\(command:([^?\s\)]+)(?:\?([^\s\)]+))?([^\)]*)\)/g; let match; let updatedContent = content; while ((match = regex.exec(content)) !== null) { const linkText = match[1]; const commandId = match[2]; const argsEncoded = match[3]; // This captures the encoded arguments const tooltipPart = match[4] || ''; // This captures any tooltip or additional content after the command and args let args: unknown[] = []; if (argsEncoded) { try { const decoded = decodeURIComponent(argsEncoded); args = JSON.parse(decoded); } catch (e) { console.error('Failed to parse command arguments:', e); } } const safeCommand = this.commandRegistry.converter.toSafeCommand( { command: commandId, title: linkText, arguments: Array.isArray(args) ? args : [args] }, new DisposableCollection() ); if (safeCommand?.id) { let newArgsPart = ''; if (safeCommand.arguments && safeCommand.arguments.length > 0) { newArgsPart = `?${encodeURIComponent(JSON.stringify(safeCommand.arguments))}`; } const argsPart = argsEncoded ? `?${argsEncoded}` : ''; const originalLink = `[${linkText}](command:${commandId}${argsPart}${tooltipPart})`; const safeLink = `[${linkText}](command:${safeCommand.id}${newArgsPart}${tooltipPart})`; updatedContent = updatedContent.replace(originalLink, safeLink); } } if (updatedContent !== content) { tooltip.value = updatedContent; } } private update(): void { if (!this._isVisible) { return; } if (this._timeoutHandle) { clearTimeout(this._timeoutHandle); } // Defer the update so that multiple changes to setters don't cause a redraw each this._timeoutHandle = setTimeout(() => { this._timeoutHandle = undefined; const commandId = typeof this.command === 'object' ? this.command.command : this.command; const args = typeof this.command === 'object' ? this.command.arguments : undefined; let color = this.color; if (this.backgroundColor) { // If an error or warning background color is set, set the corresponding foreground color color = StatusBarItemImpl.BACKGROUND_COLORS.get(this.backgroundColor.id); } const tooltip = typeof this._tooltip2 === 'function' ? true : this._tooltip2 ?? this.tooltip; // Set to status bar this._proxy.$setMessage( this.id, this.name, this.text, this.priority, this.alignment, typeof color === 'string' ? color : color?.id, this.backgroundColor?.id, tooltip, commandId, this.accessibilityInformation, args); }, 0); } public dispose(): void { this.onDispose?.(); this.hide(); } static nextId(): string { return StatusBarItemImpl.ID_PREFIX + ':' + UUID.uuid4(); } static ID_PREFIX = 'plugin-status-bar-item'; }