Skip to content

Commit

Permalink
fix: align viewWelcome to VS Code
Browse files Browse the repository at this point in the history
Closes: eclipse-theia#14309
Ref: arduino/arduino-ide#2543
Signed-off-by: dankeboy36 <[email protected]>
  • Loading branch information
dankeboy36 committed Nov 3, 2024
1 parent ea62f67 commit 1e12b94
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 54 deletions.
128 changes: 82 additions & 46 deletions packages/core/src/browser/tree/tree-view-welcome-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// some code is copied and modified from: https://github.com/microsoft/vscode/blob/573e5145ae3b50523925a6f6315d373e649d1b06/src/vs/base/common/linkedText.ts
// aligned the API and enablement behavior to https://github.com/microsoft/vscode/blob/c711bc9333ba339fde1a530de0094b3fa32f09de/src/vs/base/common/linkedText.ts

import React = require('react');
import { inject, injectable } from 'inversify';
import { CommandRegistry } from '../../common';
import { URI as CodeUri } from 'vscode-uri';
import { CommandRegistry, DisposableCollection } from '../../common';
import URI from '../../common/uri';
import { ContextKeyService } from '../context-key-service';
import { LabelIcon, LabelParser } from '../label-parser';
import { OpenerService, open } from '../opener-service';
import { codicon } from '../widgets';
import { WindowService } from '../window/window-service';
import { TreeModel } from './tree-model';
import { TreeWidget } from './tree-widget';
import { WindowService } from '../window/window-service';

interface ViewWelcome {
export interface ViewWelcome {
readonly view: string;
readonly content: string;
readonly when?: string;
readonly enablement?: string;
readonly order: number;
}

interface IItem {
export interface IItem {
readonly welcomeInfo: ViewWelcome;
visible: boolean;
}

interface ILink {
export interface ILink {
readonly label: string;
readonly href: string;
readonly title?: string;
Expand All @@ -60,6 +67,14 @@ export class TreeViewWelcomeWidget extends TreeWidget {
@inject(WindowService)
protected readonly windowService: WindowService;

@inject(LabelParser)
protected readonly labelParser: LabelParser;

@inject(OpenerService)
protected readonly openerService: OpenerService;

protected readonly toDisposeBeforeUpdateViewWelcomeNodes = new DisposableCollection();

protected viewWelcomeNodes: React.ReactNode[] = [];
protected defaultItem: IItem | undefined;
protected items: IItem[] = [];
Expand Down Expand Up @@ -130,13 +145,31 @@ export class TreeViewWelcomeWidget extends TreeWidget {

protected updateViewWelcomeNodes(): void {
this.viewWelcomeNodes = [];
this.toDisposeBeforeUpdateViewWelcomeNodes.dispose();
const items = this.visibleItems.sort((a, b) => a.order - b.order);

for (const [iIndex, { content }] of items.entries()) {
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.contextService.onDidChange(event => {
if (enablementKeys.some(keys => event.affects(keys))) {
this.updateViewWelcomeNodes();
this.update();
}
})
);
// Note: VS Code does not support the `renderSecondaryButtons` prop in welcome content either.
for (const { content, enablement } of items) {
const itemEnablementKeys = enablement
? this.contextService.parseKeys(enablement)
: undefined;
if (itemEnablementKeys) {
enablementKeys.push(itemEnablementKeys);
}
const lines = content.split('\n');

for (let [lIndex, line] of lines.entries()) {
const lineKey = `${iIndex}-${lIndex}`;
for (let line of lines) {
line = line.trim();

if (!line) {
Expand All @@ -146,95 +179,98 @@ export class TreeViewWelcomeWidget extends TreeWidget {
const linkedTextItems = this.parseLinkedText(line);

if (linkedTextItems.length === 1 && typeof linkedTextItems[0] !== 'string') {
const node = linkedTextItems[0];
this.viewWelcomeNodes.push(
this.renderButtonNode(linkedTextItems[0], lineKey)
this.renderButtonNode(
node,
this.viewWelcomeNodes.length,
enablement
)
);
} else {
const linkedTextNodes: React.ReactNode[] = [];

for (const [nIndex, node] of linkedTextItems.entries()) {
const linkedTextKey = `${lineKey}-${nIndex}`;

if (typeof node === 'string') {
linkedTextNodes.push(
this.renderTextNode(node, linkedTextKey)
);
} else {
linkedTextNodes.push(
this.renderCommandLinkNode(node, linkedTextKey)
);
}
}
const renderNode = (item: LinkedTextItem, index: number) => typeof item == 'string'
? this.renderTextNode(item, index)
: this.renderLinkNode(item, index, enablement);

this.viewWelcomeNodes.push(
<div key={`line-${lineKey}`}>
{...linkedTextNodes}
</div>
<p key={`p-${this.viewWelcomeNodes.length}`}>
{...linkedTextItems.flatMap(renderNode)}
</p>
);
}
}
}
}

protected renderButtonNode(node: ILink, lineKey: string): React.ReactNode {
protected renderButtonNode(node: ILink, lineKey: string | number, enablement: string | undefined): React.ReactNode {
return (
<div key={`line-${lineKey}`} className='theia-WelcomeViewButtonWrapper'>
<button title={node.title}
className='theia-button theia-WelcomeViewButton'
disabled={!this.isEnabledClick(node.href)}
disabled={!this.isEnabledClick(enablement)}
onClick={e => this.openLinkOrCommand(e, node.href)}>
{node.label}
</button>
</div>
);
}

protected renderTextNode(node: string, textKey: string): React.ReactNode {
return <span key={`text-${textKey}`}>{node}</span>;
protected renderTextNode(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>;
}

protected renderCommandLinkNode(node: ILink, linkKey: string): React.ReactNode {
protected renderLinkNode(node: ILink, linkKey: string | number, enablement: string | undefined): React.ReactNode {
return (
<a key={`link-${linkKey}`}
className={this.getLinkClassName(node.href)}
className={this.getLinkClassName(node.href, enablement)}
title={node.title || ''}
onClick={e => this.openLinkOrCommand(e, node.href)}>
{node.label}
</a>
);
}

protected getLinkClassName(href: string): string {
protected getLinkClassName(href: string, enablement: string | undefined): string {
const classNames = ['theia-WelcomeViewCommandLink'];
if (!this.isEnabledClick(href)) {
// Only command-backed links can be disabled. All other, https:, file: remain enabled
if (href.startsWith('command:') && !this.isEnabledClick(enablement)) {
classNames.push('disabled');
}
return classNames.join(' ');
}

protected isEnabledClick(href: string): boolean {
if (href.startsWith('command:')) {
const command = href.replace('command:', '');
return this.commands.isEnabled(command);
}
return true;
protected isEnabledClick(enablement: string | undefined): boolean {
return typeof enablement === 'string'
? this.contextService.match(enablement)
: true;
}

protected openLinkOrCommand = (event: React.MouseEvent, href: string): void => {
protected openLinkOrCommand = (event: React.MouseEvent, value: string): void => {
event.stopPropagation();

if (href.startsWith('command:')) {
const command = href.replace('command:', '');
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(href, { external: true });
this.windowService.openNewWindow(value, { external: true });
}
};

protected parseLinkedText(text: string): LinkedTextItem[] {
const result: LinkedTextItem[] = [];

const linkRegex = /\[([^\]]+)\]\(((?:https?:\/\/|command:)[^\)\s]+)(?: ("|')([^\3]+)(\3))?\)/gi;
const linkRegex = /\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi;
let index = 0;
let match: RegExpExecArray | null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { Container, interfaces, injectable, inject } from '@theia/core/shared/inversify';
import { TreeProps, ContextMenuRenderer, TreeNode, OpenerService, open, NodeProps, defaultTreeProps } from '@theia/core/lib/browser';
import { TreeProps, ContextMenuRenderer, TreeNode, open, NodeProps, defaultTreeProps } from '@theia/core/lib/browser';
import { FileTreeModel, FileStatNode, createFileTreeContainer, FileTreeWidget } from '../file-tree';

const BREADCRUMBS_FILETREE_CLASS = 'theia-FilepathBreadcrumbFileTree';
Expand All @@ -35,9 +35,6 @@ export function createFileTreeBreadcrumbsWidget(parent: interfaces.Container): B
@injectable()
export class BreadcrumbsFileTreeWidget extends FileTreeWidget {

@inject(OpenerService)
protected readonly openerService: OpenerService;

constructor(
@inject(TreeProps) props: TreeProps,
@inject(FileTreeModel) override readonly model: FileTreeModel,
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export interface PluginPackageViewWelcome {
view: string;
contents: string;
when?: string;
enablement?: string;
}

export interface PluginPackageCommand {
Expand Down Expand Up @@ -853,6 +854,7 @@ export interface ViewWelcome {
view: string;
content: string;
when?: string;
enablement?: string;
order: number;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ export class TheiaPluginScanner extends AbstractPluginScanner {
view: rawViewWelcome.view,
content: rawViewWelcome.contents,
when: rawViewWelcome.when,
enablement: rawViewWelcome.enablement,
// 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
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import { View } from '../../../common/plugin-protocol';
import { URI } from '@theia/core/lib/common/uri';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { LabelParser } from '@theia/core/lib/browser/label-parser';
import { AccessibilityInformation } from '@theia/plugin';
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
import { DecoratedTreeNode } from '@theia/core/lib/browser/tree/tree-decorator';
Expand Down Expand Up @@ -456,9 +455,6 @@ export class TreeViewWidget extends TreeViewWelcomeWidget {
@inject(HoverService)
protected readonly hoverService: HoverService;

@inject(LabelParser)
protected readonly labelParser: LabelParser;

@inject(ColorRegistry)
protected readonly colorRegistry: ColorRegistry;

Expand Down

0 comments on commit 1e12b94

Please sign in to comment.