"Fossies" - the Fresh Open Source Software Archive

Member "vscode-1.49.1/src/vs/base/browser/ui/tree/abstractTree.ts" (16 Sep 2020, 56675 Bytes) of package /linux/misc/vscode-1.49.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. See also the last Fossies "Diffs" side-by-side code changes report for "abstractTree.ts": 1.48.2_vs_1.49.0.

    1 /*---------------------------------------------------------------------------------------------
    2  *  Copyright (c) Microsoft Corporation. All rights reserved.
    3  *  Licensed under the MIT License. See License.txt in the project root for license information.
    4  *--------------------------------------------------------------------------------------------*/
    5 
    6 import 'vs/css!./media/tree';
    7 import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
    8 import { IListOptions, List, IListStyles, MouseController, DefaultKeyboardNavigationDelegate, isInputElement, isMonacoEditor } from 'vs/base/browser/ui/list/listWidget';
    9 import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider, IIdentityProvider, IKeyboardNavigationDelegate } from 'vs/base/browser/ui/list/list';
   10 import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass, hasClass, hasParentWithClass, createStyleSheet, clearNode, addClasses, removeClasses } from 'vs/base/browser/dom';
   11 import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event';
   12 import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
   13 import { KeyCode } from 'vs/base/common/keyCodes';
   14 import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent, TreeMouseEventTarget } from 'vs/base/browser/ui/tree/tree';
   15 import { ISpliceable } from 'vs/base/common/sequence';
   16 import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd';
   17 import { range, equals, distinctES6 } from 'vs/base/common/arrays';
   18 import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
   19 import { domEvent } from 'vs/base/browser/event';
   20 import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
   21 import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel';
   22 import { localize } from 'vs/nls';
   23 import { disposableTimeout } from 'vs/base/common/async';
   24 import { isMacintosh } from 'vs/base/common/platform';
   25 import { clamp } from 'vs/base/common/numbers';
   26 import { ScrollEvent } from 'vs/base/common/scrollable';
   27 import { SetMap } from 'vs/base/common/collections';
   28 import { treeItemExpandedIcon, treeFilterOnTypeOnIcon, treeFilterOnTypeOffIcon, treeFilterClearIcon } from 'vs/base/browser/ui/tree/treeIcons';
   29 
   30 class TreeElementsDragAndDropData<T, TFilterData, TContext> extends ElementsDragAndDropData<T, TContext> {
   31 
   32     set context(context: TContext | undefined) {
   33         this.data.context = context;
   34     }
   35 
   36     get context(): TContext | undefined {
   37         return this.data.context;
   38     }
   39 
   40     constructor(private data: ElementsDragAndDropData<ITreeNode<T, TFilterData>, TContext>) {
   41         super(data.elements.map(node => node.element));
   42     }
   43 }
   44 
   45 function asTreeDragAndDropData<T, TFilterData>(data: IDragAndDropData): IDragAndDropData {
   46     if (data instanceof ElementsDragAndDropData) {
   47         return new TreeElementsDragAndDropData(data);
   48     }
   49 
   50     return data;
   51 }
   52 
   53 class TreeNodeListDragAndDrop<T, TFilterData, TRef> implements IListDragAndDrop<ITreeNode<T, TFilterData>> {
   54 
   55     private autoExpandNode: ITreeNode<T, TFilterData> | undefined;
   56     private autoExpandDisposable: IDisposable = Disposable.None;
   57 
   58     constructor(private modelProvider: () => ITreeModel<T, TFilterData, TRef>, private dnd: ITreeDragAndDrop<T>) { }
   59 
   60     getDragURI(node: ITreeNode<T, TFilterData>): string | null {
   61         return this.dnd.getDragURI(node.element);
   62     }
   63 
   64     getDragLabel(nodes: ITreeNode<T, TFilterData>[], originalEvent: DragEvent): string | undefined {
   65         if (this.dnd.getDragLabel) {
   66             return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent);
   67         }
   68 
   69         return undefined;
   70     }
   71 
   72     onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
   73         if (this.dnd.onDragStart) {
   74             this.dnd.onDragStart(asTreeDragAndDropData(data), originalEvent);
   75         }
   76     }
   77 
   78     onDragOver(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent, raw = true): boolean | IListDragOverReaction {
   79         const result = this.dnd.onDragOver(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
   80         const didChangeAutoExpandNode = this.autoExpandNode !== targetNode;
   81 
   82         if (didChangeAutoExpandNode) {
   83             this.autoExpandDisposable.dispose();
   84             this.autoExpandNode = targetNode;
   85         }
   86 
   87         if (typeof targetNode === 'undefined') {
   88             return result;
   89         }
   90 
   91         if (didChangeAutoExpandNode && typeof result !== 'boolean' && result.autoExpand) {
   92             this.autoExpandDisposable = disposableTimeout(() => {
   93                 const model = this.modelProvider();
   94                 const ref = model.getNodeLocation(targetNode);
   95 
   96                 if (model.isCollapsed(ref)) {
   97                     model.setCollapsed(ref, false);
   98                 }
   99 
  100                 this.autoExpandNode = undefined;
  101             }, 500);
  102         }
  103 
  104         if (typeof result === 'boolean' || !result.accept || typeof result.bubble === 'undefined' || result.feedback) {
  105             if (!raw) {
  106                 const accept = typeof result === 'boolean' ? result : result.accept;
  107                 const effect = typeof result === 'boolean' ? undefined : result.effect;
  108                 return { accept, effect, feedback: [targetIndex!] };
  109             }
  110 
  111             return result;
  112         }
  113 
  114         if (result.bubble === TreeDragOverBubble.Up) {
  115             const model = this.modelProvider();
  116             const ref = model.getNodeLocation(targetNode);
  117             const parentRef = model.getParentNodeLocation(ref);
  118             const parentNode = model.getNode(parentRef);
  119             const parentIndex = parentRef && model.getListIndex(parentRef);
  120 
  121             return this.onDragOver(data, parentNode, parentIndex, originalEvent, false);
  122         }
  123 
  124         const model = this.modelProvider();
  125         const ref = model.getNodeLocation(targetNode);
  126         const start = model.getListIndex(ref);
  127         const length = model.getListRenderCount(ref);
  128 
  129         return { ...result, feedback: range(start, start + length) };
  130     }
  131 
  132     drop(data: IDragAndDropData, targetNode: ITreeNode<T, TFilterData> | undefined, targetIndex: number | undefined, originalEvent: DragEvent): void {
  133         this.autoExpandDisposable.dispose();
  134         this.autoExpandNode = undefined;
  135 
  136         this.dnd.drop(asTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, originalEvent);
  137     }
  138 
  139     onDragEnd(originalEvent: DragEvent): void {
  140         if (this.dnd.onDragEnd) {
  141             this.dnd.onDragEnd(originalEvent);
  142         }
  143     }
  144 }
  145 
  146 function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T, TFilterData, TRef>, options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
  147     return options && {
  148         ...options,
  149         identityProvider: options.identityProvider && {
  150             getId(el) {
  151                 return options.identityProvider!.getId(el.element);
  152             }
  153         },
  154         dnd: options.dnd && new TreeNodeListDragAndDrop(modelProvider, options.dnd),
  155         multipleSelectionController: options.multipleSelectionController && {
  156             isSelectionSingleChangeEvent(e) {
  157                 return options.multipleSelectionController!.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
  158             },
  159             isSelectionRangeChangeEvent(e) {
  160                 return options.multipleSelectionController!.isSelectionRangeChangeEvent({ ...e, element: e.element } as any);
  161             }
  162         },
  163         accessibilityProvider: options.accessibilityProvider && {
  164             ...options.accessibilityProvider,
  165             getSetSize(node) {
  166                 const model = modelProvider();
  167                 const ref = model.getNodeLocation(node);
  168                 const parentRef = model.getParentNodeLocation(ref);
  169                 const parentNode = model.getNode(parentRef);
  170 
  171                 return parentNode.visibleChildrenCount;
  172             },
  173             getPosInSet(node) {
  174                 return node.visibleChildIndex + 1;
  175             },
  176             isChecked: options.accessibilityProvider && options.accessibilityProvider.isChecked ? (node) => {
  177                 return options.accessibilityProvider!.isChecked!(node.element);
  178             } : undefined,
  179             getRole: options.accessibilityProvider && options.accessibilityProvider.getRole ? (node) => {
  180                 return options.accessibilityProvider!.getRole!(node.element);
  181             } : () => 'treeitem',
  182             getAriaLabel(e) {
  183                 return options.accessibilityProvider!.getAriaLabel(e.element);
  184             },
  185             getWidgetAriaLabel() {
  186                 return options.accessibilityProvider!.getWidgetAriaLabel();
  187             },
  188             getWidgetRole: options.accessibilityProvider && options.accessibilityProvider.getWidgetRole ? () => options.accessibilityProvider!.getWidgetRole!() : () => 'tree',
  189             getAriaLevel(node) {
  190                 return node.depth;
  191             },
  192             getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => {
  193                 return options.accessibilityProvider!.getActiveDescendantId!(node.element);
  194             })
  195         },
  196         keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && {
  197             ...options.keyboardNavigationLabelProvider,
  198             getKeyboardNavigationLabel(node) {
  199                 return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(node.element);
  200             }
  201         },
  202         enableKeyboardNavigation: options.simpleKeyboardNavigation
  203     };
  204 }
  205 
  206 export class ComposedTreeDelegate<T, N extends { element: T }> implements IListVirtualDelegate<N> {
  207 
  208     constructor(private delegate: IListVirtualDelegate<T>) { }
  209 
  210     getHeight(element: N): number {
  211         return this.delegate.getHeight(element.element);
  212     }
  213 
  214     getTemplateId(element: N): string {
  215         return this.delegate.getTemplateId(element.element);
  216     }
  217 
  218     hasDynamicHeight(element: N): boolean {
  219         return !!this.delegate.hasDynamicHeight && this.delegate.hasDynamicHeight(element.element);
  220     }
  221 
  222     setDynamicHeight(element: N, height: number): void {
  223         if (this.delegate.setDynamicHeight) {
  224             this.delegate.setDynamicHeight(element.element, height);
  225         }
  226     }
  227 }
  228 
  229 interface ITreeListTemplateData<T> {
  230     readonly container: HTMLElement;
  231     readonly indent: HTMLElement;
  232     readonly twistie: HTMLElement;
  233     indentGuidesDisposable: IDisposable;
  234     readonly templateData: T;
  235 }
  236 
  237 export enum RenderIndentGuides {
  238     None = 'none',
  239     OnHover = 'onHover',
  240     Always = 'always'
  241 }
  242 
  243 interface ITreeRendererOptions {
  244     readonly indent?: number;
  245     readonly renderIndentGuides?: RenderIndentGuides;
  246     // TODO@joao replace this with collapsible: boolean | 'ondemand'
  247     readonly hideTwistiesOfChildlessElements?: boolean;
  248 }
  249 
  250 interface IRenderData<TTemplateData> {
  251     templateData: ITreeListTemplateData<TTemplateData>;
  252     height: number;
  253 }
  254 
  255 interface Collection<T> {
  256     readonly elements: T[];
  257     readonly onDidChange: Event<T[]>;
  258 }
  259 
  260 class EventCollection<T> implements Collection<T> {
  261 
  262     readonly onDidChange: Event<T[]>;
  263 
  264     get elements(): T[] {
  265         return this._elements;
  266     }
  267 
  268     constructor(onDidChange: Event<T[]>, private _elements: T[] = []) {
  269         this.onDidChange = Event.forEach(onDidChange, elements => this._elements = elements);
  270     }
  271 }
  272 
  273 class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
  274 
  275     private static readonly DefaultIndent = 8;
  276 
  277     readonly templateId: string;
  278     private renderedElements = new Map<T, ITreeNode<T, TFilterData>>();
  279     private renderedNodes = new Map<ITreeNode<T, TFilterData>, IRenderData<TTemplateData>>();
  280     private indent: number = TreeRenderer.DefaultIndent;
  281     private hideTwistiesOfChildlessElements: boolean = false;
  282 
  283     private shouldRenderIndentGuides: boolean = false;
  284     private renderedIndentGuides = new SetMap<ITreeNode<T, TFilterData>, HTMLDivElement>();
  285     private activeIndentNodes = new Set<ITreeNode<T, TFilterData>>();
  286     private indentGuidesDisposable: IDisposable = Disposable.None;
  287 
  288     private readonly disposables = new DisposableStore();
  289 
  290     constructor(
  291         private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
  292         private modelProvider: () => ITreeModel<T, TFilterData, TRef>,
  293         onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>,
  294         private activeNodes: Collection<ITreeNode<T, TFilterData>>,
  295         options: ITreeRendererOptions = {}
  296     ) {
  297         this.templateId = renderer.templateId;
  298         this.updateOptions(options);
  299 
  300         Event.map(onDidChangeCollapseState, e => e.node)(this.onDidChangeNodeTwistieState, this, this.disposables);
  301 
  302         if (renderer.onDidChangeTwistieState) {
  303             renderer.onDidChangeTwistieState(this.onDidChangeTwistieState, this, this.disposables);
  304         }
  305     }
  306 
  307     updateOptions(options: ITreeRendererOptions = {}): void {
  308         if (typeof options.indent !== 'undefined') {
  309             this.indent = clamp(options.indent, 0, 40);
  310         }
  311 
  312         if (typeof options.renderIndentGuides !== 'undefined') {
  313             const shouldRenderIndentGuides = options.renderIndentGuides !== RenderIndentGuides.None;
  314 
  315             if (shouldRenderIndentGuides !== this.shouldRenderIndentGuides) {
  316                 this.shouldRenderIndentGuides = shouldRenderIndentGuides;
  317                 this.indentGuidesDisposable.dispose();
  318 
  319                 if (shouldRenderIndentGuides) {
  320                     const disposables = new DisposableStore();
  321                     this.activeNodes.onDidChange(this._onDidChangeActiveNodes, this, disposables);
  322                     this.indentGuidesDisposable = disposables;
  323 
  324                     this._onDidChangeActiveNodes(this.activeNodes.elements);
  325                 }
  326             }
  327         }
  328 
  329         if (typeof options.hideTwistiesOfChildlessElements !== 'undefined') {
  330             this.hideTwistiesOfChildlessElements = options.hideTwistiesOfChildlessElements;
  331         }
  332     }
  333 
  334     renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
  335         const el = append(container, $('.monaco-tl-row'));
  336         const indent = append(el, $('.monaco-tl-indent'));
  337         const twistie = append(el, $('.monaco-tl-twistie'));
  338         const contents = append(el, $('.monaco-tl-contents'));
  339         const templateData = this.renderer.renderTemplate(contents);
  340 
  341         return { container, indent, twistie, indentGuidesDisposable: Disposable.None, templateData };
  342     }
  343 
  344     renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, height: number | undefined): void {
  345         if (typeof height === 'number') {
  346             this.renderedNodes.set(node, { templateData, height });
  347             this.renderedElements.set(node.element, node);
  348         }
  349 
  350         const indent = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent;
  351         templateData.twistie.style.paddingLeft = `${indent}px`;
  352         templateData.indent.style.width = `${indent + this.indent - 16}px`;
  353 
  354         this.renderTwistie(node, templateData);
  355 
  356         if (typeof height === 'number') {
  357             this.renderIndentGuides(node, templateData);
  358         }
  359 
  360         this.renderer.renderElement(node, index, templateData.templateData, height);
  361     }
  362 
  363     disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, height: number | undefined): void {
  364         templateData.indentGuidesDisposable.dispose();
  365 
  366         if (this.renderer.disposeElement) {
  367             this.renderer.disposeElement(node, index, templateData.templateData, height);
  368         }
  369 
  370         if (typeof height === 'number') {
  371             this.renderedNodes.delete(node);
  372             this.renderedElements.delete(node.element);
  373         }
  374     }
  375 
  376     disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
  377         this.renderer.disposeTemplate(templateData.templateData);
  378     }
  379 
  380     private onDidChangeTwistieState(element: T): void {
  381         const node = this.renderedElements.get(element);
  382 
  383         if (!node) {
  384             return;
  385         }
  386 
  387         this.onDidChangeNodeTwistieState(node);
  388     }
  389 
  390     private onDidChangeNodeTwistieState(node: ITreeNode<T, TFilterData>): void {
  391         const data = this.renderedNodes.get(node);
  392 
  393         if (!data) {
  394             return;
  395         }
  396 
  397         this.renderTwistie(node, data.templateData);
  398         this._onDidChangeActiveNodes(this.activeNodes.elements);
  399         this.renderIndentGuides(node, data.templateData);
  400     }
  401 
  402     private renderTwistie(node: ITreeNode<T, TFilterData>, templateData: ITreeListTemplateData<TTemplateData>) {
  403         if (this.renderer.renderTwistie) {
  404             this.renderer.renderTwistie(node.element, templateData.twistie);
  405         }
  406 
  407         if (node.collapsible && (!this.hideTwistiesOfChildlessElements || node.visibleChildrenCount > 0)) {
  408             addClasses(templateData.twistie, treeItemExpandedIcon.classNames, 'collapsible');
  409             toggleClass(templateData.twistie, 'collapsed', node.collapsed);
  410         } else {
  411             removeClasses(templateData.twistie, treeItemExpandedIcon.classNames, 'collapsible', 'collapsed');
  412         }
  413 
  414         if (node.collapsible) {
  415             templateData.container.setAttribute('aria-expanded', String(!node.collapsed));
  416         } else {
  417             templateData.container.removeAttribute('aria-expanded');
  418         }
  419     }
  420 
  421     private renderIndentGuides(target: ITreeNode<T, TFilterData>, templateData: ITreeListTemplateData<TTemplateData>): void {
  422         clearNode(templateData.indent);
  423         templateData.indentGuidesDisposable.dispose();
  424 
  425         if (!this.shouldRenderIndentGuides) {
  426             return;
  427         }
  428 
  429         const disposableStore = new DisposableStore();
  430         const model = this.modelProvider();
  431 
  432         let node = target;
  433 
  434         while (true) {
  435             const ref = model.getNodeLocation(node);
  436             const parentRef = model.getParentNodeLocation(ref);
  437 
  438             if (!parentRef) {
  439                 break;
  440             }
  441 
  442             const parent = model.getNode(parentRef);
  443             const guide = $<HTMLDivElement>('.indent-guide', { style: `width: ${this.indent}px` });
  444 
  445             if (this.activeIndentNodes.has(parent)) {
  446                 addClass(guide, 'active');
  447             }
  448 
  449             if (templateData.indent.childElementCount === 0) {
  450                 templateData.indent.appendChild(guide);
  451             } else {
  452                 templateData.indent.insertBefore(guide, templateData.indent.firstElementChild);
  453             }
  454 
  455             this.renderedIndentGuides.add(parent, guide);
  456             disposableStore.add(toDisposable(() => this.renderedIndentGuides.delete(parent, guide)));
  457 
  458             node = parent;
  459         }
  460 
  461         templateData.indentGuidesDisposable = disposableStore;
  462     }
  463 
  464     private _onDidChangeActiveNodes(nodes: ITreeNode<T, TFilterData>[]): void {
  465         if (!this.shouldRenderIndentGuides) {
  466             return;
  467         }
  468 
  469         const set = new Set<ITreeNode<T, TFilterData>>();
  470         const model = this.modelProvider();
  471 
  472         nodes.forEach(node => {
  473             const ref = model.getNodeLocation(node);
  474             try {
  475                 const parentRef = model.getParentNodeLocation(ref);
  476 
  477                 if (node.collapsible && node.children.length > 0 && !node.collapsed) {
  478                     set.add(node);
  479                 } else if (parentRef) {
  480                     set.add(model.getNode(parentRef));
  481                 }
  482             } catch {
  483                 // noop
  484             }
  485         });
  486 
  487         this.activeIndentNodes.forEach(node => {
  488             if (!set.has(node)) {
  489                 this.renderedIndentGuides.forEach(node, line => removeClass(line, 'active'));
  490             }
  491         });
  492 
  493         set.forEach(node => {
  494             if (!this.activeIndentNodes.has(node)) {
  495                 this.renderedIndentGuides.forEach(node, line => addClass(line, 'active'));
  496             }
  497         });
  498 
  499         this.activeIndentNodes = set;
  500     }
  501 
  502     dispose(): void {
  503         this.renderedNodes.clear();
  504         this.renderedElements.clear();
  505         this.indentGuidesDisposable.dispose();
  506         dispose(this.disposables);
  507     }
  508 }
  509 
  510 class TypeFilter<T> implements ITreeFilter<T, FuzzyScore>, IDisposable {
  511 
  512     private _totalCount = 0;
  513     get totalCount(): number { return this._totalCount; }
  514     private _matchCount = 0;
  515     get matchCount(): number { return this._matchCount; }
  516 
  517     private _pattern: string = '';
  518     private _lowercasePattern: string = '';
  519     private readonly disposables = new DisposableStore();
  520 
  521     set pattern(pattern: string) {
  522         this._pattern = pattern;
  523         this._lowercasePattern = pattern.toLowerCase();
  524     }
  525 
  526     constructor(
  527         private tree: AbstractTree<T, any, any>,
  528         private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>,
  529         private _filter?: ITreeFilter<T, FuzzyScore>
  530     ) {
  531         tree.onWillRefilter(this.reset, this, this.disposables);
  532     }
  533 
  534     filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
  535         if (this._filter) {
  536             const result = this._filter.filter(element, parentVisibility);
  537 
  538             if (this.tree.options.simpleKeyboardNavigation) {
  539                 return result;
  540             }
  541 
  542             let visibility: TreeVisibility;
  543 
  544             if (typeof result === 'boolean') {
  545                 visibility = result ? TreeVisibility.Visible : TreeVisibility.Hidden;
  546             } else if (isFilterResult(result)) {
  547                 visibility = getVisibleState(result.visibility);
  548             } else {
  549                 visibility = result;
  550             }
  551 
  552             if (visibility === TreeVisibility.Hidden) {
  553                 return false;
  554             }
  555         }
  556 
  557         this._totalCount++;
  558 
  559         if (this.tree.options.simpleKeyboardNavigation || !this._pattern) {
  560             this._matchCount++;
  561             return { data: FuzzyScore.Default, visibility: true };
  562         }
  563 
  564         const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(element);
  565         const labelStr = label && label.toString();
  566 
  567         if (typeof labelStr === 'undefined') {
  568             return { data: FuzzyScore.Default, visibility: true };
  569         }
  570 
  571         const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true);
  572 
  573         if (!score) {
  574             if (this.tree.options.filterOnType) {
  575                 return TreeVisibility.Recurse;
  576             } else {
  577                 return { data: FuzzyScore.Default, visibility: true };
  578             }
  579 
  580             // DEMO: smarter filter ?
  581             // return parentVisibility === TreeVisibility.Visible ? true : TreeVisibility.Recurse;
  582         }
  583 
  584         this._matchCount++;
  585         return { data: score, visibility: true };
  586     }
  587 
  588     private reset(): void {
  589         this._totalCount = 0;
  590         this._matchCount = 0;
  591     }
  592 
  593     dispose(): void {
  594         dispose(this.disposables);
  595     }
  596 }
  597 
  598 class TypeFilterController<T, TFilterData> implements IDisposable {
  599 
  600     private _enabled = false;
  601     get enabled(): boolean { return this._enabled; }
  602 
  603     private _pattern = '';
  604     get pattern(): string { return this._pattern; }
  605 
  606     private _filterOnType: boolean;
  607     get filterOnType(): boolean { return this._filterOnType; }
  608 
  609     private _empty: boolean = false;
  610     get empty(): boolean { return this._empty; }
  611 
  612     private readonly _onDidChangeEmptyState = new Emitter<boolean>();
  613     readonly onDidChangeEmptyState: Event<boolean> = Event.latch(this._onDidChangeEmptyState.event);
  614 
  615     private positionClassName = 'ne';
  616     private domNode: HTMLElement;
  617     private messageDomNode: HTMLElement;
  618     private labelDomNode: HTMLElement;
  619     private filterOnTypeDomNode: HTMLInputElement;
  620     private clearDomNode: HTMLElement;
  621     private keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
  622 
  623     private automaticKeyboardNavigation = true;
  624     private triggered = false;
  625 
  626     private readonly _onDidChangePattern = new Emitter<string>();
  627     readonly onDidChangePattern = this._onDidChangePattern.event;
  628 
  629     private readonly enabledDisposables = new DisposableStore();
  630     private readonly disposables = new DisposableStore();
  631 
  632     constructor(
  633         private tree: AbstractTree<T, TFilterData, any>,
  634         model: ITreeModel<T, TFilterData, any>,
  635         private view: List<ITreeNode<T, TFilterData>>,
  636         private filter: TypeFilter<T>,
  637         private keyboardNavigationDelegate: IKeyboardNavigationDelegate
  638     ) {
  639         this.domNode = $(`.monaco-list-type-filter.${this.positionClassName}`);
  640         this.domNode.draggable = true;
  641         domEvent(this.domNode, 'dragstart')(this.onDragStart, this, this.disposables);
  642 
  643         this.messageDomNode = append(view.getHTMLElement(), $(`.monaco-list-type-filter-message`));
  644 
  645         this.labelDomNode = append(this.domNode, $('span.label'));
  646         const controls = append(this.domNode, $('.controls'));
  647 
  648         this._filterOnType = !!tree.options.filterOnType;
  649         this.filterOnTypeDomNode = append(controls, $<HTMLInputElement>('input.filter'));
  650         this.filterOnTypeDomNode.type = 'checkbox';
  651         this.filterOnTypeDomNode.checked = this._filterOnType;
  652         this.filterOnTypeDomNode.tabIndex = -1;
  653         this.updateFilterOnTypeTitleAndIcon();
  654         domEvent(this.filterOnTypeDomNode, 'input')(this.onDidChangeFilterOnType, this, this.disposables);
  655 
  656         this.clearDomNode = append(controls, $<HTMLInputElement>('button.clear' + treeFilterClearIcon.cssSelector));
  657         this.clearDomNode.tabIndex = -1;
  658         this.clearDomNode.title = localize('clear', "Clear");
  659 
  660         this.keyboardNavigationEventFilter = tree.options.keyboardNavigationEventFilter;
  661 
  662         model.onDidSplice(this.onDidSpliceModel, this, this.disposables);
  663         this.updateOptions(tree.options);
  664     }
  665 
  666     updateOptions(options: IAbstractTreeOptions<T, TFilterData>): void {
  667         if (options.simpleKeyboardNavigation) {
  668             this.disable();
  669         } else {
  670             this.enable();
  671         }
  672 
  673         if (typeof options.filterOnType !== 'undefined') {
  674             this._filterOnType = !!options.filterOnType;
  675             this.filterOnTypeDomNode.checked = this._filterOnType;
  676         }
  677 
  678         if (typeof options.automaticKeyboardNavigation !== 'undefined') {
  679             this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
  680         }
  681 
  682         this.tree.refilter();
  683         this.render();
  684 
  685         if (!this.automaticKeyboardNavigation) {
  686             this.onEventOrInput('');
  687         }
  688     }
  689 
  690     toggle(): void {
  691         this.triggered = !this.triggered;
  692 
  693         if (!this.triggered) {
  694             this.onEventOrInput('');
  695         }
  696     }
  697 
  698     private enable(): void {
  699         if (this._enabled) {
  700             return;
  701         }
  702 
  703         const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown'))
  704             .filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode)
  705             .filter(e => e.key !== 'Dead' && !/^Media/.test(e.key))
  706             .map(e => new StandardKeyboardEvent(e))
  707             .filter(this.keyboardNavigationEventFilter || (() => true))
  708             .filter(() => this.automaticKeyboardNavigation || this.triggered)
  709             .filter(e => (this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) && !(e.keyCode === KeyCode.DownArrow || e.keyCode === KeyCode.UpArrow || e.keyCode === KeyCode.LeftArrow || e.keyCode === KeyCode.RightArrow)) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey)))
  710             .forEach(e => { e.stopPropagation(); e.preventDefault(); })
  711             .event;
  712 
  713         const onClear = domEvent(this.clearDomNode, 'click');
  714 
  715         Event.chain(Event.any<MouseEvent | StandardKeyboardEvent>(onKeyDown, onClear))
  716             .event(this.onEventOrInput, this, this.enabledDisposables);
  717 
  718         this.filter.pattern = '';
  719         this.tree.refilter();
  720         this.render();
  721         this._enabled = true;
  722         this.triggered = false;
  723     }
  724 
  725     private disable(): void {
  726         if (!this._enabled) {
  727             return;
  728         }
  729 
  730         this.domNode.remove();
  731         this.enabledDisposables.clear();
  732         this.tree.refilter();
  733         this.render();
  734         this._enabled = false;
  735         this.triggered = false;
  736     }
  737 
  738     private onEventOrInput(e: MouseEvent | StandardKeyboardEvent | string): void {
  739         if (typeof e === 'string') {
  740             this.onInput(e);
  741         } else if (e instanceof MouseEvent || e.keyCode === KeyCode.Escape || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))) {
  742             this.onInput('');
  743         } else if (e.keyCode === KeyCode.Backspace) {
  744             this.onInput(this.pattern.length === 0 ? '' : this.pattern.substr(0, this.pattern.length - 1));
  745         } else {
  746             this.onInput(this.pattern + e.browserEvent.key);
  747         }
  748     }
  749 
  750     private onInput(pattern: string): void {
  751         const container = this.view.getHTMLElement();
  752 
  753         if (pattern && !this.domNode.parentElement) {
  754             container.append(this.domNode);
  755         } else if (!pattern && this.domNode.parentElement) {
  756             this.domNode.remove();
  757             this.tree.domFocus();
  758         }
  759 
  760         this._pattern = pattern;
  761         this._onDidChangePattern.fire(pattern);
  762 
  763         this.filter.pattern = pattern;
  764         this.tree.refilter();
  765 
  766         if (pattern) {
  767             this.tree.focusNext(0, true, undefined, node => !FuzzyScore.isDefault(node.filterData as any as FuzzyScore));
  768         }
  769 
  770         const focus = this.tree.getFocus();
  771 
  772         if (focus.length > 0) {
  773             const element = focus[0];
  774 
  775             if (this.tree.getRelativeTop(element) === null) {
  776                 this.tree.reveal(element, 0.5);
  777             }
  778         }
  779 
  780         this.render();
  781 
  782         if (!pattern) {
  783             this.triggered = false;
  784         }
  785     }
  786 
  787     private onDragStart(): void {
  788         const container = this.view.getHTMLElement();
  789         const { left } = getDomNodePagePosition(container);
  790         const containerWidth = container.clientWidth;
  791         const midContainerWidth = containerWidth / 2;
  792         const width = this.domNode.clientWidth;
  793         const disposables = new DisposableStore();
  794         let positionClassName = this.positionClassName;
  795 
  796         const updatePosition = () => {
  797             switch (positionClassName) {
  798                 case 'nw':
  799                     this.domNode.style.top = `4px`;
  800                     this.domNode.style.left = `4px`;
  801                     break;
  802                 case 'ne':
  803                     this.domNode.style.top = `4px`;
  804                     this.domNode.style.left = `${containerWidth - width - 6}px`;
  805                     break;
  806             }
  807         };
  808 
  809         const onDragOver = (event: DragEvent) => {
  810             event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
  811 
  812             const x = event.screenX - left;
  813             if (event.dataTransfer) {
  814                 event.dataTransfer.dropEffect = 'none';
  815             }
  816 
  817             if (x < midContainerWidth) {
  818                 positionClassName = 'nw';
  819             } else {
  820                 positionClassName = 'ne';
  821             }
  822 
  823             updatePosition();
  824         };
  825 
  826         const onDragEnd = () => {
  827             this.positionClassName = positionClassName;
  828             this.domNode.className = `monaco-list-type-filter ${this.positionClassName}`;
  829             this.domNode.style.top = '';
  830             this.domNode.style.left = '';
  831 
  832             dispose(disposables);
  833         };
  834 
  835         updatePosition();
  836         removeClass(this.domNode, positionClassName);
  837 
  838         addClass(this.domNode, 'dragging');
  839         disposables.add(toDisposable(() => removeClass(this.domNode, 'dragging')));
  840 
  841         domEvent(document, 'dragover')(onDragOver, null, disposables);
  842         domEvent(this.domNode, 'dragend')(onDragEnd, null, disposables);
  843 
  844         StaticDND.CurrentDragAndDropData = new DragAndDropData('vscode-ui');
  845         disposables.add(toDisposable(() => StaticDND.CurrentDragAndDropData = undefined));
  846     }
  847 
  848     private onDidSpliceModel(): void {
  849         if (!this._enabled || this.pattern.length === 0) {
  850             return;
  851         }
  852 
  853         this.tree.refilter();
  854         this.render();
  855     }
  856 
  857     private onDidChangeFilterOnType(): void {
  858         this.tree.updateOptions({ filterOnType: this.filterOnTypeDomNode.checked });
  859         this.tree.refilter();
  860         this.tree.domFocus();
  861         this.render();
  862         this.updateFilterOnTypeTitleAndIcon();
  863     }
  864 
  865     private updateFilterOnTypeTitleAndIcon(): void {
  866         if (this.filterOnType) {
  867             removeClasses(this.filterOnTypeDomNode, treeFilterOnTypeOffIcon.classNames);
  868             addClasses(this.filterOnTypeDomNode, treeFilterOnTypeOnIcon.classNames);
  869             this.filterOnTypeDomNode.title = localize('disable filter on type', "Disable Filter on Type");
  870         } else {
  871             removeClasses(this.filterOnTypeDomNode, treeFilterOnTypeOnIcon.classNames);
  872             addClasses(this.filterOnTypeDomNode, treeFilterOnTypeOffIcon.classNames);
  873             this.filterOnTypeDomNode.title = localize('enable filter on type', "Enable Filter on Type");
  874         }
  875     }
  876 
  877     private render(): void {
  878         const noMatches = this.filter.totalCount > 0 && this.filter.matchCount === 0;
  879 
  880         if (this.pattern && this.tree.options.filterOnType && noMatches) {
  881             this.messageDomNode.textContent = localize('empty', "No elements found");
  882             this._empty = true;
  883         } else {
  884             this.messageDomNode.innerText = '';
  885             this._empty = false;
  886         }
  887 
  888         toggleClass(this.domNode, 'no-matches', noMatches);
  889         this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount);
  890         this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern;
  891 
  892         this._onDidChangeEmptyState.fire(this._empty);
  893     }
  894 
  895     shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean {
  896         if (!this.enabled || !this.pattern || this.filterOnType) {
  897             return true;
  898         }
  899 
  900         if (this.filter.totalCount > 0 && this.filter.matchCount <= 1) {
  901             return true;
  902         }
  903 
  904         return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
  905     }
  906 
  907     dispose() {
  908         if (this._enabled) {
  909             this.domNode.remove();
  910             this.enabledDisposables.dispose();
  911             this._enabled = false;
  912             this.triggered = false;
  913         }
  914 
  915         this._onDidChangePattern.dispose();
  916         dispose(this.disposables);
  917     }
  918 }
  919 
  920 function asTreeMouseEvent<T>(event: IListMouseEvent<ITreeNode<T, any>>): ITreeMouseEvent<T> {
  921     let target: TreeMouseEventTarget = TreeMouseEventTarget.Unknown;
  922 
  923     if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tl-twistie', 'monaco-tl-row')) {
  924         target = TreeMouseEventTarget.Twistie;
  925     } else if (hasParentWithClass(event.browserEvent.target as HTMLElement, 'monaco-tl-contents', 'monaco-tl-row')) {
  926         target = TreeMouseEventTarget.Element;
  927     }
  928 
  929     return {
  930         browserEvent: event.browserEvent,
  931         element: event.element ? event.element.element : null,
  932         target
  933     };
  934 }
  935 
  936 function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any>>): ITreeContextMenuEvent<T> {
  937     return {
  938         element: event.element ? event.element.element : null,
  939         browserEvent: event.browserEvent,
  940         anchor: event.anchor
  941     };
  942 }
  943 
  944 export interface IKeyboardNavigationEventFilter {
  945     (e: StandardKeyboardEvent): boolean;
  946 }
  947 
  948 export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
  949     readonly automaticKeyboardNavigation?: boolean;
  950     readonly simpleKeyboardNavigation?: boolean;
  951     readonly filterOnType?: boolean;
  952     readonly smoothScrolling?: boolean;
  953     readonly horizontalScrolling?: boolean;
  954     readonly expandOnlyOnDoubleClick?: boolean;
  955 }
  956 
  957 export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTreeOptionsUpdate, IListOptions<T> {
  958     readonly collapseByDefault?: boolean; // defaults to false
  959     readonly filter?: ITreeFilter<T, TFilterData>;
  960     readonly dnd?: ITreeDragAndDrop<T>;
  961     readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
  962     readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean);
  963     readonly additionalScrollHeight?: number;
  964 }
  965 
  966 function dfs<T, TFilterData>(node: ITreeNode<T, TFilterData>, fn: (node: ITreeNode<T, TFilterData>) => void): void {
  967     fn(node);
  968     node.children.forEach(child => dfs(child, fn));
  969 }
  970 
  971 /**
  972  * The trait concept needs to exist at the tree level, because collapsed
  973  * tree nodes will not be known by the list.
  974  */
  975 class Trait<T> {
  976 
  977     private nodes: ITreeNode<T, any>[] = [];
  978     private elements: T[] | undefined;
  979 
  980     private readonly _onDidChange = new Emitter<ITreeEvent<T>>();
  981     readonly onDidChange = this._onDidChange.event;
  982 
  983     private _nodeSet: Set<ITreeNode<T, any>> | undefined;
  984     private get nodeSet(): Set<ITreeNode<T, any>> {
  985         if (!this._nodeSet) {
  986             this._nodeSet = this.createNodeSet();
  987         }
  988 
  989         return this._nodeSet;
  990     }
  991 
  992     constructor(private identityProvider?: IIdentityProvider<T>) { }
  993 
  994     set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
  995         if (equals(this.nodes, nodes)) {
  996             return;
  997         }
  998 
  999         this._set(nodes, false, browserEvent);
 1000     }
 1001 
 1002     private _set(nodes: ITreeNode<T, any>[], silent: boolean, browserEvent?: UIEvent): void {
 1003         this.nodes = [...nodes];
 1004         this.elements = undefined;
 1005         this._nodeSet = undefined;
 1006 
 1007         if (!silent) {
 1008             const that = this;
 1009             this._onDidChange.fire({ get elements() { return that.get(); }, browserEvent });
 1010         }
 1011     }
 1012 
 1013     get(): T[] {
 1014         if (!this.elements) {
 1015             this.elements = this.nodes.map(node => node.element);
 1016         }
 1017 
 1018         return [...this.elements];
 1019     }
 1020 
 1021     getNodes(): readonly ITreeNode<T, any>[] {
 1022         return this.nodes;
 1023     }
 1024 
 1025     has(node: ITreeNode<T, any>): boolean {
 1026         return this.nodeSet.has(node);
 1027     }
 1028 
 1029     onDidModelSplice({ insertedNodes, deletedNodes }: ITreeModelSpliceEvent<T, any>): void {
 1030         if (!this.identityProvider) {
 1031             const set = this.createNodeSet();
 1032             const visit = (node: ITreeNode<T, any>) => set.delete(node);
 1033             deletedNodes.forEach(node => dfs(node, visit));
 1034             this.set([...set.values()]);
 1035             return;
 1036         }
 1037 
 1038         const deletedNodesIdSet = new Set<string>();
 1039         const deletedNodesVisitor = (node: ITreeNode<T, any>) => deletedNodesIdSet.add(this.identityProvider!.getId(node.element).toString());
 1040         deletedNodes.forEach(node => dfs(node, deletedNodesVisitor));
 1041 
 1042         const insertedNodesMap = new Map<string, ITreeNode<T, any>>();
 1043         const insertedNodesVisitor = (node: ITreeNode<T, any>) => insertedNodesMap.set(this.identityProvider!.getId(node.element).toString(), node);
 1044         insertedNodes.forEach(node => dfs(node, insertedNodesVisitor));
 1045 
 1046         const nodes: ITreeNode<T, any>[] = [];
 1047 
 1048         for (const node of this.nodes) {
 1049             const id = this.identityProvider.getId(node.element).toString();
 1050             const wasDeleted = deletedNodesIdSet.has(id);
 1051 
 1052             if (!wasDeleted) {
 1053                 nodes.push(node);
 1054             } else {
 1055                 const insertedNode = insertedNodesMap.get(id);
 1056 
 1057                 if (insertedNode) {
 1058                     nodes.push(insertedNode);
 1059                 }
 1060             }
 1061         }
 1062 
 1063         this._set(nodes, true);
 1064     }
 1065 
 1066     private createNodeSet(): Set<ITreeNode<T, any>> {
 1067         const set = new Set<ITreeNode<T, any>>();
 1068 
 1069         for (const node of this.nodes) {
 1070             set.add(node);
 1071         }
 1072 
 1073         return set;
 1074     }
 1075 }
 1076 
 1077 class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<ITreeNode<T, TFilterData>> {
 1078 
 1079     constructor(list: TreeNodeList<T, TFilterData, TRef>, private tree: AbstractTree<T, TFilterData, TRef>) {
 1080         super(list);
 1081     }
 1082 
 1083     protected onViewPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
 1084         if (isInputElement(e.browserEvent.target as HTMLElement) || isMonacoEditor(e.browserEvent.target as HTMLElement)) {
 1085             return;
 1086         }
 1087 
 1088         const node = e.element;
 1089 
 1090         if (!node) {
 1091             return super.onViewPointer(e);
 1092         }
 1093 
 1094         if (this.isSelectionRangeChangeEvent(e) || this.isSelectionSingleChangeEvent(e)) {
 1095             return super.onViewPointer(e);
 1096         }
 1097 
 1098         const target = e.browserEvent.target as HTMLElement;
 1099         const onTwistie = hasClass(target, 'monaco-tl-twistie')
 1100             || (hasClass(target, 'monaco-icon-label') && hasClass(target, 'folder-icon') && e.browserEvent.offsetX < 16);
 1101 
 1102         let expandOnlyOnTwistieClick = false;
 1103 
 1104         if (typeof this.tree.expandOnlyOnTwistieClick === 'function') {
 1105             expandOnlyOnTwistieClick = this.tree.expandOnlyOnTwistieClick(node.element);
 1106         } else {
 1107             expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick;
 1108         }
 1109 
 1110         if (expandOnlyOnTwistieClick && !onTwistie) {
 1111             return super.onViewPointer(e);
 1112         }
 1113 
 1114         if (this.tree.expandOnlyOnDoubleClick && e.browserEvent.detail !== 2 && !onTwistie) {
 1115             return super.onViewPointer(e);
 1116         }
 1117 
 1118         if (node.collapsible) {
 1119             const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
 1120             const location = model.getNodeLocation(node);
 1121             const recursive = e.browserEvent.altKey;
 1122             model.setCollapsed(location, undefined, recursive);
 1123 
 1124             if (expandOnlyOnTwistieClick && onTwistie) {
 1125                 return;
 1126             }
 1127         }
 1128 
 1129         super.onViewPointer(e);
 1130     }
 1131 
 1132     protected onDoubleClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
 1133         const onTwistie = hasClass(e.browserEvent.target as HTMLElement, 'monaco-tl-twistie');
 1134 
 1135         if (onTwistie) {
 1136             return;
 1137         }
 1138 
 1139         super.onDoubleClick(e);
 1140     }
 1141 }
 1142 
 1143 interface ITreeNodeListOptions<T, TFilterData, TRef> extends IListOptions<ITreeNode<T, TFilterData>> {
 1144     readonly tree: AbstractTree<T, TFilterData, TRef>;
 1145 }
 1146 
 1147 /**
 1148  * We use this List subclass to restore selection and focus as nodes
 1149  * get rendered in the list, possibly due to a node expand() call.
 1150  */
 1151 class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>> {
 1152 
 1153     constructor(
 1154         user: string,
 1155         container: HTMLElement,
 1156         virtualDelegate: IListVirtualDelegate<ITreeNode<T, TFilterData>>,
 1157         renderers: IListRenderer<any /* TODO@joao */, any>[],
 1158         private focusTrait: Trait<T>,
 1159         private selectionTrait: Trait<T>,
 1160         options: ITreeNodeListOptions<T, TFilterData, TRef>
 1161     ) {
 1162         super(user, container, virtualDelegate, renderers, options);
 1163     }
 1164 
 1165     protected createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
 1166         return new TreeNodeListMouseController(this, options.tree);
 1167     }
 1168 
 1169     splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
 1170         super.splice(start, deleteCount, elements);
 1171 
 1172         if (elements.length === 0) {
 1173             return;
 1174         }
 1175 
 1176         const additionalFocus: number[] = [];
 1177         const additionalSelection: number[] = [];
 1178 
 1179         elements.forEach((node, index) => {
 1180             if (this.focusTrait.has(node)) {
 1181                 additionalFocus.push(start + index);
 1182             }
 1183 
 1184             if (this.selectionTrait.has(node)) {
 1185                 additionalSelection.push(start + index);
 1186             }
 1187         });
 1188 
 1189         if (additionalFocus.length > 0) {
 1190             super.setFocus(distinctES6([...super.getFocus(), ...additionalFocus]));
 1191         }
 1192 
 1193         if (additionalSelection.length > 0) {
 1194             super.setSelection(distinctES6([...super.getSelection(), ...additionalSelection]));
 1195         }
 1196     }
 1197 
 1198     setFocus(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
 1199         super.setFocus(indexes, browserEvent);
 1200 
 1201         if (!fromAPI) {
 1202             this.focusTrait.set(indexes.map(i => this.element(i)), browserEvent);
 1203         }
 1204     }
 1205 
 1206     setSelection(indexes: number[], browserEvent?: UIEvent, fromAPI = false): void {
 1207         super.setSelection(indexes, browserEvent);
 1208 
 1209         if (!fromAPI) {
 1210             this.selectionTrait.set(indexes.map(i => this.element(i)), browserEvent);
 1211         }
 1212     }
 1213 }
 1214 
 1215 export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
 1216 
 1217     protected view: TreeNodeList<T, TFilterData, TRef>;
 1218     private renderers: TreeRenderer<T, TFilterData, TRef, any>[];
 1219     protected model: ITreeModel<T, TFilterData, TRef>;
 1220     private focus: Trait<T>;
 1221     private selection: Trait<T>;
 1222     private eventBufferer = new EventBufferer();
 1223     private typeFilterController?: TypeFilterController<T, TFilterData>;
 1224     private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
 1225     private styleElement: HTMLStyleElement;
 1226     protected readonly disposables = new DisposableStore();
 1227 
 1228     get onDidScroll(): Event<ScrollEvent> { return this.view.onDidScroll; }
 1229 
 1230     get onDidChangeFocus(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.focus.onDidChange); }
 1231     get onDidChangeSelection(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); }
 1232 
 1233     get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); }
 1234     get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseDblClick, asTreeMouseEvent); }
 1235     get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.view.onContextMenu, asTreeContextMenuEvent); }
 1236     get onTap(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onTap, asTreeMouseEvent); }
 1237     get onPointer(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onPointer, asTreeMouseEvent); }
 1238 
 1239     get onKeyDown(): Event<KeyboardEvent> { return this.view.onKeyDown; }
 1240     get onKeyUp(): Event<KeyboardEvent> { return this.view.onKeyUp; }
 1241     get onKeyPress(): Event<KeyboardEvent> { return this.view.onKeyPress; }
 1242 
 1243     get onDidFocus(): Event<void> { return this.view.onDidFocus; }
 1244     get onDidBlur(): Event<void> { return this.view.onDidBlur; }
 1245 
 1246     get onDidChangeCollapseState(): Event<ICollapseStateChangeEvent<T, TFilterData>> { return this.model.onDidChangeCollapseState; }
 1247     get onDidChangeRenderNodeCount(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeRenderNodeCount; }
 1248 
 1249     private readonly _onWillRefilter = new Emitter<void>();
 1250     readonly onWillRefilter: Event<void> = this._onWillRefilter.event;
 1251 
 1252     get filterOnType(): boolean { return !!this._options.filterOnType; }
 1253     get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; }
 1254 
 1255     get expandOnlyOnDoubleClick(): boolean { return this._options.expandOnlyOnDoubleClick ?? false; }
 1256     get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? false : this._options.expandOnlyOnTwistieClick; }
 1257 
 1258     private readonly _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
 1259     readonly onDidUpdateOptions: Event<IAbstractTreeOptions<T, TFilterData>> = this._onDidUpdateOptions.event;
 1260 
 1261     get onDidDispose(): Event<void> { return this.view.onDidDispose; }
 1262 
 1263     constructor(
 1264         user: string,
 1265         container: HTMLElement,
 1266         delegate: IListVirtualDelegate<T>,
 1267         renderers: ITreeRenderer<T, TFilterData, any>[],
 1268         private _options: IAbstractTreeOptions<T, TFilterData> = {}
 1269     ) {
 1270         const treeDelegate = new ComposedTreeDelegate<T, ITreeNode<T, TFilterData>>(delegate);
 1271 
 1272         const onDidChangeCollapseStateRelay = new Relay<ICollapseStateChangeEvent<T, TFilterData>>();
 1273         const onDidChangeActiveNodes = new Relay<ITreeNode<T, TFilterData>[]>();
 1274         const activeNodes = new EventCollection(onDidChangeActiveNodes.event);
 1275         this.renderers = renderers.map(r => new TreeRenderer<T, TFilterData, TRef, any>(r, () => this.model, onDidChangeCollapseStateRelay.event, activeNodes, _options));
 1276         for (let r of this.renderers) {
 1277             this.disposables.add(r);
 1278         }
 1279 
 1280         let filter: TypeFilter<T> | undefined;
 1281 
 1282         if (_options.keyboardNavigationLabelProvider) {
 1283             filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>);
 1284             _options = { ..._options, filter: filter as ITreeFilter<T, TFilterData> }; // TODO need typescript help here
 1285             this.disposables.add(filter);
 1286         }
 1287 
 1288         this.focus = new Trait(_options.identityProvider);
 1289         this.selection = new Trait(_options.identityProvider);
 1290         this.view = new TreeNodeList(user, container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
 1291 
 1292         this.model = this.createModel(user, this.view, _options);
 1293         onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
 1294 
 1295         const onDidModelSplice = Event.forEach(this.model.onDidSplice, e => {
 1296             this.eventBufferer.bufferEvents(() => {
 1297                 this.focus.onDidModelSplice(e);
 1298                 this.selection.onDidModelSplice(e);
 1299             });
 1300         });
 1301 
 1302         // Make sure the `forEach` always runs
 1303         onDidModelSplice(() => null, null, this.disposables);
 1304 
 1305         // Active nodes can change when the model changes or when focus or selection change.
 1306         // We debounce it with 0 delay since these events may fire in the same stack and we only
 1307         // want to run this once. It also doesn't matter if it runs on the next tick since it's only
 1308         // a nice to have UI feature.
 1309         onDidChangeActiveNodes.input = Event.chain(Event.any<any>(onDidModelSplice, this.focus.onDidChange, this.selection.onDidChange))
 1310             .debounce(() => null, 0)
 1311             .map(() => {
 1312                 const set = new Set<ITreeNode<T, TFilterData>>();
 1313 
 1314                 for (const node of this.focus.getNodes()) {
 1315                     set.add(node);
 1316                 }
 1317 
 1318                 for (const node of this.selection.getNodes()) {
 1319                     set.add(node);
 1320                 }
 1321 
 1322                 return [...set.values()];
 1323             }).event;
 1324 
 1325         if (_options.keyboardSupport !== false) {
 1326             const onKeyDown = Event.chain(this.view.onKeyDown)
 1327                 .filter(e => !isInputElement(e.target as HTMLElement))
 1328                 .map(e => new StandardKeyboardEvent(e));
 1329 
 1330             onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables);
 1331             onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables);
 1332             onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables);
 1333         }
 1334 
 1335         if (_options.keyboardNavigationLabelProvider) {
 1336             const delegate = _options.keyboardNavigationDelegate || DefaultKeyboardNavigationDelegate;
 1337             this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, delegate);
 1338             this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node);
 1339             this.disposables.add(this.typeFilterController!);
 1340         }
 1341 
 1342         this.styleElement = createStyleSheet(this.view.getHTMLElement());
 1343         toggleClass(this.getHTMLElement(), 'always', this._options.renderIndentGuides === RenderIndentGuides.Always);
 1344     }
 1345 
 1346     updateOptions(optionsUpdate: IAbstractTreeOptionsUpdate = {}): void {
 1347         this._options = { ...this._options, ...optionsUpdate };
 1348 
 1349         for (const renderer of this.renderers) {
 1350             renderer.updateOptions(optionsUpdate);
 1351         }
 1352 
 1353         this.view.updateOptions({
 1354             enableKeyboardNavigation: this._options.simpleKeyboardNavigation,
 1355             automaticKeyboardNavigation: this._options.automaticKeyboardNavigation,
 1356             smoothScrolling: this._options.smoothScrolling,
 1357             horizontalScrolling: this._options.horizontalScrolling
 1358         });
 1359 
 1360         if (this.typeFilterController) {
 1361             this.typeFilterController.updateOptions(this._options);
 1362         }
 1363 
 1364         this._onDidUpdateOptions.fire(this._options);
 1365 
 1366         toggleClass(this.getHTMLElement(), 'always', this._options.renderIndentGuides === RenderIndentGuides.Always);
 1367     }
 1368 
 1369     get options(): IAbstractTreeOptions<T, TFilterData> {
 1370         return this._options;
 1371     }
 1372 
 1373     updateWidth(element: TRef): void {
 1374         const index = this.model.getListIndex(element);
 1375 
 1376         if (index === -1) {
 1377             return;
 1378         }
 1379 
 1380         this.view.updateWidth(index);
 1381     }
 1382 
 1383     // Widget
 1384 
 1385     getHTMLElement(): HTMLElement {
 1386         return this.view.getHTMLElement();
 1387     }
 1388 
 1389     get contentHeight(): number {
 1390         if (this.typeFilterController && this.typeFilterController.filterOnType && this.typeFilterController.empty) {
 1391             return 100;
 1392         }
 1393 
 1394         return this.view.contentHeight;
 1395     }
 1396 
 1397     get onDidChangeContentHeight(): Event<number> {
 1398         let result = this.view.onDidChangeContentHeight;
 1399 
 1400         if (this.typeFilterController) {
 1401             result = Event.any(result, Event.map(this.typeFilterController.onDidChangeEmptyState, () => this.contentHeight));
 1402         }
 1403 
 1404         return result;
 1405     }
 1406 
 1407     get scrollTop(): number {
 1408         return this.view.scrollTop;
 1409     }
 1410 
 1411     set scrollTop(scrollTop: number) {
 1412         this.view.scrollTop = scrollTop;
 1413     }
 1414 
 1415     get scrollLeft(): number {
 1416         return this.view.scrollLeft;
 1417     }
 1418 
 1419     set scrollLeft(scrollLeft: number) {
 1420         this.view.scrollLeft = scrollLeft;
 1421     }
 1422 
 1423     get scrollHeight(): number {
 1424         return this.view.scrollHeight;
 1425     }
 1426 
 1427     get renderHeight(): number {
 1428         return this.view.renderHeight;
 1429     }
 1430 
 1431     get firstVisibleElement(): T | undefined {
 1432         const index = this.view.firstVisibleIndex;
 1433 
 1434         if (index < 0 || index >= this.view.length) {
 1435             return undefined;
 1436         }
 1437 
 1438         const node = this.view.element(index);
 1439         return node.element;
 1440     }
 1441 
 1442     get lastVisibleElement(): T {
 1443         const index = this.view.lastVisibleIndex;
 1444         const node = this.view.element(index);
 1445         return node.element;
 1446     }
 1447 
 1448     get ariaLabel(): string {
 1449         return this.view.ariaLabel;
 1450     }
 1451 
 1452     set ariaLabel(value: string) {
 1453         this.view.ariaLabel = value;
 1454     }
 1455 
 1456     domFocus(): void {
 1457         this.view.domFocus();
 1458     }
 1459 
 1460     isDOMFocused(): boolean {
 1461         return this.getHTMLElement() === document.activeElement;
 1462     }
 1463 
 1464     layout(height?: number, width?: number): void {
 1465         this.view.layout(height, width);
 1466     }
 1467 
 1468     style(styles: IListStyles): void {
 1469         const suffix = `.${this.view.domId}`;
 1470         const content: string[] = [];
 1471 
 1472         if (styles.treeIndentGuidesStroke) {
 1473             content.push(`.monaco-list${suffix}:hover .monaco-tl-indent > .indent-guide, .monaco-list${suffix}.always .monaco-tl-indent > .indent-guide  { border-color: ${styles.treeIndentGuidesStroke.transparent(0.4)}; }`);
 1474             content.push(`.monaco-list${suffix} .monaco-tl-indent > .indent-guide.active { border-color: ${styles.treeIndentGuidesStroke}; }`);
 1475         }
 1476 
 1477         const newStyles = content.join('\n');
 1478         if (newStyles !== this.styleElement.innerHTML) {
 1479             this.styleElement.innerHTML = newStyles;
 1480         }
 1481 
 1482         this.view.style(styles);
 1483     }
 1484 
 1485     // Tree navigation
 1486 
 1487     getParentElement(location: TRef): T {
 1488         const parentRef = this.model.getParentNodeLocation(location);
 1489         const parentNode = this.model.getNode(parentRef);
 1490         return parentNode.element;
 1491     }
 1492 
 1493     getFirstElementChild(location: TRef): T | undefined {
 1494         return this.model.getFirstElementChild(location);
 1495     }
 1496 
 1497     // Tree
 1498 
 1499     getNode(location?: TRef): ITreeNode<T, TFilterData> {
 1500         return this.model.getNode(location);
 1501     }
 1502 
 1503     collapse(location: TRef, recursive: boolean = false): boolean {
 1504         return this.model.setCollapsed(location, true, recursive);
 1505     }
 1506 
 1507     expand(location: TRef, recursive: boolean = false): boolean {
 1508         return this.model.setCollapsed(location, false, recursive);
 1509     }
 1510 
 1511     toggleCollapsed(location: TRef, recursive: boolean = false): boolean {
 1512         return this.model.setCollapsed(location, undefined, recursive);
 1513     }
 1514 
 1515     expandAll(): void {
 1516         this.model.setCollapsed(this.model.rootRef, false, true);
 1517     }
 1518 
 1519     collapseAll(): void {
 1520         this.model.setCollapsed(this.model.rootRef, true, true);
 1521     }
 1522 
 1523     isCollapsible(location: TRef): boolean {
 1524         return this.model.isCollapsible(location);
 1525     }
 1526 
 1527     setCollapsible(location: TRef, collapsible?: boolean): boolean {
 1528         return this.model.setCollapsible(location, collapsible);
 1529     }
 1530 
 1531     isCollapsed(location: TRef): boolean {
 1532         return this.model.isCollapsed(location);
 1533     }
 1534 
 1535     toggleKeyboardNavigation(): void {
 1536         this.view.toggleKeyboardNavigation();
 1537 
 1538         if (this.typeFilterController) {
 1539             this.typeFilterController.toggle();
 1540         }
 1541     }
 1542 
 1543     refilter(): void {
 1544         this._onWillRefilter.fire(undefined);
 1545         this.model.refilter();
 1546     }
 1547 
 1548     setSelection(elements: TRef[], browserEvent?: UIEvent): void {
 1549         const nodes = elements.map(e => this.model.getNode(e));
 1550         this.selection.set(nodes, browserEvent);
 1551 
 1552         const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
 1553         this.view.setSelection(indexes, browserEvent, true);
 1554     }
 1555 
 1556     getSelection(): T[] {
 1557         return this.selection.get();
 1558     }
 1559 
 1560     setFocus(elements: TRef[], browserEvent?: UIEvent): void {
 1561         const nodes = elements.map(e => this.model.getNode(e));
 1562         this.focus.set(nodes, browserEvent);
 1563 
 1564         const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i > -1);
 1565         this.view.setFocus(indexes, browserEvent, true);
 1566     }
 1567 
 1568     focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1569         this.view.focusNext(n, loop, browserEvent, filter);
 1570     }
 1571 
 1572     focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1573         this.view.focusPrevious(n, loop, browserEvent, filter);
 1574     }
 1575 
 1576     focusNextPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1577         this.view.focusNextPage(browserEvent, filter);
 1578     }
 1579 
 1580     focusPreviousPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1581         this.view.focusPreviousPage(browserEvent, filter);
 1582     }
 1583 
 1584     focusLast(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1585         this.view.focusLast(browserEvent, filter);
 1586     }
 1587 
 1588     focusFirst(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
 1589         this.view.focusFirst(browserEvent, filter);
 1590     }
 1591 
 1592     getFocus(): T[] {
 1593         return this.focus.get();
 1594     }
 1595 
 1596     reveal(location: TRef, relativeTop?: number): void {
 1597         this.model.expandTo(location);
 1598 
 1599         const index = this.model.getListIndex(location);
 1600 
 1601         if (index === -1) {
 1602             return;
 1603         }
 1604 
 1605         this.view.reveal(index, relativeTop);
 1606     }
 1607 
 1608     /**
 1609      * Returns the relative position of an element rendered in the list.
 1610      * Returns `null` if the element isn't *entirely* in the visible viewport.
 1611      */
 1612     getRelativeTop(location: TRef): number | null {
 1613         const index = this.model.getListIndex(location);
 1614 
 1615         if (index === -1) {
 1616             return null;
 1617         }
 1618 
 1619         return this.view.getRelativeTop(index);
 1620     }
 1621 
 1622     // List
 1623 
 1624     private onLeftArrow(e: StandardKeyboardEvent): void {
 1625         e.preventDefault();
 1626         e.stopPropagation();
 1627 
 1628         const nodes = this.view.getFocusedElements();
 1629 
 1630         if (nodes.length === 0) {
 1631             return;
 1632         }
 1633 
 1634         const node = nodes[0];
 1635         const location = this.model.getNodeLocation(node);
 1636         const didChange = this.model.setCollapsed(location, true);
 1637 
 1638         if (!didChange) {
 1639             const parentLocation = this.model.getParentNodeLocation(location);
 1640 
 1641             if (!parentLocation) {
 1642                 return;
 1643             }
 1644 
 1645             const parentListIndex = this.model.getListIndex(parentLocation);
 1646 
 1647             this.view.reveal(parentListIndex);
 1648             this.view.setFocus([parentListIndex]);
 1649         }
 1650     }
 1651 
 1652     private onRightArrow(e: StandardKeyboardEvent): void {
 1653         e.preventDefault();
 1654         e.stopPropagation();
 1655 
 1656         const nodes = this.view.getFocusedElements();
 1657 
 1658         if (nodes.length === 0) {
 1659             return;
 1660         }
 1661 
 1662         const node = nodes[0];
 1663         const location = this.model.getNodeLocation(node);
 1664         const didChange = this.model.setCollapsed(location, false);
 1665 
 1666         if (!didChange) {
 1667             if (!node.children.some(child => child.visible)) {
 1668                 return;
 1669             }
 1670 
 1671             const [focusedIndex] = this.view.getFocus();
 1672             const firstChildIndex = focusedIndex + 1;
 1673 
 1674             this.view.reveal(firstChildIndex);
 1675             this.view.setFocus([firstChildIndex]);
 1676         }
 1677     }
 1678 
 1679     private onSpace(e: StandardKeyboardEvent): void {
 1680         e.preventDefault();
 1681         e.stopPropagation();
 1682 
 1683         const nodes = this.view.getFocusedElements();
 1684 
 1685         if (nodes.length === 0) {
 1686             return;
 1687         }
 1688 
 1689         const node = nodes[0];
 1690         const location = this.model.getNodeLocation(node);
 1691         const recursive = e.browserEvent.altKey;
 1692 
 1693         this.model.setCollapsed(location, undefined, recursive);
 1694     }
 1695 
 1696     protected abstract createModel(user: string, view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
 1697 
 1698     navigate(start?: TRef): ITreeNavigator<T> {
 1699         return new TreeNavigator(this.view, this.model, start);
 1700     }
 1701 
 1702     dispose(): void {
 1703         dispose(this.disposables);
 1704         this.view.dispose();
 1705     }
 1706 }
 1707 
 1708 interface ITreeNavigatorView<T extends NonNullable<any>, TFilterData> {
 1709     readonly length: number;
 1710     element(index: number): ITreeNode<T, TFilterData>;
 1711 }
 1712 
 1713 class TreeNavigator<T extends NonNullable<any>, TFilterData, TRef> implements ITreeNavigator<T> {
 1714 
 1715     private index: number;
 1716 
 1717     constructor(private view: ITreeNavigatorView<T, TFilterData>, private model: ITreeModel<T, TFilterData, TRef>, start?: TRef) {
 1718         if (start) {
 1719             this.index = this.model.getListIndex(start);
 1720         } else {
 1721             this.index = -1;
 1722         }
 1723     }
 1724 
 1725     current(): T | null {
 1726         if (this.index < 0 || this.index >= this.view.length) {
 1727             return null;
 1728         }
 1729 
 1730         return this.view.element(this.index).element;
 1731     }
 1732 
 1733     previous(): T | null {
 1734         this.index--;
 1735         return this.current();
 1736     }
 1737 
 1738     next(): T | null {
 1739         this.index++;
 1740         return this.current();
 1741     }
 1742 
 1743     first(): T | null {
 1744         this.index = 0;
 1745         return this.current();
 1746     }
 1747 
 1748     last(): T | null {
 1749         this.index = this.view.length - 1;
 1750         return this.current();
 1751     }
 1752 }