diff --git a/packages/core/src/common/json-schema.ts b/packages/core/src/common/json-schema.ts index 07885c0717a7d..04cb90e30e814 100644 --- a/packages/core/src/common/json-schema.ts +++ b/packages/core/src/common/json-schema.ts @@ -36,6 +36,8 @@ export interface IJSONSchema { $id?: string; $schema?: string; type?: JsonType | JsonType[]; + owner?: string; + group?: string; title?: string; default?: JSONValue; definitions?: IJSONSchemaMap; diff --git a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts index 1d309445f0bfb..338f5eea7368e 100644 --- a/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts +++ b/packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts @@ -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); } } diff --git a/packages/preferences/src/browser/util/preference-tree-generator.ts b/packages/preferences/src/browser/util/preference-tree-generator.ts index 05648a634b5b2..bacd4035d8534 100644 --- a/packages/preferences/src/browser/util/preference-tree-generator.ts +++ b/packages/preferences/src/browser/util/preference-tree-generator.ts @@ -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 { @@ -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) { @@ -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); + } } } @@ -103,6 +119,63 @@ export class PreferenceTreeGenerator { return root; }; + protected createBuiltinLeafNode(name: string, property: PreferenceDataProperty, root: CompositeTreeNode, groups: Map): 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): 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}`; @@ -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, label?: string - ): Preference.CompositeTreeNode { - const existingGroup = groups.get(id); + protected getOrCreatePreferencesGroup(options: CreatePreferencesGroupOptions & { groups: Map }): 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; }; } diff --git a/packages/preferences/src/browser/util/preference-tree-label-provider.ts b/packages/preferences/src/browser/util/preference-tree-label-provider.ts index a40f1fbd272af..7ed9b0d35f23a 100644 --- a/packages/preferences/src/browser/util/preference-tree-label-provider.ts +++ b/packages/preferences/src/browser/util/preference-tree-label-provider.ts @@ -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 { diff --git a/packages/preferences/src/browser/util/preference-types.ts b/packages/preferences/src/browser/util/preference-types.ts index 29ee2bf3f67c9..bf4a81521548c 100644 --- a/packages/preferences/src/browser/util/preference-types.ts +++ b/packages/preferences/src/browser/util/preference-types.ts @@ -18,7 +18,7 @@ import { PreferenceDataProperty, PreferenceScope, TreeNode as BaseTreeNode, - ExpandableTreeNode, + CompositeTreeNode as BaseCompositeTreeNode, SelectableTreeNode, PreferenceInspection, CommonCommands, @@ -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; } @@ -69,6 +70,7 @@ export namespace Preference { } export interface LeafNode extends BaseTreeNode { + label?: string; depth: number; preference: { data: PreferenceDataProperty }; preferenceId: string; diff --git a/packages/preferences/src/browser/views/preference-tree-widget.tsx b/packages/preferences/src/browser/views/preference-tree-widget.tsx index 29a94d1875b90..77f0caef02682 100644 --- a/packages/preferences/src/browser/views/preference-tree-widget.tsx +++ b/packages/preferences/src/browser/views/preference-tree-widget.tsx @@ -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 { @@ -50,13 +51,28 @@ 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)) { this.rows.set(id, { ...nodeRow, index: index++ }); } } this.updateScrollToRow(); } + protected isVisibleNode(row: PreferenceTreeNodeRow): boolean { + const node = row.node; + if (Preference.TreeNode.isTopLevel(node)) { + return true; + } + let parent = node.parent; + while (parent) { + if (ExpandableTreeNode.isCollapsed(parent)) { + return false; + } + parent = parent.parent; + } + return true; + } + protected override doRenderNodeRow({ depth, visibleChildren, node, isExpansible }: PreferenceTreeNodeRow): React.ReactNode { return this.renderNode(node, { depth, visibleChildren, isExpansible }); }