Skip to content

Commit 3fc8474

Browse files
authored
fix: align viewsWelcome behavior to VS Code (#2543)
* 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]>
1 parent 4cf9909 commit 3fc8474

File tree

5 files changed

+351
-2
lines changed

5 files changed

+351
-2
lines changed

arduino-ide-extension/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"@theia/outline-view": "1.41.0",
4040
"@theia/output": "1.41.0",
4141
"@theia/plugin-ext": "1.41.0",
42+
"@theia/plugin-ext-vscode": "1.41.0",
4243
"@theia/preferences": "1.41.0",
4344
"@theia/scm": "1.41.0",
4445
"@theia/search-in-workspace": "1.41.0",

arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import '../../src/browser/style/index.css';
2-
import { Container, ContainerModule } from '@theia/core/shared/inversify';
2+
import {
3+
Container,
4+
ContainerModule,
5+
interfaces,
6+
} from '@theia/core/shared/inversify';
37
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
48
import { CommandContribution } from '@theia/core/lib/common/command';
59
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
@@ -53,6 +57,8 @@ import {
5357
DockPanelRenderer as TheiaDockPanelRenderer,
5458
TabBarRendererFactory,
5559
ContextMenuRenderer,
60+
createTreeContainer,
61+
TreeWidget,
5662
} from '@theia/core/lib/browser';
5763
import { MenuContribution } from '@theia/core/lib/common/menu';
5864
import {
@@ -372,6 +378,15 @@ import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-
372378
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
373379
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/debug-configuration-widget';
374380
import { DebugToolBar } from '@theia/debug/lib/browser/view/debug-toolbar-widget';
381+
import {
382+
PluginTree,
383+
PluginTreeModel,
384+
TreeViewWidgetOptions,
385+
VIEW_ITEM_CONTEXT_MENU,
386+
} from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget';
387+
import { TreeViewDecoratorService } from '@theia/plugin-ext/lib/main/browser/view/tree-view-decorator-service';
388+
import { PLUGIN_VIEW_DATA_FACTORY_ID } from '@theia/plugin-ext/lib/main/browser/view/plugin-view-registry';
389+
import { TreeViewWidget } from './theia/plugin-ext/tree-view-widget';
375390

376391
// Hack to fix copy/cut/paste issue after electron version update in Theia.
377392
// https://github.com/eclipse-theia/theia/issues/12487
@@ -1082,4 +1097,43 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
10821097
rebind(TheiaTerminalFrontendContribution).toService(
10831098
TerminalFrontendContribution
10841099
);
1100+
1101+
bindViewsWelcome_TheiaGH14309({ bind, widget: TreeViewWidget });
10851102
});
1103+
1104+
// Align the viewsWelcome rendering with VS Code (https://github.com/eclipse-theia/theia/issues/14309)
1105+
// Copied from Theia code but with customized TreeViewWidget with the customized viewsWelcome rendering
1106+
// https://github.com/eclipse-theia/theia/blob/0c5f69455d9ee355b1a7ca510ffa63d2b20f0c77/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts#L159-L181
1107+
function bindViewsWelcome_TheiaGH14309({
1108+
bind,
1109+
widget,
1110+
}: {
1111+
bind: interfaces.Bind;
1112+
widget: interfaces.Newable<TreeWidget>;
1113+
}) {
1114+
bind(WidgetFactory)
1115+
.toDynamicValue(({ container }) => ({
1116+
id: PLUGIN_VIEW_DATA_FACTORY_ID,
1117+
createWidget: (options: TreeViewWidgetOptions) => {
1118+
const props = {
1119+
contextMenuPath: VIEW_ITEM_CONTEXT_MENU,
1120+
expandOnlyOnExpansionToggleClick: true,
1121+
expansionTogglePadding: 22,
1122+
globalSelection: true,
1123+
leftPadding: 8,
1124+
search: true,
1125+
multiSelect: options.multiSelect,
1126+
};
1127+
const child = createTreeContainer(container, {
1128+
props,
1129+
tree: PluginTree,
1130+
model: PluginTreeModel,
1131+
widget,
1132+
decoratorService: TreeViewDecoratorService,
1133+
});
1134+
child.bind(TreeViewWidgetOptions).toConstantValue(options);
1135+
return child.get(TreeWidget);
1136+
},
1137+
}))
1138+
.inSingletonScope();
1139+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import { LabelIcon } from '@theia/core/lib/browser/label-parser';
2+
import { OpenerService, open } from '@theia/core/lib/browser/opener-service';
3+
import { codicon } from '@theia/core/lib/browser/widgets/widget';
4+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
5+
import { URI } from '@theia/core/lib/common/uri';
6+
import { inject, injectable } from '@theia/core/shared/inversify';
7+
import React from '@theia/core/shared/react';
8+
import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
9+
import { TreeViewWidget as TheiaTreeViewWidget } from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget';
10+
11+
// Copied back from https://github.com/eclipse-theia/theia/pull/14391
12+
// Remove the patching when Arduino uses Eclipse Theia >1.55.0
13+
// https://github.com/eclipse-theia/theia/blob/8d3c5a11af65448b6700bedd096f8d68f0675541/packages/core/src/browser/tree/tree-view-welcome-widget.tsx#L37-L54
14+
// https://github.com/eclipse-theia/theia/blob/8d3c5a11af65448b6700bedd096f8d68f0675541/packages/core/src/browser/tree/tree-view-welcome-widget.tsx#L146-L298
15+
16+
interface ViewWelcome {
17+
readonly view: string;
18+
readonly content: string;
19+
readonly when?: string;
20+
readonly enablement?: string;
21+
readonly order: number;
22+
}
23+
24+
export interface IItem {
25+
readonly welcomeInfo: ViewWelcome;
26+
visible: boolean;
27+
}
28+
29+
export interface ILink {
30+
readonly label: string;
31+
readonly href: string;
32+
readonly title?: string;
33+
}
34+
35+
type LinkedTextItem = string | ILink;
36+
37+
@injectable()
38+
export class TreeViewWidget extends TheiaTreeViewWidget {
39+
@inject(OpenerService)
40+
private readonly openerService: OpenerService;
41+
42+
private readonly toDisposeBeforeUpdateViewWelcomeNodes =
43+
new DisposableCollection();
44+
45+
protected override updateViewWelcomeNodes(): void {
46+
this.viewWelcomeNodes = [];
47+
this.toDisposeBeforeUpdateViewWelcomeNodes.dispose();
48+
const items = this.visibleItems.sort((a, b) => a.order - b.order);
49+
50+
const enablementKeys: Set<string>[] = [];
51+
// the plugin-view-registry will push the changes when there is a change in the `when` prop which controls the visibility
52+
// this listener is to update the enablement of the components in the view welcome
53+
this.toDisposeBeforeUpdateViewWelcomeNodes.push(
54+
this.contextService.onDidChange((event) => {
55+
if (enablementKeys.some((keys) => event.affects(keys))) {
56+
this.updateViewWelcomeNodes();
57+
this.update();
58+
}
59+
})
60+
);
61+
// Note: VS Code does not support the `renderSecondaryButtons` prop in welcome content either.
62+
for (const item of items) {
63+
const { content } = item;
64+
const enablement = isEnablementAware(item) ? item.enablement : undefined;
65+
const itemEnablementKeys = enablement
66+
? this.contextService.parseKeys(enablement)
67+
: undefined;
68+
if (itemEnablementKeys) {
69+
enablementKeys.push(itemEnablementKeys);
70+
}
71+
const lines = content.split('\n');
72+
73+
for (let line of lines) {
74+
line = line.trim();
75+
76+
if (!line) {
77+
continue;
78+
}
79+
80+
const linkedTextItems = this.parseLinkedText_patch14309(line);
81+
82+
if (
83+
linkedTextItems.length === 1 &&
84+
typeof linkedTextItems[0] !== 'string'
85+
) {
86+
const node = linkedTextItems[0];
87+
this.viewWelcomeNodes.push(
88+
this.renderButtonNode_patch14309(
89+
node,
90+
this.viewWelcomeNodes.length,
91+
enablement
92+
)
93+
);
94+
} else {
95+
const renderNode = (item: LinkedTextItem, index: number) =>
96+
typeof item == 'string'
97+
? this.renderTextNode_patch14309(item, index)
98+
: this.renderLinkNode_patch14309(item, index, enablement);
99+
100+
this.viewWelcomeNodes.push(
101+
<p key={`p-${this.viewWelcomeNodes.length}`}>
102+
{...linkedTextItems.flatMap(renderNode)}
103+
</p>
104+
);
105+
}
106+
}
107+
}
108+
}
109+
110+
private renderButtonNode_patch14309(
111+
node: ILink,
112+
lineKey: string | number,
113+
enablement: string | undefined
114+
): React.ReactNode {
115+
return (
116+
<div key={`line-${lineKey}`} className="theia-WelcomeViewButtonWrapper">
117+
<button
118+
title={node.title}
119+
className="theia-button theia-WelcomeViewButton"
120+
disabled={!this.isEnabledClick_patch14309(enablement)}
121+
onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
122+
>
123+
{node.label}
124+
</button>
125+
</div>
126+
);
127+
}
128+
129+
private renderTextNode_patch14309(
130+
node: string,
131+
textKey: string | number
132+
): React.ReactNode {
133+
return (
134+
<span key={`text-${textKey}`}>
135+
{this.labelParser
136+
.parse(node)
137+
.map((segment, index) =>
138+
LabelIcon.is(segment) ? (
139+
<span key={index} className={codicon(segment.name)} />
140+
) : (
141+
<span key={index}>{segment}</span>
142+
)
143+
)}
144+
</span>
145+
);
146+
}
147+
148+
private renderLinkNode_patch14309(
149+
node: ILink,
150+
linkKey: string | number,
151+
enablement: string | undefined
152+
): React.ReactNode {
153+
return (
154+
<a
155+
key={`link-${linkKey}`}
156+
className={this.getLinkClassName_patch14309(node.href, enablement)}
157+
title={node.title || ''}
158+
onClick={(e) => this.openLinkOrCommand_patch14309(e, node.href)}
159+
>
160+
{node.label}
161+
</a>
162+
);
163+
}
164+
165+
private getLinkClassName_patch14309(
166+
href: string,
167+
enablement: string | undefined
168+
): string {
169+
const classNames = ['theia-WelcomeViewCommandLink'];
170+
// Only command-backed links can be disabled. All other, https:, file: remain enabled
171+
if (
172+
href.startsWith('command:') &&
173+
!this.isEnabledClick_patch14309(enablement)
174+
) {
175+
classNames.push('disabled');
176+
}
177+
return classNames.join(' ');
178+
}
179+
180+
private isEnabledClick_patch14309(enablement: string | undefined): boolean {
181+
return typeof enablement === 'string'
182+
? this.contextService.match(enablement)
183+
: true;
184+
}
185+
186+
private openLinkOrCommand_patch14309 = (
187+
event: React.MouseEvent,
188+
value: string
189+
): void => {
190+
event.stopPropagation();
191+
192+
if (value.startsWith('command:')) {
193+
const command = value.replace('command:', '');
194+
this.commands.executeCommand(command);
195+
} else if (value.startsWith('file:')) {
196+
const uri = value.replace('file:', '');
197+
open(this.openerService, new URI(CodeUri.file(uri).toString()));
198+
} else {
199+
this.windowService.openNewWindow(value, { external: true });
200+
}
201+
};
202+
203+
private parseLinkedText_patch14309(text: string): LinkedTextItem[] {
204+
const result: LinkedTextItem[] = [];
205+
206+
const linkRegex =
207+
/\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi;
208+
let index = 0;
209+
let match: RegExpExecArray | null;
210+
211+
while ((match = linkRegex.exec(text))) {
212+
if (match.index - index > 0) {
213+
result.push(text.substring(index, match.index));
214+
}
215+
216+
const [, label, href, , title] = match;
217+
218+
if (title) {
219+
result.push({ label, href, title });
220+
} else {
221+
result.push({ label, href });
222+
}
223+
224+
index = match.index + match[0].length;
225+
}
226+
227+
if (index < text.length) {
228+
result.push(text.substring(index));
229+
}
230+
231+
return result;
232+
}
233+
}
234+
235+
interface EnablementAware {
236+
readonly enablement: string | undefined;
237+
}
238+
239+
function isEnablementAware(arg: unknown): arg is EnablementAware {
240+
return !!arg && typeof arg === 'object' && 'enablement' in arg;
241+
}

arduino-ide-extension/src/node/arduino-ide-backend-module.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,16 @@ import { MessagingContribution } from './theia/core/messaging-contribution';
116116
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
117117
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
118118
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader';
119-
import { PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol';
119+
import {
120+
PluginDeployer,
121+
PluginScanner,
122+
} from '@theia/plugin-ext/lib/common/plugin-protocol';
120123
import {
121124
LocalDirectoryPluginDeployerResolverWithFallback,
122125
PluginDeployer_GH_12064,
123126
} from './theia/plugin-ext/plugin-deployer';
124127
import { SettingsReader } from './settings-reader';
128+
import { VsCodePluginScanner } from './theia/plugin-ext-vscode/scanner-vscode';
125129

126130
export default new ContainerModule((bind, unbind, isBound, rebind) => {
127131
bind(BackendApplication).toSelf().inSingletonScope();
@@ -410,6 +414,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
410414
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
411415

412416
bind(SettingsReader).toSelf().inSingletonScope();
417+
418+
// To read the enablement property of the viewsWelcome
419+
// https://github.com/eclipse-theia/theia/issues/14309
420+
bind(VsCodePluginScanner).toSelf().inSingletonScope();
421+
rebind(PluginScanner).toService(VsCodePluginScanner);
413422
});
414423

415424
function bindChildLogger(bind: interfaces.Bind, name: string): void {

0 commit comments

Comments
 (0)