Skip to content

Commit 7a8acdc

Browse files
committed
fix: align viewsWelcome behavior to VS Code
Ref: eclipse-theia/theia#14309 Signed-off-by: dankeboy36 <[email protected]>
1 parent 7713668 commit 7a8acdc

File tree

5 files changed

+338
-2
lines changed

5 files changed

+338
-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,228 @@
1+
// import { OpenerService } from '@theia/core/lib/browser';
2+
import { DisposableCollection } from '@theia/core/lib/common/disposable';
3+
import { /*inject,*/ injectable } from '@theia/core/shared/inversify';
4+
import React from '@theia/core/shared/react';
5+
import { TreeViewWidget as TheiaTreeViewWidget } from '@theia/plugin-ext/lib/main/browser/view/tree-view-widget';
6+
7+
@injectable()
8+
export class TreeViewWidget extends TheiaTreeViewWidget {
9+
// @inject(OpenerService)
10+
// private readonly openerService: OpenerService;
11+
private readonly toDisposeBeforeUpdateViewWelcomeNodes =
12+
new DisposableCollection();
13+
14+
// The actual rewrite of the viewsWelcome rendering aligned to VS Code to fix https://github.com/eclipse-theia/theia/issues/14309
15+
// Based on https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/workbench/browser/parts/views/viewPane.ts#L228-L299
16+
protected override updateViewWelcomeNodes(): void {
17+
this.toDisposeBeforeUpdateViewWelcomeNodes.dispose();
18+
const viewWelcomes = this.visibleItems.sort((a, b) => a.order - b.order);
19+
this.viewWelcomeNodes = [];
20+
const allEnablementKeys: Set<string>[] = [];
21+
// the plugin-view-registry will push the changes when there is a change in the when context
22+
// this listener is to update the view when the `enablement` of the viewWelcomes changes
23+
this.toDisposeBeforeUpdateViewWelcomeNodes.push(
24+
this.contextKeyService.onDidChange((event) => {
25+
if (allEnablementKeys.some((keys) => event.affects(keys))) {
26+
this.updateViewWelcomeNodes();
27+
this.update();
28+
}
29+
})
30+
);
31+
// TODO: support `renderSecondaryButtons` prop from VS Code?
32+
for (const viewWelcome of viewWelcomes) {
33+
const { content } = viewWelcome;
34+
const enablement = isEnablementAware(viewWelcome)
35+
? viewWelcome.enablement
36+
: undefined;
37+
const enablementKeys = enablement
38+
? this.contextKeyService.parseKeys(enablement)
39+
: undefined;
40+
if (enablementKeys) {
41+
allEnablementKeys.push(enablementKeys);
42+
}
43+
const lines = content.split('\n');
44+
45+
for (let line of lines) {
46+
line = line.trim();
47+
48+
if (!line) {
49+
continue;
50+
}
51+
52+
const linkedText = parseLinkedText(line);
53+
54+
if (
55+
linkedText.nodes.length === 1 &&
56+
typeof linkedText.nodes[0] !== 'string'
57+
) {
58+
const node = linkedText.nodes[0];
59+
this.viewWelcomeNodes.push(
60+
this.renderButtonNode(
61+
node,
62+
this.viewWelcomeNodes.length,
63+
enablement
64+
)
65+
);
66+
} else {
67+
const paragraphNodes: React.ReactNode[] = [];
68+
for (const node of linkedText.nodes) {
69+
if (typeof node === 'string') {
70+
paragraphNodes.push(
71+
this.renderTextNode(node, this.viewWelcomeNodes.length)
72+
);
73+
} else {
74+
paragraphNodes.push(
75+
this.renderCommandLinkNode(
76+
node,
77+
this.viewWelcomeNodes.length,
78+
enablement
79+
)
80+
);
81+
}
82+
}
83+
if (paragraphNodes.length) {
84+
this.viewWelcomeNodes.push(
85+
<p key={`p-${this.viewWelcomeNodes.length}`}>
86+
{...paragraphNodes}
87+
</p>
88+
);
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
protected override renderButtonNode(
96+
node: ILink,
97+
lineKey: string | number,
98+
enablement: string | undefined = undefined
99+
): React.ReactNode {
100+
return (
101+
<div key={`line-${lineKey}`} className="theia-WelcomeViewButtonWrapper">
102+
<button
103+
title={node.title}
104+
className="theia-button theia-WelcomeViewButton"
105+
disabled={!this.isEnabled(enablement)}
106+
onClick={(e) => this.open(e, node)}
107+
>
108+
{node.label}
109+
</button>
110+
</div>
111+
);
112+
}
113+
114+
protected override renderCommandLinkNode(
115+
node: ILink,
116+
linkKey: string | number,
117+
enablement: string | undefined = undefined
118+
): React.ReactNode {
119+
return (
120+
<a
121+
key={`link-${linkKey}`}
122+
className={this.getLinkClassName(node.href, enablement)}
123+
title={node.title ?? ''}
124+
onClick={(e) => this.open(e, node)}
125+
>
126+
{node.label}
127+
</a>
128+
);
129+
}
130+
131+
protected override renderTextNode(
132+
node: string,
133+
textKey: string | number
134+
): React.ReactNode {
135+
return <span key={`text-${textKey}`}>{node}</span>;
136+
}
137+
138+
protected override getLinkClassName(
139+
href: string,
140+
enablement: string | undefined = undefined
141+
): string {
142+
const classNames = ['theia-WelcomeViewCommandLink'];
143+
// Only command-backed links can be disabled. All other, https:, file: remain enabled
144+
if (href.startsWith('command:') && !this.isEnabled(enablement)) {
145+
classNames.push('disabled');
146+
}
147+
return classNames.join(' ');
148+
}
149+
150+
private open(event: React.MouseEvent, node: ILink): void {
151+
event.preventDefault();
152+
if (node.href.startsWith('command:')) {
153+
const commandId = node.href.substring('commands:'.length - 1);
154+
this.commands.executeCommand(commandId);
155+
} else if (node.href.startsWith('file:')) {
156+
// TODO: check what Code does
157+
} else if (node.href.startsWith('https:')) {
158+
this.windowService.openNewWindow(node.href, { external: true });
159+
}
160+
}
161+
162+
/**
163+
* @param enablement [when context](https://code.visualstudio.com/api/references/when-clause-contexts) expression string
164+
*/
165+
private isEnabled(enablement: string | undefined): boolean {
166+
return typeof enablement === 'string'
167+
? this.contextKeyService.match(enablement)
168+
: true;
169+
}
170+
}
171+
172+
interface EnablementAware {
173+
readonly enablement: string | undefined;
174+
}
175+
176+
function isEnablementAware(arg: unknown): arg is EnablementAware {
177+
return !!arg && typeof arg === 'object' && 'enablement' in arg;
178+
}
179+
180+
// https://github.com/microsoft/vscode/blob/56b535f40900080fac8202c77914c5ce49fa4aae/src/vs/base/common/linkedText.ts#L8-L56
181+
export interface ILink {
182+
readonly label: string;
183+
readonly href: string;
184+
readonly title?: string;
185+
}
186+
187+
export type LinkedTextNode = string | ILink;
188+
189+
export class LinkedText {
190+
constructor(readonly nodes: LinkedTextNode[]) {}
191+
toString(): string {
192+
return this.nodes
193+
.map((node) => (typeof node === 'string' ? node : node.label))
194+
.join('');
195+
}
196+
}
197+
198+
const LINK_REGEX =
199+
/\[([^\]]+)\]\(((?:https?:\/\/|command:|file:)[^\)\s]+)(?: (["'])(.+?)(\3))?\)/gi;
200+
201+
export function parseLinkedText(text: string): LinkedText {
202+
const result: LinkedTextNode[] = [];
203+
204+
let index = 0;
205+
let match: RegExpExecArray | null;
206+
207+
while ((match = LINK_REGEX.exec(text))) {
208+
if (match.index - index > 0) {
209+
result.push(text.substring(index, match.index));
210+
}
211+
212+
const [, label, href, , title] = match;
213+
214+
if (title) {
215+
result.push({ label, href, title });
216+
} else {
217+
result.push({ label, href });
218+
}
219+
220+
index = match.index + match[0].length;
221+
}
222+
223+
if (index < text.length) {
224+
result.push(text.substring(index));
225+
}
226+
227+
return new LinkedText(result);
228+
}

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 {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { injectable, postConstruct } from '@theia/core/shared/inversify';
2+
import { VsCodePluginScanner as TheiaVsCodePluginScanner } from '@theia/plugin-ext-vscode/lib/node/scanner-vscode';
3+
import {
4+
PluginPackageViewWelcome,
5+
ViewWelcome,
6+
} from '@theia/plugin-ext/lib/common/plugin-protocol';
7+
8+
@injectable()
9+
export class VsCodePluginScanner extends TheiaVsCodePluginScanner {
10+
@postConstruct()
11+
protected init(): void {
12+
this['readViewWelcome'] = (
13+
rawViewWelcome: PluginPackageViewWelcome,
14+
pluginViewsIds: string[]
15+
) => {
16+
const result = {
17+
view: rawViewWelcome.view,
18+
content: rawViewWelcome.contents,
19+
when: rawViewWelcome.when,
20+
// if the plugin contributes Welcome view to its own view - it will be ordered first
21+
order:
22+
pluginViewsIds.findIndex((v) => v === rawViewWelcome.view) > -1
23+
? 0
24+
: 1,
25+
};
26+
return maybeSetEnablement(rawViewWelcome, result);
27+
};
28+
}
29+
}
30+
31+
// This is not yet supported by Theia but available in Code (https://github.com/microsoft/vscode/issues/114304)
32+
function maybeSetEnablement(
33+
rawViewWelcome: PluginPackageViewWelcome,
34+
result: ViewWelcome
35+
) {
36+
const enablement =
37+
'enablement' in rawViewWelcome &&
38+
typeof rawViewWelcome['enablement'] === 'string' &&
39+
rawViewWelcome['enablement'];
40+
if (enablement) {
41+
Object.assign(result, { enablement });
42+
}
43+
return result;
44+
}

0 commit comments

Comments
 (0)