Path: blob/main/src/vs/base/browser/ui/list/listView.ts
5253 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { DataTransfers, IDragAndDropData } from '../../dnd.js';6import { addDisposableListener, animate, Dimension, getActiveElement, getContentHeight, getContentWidth, getDocument, getTopLeftOffset, getWindow, isAncestor, isHTMLElement, isSVGElement, scheduleAtNextAnimationFrame } from '../../dom.js';7import { DomEmitter } from '../../event.js';8import { IMouseWheelEvent } from '../../mouseEvent.js';9import { EventType as TouchEventType, Gesture, GestureEvent } from '../../touch.js';10import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';11import { distinct, equals, splice } from '../../../common/arrays.js';12import { Delayer, disposableTimeout } from '../../../common/async.js';13import { memoize } from '../../../common/decorators.js';14import { Emitter, Event, IValueWithChangeEvent } from '../../../common/event.js';15import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../common/lifecycle.js';16import { IRange, Range } from '../../../common/range.js';17import { INewScrollDimensions, Scrollable, ScrollbarVisibility, ScrollEvent } from '../../../common/scrollable.js';18import { ISpliceable } from '../../../common/sequence.js';19import { IListDragAndDrop, IListDragEvent, IListGestureEvent, IListMouseEvent, IListRenderer, IListTouchEvent, IListVirtualDelegate, ListDragOverEffectPosition, ListDragOverEffectType } from './list.js';20import { IRangeMap, RangeMap, shift } from './rangeMap.js';21import { IRow, RowCache } from './rowCache.js';22import { BugIndicatingError } from '../../../common/errors.js';23import { AriaRole } from '../aria/aria.js';24import { ScrollableElementChangeOptions } from '../scrollbar/scrollableElementOptions.js';25import { clamp } from '../../../common/numbers.js';26import { applyDragImage } from '../dnd/dnd.js';2728interface IItem<T> {29readonly id: string;30readonly element: T;31readonly templateId: string;32row: IRow | null;33size: number;34width: number | undefined;35hasDynamicHeight: boolean;36lastDynamicHeightWidth: number | undefined;37uri: string | undefined;38dropTarget: boolean;39dragStartDisposable: IDisposable;40checkedDisposable: IDisposable;41stale: boolean;42}4344const StaticDND = {45CurrentDragAndDropData: undefined as IDragAndDropData | undefined46};4748export interface IListViewDragAndDrop<T> extends IListDragAndDrop<T> {49getDragElements(element: T): T[];50}5152export const enum ListViewTargetSector {53// drop position relative to the top of the item54TOP = 0, // [0%-25%)55CENTER_TOP = 1, // [25%-50%)56CENTER_BOTTOM = 2, // [50%-75%)57BOTTOM = 3 // [75%-100%)58}5960export type CheckBoxAccessibleState = boolean | 'mixed';6162export interface IListViewAccessibilityProvider<T> {63getSetSize?(element: T, index: number, listLength: number): number;64getPosInSet?(element: T, index: number): number;65getRole?(element: T): AriaRole | undefined;66isChecked?(element: T): CheckBoxAccessibleState | IValueWithChangeEvent<CheckBoxAccessibleState> | undefined;67}6869export interface IListViewOptionsUpdate {70readonly smoothScrolling?: boolean;71readonly horizontalScrolling?: boolean;72readonly scrollByPage?: boolean;73readonly mouseWheelScrollSensitivity?: number;74readonly fastScrollSensitivity?: number;75readonly paddingTop?: number;76readonly paddingBottom?: number;77}7879export interface IListViewOptions<T> extends IListViewOptionsUpdate {80readonly dnd?: IListViewDragAndDrop<T>;81readonly useShadows?: boolean;82readonly verticalScrollMode?: ScrollbarVisibility;83readonly setRowLineHeight?: boolean;84readonly setRowHeight?: boolean;85readonly supportDynamicHeights?: boolean;86readonly mouseSupport?: boolean;87readonly userSelection?: boolean;88readonly accessibilityProvider?: IListViewAccessibilityProvider<T>;89readonly transformOptimization?: boolean;90readonly alwaysConsumeMouseWheel?: boolean;91readonly initialSize?: Dimension;92readonly scrollToActiveElement?: boolean;93}9495const DefaultOptions = {96useShadows: true,97verticalScrollMode: ScrollbarVisibility.Auto,98setRowLineHeight: true,99setRowHeight: true,100supportDynamicHeights: false,101dnd: {102getDragElements<T>(e: T) { return [e]; },103getDragURI() { return null; },104onDragStart(): void { },105onDragOver() { return false; },106drop() { },107dispose() { }108},109horizontalScrolling: false,110transformOptimization: true,111alwaysConsumeMouseWheel: true,112} satisfies IListViewOptions<any>;113114export class ElementsDragAndDropData<T, TContext = void> implements IDragAndDropData {115116readonly elements: T[];117118private _context: TContext | undefined;119public get context(): TContext | undefined {120return this._context;121}122public set context(value: TContext | undefined) {123this._context = value;124}125126constructor(elements: T[]) {127this.elements = elements;128}129130update(): void { }131132getData(): T[] {133return this.elements;134}135}136137export class ExternalElementsDragAndDropData<T> implements IDragAndDropData {138139readonly elements: T[];140141constructor(elements: T[]) {142this.elements = elements;143}144145update(): void { }146147getData(): T[] {148return this.elements;149}150}151152export class NativeDragAndDropData implements IDragAndDropData {153154readonly types: any[];155readonly files: any[];156157constructor() {158this.types = [];159this.files = [];160}161162update(dataTransfer: DataTransfer): void {163if (dataTransfer.types) {164this.types.splice(0, this.types.length, ...dataTransfer.types);165}166167if (dataTransfer.files) {168this.files.splice(0, this.files.length);169170for (let i = 0; i < dataTransfer.files.length; i++) {171const file = dataTransfer.files.item(i);172173if (file && (file.size || file.type)) {174this.files.push(file);175}176}177}178}179180getData() {181return {182types: this.types,183files: this.files184};185}186}187188function equalsDragFeedback(f1: number[] | undefined, f2: number[] | undefined): boolean {189if (Array.isArray(f1) && Array.isArray(f2)) {190return equals(f1, f2);191}192193return f1 === f2;194}195196class ListViewAccessibilityProvider<T> implements Required<IListViewAccessibilityProvider<T>> {197198readonly getSetSize: (element: T, index: number, listLength: number) => number;199readonly getPosInSet: (element: T, index: number) => number;200readonly getRole: (element: T) => AriaRole | undefined;201readonly isChecked: (element: T) => CheckBoxAccessibleState | IValueWithChangeEvent<CheckBoxAccessibleState> | undefined;202203constructor(accessibilityProvider?: IListViewAccessibilityProvider<T>) {204if (accessibilityProvider?.getSetSize) {205this.getSetSize = accessibilityProvider.getSetSize.bind(accessibilityProvider);206} else {207this.getSetSize = (e, i, l) => l;208}209210if (accessibilityProvider?.getPosInSet) {211this.getPosInSet = accessibilityProvider.getPosInSet.bind(accessibilityProvider);212} else {213this.getPosInSet = (e, i) => i + 1;214}215216if (accessibilityProvider?.getRole) {217this.getRole = accessibilityProvider.getRole.bind(accessibilityProvider);218} else {219this.getRole = _ => 'listitem';220}221222if (accessibilityProvider?.isChecked) {223this.isChecked = accessibilityProvider.isChecked.bind(accessibilityProvider);224} else {225this.isChecked = _ => undefined;226}227}228}229230export interface IListView<T> extends ISpliceable<T>, IDisposable {231readonly domId: string;232readonly domNode: HTMLElement;233readonly containerDomNode: HTMLElement;234readonly scrollableElementDomNode: HTMLElement;235readonly length: number;236readonly contentHeight: number;237readonly contentWidth: number;238readonly onDidChangeContentHeight: Event<number>;239readonly onDidChangeContentWidth: Event<number>;240readonly renderHeight: number;241readonly scrollHeight: number;242readonly firstVisibleIndex: number;243readonly firstMostlyVisibleIndex: number;244readonly lastVisibleIndex: number;245onDidScroll: Event<ScrollEvent>;246onWillScroll: Event<ScrollEvent>;247onMouseClick: Event<IListMouseEvent<T>>;248onMouseDblClick: Event<IListMouseEvent<T>>;249onMouseMiddleClick: Event<IListMouseEvent<T>>;250onMouseUp: Event<IListMouseEvent<T>>;251onMouseDown: Event<IListMouseEvent<T>>;252onMouseOver: Event<IListMouseEvent<T>>;253onMouseMove: Event<IListMouseEvent<T>>;254onMouseOut: Event<IListMouseEvent<T>>;255onContextMenu: Event<IListMouseEvent<T>>;256onTouchStart: Event<IListTouchEvent<T>>;257onTap: Event<IListGestureEvent<T>>;258element(index: number): T;259domElement(index: number): HTMLElement | null;260getElementDomId(index: number): string;261elementHeight(index: number): number;262elementTop(index: number): number;263indexOf(element: T): number;264indexAt(position: number): number;265indexAfter(position: number): number;266updateOptions(options: IListViewOptionsUpdate): void;267getScrollTop(): number;268setScrollTop(scrollTop: number, reuseAnimation?: boolean): void;269getScrollLeft(): number;270setScrollLeft(scrollLeft: number): void;271delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void;272delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void;273updateWidth(index: number): void;274updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void;275rerender(): void;276layout(height?: number, width?: number): void;277}278279/**280* The {@link ListView} is a virtual scrolling engine.281*282* Given that it only renders elements within its viewport, it can hold large283* collections of elements and stay very performant. The performance bottleneck284* usually lies within the user's rendering code for each element.285*286* @remarks It is a low-level widget, not meant to be used directly. Refer to the287* List widget instead.288*/289export class ListView<T> implements IListView<T> {290291private static InstanceCount = 0;292readonly domId = `list_id_${++ListView.InstanceCount}`;293294readonly domNode: HTMLElement;295296private items: IItem<T>[];297private itemId: number;298protected rangeMap: IRangeMap;299private cache: RowCache<T>;300private renderers = new Map<string, IListRenderer<any /* TODO@joao */, any>>();301protected lastRenderTop: number;302protected lastRenderHeight: number;303private renderWidth = 0;304private rowsContainer: HTMLElement;305private scrollable: Scrollable;306private scrollableElement: SmoothScrollableElement;307private _scrollHeight: number = 0;308private scrollableElementUpdateDisposable: IDisposable | null = null;309private scrollableElementWidthDelayer = new Delayer<void>(50);310private splicing = false;311private dragOverAnimationDisposable: IDisposable | undefined;312private dragOverAnimationStopDisposable: IDisposable = Disposable.None;313private dragOverMouseY: number = 0;314private setRowLineHeight: boolean;315private setRowHeight: boolean;316private supportDynamicHeights: boolean;317private paddingBottom: number;318private accessibilityProvider: ListViewAccessibilityProvider<T>;319private scrollWidth: number | undefined;320321private dnd: IListViewDragAndDrop<T>;322private canDrop: boolean = false;323private currentDragData: IDragAndDropData | undefined;324private currentDragFeedback: number[] | undefined;325private currentDragFeedbackPosition: ListDragOverEffectPosition | undefined;326private currentDragFeedbackDisposable: IDisposable = Disposable.None;327private onDragLeaveTimeout: IDisposable = Disposable.None;328private currentSelectionDisposable: IDisposable = Disposable.None;329private currentSelectionBounds: IRange | undefined;330private activeElement: HTMLElement | undefined;331332private readonly disposables: DisposableStore = new DisposableStore();333334private readonly _onDidChangeContentHeight = new Emitter<number>();335private readonly _onDidChangeContentWidth = new Emitter<number>();336readonly onDidChangeContentHeight: Event<number> = Event.latch(this._onDidChangeContentHeight.event, undefined, this.disposables);337readonly onDidChangeContentWidth: Event<number> = Event.latch(this._onDidChangeContentWidth.event, undefined, this.disposables);338get contentHeight(): number { return this.rangeMap.size; }339get contentWidth(): number { return this.scrollWidth ?? 0; }340341get onDidScroll(): Event<ScrollEvent> { return this.scrollableElement.onScroll; }342get onWillScroll(): Event<ScrollEvent> { return this.scrollableElement.onWillScroll; }343get containerDomNode(): HTMLElement { return this.rowsContainer; }344get scrollableElementDomNode(): HTMLElement { return this.scrollableElement.getDomNode(); }345346private _horizontalScrolling: boolean = false;347private get horizontalScrolling(): boolean { return this._horizontalScrolling; }348private set horizontalScrolling(value: boolean) {349if (value === this._horizontalScrolling) {350return;351}352353if (value && this.supportDynamicHeights) {354throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously');355}356357this._horizontalScrolling = value;358this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling);359360if (this._horizontalScrolling) {361for (const item of this.items) {362this.measureItemWidth(item);363}364365this.updateScrollWidth();366this.scrollableElement.setScrollDimensions({ width: getContentWidth(this.domNode) });367this.rowsContainer.style.width = `${Math.max(this.scrollWidth || 0, this.renderWidth)}px`;368} else {369this.scrollableElementWidthDelayer.cancel();370this.scrollableElement.setScrollDimensions({ width: this.renderWidth, scrollWidth: this.renderWidth });371this.rowsContainer.style.width = '';372}373}374375constructor(376container: HTMLElement,377private virtualDelegate: IListVirtualDelegate<T>,378renderers: IListRenderer<any /* TODO@joao */, any>[],379options: IListViewOptions<T> = DefaultOptions380) {381if (options.horizontalScrolling && options.supportDynamicHeights) {382throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously');383}384385this.items = [];386this.itemId = 0;387this.rangeMap = this.createRangeMap(options.paddingTop ?? 0);388389for (const renderer of renderers) {390this.renderers.set(renderer.templateId, renderer);391}392393this.cache = this.disposables.add(new RowCache(this.renderers));394395this.lastRenderTop = 0;396this.lastRenderHeight = 0;397398this.domNode = document.createElement('div');399this.domNode.className = 'monaco-list';400401this.domNode.classList.add(this.domId);402this.domNode.tabIndex = 0;403404this.domNode.classList.toggle('mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true);405406this._horizontalScrolling = options.horizontalScrolling ?? DefaultOptions.horizontalScrolling;407this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling);408409this.paddingBottom = typeof options.paddingBottom === 'undefined' ? 0 : options.paddingBottom;410411this.accessibilityProvider = new ListViewAccessibilityProvider(options.accessibilityProvider);412413this.rowsContainer = document.createElement('div');414this.rowsContainer.className = 'monaco-list-rows';415416const transformOptimization = options.transformOptimization ?? DefaultOptions.transformOptimization;417if (transformOptimization) {418this.rowsContainer.style.transform = 'translate3d(0px, 0px, 0px)';419this.rowsContainer.style.overflow = 'hidden';420this.rowsContainer.style.contain = 'strict';421}422423this.disposables.add(Gesture.addTarget(this.rowsContainer));424425this.scrollable = this.disposables.add(new Scrollable({426forceIntegerValues: true,427smoothScrollDuration: (options.smoothScrolling ?? false) ? 125 : 0,428scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(getWindow(this.domNode), cb)429}));430this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, {431alwaysConsumeMouseWheel: options.alwaysConsumeMouseWheel ?? DefaultOptions.alwaysConsumeMouseWheel,432horizontal: ScrollbarVisibility.Auto,433vertical: options.verticalScrollMode ?? DefaultOptions.verticalScrollMode,434useShadows: options.useShadows ?? DefaultOptions.useShadows,435mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity,436fastScrollSensitivity: options.fastScrollSensitivity,437scrollByPage: options.scrollByPage438}, this.scrollable));439440this.domNode.appendChild(this.scrollableElement.getDomNode());441container.appendChild(this.domNode);442443this.scrollableElement.onScroll(this.onScroll, this, this.disposables);444this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e as GestureEvent)));445446this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => {447// Make sure the active element is scrolled into view448const element = (e.target as HTMLElement);449const scrollValue = element.scrollTop;450element.scrollTop = 0;451if (options.scrollToActiveElement) {452this.setScrollTop(this.scrollTop + scrollValue);453}454}));455456this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e))));457this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e))));458this.disposables.add(addDisposableListener(this.domNode, 'dragleave', e => this.onDragLeave(this.toDragEvent(e))));459this.disposables.add(addDisposableListener(this.domNode, 'dragend', e => this.onDragEnd(e)));460if (options.userSelection) {461if (options.dnd) {462throw new Error('DND and user selection cannot be used simultaneously');463}464this.disposables.add(addDisposableListener(this.domNode, 'mousedown', e => this.onPotentialSelectionStart(e)));465}466467this.setRowLineHeight = options.setRowLineHeight ?? DefaultOptions.setRowLineHeight;468this.setRowHeight = options.setRowHeight ?? DefaultOptions.setRowHeight;469this.supportDynamicHeights = options.supportDynamicHeights ?? DefaultOptions.supportDynamicHeights;470this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd);471472this.layout(options.initialSize?.height, options.initialSize?.width);473if (options.scrollToActiveElement) {474this._setupFocusObserver(container);475}476}477478private _setupFocusObserver(container: HTMLElement): void {479this.disposables.add(addDisposableListener(container, 'focus', () => {480const element = getActiveElement() as HTMLElement | null;481if (this.activeElement !== element && element !== null) {482this.activeElement = element;483this._scrollToActiveElement(this.activeElement, container);484}485}, true));486}487488private _scrollToActiveElement(element: HTMLElement, container: HTMLElement) {489// The scroll event on the list only fires when scrolling down.490// If the active element is above the viewport, we need to scroll up.491const containerRect = container.getBoundingClientRect();492const elementRect = element.getBoundingClientRect();493494const topOffset = elementRect.top - containerRect.top;495496if (topOffset < 0) {497// Scroll up498this.setScrollTop(this.scrollTop + topOffset);499}500}501502updateOptions(options: IListViewOptionsUpdate) {503if (options.paddingBottom !== undefined) {504this.paddingBottom = options.paddingBottom;505this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });506}507508if (options.smoothScrolling !== undefined) {509this.scrollable.setSmoothScrollDuration(options.smoothScrolling ? 125 : 0);510}511512if (options.horizontalScrolling !== undefined) {513this.horizontalScrolling = options.horizontalScrolling;514}515516let scrollableOptions: ScrollableElementChangeOptions | undefined;517518if (options.scrollByPage !== undefined) {519scrollableOptions = { ...(scrollableOptions ?? {}), scrollByPage: options.scrollByPage };520}521522if (options.mouseWheelScrollSensitivity !== undefined) {523scrollableOptions = { ...(scrollableOptions ?? {}), mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity };524}525526if (options.fastScrollSensitivity !== undefined) {527scrollableOptions = { ...(scrollableOptions ?? {}), fastScrollSensitivity: options.fastScrollSensitivity };528}529530if (scrollableOptions) {531this.scrollableElement.updateOptions(scrollableOptions);532}533534if (options.paddingTop !== undefined && options.paddingTop !== this.rangeMap.paddingTop) {535// trigger a rerender536const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);537const offset = options.paddingTop - this.rangeMap.paddingTop;538this.rangeMap.paddingTop = options.paddingTop;539540this.render(lastRenderRange, Math.max(0, this.lastRenderTop + offset), this.lastRenderHeight, undefined, undefined, true);541this.setScrollTop(this.lastRenderTop);542543this.eventuallyUpdateScrollDimensions();544545if (this.supportDynamicHeights) {546this._rerender(this.lastRenderTop, this.lastRenderHeight);547}548}549}550551delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {552this.scrollableElement.delegateScrollFromMouseWheelEvent(browserEvent);553}554555delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent) {556this.scrollableElement.delegateVerticalScrollbarPointerDown(browserEvent);557}558559updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void {560if (index < 0 || index >= this.items.length) {561return;562}563564const originalSize = this.items[index].size;565566if (typeof size === 'undefined') {567if (!this.supportDynamicHeights) {568console.warn('Dynamic heights not supported', new Error().stack);569return;570}571572this.items[index].lastDynamicHeightWidth = undefined;573size = originalSize + this.probeDynamicHeight(index);574}575576if (originalSize === size) {577return;578}579580const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);581582let heightDiff = 0;583584if (index < lastRenderRange.start) {585// do not scroll the viewport if resized element is out of viewport586heightDiff = size - originalSize;587} else {588if (anchorIndex !== null && anchorIndex > index && anchorIndex < lastRenderRange.end) {589// anchor in viewport590// resized element in viewport and above the anchor591heightDiff = size - originalSize;592} else {593heightDiff = 0;594}595}596597this.rangeMap.splice(index, 1, [{ size: size }]);598this.items[index].size = size;599600this.render(lastRenderRange, Math.max(0, this.lastRenderTop + heightDiff), this.lastRenderHeight, undefined, undefined, true);601this.setScrollTop(this.lastRenderTop);602603this.eventuallyUpdateScrollDimensions();604605if (this.supportDynamicHeights) {606this._rerender(this.lastRenderTop, this.lastRenderHeight);607} else {608this._onDidChangeContentHeight.fire(this.contentHeight); // otherwise fired in _rerender()609}610}611612protected createRangeMap(paddingTop: number): IRangeMap {613return new RangeMap(paddingTop);614}615616splice(start: number, deleteCount: number, elements: readonly T[] = []): T[] {617if (this.splicing) {618throw new Error('Can\'t run recursive splices.');619}620621this.splicing = true;622623try {624return this._splice(start, deleteCount, elements);625} finally {626this.splicing = false;627this._onDidChangeContentHeight.fire(this.contentHeight);628}629}630631private _splice(start: number, deleteCount: number, elements: readonly T[] = []): T[] {632const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);633const deleteRange = { start, end: start + deleteCount };634const removeRange = Range.intersect(previousRenderRange, deleteRange);635636// try to reuse rows, avoid removing them from DOM637const rowsToDispose = new Map<string, IRow[]>();638for (let i = removeRange.end - 1; i >= removeRange.start; i--) {639const item = this.items[i];640item.dragStartDisposable.dispose();641item.checkedDisposable.dispose();642643if (item.row) {644let rows = rowsToDispose.get(item.templateId);645646if (!rows) {647rows = [];648rowsToDispose.set(item.templateId, rows);649}650651const renderer = this.renderers.get(item.templateId);652653if (renderer && renderer.disposeElement) {654renderer.disposeElement(item.element, i, item.row.templateData, { height: item.size });655}656657rows.unshift(item.row);658}659660item.row = null;661item.stale = true;662}663664const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };665const previousRenderedRestRange = Range.intersect(previousRestRange, previousRenderRange);666const previousUnrenderedRestRanges = Range.relativeComplement(previousRestRange, previousRenderRange);667668const inserted = elements.map<IItem<T>>(element => ({669id: String(this.itemId++),670element,671templateId: this.virtualDelegate.getTemplateId(element),672size: this.virtualDelegate.getHeight(element),673width: undefined,674hasDynamicHeight: !!this.virtualDelegate.hasDynamicHeight && this.virtualDelegate.hasDynamicHeight(element),675lastDynamicHeightWidth: undefined,676row: null,677uri: undefined,678dropTarget: false,679dragStartDisposable: Disposable.None,680checkedDisposable: Disposable.None,681stale: false682}));683684let deleted: IItem<T>[];685686// TODO@joao: improve this optimization to catch even more cases687if (start === 0 && deleteCount >= this.items.length) {688this.rangeMap = this.createRangeMap(this.rangeMap.paddingTop);689this.rangeMap.splice(0, 0, inserted);690deleted = this.items;691this.items = inserted;692} else {693this.rangeMap.splice(start, deleteCount, inserted);694deleted = splice(this.items, start, deleteCount, inserted);695}696697const delta = elements.length - deleteCount;698const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);699const renderedRestRange = shift(previousRenderedRestRange, delta);700const updateRange = Range.intersect(renderRange, renderedRestRange);701702for (let i = updateRange.start; i < updateRange.end; i++) {703this.updateItemInDOM(this.items[i], i);704}705706const removeRanges = Range.relativeComplement(renderedRestRange, renderRange);707708for (const range of removeRanges) {709for (let i = range.start; i < range.end; i++) {710this.removeItemFromDOM(i);711}712}713714const unrenderedRestRanges = previousUnrenderedRestRanges.map(r => shift(r, delta));715const elementsRange = { start, end: start + elements.length };716const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => Range.intersect(renderRange, r)).reverse();717718for (const range of insertRanges) {719for (let i = range.end - 1; i >= range.start; i--) {720const item = this.items[i];721const rows = rowsToDispose.get(item.templateId);722const row = rows?.pop();723this.insertItemInDOM(i, row);724}725}726727for (const rows of rowsToDispose.values()) {728for (const row of rows) {729this.cache.release(row);730}731}732733this.eventuallyUpdateScrollDimensions();734735if (this.supportDynamicHeights) {736this._rerender(this.scrollTop, this.renderHeight);737}738739return deleted.map(i => i.element);740}741742protected eventuallyUpdateScrollDimensions(): void {743this._scrollHeight = this.contentHeight;744this.rowsContainer.style.height = `${this._scrollHeight}px`;745746if (!this.scrollableElementUpdateDisposable) {747this.scrollableElementUpdateDisposable = scheduleAtNextAnimationFrame(getWindow(this.domNode), () => {748this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });749this.updateScrollWidth();750this.scrollableElementUpdateDisposable = null;751});752}753}754755private eventuallyUpdateScrollWidth(): void {756if (!this.horizontalScrolling) {757this.scrollableElementWidthDelayer.cancel();758return;759}760761this.scrollableElementWidthDelayer.trigger(() => this.updateScrollWidth());762}763764private updateScrollWidth(): void {765if (!this.horizontalScrolling) {766return;767}768769let scrollWidth = 0;770771for (const item of this.items) {772if (typeof item.width !== 'undefined') {773scrollWidth = Math.max(scrollWidth, item.width);774}775}776777this.scrollWidth = scrollWidth;778this.scrollableElement.setScrollDimensions({ scrollWidth: scrollWidth === 0 ? 0 : (scrollWidth + 10) });779this._onDidChangeContentWidth.fire(this.scrollWidth);780}781782updateWidth(index: number): void {783if (!this.horizontalScrolling || typeof this.scrollWidth === 'undefined') {784return;785}786787const item = this.items[index];788this.measureItemWidth(item);789790if (typeof item.width !== 'undefined' && item.width > this.scrollWidth) {791this.scrollWidth = item.width;792this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth + 10 });793this._onDidChangeContentWidth.fire(this.scrollWidth);794}795}796797rerender(): void {798if (!this.supportDynamicHeights) {799return;800}801802for (const item of this.items) {803item.lastDynamicHeightWidth = undefined;804}805806this._rerender(this.lastRenderTop, this.lastRenderHeight);807}808809get length(): number {810return this.items.length;811}812813get renderHeight(): number {814const scrollDimensions = this.scrollableElement.getScrollDimensions();815return scrollDimensions.height;816}817818get firstVisibleIndex(): number {819const range = this.getVisibleRange(this.lastRenderTop, this.lastRenderHeight);820return range.start;821}822823get firstMostlyVisibleIndex(): number {824const firstVisibleIndex = this.firstVisibleIndex;825const firstElTop = this.rangeMap.positionAt(firstVisibleIndex);826const nextElTop = this.rangeMap.positionAt(firstVisibleIndex + 1);827if (nextElTop !== -1) {828const firstElMidpoint = (nextElTop - firstElTop) / 2 + firstElTop;829if (firstElMidpoint < this.scrollTop) {830return firstVisibleIndex + 1;831}832}833834return firstVisibleIndex;835}836837get lastVisibleIndex(): number {838const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);839return range.end - 1;840}841842element(index: number): T {843return this.items[index].element;844}845846indexOf(element: T): number {847return this.items.findIndex(item => item.element === element);848}849850domElement(index: number): HTMLElement | null {851const row = this.items[index].row;852return row && row.domNode;853}854855elementHeight(index: number): number {856return this.items[index].size;857}858859elementTop(index: number): number {860return this.rangeMap.positionAt(index);861}862863indexAt(position: number): number {864return this.rangeMap.indexAt(position);865}866867indexAfter(position: number): number {868return this.rangeMap.indexAfter(position);869}870871layout(height?: number, width?: number): void {872const scrollDimensions: INewScrollDimensions = {873height: typeof height === 'number' ? height : getContentHeight(this.domNode)874};875876if (this.scrollableElementUpdateDisposable) {877this.scrollableElementUpdateDisposable.dispose();878this.scrollableElementUpdateDisposable = null;879scrollDimensions.scrollHeight = this.scrollHeight;880}881882this.scrollableElement.setScrollDimensions(scrollDimensions);883884if (typeof width !== 'undefined') {885this.renderWidth = width;886887if (this.supportDynamicHeights) {888this._rerender(this.scrollTop, this.renderHeight);889}890}891892if (this.horizontalScrolling) {893this.scrollableElement.setScrollDimensions({894width: typeof width === 'number' ? width : getContentWidth(this.domNode)895});896}897}898899// Render900901protected render(previousRenderRange: IRange, renderTop: number, renderHeight: number, renderLeft: number | undefined, scrollWidth: number | undefined, updateItemsInDOM: boolean = false, onScroll: boolean = false): void {902const renderRange = this.getRenderRange(renderTop, renderHeight);903904const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange).reverse();905const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange);906907if (updateItemsInDOM) {908const rangesToUpdate = Range.intersect(previousRenderRange, renderRange);909910for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) {911this.updateItemInDOM(this.items[i], i);912}913}914915this.cache.transact(() => {916for (const range of rangesToRemove) {917for (let i = range.start; i < range.end; i++) {918this.removeItemFromDOM(i, onScroll);919}920}921922for (const range of rangesToInsert) {923for (let i = range.end - 1; i >= range.start; i--) {924this.insertItemInDOM(i);925}926}927});928929if (renderLeft !== undefined) {930this.rowsContainer.style.left = `-${renderLeft}px`;931}932933this.rowsContainer.style.top = `-${renderTop}px`;934935if (this.horizontalScrolling && scrollWidth !== undefined) {936this.rowsContainer.style.width = `${Math.max(scrollWidth, this.renderWidth)}px`;937}938939this.lastRenderTop = renderTop;940this.lastRenderHeight = renderHeight;941}942943// DOM operations944945private insertItemInDOM(index: number, row?: IRow): void {946const item = this.items[index];947948if (!item.row) {949if (row) {950item.row = row;951item.stale = true;952} else {953const result = this.cache.alloc(item.templateId);954item.row = result.row;955item.stale ||= result.isReusingConnectedDomNode;956}957}958959const role = this.accessibilityProvider.getRole(item.element) || 'listitem';960item.row.domNode.setAttribute('role', role);961962const checked = this.accessibilityProvider.isChecked(item.element);963const toAriaState = (value: CheckBoxAccessibleState) => value === 'mixed' ? 'mixed' : String(!!value);964965if (typeof checked === 'boolean' || checked === 'mixed') {966item.row.domNode.setAttribute('aria-checked', toAriaState(checked));967} else if (checked) {968const update = (value: CheckBoxAccessibleState) => item.row!.domNode.setAttribute('aria-checked', toAriaState(value));969update(checked.value);970item.checkedDisposable = checked.onDidChange(() => update(checked.value));971}972973if (item.stale || !item.row.domNode.parentElement) {974const referenceNode = this.items.at(index + 1)?.row?.domNode ?? null;975if (item.row.domNode.parentElement !== this.rowsContainer || item.row.domNode.nextElementSibling !== referenceNode) {976this.rowsContainer.insertBefore(item.row.domNode, referenceNode);977}978item.stale = false;979}980981this.updateItemInDOM(item, index);982983const renderer = this.renderers.get(item.templateId);984985if (!renderer) {986throw new Error(`No renderer found for template id ${item.templateId}`);987}988989renderer?.renderElement(item.element, index, item.row.templateData, { height: item.size });990991const uri = this.dnd.getDragURI(item.element);992item.dragStartDisposable.dispose();993item.row.domNode.draggable = !!uri;994995if (uri) {996item.dragStartDisposable = addDisposableListener(item.row.domNode, 'dragstart', event => this.onDragStart(item.element, uri, event));997}998999if (this.horizontalScrolling) {1000this.measureItemWidth(item);1001this.eventuallyUpdateScrollWidth();1002}1003}10041005private measureItemWidth(item: IItem<T>): void {1006if (!item.row || !item.row.domNode) {1007return;1008}10091010item.row.domNode.style.width = 'fit-content';1011item.width = getContentWidth(item.row.domNode);1012const style = getWindow(item.row.domNode).getComputedStyle(item.row.domNode);10131014if (style.paddingLeft) {1015item.width += parseFloat(style.paddingLeft);1016}10171018if (style.paddingRight) {1019item.width += parseFloat(style.paddingRight);1020}10211022item.row.domNode.style.width = '';1023}10241025private updateItemInDOM(item: IItem<T>, index: number): void {1026item.row!.domNode.style.top = `${this.elementTop(index)}px`;10271028if (this.setRowHeight) {1029item.row!.domNode.style.height = `${item.size}px`;1030}10311032if (this.setRowLineHeight) {1033item.row!.domNode.style.lineHeight = `${item.size}px`;1034}10351036item.row!.domNode.setAttribute('data-index', `${index}`);1037item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');1038item.row!.domNode.setAttribute('data-parity', index % 2 === 0 ? 'even' : 'odd');1039item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));1040item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));1041item.row!.domNode.setAttribute('id', this.getElementDomId(index));10421043item.row!.domNode.classList.toggle('drop-target', item.dropTarget);1044}10451046private removeItemFromDOM(index: number, onScroll?: boolean): void {1047const item = this.items[index];1048item.dragStartDisposable.dispose();1049item.checkedDisposable.dispose();10501051if (item.row) {1052const renderer = this.renderers.get(item.templateId);10531054if (renderer && renderer.disposeElement) {1055renderer.disposeElement(item.element, index, item.row.templateData, { height: item.size, onScroll });1056}10571058this.cache.release(item.row);1059item.row = null;1060}10611062if (this.horizontalScrolling) {1063this.eventuallyUpdateScrollWidth();1064}1065}10661067getScrollTop(): number {1068const scrollPosition = this.scrollableElement.getScrollPosition();1069return scrollPosition.scrollTop;1070}10711072setScrollTop(scrollTop: number, reuseAnimation?: boolean): void {1073if (this.scrollableElementUpdateDisposable) {1074this.scrollableElementUpdateDisposable.dispose();1075this.scrollableElementUpdateDisposable = null;1076this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });1077}10781079this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation });1080}10811082getScrollLeft(): number {1083const scrollPosition = this.scrollableElement.getScrollPosition();1084return scrollPosition.scrollLeft;1085}10861087setScrollLeft(scrollLeft: number): void {1088if (this.scrollableElementUpdateDisposable) {1089this.scrollableElementUpdateDisposable.dispose();1090this.scrollableElementUpdateDisposable = null;1091this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth });1092}10931094this.scrollableElement.setScrollPosition({ scrollLeft });1095}109610971098get scrollTop(): number {1099return this.getScrollTop();1100}11011102set scrollTop(scrollTop: number) {1103this.setScrollTop(scrollTop);1104}11051106get scrollHeight(): number {1107return this._scrollHeight + (this.horizontalScrolling ? 10 : 0) + this.paddingBottom;1108}11091110// Events11111112@memoize get onMouseClick(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'click')).event, e => this.toMouseEvent(e), this.disposables); }1113@memoize get onMouseDblClick(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'dblclick')).event, e => this.toMouseEvent(e), this.disposables); }1114@memoize get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return Event.filter(Event.map(this.disposables.add(new DomEmitter(this.domNode, 'auxclick')).event, e => this.toMouseEvent(e as MouseEvent), this.disposables), e => e.browserEvent.button === 1, this.disposables); }1115@memoize get onMouseUp(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseup')).event, e => this.toMouseEvent(e), this.disposables); }1116@memoize get onMouseDown(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mousedown')).event, e => this.toMouseEvent(e), this.disposables); }1117@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseover')).event, e => this.toMouseEvent(e), this.disposables); }1118@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mousemove')).event, e => this.toMouseEvent(e), this.disposables); }1119@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseout')).event, e => this.toMouseEvent(e), this.disposables); }1120@memoize get onContextMenu(): Event<IListMouseEvent<T> | IListGestureEvent<T>> { return Event.any<IListMouseEvent<any> | IListGestureEvent<any>>(Event.map(this.disposables.add(new DomEmitter(this.domNode, 'contextmenu')).event, e => this.toMouseEvent(e), this.disposables), Event.map(this.disposables.add(new DomEmitter(this.domNode, TouchEventType.Contextmenu)).event, e => this.toGestureEvent(e), this.disposables)); }1121@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'touchstart')).event, e => this.toTouchEvent(e), this.disposables); }1122@memoize get onTap(): Event<IListGestureEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.rowsContainer, TouchEventType.Tap)).event, e => this.toGestureEvent(e), this.disposables); }11231124private toMouseEvent(browserEvent: MouseEvent): IListMouseEvent<T> {1125const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1126const item = typeof index === 'undefined' ? undefined : this.items[index];1127const element = item && item.element;1128return { browserEvent, index, element };1129}11301131private toTouchEvent(browserEvent: TouchEvent): IListTouchEvent<T> {1132const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1133const item = typeof index === 'undefined' ? undefined : this.items[index];1134const element = item && item.element;1135return { browserEvent, index, element };1136}11371138private toGestureEvent(browserEvent: GestureEvent): IListGestureEvent<T> {1139const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget || null);1140const item = typeof index === 'undefined' ? undefined : this.items[index];1141const element = item && item.element;1142return { browserEvent, index, element };1143}11441145private toDragEvent(browserEvent: DragEvent): IListDragEvent<T> {1146const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1147const item = typeof index === 'undefined' ? undefined : this.items[index];1148const element = item && item.element;1149const sector = this.getTargetSector(browserEvent, index);1150return { browserEvent, index, element, sector };1151}11521153private onScroll(e: ScrollEvent): void {1154try {1155const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);1156this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth, undefined, true);11571158if (this.supportDynamicHeights) {1159this._rerender(e.scrollTop, e.height, e.inSmoothScrolling);1160}1161} catch (err) {1162console.error('Got bad scroll event:', e);1163throw err;1164}1165}11661167private onTouchChange(event: GestureEvent): void {1168event.preventDefault();1169event.stopPropagation();11701171this.scrollTop -= event.translationY;1172}11731174// DND11751176private onDragStart(element: T, uri: string, event: DragEvent): void {1177if (!event.dataTransfer) {1178return;1179}11801181const elements = this.dnd.getDragElements(element);11821183event.dataTransfer.effectAllowed = 'copyMove';1184event.dataTransfer.setData(DataTransfers.TEXT, uri);11851186let label: string | undefined;1187if (this.dnd.getDragLabel) {1188label = this.dnd.getDragLabel(elements, event);1189}1190if (typeof label === 'undefined') {1191label = String(elements.length);1192}11931194applyDragImage(event, this.domNode, label, [this.domId /* add domId to get list specific styling */]);11951196this.domNode.classList.add('dragging');1197this.currentDragData = new ElementsDragAndDropData(elements);1198StaticDND.CurrentDragAndDropData = new ExternalElementsDragAndDropData(elements);11991200this.dnd.onDragStart?.(this.currentDragData, event);1201}12021203private onPotentialSelectionStart(e: MouseEvent) {1204this.currentSelectionDisposable.dispose();1205const doc = getDocument(this.domNode);12061207// Set up both the 'movement store' for watching the mouse, and the1208// 'selection store' which lasts as long as there's a selection, even1209// after the usr has stopped modifying it.1210const selectionStore = this.currentSelectionDisposable = new DisposableStore();1211const movementStore = selectionStore.add(new DisposableStore());12121213// The selection events we get from the DOM are fairly limited and we lack a 'selection end' event.1214// Selection events also don't tell us where the input doing the selection is. So, make a poor1215// assumption that a user is using the mouse, and base our events on that.1216movementStore.add(addDisposableListener(this.domNode, 'selectstart', () => {1217movementStore.add(addDisposableListener(doc, 'mousemove', e => {1218if (doc.getSelection()?.isCollapsed === false) {1219this.setupDragAndDropScrollTopAnimation(e);1220}1221}));12221223// The selection is cleared either on mouseup if there's no selection, or on next mousedown1224// when `this.currentSelectionDisposable` is reset.1225selectionStore.add(toDisposable(() => {1226const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);1227this.currentSelectionBounds = undefined;1228this.render(previousRenderRange, this.lastRenderTop, this.lastRenderHeight, undefined, undefined);1229}));1230selectionStore.add(addDisposableListener(doc, 'selectionchange', () => {1231const selection = doc.getSelection();1232// if the selection changed _after_ mouseup, it's from clearing the list or similar, so teardown1233if (!selection || selection.isCollapsed) {1234if (movementStore.isDisposed) {1235selectionStore.dispose();1236}1237return;1238}12391240let start = this.getIndexOfListElement(selection.anchorNode as HTMLElement);1241let end = this.getIndexOfListElement(selection.focusNode as HTMLElement);1242if (start !== undefined && end !== undefined) {1243if (end < start) {1244[start, end] = [end, start];1245}1246this.currentSelectionBounds = { start, end };1247}1248}));1249}));12501251movementStore.add(addDisposableListener(doc, 'mouseup', () => {1252movementStore.dispose();1253this.teardownDragAndDropScrollTopAnimation();12541255if (doc.getSelection()?.isCollapsed !== false) {1256selectionStore.dispose();1257}1258}));1259}12601261private getIndexOfListElement(element: HTMLElement | null): number | undefined {1262if (!element || !this.domNode.contains(element)) {1263return undefined;1264}12651266while (element && element !== this.domNode) {1267if (element.dataset?.index) {1268return Number(element.dataset.index);1269}12701271element = element.parentElement;1272}12731274return undefined;1275}12761277private onDragOver(event: IListDragEvent<T>): boolean {1278event.browserEvent.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)12791280this.onDragLeaveTimeout.dispose();12811282if (StaticDND.CurrentDragAndDropData && StaticDND.CurrentDragAndDropData.getData() === 'vscode-ui') {1283return false;1284}12851286this.setupDragAndDropScrollTopAnimation(event.browserEvent);12871288if (!event.browserEvent.dataTransfer) {1289return false;1290}12911292// Drag over from outside1293if (!this.currentDragData) {1294if (StaticDND.CurrentDragAndDropData) {1295// Drag over from another list1296this.currentDragData = StaticDND.CurrentDragAndDropData;12971298} else {1299// Drag over from the desktop1300if (!event.browserEvent.dataTransfer.types) {1301return false;1302}13031304this.currentDragData = new NativeDragAndDropData();1305}1306}13071308const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.sector, event.browserEvent);1309this.canDrop = typeof result === 'boolean' ? result : result.accept;13101311if (!this.canDrop) {1312this.currentDragFeedback = undefined;1313this.currentDragFeedbackDisposable.dispose();1314return false;1315}13161317event.browserEvent.dataTransfer.dropEffect = (typeof result !== 'boolean' && result.effect?.type === ListDragOverEffectType.Copy) ? 'copy' : 'move';13181319let feedback: number[];13201321if (typeof result !== 'boolean' && result.feedback) {1322feedback = result.feedback;1323} else {1324if (typeof event.index === 'undefined') {1325feedback = [-1];1326} else {1327feedback = [event.index];1328}1329}13301331// sanitize feedback list1332feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort((a, b) => a - b);1333feedback = feedback[0] === -1 ? [-1] : feedback;13341335let dragOverEffectPosition = typeof result !== 'boolean' && result.effect && result.effect.position ? result.effect.position : ListDragOverEffectPosition.Over;13361337if (equalsDragFeedback(this.currentDragFeedback, feedback) && this.currentDragFeedbackPosition === dragOverEffectPosition) {1338return true;1339}13401341this.currentDragFeedback = feedback;1342this.currentDragFeedbackPosition = dragOverEffectPosition;1343this.currentDragFeedbackDisposable.dispose();13441345if (feedback[0] === -1) { // entire list feedback1346this.domNode.classList.add(dragOverEffectPosition);1347this.rowsContainer.classList.add(dragOverEffectPosition);1348this.currentDragFeedbackDisposable = toDisposable(() => {1349this.domNode.classList.remove(dragOverEffectPosition);1350this.rowsContainer.classList.remove(dragOverEffectPosition);1351});1352} else {13531354if (feedback.length > 1 && dragOverEffectPosition !== ListDragOverEffectPosition.Over) {1355throw new Error('Can\'t use multiple feedbacks with position different than \'over\'');1356}13571358// Make sure there is no flicker when moving between two items1359// Always use the before feedback if possible1360if (dragOverEffectPosition === ListDragOverEffectPosition.After) {1361if (feedback[0] < this.length - 1) {1362feedback[0] += 1;1363dragOverEffectPosition = ListDragOverEffectPosition.Before;1364}1365}13661367for (const index of feedback) {1368const item = this.items[index];1369item.dropTarget = true;13701371item.row?.domNode.classList.add(dragOverEffectPosition);1372}13731374this.currentDragFeedbackDisposable = toDisposable(() => {1375for (const index of feedback) {1376const item = this.items[index];1377item.dropTarget = false;13781379item.row?.domNode.classList.remove(dragOverEffectPosition);1380}1381});1382}13831384return true;1385}13861387private onDragLeave(event: IListDragEvent<T>): void {1388this.onDragLeaveTimeout.dispose();1389this.onDragLeaveTimeout = disposableTimeout(() => this.clearDragOverFeedback(), 100, this.disposables);1390if (this.currentDragData) {1391this.dnd.onDragLeave?.(this.currentDragData, event.element, event.index, event.browserEvent);1392}1393}13941395private onDrop(event: IListDragEvent<T>): void {1396if (!this.canDrop) {1397return;1398}13991400const dragData = this.currentDragData;1401this.teardownDragAndDropScrollTopAnimation();1402this.clearDragOverFeedback();1403this.domNode.classList.remove('dragging');1404this.currentDragData = undefined;1405StaticDND.CurrentDragAndDropData = undefined;14061407if (!dragData || !event.browserEvent.dataTransfer) {1408return;1409}14101411event.browserEvent.preventDefault();1412dragData.update(event.browserEvent.dataTransfer);1413this.dnd.drop(dragData, event.element, event.index, event.sector, event.browserEvent);1414}14151416private onDragEnd(event: DragEvent): void {1417this.canDrop = false;1418this.teardownDragAndDropScrollTopAnimation();1419this.clearDragOverFeedback();1420this.domNode.classList.remove('dragging');1421this.currentDragData = undefined;1422StaticDND.CurrentDragAndDropData = undefined;14231424this.dnd.onDragEnd?.(event);1425}14261427private clearDragOverFeedback(): void {1428this.currentDragFeedback = undefined;1429this.currentDragFeedbackPosition = undefined;1430this.currentDragFeedbackDisposable.dispose();1431this.currentDragFeedbackDisposable = Disposable.None;1432}14331434// DND scroll top animation14351436private setupDragAndDropScrollTopAnimation(event: DragEvent | MouseEvent): void {1437if (!this.dragOverAnimationDisposable) {1438const viewTop = getTopLeftOffset(this.domNode).top;1439this.dragOverAnimationDisposable = animate(getWindow(this.domNode), this.animateDragAndDropScrollTop.bind(this, viewTop));1440}14411442this.dragOverAnimationStopDisposable.dispose();1443this.dragOverAnimationStopDisposable = disposableTimeout(() => {1444if (this.dragOverAnimationDisposable) {1445this.dragOverAnimationDisposable.dispose();1446this.dragOverAnimationDisposable = undefined;1447}1448}, 1000, this.disposables);14491450this.dragOverMouseY = event.pageY;1451}14521453private animateDragAndDropScrollTop(viewTop: number): void {1454if (this.dragOverMouseY === undefined) {1455return;1456}14571458const diff = this.dragOverMouseY - viewTop;1459const upperLimit = this.renderHeight - 35;14601461if (diff < 35) {1462this.scrollTop += Math.max(-14, Math.floor(0.3 * (diff - 35)));1463} else if (diff > upperLimit) {1464this.scrollTop += Math.min(14, Math.floor(0.3 * (diff - upperLimit)));1465}1466}14671468private teardownDragAndDropScrollTopAnimation(): void {1469this.dragOverAnimationStopDisposable.dispose();14701471if (this.dragOverAnimationDisposable) {1472this.dragOverAnimationDisposable.dispose();1473this.dragOverAnimationDisposable = undefined;1474}1475}14761477// Util14781479private getTargetSector(browserEvent: DragEvent, targetIndex: number | undefined): ListViewTargetSector | undefined {1480if (targetIndex === undefined) {1481return undefined;1482}14831484const relativePosition = browserEvent.offsetY / this.items[targetIndex].size;1485const sector = Math.floor(relativePosition / 0.25);1486return clamp(sector, 0, 3);1487}14881489private getItemIndexFromEventTarget(target: EventTarget | null): number | undefined {1490const scrollableElement = this.scrollableElement.getDomNode();1491let element: HTMLElement | SVGElement | null = target as (HTMLElement | SVGElement | null);14921493while ((isHTMLElement(element) || isSVGElement(element)) && element !== this.rowsContainer && scrollableElement.contains(element)) {1494const rawIndex = element.getAttribute('data-index');14951496if (rawIndex) {1497const index = Number(rawIndex);14981499if (!isNaN(index)) {1500return index;1501}1502}15031504element = element.parentElement;1505}15061507return undefined;1508}15091510private getVisibleRange(renderTop: number, renderHeight: number): IRange {1511return {1512start: this.rangeMap.indexAt(renderTop),1513end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)1514};1515}15161517protected getRenderRange(renderTop: number, renderHeight: number): IRange {1518const range = this.getVisibleRange(renderTop, renderHeight);1519if (this.currentSelectionBounds) {1520const max = this.rangeMap.count;1521range.start = Math.min(range.start, this.currentSelectionBounds.start, max);1522range.end = Math.min(Math.max(range.end, this.currentSelectionBounds.end + 1), max);1523}15241525return range;1526}15271528/**1529* Given a stable rendered state, checks every rendered element whether it needs1530* to be probed for dynamic height. Adjusts scroll height and top if necessary.1531*/1532protected _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void {1533const previousRenderRange = this.getRenderRange(renderTop, renderHeight);15341535// Let's remember the second element's position, this helps in scrolling up1536// and preserving a linear upwards scroll movement1537let anchorElementIndex: number | undefined;1538let anchorElementTopDelta: number | undefined;15391540if (renderTop === this.elementTop(previousRenderRange.start)) {1541anchorElementIndex = previousRenderRange.start;1542anchorElementTopDelta = 0;1543} else if (previousRenderRange.end - previousRenderRange.start > 1) {1544anchorElementIndex = previousRenderRange.start + 1;1545anchorElementTopDelta = this.elementTop(anchorElementIndex) - renderTop;1546}15471548let heightDiff = 0;15491550while (true) {1551const renderRange = this.getRenderRange(renderTop, renderHeight);15521553let didChange = false;15541555for (let i = renderRange.start; i < renderRange.end; i++) {1556const diff = this.probeDynamicHeight(i);15571558if (diff !== 0) {1559this.rangeMap.splice(i, 1, [this.items[i]]);1560}15611562heightDiff += diff;1563didChange = didChange || diff !== 0;1564}15651566if (!didChange) {1567if (heightDiff !== 0) {1568this.eventuallyUpdateScrollDimensions();1569}15701571const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange);15721573for (const range of unrenderRanges) {1574for (let i = range.start; i < range.end; i++) {1575if (this.items[i].row) {1576this.removeItemFromDOM(i);1577}1578}1579}15801581const renderRanges = Range.relativeComplement(renderRange, previousRenderRange).reverse();15821583for (const range of renderRanges) {1584for (let i = range.end - 1; i >= range.start; i--) {1585this.insertItemInDOM(i);1586}1587}15881589for (let i = renderRange.start; i < renderRange.end; i++) {1590if (this.items[i].row) {1591this.updateItemInDOM(this.items[i], i);1592}1593}15941595if (typeof anchorElementIndex === 'number') {1596// To compute a destination scroll top, we need to take into account the current smooth scrolling1597// animation, and then reuse it with a new target (to avoid prolonging the scroll)1598// See https://github.com/microsoft/vscode/issues/1041441599// See https://github.com/microsoft/vscode/pull/1042841600// See https://github.com/microsoft/vscode/issues/1077041601const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;1602const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;1603this.setScrollTop(newScrollTop, inSmoothScrolling);1604}16051606this._onDidChangeContentHeight.fire(this.contentHeight);1607return;1608}1609}1610}16111612private probeDynamicHeight(index: number): number {1613const item = this.items[index];1614const diff = this.probeDynamicHeightForItem(item, index);1615if (diff > 0) {1616this.virtualDelegate.setDynamicHeight?.(item.element, item.size);1617}16181619return diff;1620}16211622private probeDynamicHeightForItem(item: IItem<T>, index: number): number {1623if (!!this.virtualDelegate.getDynamicHeight) {1624const newSize = this.virtualDelegate.getDynamicHeight(item.element);1625if (newSize !== null) {1626const size = item.size;1627item.size = newSize;1628item.lastDynamicHeightWidth = this.renderWidth;1629return newSize - size;1630}1631}16321633if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) {1634return 0;1635}16361637if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) {1638return 0;1639}16401641const size = item.size;16421643if (item.row) {1644item.row.domNode.style.height = '';1645item.size = item.row.domNode.offsetHeight;1646if (item.size === 0) {1647if (!isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) {1648console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack);1649} else {1650console.warn('Measured item node at 0px- ensure that ListView is not display:none before measuring row height!', new Error().stack);1651}1652}1653item.lastDynamicHeightWidth = this.renderWidth;1654return item.size - size;1655}16561657const { row } = this.cache.alloc(item.templateId);1658row.domNode.style.height = '';1659this.rowsContainer.appendChild(row.domNode);16601661const renderer = this.renderers.get(item.templateId);16621663if (!renderer) {1664throw new BugIndicatingError('Missing renderer for templateId: ' + item.templateId);1665}16661667renderer.renderElement(item.element, index, row.templateData);1668item.size = row.domNode.offsetHeight;1669renderer.disposeElement?.(item.element, index, row.templateData);16701671item.lastDynamicHeightWidth = this.renderWidth;1672row.domNode.remove();1673this.cache.release(row);16741675return item.size - size;1676}16771678getElementDomId(index: number): string {1679return `${this.domId}_${index}`;1680}16811682// Dispose16831684dispose() {1685for (const item of this.items) {1686item.dragStartDisposable.dispose();1687item.checkedDisposable.dispose();16881689if (item.row) {1690const renderer = this.renderers.get(item.row.templateId);1691if (renderer) {1692renderer.disposeElement?.(item.element, -1, item.row.templateData, undefined);1693renderer.disposeTemplate(item.row.templateData);1694}1695}1696}16971698this.items = [];16991700this.domNode?.remove();17011702this.dragOverAnimationDisposable?.dispose();1703this.disposables.dispose();1704}1705}170617071708