diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index c94d2ee4ea8..022618d0381 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -54,6 +54,7 @@ "./vs/editor/contrib/cursorUndo/cursorUndo.ts", "./vs/editor/contrib/dnd/dnd.ts", "./vs/editor/contrib/dnd/dragAndDropCommand.ts", + "./vs/editor/contrib/documentSymbols/outline.ts", "./vs/editor/contrib/documentSymbols/outlineModel.ts", "./vs/editor/contrib/find/findController.ts", "./vs/editor/contrib/find/findDecorations.ts", @@ -547,7 +548,6 @@ "./vs/workbench/contrib/markers/electron-browser/markersPanelActions.ts", "./vs/workbench/contrib/markers/electron-browser/messages.ts", "./vs/workbench/contrib/markers/test/electron-browser/markersModel.test.ts", - "./vs/workbench/contrib/outline/electron-browser/outline.ts", "./vs/workbench/contrib/output/common/output.ts", "./vs/workbench/contrib/output/common/outputLinkComputer.ts", "./vs/workbench/contrib/output/common/outputLinkProvider.ts", @@ -746,4 +746,4 @@ "exclude": [ "./typings/require-monaco.d.ts" ] -} \ No newline at end of file +} diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 1468afc9581..4bad77f0289 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -864,8 +864,8 @@ export const symbolKindToCssClass = (function () { _fromMapping[SymbolKind.Operator] = 'operator'; _fromMapping[SymbolKind.TypeParameter] = 'type-parameter'; - return function toCssClassName(kind: SymbolKind): string { - return `symbol-icon ${_fromMapping[kind] || 'property'}`; + return function toCssClassName(kind: SymbolKind, inline?: boolean): string { + return `symbol-icon ${inline ? 'inline' : 'block'} ${_fromMapping[kind] || 'property'}`; }; })(); diff --git a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css index a208aacb5dd..ebc2389a0e2 100644 --- a/src/vs/editor/contrib/documentSymbols/media/outlineTree.css +++ b/src/vs/editor/contrib/documentSymbols/media/outlineTree.css @@ -3,49 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-tree.focused .selected .outline-element-label, .monaco-tree.focused .selected .outline-element-decoration { +.monaco-list .monaco-list-row.focused.selected .outline-element .monaco-highlighted-label, +.monaco-list .monaco-list-row.focused.selected .outline-element-decoration { /* make sure selection color wins when a label is being selected */ color: inherit !important; } -.monaco-tree .outline-element { +.monaco-list .outline-element { display: flex; flex: 1; flex-flow: row nowrap; align-items: center; } -.monaco-tree .outline-element .outline-element-icon { - padding-right: 3px; -} - -.monaco-tree .outline-element .outline-element-label { - text-overflow: ellipsis; - overflow: hidden; +.monaco-list .outline-element .monaco-highlighted-label { color: var(--outline-element-color); } -.monaco-tree .outline-element .outline-element-label .monaco-highlighted-label .highlight { - font-weight: bold; -} - -.monaco-tree .outline-element .outline-element-detail { - visibility: hidden; - flex: 1; - flex-basis: 10%; - opacity: 0.8; - overflow: hidden; - text-overflow: ellipsis; - font-size: 90%; - padding-left: 4px; - padding-top: 3px; -} - .monaco-tree .monaco-tree-row.focused .outline-element .outline-element-detail { visibility: inherit; } -.monaco-tree .outline-element .outline-element-decoration { +.monaco-list .outline-element .outline-element-decoration { opacity: 0.75; font-size: 90%; font-weight: 600; @@ -55,7 +34,7 @@ color: var(--outline-element-color); } -.monaco-tree .outline-element .outline-element-decoration.bubble { +.monaco-list .outline-element .outline-element-decoration.bubble { font-family: octicons; font-size: 14px; opacity: 0.4; diff --git a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css index 76d67d5efcc..2a6a31185fa 100644 --- a/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css +++ b/src/vs/editor/contrib/documentSymbols/media/symbol-icons.css @@ -3,7 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .symbol-icon { +.monaco-workbench .symbol-icon.inline { + background-position: left center; + padding-left: 20px; + background-size: 16px 16px; +} + +.monaco-workbench .symbol-icon.block { display: inline-block; height: 14px; width: 16px; diff --git a/src/vs/workbench/contrib/outline/electron-browser/outline.ts b/src/vs/editor/contrib/documentSymbols/outline.ts similarity index 100% rename from src/vs/workbench/contrib/outline/electron-browser/outline.ts rename to src/vs/editor/contrib/documentSymbols/outline.ts diff --git a/src/vs/editor/contrib/documentSymbols/outlineModel.ts b/src/vs/editor/contrib/documentSymbols/outlineModel.ts index 76219b58881..179b23f3a11 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineModel.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineModel.ts @@ -7,7 +7,6 @@ import { binarySearch, coalesceInPlace } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { first, forEach, size } from 'vs/base/common/collections'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { LRUCache } from 'vs/base/common/map'; import { commonPrefixLength } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; @@ -90,7 +89,6 @@ export abstract class TreeElement { export class OutlineElement extends TreeElement { children: { [id: string]: OutlineElement; } = Object.create(null); - score: FuzzyScore | undefined = FuzzyScore.Default; marker: { count: number, topSev: MarkerSeverity } | undefined; constructor( @@ -127,33 +125,6 @@ export class OutlineGroup extends TreeElement { return res; } - updateMatches(pattern: string, topMatch: OutlineElement | undefined): OutlineElement | undefined { - for (const key in this.children) { - topMatch = this._updateMatches(pattern, this.children[key], topMatch); - } - return topMatch; - } - - private _updateMatches(pattern: string, item: OutlineElement, topMatch: OutlineElement | undefined): OutlineElement | undefined { - - item.score = pattern - ? fuzzyScore(pattern, pattern.toLowerCase(), 0, item.symbol.name, item.symbol.name.toLowerCase(), 0, true) - : FuzzyScore.Default; - - if (item.score && (!topMatch || !topMatch.score || item.score[0] > topMatch.score[0])) { - topMatch = item; - } - for (const key in item.children) { - let child = item.children[key]; - topMatch = this._updateMatches(pattern, child, topMatch); - if (!item.score && child.score) { - // don't filter parents with unfiltered children - item.score = FuzzyScore.Default; - } - } - return topMatch; - } - getItemEnclosingPosition(position: IPosition): OutlineElement | undefined { return position ? this._getItemEnclosingPosition(position, this.children) : undefined; } @@ -395,20 +366,6 @@ export class OutlineModel extends TreeElement { return true; } - private _matches: [string, OutlineElement | undefined]; - - updateMatches(pattern: string): OutlineElement | undefined { - if (this._matches && this._matches[0] === pattern) { - return this._matches[1]; - } - let topMatch: OutlineElement | undefined; - for (const key in this._groups) { - topMatch = this._groups[key].updateMatches(pattern, topMatch); - } - this._matches = [pattern, topMatch]; - return topMatch; - } - getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement | undefined { let preferredGroup: OutlineGroup | undefined; diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 4c529be6e97..46f351a34e6 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -4,178 +4,140 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { values } from 'vs/base/common/collections'; -import { createMatches } from 'vs/base/common/filters'; -import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import 'vs/css!./media/outlineTree'; import 'vs/css!./media/symbol-icons'; import { Range } from 'vs/editor/common/core/range'; import { SymbolKind, symbolKindToCssClass } from 'vs/editor/common/modes'; import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; +import { IKeybindingService, IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { OutlineConfigKeys } from 'vs/editor/contrib/documentSymbols/outline'; import { MarkerSeverity } from 'vs/platform/markers/common/markers'; -import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; -export const enum OutlineItemCompareType { - ByPosition, - ByName, - ByKind -} +export type OutlineItem = OutlineGroup | OutlineElement; -export class OutlineItemComparator implements ISorter { +export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelProvider { - constructor( - public type: OutlineItemCompareType = OutlineItemCompareType.ByPosition - ) { } + constructor(@IKeybindingService private readonly _keybindingService: IKeybindingService) { } - compare(tree: ITree, a: OutlineGroup | OutlineElement, b: OutlineGroup | OutlineElement): number { - - if (a instanceof OutlineGroup && b instanceof OutlineGroup) { - return a.providerIndex - b.providerIndex; + getKeyboardNavigationLabel(element: OutlineItem): { toString(): string; } { + if (element instanceof OutlineGroup) { + return element.provider.displayName; + } else { + return element.symbol.name; } + } - if (a instanceof OutlineElement && b instanceof OutlineElement) { - switch (this.type) { - case OutlineItemCompareType.ByKind: - return a.symbol.kind - b.symbol.kind; - case OutlineItemCompareType.ByName: - return a.symbol.name.localeCompare(b.symbol.name); - case OutlineItemCompareType.ByPosition: - default: - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); - } - } - - return 0; + mightProducePrintableCharacter(event: IKeyboardEvent): boolean { + return this._keybindingService.mightProducePrintableCharacter(event); } } -export class OutlineItemFilter implements IFilter { - enabled: boolean = true; - - isVisible(tree: ITree, element: OutlineElement | any): boolean { - if (!this.enabled) { - return true; - } - return !(element instanceof OutlineElement) || Boolean(element.score); +export class OutlineIdentityProvider implements IIdentityProvider { + getId(element: TreeElement): { toString(): string; } { + return element.id; } } -export class OutlineDataSource implements IDataSource { +export class OutlineGroupTemplate { + static id = 'OutlineGroupTemplate'; - // this is a workaround for the tree showing twisties for items - // with only filtered children - filterOnScore: boolean = true; - - getId(tree: ITree, element: TreeElement): string { - return element ? element.id : 'empty'; - } - - hasChildren(tree: ITree, element: OutlineModel | OutlineGroup | OutlineElement): boolean { - if (!element) { - return false; - } - if (element instanceof OutlineModel) { - return true; - } - if (element instanceof OutlineElement && (this.filterOnScore && !element.score)) { - return false; - } - for (const id in element.children) { - if (!this.filterOnScore || element.children[id].score) { - return true; - } - } - return false; - } - - getChildren(tree: ITree, element: TreeElement): Promise { - let res = values(element.children); - // console.log(element.id + ' with children ' + res.length); - return Promise.resolve(res); - } - - getParent(tree: ITree, element: TreeElement | any): Promise { - return Promise.resolve(element && element.parent); - } - - shouldAutoexpand(tree: ITree, element: TreeElement): boolean { - return element && (element instanceof OutlineModel || element.parent instanceof OutlineModel || element instanceof OutlineGroup || element.parent instanceof OutlineGroup); - } -} - -export interface OutlineTemplate { labelContainer: HTMLElement; label: HighlightedLabel; - icon?: HTMLElement; - detail?: HTMLElement; - decoration?: HTMLElement; } -export class OutlineRenderer implements IRenderer { +export class OutlineElementTemplate { + static id = 'OutlineElementTemplate'; + container: HTMLElement; + iconLabel: IconLabel; + decoration: HTMLElement; +} - renderProblemColors = true; - renderProblemBadges = true; +export class OutlineVirtualDelegate implements IListVirtualDelegate { - constructor( - @IThemeService readonly _themeService: IThemeService, - @IConfigurationService readonly _configurationService: IConfigurationService - ) { - // - } - - getHeight(tree: ITree, element: any): number { + getHeight(_element: OutlineItem): number { return 22; } - getTemplateId(tree: ITree, element: OutlineGroup | OutlineElement): string { - return element instanceof OutlineGroup ? 'outline-group' : 'outline-element'; - } - - renderTemplate(tree: ITree, templateId: string, container: HTMLElement): OutlineTemplate { - if (templateId === 'outline-element') { - const icon = dom.$('.outline-element-icon symbol-icon'); - const labelContainer = dom.$('.outline-element-label'); - const detail = dom.$('.outline-element-detail'); - const decoration = dom.$('.outline-element-decoration'); - dom.addClass(container, 'outline-element'); - dom.append(container, icon, labelContainer, detail, decoration); - return { icon, labelContainer, label: new HighlightedLabel(labelContainer, true), detail, decoration }; - } - if (templateId === 'outline-group') { - const labelContainer = dom.$('.outline-element-label'); - dom.addClass(container, 'outline-element'); - dom.append(container, labelContainer); - return { labelContainer, label: new HighlightedLabel(labelContainer, true) }; - } - - throw new Error(templateId); - } - - renderElement(tree: ITree, element: OutlineGroup | OutlineElement, templateId: string, template: OutlineTemplate): void { - if (element instanceof OutlineElement) { - template.icon.className = `outline-element-icon ${symbolKindToCssClass(element.symbol.kind)}`; - template.label.set(element.symbol.name, element.score ? createMatches(element.score) : undefined, localize('title.template', "{0} ({1})", element.symbol.name, OutlineRenderer._symbolKindNames[element.symbol.kind])); - template.detail.innerText = element.symbol.detail || ''; - this._renderMarkerInfo(element, template); - - } + getTemplateId(element: OutlineItem): string { if (element instanceof OutlineGroup) { - template.label.set(element.provider.displayName || localize('provider', "Outline Provider")); + return OutlineGroupTemplate.id; + } else { + return OutlineElementTemplate.id; } } +} - private _renderMarkerInfo(element: OutlineElement, template: OutlineTemplate): void { +export class OutlineGroupRenderer implements ITreeRenderer { + + readonly templateId: string = OutlineGroupTemplate.id; + + renderTemplate(container: HTMLElement): OutlineGroupTemplate { + const labelContainer = dom.$('.outline-element-label'); + dom.addClass(container, 'outline-element'); + dom.append(container, labelContainer); + return { labelContainer, label: new HighlightedLabel(labelContainer, true) }; + } + + renderElement(node: ITreeNode, index: number, template: OutlineGroupTemplate): void { + template.label.set( + node.element.provider.displayName || localize('provider', "Outline Provider"), + createMatches(node.filterData) + ); + } + + disposeTemplate(_template: OutlineGroupTemplate): void { + // nothing + } +} + +export class OutlineElementRenderer implements ITreeRenderer { + + readonly templateId: string = OutlineElementTemplate.id; + + constructor( + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IThemeService private readonly _themeService: IThemeService, + ) { } + + renderTemplate(container: HTMLElement): OutlineElementTemplate { + dom.addClass(container, 'outline-element'); + const iconLabel = new IconLabel(container, { supportHighlights: true }); + const decoration = dom.$('.outline-element-decoration'); + container.appendChild(decoration); + return { container, iconLabel, decoration }; + } + + renderElement(node: ITreeNode, index: number, template: OutlineElementTemplate): void { + const { element } = node; + const options = { + matches: createMatches(node.filterData), + extraClasses: [], + title: localize('title.template', "{0} ({1})", element.symbol.name, OutlineElementRenderer._symbolKindNames[element.symbol.kind]) + }; + if (this._configurationService.getValue(OutlineConfigKeys.icons)) { + // add styles for the icons + options.extraClasses.push(`outline-element-icon ${symbolKindToCssClass(element.symbol.kind, true)}`); + } + template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options); + this._renderMarkerInfo(element, template); + } + + private _renderMarkerInfo(element: OutlineElement, template: OutlineElementTemplate): void { if (!element.marker) { dom.hide(template.decoration); - template.labelContainer.style.removeProperty('--outline-element-color'); + template.container.style.removeProperty('--outline-element-color'); return; } @@ -184,14 +146,14 @@ export class OutlineRenderer implements IRenderer { const cssColor = color ? color.toString() : 'inherit'; // color of the label - if (this.renderProblemColors) { - template.labelContainer.style.setProperty('--outline-element-color', cssColor); + if (this._configurationService.getValue(OutlineConfigKeys.problemsColors)) { + template.container.style.setProperty('--outline-element-color', cssColor); } else { - template.labelContainer.style.removeProperty('--outline-element-color'); + template.container.style.removeProperty('--outline-element-color'); } // badge with color/rollup - if (!this.renderProblemBadges) { + if (!this._configurationService.getValue(OutlineConfigKeys.problemsBadges)) { dom.hide(template.decoration); } else if (count > 0) { @@ -239,77 +201,46 @@ export class OutlineRenderer implements IRenderer { [SymbolKind.Variable]: localize('Variable', "variable"), }; - disposeTemplate(tree: ITree, templateId: string, template: OutlineTemplate): void { - // noop + disposeTemplate(_template: OutlineElementTemplate): void { + _template.iconLabel.dispose(); } } -export class OutlineTreeState { +export const enum OutlineSortOrder { + ByPosition, + ByName, + ByKind +} - readonly selected: string; - readonly focused: string; - readonly expanded: string[]; +export class OutlineItemComparator implements ITreeSorter { - static capture(tree: ITree): OutlineTreeState { - // selection - let selected: string; - let element = tree.getSelection()[0]; - if (element instanceof TreeElement) { - selected = element.id; - } + constructor( + public type: OutlineSortOrder = OutlineSortOrder.ByPosition + ) { } - // focus - let focused: string; - element = tree.getFocus(true); - if (element instanceof TreeElement) { - focused = element.id; - } + compare(a: OutlineItem, b: OutlineItem): number { + if (a instanceof OutlineGroup && b instanceof OutlineGroup) { + return a.providerIndex - b.providerIndex; - // expansion - let expanded = new Array(); - let nav = tree.getNavigator(); - while (nav.next()) { - let element = nav.current(); - if (element instanceof TreeElement) { - if (tree.isExpanded(element)) { - expanded.push(element.id); - } + } else if (a instanceof OutlineElement && b instanceof OutlineElement) { + if (this.type === OutlineSortOrder.ByKind) { + return a.symbol.kind - b.symbol.kind || a.symbol.name.localeCompare(b.symbol.name); + } else if (this.type === OutlineSortOrder.ByName) { + return a.symbol.name.localeCompare(b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + } else if (this.type === OutlineSortOrder.ByPosition) { + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || a.symbol.name.localeCompare(b.symbol.name); } } - return { selected, focused, expanded }; - } - - static async restore(tree: ITree, state: OutlineTreeState, eventPayload: any): Promise { - let model = tree.getInput(); - if (!state || !(model instanceof OutlineModel)) { - return Promise.resolve(undefined); - } - - // expansion - let items: TreeElement[] = []; - for (const id of state.expanded) { - let item = model.getItemById(id); - if (item) { - items.push(item); - } - } - await tree.collapseAll(undefined); - await tree.expandAll(items); - - // selection & focus - let selected = model.getItemById(state.selected); - let focused = model.getItemById(state.focused); - tree.setSelection([selected], eventPayload); - tree.setFocus(focused, eventPayload); + return 0; } } -export class OutlineController extends WorkbenchTreeController { - protected shouldToggleExpansion(element: any, event: IMouseEvent, origin: string): boolean { - if (element instanceof OutlineElement) { - return this.isClickOnTwistie(event); - } else { - return super.shouldToggleExpansion(element, event, origin); +export class OutlineDataSource implements IDataSource { + + getChildren(element: undefined | OutlineModel | OutlineGroup | OutlineElement): OutlineItem[] { + if (!element) { + return []; } + return values(element.children); } } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 43eaa56afa9..389b713aebf 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -3,32 +3,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { addClass, addStandardDisposableListener, createStyleSheet, getTotalHeight, removeClass } from 'vs/base/browser/dom'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { createStyleSheet } from 'vs/base/browser/dom'; import { IListMouseEvent, IListTouchEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IPagedRenderer, PagedList } from 'vs/base/browser/ui/list/listPaging'; import { DefaultStyleController, IListOptions, IMultipleSelectionController, IOpenController, isSelectionRangeChangeEvent, isSelectionSingleChangeEvent, List } from 'vs/base/browser/ui/list/listWidget'; -import { canceled, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { FuzzyScore } from 'vs/base/common/filters'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { combinedDisposable, Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IFilter, ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; +import { ITree, ITreeConfiguration, ITreeOptions } from 'vs/base/parts/tree/browser/tree'; import { ClickBehavior, DefaultController, DefaultTreestyler, IControllerOptions, OpenMode } from 'vs/base/parts/tree/browser/treeDefaults'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; -import { attachInputBoxStyler, attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; +import { attachListStyler, computeStyles, defaultListStyles } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { InputFocusedContextKey } from 'vs/platform/workbench/common/contextkeys'; import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; @@ -723,241 +717,6 @@ export class TreeResourceNavigator2 extends Disposable { } } -export interface IHighlighter { - getHighlights(tree: ITree, element: any, pattern: string): FuzzyScore; - getHighlightsStorageKey?(element: any): any; -} - -export interface IHighlightingTreeConfiguration extends ITreeConfiguration { - highlighter: IHighlighter; -} - -export interface IHighlightingTreeOptions extends ITreeOptions { - filterOnType?: boolean; -} - -export class HighlightingTreeController extends WorkbenchTreeController { - - constructor( - options: IControllerOptions, - private readonly onType: () => any, - @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - ) { - super(options, configurationService); - } - - onKeyDown(tree: ITree, event: IKeyboardEvent) { - let handled = super.onKeyDown(tree, event); - if (handled) { - return true; - } - if (this.upKeyBindingDispatcher.has(event.keyCode)) { - return false; - } - if (this._keybindingService.mightProducePrintableCharacter(event)) { - this.onType(); - return true; - } - return false; - } -} - -class HightlightsFilter implements IFilter { - - static add(config: ITreeConfiguration, options: IHighlightingTreeOptions): ITreeConfiguration { - const myFilter = new HightlightsFilter(); - myFilter.enabled = !!options.filterOnType; - if (!config.filter) { - config.filter = myFilter; - } else { - let otherFilter = config.filter; - config.filter = { - isVisible(tree: ITree, element: any): boolean { - return myFilter.isVisible(tree, element) && otherFilter.isVisible(tree, element); - } - }; - } - return config; - } - - enabled: boolean = true; - - isVisible(tree: ITree, element: any): boolean { - if (!this.enabled) { - return true; - } - let tree2 = (tree as HighlightingWorkbenchTree); - if (!tree2.isHighlighterScoring()) { - return true; - } - if (tree2.getHighlighterScore(element)) { - return true; - } - return false; - } -} - -export class HighlightingWorkbenchTree extends WorkbenchTree { - - protected readonly domNode: HTMLElement; - protected readonly inputContainer: HTMLElement; - protected readonly input: InputBox; - - protected readonly highlighter: IHighlighter; - protected readonly highlights: Map; - - private readonly _onDidStartFilter: Emitter; - readonly onDidStartFiltering: Event; - - constructor( - parent: HTMLElement, - treeConfiguration: IHighlightingTreeConfiguration, - treeOptions: IHighlightingTreeOptions, - listOptions: IInputOptions, - @IContextKeyService contextKeyService: IContextKeyService, - @IContextViewService contextViewService: IContextViewService, - @IListService listService: IListService, - @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService - ) { - // build html skeleton - const container = document.createElement('div'); - container.className = 'highlighting-tree'; - const inputContainer = document.createElement('div'); - inputContainer.className = 'input'; - const treeContainer = document.createElement('div'); - treeContainer.className = 'tree'; - container.appendChild(inputContainer); - container.appendChild(treeContainer); - parent.appendChild(container); - - // create tree - treeConfiguration.controller = treeConfiguration.controller || instantiationService.createInstance(HighlightingTreeController, {}, () => this.onTypeInTree()); - super(treeContainer, HightlightsFilter.add(treeConfiguration, treeOptions), treeOptions, contextKeyService, listService, themeService, instantiationService, configurationService); - this.highlighter = treeConfiguration.highlighter; - this.highlights = new Map(); - - this.domNode = container; - addClass(this.domNode, 'inactive'); - - // create input - this.inputContainer = inputContainer; - this.input = new InputBox(inputContainer, contextViewService, listOptions); - this.input.setEnabled(false); - this.input.onDidChange(this.updateHighlights, this, this.disposables); - this.disposables.push(attachInputBoxStyler(this.input, themeService)); - this.disposables.push(this.input); - this.disposables.push(addStandardDisposableListener(this.input.inputElement, 'keydown', event => { - //todo@joh make this command/context-key based - switch (event.keyCode) { - case KeyCode.UpArrow: - case KeyCode.DownArrow: - case KeyCode.Tab: - this.domFocus(); - event.preventDefault(); - break; - case KeyCode.Enter: - this.setSelection(this.getSelection()); - event.preventDefault(); - break; - case KeyCode.Escape: - this.input.value = ''; - this.domFocus(); - event.preventDefault(); - break; - } - })); - - this._onDidStartFilter = new Emitter(); - this.onDidStartFiltering = this._onDidStartFilter.event; - this.disposables.push(this._onDidStartFilter); - } - - setInput(element: any): Promise { - this.input.setEnabled(false); - return super.setInput(element).then(value => { - if (!this.input.inputElement) { - // has been disposed in the meantime -> cancel - return Promise.reject(canceled()); - } - this.input.setEnabled(true); - return value; - }); - } - - layout(height?: number, width?: number): void { - this.input.layout(); - super.layout(typeof height !== 'number' || isNaN(height) ? height : height - getTotalHeight(this.inputContainer), width); - } - - private onTypeInTree(): void { - removeClass(this.domNode, 'inactive'); - this.input.focus(); - this.layout(); - this._onDidStartFilter.fire(this); - } - - private lastSelection: any[]; - - private updateHighlights(pattern: string): void { - - // remember old selection - let defaultSelection: any[] = []; - if (!this.lastSelection && pattern) { - this.lastSelection = this.getSelection(); - } else if (this.lastSelection && !pattern) { - defaultSelection = this.lastSelection; - this.lastSelection = []; - } - - let topElement: any; - if (pattern) { - let nav = this.getNavigator(undefined, false); - let topScore: FuzzyScore | undefined; - while (nav.next()) { - let element = nav.current(); - let score = this.highlighter.getHighlights(this, element, pattern); - this.highlights.set(this._getHighlightsStorageKey(element), score); - element.foo = 1; - if (!topScore || score && topScore[0] < score[0]) { - topScore = score; - topElement = element; - } - } - } else { - // no pattern, clear highlights - this.highlights.clear(); - } - - this.refresh().then(() => { - if (topElement) { - this.reveal(topElement, 0.5).then(_ => { - this.setSelection([topElement], this); - this.setFocus(topElement, this); - }); - } else { - this.setSelection(defaultSelection, this); - } - }, onUnexpectedError); - } - - isHighlighterScoring(): boolean { - return this.highlights.size > 0; - } - - getHighlighterScore(element: any): FuzzyScore | undefined { - return this.highlights.get(this._getHighlightsStorageKey(element)); - } - - private _getHighlightsStorageKey(element: any): any { - return typeof this.highlighter.getHighlightsStorageKey === 'function' - ? this.highlighter.getHighlightsStorageKey(element) - : element; - } -} - function createKeyboardNavigationEventFilter(container: HTMLElement, keybindingService: IKeybindingService): IKeyboardNavigationEventFilter { let inChord = false; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 8e7971e1124..917a426bdbd 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -381,7 +381,7 @@ export class BreadcrumbsControl { } else { pickerArrowOffset = (data.left + (data.width * 0.3)) - x; } - picker.setInput(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset)); + picker.show(element, maxHeight, pickerWidth, pickerArrowSize, Math.max(0, pickerArrowOffset)); return { x, y }; }, onHide: (data) => { diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts index b027e26f87b..b22e048f38e 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsPicker.ts @@ -8,30 +8,33 @@ import * as dom from 'vs/base/browser/dom'; import { compareFileNames } from 'vs/base/common/comparers'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { createMatches, FuzzyScore, fuzzyScore } from 'vs/base/common/filters'; +import { createMatches, FuzzyScore } from 'vs/base/common/filters'; import * as glob from 'vs/base/common/glob'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { join } from 'vs/base/common/paths'; import { basename, dirname, isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; -import { IDataSource, IFilter, IRenderer, ISorter, ITree } from 'vs/base/parts/tree/browser/tree'; import 'vs/css!./media/breadcrumbscontrol'; import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; -import { OutlineDataSource, OutlineItemComparator, OutlineRenderer, OutlineItemCompareType } from 'vs/editor/contrib/documentSymbols/outlineTree'; -import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files'; import { IConstructorSignature1, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { HighlightingWorkbenchTree, IHighlighter, IHighlightingTreeConfiguration, IHighlightingTreeOptions } from 'vs/platform/list/browser/listService'; +import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; import { IFileIconTheme, IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree'; +import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineItem } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list'; export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker { - let ctor: IConstructorSignature1 = element instanceof FileElement ? BreadcrumbsFilePicker : BreadcrumbsOutlinePicker; + const ctor: IConstructorSignature1 = element instanceof FileElement + ? BreadcrumbsFilePicker + : BreadcrumbsOutlinePicker; + return instantiationService.createInstance(ctor, parent); } @@ -43,16 +46,18 @@ interface ILayoutInfo { inputHeight: number; } +type Tree = WorkbenchDataTree | WorkbenchAsyncDataTree; + export abstract class BreadcrumbsPicker { protected readonly _disposables = new Array(); protected readonly _domNode: HTMLDivElement; - protected readonly _arrow: HTMLDivElement; - protected readonly _treeContainer: HTMLDivElement; - protected readonly _tree: HighlightingWorkbenchTree; - protected readonly _focus: dom.IFocusTracker; - protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>; - private _layoutInfo: ILayoutInfo; + protected _arrow: HTMLDivElement; + protected _treeContainer: HTMLDivElement; + protected _tree: Tree; + protected _fakeEvent = new UIEvent('fakeEvent'); + protected _focus: dom.IFocusTracker; + protected _layoutInfo: ILayoutInfo; private readonly _onDidPickElement = new Emitter<{ target: any, payload: any }>(); readonly onDidPickElement: Event<{ target: any, payload: any }> = this._onDidPickElement.event; @@ -64,7 +69,7 @@ export abstract class BreadcrumbsPicker { parent: HTMLElement, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @IWorkbenchThemeService protected readonly _themeService: IWorkbenchThemeService, - @IConfigurationService private readonly _configurationService: IConfigurationService, + @IConfigurationService protected readonly _configurationService: IConfigurationService, ) { this._domNode = document.createElement('div'); this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons'; @@ -73,6 +78,16 @@ export abstract class BreadcrumbsPicker { this._focus = dom.trackFocus(this._domNode); this._focus.onDidBlur(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }), undefined, this._disposables); this._disposables.push(onDidChangeZoomLevel(_ => this._onDidPickElement.fire({ target: undefined, payload: undefined }))); + } + + dispose(): void { + dispose(this._disposables); + this._onDidPickElement.dispose(); + this._tree.dispose(); + this._focus.dispose(); + } + + show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void { const theme = this._themeService.getTheme(); const color = theme.getColor(breadcrumbsPickerBackground); @@ -88,100 +103,45 @@ export abstract class BreadcrumbsPicker { this._treeContainer.style.boxShadow = `0px 5px 8px ${this._themeService.getTheme().getColor(widgetShadow)}`; this._domNode.appendChild(this._treeContainer); - this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService); const filterConfig = BreadcrumbsConfig.FilterOnType.bindTo(this._configurationService); this._disposables.push(filterConfig); - const treeConfig = this._completeTreeConfiguration({ dataSource: undefined, renderer: undefined, highlighter: undefined }); - this._tree = this._instantiationService.createInstance( - HighlightingWorkbenchTree, - this._treeContainer, - treeConfig, - { useShadows: false, filterOnType: filterConfig.getValue(), showTwistie: false, twistiePixels: 12 }, - { placeholder: localize('placeholder', "Find") } - ); + this._tree = this._createTree(this._treeContainer); + this._disposables.push(this._tree.onDidChangeSelection(e => { - if (e.payload !== this._tree) { - const target = this._getTargetFromEvent(e.selection[0], e.payload); + if (e.browserEvent !== this._fakeEvent) { + const target = this._getTargetFromEvent(e.elements[0], e.browserEvent); if (target) { setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click - this._onDidPickElement.fire({ target, payload: e.payload }); + this._onDidPickElement.fire({ target, payload: undefined }); }, 0); } } })); this._disposables.push(this._tree.onDidChangeFocus(e => { - const target = this._getTargetFromEvent(e.focus, e.payload); + const target = this._getTargetFromEvent(e.elements[0], e.browserEvent); if (target) { - this._onDidFocusElement.fire({ target, payload: e.payload }); + this._onDidFocusElement.fire({ target, payload: undefined }); } })); - this._disposables.push(this._tree.onDidStartFiltering(() => { - this._layoutInfo.inputHeight = 36; + this._disposables.push(this._tree.onDidChangeContentHeight(() => { this._layout(); })); - this._disposables.push(this._tree.onDidExpandItem(() => { - this._layout(); - })); - this._disposables.push(this._tree.onDidCollapseItem(() => { - this._layout(); - })); - - // tree icon theme specials - dom.addClass(this._treeContainer, 'file-icon-themable-tree'); - dom.addClass(this._treeContainer, 'show-file-icons'); - const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => { - dom.toggleClass(this._treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons); - dom.toggleClass(this._treeContainer, 'hide-arrows', fileIconTheme.hidesExplorerArrows === true); - }; - this._disposables.push(_themeService.onDidFileIconThemeChange(onFileIconThemeChange)); - onFileIconThemeChange(_themeService.getFileIconTheme()); this._domNode.focus(); - } + this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 }; - dispose(): void { - dispose(this._disposables); - this._onDidPickElement.dispose(); - this._tree.dispose(); - this._focus.dispose(); - this._symbolSortOrder.dispose(); - } - - setInput(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void { - let actualInput = this._getInput(input); - this._tree.setInput(actualInput).then(() => { - - this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 }; + this._setInput(input).then(() => { this._layout(); - - // use proper selection, reveal - let selection = this._getInitialSelection(this._tree, input); - if (selection) { - return this._tree.reveal(selection, 0.5).then(() => { - this._tree.setSelection([selection], this._tree); - this._tree.setFocus(selection); - this._tree.domFocus(); - }); - } else { - this._tree.focusFirst(); - this._tree.setSelection([this._tree.getFocus()], this._tree); - this._tree.domFocus(); - return Promise.resolve(null); - } - }, onUnexpectedError); + }).catch(onUnexpectedError); } - private _layout(info: ILayoutInfo = this._layoutInfo): void { + protected _layout(info: ILayoutInfo = this._layoutInfo): void { - let count = 0; - let nav = this._tree.getNavigator(undefined, false); - while (nav.next() && count < 13) { count += 1; } - - let headerHeight = 2 * info.arrowSize; - let treeHeight = Math.min(info.maxHeight - headerHeight, count * 22); - let totalHeight = treeHeight + headerHeight; + const headerHeight = 2 * info.arrowSize; + const treeHeight = Math.min(info.maxHeight - headerHeight, this._tree.visibleNodeCount * 22); + const totalHeight = treeHeight + headerHeight; this._domNode.style.height = `${totalHeight}px`; this._domNode.style.width = `${info.width}px`; @@ -195,23 +155,24 @@ export abstract class BreadcrumbsPicker { } - protected abstract _getInput(input: BreadcrumbElement): any; - protected abstract _getInitialSelection(tree: ITree, input: BreadcrumbElement): any; - protected abstract _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration; - protected abstract _getTargetFromEvent(element: any, payload: any): any | undefined; + protected abstract _setInput(element: BreadcrumbElement): Promise; + protected abstract _createTree(container: HTMLElement): Tree; + protected abstract _getTargetFromEvent(element: any, payload: UIEvent): any | undefined; } //#region - Files -export class FileDataSource implements IDataSource { +class FileVirtualDelegate implements IListVirtualDelegate { + getHeight(_element: IFileStat | IWorkspaceFolder) { + return 22; + } + getTemplateId(_element: IFileStat | IWorkspaceFolder): string { + return 'FileStat'; + } +} - private readonly _parents = new WeakMap(); - - constructor( - @IFileService private readonly _fileService: IFileService, - ) { } - - getId(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): string { +class FileIdentityProvider implements IIdentityProvider { + getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string; } { if (URI.isUri(element)) { return element.toString(); } else if (IWorkspace.isIWorkspace(element)) { @@ -222,12 +183,26 @@ export class FileDataSource implements IDataSource { return element.resource.toString(); } } +} - hasChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): boolean { - return URI.isUri(element) || IWorkspace.isIWorkspace(element) || IWorkspaceFolder.isIWorkspaceFolder(element) || element.isDirectory; + +class FileDataSource implements IAsyncDataSource { + + private readonly _parents = new WeakMap(); + + constructor( + @IFileService private readonly _fileService: IFileService, + ) { } + + hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean { + return URI.isUri(element) + || IWorkspace.isIWorkspace(element) + || IWorkspaceFolder.isIWorkspaceFolder(element) + || element.isDirectory; } - getChildren(tree: ITree, element: IWorkspace | IWorkspaceFolder | IFileStat | URI): Promise { + getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> { + if (IWorkspace.isIWorkspace(element)) { return Promise.resolve(element.folders).then(folders => { for (let child of folders) { @@ -251,13 +226,56 @@ export class FileDataSource implements IDataSource { return stat.children; }); } +} - getParent(tree: ITree, element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise { - return Promise.resolve(this._parents.get(element)); +class FileRenderer implements ITreeRenderer { + + readonly templateId: string = 'FileStat'; + + constructor( + private readonly _labels: ResourceLabels, + @IConfigurationService private readonly _configService: IConfigurationService, + ) { } + + + renderTemplate(container: HTMLElement): IResourceLabel { + return this._labels.create(container, { supportHighlights: true }); + } + + renderElement(node: ITreeNode, index: number, templateData: IResourceLabel): void { + const fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); + const { element } = node; + let resource: URI; + let fileKind: FileKind; + if (IWorkspaceFolder.isIWorkspaceFolder(element)) { + resource = element.uri; + fileKind = FileKind.ROOT_FOLDER; + } else { + resource = element.resource; + fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE; + } + templateData.setFile(resource, { + fileKind, + hidePath: true, + fileDecorations: fileDecorations, + matches: createMatches(node.filterData), + extraClasses: ['picker-item'] + }); + } + + disposeTemplate(templateData: IResourceLabel): void { + templateData.dispose(); } } -export class FileFilter implements IFilter { +class FileNavigationLabelProvider implements IKeyboardNavigationLabelProvider { + + getKeyboardNavigationLabel(element: IWorkspaceFolder | IFileStat): { toString(): string; } { + return element.name; + } +} + +class FileFilter implements ITreeFilter { private readonly _cachedExpressions = new Map(); private readonly _disposables: IDisposable[] = []; @@ -301,7 +319,7 @@ export class FileFilter implements IFilter { dispose(this._disposables); } - isVisible(tree: ITree, element: IWorkspaceFolder | IFileStat): boolean { + filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean { if (IWorkspaceFolder.isIWorkspaceFolder(element)) { // not a file return true; @@ -317,72 +335,19 @@ export class FileFilter implements IFilter { } } -export class FileHighlighter implements IHighlighter { - getHighlightsStorageKey(element: IFileStat | IWorkspaceFolder): string { - return IWorkspaceFolder.isIWorkspaceFolder(element) ? element.uri.toString() : element.resource.toString(); - } - getHighlights(tree: ITree, element: IFileStat | IWorkspaceFolder, pattern: string): FuzzyScore { - return fuzzyScore(pattern, pattern.toLowerCase(), 0, element.name, element.name.toLowerCase(), 0, true); - } -} -export class FileRenderer implements IRenderer { - - constructor( - private readonly _labels: ResourceLabels, - @IConfigurationService private readonly _configService: IConfigurationService, - ) { } - - getHeight(tree: ITree, element: any): number { - return 22; - } - - getTemplateId(tree: ITree, element: any): string { - return 'FileStat'; - } - - renderTemplate(tree: ITree, templateId: string, container: HTMLElement) { - return this._labels.create(container, { supportHighlights: true }); - } - - renderElement(tree: ITree, element: IFileStat | IWorkspaceFolder, templateId: string, templateData: IResourceLabel): void { - let fileDecorations = this._configService.getValue<{ colors: boolean, badges: boolean }>('explorer.decorations'); - let resource: URI; - let fileKind: FileKind; - if (IWorkspaceFolder.isIWorkspaceFolder(element)) { - resource = element.uri; - fileKind = FileKind.ROOT_FOLDER; - } else { - resource = element.resource; - fileKind = element.isDirectory ? FileKind.FOLDER : FileKind.FILE; - } - templateData.setFile(resource, { - fileKind, - hidePath: true, - fileDecorations: fileDecorations, - matches: createMatches((tree as HighlightingWorkbenchTree).getHighlighterScore(element)), - extraClasses: ['picker-item'] - }); - } - - disposeTemplate(tree: ITree, templateId: string, templateData: IResourceLabel): void { - templateData.dispose(); - } -} - -export class FileSorter implements ISorter { - compare(tree: ITree, a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { +export class FileSorter implements ITreeSorter { + compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number { if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) { return a.index - b.index; + } + if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { + // same type -> compare on names + return compareFileNames(a.name, b.name); + } else if ((a as IFileStat).isDirectory) { + return -1; } else { - if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) { - // same type -> compare on names - return compareFileNames(a.name, b.name); - } else if ((a as IFileStat).isDirectory) { - return -1; - } else { - return 1; - } + return 1; } } } @@ -399,44 +364,69 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { super(parent, instantiationService, themeService, configService); } - protected _getInput(input: BreadcrumbElement): any { - let { uri, kind } = (input as FileElement); - if (kind === FileKind.ROOT_FOLDER) { - return this._workspaceService.getWorkspace(); - } else { - return dirname(uri); - } - } + _createTree(container: HTMLElement) { - protected _getInitialSelection(tree: ITree, input: BreadcrumbElement): any { - let { uri } = (input as FileElement); - let nav = tree.getNavigator(); - while (nav.next()) { - let cur = nav.current(); - let candidate = IWorkspaceFolder.isIWorkspaceFolder(cur) ? cur.uri : (cur as IFileStat).resource; - if (isEqual(uri, candidate)) { - return cur; - } - } - return undefined; - } + // tree icon theme specials + dom.addClass(this._treeContainer, 'file-icon-themable-tree'); + dom.addClass(this._treeContainer, 'show-file-icons'); + const onFileIconThemeChange = (fileIconTheme: IFileIconTheme) => { + dom.toggleClass(this._treeContainer, 'align-icons-and-twisties', fileIconTheme.hasFileIcons && !fileIconTheme.hasFolderIcons); + dom.toggleClass(this._treeContainer, 'hide-arrows', fileIconTheme.hidesExplorerArrows === true); + }; + this._disposables.push(this._themeService.onDidFileIconThemeChange(onFileIconThemeChange)); + onFileIconThemeChange(this._themeService.getFileIconTheme()); - protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { - // todo@joh reuse explorer implementations? - const filter = this._instantiationService.createInstance(FileFilter); - this._disposables.push(filter); - - config.dataSource = this._instantiationService.createInstance(FileDataSource); const labels = this._instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER /* TODO@Jo visibility propagation */); this._disposables.push(labels); - config.renderer = this._instantiationService.createInstance(FileRenderer, labels); - config.sorter = new FileSorter(); - config.highlighter = new FileHighlighter(); - config.filter = filter; - return config; + + return this._instantiationService.createInstance( + WorkbenchAsyncDataTree, + container, + new FileVirtualDelegate(), + [this._instantiationService.createInstance(FileRenderer, labels)], + this._instantiationService.createInstance(FileDataSource), + { + filterOnType: true, + multipleSelectionSupport: false, + sorter: new FileSorter(), + filter: this._instantiationService.createInstance(FileFilter), + identityProvider: new FileIdentityProvider(), + keyboardNavigationLabelProvider: new FileNavigationLabelProvider() + } + ) as WorkbenchAsyncDataTree; + } + + _setInput(element: BreadcrumbElement): Promise { + const { uri, kind } = (element as FileElement); + let input: IWorkspace | URI; + if (kind === FileKind.ROOT_FOLDER) { + input = this._workspaceService.getWorkspace(); + } else { + input = dirname(uri); + } + + const tree = this._tree as WorkbenchAsyncDataTree; + return tree.setInput(input).then(() => { + let focusElement: IWorkspaceFolder | IFileStat; + for (const { element } of tree.getNode().children) { + if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) { + focusElement = element; + break; + } else if (isEqual((element as IFileStat).resource, uri)) { + focusElement = element as IFileStat; + break; + } + } + if (focusElement) { + tree.reveal(focusElement, 0.5); + tree.setFocus([focusElement], this._fakeEvent); + } + tree.domFocus(); + }); } protected _getTargetFromEvent(element: any, _payload: any): any | undefined { + // todo@joh if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) { return new FileElement((element as IFileStat).resource, FileKind.FILE); } @@ -446,52 +436,77 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker { //#region - Symbols -class OutlineHighlighter implements IHighlighter { - getHighlights(tree: ITree, element: OutlineElement, pattern: string): FuzzyScore { - OutlineModel.get(element).updateMatches(pattern); - return element.score; - } -} - export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker { - protected _getInput(input: BreadcrumbElement): any { - let element = input as TreeElement; - let model = OutlineModel.get(element); - model.updateMatches(''); - return model; + protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>; + + constructor( + parent: HTMLElement, + @IInstantiationService instantiationService: IInstantiationService, + @IWorkbenchThemeService themeService: IWorkbenchThemeService, + @IConfigurationService configurationService: IConfigurationService, + ) { + super(parent, instantiationService, themeService, configurationService); + this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService); } - protected _getInitialSelection(_tree: ITree, input: BreadcrumbElement): any { - return input instanceof OutlineModel ? undefined : input; + protected _createTree(container: HTMLElement) { + return this._instantiationService.createInstance( + WorkbenchDataTree, + container, + new OutlineVirtualDelegate(), + [new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)], + new OutlineDataSource(), + { + filterOnType: true, + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + sorter: new OutlineItemComparator(this._getOutlineItemCompareType()), + identityProvider: new OutlineIdentityProvider(), + keyboardNavigationLabelProvider: this._instantiationService.createInstance(OutlineNavigationLabelProvider) + } + ) as WorkbenchDataTree; } - protected _completeTreeConfiguration(config: IHighlightingTreeConfiguration): IHighlightingTreeConfiguration { - config.dataSource = this._instantiationService.createInstance(OutlineDataSource); - config.renderer = this._instantiationService.createInstance(OutlineRenderer); - config.sorter = new OutlineItemComparator(this._getOutlineItemComparator()); - config.highlighter = new OutlineHighlighter(); - return config; + dispose(): void { + this._symbolSortOrder.dispose(); + super.dispose(); } - protected _getTargetFromEvent(element: any, payload: any): any | undefined { - if (payload && payload.didClickOnTwistie) { - return; + protected _setInput(input: BreadcrumbElement): Promise { + const element = input as TreeElement; + const model = OutlineModel.get(element); + const tree = this._tree as WorkbenchDataTree; + tree.setInput(model); + + let focusElement: TreeElement; + if (element === model) { + focusElement = tree.navigate().first(); + } else { + focusElement = element; } + tree.reveal(focusElement, 0.5); + tree.setFocus([focusElement], this._fakeEvent); + tree.domFocus(); + + return Promise.resolve(); + } + + protected _getTargetFromEvent(element: any): any | undefined { if (element instanceof OutlineElement) { return element; } } - private _getOutlineItemComparator(): OutlineItemCompareType { + private _getOutlineItemCompareType(): OutlineSortOrder { switch (this._symbolSortOrder.getValue()) { case 'name': - return OutlineItemCompareType.ByName; + return OutlineSortOrder.ByName; case 'type': - return OutlineItemCompareType.ByKind; + return OutlineSortOrder.ByKind; case 'position': default: - return OutlineItemCompareType.ByPosition; + return OutlineSortOrder.ByPosition; } } } diff --git a/src/vs/workbench/browser/viewlet.ts b/src/vs/workbench/browser/viewlet.ts index cc9ec9b3bfa..1da57592349 100644 --- a/src/vs/workbench/browser/viewlet.ts +++ b/src/vs/workbench/browser/viewlet.ts @@ -21,6 +21,7 @@ import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { AsyncDataTree } from 'vs/base/browser/ui/tree/asyncDataTree'; +import { AbstractTree } from 'vs/base/browser/ui/tree/abstractTree'; export abstract class Viewlet extends Composite implements IViewlet { @@ -195,7 +196,7 @@ export class CollapseAction extends Action { // Collapse All action for the new tree export class CollapseAction2 extends Action { - constructor(tree: AsyncDataTree, enabled: boolean, clazz: string) { + constructor(tree: AsyncDataTree | AbstractTree, enabled: boolean, clazz: string) { super('workbench.action.collapse', nls.localize('collapse', "Collapse All"), clazz, enabled, () => { tree.collapseAll(); return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/outline/electron-browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/electron-browser/outline.contribution.ts index 00467761391..a53f3eae4d7 100644 --- a/src/vs/workbench/contrib/outline/electron-browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/electron-browser/outline.contribution.ts @@ -9,7 +9,7 @@ import { OutlinePanel } from './outlinePanel'; import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; -import { OutlineConfigKeys, OutlineViewId } from 'vs/workbench/contrib/outline/electron-browser/outline'; +import { OutlineConfigKeys, OutlineViewId } from 'vs/editor/contrib/documentSymbols/outline'; const _outlineDesc = { id: OutlineViewId, diff --git a/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.css b/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.css index c8efd60317f..950fb41088b 100644 --- a/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.css +++ b/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.css @@ -12,6 +12,7 @@ width: 100%; height: 2px; padding-bottom: 3px; + position: absolute; } .monaco-workbench .outline-panel .outline-progress .monaco-progress-container { @@ -22,20 +23,6 @@ height: 2px; } -.monaco-workbench .outline-panel .outline-input { - box-sizing: border-box; - padding: 2px 9px 5px 9px; - position: relative; -} - -.monaco-workbench .outline-panel .outline-input .monaco-inputbox { - width: 100%; -} - -.monaco-workbench .outline-panel .outline-input .monaco-inputbox .input { - padding-right: 22px; -} - .monaco-workbench .outline-panel .outline-tree { height: 100%; } @@ -50,10 +37,6 @@ display: inherit; } -.monaco-workbench .outline-panel.message .outline-input { - display: none; -} - .monaco-workbench .outline-panel.message .outline-progress { display: none; } @@ -72,7 +55,3 @@ /* allows text color to use the default when selected */ color: inherit !important; } - -.monaco-workbench .outline-panel.no-icons .outline-element .outline-element-icon { - display: none; -} diff --git a/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.ts b/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.ts index f2fda75d216..70684ff8365 100644 --- a/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.ts +++ b/src/vs/workbench/contrib/outline/electron-browser/outlinePanel.ts @@ -5,26 +5,20 @@ import { posix } from 'path'; import * as dom from 'vs/base/browser/dom'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Action, IAction, RadioGroup } from 'vs/base/common/actions'; import { firstIndex } from 'vs/base/common/arrays'; import { createCancelablePromise, TimeoutTimer } from 'vs/base/common/async'; -import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; +import { isPromiseCanceledError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; import { defaultGenerator } from 'vs/base/common/idGenerator'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { ITree } from 'vs/base/parts/tree/browser/tree'; import 'vs/css!./outlinePanel'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; @@ -34,24 +28,25 @@ import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureR import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IResourceInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { WorkbenchTree } from 'vs/platform/list/browser/listService'; +import { WorkbenchDataTree } from 'vs/platform/list/browser/listService'; import { IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { attachInputBoxStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; +import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { CollapseAction } from 'vs/workbench/browser/viewlet'; -import { IViewsService } from 'vs/workbench/common/views'; +import { CollapseAction2 } from 'vs/workbench/browser/viewlet'; import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { OutlineController, OutlineDataSource, OutlineItemComparator, OutlineItemCompareType, OutlineItemFilter, OutlineRenderer, OutlineTreeState } from 'vs/editor/contrib/documentSymbols/outlineTree'; -import { OutlineConfigKeys, OutlineViewFiltered, OutlineViewFocused, OutlineViewId } from './outline'; +import { OutlineConfigKeys, OutlineViewFiltered, OutlineViewFocused } from '../../../../editor/contrib/documentSymbols/outline'; +import { FuzzyScore } from 'vs/base/common/filters'; +import { OutlineDataSource, OutlineItemComparator, OutlineSortOrder, OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItem, OutlineIdentityProvider, OutlineNavigationLabelProvider } from 'vs/editor/contrib/documentSymbols/outlineTree'; +import { IDataTreeViewState } from 'vs/base/browser/ui/tree/dataTree'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; class RequestState { @@ -164,7 +159,7 @@ class OutlineViewState { private _followCursor = false; private _filterOnType = true; - private _sortBy = OutlineItemCompareType.ByKind; + private _sortBy = OutlineSortOrder.ByKind; private _onDidChange = new Emitter<{ followCursor?: boolean, sortBy?: boolean, filterOnType?: boolean }>(); readonly onDidChange = this._onDidChange.event; @@ -191,14 +186,14 @@ class OutlineViewState { } } - set sortBy(value: OutlineItemCompareType) { + set sortBy(value: OutlineSortOrder) { if (value !== this._sortBy) { this._sortBy = value; this._onDidChange.fire({ sortBy: true }); } } - get sortBy(): OutlineItemCompareType { + get sortBy(): OutlineSortOrder { return this._sortBy; } @@ -233,14 +228,14 @@ export class OutlinePanel extends ViewletPanel { private _domNode: HTMLElement; private _message: HTMLDivElement; private _inputContainer: HTMLDivElement; - private _input: InputBox; private _progressBar: ProgressBar; - private _tree: WorkbenchTree; + private _tree: WorkbenchDataTree; private _treeDataSource: OutlineDataSource; - private _treeRenderer: OutlineRenderer; - private _treeFilter: OutlineItemFilter; + private _treeRenderer: OutlineElementRenderer; private _treeComparator: OutlineItemComparator; - private _treeStates = new LRUCache(10); + private _treeStates = new LRUCache(10); + + private _treeFakeUIEvent = new UIEvent('me'); private readonly _contextKeyFocused: IContextKey; private readonly _contextKeyFiltered: IContextKey; @@ -253,12 +248,12 @@ export class OutlinePanel extends ViewletPanel { @IEditorService private readonly _editorService: IEditorService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IKeybindingService keybindingService: IKeybindingService, @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IContextMenuService contextMenuService: IContextMenuService, ) { - super(options, _keybindingService, contextMenuService, configurationService); + super(options, keybindingService, contextMenuService, configurationService); this._outlineViewState.restore(this._storageService); this._contextKeyFocused = OutlineViewFocused.bindTo(contextKeyService); this._contextKeyFiltered = OutlineViewFiltered.bindTo(contextKeyService); @@ -303,73 +298,38 @@ export class OutlinePanel extends ViewletPanel { progressContainer, this._message, this._inputContainer, treeContainer ); - this._input = new InputBox(this._inputContainer, null, { - placeholder: this._outlineViewState.filterOnType ? localize('filter.placeholder', "Filter") : localize('find.placeholder', "Find") - }); - this._input.disable(); - - this.disposables.push(attachInputBoxStyler(this._input, this._themeService)); - this.disposables.push(dom.addStandardDisposableListener(this._input.inputElement, 'keyup', event => { - if (event.keyCode === KeyCode.DownArrow) { - this._tree.focusNext(); - this._tree.domFocus(); - } else if (event.keyCode === KeyCode.UpArrow) { - this._tree.focusPrevious(); - this._tree.domFocus(); - } else if (event.keyCode === KeyCode.Enter) { - let element = this._tree.getFocus(); - if (element instanceof OutlineElement) { - this._revealTreeSelection(OutlineModel.get(element), element, true, false); - } - } else if (event.keyCode === KeyCode.Escape) { - this._input.value = ''; - this._tree.domFocus(); - } - })); - - const $this = this; - const controller = new class extends OutlineController { - - constructor() { - super({}, $this.configurationService); - } - - onKeyDown(tree: ITree, event: IKeyboardEvent) { - let handled = super.onKeyDown(tree, event); - if (handled) { - return true; - } - if (this.upKeyBindingDispatcher.has(event.keyCode)) { - return false; - } - // crazy -> during keydown focus moves to the input box - // and because of that the keyup event is handled by the - // input field - if ($this._keybindingService.mightProducePrintableCharacter(event)) { - $this._input.focus(); - return true; - } - return false; - } - }; - - this._treeRenderer = this._instantiationService.createInstance(OutlineRenderer); + this._treeRenderer = this._instantiationService.createInstance(OutlineElementRenderer); this._treeDataSource = new OutlineDataSource(); this._treeComparator = new OutlineItemComparator(this._outlineViewState.sortBy); - this._treeFilter = new OutlineItemFilter(); - this._tree = this._instantiationService.createInstance(WorkbenchTree, treeContainer, { controller, renderer: this._treeRenderer, dataSource: this._treeDataSource, sorter: this._treeComparator, filter: this._treeFilter }, {}); + this._tree = this._instantiationService.createInstance( + WorkbenchDataTree, + treeContainer, + new OutlineVirtualDelegate(), + [new OutlineGroupRenderer(), this._treeRenderer], + this._treeDataSource, + { + expandOnlyOnTwistieClick: true, + multipleSelectionSupport: false, + filterOnType: this._outlineViewState.filterOnType, + sorter: this._treeComparator, + identityProvider: new OutlineIdentityProvider(), + keyboardNavigationLabelProvider: this._instantiationService.createInstance(OutlineNavigationLabelProvider) + } + ) as WorkbenchDataTree; - this._treeRenderer.renderProblemColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); - this._treeRenderer.renderProblemBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); - - this._disposables.push(this._tree, this._input); + this._disposables.push(this._tree); this._disposables.push(this._outlineViewState.onDidChange(this._onDidChangeUserState, this)); + // todo@joh workaournd for the tree resetting the filter behaviour + // to something globally defined + this._tree.updateOptions({ + filterOnType: this._outlineViewState.filterOnType + }); + // feature: toggle icons - dom.toggleClass(this._domNode, 'no-icons', !this._configurationService.getValue(OutlineConfigKeys.icons)); this.disposables.push(this._configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(OutlineConfigKeys.icons)) { - dom.toggleClass(this._domNode, 'no-icons', !this._configurationService.getValue(OutlineConfigKeys.icons)); + this._tree.updateChildren(); } })); @@ -386,25 +346,23 @@ export class OutlinePanel extends ViewletPanel { protected layoutBody(height: number): void { if (height !== this._cachedHeight) { - this._cachedHeight = height; - const treeHeight = height - (5 /*progressbar height*/ + 33 /*input height*/); - this._tree.layout(treeHeight); + this._tree.layout(height); } } getActions(): IAction[] { return [ new Action('collapse', localize('collapse', "Collapse All"), 'explorer-action collapse-explorer', true, () => { - return new CollapseAction(this._tree, true, undefined).run(); + return new CollapseAction2(this._tree, true, undefined).run(); }) ]; } getSecondaryActions(): IAction[] { let group = new RadioGroup([ - new SimpleToggleAction(localize('sortByPosition', "Sort By: Position"), this._outlineViewState.sortBy === OutlineItemCompareType.ByPosition, _ => this._outlineViewState.sortBy = OutlineItemCompareType.ByPosition), - new SimpleToggleAction(localize('sortByName', "Sort By: Name"), this._outlineViewState.sortBy === OutlineItemCompareType.ByName, _ => this._outlineViewState.sortBy = OutlineItemCompareType.ByName), - new SimpleToggleAction(localize('sortByKind', "Sort By: Type"), this._outlineViewState.sortBy === OutlineItemCompareType.ByKind, _ => this._outlineViewState.sortBy = OutlineItemCompareType.ByKind), + new SimpleToggleAction(localize('sortByPosition', "Sort By: Position"), this._outlineViewState.sortBy === OutlineSortOrder.ByPosition, _ => this._outlineViewState.sortBy = OutlineSortOrder.ByPosition), + new SimpleToggleAction(localize('sortByName', "Sort By: Name"), this._outlineViewState.sortBy === OutlineSortOrder.ByName, _ => this._outlineViewState.sortBy = OutlineSortOrder.ByName), + new SimpleToggleAction(localize('sortByKind', "Sort By: Type"), this._outlineViewState.sortBy === OutlineSortOrder.ByKind, _ => this._outlineViewState.sortBy = OutlineSortOrder.ByKind), ]); let result = [ new SimpleToggleAction(localize('followCur', "Follow Cursor"), this._outlineViewState.followCursor, action => this._outlineViewState.followCursor = action.checked), @@ -425,10 +383,13 @@ export class OutlinePanel extends ViewletPanel { } if (e.sortBy) { this._treeComparator.type = this._outlineViewState.sortBy; - this._tree.refresh(undefined, true); + // todo@joh resort + this._tree.updateChildren(); } if (e.filterOnType) { - this._applyTypeToFilter(); + this._tree.updateOptions({ + filterOnType: this._outlineViewState.filterOnType + }); } } @@ -455,9 +416,13 @@ export class OutlinePanel extends ViewletPanel { this._editorDisposables = new Array(); this._progressBar.infinite().show(150); - this._input.disable(); - if (!event) { - this._input.value = ''; + + let oldModel = this._tree.getInput(); + + // persist state + if (oldModel) { + let state = this._tree.getViewState(); + this._treeStates.set(oldModel.textModel.uri.toString(), state); } if (!editor || !DocumentSymbolProviderRegistry.has(editor.getModel())) { @@ -466,7 +431,6 @@ export class OutlinePanel extends ViewletPanel { let textModel = editor.getModel(); let loadingMessage: IDisposable; - let oldModel = this._tree.getInput(); if (!oldModel) { loadingMessage = new TimeoutTimer( () => this._showMessage(localize('loading', "Loading document symbols for '{0}'...", posix.basename(textModel.uri.path))), @@ -474,17 +438,17 @@ export class OutlinePanel extends ViewletPanel { ); } - let model = await OutlinePanel._createOutlineModel(textModel, this._editorDisposables); + let newModel = await OutlinePanel._createOutlineModel(textModel, this._editorDisposables); dispose(loadingMessage); - if (!model) { + if (!newModel) { return; } - if (TreeElement.empty(model)) { + if (TreeElement.empty(newModel)) { return this._showMessage(localize('no-symbols', "No symbols found in document '{0}'", posix.basename(textModel.uri.path))); } - let newSize = TreeElement.size(model); + let newSize = TreeElement.size(newModel); if (newSize > 7500) { // this is a workaround for performance issues with the tree: https://github.com/Microsoft/vscode/issues/18180 return this._showMessage(localize('too-many-symbols', "We are sorry, but this file is too large for showing an outline.")); @@ -523,22 +487,14 @@ export class OutlinePanel extends ViewletPanel { this._progressBar.stop().hide(); - if (oldModel && oldModel.merge(model)) { - this._tree.refresh(undefined, true); - model = oldModel; - + if (oldModel && oldModel.merge(newModel)) { + this._tree.updateChildren(); + newModel = oldModel; } else { - // persist state - if (oldModel) { - let state = OutlineTreeState.capture(this._tree); - this._treeStates.set(oldModel.textModel.uri.toString(), state); - } - await this._tree.setInput(model); - let state = this._treeStates.get(model.textModel.uri.toString()); - await OutlineTreeState.restore(this._tree, state, this); + let state = this._treeStates.get(newModel.textModel.uri.toString()); + await this._tree.setInput(newModel, state); } - this._input.enable(); this.layoutBody(this._cachedHeight); // transfer focus from domNode to the tree @@ -546,74 +502,44 @@ export class OutlinePanel extends ViewletPanel { this._tree.domFocus(); } - // feature: filter on type - // on type -> update filters - // on first type -> capture tree state - // on erase -> restore captured tree state - let beforePatternState: OutlineTreeState; - let onInputValueChanged = async pattern => { - - this._contextKeyFiltered.set(pattern.length > 0); - - if (pattern && !beforePatternState) { - beforePatternState = OutlineTreeState.capture(this._tree); - } - let item = model.updateMatches(pattern); - await this._tree.refresh(undefined, true); - if (item) { - await this._tree.expandAll(undefined /*all*/); - await this._tree.reveal(item); - this._tree.setFocus(item, this); - this._tree.setSelection([item], this); - } - - if (!pattern && beforePatternState) { - await OutlineTreeState.restore(this._tree, beforePatternState, this); - beforePatternState = undefined; - } - }; - if (this._input.value) { - onInputValueChanged(this._input.value); - } - this._editorDisposables.push(this._input.onDidChange(onInputValueChanged)); - this._editorDisposables.push(toDisposable(() => this._contextKeyFiltered.reset())); // feature: reveal outline selection in editor // on change -> reveal/select defining range this._editorDisposables.push(this._tree.onDidChangeSelection(e => { - if (e.payload === this || e.payload && e.payload.didClickOnTwistie) { + if (e.browserEvent === this._treeFakeUIEvent /* || e.payload && e.payload.didClickOnTwistie */) { return; } - let [first] = e.selection; + let [first] = e.elements; if (!(first instanceof OutlineElement)) { return; } let focus = false; let aside = false; - if (e.payload) { - if (e.payload.origin === 'keyboard') { + // todo@Joh + if (e.browserEvent) { + if (e.browserEvent.type === 'keydown') { focus = true; - - } else if (e.payload.origin === 'mouse' && e.payload.originalEvent instanceof StandardMouseEvent) { - let event = e.payload.originalEvent; - focus = event.detail === 2; - aside = !this._tree.useAltAsMultipleSelectionModifier && event.altKey || this._tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey); + } else if (e.browserEvent.type === 'click') { + const event = new StandardMouseEvent(e.browserEvent as MouseEvent); + focus = e.browserEvent.detail === 2; + aside = (!this._tree.useAltAsMultipleSelectionModifier && event.altKey) + || (this._tree.useAltAsMultipleSelectionModifier && (event.ctrlKey || event.metaKey)); } } - this._revealTreeSelection(model, first, focus, aside); + this._revealTreeSelection(newModel, first, focus, aside); })); // feature: reveal editor selection in outline - this._revealEditorSelection(model, editor.getSelection()); - const versionIdThen = model.textModel.getVersionId(); + this._revealEditorSelection(newModel, editor.getSelection()); + const versionIdThen = newModel.textModel.getVersionId(); this._editorDisposables.push(editor.onDidChangeCursorSelection(e => { // first check if the document has changed and stop revealing the // cursor position iff it has -> we will update/recompute the // outline view then anyways - if (!model.textModel.isDisposed() && model.textModel.getVersionId() === versionIdThen) { - this._revealEditorSelection(model, e.selection); + if (!newModel.textModel.isDisposed() && newModel.textModel.getVersionId() === versionIdThen) { + this._revealEditorSelection(newModel, e.selection); } })); @@ -627,8 +553,8 @@ export class OutlinePanel extends ViewletPanel { } const marker = this._markerService.read({ resource: textModel.uri, severities: MarkerSeverity.Error | MarkerSeverity.Warning }); if (marker.length > 0 || !ignoreEmpty) { - model.updateMarker(marker); - this._tree.refresh(undefined, true); + newModel.updateMarker(marker); + this._tree.updateChildren(); } }; updateMarker([textModel.uri], true); @@ -636,39 +562,21 @@ export class OutlinePanel extends ViewletPanel { this._editorDisposables.push(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(OutlineConfigKeys.problemsBadges) || e.affectsConfiguration(OutlineConfigKeys.problemsColors)) { - this._treeRenderer.renderProblemColors = this._configurationService.getValue(OutlineConfigKeys.problemsColors); - this._treeRenderer.renderProblemBadges = this._configurationService.getValue(OutlineConfigKeys.problemsBadges); - this._tree.refresh(undefined, true); + this._tree.updateChildren(); return; } if (!e.affectsConfiguration(OutlineConfigKeys.problemsEnabled)) { return; } if (!this._configurationService.getValue(OutlineConfigKeys.problemsEnabled)) { - model.updateMarker([]); - this._tree.refresh(undefined, true); + newModel.updateMarker([]); + this._tree.updateChildren(); } else { updateMarker([textModel.uri], true); } })); } - private _applyTypeToFilter(): void { - // depending on the user setting we filter or find elements - if (this._outlineViewState.filterOnType) { - this._treeFilter.enabled = true; - this._treeDataSource.filterOnScore = true; - this._input.setPlaceHolder(localize('filter', "Filter")); - } else { - this._treeFilter.enabled = false; - this._treeDataSource.filterOnScore = false; - this._input.setPlaceHolder(localize('find', "Find")); - } - if (this._tree.getInput()) { - this._tree.refresh(undefined, true); - } - } - private async _revealTreeSelection(model: OutlineModel, element: OutlineElement, focus: boolean, aside: boolean): Promise { await this._editorService.openEditor({ @@ -699,50 +607,7 @@ export class OutlinePanel extends ViewletPanel { // only when outside view port await this._tree.reveal(item, 0.5); } - this._tree.setFocus(item, this); - this._tree.setSelection([item], this); - } - - focusHighlightedElement(up: boolean): void { - if (!this._tree.getInput()) { - return; - } - if (!this._tree.isDOMFocused()) { - this._tree.domFocus(); - return; - } - let navi = this._tree.getNavigator(this._tree.getFocus(), false); - let candidate: any; - while (candidate = up ? navi.previous() : navi.next()) { - if (candidate instanceof OutlineElement && candidate.score && candidate.score[1] > 0) { - this._tree.setFocus(candidate, this); - this._tree.reveal(candidate).then(undefined, onUnexpectedError); - break; - } - } + this._tree.setFocus([item], this._treeFakeUIEvent); + this._tree.setSelection([item], this._treeFakeUIEvent); } } - -async function goUpOrDownToHighligthedElement(accessor: ServicesAccessor, prev: boolean) { - const viewsService = accessor.get(IViewsService); - const view = await viewsService.openView(OutlineViewId); - if (view instanceof OutlinePanel) { - view.focusHighlightedElement(prev); - } -} - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'outline.focusDownHighlighted', - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.DownArrow, - when: ContextKeyExpr.and(OutlineViewFiltered, OutlineViewFocused), - handler: accessor => goUpOrDownToHighligthedElement(accessor, false) -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'outline.focusUpHighlighted', - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.UpArrow, - when: ContextKeyExpr.and(OutlineViewFiltered, OutlineViewFocused), - handler: accessor => goUpOrDownToHighligthedElement(accessor, true) -});