Path: blob/main/src/vs/base/browser/ui/list/listView.ts
3296 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 interface IListViewAccessibilityProvider<T> {61getSetSize?(element: T, index: number, listLength: number): number;62getPosInSet?(element: T, index: number): number;63getRole?(element: T): AriaRole | undefined;64isChecked?(element: T): boolean | IValueWithChangeEvent<boolean> | undefined;65}6667export interface IListViewOptionsUpdate {68readonly smoothScrolling?: boolean;69readonly horizontalScrolling?: boolean;70readonly scrollByPage?: boolean;71readonly mouseWheelScrollSensitivity?: number;72readonly fastScrollSensitivity?: number;73readonly paddingTop?: number;74readonly paddingBottom?: number;75}7677export interface IListViewOptions<T> extends IListViewOptionsUpdate {78readonly dnd?: IListViewDragAndDrop<T>;79readonly useShadows?: boolean;80readonly verticalScrollMode?: ScrollbarVisibility;81readonly setRowLineHeight?: boolean;82readonly setRowHeight?: boolean;83readonly supportDynamicHeights?: boolean;84readonly mouseSupport?: boolean;85readonly userSelection?: boolean;86readonly accessibilityProvider?: IListViewAccessibilityProvider<T>;87readonly transformOptimization?: boolean;88readonly alwaysConsumeMouseWheel?: boolean;89readonly initialSize?: Dimension;90readonly scrollToActiveElement?: boolean;91}9293const DefaultOptions = {94useShadows: true,95verticalScrollMode: ScrollbarVisibility.Auto,96setRowLineHeight: true,97setRowHeight: true,98supportDynamicHeights: false,99dnd: {100getDragElements<T>(e: T) { return [e]; },101getDragURI() { return null; },102onDragStart(): void { },103onDragOver() { return false; },104drop() { },105dispose() { }106},107horizontalScrolling: false,108transformOptimization: true,109alwaysConsumeMouseWheel: true,110} satisfies IListViewOptions<any>;111112export class ElementsDragAndDropData<T, TContext = void> implements IDragAndDropData {113114readonly elements: T[];115116private _context: TContext | undefined;117public get context(): TContext | undefined {118return this._context;119}120public set context(value: TContext | undefined) {121this._context = value;122}123124constructor(elements: T[]) {125this.elements = elements;126}127128update(): void { }129130getData(): T[] {131return this.elements;132}133}134135export class ExternalElementsDragAndDropData<T> implements IDragAndDropData {136137readonly elements: T[];138139constructor(elements: T[]) {140this.elements = elements;141}142143update(): void { }144145getData(): T[] {146return this.elements;147}148}149150export class NativeDragAndDropData implements IDragAndDropData {151152readonly types: any[];153readonly files: any[];154155constructor() {156this.types = [];157this.files = [];158}159160update(dataTransfer: DataTransfer): void {161if (dataTransfer.types) {162this.types.splice(0, this.types.length, ...dataTransfer.types);163}164165if (dataTransfer.files) {166this.files.splice(0, this.files.length);167168for (let i = 0; i < dataTransfer.files.length; i++) {169const file = dataTransfer.files.item(i);170171if (file && (file.size || file.type)) {172this.files.push(file);173}174}175}176}177178getData() {179return {180types: this.types,181files: this.files182};183}184}185186function equalsDragFeedback(f1: number[] | undefined, f2: number[] | undefined): boolean {187if (Array.isArray(f1) && Array.isArray(f2)) {188return equals(f1, f2);189}190191return f1 === f2;192}193194class ListViewAccessibilityProvider<T> implements Required<IListViewAccessibilityProvider<T>> {195196readonly getSetSize: (element: T, index: number, listLength: number) => number;197readonly getPosInSet: (element: T, index: number) => number;198readonly getRole: (element: T) => AriaRole | undefined;199readonly isChecked: (element: T) => boolean | IValueWithChangeEvent<boolean> | undefined;200201constructor(accessibilityProvider?: IListViewAccessibilityProvider<T>) {202if (accessibilityProvider?.getSetSize) {203this.getSetSize = accessibilityProvider.getSetSize.bind(accessibilityProvider);204} else {205this.getSetSize = (e, i, l) => l;206}207208if (accessibilityProvider?.getPosInSet) {209this.getPosInSet = accessibilityProvider.getPosInSet.bind(accessibilityProvider);210} else {211this.getPosInSet = (e, i) => i + 1;212}213214if (accessibilityProvider?.getRole) {215this.getRole = accessibilityProvider.getRole.bind(accessibilityProvider);216} else {217this.getRole = _ => 'listitem';218}219220if (accessibilityProvider?.isChecked) {221this.isChecked = accessibilityProvider.isChecked.bind(accessibilityProvider);222} else {223this.isChecked = _ => undefined;224}225}226}227228export interface IListView<T> extends ISpliceable<T>, IDisposable {229readonly domId: string;230readonly domNode: HTMLElement;231readonly containerDomNode: HTMLElement;232readonly scrollableElementDomNode: HTMLElement;233readonly length: number;234readonly contentHeight: number;235readonly contentWidth: number;236readonly onDidChangeContentHeight: Event<number>;237readonly onDidChangeContentWidth: Event<number>;238readonly renderHeight: number;239readonly scrollHeight: number;240readonly firstVisibleIndex: number;241readonly firstMostlyVisibleIndex: number;242readonly lastVisibleIndex: number;243onDidScroll: Event<ScrollEvent>;244onWillScroll: Event<ScrollEvent>;245onMouseClick: Event<IListMouseEvent<T>>;246onMouseDblClick: Event<IListMouseEvent<T>>;247onMouseMiddleClick: Event<IListMouseEvent<T>>;248onMouseUp: Event<IListMouseEvent<T>>;249onMouseDown: Event<IListMouseEvent<T>>;250onMouseOver: Event<IListMouseEvent<T>>;251onMouseMove: Event<IListMouseEvent<T>>;252onMouseOut: Event<IListMouseEvent<T>>;253onContextMenu: Event<IListMouseEvent<T>>;254onTouchStart: Event<IListTouchEvent<T>>;255onTap: Event<IListGestureEvent<T>>;256element(index: number): T;257domElement(index: number): HTMLElement | null;258getElementDomId(index: number): string;259elementHeight(index: number): number;260elementTop(index: number): number;261indexOf(element: T): number;262indexAt(position: number): number;263indexAfter(position: number): number;264updateOptions(options: IListViewOptionsUpdate): void;265getScrollTop(): number;266setScrollTop(scrollTop: number, reuseAnimation?: boolean): void;267getScrollLeft(): number;268setScrollLeft(scrollLeft: number): void;269delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void;270delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent): void;271updateWidth(index: number): void;272updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void;273rerender(): void;274layout(height?: number, width?: number): void;275}276277/**278* The {@link ListView} is a virtual scrolling engine.279*280* Given that it only renders elements within its viewport, it can hold large281* collections of elements and stay very performant. The performance bottleneck282* usually lies within the user's rendering code for each element.283*284* @remarks It is a low-level widget, not meant to be used directly. Refer to the285* List widget instead.286*/287export class ListView<T> implements IListView<T> {288289private static InstanceCount = 0;290readonly domId = `list_id_${++ListView.InstanceCount}`;291292readonly domNode: HTMLElement;293294private items: IItem<T>[];295private itemId: number;296protected rangeMap: IRangeMap;297private cache: RowCache<T>;298private renderers = new Map<string, IListRenderer<any /* TODO@joao */, any>>();299protected lastRenderTop: number;300protected lastRenderHeight: number;301private renderWidth = 0;302private rowsContainer: HTMLElement;303private scrollable: Scrollable;304private scrollableElement: SmoothScrollableElement;305private _scrollHeight: number = 0;306private scrollableElementUpdateDisposable: IDisposable | null = null;307private scrollableElementWidthDelayer = new Delayer<void>(50);308private splicing = false;309private dragOverAnimationDisposable: IDisposable | undefined;310private dragOverAnimationStopDisposable: IDisposable = Disposable.None;311private dragOverMouseY: number = 0;312private setRowLineHeight: boolean;313private setRowHeight: boolean;314private supportDynamicHeights: boolean;315private paddingBottom: number;316private accessibilityProvider: ListViewAccessibilityProvider<T>;317private scrollWidth: number | undefined;318319private dnd: IListViewDragAndDrop<T>;320private canDrop: boolean = false;321private currentDragData: IDragAndDropData | undefined;322private currentDragFeedback: number[] | undefined;323private currentDragFeedbackPosition: ListDragOverEffectPosition | undefined;324private currentDragFeedbackDisposable: IDisposable = Disposable.None;325private onDragLeaveTimeout: IDisposable = Disposable.None;326private currentSelectionDisposable: IDisposable = Disposable.None;327private currentSelectionBounds: IRange | undefined;328private activeElement: HTMLElement | undefined;329330private readonly disposables: DisposableStore = new DisposableStore();331332private readonly _onDidChangeContentHeight = new Emitter<number>();333private readonly _onDidChangeContentWidth = new Emitter<number>();334readonly onDidChangeContentHeight: Event<number> = Event.latch(this._onDidChangeContentHeight.event, undefined, this.disposables);335readonly onDidChangeContentWidth: Event<number> = Event.latch(this._onDidChangeContentWidth.event, undefined, this.disposables);336get contentHeight(): number { return this.rangeMap.size; }337get contentWidth(): number { return this.scrollWidth ?? 0; }338339get onDidScroll(): Event<ScrollEvent> { return this.scrollableElement.onScroll; }340get onWillScroll(): Event<ScrollEvent> { return this.scrollableElement.onWillScroll; }341get containerDomNode(): HTMLElement { return this.rowsContainer; }342get scrollableElementDomNode(): HTMLElement { return this.scrollableElement.getDomNode(); }343344private _horizontalScrolling: boolean = false;345private get horizontalScrolling(): boolean { return this._horizontalScrolling; }346private set horizontalScrolling(value: boolean) {347if (value === this._horizontalScrolling) {348return;349}350351if (value && this.supportDynamicHeights) {352throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously');353}354355this._horizontalScrolling = value;356this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling);357358if (this._horizontalScrolling) {359for (const item of this.items) {360this.measureItemWidth(item);361}362363this.updateScrollWidth();364this.scrollableElement.setScrollDimensions({ width: getContentWidth(this.domNode) });365this.rowsContainer.style.width = `${Math.max(this.scrollWidth || 0, this.renderWidth)}px`;366} else {367this.scrollableElementWidthDelayer.cancel();368this.scrollableElement.setScrollDimensions({ width: this.renderWidth, scrollWidth: this.renderWidth });369this.rowsContainer.style.width = '';370}371}372373constructor(374container: HTMLElement,375private virtualDelegate: IListVirtualDelegate<T>,376renderers: IListRenderer<any /* TODO@joao */, any>[],377options: IListViewOptions<T> = DefaultOptions378) {379if (options.horizontalScrolling && options.supportDynamicHeights) {380throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously');381}382383this.items = [];384this.itemId = 0;385this.rangeMap = this.createRangeMap(options.paddingTop ?? 0);386387for (const renderer of renderers) {388this.renderers.set(renderer.templateId, renderer);389}390391this.cache = this.disposables.add(new RowCache(this.renderers));392393this.lastRenderTop = 0;394this.lastRenderHeight = 0;395396this.domNode = document.createElement('div');397this.domNode.className = 'monaco-list';398399this.domNode.classList.add(this.domId);400this.domNode.tabIndex = 0;401402this.domNode.classList.toggle('mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true);403404this._horizontalScrolling = options.horizontalScrolling ?? DefaultOptions.horizontalScrolling;405this.domNode.classList.toggle('horizontal-scrolling', this._horizontalScrolling);406407this.paddingBottom = typeof options.paddingBottom === 'undefined' ? 0 : options.paddingBottom;408409this.accessibilityProvider = new ListViewAccessibilityProvider(options.accessibilityProvider);410411this.rowsContainer = document.createElement('div');412this.rowsContainer.className = 'monaco-list-rows';413414const transformOptimization = options.transformOptimization ?? DefaultOptions.transformOptimization;415if (transformOptimization) {416this.rowsContainer.style.transform = 'translate3d(0px, 0px, 0px)';417this.rowsContainer.style.overflow = 'hidden';418this.rowsContainer.style.contain = 'strict';419}420421this.disposables.add(Gesture.addTarget(this.rowsContainer));422423this.scrollable = this.disposables.add(new Scrollable({424forceIntegerValues: true,425smoothScrollDuration: (options.smoothScrolling ?? false) ? 125 : 0,426scheduleAtNextAnimationFrame: cb => scheduleAtNextAnimationFrame(getWindow(this.domNode), cb)427}));428this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, {429alwaysConsumeMouseWheel: options.alwaysConsumeMouseWheel ?? DefaultOptions.alwaysConsumeMouseWheel,430horizontal: ScrollbarVisibility.Auto,431vertical: options.verticalScrollMode ?? DefaultOptions.verticalScrollMode,432useShadows: options.useShadows ?? DefaultOptions.useShadows,433mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity,434fastScrollSensitivity: options.fastScrollSensitivity,435scrollByPage: options.scrollByPage436}, this.scrollable));437438this.domNode.appendChild(this.scrollableElement.getDomNode());439container.appendChild(this.domNode);440441this.scrollableElement.onScroll(this.onScroll, this, this.disposables);442this.disposables.add(addDisposableListener(this.rowsContainer, TouchEventType.Change, e => this.onTouchChange(e as GestureEvent)));443444this.disposables.add(addDisposableListener(this.scrollableElement.getDomNode(), 'scroll', e => {445// Make sure the active element is scrolled into view446const element = (e.target as HTMLElement);447const scrollValue = element.scrollTop;448element.scrollTop = 0;449if (options.scrollToActiveElement) {450this.setScrollTop(this.scrollTop + scrollValue);451}452}));453454this.disposables.add(addDisposableListener(this.domNode, 'dragover', e => this.onDragOver(this.toDragEvent(e))));455this.disposables.add(addDisposableListener(this.domNode, 'drop', e => this.onDrop(this.toDragEvent(e))));456this.disposables.add(addDisposableListener(this.domNode, 'dragleave', e => this.onDragLeave(this.toDragEvent(e))));457this.disposables.add(addDisposableListener(this.domNode, 'dragend', e => this.onDragEnd(e)));458if (options.userSelection) {459if (options.dnd) {460throw new Error('DND and user selection cannot be used simultaneously');461}462this.disposables.add(addDisposableListener(this.domNode, 'mousedown', e => this.onPotentialSelectionStart(e)));463}464465this.setRowLineHeight = options.setRowLineHeight ?? DefaultOptions.setRowLineHeight;466this.setRowHeight = options.setRowHeight ?? DefaultOptions.setRowHeight;467this.supportDynamicHeights = options.supportDynamicHeights ?? DefaultOptions.supportDynamicHeights;468this.dnd = options.dnd ?? this.disposables.add(DefaultOptions.dnd);469470this.layout(options.initialSize?.height, options.initialSize?.width);471if (options.scrollToActiveElement) {472this._setupFocusObserver(container);473}474}475476private _setupFocusObserver(container: HTMLElement): void {477this.disposables.add(addDisposableListener(container, 'focus', () => {478const element = getActiveElement() as HTMLElement | null;479if (this.activeElement !== element && element !== null) {480this.activeElement = element;481this._scrollToActiveElement(this.activeElement, container);482}483}, true));484}485486private _scrollToActiveElement(element: HTMLElement, container: HTMLElement) {487// The scroll event on the list only fires when scrolling down.488// If the active element is above the viewport, we need to scroll up.489const containerRect = container.getBoundingClientRect();490const elementRect = element.getBoundingClientRect();491492const topOffset = elementRect.top - containerRect.top;493494if (topOffset < 0) {495// Scroll up496this.setScrollTop(this.scrollTop + topOffset);497}498}499500updateOptions(options: IListViewOptionsUpdate) {501if (options.paddingBottom !== undefined) {502this.paddingBottom = options.paddingBottom;503this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });504}505506if (options.smoothScrolling !== undefined) {507this.scrollable.setSmoothScrollDuration(options.smoothScrolling ? 125 : 0);508}509510if (options.horizontalScrolling !== undefined) {511this.horizontalScrolling = options.horizontalScrolling;512}513514let scrollableOptions: ScrollableElementChangeOptions | undefined;515516if (options.scrollByPage !== undefined) {517scrollableOptions = { ...(scrollableOptions ?? {}), scrollByPage: options.scrollByPage };518}519520if (options.mouseWheelScrollSensitivity !== undefined) {521scrollableOptions = { ...(scrollableOptions ?? {}), mouseWheelScrollSensitivity: options.mouseWheelScrollSensitivity };522}523524if (options.fastScrollSensitivity !== undefined) {525scrollableOptions = { ...(scrollableOptions ?? {}), fastScrollSensitivity: options.fastScrollSensitivity };526}527528if (scrollableOptions) {529this.scrollableElement.updateOptions(scrollableOptions);530}531532if (options.paddingTop !== undefined && options.paddingTop !== this.rangeMap.paddingTop) {533// trigger a rerender534const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);535const offset = options.paddingTop - this.rangeMap.paddingTop;536this.rangeMap.paddingTop = options.paddingTop;537538this.render(lastRenderRange, Math.max(0, this.lastRenderTop + offset), this.lastRenderHeight, undefined, undefined, true);539this.setScrollTop(this.lastRenderTop);540541this.eventuallyUpdateScrollDimensions();542543if (this.supportDynamicHeights) {544this._rerender(this.lastRenderTop, this.lastRenderHeight);545}546}547}548549delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) {550this.scrollableElement.delegateScrollFromMouseWheelEvent(browserEvent);551}552553delegateVerticalScrollbarPointerDown(browserEvent: PointerEvent) {554this.scrollableElement.delegateVerticalScrollbarPointerDown(browserEvent);555}556557updateElementHeight(index: number, size: number | undefined, anchorIndex: number | null): void {558if (index < 0 || index >= this.items.length) {559return;560}561562const originalSize = this.items[index].size;563564if (typeof size === 'undefined') {565if (!this.supportDynamicHeights) {566console.warn('Dynamic heights not supported', new Error().stack);567return;568}569570this.items[index].lastDynamicHeightWidth = undefined;571size = originalSize + this.probeDynamicHeight(index);572}573574if (originalSize === size) {575return;576}577578const lastRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);579580let heightDiff = 0;581582if (index < lastRenderRange.start) {583// do not scroll the viewport if resized element is out of viewport584heightDiff = size - originalSize;585} else {586if (anchorIndex !== null && anchorIndex > index && anchorIndex < lastRenderRange.end) {587// anchor in viewport588// resized element in viewport and above the anchor589heightDiff = size - originalSize;590} else {591heightDiff = 0;592}593}594595this.rangeMap.splice(index, 1, [{ size: size }]);596this.items[index].size = size;597598this.render(lastRenderRange, Math.max(0, this.lastRenderTop + heightDiff), this.lastRenderHeight, undefined, undefined, true);599this.setScrollTop(this.lastRenderTop);600601this.eventuallyUpdateScrollDimensions();602603if (this.supportDynamicHeights) {604this._rerender(this.lastRenderTop, this.lastRenderHeight);605} else {606this._onDidChangeContentHeight.fire(this.contentHeight); // otherwise fired in _rerender()607}608}609610protected createRangeMap(paddingTop: number): IRangeMap {611return new RangeMap(paddingTop);612}613614splice(start: number, deleteCount: number, elements: readonly T[] = []): T[] {615if (this.splicing) {616throw new Error('Can\'t run recursive splices.');617}618619this.splicing = true;620621try {622return this._splice(start, deleteCount, elements);623} finally {624this.splicing = false;625this._onDidChangeContentHeight.fire(this.contentHeight);626}627}628629private _splice(start: number, deleteCount: number, elements: readonly T[] = []): T[] {630const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);631const deleteRange = { start, end: start + deleteCount };632const removeRange = Range.intersect(previousRenderRange, deleteRange);633634// try to reuse rows, avoid removing them from DOM635const rowsToDispose = new Map<string, IRow[]>();636for (let i = removeRange.end - 1; i >= removeRange.start; i--) {637const item = this.items[i];638item.dragStartDisposable.dispose();639item.checkedDisposable.dispose();640641if (item.row) {642let rows = rowsToDispose.get(item.templateId);643644if (!rows) {645rows = [];646rowsToDispose.set(item.templateId, rows);647}648649const renderer = this.renderers.get(item.templateId);650651if (renderer && renderer.disposeElement) {652renderer.disposeElement(item.element, i, item.row.templateData, { height: item.size });653}654655rows.unshift(item.row);656}657658item.row = null;659item.stale = true;660}661662const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };663const previousRenderedRestRange = Range.intersect(previousRestRange, previousRenderRange);664const previousUnrenderedRestRanges = Range.relativeComplement(previousRestRange, previousRenderRange);665666const inserted = elements.map<IItem<T>>(element => ({667id: String(this.itemId++),668element,669templateId: this.virtualDelegate.getTemplateId(element),670size: this.virtualDelegate.getHeight(element),671width: undefined,672hasDynamicHeight: !!this.virtualDelegate.hasDynamicHeight && this.virtualDelegate.hasDynamicHeight(element),673lastDynamicHeightWidth: undefined,674row: null,675uri: undefined,676dropTarget: false,677dragStartDisposable: Disposable.None,678checkedDisposable: Disposable.None,679stale: false680}));681682let deleted: IItem<T>[];683684// TODO@joao: improve this optimization to catch even more cases685if (start === 0 && deleteCount >= this.items.length) {686this.rangeMap = this.createRangeMap(this.rangeMap.paddingTop);687this.rangeMap.splice(0, 0, inserted);688deleted = this.items;689this.items = inserted;690} else {691this.rangeMap.splice(start, deleteCount, inserted);692deleted = splice(this.items, start, deleteCount, inserted);693}694695const delta = elements.length - deleteCount;696const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);697const renderedRestRange = shift(previousRenderedRestRange, delta);698const updateRange = Range.intersect(renderRange, renderedRestRange);699700for (let i = updateRange.start; i < updateRange.end; i++) {701this.updateItemInDOM(this.items[i], i);702}703704const removeRanges = Range.relativeComplement(renderedRestRange, renderRange);705706for (const range of removeRanges) {707for (let i = range.start; i < range.end; i++) {708this.removeItemFromDOM(i);709}710}711712const unrenderedRestRanges = previousUnrenderedRestRanges.map(r => shift(r, delta));713const elementsRange = { start, end: start + elements.length };714const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => Range.intersect(renderRange, r)).reverse();715716for (const range of insertRanges) {717for (let i = range.end - 1; i >= range.start; i--) {718const item = this.items[i];719const rows = rowsToDispose.get(item.templateId);720const row = rows?.pop();721this.insertItemInDOM(i, row);722}723}724725for (const rows of rowsToDispose.values()) {726for (const row of rows) {727this.cache.release(row);728}729}730731this.eventuallyUpdateScrollDimensions();732733if (this.supportDynamicHeights) {734this._rerender(this.scrollTop, this.renderHeight);735}736737return deleted.map(i => i.element);738}739740protected eventuallyUpdateScrollDimensions(): void {741this._scrollHeight = this.contentHeight;742this.rowsContainer.style.height = `${this._scrollHeight}px`;743744if (!this.scrollableElementUpdateDisposable) {745this.scrollableElementUpdateDisposable = scheduleAtNextAnimationFrame(getWindow(this.domNode), () => {746this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });747this.updateScrollWidth();748this.scrollableElementUpdateDisposable = null;749});750}751}752753private eventuallyUpdateScrollWidth(): void {754if (!this.horizontalScrolling) {755this.scrollableElementWidthDelayer.cancel();756return;757}758759this.scrollableElementWidthDelayer.trigger(() => this.updateScrollWidth());760}761762private updateScrollWidth(): void {763if (!this.horizontalScrolling) {764return;765}766767let scrollWidth = 0;768769for (const item of this.items) {770if (typeof item.width !== 'undefined') {771scrollWidth = Math.max(scrollWidth, item.width);772}773}774775this.scrollWidth = scrollWidth;776this.scrollableElement.setScrollDimensions({ scrollWidth: scrollWidth === 0 ? 0 : (scrollWidth + 10) });777this._onDidChangeContentWidth.fire(this.scrollWidth);778}779780updateWidth(index: number): void {781if (!this.horizontalScrolling || typeof this.scrollWidth === 'undefined') {782return;783}784785const item = this.items[index];786this.measureItemWidth(item);787788if (typeof item.width !== 'undefined' && item.width > this.scrollWidth) {789this.scrollWidth = item.width;790this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth + 10 });791this._onDidChangeContentWidth.fire(this.scrollWidth);792}793}794795rerender(): void {796if (!this.supportDynamicHeights) {797return;798}799800for (const item of this.items) {801item.lastDynamicHeightWidth = undefined;802}803804this._rerender(this.lastRenderTop, this.lastRenderHeight);805}806807get length(): number {808return this.items.length;809}810811get renderHeight(): number {812const scrollDimensions = this.scrollableElement.getScrollDimensions();813return scrollDimensions.height;814}815816get firstVisibleIndex(): number {817const range = this.getVisibleRange(this.lastRenderTop, this.lastRenderHeight);818return range.start;819}820821get firstMostlyVisibleIndex(): number {822const firstVisibleIndex = this.firstVisibleIndex;823const firstElTop = this.rangeMap.positionAt(firstVisibleIndex);824const nextElTop = this.rangeMap.positionAt(firstVisibleIndex + 1);825if (nextElTop !== -1) {826const firstElMidpoint = (nextElTop - firstElTop) / 2 + firstElTop;827if (firstElMidpoint < this.scrollTop) {828return firstVisibleIndex + 1;829}830}831832return firstVisibleIndex;833}834835get lastVisibleIndex(): number {836const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);837return range.end - 1;838}839840element(index: number): T {841return this.items[index].element;842}843844indexOf(element: T): number {845return this.items.findIndex(item => item.element === element);846}847848domElement(index: number): HTMLElement | null {849const row = this.items[index].row;850return row && row.domNode;851}852853elementHeight(index: number): number {854return this.items[index].size;855}856857elementTop(index: number): number {858return this.rangeMap.positionAt(index);859}860861indexAt(position: number): number {862return this.rangeMap.indexAt(position);863}864865indexAfter(position: number): number {866return this.rangeMap.indexAfter(position);867}868869layout(height?: number, width?: number): void {870const scrollDimensions: INewScrollDimensions = {871height: typeof height === 'number' ? height : getContentHeight(this.domNode)872};873874if (this.scrollableElementUpdateDisposable) {875this.scrollableElementUpdateDisposable.dispose();876this.scrollableElementUpdateDisposable = null;877scrollDimensions.scrollHeight = this.scrollHeight;878}879880this.scrollableElement.setScrollDimensions(scrollDimensions);881882if (typeof width !== 'undefined') {883this.renderWidth = width;884885if (this.supportDynamicHeights) {886this._rerender(this.scrollTop, this.renderHeight);887}888}889890if (this.horizontalScrolling) {891this.scrollableElement.setScrollDimensions({892width: typeof width === 'number' ? width : getContentWidth(this.domNode)893});894}895}896897// Render898899protected render(previousRenderRange: IRange, renderTop: number, renderHeight: number, renderLeft: number | undefined, scrollWidth: number | undefined, updateItemsInDOM: boolean = false, onScroll: boolean = false): void {900const renderRange = this.getRenderRange(renderTop, renderHeight);901902const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange).reverse();903const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange);904905if (updateItemsInDOM) {906const rangesToUpdate = Range.intersect(previousRenderRange, renderRange);907908for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) {909this.updateItemInDOM(this.items[i], i);910}911}912913this.cache.transact(() => {914for (const range of rangesToRemove) {915for (let i = range.start; i < range.end; i++) {916this.removeItemFromDOM(i, onScroll);917}918}919920for (const range of rangesToInsert) {921for (let i = range.end - 1; i >= range.start; i--) {922this.insertItemInDOM(i);923}924}925});926927if (renderLeft !== undefined) {928this.rowsContainer.style.left = `-${renderLeft}px`;929}930931this.rowsContainer.style.top = `-${renderTop}px`;932933if (this.horizontalScrolling && scrollWidth !== undefined) {934this.rowsContainer.style.width = `${Math.max(scrollWidth, this.renderWidth)}px`;935}936937this.lastRenderTop = renderTop;938this.lastRenderHeight = renderHeight;939}940941// DOM operations942943private insertItemInDOM(index: number, row?: IRow): void {944const item = this.items[index];945946if (!item.row) {947if (row) {948item.row = row;949item.stale = true;950} else {951const result = this.cache.alloc(item.templateId);952item.row = result.row;953item.stale ||= result.isReusingConnectedDomNode;954}955}956957const role = this.accessibilityProvider.getRole(item.element) || 'listitem';958item.row.domNode.setAttribute('role', role);959960const checked = this.accessibilityProvider.isChecked(item.element);961962if (typeof checked === 'boolean') {963item.row.domNode.setAttribute('aria-checked', String(!!checked));964} else if (checked) {965const update = (checked: boolean) => item.row!.domNode.setAttribute('aria-checked', String(!!checked));966update(checked.value);967item.checkedDisposable = checked.onDidChange(() => update(checked.value));968}969970if (item.stale || !item.row.domNode.parentElement) {971const referenceNode = this.items.at(index + 1)?.row?.domNode ?? null;972if (item.row.domNode.parentElement !== this.rowsContainer || item.row.domNode.nextElementSibling !== referenceNode) {973this.rowsContainer.insertBefore(item.row.domNode, referenceNode);974}975item.stale = false;976}977978this.updateItemInDOM(item, index);979980const renderer = this.renderers.get(item.templateId);981982if (!renderer) {983throw new Error(`No renderer found for template id ${item.templateId}`);984}985986renderer?.renderElement(item.element, index, item.row.templateData, { height: item.size });987988const uri = this.dnd.getDragURI(item.element);989item.dragStartDisposable.dispose();990item.row.domNode.draggable = !!uri;991992if (uri) {993item.dragStartDisposable = addDisposableListener(item.row.domNode, 'dragstart', event => this.onDragStart(item.element, uri, event));994}995996if (this.horizontalScrolling) {997this.measureItemWidth(item);998this.eventuallyUpdateScrollWidth();999}1000}10011002private measureItemWidth(item: IItem<T>): void {1003if (!item.row || !item.row.domNode) {1004return;1005}10061007item.row.domNode.style.width = 'fit-content';1008item.width = getContentWidth(item.row.domNode);1009const style = getWindow(item.row.domNode).getComputedStyle(item.row.domNode);10101011if (style.paddingLeft) {1012item.width += parseFloat(style.paddingLeft);1013}10141015if (style.paddingRight) {1016item.width += parseFloat(style.paddingRight);1017}10181019item.row.domNode.style.width = '';1020}10211022private updateItemInDOM(item: IItem<T>, index: number): void {1023item.row!.domNode.style.top = `${this.elementTop(index)}px`;10241025if (this.setRowHeight) {1026item.row!.domNode.style.height = `${item.size}px`;1027}10281029if (this.setRowLineHeight) {1030item.row!.domNode.style.lineHeight = `${item.size}px`;1031}10321033item.row!.domNode.setAttribute('data-index', `${index}`);1034item.row!.domNode.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');1035item.row!.domNode.setAttribute('data-parity', index % 2 === 0 ? 'even' : 'odd');1036item.row!.domNode.setAttribute('aria-setsize', String(this.accessibilityProvider.getSetSize(item.element, index, this.length)));1037item.row!.domNode.setAttribute('aria-posinset', String(this.accessibilityProvider.getPosInSet(item.element, index)));1038item.row!.domNode.setAttribute('id', this.getElementDomId(index));10391040item.row!.domNode.classList.toggle('drop-target', item.dropTarget);1041}10421043private removeItemFromDOM(index: number, onScroll?: boolean): void {1044const item = this.items[index];1045item.dragStartDisposable.dispose();1046item.checkedDisposable.dispose();10471048if (item.row) {1049const renderer = this.renderers.get(item.templateId);10501051if (renderer && renderer.disposeElement) {1052renderer.disposeElement(item.element, index, item.row.templateData, { height: item.size, onScroll });1053}10541055this.cache.release(item.row);1056item.row = null;1057}10581059if (this.horizontalScrolling) {1060this.eventuallyUpdateScrollWidth();1061}1062}10631064getScrollTop(): number {1065const scrollPosition = this.scrollableElement.getScrollPosition();1066return scrollPosition.scrollTop;1067}10681069setScrollTop(scrollTop: number, reuseAnimation?: boolean): void {1070if (this.scrollableElementUpdateDisposable) {1071this.scrollableElementUpdateDisposable.dispose();1072this.scrollableElementUpdateDisposable = null;1073this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight });1074}10751076this.scrollableElement.setScrollPosition({ scrollTop, reuseAnimation });1077}10781079getScrollLeft(): number {1080const scrollPosition = this.scrollableElement.getScrollPosition();1081return scrollPosition.scrollLeft;1082}10831084setScrollLeft(scrollLeft: number): void {1085if (this.scrollableElementUpdateDisposable) {1086this.scrollableElementUpdateDisposable.dispose();1087this.scrollableElementUpdateDisposable = null;1088this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth });1089}10901091this.scrollableElement.setScrollPosition({ scrollLeft });1092}109310941095get scrollTop(): number {1096return this.getScrollTop();1097}10981099set scrollTop(scrollTop: number) {1100this.setScrollTop(scrollTop);1101}11021103get scrollHeight(): number {1104return this._scrollHeight + (this.horizontalScrolling ? 10 : 0) + this.paddingBottom;1105}11061107// Events11081109@memoize get onMouseClick(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'click')).event, e => this.toMouseEvent(e), this.disposables); }1110@memoize get onMouseDblClick(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'dblclick')).event, e => this.toMouseEvent(e), this.disposables); }1111@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); }1112@memoize get onMouseUp(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseup')).event, e => this.toMouseEvent(e), this.disposables); }1113@memoize get onMouseDown(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mousedown')).event, e => this.toMouseEvent(e), this.disposables); }1114@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseover')).event, e => this.toMouseEvent(e), this.disposables); }1115@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mousemove')).event, e => this.toMouseEvent(e), this.disposables); }1116@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'mouseout')).event, e => this.toMouseEvent(e), this.disposables); }1117@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 as Event<GestureEvent>, e => this.toGestureEvent(e), this.disposables)); }1118@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.domNode, 'touchstart')).event, e => this.toTouchEvent(e), this.disposables); }1119@memoize get onTap(): Event<IListGestureEvent<T>> { return Event.map(this.disposables.add(new DomEmitter(this.rowsContainer, TouchEventType.Tap)).event, e => this.toGestureEvent(e as GestureEvent), this.disposables); }11201121private toMouseEvent(browserEvent: MouseEvent): IListMouseEvent<T> {1122const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1123const item = typeof index === 'undefined' ? undefined : this.items[index];1124const element = item && item.element;1125return { browserEvent, index, element };1126}11271128private toTouchEvent(browserEvent: TouchEvent): IListTouchEvent<T> {1129const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1130const item = typeof index === 'undefined' ? undefined : this.items[index];1131const element = item && item.element;1132return { browserEvent, index, element };1133}11341135private toGestureEvent(browserEvent: GestureEvent): IListGestureEvent<T> {1136const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget || null);1137const item = typeof index === 'undefined' ? undefined : this.items[index];1138const element = item && item.element;1139return { browserEvent, index, element };1140}11411142private toDragEvent(browserEvent: DragEvent): IListDragEvent<T> {1143const index = this.getItemIndexFromEventTarget(browserEvent.target || null);1144const item = typeof index === 'undefined' ? undefined : this.items[index];1145const element = item && item.element;1146const sector = this.getTargetSector(browserEvent, index);1147return { browserEvent, index, element, sector };1148}11491150private onScroll(e: ScrollEvent): void {1151try {1152const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);1153this.render(previousRenderRange, e.scrollTop, e.height, e.scrollLeft, e.scrollWidth, undefined, true);11541155if (this.supportDynamicHeights) {1156this._rerender(e.scrollTop, e.height, e.inSmoothScrolling);1157}1158} catch (err) {1159console.error('Got bad scroll event:', e);1160throw err;1161}1162}11631164private onTouchChange(event: GestureEvent): void {1165event.preventDefault();1166event.stopPropagation();11671168this.scrollTop -= event.translationY;1169}11701171// DND11721173private onDragStart(element: T, uri: string, event: DragEvent): void {1174if (!event.dataTransfer) {1175return;1176}11771178const elements = this.dnd.getDragElements(element);11791180event.dataTransfer.effectAllowed = 'copyMove';1181event.dataTransfer.setData(DataTransfers.TEXT, uri);11821183let label: string | undefined;1184if (this.dnd.getDragLabel) {1185label = this.dnd.getDragLabel(elements, event);1186}1187if (typeof label === 'undefined') {1188label = String(elements.length);1189}11901191applyDragImage(event, this.domNode, label, [this.domId /* add domId to get list specific styling */]);11921193this.domNode.classList.add('dragging');1194this.currentDragData = new ElementsDragAndDropData(elements);1195StaticDND.CurrentDragAndDropData = new ExternalElementsDragAndDropData(elements);11961197this.dnd.onDragStart?.(this.currentDragData, event);1198}11991200private onPotentialSelectionStart(e: MouseEvent) {1201this.currentSelectionDisposable.dispose();1202const doc = getDocument(this.domNode);12031204// Set up both the 'movement store' for watching the mouse, and the1205// 'selection store' which lasts as long as there's a selection, even1206// after the usr has stopped modifying it.1207const selectionStore = this.currentSelectionDisposable = new DisposableStore();1208const movementStore = selectionStore.add(new DisposableStore());12091210// The selection events we get from the DOM are fairly limited and we lack a 'selection end' event.1211// Selection events also don't tell us where the input doing the selection is. So, make a poor1212// assumption that a user is using the mouse, and base our events on that.1213movementStore.add(addDisposableListener(this.domNode, 'selectstart', () => {1214movementStore.add(addDisposableListener(doc, 'mousemove', e => {1215if (doc.getSelection()?.isCollapsed === false) {1216this.setupDragAndDropScrollTopAnimation(e);1217}1218}));12191220// The selection is cleared either on mouseup if there's no selection, or on next mousedown1221// when `this.currentSelectionDisposable` is reset.1222selectionStore.add(toDisposable(() => {1223const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);1224this.currentSelectionBounds = undefined;1225this.render(previousRenderRange, this.lastRenderTop, this.lastRenderHeight, undefined, undefined);1226}));1227selectionStore.add(addDisposableListener(doc, 'selectionchange', () => {1228const selection = doc.getSelection();1229// if the selection changed _after_ mouseup, it's from clearing the list or similar, so teardown1230if (!selection || selection.isCollapsed) {1231if (movementStore.isDisposed) {1232selectionStore.dispose();1233}1234return;1235}12361237let start = this.getIndexOfListElement(selection.anchorNode as HTMLElement);1238let end = this.getIndexOfListElement(selection.focusNode as HTMLElement);1239if (start !== undefined && end !== undefined) {1240if (end < start) {1241[start, end] = [end, start];1242}1243this.currentSelectionBounds = { start, end };1244}1245}));1246}));12471248movementStore.add(addDisposableListener(doc, 'mouseup', () => {1249movementStore.dispose();1250this.teardownDragAndDropScrollTopAnimation();12511252if (doc.getSelection()?.isCollapsed !== false) {1253selectionStore.dispose();1254}1255}));1256}12571258private getIndexOfListElement(element: HTMLElement | null): number | undefined {1259if (!element || !this.domNode.contains(element)) {1260return undefined;1261}12621263while (element && element !== this.domNode) {1264if (element.dataset?.index) {1265return Number(element.dataset.index);1266}12671268element = element.parentElement;1269}12701271return undefined;1272}12731274private onDragOver(event: IListDragEvent<T>): boolean {1275event.browserEvent.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)12761277this.onDragLeaveTimeout.dispose();12781279if (StaticDND.CurrentDragAndDropData && StaticDND.CurrentDragAndDropData.getData() === 'vscode-ui') {1280return false;1281}12821283this.setupDragAndDropScrollTopAnimation(event.browserEvent);12841285if (!event.browserEvent.dataTransfer) {1286return false;1287}12881289// Drag over from outside1290if (!this.currentDragData) {1291if (StaticDND.CurrentDragAndDropData) {1292// Drag over from another list1293this.currentDragData = StaticDND.CurrentDragAndDropData;12941295} else {1296// Drag over from the desktop1297if (!event.browserEvent.dataTransfer.types) {1298return false;1299}13001301this.currentDragData = new NativeDragAndDropData();1302}1303}13041305const result = this.dnd.onDragOver(this.currentDragData, event.element, event.index, event.sector, event.browserEvent);1306this.canDrop = typeof result === 'boolean' ? result : result.accept;13071308if (!this.canDrop) {1309this.currentDragFeedback = undefined;1310this.currentDragFeedbackDisposable.dispose();1311return false;1312}13131314event.browserEvent.dataTransfer.dropEffect = (typeof result !== 'boolean' && result.effect?.type === ListDragOverEffectType.Copy) ? 'copy' : 'move';13151316let feedback: number[];13171318if (typeof result !== 'boolean' && result.feedback) {1319feedback = result.feedback;1320} else {1321if (typeof event.index === 'undefined') {1322feedback = [-1];1323} else {1324feedback = [event.index];1325}1326}13271328// sanitize feedback list1329feedback = distinct(feedback).filter(i => i >= -1 && i < this.length).sort((a, b) => a - b);1330feedback = feedback[0] === -1 ? [-1] : feedback;13311332let dragOverEffectPosition = typeof result !== 'boolean' && result.effect && result.effect.position ? result.effect.position : ListDragOverEffectPosition.Over;13331334if (equalsDragFeedback(this.currentDragFeedback, feedback) && this.currentDragFeedbackPosition === dragOverEffectPosition) {1335return true;1336}13371338this.currentDragFeedback = feedback;1339this.currentDragFeedbackPosition = dragOverEffectPosition;1340this.currentDragFeedbackDisposable.dispose();13411342if (feedback[0] === -1) { // entire list feedback1343this.domNode.classList.add(dragOverEffectPosition);1344this.rowsContainer.classList.add(dragOverEffectPosition);1345this.currentDragFeedbackDisposable = toDisposable(() => {1346this.domNode.classList.remove(dragOverEffectPosition);1347this.rowsContainer.classList.remove(dragOverEffectPosition);1348});1349} else {13501351if (feedback.length > 1 && dragOverEffectPosition !== ListDragOverEffectPosition.Over) {1352throw new Error('Can\'t use multiple feedbacks with position different than \'over\'');1353}13541355// Make sure there is no flicker when moving between two items1356// Always use the before feedback if possible1357if (dragOverEffectPosition === ListDragOverEffectPosition.After) {1358if (feedback[0] < this.length - 1) {1359feedback[0] += 1;1360dragOverEffectPosition = ListDragOverEffectPosition.Before;1361}1362}13631364for (const index of feedback) {1365const item = this.items[index]!;1366item.dropTarget = true;13671368item.row?.domNode.classList.add(dragOverEffectPosition);1369}13701371this.currentDragFeedbackDisposable = toDisposable(() => {1372for (const index of feedback) {1373const item = this.items[index]!;1374item.dropTarget = false;13751376item.row?.domNode.classList.remove(dragOverEffectPosition);1377}1378});1379}13801381return true;1382}13831384private onDragLeave(event: IListDragEvent<T>): void {1385this.onDragLeaveTimeout.dispose();1386this.onDragLeaveTimeout = disposableTimeout(() => this.clearDragOverFeedback(), 100, this.disposables);1387if (this.currentDragData) {1388this.dnd.onDragLeave?.(this.currentDragData, event.element, event.index, event.browserEvent);1389}1390}13911392private onDrop(event: IListDragEvent<T>): void {1393if (!this.canDrop) {1394return;1395}13961397const dragData = this.currentDragData;1398this.teardownDragAndDropScrollTopAnimation();1399this.clearDragOverFeedback();1400this.domNode.classList.remove('dragging');1401this.currentDragData = undefined;1402StaticDND.CurrentDragAndDropData = undefined;14031404if (!dragData || !event.browserEvent.dataTransfer) {1405return;1406}14071408event.browserEvent.preventDefault();1409dragData.update(event.browserEvent.dataTransfer);1410this.dnd.drop(dragData, event.element, event.index, event.sector, event.browserEvent);1411}14121413private onDragEnd(event: DragEvent): void {1414this.canDrop = false;1415this.teardownDragAndDropScrollTopAnimation();1416this.clearDragOverFeedback();1417this.domNode.classList.remove('dragging');1418this.currentDragData = undefined;1419StaticDND.CurrentDragAndDropData = undefined;14201421this.dnd.onDragEnd?.(event);1422}14231424private clearDragOverFeedback(): void {1425this.currentDragFeedback = undefined;1426this.currentDragFeedbackPosition = undefined;1427this.currentDragFeedbackDisposable.dispose();1428this.currentDragFeedbackDisposable = Disposable.None;1429}14301431// DND scroll top animation14321433private setupDragAndDropScrollTopAnimation(event: DragEvent | MouseEvent): void {1434if (!this.dragOverAnimationDisposable) {1435const viewTop = getTopLeftOffset(this.domNode).top;1436this.dragOverAnimationDisposable = animate(getWindow(this.domNode), this.animateDragAndDropScrollTop.bind(this, viewTop));1437}14381439this.dragOverAnimationStopDisposable.dispose();1440this.dragOverAnimationStopDisposable = disposableTimeout(() => {1441if (this.dragOverAnimationDisposable) {1442this.dragOverAnimationDisposable.dispose();1443this.dragOverAnimationDisposable = undefined;1444}1445}, 1000, this.disposables);14461447this.dragOverMouseY = event.pageY;1448}14491450private animateDragAndDropScrollTop(viewTop: number): void {1451if (this.dragOverMouseY === undefined) {1452return;1453}14541455const diff = this.dragOverMouseY - viewTop;1456const upperLimit = this.renderHeight - 35;14571458if (diff < 35) {1459this.scrollTop += Math.max(-14, Math.floor(0.3 * (diff - 35)));1460} else if (diff > upperLimit) {1461this.scrollTop += Math.min(14, Math.floor(0.3 * (diff - upperLimit)));1462}1463}14641465private teardownDragAndDropScrollTopAnimation(): void {1466this.dragOverAnimationStopDisposable.dispose();14671468if (this.dragOverAnimationDisposable) {1469this.dragOverAnimationDisposable.dispose();1470this.dragOverAnimationDisposable = undefined;1471}1472}14731474// Util14751476private getTargetSector(browserEvent: DragEvent, targetIndex: number | undefined): ListViewTargetSector | undefined {1477if (targetIndex === undefined) {1478return undefined;1479}14801481const relativePosition = browserEvent.offsetY / this.items[targetIndex].size;1482const sector = Math.floor(relativePosition / 0.25);1483return clamp(sector, 0, 3);1484}14851486private getItemIndexFromEventTarget(target: EventTarget | null): number | undefined {1487const scrollableElement = this.scrollableElement.getDomNode();1488let element: HTMLElement | SVGElement | null = target as (HTMLElement | SVGElement | null);14891490while ((isHTMLElement(element) || isSVGElement(element)) && element !== this.rowsContainer && scrollableElement.contains(element)) {1491const rawIndex = element.getAttribute('data-index');14921493if (rawIndex) {1494const index = Number(rawIndex);14951496if (!isNaN(index)) {1497return index;1498}1499}15001501element = element.parentElement;1502}15031504return undefined;1505}15061507private getVisibleRange(renderTop: number, renderHeight: number): IRange {1508return {1509start: this.rangeMap.indexAt(renderTop),1510end: this.rangeMap.indexAfter(renderTop + renderHeight - 1)1511};1512}15131514protected getRenderRange(renderTop: number, renderHeight: number): IRange {1515const range = this.getVisibleRange(renderTop, renderHeight);1516if (this.currentSelectionBounds) {1517const max = this.rangeMap.count;1518range.start = Math.min(range.start, this.currentSelectionBounds.start, max);1519range.end = Math.min(Math.max(range.end, this.currentSelectionBounds.end + 1), max);1520}15211522return range;1523}15241525/**1526* Given a stable rendered state, checks every rendered element whether it needs1527* to be probed for dynamic height. Adjusts scroll height and top if necessary.1528*/1529protected _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void {1530const previousRenderRange = this.getRenderRange(renderTop, renderHeight);15311532// Let's remember the second element's position, this helps in scrolling up1533// and preserving a linear upwards scroll movement1534let anchorElementIndex: number | undefined;1535let anchorElementTopDelta: number | undefined;15361537if (renderTop === this.elementTop(previousRenderRange.start)) {1538anchorElementIndex = previousRenderRange.start;1539anchorElementTopDelta = 0;1540} else if (previousRenderRange.end - previousRenderRange.start > 1) {1541anchorElementIndex = previousRenderRange.start + 1;1542anchorElementTopDelta = this.elementTop(anchorElementIndex) - renderTop;1543}15441545let heightDiff = 0;15461547while (true) {1548const renderRange = this.getRenderRange(renderTop, renderHeight);15491550let didChange = false;15511552for (let i = renderRange.start; i < renderRange.end; i++) {1553const diff = this.probeDynamicHeight(i);15541555if (diff !== 0) {1556this.rangeMap.splice(i, 1, [this.items[i]]);1557}15581559heightDiff += diff;1560didChange = didChange || diff !== 0;1561}15621563if (!didChange) {1564if (heightDiff !== 0) {1565this.eventuallyUpdateScrollDimensions();1566}15671568const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange);15691570for (const range of unrenderRanges) {1571for (let i = range.start; i < range.end; i++) {1572if (this.items[i].row) {1573this.removeItemFromDOM(i);1574}1575}1576}15771578const renderRanges = Range.relativeComplement(renderRange, previousRenderRange).reverse();15791580for (const range of renderRanges) {1581for (let i = range.end - 1; i >= range.start; i--) {1582this.insertItemInDOM(i);1583}1584}15851586for (let i = renderRange.start; i < renderRange.end; i++) {1587if (this.items[i].row) {1588this.updateItemInDOM(this.items[i], i);1589}1590}15911592if (typeof anchorElementIndex === 'number') {1593// To compute a destination scroll top, we need to take into account the current smooth scrolling1594// animation, and then reuse it with a new target (to avoid prolonging the scroll)1595// See https://github.com/microsoft/vscode/issues/1041441596// See https://github.com/microsoft/vscode/pull/1042841597// See https://github.com/microsoft/vscode/issues/1077041598const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;1599const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;1600this.setScrollTop(newScrollTop, inSmoothScrolling);1601}16021603this._onDidChangeContentHeight.fire(this.contentHeight);1604return;1605}1606}1607}16081609private probeDynamicHeight(index: number): number {1610const item = this.items[index];16111612if (!!this.virtualDelegate.getDynamicHeight) {1613const newSize = this.virtualDelegate.getDynamicHeight(item.element);1614if (newSize !== null) {1615const size = item.size;1616item.size = newSize;1617item.lastDynamicHeightWidth = this.renderWidth;1618return newSize - size;1619}1620}16211622if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) {1623return 0;1624}16251626if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) {1627return 0;1628}16291630const size = item.size;16311632if (item.row) {1633item.row.domNode.style.height = '';1634item.size = item.row.domNode.offsetHeight;1635if (item.size === 0 && !isAncestor(item.row.domNode, getWindow(item.row.domNode).document.body)) {1636console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack);1637}1638item.lastDynamicHeightWidth = this.renderWidth;1639return item.size - size;1640}16411642const { row } = this.cache.alloc(item.templateId);1643row.domNode.style.height = '';1644this.rowsContainer.appendChild(row.domNode);16451646const renderer = this.renderers.get(item.templateId);16471648if (!renderer) {1649throw new BugIndicatingError('Missing renderer for templateId: ' + item.templateId);1650}16511652renderer.renderElement(item.element, index, row.templateData);1653item.size = row.domNode.offsetHeight;1654renderer.disposeElement?.(item.element, index, row.templateData);16551656this.virtualDelegate.setDynamicHeight?.(item.element, item.size);16571658item.lastDynamicHeightWidth = this.renderWidth;1659row.domNode.remove();1660this.cache.release(row);16611662return item.size - size;1663}16641665getElementDomId(index: number): string {1666return `${this.domId}_${index}`;1667}16681669// Dispose16701671dispose() {1672for (const item of this.items) {1673item.dragStartDisposable.dispose();1674item.checkedDisposable.dispose();16751676if (item.row) {1677const renderer = this.renderers.get(item.row.templateId);1678if (renderer) {1679renderer.disposeElement?.(item.element, -1, item.row.templateData, undefined);1680renderer.disposeTemplate(item.row.templateData);1681}1682}1683}16841685this.items = [];16861687this.domNode?.remove();16881689this.dragOverAnimationDisposable?.dispose();1690this.disposables.dispose();1691}1692}169316941695