Skip to content

Commit

Permalink
fix: align viewsWelcome behavior to VS Code (#2543)
Browse files Browse the repository at this point in the history
* fix: align `viewsWelcome` behavior to VS Code

Ref: eclipse-theia/theia#14309
Signed-off-by: dankeboy36 <[email protected]>

* fix: update change proposal from Theia as is

Ref: #2543
Signed-off-by: dankeboy36 <[email protected]>

---------

Signed-off-by: dankeboy36 <[email protected]>
  • Loading branch information
dankeboy36 authored Nov 21, 2024
1 parent 4cf9909 commit 3fc8474
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 2 deletions.
1 change: 1 addition & 0 deletions arduino-ide-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
56 changes: 55 additions & 1 deletion arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
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 { 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;

private readonly toDisposeBeforeUpdateViewWelcomeNodes =
new DisposableCollection();

protected override updateViewWelcomeNodes(): void {
this.viewWelcomeNodes = [];
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.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 item of items) {
const { content } = item;
const enablement = isEnablementAware(item) ? item.enablement : undefined;
const itemEnablementKeys = enablement
? this.contextService.parseKeys(enablement)
: undefined;
if (itemEnablementKeys) {
enablementKeys.push(itemEnablementKeys);
}
const lines = content.split('\n');

for (let line of lines) {
line = line.trim();

if (!line) {
continue;
}

const linkedTextItems = this.parseLinkedText_patch14309(line);

if (
linkedTextItems.length === 1 &&
typeof linkedTextItems[0] !== 'string'
) {
const node = linkedTextItems[0];
this.viewWelcomeNodes.push(
this.renderButtonNode_patch14309(
node,
this.viewWelcomeNodes.length,
enablement
)
);
} else {
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>
);
}
}
}
}

private renderButtonNode_patch14309(
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_patch14309(enablement)}
onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
>
{node.label}
</button>
</div>
);
}

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
): React.ReactNode {
return (
<a
key={`link-${linkKey}`}
className={this.getLinkClassName_patch14309(node.href, enablement)}
title={node.title || ''}
onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
>
{node.label}
</a>
);
}

private getLinkClassName_patch14309(
href: string,
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.isEnabledClick_patch14309(enablement)
) {
classNames.push('disabled');
}
return classNames.join(' ');
}

private isEnabledClick_patch14309(enablement: string | undefined): boolean {
return typeof enablement === 'string'
? this.contextService.match(enablement)
: true;
}

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 });
}
};

private parseLinkedText_patch14309(text: string): LinkedTextItem[] {
const result: LinkedTextItem[] = [];

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

while ((match = linkRegex.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 result;
}
}

interface EnablementAware {
readonly enablement: string | undefined;
}

function isEnablementAware(arg: unknown): arg is EnablementAware {
return !!arg && typeof arg === 'object' && 'enablement' in arg;
}
11 changes: 10 additions & 1 deletion arduino-ide-extension/src/node/arduino-ide-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 3fc8474

Please sign in to comment.