Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix preference tree for plugins #14036

Merged
merged 2 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/common/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface IJSONSchema {
$id?: string;
$schema?: string;
type?: JsonType | JsonType[];
owner?: string;
group?: string;
title?: string;
default?: JSONValue;
definitions?: IJSONSchemaMap;
Expand Down
13 changes: 12 additions & 1 deletion packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,22 @@ export class TheiaPluginScanner extends AbstractPluginScanner {
try {
if (rawPlugin.contributes.configuration) {
const configurations = Array.isArray(rawPlugin.contributes.configuration) ? rawPlugin.contributes.configuration : [rawPlugin.contributes.configuration];
const hasMultipleConfigs = configurations.length > 1;
contributions.configuration = [];
for (const c of configurations) {
const config = this.readConfiguration(c, rawPlugin.packagePath);
if (config) {
Object.values(config.properties).forEach(property => property.title = config.title);
Object.values(config.properties).forEach(property => {
if (hasMultipleConfigs) {
// If there are multiple configuration contributions, we need to distinguish them by their title in the settings UI.
// They are placed directly under the plugin's name in the settings UI.
property.owner = rawPlugin.displayName;
property.group = config.title;
} else {
// If there's only one configuration contribution, we display the title in the settings UI.
property.owner = config.title;
}
});
contributions.configuration.push(config);
}
}
Expand Down
126 changes: 98 additions & 28 deletions packages/preferences/src/browser/util/preference-tree-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ import debounce = require('@theia/core/shared/lodash.debounce');
import { Preference } from './preference-types';
import { COMMONLY_USED_SECTION_PREFIX, PreferenceLayoutProvider } from './preference-layout';

export interface CreatePreferencesGroupOptions {
id: string,
group: string,
root: CompositeTreeNode,
expanded?: boolean,
depth?: number,
label?: string
}

@injectable()
export class PreferenceTreeGenerator {

Expand Down Expand Up @@ -57,10 +66,22 @@ export class PreferenceTreeGenerator {
const root = this.createRootNode();

const commonlyUsedLayout = this.layoutProvider.getCommonlyUsedLayout();
const commonlyUsed = this.getOrCreatePreferencesGroup(commonlyUsedLayout.id, commonlyUsedLayout.id, root, groups, commonlyUsedLayout.label);
const commonlyUsed = this.getOrCreatePreferencesGroup({
id: commonlyUsedLayout.id,
group: commonlyUsedLayout.id,
root,
groups,
label: commonlyUsedLayout.label
});

for (const layout of this.layoutProvider.getLayout()) {
this.getOrCreatePreferencesGroup(layout.id, layout.id, root, groups, layout.label);
this.getOrCreatePreferencesGroup({
id: layout.id,
group: layout.id,
root,
groups,
label: layout.label
});
}
for (const preference of commonlyUsedLayout.settings ?? []) {
if (preference in preferencesSchema.properties) {
Expand All @@ -70,16 +91,11 @@ export class PreferenceTreeGenerator {
for (const propertyName of propertyNames) {
const property = preferencesSchema.properties[propertyName];
if (!this.preferenceConfigs.isSectionName(propertyName) && !OVERRIDE_PROPERTY_PATTERN.test(propertyName) && !property.deprecationMessage) {
const layoutItem = this.layoutProvider.getLayoutForPreference(propertyName);
const labels = layoutItem ? layoutItem.id.split('.') : propertyName.split('.');
// If a title is set, this property belongs to the 'extensions' category
const groupID = property.title ? this.defaultTopLevelCategory : this.getGroupName(labels);
// Automatically assign all properties with the same title to the same subgroup
const subgroupName = property.title ?? this.getSubgroupName(labels, groupID);
const subgroupID = [groupID, subgroupName].join('.');
const toplevelParent = this.getOrCreatePreferencesGroup(groupID, groupID, root, groups);
const immediateParent = subgroupName && this.getOrCreatePreferencesGroup(subgroupID, groupID, toplevelParent, groups, property.title ?? layoutItem?.label);
this.createLeafNode(propertyName, immediateParent || toplevelParent, property);
if (property.owner) {
this.createPluginLeafNode(propertyName, property, root, groups);
} else {
this.createBuiltinLeafNode(propertyName, property, root, groups);
}
}
}

Expand All @@ -103,6 +119,63 @@ export class PreferenceTreeGenerator {
return root;
};

protected createBuiltinLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>): void {
const layoutItem = this.layoutProvider.getLayoutForPreference(name);
const labels = layoutItem ? layoutItem.id.split('.') : name.split('.');
const groupID = this.getGroupName(labels);
const subgroupName = this.getSubgroupName(labels, groupID);
const subgroupID = [groupID, subgroupName].join('.');
const toplevelParent = this.getOrCreatePreferencesGroup({
id: groupID,
group: groupID,
root,
groups
});
const immediateParent = subgroupName ? this.getOrCreatePreferencesGroup({
id: subgroupID,
group: groupID,
root: toplevelParent,
groups,
label: layoutItem?.label
}) : undefined;
this.createLeafNode(name, immediateParent || toplevelParent, property);
}

protected createPluginLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>): void {
if (!property.owner) {
return;
}
const groupID = this.defaultTopLevelCategory;
const subgroupName = property.owner;
const subsubgroupName = property.group;
const hasGroup = Boolean(subsubgroupName);
const toplevelParent = this.getOrCreatePreferencesGroup({
id: groupID,
group: groupID,
root,
groups
});
const subgroupID = [groupID, subgroupName].join('.');
const subgroupParent = this.getOrCreatePreferencesGroup({
id: subgroupID,
group: groupID,
root: toplevelParent,
groups,
expanded: hasGroup,
label: subgroupName
});
const subsubgroupID = [groupID, subgroupName, subsubgroupName].join('.');
const subsubgroupParent = hasGroup ? this.getOrCreatePreferencesGroup({
id: subsubgroupID,
group: subgroupID,
root: subgroupParent,
groups,
depth: 2,
label: subsubgroupName
}) : undefined;
this.createLeafNode(name, subsubgroupParent || subgroupParent, property);
}

getNodeId(preferenceId: string): string {
const expectedGroup = this.getGroupName(preferenceId.split('.'));
const expectedId = `${expectedGroup}@${preferenceId}`;
Expand Down Expand Up @@ -151,40 +224,37 @@ export class PreferenceTreeGenerator {
preferenceId: property,
parent: preferencesGroup,
preference: { data },
depth: Preference.TreeNode.isTopLevel(preferencesGroup) ? 1 : 2,
depth: Preference.TreeNode.isTopLevel(preferencesGroup) ? 1 : 2
};
CompositeTreeNode.addChild(preferencesGroup, newNode);
return newNode;
}

protected createPreferencesGroup(id: string, group: string, root: CompositeTreeNode, label?: string): Preference.CompositeTreeNode {
protected createPreferencesGroup(options: CreatePreferencesGroupOptions): Preference.CompositeTreeNode {
const newNode: Preference.CompositeTreeNode = {
id: `${group}@${id}`,
id: `${options.group}@${options.id}`,
visible: true,
parent: root,
parent: options.root,
children: [],
expanded: false,
selected: false,
depth: 0,
label
label: options.label
};
const isTopLevel = Preference.TreeNode.isTopLevel(newNode);
if (!isTopLevel) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (newNode as any).expanded;
if (!(options.expanded ?? isTopLevel)) {
delete newNode.expanded;
}
newNode.depth = isTopLevel ? 0 : 1;
CompositeTreeNode.addChild(root, newNode);
newNode.depth = options.depth ?? (isTopLevel ? 0 : 1);
CompositeTreeNode.addChild(options.root, newNode);
return newNode;
}

protected getOrCreatePreferencesGroup(
id: string, group: string, root: CompositeTreeNode, groups: Map<string, Preference.CompositeTreeNode>, label?: string
): Preference.CompositeTreeNode {
const existingGroup = groups.get(id);
protected getOrCreatePreferencesGroup(options: CreatePreferencesGroupOptions & { groups: Map<string, Preference.CompositeTreeNode> }): Preference.CompositeTreeNode {
const existingGroup = options.groups.get(options.id);
if (existingGroup) { return existingGroup; }
const newNode = this.createPreferencesGroup(id, group, root, label);
groups.set(id, newNode);
const newNode = this.createPreferencesGroup(options);
options.groups.set(options.id, newNode);
return newNode;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,13 @@ export class PreferenceTreeLabelProvider implements LabelProviderContribution {
}

getName(node: Preference.TreeNode): string {
if (Preference.CompositeTreeNode.is(node) && node.label) {
if (Preference.TreeNode.is(node) && node.label) {
return node.label;
}
const { id } = Preference.TreeNode.getGroupAndIdFromNodeId(node.id);
const layouts = this.layoutProvider.getLayout();
const layout = layouts.find(e => e.id === id);
if (layout) {
return layout.label;
} else {
const labels = id.split('.');
const groupName = labels[labels.length - 1];
return this.formatString(groupName);
}
const labels = id.split('.');
const groupName = labels[labels.length - 1];
return this.formatString(groupName);
}

getPrefix(node: Preference.TreeNode, fullPath = false): string | undefined {
Expand Down
6 changes: 4 additions & 2 deletions packages/preferences/src/browser/util/preference-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
PreferenceDataProperty,
PreferenceScope,
TreeNode as BaseTreeNode,
ExpandableTreeNode,
CompositeTreeNode as BaseCompositeTreeNode,
SelectableTreeNode,
PreferenceInspection,
CommonCommands,
Expand Down Expand Up @@ -59,7 +59,8 @@ export namespace Preference {
};
}

export interface CompositeTreeNode extends ExpandableTreeNode, SelectableTreeNode {
export interface CompositeTreeNode extends BaseCompositeTreeNode, SelectableTreeNode {
expanded?: boolean;
depth: number;
label?: string;
}
Expand All @@ -69,6 +70,7 @@ export namespace Preference {
}

export interface LeafNode extends BaseTreeNode {
label?: string;
depth: number;
preference: { data: PreferenceDataProperty };
preferenceId: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '@theia/core/lib/browser';
import React = require('@theia/core/shared/react');
import { PreferenceTreeModel, PreferenceTreeNodeRow, PreferenceTreeNodeProps } from '../preference-tree-model';
import { Preference } from '../util/preference-types';

@injectable()
export class PreferencesTreeWidget extends TreeWidget {
Expand All @@ -50,13 +51,21 @@ export class PreferencesTreeWidget extends TreeWidget {
this.rows = new Map();
let index = 0;
for (const [id, nodeRow] of this.model.currentRows.entries()) {
if (nodeRow.visibleChildren > 0 && (ExpandableTreeNode.is(nodeRow.node) || ExpandableTreeNode.isExpanded(nodeRow.node.parent))) {
if (nodeRow.visibleChildren > 0 && this.isVisibleNode(nodeRow.node)) {
this.rows.set(id, { ...nodeRow, index: index++ });
}
}
this.updateScrollToRow();
}

protected isVisibleNode(node: Preference.TreeNode): boolean {
if (Preference.TreeNode.isTopLevel(node)) {
return true;
} else {
return ExpandableTreeNode.isExpanded(node.parent) && Preference.TreeNode.is(node.parent) && this.isVisibleNode(node.parent);
}
}

protected override doRenderNodeRow({ depth, visibleChildren, node, isExpansible }: PreferenceTreeNodeRow): React.ReactNode {
return this.renderNode(node, { depth, visibleChildren, isExpansible });
}
Expand Down
Loading