From 701c8482efae0c6e98f6ef14e57f24343bc35869 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Wed, 28 Nov 2018 15:25:56 +0100 Subject: [PATCH] adopt DataTree in references widget, #63904 --- .../contrib/referenceSearch/referencesTree.ts | 210 +++++++++ .../referenceSearch/referencesWidget.ts | 407 ++++-------------- 2 files changed, 283 insertions(+), 334 deletions(-) create mode 100644 src/vs/editor/contrib/referenceSearch/referencesTree.ts diff --git a/src/vs/editor/contrib/referenceSearch/referencesTree.ts b/src/vs/editor/contrib/referenceSearch/referencesTree.ts new file mode 100644 index 00000000000..7200367bc5b --- /dev/null +++ b/src/vs/editor/contrib/referenceSearch/referencesTree.ts @@ -0,0 +1,210 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import { ReferencesModel, FileReferences, OneReference } from './referencesModel'; +import { IDataSource } from 'vs/base/browser/ui/tree/dataTree'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree'; +import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; +import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; +import * as dom from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { dirname } from 'vs/base/common/resources'; +import { escape } from 'vs/base/common/strings'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; + +//#region data source + +export type TreeElement = FileReferences | OneReference; + +export class DataSource implements IDataSource { + + root: ReferencesModel | FileReferences; + + constructor( + @ITextModelService private readonly _resolverService: ITextModelService, + ) { + // + } + + hasChildren(element: TreeElement): boolean { + if (!element) { + return true; + } + if (element instanceof FileReferences && !element.failure) { + return true; + } + return false; + } + + getChildren(element: TreeElement): Thenable { + if (!element && this.root instanceof FileReferences) { + element = this.root; + } + if (element instanceof FileReferences) { + return element.resolve(this._resolverService).then(val => { + // if (element.failure) { + // // refresh the element on failure so that + // // we can update its rendering + // return tree.refresh(element).then(() => val.children); + // } + return val.children; + }); + } + if (this.root instanceof ReferencesModel) { + return Promise.resolve(this.root.groups); + } + throw new Error('bad tree'); + } +} + +//#endregion + +export class Delegate implements IListVirtualDelegate { + getHeight(): number { + return 23; + } + getTemplateId(element: FileReferences | OneReference): string { + if (element instanceof FileReferences) { + return FileReferencesRenderer.id; + } else { + return OneReferenceRenderer.id; + } + } +} + +//#region render: File + +class FileReferencesTemplate extends Disposable { + + readonly file: IconLabel; + readonly badge: CountBadge; + + constructor( + container: HTMLElement, + @ILabelService private readonly _uriLabel: ILabelService, + @IThemeService themeService: IThemeService, + ) { + super(); + const parent = document.createElement('div'); + dom.addClass(parent, 'reference-file'); + this.file = this._register(new IconLabel(parent)); + + this.badge = new CountBadge(dom.append(parent, dom.$('.count'))); + this._register(attachBadgeStyler(this.badge, themeService)); + + container.appendChild(parent); + } + + set(element: FileReferences) { + let parent = dirname(element.uri); + this.file.setValue(getBaseLabel(element.uri), parent ? this._uriLabel.getUriLabel(parent, { relative: true }) : undefined, { title: this._uriLabel.getUriLabel(element.uri) }); + const len = element.children.length; + this.badge.setCount(len); + if (element.failure) { + this.badge.setTitleFormat(localize('referencesFailre', "Failed to resolve file.")); + } else if (len > 1) { + this.badge.setTitleFormat(localize('referencesCount', "{0} references", len)); + } else { + this.badge.setTitleFormat(localize('referenceCount', "{0} reference", len)); + } + } +} + +export class FileReferencesRenderer implements ITreeRenderer { + + static readonly id = 'FileReferencesRenderer'; + + readonly templateId: string = FileReferencesRenderer.id; + + constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { } + + renderTemplate(container: HTMLElement): FileReferencesTemplate { + return this._instantiationService.createInstance(FileReferencesTemplate, container); + } + renderElement(node: ITreeNode, index: number, template: FileReferencesTemplate): void { + template.set(node.element); + } + disposeElement(element: ITreeNode, index: number, templateData: FileReferencesTemplate): void { + // + } + disposeTemplate(templateData: FileReferencesTemplate): void { + templateData.dispose(); + } +} + +//#endregion + +//#region render: Reference +class OneReferenceTemplate { + + readonly before: HTMLSpanElement; + readonly inside: HTMLSpanElement; + readonly after: HTMLSpanElement; + + constructor(container: HTMLElement) { + const parent = document.createElement('div'); + this.before = document.createElement('span'); + this.inside = document.createElement('span'); + this.after = document.createElement('span'); + dom.addClass(this.inside, 'referenceMatch'); + dom.addClass(parent, 'reference'); + parent.appendChild(this.before); + parent.appendChild(this.inside); + parent.appendChild(this.after); + container.appendChild(parent); + } + + set(element: OneReference): void { + const { before, inside, after } = element.parent.preview.preview(element.range); + this.before.innerHTML = escape(before); + this.inside.innerHTML = escape(inside); + this.after.innerHTML = escape(after); + } +} + +export class OneReferenceRenderer implements ITreeRenderer { + + static readonly id = 'OneReferenceRenderer'; + + readonly templateId: string = OneReferenceRenderer.id; + + renderTemplate(container: HTMLElement): OneReferenceTemplate { + return new OneReferenceTemplate(container); + } + renderElement(element: ITreeNode, index: number, templateData: OneReferenceTemplate): void { + templateData.set(element.element); + } + disposeElement(): void { + // + } + disposeTemplate(): void { + // + } +} + +//#endregion + + +export class AriaProvider implements IAccessibilityProvider { + + getAriaLabel(element: FileReferences | OneReference): string | null { + if (element instanceof FileReferences) { + return element.getAriaMessage(); + } else if (element instanceof OneReference) { + return element.getAriaMessage(); + } else { + return null; + } + } +} diff --git a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts index f3a6fab92fe..cb95293d778 100644 --- a/src/vs/editor/contrib/referenceSearch/referencesWidget.ts +++ b/src/vs/editor/contrib/referenceSearch/referencesWidget.ts @@ -4,22 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { GestureEvent } from 'vs/base/browser/touch'; -import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ISashEvent, IVerticalSashLayoutProvider, Sash } from 'vs/base/browser/ui/sash/sash'; import { Color } from 'vs/base/common/color'; -import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { getBaseLabel } from 'vs/base/common/labels'; import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; -import * as strings from 'vs/base/common/strings'; -import * as tree from 'vs/base/parts/tree/browser/tree'; -import { ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults'; import 'vs/css!./media/referencesWidget'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; @@ -30,17 +21,17 @@ import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/ import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel'; import { Location } from 'vs/editor/common/modes'; import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { AriaProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement } from 'vs/editor/contrib/referenceSearch/referencesTree'; import * as nls from 'vs/nls'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { WorkbenchTree, WorkbenchTreeController } from 'vs/platform/list/browser/listService'; +import { WorkbenchDataTree } from 'vs/platform/list/browser/listService'; import { activeContrastBorder, contrastBorder, registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PeekViewWidget } from './peekViewWidget'; import { FileReferences, OneReference, ReferencesModel } from './referencesModel'; - +import { timeout } from 'vs/base/common/async'; class DecorationsManager implements IDisposable { @@ -153,275 +144,6 @@ class DecorationsManager implements IDisposable { } } -class DataSource implements tree.IDataSource { - - constructor( - @ITextModelService private readonly _textModelResolverService: ITextModelService - ) { - // - } - - public getId(tree: tree.ITree, element: any): string { - if (element instanceof ReferencesModel) { - return 'root'; - } else if (element instanceof FileReferences) { - return (element).id; - } else if (element instanceof OneReference) { - return (element).id; - } - return undefined; - } - - public hasChildren(tree: tree.ITree, element: any): boolean { - if (element instanceof ReferencesModel) { - return true; - } - if (element instanceof FileReferences && !(element).failure) { - return true; - } - return false; - } - - public getChildren(tree: tree.ITree, element: ReferencesModel | FileReferences): Promise { - if (element instanceof ReferencesModel) { - return Promise.resolve(element.groups); - } else if (element instanceof FileReferences) { - return element.resolve(this._textModelResolverService).then(val => { - if (element.failure) { - // refresh the element on failure so that - // we can update its rendering - return tree.refresh(element).then(() => val.children); - } - return val.children; - }); - } else { - return Promise.resolve([]); - } - } - - public getParent(tree: tree.ITree, element: any): Promise { - let result: any = null; - if (element instanceof FileReferences) { - result = (element).parent; - } else if (element instanceof OneReference) { - result = (element).parent; - } - return Promise.resolve(result); - } -} - -class Controller extends WorkbenchTreeController { - - private _onDidFocus = new Emitter(); - readonly onDidFocus: Event = this._onDidFocus.event; - - private _onDidSelect = new Emitter(); - readonly onDidSelect: Event = this._onDidSelect.event; - - private _onDidOpenToSide = new Emitter(); - readonly onDidOpenToSide: Event = this._onDidOpenToSide.event; - - public onTap(tree: tree.ITree, element: any, event: GestureEvent): boolean { - if (element instanceof FileReferences) { - event.preventDefault(); - event.stopPropagation(); - return this._expandCollapse(tree, element); - } - - let result = super.onTap(tree, element, event); - - this._onDidFocus.fire(element); - return result; - } - - public onMouseDown(tree: tree.ITree, element: any, event: IMouseEvent): boolean { - let isDoubleClick = event.detail === 2; - if (event.leftButton) { - if (element instanceof FileReferences) { - if (this.openOnSingleClick || isDoubleClick || this.isClickOnTwistie(event)) { - event.preventDefault(); - event.stopPropagation(); - return this._expandCollapse(tree, element); - } - } - - let result = super.onClick(tree, element, event); - let openToSide = event.ctrlKey || event.metaKey || event.altKey; - if (openToSide && (isDoubleClick || this.openOnSingleClick)) { - this._onDidOpenToSide.fire(element); - } else if (isDoubleClick) { - this._onDidSelect.fire(element); - } else if (this.openOnSingleClick) { - this._onDidFocus.fire(element); - } - return result; - } - - return false; - } - - public onClick(tree: tree.ITree, element: any, event: IMouseEvent): boolean { - if (event.leftButton) { - return false; // Already handled by onMouseDown - } - - return super.onClick(tree, element, event); - } - - private _expandCollapse(tree: tree.ITree, element: any): boolean { - - if (tree.isExpanded(element)) { - tree.collapse(element).then(null, onUnexpectedError); - } else { - tree.expand(element).then(null, onUnexpectedError); - } - return true; - } - - public onEscape(tree: tree.ITree, event: IKeyboardEvent): boolean { - return false; - } - - dispose(): void { - this._onDidFocus.dispose(); - this._onDidSelect.dispose(); - this._onDidOpenToSide.dispose(); - } -} - -class FileReferencesTemplate { - - readonly file: IconLabel; - readonly badge: CountBadge; - readonly dispose: () => void; - - constructor( - container: HTMLElement, - @ILabelService private readonly _uriLabel: ILabelService, - @IThemeService themeService: IThemeService, - ) { - const parent = document.createElement('div'); - dom.addClass(parent, 'reference-file'); - container.appendChild(parent); - this.file = new IconLabel(parent); - - this.badge = new CountBadge(dom.append(parent, dom.$('.count'))); - const styler = attachBadgeStyler(this.badge, themeService); - - this.dispose = () => { - this.file.dispose(); - styler.dispose(); - }; - } - - set(element: FileReferences) { - let parent = dirname(element.uri); - this.file.setValue(getBaseLabel(element.uri), parent ? this._uriLabel.getUriLabel(parent, { relative: true }) : undefined, { title: this._uriLabel.getUriLabel(element.uri) }); - const len = element.children.length; - this.badge.setCount(len); - if (element.failure) { - this.badge.setTitleFormat(nls.localize('referencesFailre', "Failed to resolve file.")); - } else if (len > 1) { - this.badge.setTitleFormat(nls.localize('referencesCount', "{0} references", len)); - } else { - this.badge.setTitleFormat(nls.localize('referenceCount', "{0} reference", len)); - } - } -} - -class OneReferenceTemplate { - - readonly before: HTMLSpanElement; - readonly inside: HTMLSpanElement; - readonly after: HTMLSpanElement; - - constructor(container: HTMLElement) { - const parent = document.createElement('div'); - this.before = document.createElement('span'); - this.inside = document.createElement('span'); - this.after = document.createElement('span'); - dom.addClass(this.inside, 'referenceMatch'); - dom.addClass(parent, 'reference'); - parent.appendChild(this.before); - parent.appendChild(this.inside); - parent.appendChild(this.after); - container.appendChild(parent); - } - - set(element: OneReference): void { - const { before, inside, after } = element.parent.preview.preview(element.range); - this.before.innerHTML = strings.escape(before); - this.inside.innerHTML = strings.escape(inside); - this.after.innerHTML = strings.escape(after); - } -} - -class Renderer implements tree.IRenderer { - - private static readonly _ids = { - FileReferences: 'FileReferences', - OneReference: 'OneReference' - }; - - constructor( - @IThemeService private readonly _themeService: IThemeService, - @ILabelService private readonly _uriLabel: ILabelService, - ) { - // - } - - getHeight(tree: tree.ITree, element: FileReferences | OneReference): number { - return 23; - } - - getTemplateId(tree: tree.ITree, element: FileReferences | OneReference): string { - if (element instanceof FileReferences) { - return Renderer._ids.FileReferences; - } else if (element instanceof OneReference) { - return Renderer._ids.OneReference; - } - throw element; - } - - renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement) { - if (templateId === Renderer._ids.FileReferences) { - return new FileReferencesTemplate(container, this._uriLabel, this._themeService); - } else if (templateId === Renderer._ids.OneReference) { - return new OneReferenceTemplate(container); - } - throw templateId; - } - - renderElement(tree: tree.ITree, element: FileReferences | OneReference, templateId: string, templateData: any): void { - if (element instanceof FileReferences) { - (templateData).set(element); - } else if (element instanceof OneReference) { - (templateData).set(element); - } else { - throw templateId; - } - } - - disposeTemplate(tree: tree.ITree, templateId: string, templateData: FileReferencesTemplate | OneReferenceTemplate): void { - if (templateData instanceof FileReferencesTemplate) { - templateData.dispose(); - } - } -} - -class AriaProvider implements tree.IAccessibilityProvider { - - getAriaLabel(tree: tree.ITree, element: FileReferences | OneReference): string { - if (element instanceof FileReferences) { - return element.getAriaMessage(); - } else if (element instanceof OneReference) { - return element.getAriaMessage(); - } else { - return undefined; - } - } -} - class VSash { private _disposables: IDisposable[] = []; @@ -513,7 +235,8 @@ export class ReferenceWidget extends PeekViewWidget { private _callOnDispose: IDisposable[] = []; private _onDidSelectReference = new Emitter(); - private _tree: WorkbenchTree; + private _treeDataSource: DataSource; + private _tree: WorkbenchDataTree; private _treeContainer: HTMLElement; private _sash: VSash; private _preview: ICodeEditor; @@ -527,9 +250,9 @@ export class ReferenceWidget extends PeekViewWidget { private _defaultTreeKeyboardSupport: boolean, public layoutData: LayoutData, @IThemeService themeService: IThemeService, - @ITextModelService private _textModelResolverService: ITextModelService, - @IInstantiationService private _instantiationService: IInstantiationService, - @ILabelService private _uriLabel: ILabelService + @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILabelService private readonly _uriLabel: ILabelService ) { super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }); @@ -620,22 +343,26 @@ export class ReferenceWidget extends PeekViewWidget { // tree this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline')); - let controller = this._instantiationService.createInstance(Controller, { keyboardSupport: this._defaultTreeKeyboardSupport, clickBehavior: ClickBehavior.ON_MOUSE_UP /* our controller already deals with this */ }); - this._callOnDispose.push(controller); - let config = { - dataSource: this._instantiationService.createInstance(DataSource), - renderer: this._instantiationService.createInstance(Renderer), - controller, + const renderer = [ + this._instantiationService.createInstance(FileReferencesRenderer), + this._instantiationService.createInstance(OneReferenceRenderer), + ]; + + const treeOptions = { + ariaLabel: nls.localize('treeAriaLabel', "References"), + keyboardSupport: this._defaultTreeKeyboardSupport, accessibilityProvider: new AriaProvider() }; - let treeOptions: tree.ITreeOptions = { - twistiePixels: 20, - ariaLabel: nls.localize('treeAriaLabel', "References") - }; + this._treeDataSource = this._instantiationService.createInstance(DataSource); - this._tree = this._instantiationService.createInstance(WorkbenchTree, this._treeContainer, config, treeOptions); + this._tree = this._instantiationService.createInstance( + WorkbenchDataTree, this._treeContainer, new Delegate(), + renderer as any, + this._treeDataSource, + treeOptions + ) as any as WorkbenchDataTree; ctxReferenceWidgetSearchTreeFocused.bindTo(this._tree.contextKeyService); @@ -648,19 +375,29 @@ export class ReferenceWidget extends PeekViewWidget { this._onDidSelectReference.fire({ element, kind, source: 'tree' }); } }; - this._disposables.push(this._tree.onDidChangeFocus(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - onEvent(event.focus, 'show'); // only handle events from keyboard, mouse/touch is handled by other listeners below + this._tree.onDidChangeFocus(e => { + onEvent(e.elements[0], 'show'); + }); + this._tree.onDidChangeSelection(e => { + let aside = false; + let goto = false; + if (e.browserEvent instanceof KeyboardEvent) { + // todo@joh make this a command + goto = true; + + } else if (e.browserEvent instanceof MouseEvent) { + aside = e.browserEvent.metaKey || e.browserEvent.metaKey || e.browserEvent.altKey; + goto = e.browserEvent.detail === 2; } - })); - this._disposables.push(this._tree.onDidChangeSelection(event => { - if (event && event.payload && event.payload.origin === 'keyboard') { - onEvent(event.selection[0], 'goto'); // only handle events from keyboard, mouse/touch is handled by other listeners below + if (aside) { + onEvent(e.elements[0], 'side'); + } else if (goto) { + onEvent(e.elements[0], 'goto'); + } else { + onEvent(e.elements[0], 'show'); } - })); - this._disposables.push(controller.onDidFocus(element => onEvent(element, 'show'))); - this._disposables.push(controller.onDidSelect(element => onEvent(element, 'goto'))); - this._disposables.push(controller.onDidOpenToSide(element => onEvent(element, 'side'))); + }); + dom.hide(this._treeContainer); } @@ -701,7 +438,7 @@ export class ReferenceWidget extends PeekViewWidget { } // show in tree this._tree.setSelection([selection]); - this._tree.setFocus(selection); + this._tree.setFocus([selection]); }); } @@ -752,8 +489,8 @@ export class ReferenceWidget extends PeekViewWidget { this.focus(); // pick input and a reference to begin with - const input = this._model.groups.length === 1 ? this._model.groups[0] : this._model; - return this._tree.setInput(input); + this._treeDataSource.root = this._model.groups.length === 1 ? this._model.groups[0] : this._model; + return this._tree.refresh(null); } private _getFocusedReference(): OneReference { @@ -780,34 +517,36 @@ export class ReferenceWidget extends PeekViewWidget { const promise = this._textModelResolverService.createModelReference(reference.uri); if (revealParent) { - await this._tree.reveal(reference.parent); + this._tree.reveal(reference.parent); } - return Promise.all([promise, this._tree.reveal(reference)]).then(values => { - const ref = values[0]; + await this._tree.expand(reference.parent); + await timeout(75); //todo@joao expand resolves to soon + this._tree.reveal(reference); - if (!this._model) { - ref.dispose(); - // disposed - return; - } + const ref = await promise; - dispose(this._previewModelReference); - - // show in editor - const model = ref.object; - if (model) { - this._previewModelReference = ref; - let isSameModel = (this._preview.getModel() === model.textEditorModel); - this._preview.setModel(model.textEditorModel); - let sel = Range.lift(reference.range).collapseToStart(); - this._preview.setSelection(sel); - this._preview.revealRangeInCenter(sel, isSameModel ? editorCommon.ScrollType.Smooth : editorCommon.ScrollType.Immediate); - } else { - this._preview.setModel(this._previewNotAvailableMessage); - ref.dispose(); - } - }, onUnexpectedError); + if (!this._model) { + // disposed + ref.dispose(); + return; + } + + dispose(this._previewModelReference); + + // show in editor + const model = ref.object; + if (model) { + const scrollType = this._preview.getModel() === model.textEditorModel ? editorCommon.ScrollType.Smooth : editorCommon.ScrollType.Immediate; + const sel = Range.lift(reference.range).collapseToStart(); + this._previewModelReference = ref; + this._preview.setModel(model.textEditorModel); + this._preview.setSelection(sel); + this._preview.revealRangeInCenter(sel, scrollType); + } else { + this._preview.setModel(this._previewNotAvailableMessage); + ref.dispose(); + } } } -- GitLab