mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-07-12 13:56:34 +00:00
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: arduino/arduino-ide#2543 Signed-off-by: dankeboy36 <dankeboy36@gmail.com> --------- Signed-off-by: dankeboy36 <dankeboy36@gmail.com>
This commit is contained in:
parent
4cf9909a07
commit
3fc8474d71
@ -39,6 +39,7 @@
|
|||||||
"@theia/outline-view": "1.41.0",
|
"@theia/outline-view": "1.41.0",
|
||||||
"@theia/output": "1.41.0",
|
"@theia/output": "1.41.0",
|
||||||
"@theia/plugin-ext": "1.41.0",
|
"@theia/plugin-ext": "1.41.0",
|
||||||
|
"@theia/plugin-ext-vscode": "1.41.0",
|
||||||
"@theia/preferences": "1.41.0",
|
"@theia/preferences": "1.41.0",
|
||||||
"@theia/scm": "1.41.0",
|
"@theia/scm": "1.41.0",
|
||||||
"@theia/search-in-workspace": "1.41.0",
|
"@theia/search-in-workspace": "1.41.0",
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import '../../src/browser/style/index.css';
|
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 { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||||
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
import { bindViewContribution } from '@theia/core/lib/browser/shell/view-contribution';
|
||||||
@ -53,6 +57,8 @@ import {
|
|||||||
DockPanelRenderer as TheiaDockPanelRenderer,
|
DockPanelRenderer as TheiaDockPanelRenderer,
|
||||||
TabBarRendererFactory,
|
TabBarRendererFactory,
|
||||||
ContextMenuRenderer,
|
ContextMenuRenderer,
|
||||||
|
createTreeContainer,
|
||||||
|
TreeWidget,
|
||||||
} from '@theia/core/lib/browser';
|
} from '@theia/core/lib/browser';
|
||||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||||
import {
|
import {
|
||||||
@ -372,6 +378,15 @@ import { DebugSessionWidget } from '@theia/debug/lib/browser/view/debug-session-
|
|||||||
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
|
import { DebugConfigurationWidget } from './theia/debug/debug-configuration-widget';
|
||||||
import { DebugConfigurationWidget as TheiaDebugConfigurationWidget } from '@theia/debug/lib/browser/view/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 { 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.
|
// Hack to fix copy/cut/paste issue after electron version update in Theia.
|
||||||
// https://github.com/eclipse-theia/theia/issues/12487
|
// https://github.com/eclipse-theia/theia/issues/12487
|
||||||
@ -1082,4 +1097,43 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
rebind(TheiaTerminalFrontendContribution).toService(
|
rebind(TheiaTerminalFrontendContribution).toService(
|
||||||
TerminalFrontendContribution
|
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();
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -116,12 +116,16 @@ import { MessagingContribution } from './theia/core/messaging-contribution';
|
|||||||
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
|
import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service';
|
||||||
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
|
import { HostedPluginReader } from './theia/plugin-ext/plugin-reader';
|
||||||
import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/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 {
|
import {
|
||||||
LocalDirectoryPluginDeployerResolverWithFallback,
|
LocalDirectoryPluginDeployerResolverWithFallback,
|
||||||
PluginDeployer_GH_12064,
|
PluginDeployer_GH_12064,
|
||||||
} from './theia/plugin-ext/plugin-deployer';
|
} from './theia/plugin-ext/plugin-deployer';
|
||||||
import { SettingsReader } from './settings-reader';
|
import { SettingsReader } from './settings-reader';
|
||||||
|
import { VsCodePluginScanner } from './theia/plugin-ext-vscode/scanner-vscode';
|
||||||
|
|
||||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
bind(BackendApplication).toSelf().inSingletonScope();
|
bind(BackendApplication).toSelf().inSingletonScope();
|
||||||
@ -410,6 +414,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
|||||||
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope();
|
||||||
|
|
||||||
bind(SettingsReader).toSelf().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 {
|
function bindChildLogger(bind: interfaces.Bind, name: string): void {
|
||||||
|
@ -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;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user