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 <dankeboy36@gmail.com> * fix: update change proposal from Theia as is Ref: #2543 Signed-off-by: dankeboy36 <dankeboy36@gmail.com> --------- Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 55 additions & 1 deletion
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+
}
Lines changed: 241 additions & 0 deletions
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

Lines changed: 10 additions & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy