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
21 changes: 10 additions & 11 deletions extensions/json-language-features/server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions extensions/json-language-features/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
"main": "./out/node/jsonServerMain",
"dependencies": {
"@vscode/l10n": "^0.0.18",
"jsonc-parser": "^4.0.0-next.1",
"request-light": "^0.8.0",
"vscode-json-languageservice": "^6.0.0-next.2",
"vscode-json-languageservice": "^6.0.0-next.3",
"vscode-languageserver": "^10.0.0-next.18",
"vscode-uri": "^3.1.0"
},
Expand Down
165 changes: 116 additions & 49 deletions src/vs/base/browser/ui/tree/abstractTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,16 @@ export interface StickyScrollNode<T, TFilterData> {
readonly endIndex: number;
readonly height: number;
readonly position: number;
readonly sourceNodeEnd: number;
readonly sourceNodePartiallyVisible: boolean;
readonly hasExplicitHeight: boolean;
}

export interface IStickyScrollNodeSourceRange {
readonly start: number;
readonly end: number;
readonly stickyNodeHeight?: number;
readonly estimated?: boolean;
}

function stickyScrollNodeStateEquals<T, TFilterData>(node1: StickyScrollNode<T, TFilterData>, node2: StickyScrollNode<T, TFilterData>) {
Expand All @@ -1285,7 +1295,10 @@ function stickyScrollNodeEquals<T, TFilterData>(node1: StickyScrollNode<T, TFilt
return node1.node.element === node2.node.element &&
node1.startIndex === node2.startIndex &&
node1.height === node2.height &&
node1.endIndex === node2.endIndex;
node1.endIndex === node2.endIndex &&
node1.sourceNodeEnd === node2.sourceNodeEnd &&
node1.sourceNodePartiallyVisible === node2.sourceNodePartiallyVisible &&
node1.hasExplicitHeight === node2.hasExplicitHeight;
}

class StickyScrollState<T, TFilterData, TRef> {
Expand Down Expand Up @@ -1499,28 +1512,43 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
return undefined;
}

if (nextStickyNode === firstVisibleNodeUnderWidget) {
if (!this.nodeIsUncollapsedParent(firstVisibleNodeUnderWidget)) {
return undefined;
}
if (nextStickyNode === firstVisibleNodeUnderWidget && !this.nodeIsUncollapsedParent(firstVisibleNodeUnderWidget)) {
return undefined;
}

if (this.tree.options.stickyScrollShowOnlyWhenNodeFullyHidden) {
return undefined;
}
const sourceRange = this.getStickyScrollNodeSourceRange(nextStickyNode);
if (!sourceRange || !Number.isFinite(sourceRange.start) || !Number.isFinite(sourceRange.end) || sourceRange.end <= sourceRange.start) {
return undefined;
}
const sourceNodeTop = this.view.getElementTop(this.getNodeIndex(nextStickyNode));
const stickyViewportBottom = this.view.scrollTop + stickyNodesHeight;
if (stickyViewportBottom <= sourceNodeTop + sourceRange.start) {
return undefined;
}
const height = this.getStickyScrollNodeHeight(sourceRange, stickyNodesHeight);
const sourceNodePartiallyVisible = stickyViewportBottom + height < sourceNodeTop + sourceRange.end;

if (this.nodeTopAlignsWithStickyNodesBottom(firstVisibleNodeUnderWidget, stickyNodesHeight)) {
return undefined;
}
return this.createStickyScrollNode(nextStickyNode, stickyNodesHeight, height, sourceRange.end, sourceNodePartiallyVisible, sourceRange.stickyNodeHeight !== undefined);
}

private getStickyScrollNodeSourceRange(node: ITreeNode<T, TFilterData>): IStickyScrollNodeSourceRange | undefined {
const defaultRange = { start: 0, end: this.getNodeHeight(node) };
const sourceRangeProvider = this.tree.options.stickyScrollNodeSourceRangeProvider;
if (sourceRangeProvider) {
return sourceRangeProvider(node.element, defaultRange);
}

return this.createStickyScrollNode(nextStickyNode, stickyNodesHeight);
return defaultRange;
}

private nodeTopAlignsWithStickyNodesBottom(node: ITreeNode<T, TFilterData>, stickyNodesHeight: number): boolean {
const nodeIndex = this.getNodeIndex(node);
const elementTop = this.view.getElementTop(nodeIndex);
const stickyPosition = stickyNodesHeight;
return this.view.scrollTop === elementTop - stickyPosition;
private getStickyScrollNodeHeight(sourceRange: IStickyScrollNodeSourceRange, currentStickyNodesHeight: number): number {
const height = this.clampNodeHeight(sourceRange.stickyNodeHeight ?? sourceRange.end - sourceRange.start);
if (!sourceRange.estimated) {
return height;
}

const availableHeight = Math.max(1, this.view.renderHeight * this.maxWidgetViewRatio - currentStickyNodesHeight);
return Math.min(height, availableHeight);
}

private getNodeHeight(node: ITreeNode<T, TFilterData>): number {
Expand All @@ -1537,13 +1565,12 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
return max !== undefined ? Math.min(height, max) : height;
}

private createStickyScrollNode(node: ITreeNode<T, TFilterData>, currentStickyNodesHeight: number): StickyScrollNode<T, TFilterData> {
const height = this.clampNodeHeight(this.getNodeHeight(node));
private createStickyScrollNode(node: ITreeNode<T, TFilterData>, currentStickyNodesHeight: number, height: number, sourceNodeEnd: number, sourceNodePartiallyVisible: boolean, hasExplicitHeight: boolean): StickyScrollNode<T, TFilterData> {
const { startIndex, endIndex } = this.getNodeRange(node);

const position = this.calculateStickyNodePosition(endIndex, currentStickyNodesHeight, height);

return { node, position, height, startIndex, endIndex };
return { node, position, height, startIndex, endIndex, sourceNodeEnd, sourceNodePartiallyVisible, hasExplicitHeight };
}

private getAncestorUnderPrevious(node: ITreeNode<T, TFilterData>, previousAncestor: ITreeNode<T, TFilterData> | undefined = undefined): ITreeNode<T, TFilterData> | undefined {
Expand All @@ -1566,23 +1593,8 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
}

private calculateStickyNodePosition(lastDescendantIndex: number, stickyRowPositionTop: number, stickyNodeHeight: number): number {
let lastChildRelativeTop = this.view.getRelativeTop(lastDescendantIndex);

// If the last descendant is only partially visible at the top of the view, getRelativeTop() returns null
// In that case, utilize the next node's relative top to calculate the sticky node's position
if (lastChildRelativeTop === null && this.view.firstVisibleIndex === lastDescendantIndex && lastDescendantIndex + 1 < this.view.length) {
const nodeHeight = this.view.getElementHeight(lastDescendantIndex);
const nextNodeRelativeTop = this.view.getRelativeTop(lastDescendantIndex + 1);
lastChildRelativeTop = nextNodeRelativeTop ? nextNodeRelativeTop - nodeHeight / this.view.renderHeight : null;
}

if (lastChildRelativeTop === null) {
return stickyRowPositionTop;
}

const lastChildHeight = this.view.getElementHeight(lastDescendantIndex);
const topOfLastChild = lastChildRelativeTop * this.view.renderHeight;
const bottomOfLastChild = topOfLastChild + lastChildHeight;
const bottomOfLastChild = this.view.getElementTop(lastDescendantIndex) + lastChildHeight - this.view.scrollTop;

if (stickyRowPositionTop + stickyNodeHeight > bottomOfLastChild && stickyRowPositionTop <= bottomOfLastChild) {
return bottomOfLastChild - stickyNodeHeight;
Expand Down Expand Up @@ -1660,7 +1672,10 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {

let widgetHeight = 0;
for (let i = 0; i < ancestors.length && i < this.stickyScrollMaxItemCount; i++) {
widgetHeight += this.clampNodeHeight(this.getNodeHeight(ancestors[i]));
const sourceRange = this.getStickyScrollNodeSourceRange(ancestors[i]);
if (sourceRange) {
widgetHeight += this.getStickyScrollNodeHeight(sourceRange, widgetHeight);
}
}
return widgetHeight;
}
Expand Down Expand Up @@ -1692,6 +1707,15 @@ class StickyScrollController<T, TFilterData, TRef> extends Disposable {
}
}

refresh(): void {
this.update();
}

rerender(): void {
this._widget.rerender();
this.update();
}

validateStickySettings(options: IAbstractTreeOptionsUpdate<T>): { stickyScrollMaxItemCount: number } {
let stickyScrollMaxItemCount = 7;
if (typeof options.stickyScrollMaxItemCount === 'number') {
Expand All @@ -1706,6 +1730,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
private readonly _rootDomNode: HTMLElement;
private _previousState: StickyScrollState<T, TFilterData, TRef> | undefined;
private _previousElements: HTMLElement[] = [];
private _previousElementHeights: number[] = [];
private readonly _previousStateDisposables: DisposableStore = new DisposableStore();
get state(): StickyScrollState<T, TFilterData, TRef> | undefined { return this._previousState; }

Expand Down Expand Up @@ -1758,6 +1783,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {

// If state has not changed, do nothing
if ((!wasVisible && !isVisible) || (wasVisible && isVisible && this._previousState!.equal(state))) {
this.updateSourceNodeVisibility(state);
return;
}

Expand All @@ -1767,8 +1793,10 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
}

if (!isVisible) {
this.updateSourceNodeVisibility(undefined);
this._previousState = undefined;
this._previousElements = [];
this._previousElementHeights = [];
this._previousStateDisposables.clear();
return;
}
Expand All @@ -1786,9 +1814,28 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {

this._previousState = state;

this.updateSourceNodeVisibility(state);
this.updateRootHeight(state);
}

private updateSourceNodeVisibility(state: StickyScrollState<T, TFilterData, TRef> | undefined): void {
let sourceNodePartiallyVisible = false;
for (let i = 0; state && i < state.count; i++) {
const stickyNode = state.stickyNodes[i];
const stickyElement = this._previousElements[i];
let nodePartiallyVisible = stickyNode.sourceNodePartiallyVisible;
if (stickyElement) {
const sourceNodeBottom = this.view.getElementTop(stickyNode.startIndex) + stickyNode.sourceNodeEnd;
const stickyElementHeight = this.getRenderedNodeHeight(stickyNode, i);
const stickyNodeBottom = this.view.scrollTop + stickyNode.position + stickyElementHeight;
nodePartiallyVisible = stickyNodeBottom < sourceNodeBottom;
}
stickyElement?.classList.toggle('source-node-partially-visible', nodePartiallyVisible);
sourceNodePartiallyVisible ||= nodePartiallyVisible;
}
this._rootDomNode.classList.toggle('source-node-partially-visible', sourceNodePartiallyVisible);
}

private renderState(state: StickyScrollState<T, TFilterData, TRef>): void {
this._previousStateDisposables.clear();

Expand All @@ -1807,13 +1854,13 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {

this._previousElements = elements;

// Probe dynamic heights after rendering into DOM
this.probeDynamicHeights(state, elements);
this._previousElementHeights = this.probeDynamicHeights(state, elements);
}

rerender(): void {
if (this._previousState) {
this.renderState(this._previousState);
this.updateSourceNodeVisibility(this._previousState);
this.updateRootHeight(this._previousState);
}
}
Expand All @@ -1824,17 +1871,25 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {

private getRootHeight(state: StickyScrollState<T, TFilterData, TRef>): number {
const lastStickyNode = state.stickyNodes[state.count - 1];
const lastStickyElement = this._previousElements[state.count - 1];
const lastStickyElementHeight = lastStickyElement?.offsetHeight ?? lastStickyNode.height;
return lastStickyNode.position + lastStickyElementHeight;
const lastStickyElementHeight = this.getRenderedNodeHeight(lastStickyNode, state.count - 1);
return Math.max(0, lastStickyNode.position + lastStickyElementHeight);
}

private probeDynamicHeights(state: StickyScrollState<T, TFilterData, TRef>, elements: HTMLElement[]): void {
private getRenderedNodeHeight(stickyNode: StickyScrollNode<T, TFilterData>, index: number): number {
return Math.min(this._previousElementHeights[index] ?? stickyNode.height, stickyNode.height);
}

private probeDynamicHeights(state: StickyScrollState<T, TFilterData, TRef>, elements: HTMLElement[]): number[] {
const heightChanges: { index: number; height: number }[] = [];
const elementHeights = state.stickyNodes.map(node => node.height);

for (let i = 0; i < state.count; i++) {
const stickyNode = state.stickyNodes[i];
if (!this.treeDelegate.hasDynamicHeight || !this.treeDelegate.hasDynamicHeight(stickyNode.node)) {
const measuredHeight = elements[i].offsetHeight;
if (measuredHeight > 0) {
elementHeights[i] = measuredHeight;
}
continue;
}

Expand All @@ -1850,25 +1905,28 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {
}
const maxNodeHeight = this.tree.options.stickyScrollMaxNodeHeight;
const clampedMeasuredHeight = maxNodeHeight !== undefined ? Math.min(measuredHeight, maxNodeHeight) : measuredHeight;
const renderedHeight = stickyNode.hasExplicitHeight ? stickyNode.height : clampedMeasuredHeight;
elementHeights[i] = renderedHeight;

// Always update the sticky element's visual height to match the measured content
if (this.tree.options.setRowHeight !== false) {
element.style.height = `${clampedMeasuredHeight}px`;
element.style.height = `${renderedHeight}px`;
}
if (this.tree.options.setRowLineHeight !== false) {
element.style.lineHeight = `${clampedMeasuredHeight}px`;
element.style.lineHeight = `${renderedHeight}px`;
}

// Only propagate height increases to the real row — never shrink it,
// since sticky elements may have CSS truncation (e.g. line-clamp).
if (clampedMeasuredHeight > stickyNode.height) {
// A sticky row may represent only part of its source, so never shrink the source row.
if (!stickyNode.hasExplicitHeight && clampedMeasuredHeight > this.view.getElementHeight(stickyNode.startIndex)) {
heightChanges.push({ index: stickyNode.startIndex, height: clampedMeasuredHeight });
}
}

if (heightChanges.length > 0) {
this._onDidChangeHeight.fire(heightChanges);
}

return elementHeights;
}

private createElement(stickyNode: StickyScrollNode<T, TFilterData>, stickyIndex: number, stickyNodesTotal: number): { element: HTMLElement; disposable: IDisposable } {
Expand All @@ -1894,6 +1952,7 @@ class StickyScrollWidget<T, TFilterData, TRef> implements IDisposable {

stickyElement.classList.add('monaco-tree-sticky-row');
stickyElement.classList.add('monaco-list-row');
stickyElement.classList.toggle('source-node-partially-visible', stickyNode.sourceNodePartiallyVisible);

stickyElement.setAttribute('data-index', `${nodeIndex}`);
stickyElement.setAttribute('data-parity', nodeIndex % 2 === 0 ? 'even' : 'odd');
Expand Down Expand Up @@ -2311,7 +2370,6 @@ export interface IAbstractTreeOptionsUpdate<T> extends ITreeRendererOptions<T> {
readonly enableStickyScroll?: boolean;
readonly stickyScrollMaxItemCount?: number;
readonly stickyScrollMaxNodeHeight?: number;
readonly stickyScrollShowOnlyWhenNodeFullyHidden?: boolean;
readonly paddingTop?: number;
}

Expand All @@ -2327,6 +2385,7 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
readonly findWidgetContainer?: HTMLElement;
readonly defaultFindVisibility?: TreeVisibility | ((e: T) => TreeVisibility);
readonly stickyScrollDelegate?: IStickyScrollDelegate<T, TFilterData>;
readonly stickyScrollNodeSourceRangeProvider?: (element: T, defaultRange: IStickyScrollNodeSourceRange) => IStickyScrollNodeSourceRange | undefined;
readonly disableExpandOnSpacebar?: boolean; // defaults to false
}

Expand Down Expand Up @@ -2909,6 +2968,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
return this.view.renderHeight;
}

refreshStickyScroll(): void {
this.stickyScrollController?.refresh();
}

rerenderStickyScroll(): void {
this.stickyScrollController?.rerender();
}

get firstVisibleElement(): T | undefined {
let index = this.view.firstVisibleIndex;

Expand Down
Loading
Loading