Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 209 additions & 72 deletions src/vs/base/browser/ui/list/listView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ interface IItem<T> {
stale: boolean;
}

interface IDynamicHeightMeasurement<T> {
readonly item: IItem<T>;
readonly index: number;
readonly previousSize: number;
readonly row: IRow;
rendered: boolean;
}

const StaticDND = {
CurrentDragAndDropData: undefined as IDragAndDropData | undefined
};
Expand Down Expand Up @@ -700,7 +708,9 @@ export class ListView<T> implements IListView<T> {
const updateRange = Range.intersect(renderRange, renderedRestRange);

for (let i = updateRange.start; i < updateRange.end; i++) {
this.updateItemInDOM(this.items[i], i);
if (this.items[i].row) {
this.updateItemInDOM(this.items[i], i);
}
}

const removeRanges = Range.relativeComplement(renderedRestRange, renderRange);
Expand Down Expand Up @@ -964,7 +974,7 @@ export class ListView<T> implements IListView<T> {

// DOM operations

private insertItemInDOM(index: number, row?: IRow): void {
private insertItemInDOM(index: number, row?: IRow, alreadyRendered = false): void {
const item = this.items[index];

if (!item.row) {
Expand Down Expand Up @@ -1008,7 +1018,9 @@ export class ListView<T> implements IListView<T> {
throw new Error(`No renderer found for template id ${item.templateId}`);
}

renderer?.renderElement(item.element, index, item.row.templateData, { height: item.size });
if (!alreadyRendered) {
renderer.renderElement(item.element, index, item.row.templateData, { height: item.size });
}

const uri = this.dnd.getDragURI(item.element);
item.dragStartDisposable.dispose();
Expand Down Expand Up @@ -1559,89 +1571,128 @@ export class ListView<T> implements IListView<T> {
* to be probed for dynamic height. Adjusts scroll height and top if necessary.
*/
protected _rerender(renderTop: number, renderHeight: number, inSmoothScrolling?: boolean): void {
const previousRenderRange = this.getRenderRange(renderTop, renderHeight);
let previousRenderRange = this.getRenderRange(renderTop, renderHeight);
const retainedMeasurements = new Map<number, IDynamicHeightMeasurement<T>>();

// Let's remember the second element's position, this helps in scrolling up
// and preserving a linear upwards scroll movement
let anchorElementIndex: number | undefined;
let anchorElementTopDelta: number | undefined;

if (renderTop === this.elementTop(previousRenderRange.start)) {
anchorElementIndex = previousRenderRange.start;
anchorElementTopDelta = 0;
} else if (previousRenderRange.end - previousRenderRange.start > 1) {
anchorElementIndex = previousRenderRange.start + 1;
anchorElementTopDelta = this.elementTop(anchorElementIndex) - renderTop;
}
const updateAnchorElement = () => {
anchorElementIndex = undefined;
anchorElementTopDelta = undefined;

let heightDiff = 0;
if (renderTop === this.elementTop(previousRenderRange.start)) {
anchorElementIndex = previousRenderRange.start;
anchorElementTopDelta = 0;
} else if (previousRenderRange.end - previousRenderRange.start > 1) {
anchorElementIndex = previousRenderRange.start + 1;
anchorElementTopDelta = this.elementTop(anchorElementIndex) - renderTop;
}
};

while (true) {
const renderRange = this.getRenderRange(renderTop, renderHeight);
updateAnchorElement();

let didChange = false;
let heightDiff = 0;

for (let i = renderRange.start; i < renderRange.end; i++) {
const diff = this.probeDynamicHeight(i);
try {
while (true) {
const renderRange = this.getRenderRange(renderTop, renderHeight);

let didChange = false;

const probedItems = this.items.slice(renderRange.start, renderRange.end);
const dynamicHeightDiffs = this.probeDynamicHeights(renderRange, retainedMeasurements);
const modelDidChange = this.items.length < renderRange.end
|| probedItems.some((item, index) => item !== this.items[renderRange.start + index]);
if (modelDidChange) {
for (let index = 0; index < probedItems.length; index++) {
const diff = dynamicHeightDiffs[index];
const currentIndex = this.items.indexOf(probedItems[index]);
if (diff !== 0 && currentIndex !== -1) {
this.rangeMap.splice(currentIndex, 1, [probedItems[index]]);
heightDiff += diff;
}
}

if (diff !== 0) {
this.rangeMap.splice(i, 1, [this.items[i]]);
this.disposeDynamicHeightMeasurements(retainedMeasurements);
previousRenderRange = this.getRenderRange(renderTop, renderHeight);
updateAnchorElement();
continue;
}

heightDiff += diff;
didChange = didChange || diff !== 0;
}
for (let i = renderRange.start; i < renderRange.end; i++) {
const diff = dynamicHeightDiffs[i - renderRange.start];

if (!didChange) {
if (heightDiff !== 0) {
this.eventuallyUpdateScrollDimensions();
if (diff !== 0) {
this.rangeMap.splice(i, 1, [this.items[i]]);
}

heightDiff += diff;
didChange = didChange || diff !== 0;
}

const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange);
if (!didChange) {
if (heightDiff !== 0) {
this.eventuallyUpdateScrollDimensions();
}

for (const range of unrenderRanges) {
for (let i = range.start; i < range.end; i++) {
if (this.items[i].row) {
this.removeItemFromDOM(i);
const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange);

for (const range of unrenderRanges) {
for (let i = range.start; i < range.end; i++) {
if (this.items[i].row) {
this.removeItemFromDOM(i);
}
}
}
}

const renderRanges = Range.relativeComplement(renderRange, previousRenderRange).reverse();
const insertedItems: IItem<T>[] = [];
const insertedItems: IItem<T>[] = [];

for (let i = renderRange.end - 1; i >= renderRange.start; i--) {
const item = this.items[i];
if (!item.row) {
const measurement = retainedMeasurements.get(i);
const canPromoteMeasurement = measurement?.item === item && measurement.item.templateId === item.templateId;
if (canPromoteMeasurement) {
retainedMeasurements.delete(i);
}
this.insertItemInDOM(i, canPromoteMeasurement ? measurement.row : undefined, canPromoteMeasurement);
insertedItems.push(item);
}
}

for (const range of renderRanges) {
for (let i = range.end - 1; i >= range.start; i--) {
this.insertItemInDOM(i);
insertedItems.push(this.items[i]);
this.disposeDynamicHeightMeasurements(retainedMeasurements);

if (this.horizontalScrolling && insertedItems.length > 0) {
this.measureItemWidths(insertedItems);
this.eventuallyUpdateScrollWidth();
}
}

if (this.horizontalScrolling && insertedItems.length > 0) {
this.measureItemWidths(insertedItems);
this.eventuallyUpdateScrollWidth();
}
for (let i = renderRange.start; i < renderRange.end; i++) {
if (this.items[i].row) {
this.updateItemInDOM(this.items[i], i);
}
}

for (let i = renderRange.start; i < renderRange.end; i++) {
if (this.items[i].row) {
this.updateItemInDOM(this.items[i], i);
if (typeof anchorElementIndex === 'number') {
// To compute a destination scroll top, we need to take into account the current smooth scrolling
// animation, and then reuse it with a new target (to avoid prolonging the scroll)
// See https://github.com/microsoft/vscode/issues/104144
// See https://github.com/microsoft/vscode/pull/104284
// See https://github.com/microsoft/vscode/issues/107704
const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;
const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;
this.setScrollTop(newScrollTop, inSmoothScrolling);
}
}

if (typeof anchorElementIndex === 'number') {
// To compute a destination scroll top, we need to take into account the current smooth scrolling
// animation, and then reuse it with a new target (to avoid prolonging the scroll)
// See https://github.com/microsoft/vscode/issues/104144
// See https://github.com/microsoft/vscode/pull/104284
// See https://github.com/microsoft/vscode/issues/107704
const deltaScrollTop = this.scrollable.getFutureScrollPosition().scrollTop - renderTop;
const newScrollTop = this.elementTop(anchorElementIndex) - anchorElementTopDelta! + deltaScrollTop;
this.setScrollTop(newScrollTop, inSmoothScrolling);
this._onDidChangeContentHeight.fire(this.contentHeight);
return;
}

this._onDidChangeContentHeight.fire(this.contentHeight);
return;
}
} finally {
this.disposeDynamicHeightMeasurements(retainedMeasurements);
}
}

Expand All @@ -1651,22 +1702,12 @@ export class ListView<T> implements IListView<T> {
}

private probeDynamicHeightForItem(item: IItem<T>, index: number): number {
if (!!this.virtualDelegate.getDynamicHeight) {
const newSize = this.virtualDelegate.getDynamicHeight(item.element);
if (newSize !== null) {
const size = item.size;
item.size = newSize;
item.lastDynamicHeightWidth = this.renderWidth;
this.publishDynamicHeight(item);
return newSize - size;
}
}

if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) {
return 0;
const delegateHeightDiff = this.probeDynamicHeightFromDelegate(item);
if (delegateHeightDiff !== undefined) {
return delegateHeightDiff;
}

if (!!this.virtualDelegate.hasDynamicHeight && !this.virtualDelegate.hasDynamicHeight(item.element)) {
if (!this.shouldProbeDynamicHeight(item)) {
return 0;
}

Expand Down Expand Up @@ -1709,6 +1750,102 @@ export class ListView<T> implements IListView<T> {
return item.size - size;
}

private probeDynamicHeights(range: IRange, retainedMeasurements: Map<number, IDynamicHeightMeasurement<T>>): number[] {
const diffs = new Array<number>(range.end - range.start).fill(0);
const measurements: IDynamicHeightMeasurement<T>[] = [];

for (let index = range.start; index < range.end; index++) {
const item = this.items[index];
const delegateHeightDiff = this.probeDynamicHeightFromDelegate(item);
if (delegateHeightDiff !== undefined) {
diffs[index - range.start] = delegateHeightDiff;
continue;
}

if (!this.shouldProbeDynamicHeight(item) || retainedMeasurements.has(index)) {
continue;
}

if (item.row) {
item.row.domNode.style.height = '';
measurements.push({ item, index, previousSize: item.size, row: item.row, rendered: true });
continue;
}

const { row } = this.cache.alloc(item.templateId);
const measurement: IDynamicHeightMeasurement<T> = { item, index, previousSize: item.size, row, rendered: false };
retainedMeasurements.set(index, measurement);
measurements.push(measurement);

row.domNode.style.height = '';
this.rowsContainer.appendChild(row.domNode);

const renderer = this.renderers.get(item.templateId);
if (!renderer) {
throw new BugIndicatingError('Missing renderer for templateId: ' + item.templateId);
}

measurement.rendered = true;
renderer.renderElement(item.element, index, row.templateData, { height: item.size });
}

for (const measurement of measurements) {
measurement.item.size = measurement.row.domNode.offsetHeight;
}

for (const measurement of measurements) {
const { item, index, previousSize, row } = measurement;
if (item.size === 0) {
if (!isAncestor(row.domNode, getWindow(row.domNode).document.body)) {
console.warn('Measuring item node that is not in DOM! Add ListView to the DOM before measuring row height!', new Error().stack);
} else {
console.warn('Measured item node at 0px- ensure that ListView is not display:none before measuring row height!', new Error().stack);
}
}

item.lastDynamicHeightWidth = this.renderWidth;
this.publishDynamicHeight(item);
diffs[index - range.start] = item.size - previousSize;
}

return diffs;
}

private disposeDynamicHeightMeasurements(measurements: Map<number, IDynamicHeightMeasurement<T>>): void {
for (const [measurementIndex, { item, index, row, rendered }] of measurements) {
measurements.delete(measurementIndex);
try {
if (rendered) {
this.renderers.get(item.templateId)?.disposeElement?.(item.element, index, row.templateData, { height: item.size });
}
} finally {
row.domNode.remove();
this.cache.release(row);
}
}
}

private probeDynamicHeightFromDelegate(item: IItem<T>): number | undefined {
const newSize = this.virtualDelegate.getDynamicHeight?.(item.element);
if (newSize === undefined || newSize === null) {
return undefined;
}

const size = item.size;
item.size = newSize;
item.lastDynamicHeightWidth = this.renderWidth;
this.publishDynamicHeight(item);
return newSize - size;
}

private shouldProbeDynamicHeight(item: IItem<T>): boolean {
if (!item.hasDynamicHeight || item.lastDynamicHeightWidth === this.renderWidth) {
return false;
}

return !this.virtualDelegate.hasDynamicHeight || this.virtualDelegate.hasDynamicHeight(item.element);
}

private publishDynamicHeight(item: IItem<T>): void {
if (item.size > 0) {
this.virtualDelegate.setDynamicHeight?.(item.element, item.size);
Expand Down
6 changes: 4 additions & 2 deletions src/vs/base/browser/ui/tree/abstractTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,9 +450,11 @@ export class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListR

this.renderer.disposeElement?.(node, index, templateData.templateData, { ...details, indent: templateData.indentSize });

if (typeof details?.height === 'number') {
if (typeof details?.height === 'number' && this.renderedNodes.get(node) === templateData) {
this.renderedNodes.delete(node);
this.renderedElements.delete(node.element);
if (this.renderedElements.get(node.element) === node) {
this.renderedElements.delete(node.element);
}
}
}

Expand Down
Loading
Loading