From 7a8acdc11807d49a30643edb073dbfe65e20509c Mon Sep 17 00:00:00 2001
From: dankeboy36 <dankeboy36@gmail.com>
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 <dankeboy36@gmail.com>
---
 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<TreeWidget>;
+}) {
+  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<string>[] = [];
+    // 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(
+              <p key={`p-${this.viewWelcomeNodes.length}`}>
+                {...paragraphNodes}
+              </p>
+            );
+          }
+        }
+      }
+    }
+  }
+
+  protected override renderButtonNode(
+    node: ILink,
+    lineKey: string | number,
+    enablement: string | undefined = undefined
+  ): React.ReactNode {
+    return (
+      <div key={`line-${lineKey}`} className="theia-WelcomeViewButtonWrapper">
+        <button
+          title={node.title}
+          className="theia-button theia-WelcomeViewButton"
+          disabled={!this.isEnabled(enablement)}
+          onClick={(e) => this.open(e, node)}
+        >
+          {node.label}
+        </button>
+      </div>
+    );
+  }
+
+  protected override renderCommandLinkNode(
+    node: ILink,
+    linkKey: string | number,
+    enablement: string | undefined = undefined
+  ): React.ReactNode {
+    return (
+      <a
+        key={`link-${linkKey}`}
+        className={this.getLinkClassName(node.href, enablement)}
+        title={node.title ?? ''}
+        onClick={(e) => this.open(e, node)}
+      >
+        {node.label}
+      </a>
+    );
+  }
+
+  protected override renderTextNode(
+    node: string,
+    textKey: string | number
+  ): React.ReactNode {
+    return <span key={`text-${textKey}`}>{node}</span>;
+  }
+
+  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 <dankeboy36@gmail.com>
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 <dankeboy36@gmail.com>
---
 .../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<string>[] = [];
-    // 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<string>[] = [];
+    // 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(
-              <p key={`p-${this.viewWelcomeNodes.length}`}>
-                {...paragraphNodes}
-              </p>
-            );
-          }
+          const renderNode = (item: LinkedTextItem, index: number) =>
+            typeof item == 'string'
+              ? this.renderTextNode_patch14309(item, index)
+              : this.renderLinkNode_patch14309(item, index, enablement);
+
+          this.viewWelcomeNodes.push(
+            <p key={`p-${this.viewWelcomeNodes.length}`}>
+              {...linkedTextItems.flatMap(renderNode)}
+            </p>
+          );
         }
       }
     }
   }
 
-  protected override renderButtonNode(
+  private renderButtonNode_patch14309(
     node: ILink,
     lineKey: string | number,
-    enablement: string | undefined = undefined
+    enablement: string | undefined
   ): React.ReactNode {
     return (
       <div key={`line-${lineKey}`} className="theia-WelcomeViewButtonWrapper">
         <button
           title={node.title}
           className="theia-button theia-WelcomeViewButton"
-          disabled={!this.isEnabled(enablement)}
-          onClick={(e) => this.open(e, node)}
+          disabled={!this.isEnabledClick_patch14309(enablement)}
+          onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
         >
           {node.label}
         </button>
@@ -111,118 +126,116 @@ export class TreeViewWidget extends TheiaTreeViewWidget {
     );
   }
 
-  protected override renderCommandLinkNode(
+  private renderTextNode_patch14309(
+    node: string,
+    textKey: string | number
+  ): React.ReactNode {
+    return (
+      <span key={`text-${textKey}`}>
+        {this.labelParser
+          .parse(node)
+          .map((segment, index) =>
+            LabelIcon.is(segment) ? (
+              <span key={index} className={codicon(segment.name)} />
+            ) : (
+              <span key={index}>{segment}</span>
+            )
+          )}
+      </span>
+    );
+  }
+
+  private renderLinkNode_patch14309(
     node: ILink,
     linkKey: string | number,
-    enablement: string | undefined = undefined
+    enablement: string | undefined
   ): React.ReactNode {
     return (
       <a
         key={`link-${linkKey}`}
-        className={this.getLinkClassName(node.href, enablement)}
-        title={node.title ?? ''}
-        onClick={(e) => this.open(e, node)}
+        className={this.getLinkClassName_patch14309(node.href, enablement)}
+        title={node.title || ''}
+        onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
       >
         {node.label}
       </a>
     );
   }
 
-  protected override renderTextNode(
-    node: string,
-    textKey: string | number
-  ): React.ReactNode {
-    return <span key={`text-${textKey}`}>{node}</span>;
-  }
-
-  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;
 }