diff --git a/packages/core/src/browser/common-frontend-contribution.ts b/packages/core/src/browser/common-frontend-contribution.ts index 0772465578dc3..59b91ba310b48 100644 --- a/packages/core/src/browser/common-frontend-contribution.ts +++ b/packages/core/src/browser/common-frontend-contribution.ts @@ -1090,7 +1090,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi // if not yet contributed by Monaco, check runtime css variables to learn. // TODO: Following are not yet supported/no respective elements in theia: // list.focusBackground, list.focusForeground, list.inactiveFocusBackground, list.filterMatchBorder, - // list.dropBackground, listFilterWidget.outline, listFilterWidget.noMatchesOutline, tree.indentGuidesStroke + // list.dropBackground, listFilterWidget.outline, listFilterWidget.noMatchesOutline // list.invalidItemForeground, // list.warningForeground, list.errorForeground => tree node needs an respective class { id: 'list.activeSelectionBackground', defaults: { dark: '#094771', light: '#0074E8' }, description: 'List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.' }, @@ -1100,6 +1100,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi { id: 'list.hoverBackground', defaults: { dark: '#2A2D2E', light: '#F0F0F0' }, description: 'List/Tree background when hovering over items using the mouse.' }, { id: 'list.hoverForeground', description: 'List/Tree foreground when hovering over items using the mouse.' }, { id: 'list.filterMatchBackground', defaults: { dark: 'editor.findMatchHighlightBackground', light: 'editor.findMatchHighlightBackground' }, description: 'Background color of the filtered match.' }, + { id: 'tree.inactiveIndentGuidesStroke', defaults: { dark: Color.transparent('tree.indentGuidesStroke', 0.4), light: Color.transparent('tree.indentGuidesStroke', 0.4), hc: Color.transparent('tree.indentGuidesStroke', 0.4) }, description: 'Tree stroke color for the inactive indentation guides.' }, // Editor Group & Tabs colors should be aligned with https://code.visualstudio.com/api/references/theme-color#editor-groups-tabs { diff --git a/packages/core/src/browser/core-preferences.ts b/packages/core/src/browser/core-preferences.ts index 0bbbda9d654b3..e4dcd5ae3eeed 100644 --- a/packages/core/src/browser/core-preferences.ts +++ b/packages/core/src/browser/core-preferences.ts @@ -72,6 +72,12 @@ export const corePreferenceSchema: PreferenceSchema = { 'scope': 'language-overridable', 'enumDescriptions': Object.keys(SUPPORTED_ENCODINGS).map(key => SUPPORTED_ENCODINGS[key].labelLong), 'included': Object.keys(SUPPORTED_ENCODINGS).length > 1 + }, + 'workbench.tree.renderIndentGuides': { + type: 'string', + enum: ['onHover', 'none', 'always'], + default: 'onHover', + description: 'Controls whether the tree should render indent guides.' } } }; @@ -85,6 +91,7 @@ export interface CoreConfiguration { 'workbench.iconTheme'?: string | null; 'workbench.silentNotifications': boolean; 'files.encoding': string + 'workbench.tree.renderIndentGuides': 'onHover' | 'none' | 'always'; } export const CorePreferences = Symbol('CorePreferences'); diff --git a/packages/core/src/browser/style/tree.css b/packages/core/src/browser/style/tree.css index cf40dd2f687eb..b17c617a16c58 100644 --- a/packages/core/src/browser/style/tree.css +++ b/packages/core/src/browser/style/tree.css @@ -139,3 +139,16 @@ .theia-tree-element-node { width: 100% } + +.theia-tree-node-indent { + position: absolute; + height: var(--theia-content-line-height); + border-right: var(--theia-border-width) solid transparent; +} +.theia-tree-node-indent.always, +.theia-TreeContainer:hover .theia-tree-node-indent.hover { + border-color: var(--theia-tree-inactiveIndentGuidesStroke); +} +.theia-tree-node-indent.active { + border-color: var(--theia-tree-indentGuidesStroke); +} diff --git a/packages/core/src/browser/tree/tree-widget.tsx b/packages/core/src/browser/tree/tree-widget.tsx index cfaec1187ac83..00f4a77e268a5 100644 --- a/packages/core/src/browser/tree/tree-widget.tsx +++ b/packages/core/src/browser/tree/tree-widget.tsx @@ -38,6 +38,7 @@ import { ElementExt } from '@phosphor/domutils'; import { TreeWidgetSelection } from './tree-widget-selection'; import { MaybePromise } from '../../common/types'; import { LabelProvider } from '../label-provider'; +import { CorePreferences } from '../core-preferences'; const debounce = require('lodash.debounce'); @@ -52,6 +53,7 @@ export const TREE_NODE_SEGMENT_GROW_CLASS = 'theia-TreeNodeSegmentGrow'; export const EXPANDABLE_TREE_NODE_CLASS = 'theia-ExpandableTreeNode'; export const COMPOSITE_TREE_NODE_CLASS = 'theia-CompositeTreeNode'; export const TREE_NODE_CAPTION_CLASS = 'theia-TreeNodeCaption'; +export const TREE_NODE_INDENT_GUIDE_CLASS = 'theia-tree-node-indent'; export const TreeProps = Symbol('TreeProps'); @@ -162,6 +164,9 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { @inject(LabelProvider) protected readonly labelProvider: LabelProvider; + @inject(CorePreferences) + protected readonly corePreferences: CorePreferences; + protected shouldScrollToRow = true; constructor( @@ -251,6 +256,11 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { }) ]); } + this.toDispose.push(this.corePreferences.onPreferenceChanged(preference => { + if (preference.preferenceName === 'workbench.tree.renderIndentGuides') { + this.update(); + } + })); } /** @@ -478,7 +488,10 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { * Actually render the node row. */ protected doRenderNodeRow({ index, node, depth }: TreeWidget.NodeRow): React.ReactNode { - return this.renderNode(node, { depth }); + return + {this.renderIndent(node, { depth })} + {this.renderNode(node, { depth })} + ; } /** @@ -776,6 +789,55 @@ export class TreeWidget extends ReactWidget implements StatefulWidget { return iconClass.concat(additionalClasses).join(' '); } + /** + * Render indent for the file tree based on the depth + * @param node the tree node. + * @param depth the depth of the tree node. + */ + protected renderIndent(node: TreeNode, props: NodeProps): React.ReactNode { + const renderIndentGuides = this.corePreferences['workbench.tree.renderIndentGuides']; + if (renderIndentGuides === 'none') { + return undefined; + } + + const indentDivs: React.ReactNode[] = []; + let current: TreeNode | undefined = node; + let depth = props.depth; + while (current && depth) { + const classNames: string[] = [TREE_NODE_INDENT_GUIDE_CLASS]; + if (this.needsActiveIndentGuideline(current)) { + classNames.push('active'); + } else { + classNames.push(renderIndentGuides === 'onHover' ? 'hover' : 'always'); + } + const paddingLeft = this.props.leftPadding * depth; + indentDivs.unshift(
); + current = current.parent; + depth--; + } + return indentDivs; + } + + protected needsActiveIndentGuideline(node: TreeNode): boolean { + const parent = node.parent; + if (!parent || !this.isExpandable(parent)) { + return false; + } + if (SelectableTreeNode.isSelected(parent)) { + return true; + } + if (parent.expanded) { + for (const sibling of parent.children) { + if (SelectableTreeNode.isSelected(sibling) && !(this.isExpandable(sibling) && sibling.expanded)) { + return true; + } + } + } + return false; + } + /** * Render the node given the tree node and node properties. * @param node the tree node.