From 7a8acdc11807d49a30643edb073dbfe65e20509c Mon Sep 17 00:00:00 2001 From: dankeboy36 Date: Sat, 26 Oct 2024 15:39:34 +0200 Subject: [PATCH 1/2] fix: align `viewsWelcome` behavior to VS Code Ref: eclipse-theia/theia#14309 Signed-off-by: dankeboy36 --- arduino-ide-extension/package.json | 1 + .../browser/arduino-ide-frontend-module.ts | 56 ++++- .../theia/plugin-ext/tree-view-widget.tsx | 228 ++++++++++++++++++ .../src/node/arduino-ide-backend-module.ts | 11 +- .../theia/plugin-ext-vscode/scanner-vscode.ts | 44 ++++ 5 files changed, 338 insertions(+), 2 deletions(-) create mode 100644 arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx create mode 100644 arduino-ide-extension/src/node/theia/plugin-ext-vscode/scanner-vscode.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index bef1f07b7..b28dda5a5 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -39,6 +39,7 @@ "@theia/outline-view": "1.41.0", "@theia/output": "1.41.0", "@theia/plugin-ext": "1.41.0", + "@theia/plugin-ext-vscode": "1.41.0", "@theia/preferences": "1.41.0", "@theia/scm": "1.41.0", "@theia/search-in-workspace": "1.41.0", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 89b2b218d..2b170dd7b 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -1,5 +1,9 @@ import '../../src/browser/style/index.css'; -import { Container, ContainerModule } from '@theia/core/shared/inversify'; +import { + Container, + ContainerModule, + interfaces, +} from '@theia/core/shared/inversify'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; import { CommandContribution } from '@theia/core/lib/common/command'; import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution'; @@ -53,6 +57,8 @@ import { DockPanelRenderer as TheiaDockPanelRenderer, TabBarRendererFactory, ContextMenuRenderer, + createTreeContainer, + TreeWidget, } from '@theia/core/lib/browser'; import { MenuContribution } from '@theia/core/lib/common/menu'; import { @@ -372,6 +378,15 @@ import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session- import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget'; import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget'; import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget'; +import { + PluginTree, + PluginTreeModel, + TreeViewWidgetOptions, + VIEW_ITEM_CONTEXT_MENU, +} from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget'; +import { TreeViewDecoratorService } from '@theia/plugin-ext/lib/main/browser/view/tree-view-decorator-service'; +import { PLUGIN_VIEW_DATA_FACTORY_ID } from '@theia/plugin-ext/lib/main/browser/view/plugin-view-registry'; +import { TreeViewWidget } from './theia/plugin-ext/tree-view-widget'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -1082,4 +1097,43 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaTerminalFrontendContribution).toService( TerminalFrontendContribution ); + + bindViewsWelcome_TheiaGH14309({ bind, widget: TreeViewWidget }); }); + +// Align the viewsWelcome rendering with VS Code (https://github.com/eclipse-theia/theia/issues/14309) +// Copied from Theia code but with customized TreeViewWidget with the customized viewsWelcome rendering +// https://github.com/eclipse-theia/theia/blob/0c5f69455d9ee355b1a7ca510ffa63d2b20f0c77/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts#L159-L181 +function bindViewsWelcome_TheiaGH14309({ + bind, + widget, +}: { + bind: interfaces.Bind; + widget: interfaces.Newable; +}) { + bind(WidgetFactory) + .toDynamicValue(({ container }) => ({ + id: PLUGIN_VIEW_DATA_FACTORY_ID, + createWidget: (options: TreeViewWidgetOptions) => { + const props = { + contextMenuPath: VIEW_ITEM_CONTEXT_MENU, + expandOnlyOnExpansionToggleClick: true, + expansionTogglePadding: 22, + globalSelection: true, + leftPadding: 8, + search: true, + multiSelect: options.multiSelect, + }; + const child = createTreeContainer(container, { + props, + tree: PluginTree, + model: PluginTreeModel, + widget, + decoratorService: TreeViewDecoratorService, + }); + child.bind(TreeViewWidgetOptions).toConstantValue(options); + return child.get(TreeWidget); + }, + })) + .inSingletonScope(); +} diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx b/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx new file mode 100644 index 000000000..c55b91a12 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx @@ -0,0 +1,228 @@ +// import { OpenerService } from '@theia/core/lib/browser'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { /*inject,*/ injectable } from '@theia/core/shared/inversify'; +import React from '@theia/core/shared/react'; +import { TreeViewWidget as TheiaTreeViewWidget } from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget'; + +@injectable() +export class TreeViewWidget extends TheiaTreeViewWidget { + // @inject(OpenerService) + // private readonly openerService: OpenerService; + private readonly toDisposeBeforeUpdateViewWelcomeNodes = + new DisposableCollection(); + + // The actual rewrite of the viewsWelcome rendering aligned to VS Code to fix https://github.com/eclipse-theia/theia/issues/14309 + // Based on https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/workbench/browser/parts/views/viewPane.ts#L228-L299 + protected override updateViewWelcomeNodes(): void { + this.toDisposeBeforeUpdateViewWelcomeNodes.dispose(); + const viewWelcomes = this.visibleItems.sort((a, b) => a.order - b.order); + this.viewWelcomeNodes = []; + const allEnablementKeys: Set[] = []; + // the plugin-view-registry will push the changes when there is a change in the when context + // this listener is to update the view when the `enablement` of the viewWelcomes changes + this.toDisposeBeforeUpdateViewWelcomeNodes.push( + this.contextKeyService.onDidChange((event) => { + if (allEnablementKeys.some((keys) => event.affects(keys))) { + this.updateViewWelcomeNodes(); + this.update(); + } + }) + ); + // TODO: support `renderSecondaryButtons` prop from VS Code? + for (const viewWelcome of viewWelcomes) { + const { content } = viewWelcome; + const enablement = isEnablementAware(viewWelcome) + ? viewWelcome.enablement + : undefined; + const enablementKeys = enablement + ? this.contextKeyService.parseKeys(enablement) + : undefined; + if (enablementKeys) { + allEnablementKeys.push(enablementKeys); + } + const lines = content.split('\n'); + + for (let line of lines) { + line = line.trim(); + + if (!line) { + continue; + } + + const linkedText = parseLinkedText(line); + + if ( + linkedText.nodes.length === 1 && + typeof linkedText.nodes[0] !== 'string' + ) { + const node = linkedText.nodes[0]; + this.viewWelcomeNodes.push( + this.renderButtonNode( + node, + this.viewWelcomeNodes.length, + enablement + ) + ); + } else { + const paragraphNodes: React.ReactNode[] = []; + for (const node of linkedText.nodes) { + if (typeof node === 'string') { + paragraphNodes.push( + this.renderTextNode(node, this.viewWelcomeNodes.length) + ); + } else { + paragraphNodes.push( + this.renderCommandLinkNode( + node, + this.viewWelcomeNodes.length, + enablement + ) + ); + } + } + if (paragraphNodes.length) { + this.viewWelcomeNodes.push( +

+ {...paragraphNodes} +

+ ); + } + } + } + } + } + + protected override renderButtonNode( + node: ILink, + lineKey: string | number, + enablement: string | undefined = undefined + ): React.ReactNode { + return ( +
+ +
+ ); + } + + protected override renderCommandLinkNode( + node: ILink, + linkKey: string | number, + enablement: string | undefined = undefined + ): React.ReactNode { + return ( + this.open(e, node)} + > + {node.label} + + ); + } + + protected override renderTextNode( + node: string, + textKey: string | number + ): React.ReactNode { + return {node}; + } + + protected override getLinkClassName( + href: string, + enablement: string | undefined = undefined + ): string { + const classNames = ['theia-WelcomeViewCommandLink']; + // Only command-backed links can be disabled. All other, https:, file: remain enabled + if (href.startsWith('command:') && !this.isEnabled(enablement)) { + classNames.push('disabled'); + } + return classNames.join(' '); + } + + private open(event: React.MouseEvent, node: ILink): void { + event.preventDefault(); + if (node.href.startsWith('command:')) { + const commandId = node.href.substring('commands:'.length - 1); + this.commands.executeCommand(commandId); + } else if (node.href.startsWith('file:')) { + // TODO: check what Code does + } else if (node.href.startsWith('https:')) { + this.windowService.openNewWindow(node.href, { external: true }); + } + } + + /** + * @param enablement [when context](https://code.visualstudio.com/api/references/when-clause-contexts) expression string + */ + private isEnabled(enablement: string | undefined): boolean { + return typeof enablement === 'string' + ? this.contextKeyService.match(enablement) + : true; + } +} + +interface EnablementAware { + readonly enablement: string | undefined; +} + +function isEnablementAware(arg: unknown): arg is EnablementAware { + return !!arg && typeof arg === 'object' && 'enablement' in arg; +} + +// https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/base/common/linkedText.ts#L8-L56 +export interface ILink { + readonly label: string; + readonly href: string; + readonly title?: string; +} + +export type LinkedTextNode = string | ILink; + +export class LinkedText { + constructor(readonly nodes: LinkedTextNode[]) {} + toString(): string { + return this.nodes + .map((node) => (typeof node === 'string' ? node : node.label)) + .join(''); + } +} + +const LINK_REGEX = + /\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi; + +export function parseLinkedText(text: string): LinkedText { + const result: LinkedTextNode[] = []; + + let index = 0; + let match: RegExpExecArray | null; + + while ((match = LINK_REGEX.exec(text))) { + if (match.index - index > 0) { + result.push(text.substring(index, match.index)); + } + + const [, label, href, , title] = match; + + if (title) { + result.push({ label, href, title }); + } else { + result.push({ label, href }); + } + + index = match.index + match[0].length; + } + + if (index < text.length) { + result.push(text.substring(index)); + } + + return new LinkedText(result); +} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 02d534044..4eb572b3b 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -116,12 +116,16 @@ import { MessagingContribution } from './theia/core/messaging-contribution'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; import { HostedPluginReader } from './theia/plugin-ext/plugin-reader'; import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; -import { PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { + PluginDeployer, + PluginScanner, +} from '@theia/plugin-ext/lib/common/plugin-protocol'; import { LocalDirectoryPluginDeployerResolverWithFallback, PluginDeployer_GH_12064, } from './theia/plugin-ext/plugin-deployer'; import { SettingsReader } from './settings-reader'; +import { VsCodePluginScanner } from './theia/plugin-ext-vscode/scanner-vscode'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -410,6 +414,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope(); bind(SettingsReader).toSelf().inSingletonScope(); + + // To read the enablement property of the viewsWelcome + // https://github.com/eclipse-theia/theia/issues/14309 + bind(VsCodePluginScanner).toSelf().inSingletonScope(); + rebind(PluginScanner).toService(VsCodePluginScanner); }); function bindChildLogger(bind: interfaces.Bind, name: string): void { diff --git a/arduino-ide-extension/src/node/theia/plugin-ext-vscode/scanner-vscode.ts b/arduino-ide-extension/src/node/theia/plugin-ext-vscode/scanner-vscode.ts new file mode 100644 index 000000000..0fd68c9b4 --- /dev/null +++ b/arduino-ide-extension/src/node/theia/plugin-ext-vscode/scanner-vscode.ts @@ -0,0 +1,44 @@ +import { injectable, postConstruct } from '@theia/core/shared/inversify'; +import { VsCodePluginScanner as TheiaVsCodePluginScanner } from '@theia/plugin-ext-vscode/lib/node/scanner-vscode'; +import { + PluginPackageViewWelcome, + ViewWelcome, +} from '@theia/plugin-ext/lib/common/plugin-protocol'; + +@injectable() +export class VsCodePluginScanner extends TheiaVsCodePluginScanner { + @postConstruct() + protected init(): void { + this['readViewWelcome'] = ( + rawViewWelcome: PluginPackageViewWelcome, + pluginViewsIds: string[] + ) => { + const result = { + view: rawViewWelcome.view, + content: rawViewWelcome.contents, + when: rawViewWelcome.when, + // if the plugin contributes Welcome view to its own view - it will be ordered first + order: + pluginViewsIds.findIndex((v) => v === rawViewWelcome.view) > -1 + ? 0 + : 1, + }; + return maybeSetEnablement(rawViewWelcome, result); + }; + } +} + +// This is not yet supported by Theia but available in Code (https://github.com/microsoft/vscode/issues/114304) +function maybeSetEnablement( + rawViewWelcome: PluginPackageViewWelcome, + result: ViewWelcome +) { + const enablement = + 'enablement' in rawViewWelcome && + typeof rawViewWelcome['enablement'] === 'string' && + rawViewWelcome['enablement']; + if (enablement) { + Object.assign(result, { enablement }); + } + return result; +} From 5f318d8dc8c57414f10d158cc8cc063763700939 Mon Sep 17 00:00:00 2001 From: dankeboy36 Date: Fri, 8 Nov 2024 17:49:33 +0100 Subject: [PATCH 2/2] fix: update change proposal from Theia as is Ref: arduino/arduino-ide#2543 Signed-off-by: dankeboy36 --- .../theia/plugin-ext/tree-view-widget.tsx | 275 +++++++++--------- 1 file changed, 144 insertions(+), 131 deletions(-) diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx b/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx index c55b91a12..dc83272c2 100644 --- a/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/tree-view-widget.tsx @@ -1,44 +1,72 @@ -// import { OpenerService } from '@theia/core/lib/browser'; +import { LabelIcon } from '@theia/core/lib/browser/label-parser'; +import { OpenerService, open } from '@theia/core/lib/browser/opener-service'; +import { codicon } from '@theia/core/lib/browser/widgets/widget'; import { DisposableCollection } from '@theia/core/lib/common/disposable'; -import { /*inject,*/ injectable } from '@theia/core/shared/inversify'; +import { URI } from '@theia/core/lib/common/uri'; +import { inject, injectable } from '@theia/core/shared/inversify'; import React from '@theia/core/shared/react'; +import { URI as CodeUri } from '@theia/core/shared/vscode-uri'; import { TreeViewWidget as TheiaTreeViewWidget } from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget'; +// Copied back from https://github.com/eclipse-theia/theia/pull/14391 +// Remove the patching when Arduino uses Eclipse Theia >1.55.0 +// https://github.com/eclipse-theia/theia/blob/8d3c5a11af65448b6700bedd096f8d68f0675541/packages/core/src/browser/tree/tree-view-welcome-widget.tsx#L37-L54 +// https://github.com/eclipse-theia/theia/blob/8d3c5a11af65448b6700bedd096f8d68f0675541/packages/core/src/browser/tree/tree-view-welcome-widget.tsx#L146-L298 + +interface ViewWelcome { + readonly view: string; + readonly content: string; + readonly when?: string; + readonly enablement?: string; + readonly order: number; +} + +export interface IItem { + readonly welcomeInfo: ViewWelcome; + visible: boolean; +} + +export interface ILink { + readonly label: string; + readonly href: string; + readonly title?: string; +} + +type LinkedTextItem = string | ILink; + @injectable() export class TreeViewWidget extends TheiaTreeViewWidget { - // @inject(OpenerService) - // private readonly openerService: OpenerService; + @inject(OpenerService) + private readonly openerService: OpenerService; + private readonly toDisposeBeforeUpdateViewWelcomeNodes = new DisposableCollection(); - // The actual rewrite of the viewsWelcome rendering aligned to VS Code to fix https://github.com/eclipse-theia/theia/issues/14309 - // Based on https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/workbench/browser/parts/views/viewPane.ts#L228-L299 protected override updateViewWelcomeNodes(): void { - this.toDisposeBeforeUpdateViewWelcomeNodes.dispose(); - const viewWelcomes = this.visibleItems.sort((a, b) => a.order - b.order); this.viewWelcomeNodes = []; - const allEnablementKeys: Set[] = []; - // the plugin-view-registry will push the changes when there is a change in the when context - // this listener is to update the view when the `enablement` of the viewWelcomes changes + this.toDisposeBeforeUpdateViewWelcomeNodes.dispose(); + const items = this.visibleItems.sort((a, b) => a.order - b.order); + + const enablementKeys: Set[] = []; + // the plugin-view-registry will push the changes when there is a change in the `when` prop which controls the visibility + // this listener is to update the enablement of the components in the view welcome this.toDisposeBeforeUpdateViewWelcomeNodes.push( - this.contextKeyService.onDidChange((event) => { - if (allEnablementKeys.some((keys) => event.affects(keys))) { + this.contextService.onDidChange((event) => { + if (enablementKeys.some((keys) => event.affects(keys))) { this.updateViewWelcomeNodes(); this.update(); } }) ); - // TODO: support `renderSecondaryButtons` prop from VS Code? - for (const viewWelcome of viewWelcomes) { - const { content } = viewWelcome; - const enablement = isEnablementAware(viewWelcome) - ? viewWelcome.enablement - : undefined; - const enablementKeys = enablement - ? this.contextKeyService.parseKeys(enablement) + // Note: VS Code does not support the `renderSecondaryButtons` prop in welcome content either. + for (const item of items) { + const { content } = item; + const enablement = isEnablementAware(item) ? item.enablement : undefined; + const itemEnablementKeys = enablement + ? this.contextService.parseKeys(enablement) : undefined; - if (enablementKeys) { - allEnablementKeys.push(enablementKeys); + if (itemEnablementKeys) { + enablementKeys.push(itemEnablementKeys); } const lines = content.split('\n'); @@ -49,61 +77,48 @@ export class TreeViewWidget extends TheiaTreeViewWidget { continue; } - const linkedText = parseLinkedText(line); + const linkedTextItems = this.parseLinkedText_patch14309(line); if ( - linkedText.nodes.length === 1 && - typeof linkedText.nodes[0] !== 'string' + linkedTextItems.length === 1 && + typeof linkedTextItems[0] !== 'string' ) { - const node = linkedText.nodes[0]; + const node = linkedTextItems[0]; this.viewWelcomeNodes.push( - this.renderButtonNode( + this.renderButtonNode_patch14309( node, this.viewWelcomeNodes.length, enablement ) ); } else { - const paragraphNodes: React.ReactNode[] = []; - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - paragraphNodes.push( - this.renderTextNode(node, this.viewWelcomeNodes.length) - ); - } else { - paragraphNodes.push( - this.renderCommandLinkNode( - node, - this.viewWelcomeNodes.length, - enablement - ) - ); - } - } - if (paragraphNodes.length) { - this.viewWelcomeNodes.push( -

- {...paragraphNodes} -

- ); - } + const renderNode = (item: LinkedTextItem, index: number) => + typeof item == 'string' + ? this.renderTextNode_patch14309(item, index) + : this.renderLinkNode_patch14309(item, index, enablement); + + this.viewWelcomeNodes.push( +

+ {...linkedTextItems.flatMap(renderNode)} +

+ ); } } } } - protected override renderButtonNode( + private renderButtonNode_patch14309( node: ILink, lineKey: string | number, - enablement: string | undefined = undefined + enablement: string | undefined ): React.ReactNode { return (
@@ -111,118 +126,116 @@ export class TreeViewWidget extends TheiaTreeViewWidget { ); } - protected override renderCommandLinkNode( + private renderTextNode_patch14309( + node: string, + textKey: string | number + ): React.ReactNode { + return ( + + {this.labelParser + .parse(node) + .map((segment, index) => + LabelIcon.is(segment) ? ( + + ) : ( + {segment} + ) + )} + + ); + } + + private renderLinkNode_patch14309( node: ILink, linkKey: string | number, - enablement: string | undefined = undefined + enablement: string | undefined ): React.ReactNode { return ( this.open(e, node)} + className={this.getLinkClassName_patch14309(node.href, enablement)} + title={node.title || ''} + onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)} > {node.label} ); } - protected override renderTextNode( - node: string, - textKey: string | number - ): React.ReactNode { - return {node}; - } - - protected override getLinkClassName( + private getLinkClassName_patch14309( href: string, - enablement: string | undefined = undefined + enablement: string | undefined ): string { const classNames = ['theia-WelcomeViewCommandLink']; // Only command-backed links can be disabled. All other, https:, file: remain enabled - if (href.startsWith('command:') && !this.isEnabled(enablement)) { + if ( + href.startsWith('command:') && + !this.isEnabledClick_patch14309(enablement) + ) { classNames.push('disabled'); } return classNames.join(' '); } - private open(event: React.MouseEvent, node: ILink): void { - event.preventDefault(); - if (node.href.startsWith('command:')) { - const commandId = node.href.substring('commands:'.length - 1); - this.commands.executeCommand(commandId); - } else if (node.href.startsWith('file:')) { - // TODO: check what Code does - } else if (node.href.startsWith('https:')) { - this.windowService.openNewWindow(node.href, { external: true }); - } - } - - /** - * @param enablement [when context](https://code.visualstudio.com/api/references/when-clause-contexts) expression string - */ - private isEnabled(enablement: string | undefined): boolean { + private isEnabledClick_patch14309(enablement: string | undefined): boolean { return typeof enablement === 'string' - ? this.contextKeyService.match(enablement) + ? this.contextService.match(enablement) : true; } -} - -interface EnablementAware { - readonly enablement: string | undefined; -} -function isEnablementAware(arg: unknown): arg is EnablementAware { - return !!arg && typeof arg === 'object' && 'enablement' in arg; -} - -// https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/base/common/linkedText.ts#L8-L56 -export interface ILink { - readonly label: string; - readonly href: string; - readonly title?: string; -} + private openLinkOrCommand_patch14309 = ( + event: React.MouseEvent, + value: string + ): void => { + event.stopPropagation(); + + if (value.startsWith('command:')) { + const command = value.replace('command:', ''); + this.commands.executeCommand(command); + } else if (value.startsWith('file:')) { + const uri = value.replace('file:', ''); + open(this.openerService, new URI(CodeUri.file(uri).toString())); + } else { + this.windowService.openNewWindow(value, { external: true }); + } + }; -export type LinkedTextNode = string | ILink; + private parseLinkedText_patch14309(text: string): LinkedTextItem[] { + const result: LinkedTextItem[] = []; -export class LinkedText { - constructor(readonly nodes: LinkedTextNode[]) {} - toString(): string { - return this.nodes - .map((node) => (typeof node === 'string' ? node : node.label)) - .join(''); - } -} + const linkRegex = + /\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi; + let index = 0; + let match: RegExpExecArray | null; -const LINK_REGEX = - /\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi; + while ((match = linkRegex.exec(text))) { + if (match.index - index > 0) { + result.push(text.substring(index, match.index)); + } -export function parseLinkedText(text: string): LinkedText { - const result: LinkedTextNode[] = []; + const [, label, href, , title] = match; - let index = 0; - let match: RegExpExecArray | null; + if (title) { + result.push({ label, href, title }); + } else { + result.push({ label, href }); + } - while ((match = LINK_REGEX.exec(text))) { - if (match.index - index > 0) { - result.push(text.substring(index, match.index)); + index = match.index + match[0].length; } - const [, label, href, , title] = match; - - if (title) { - result.push({ label, href, title }); - } else { - result.push({ label, href }); + if (index < text.length) { + result.push(text.substring(index)); } - index = match.index + match[0].length; + return result; } +} - if (index < text.length) { - result.push(text.substring(index)); - } +interface EnablementAware { + readonly enablement: string | undefined; +} - return new LinkedText(result); +function isEnablementAware(arg: unknown): arg is EnablementAware { + return !!arg && typeof arg === 'object' && 'enablement' in arg; }