Skip to content

Commit

Permalink
fix: align viewsWelcome behavior to VS Code
Browse files Browse the repository at this point in the history
Ref: eclipse-theia/theia#14309
Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
  • Loading branch information
dankeboy36 committed Oct 26, 2024
1 parent 7713668 commit 7a8acdc
Show file tree
Hide file tree
Showing 5 changed files with 338 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,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);
}
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
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 7a8acdc

Please sign in to comment.