Notebook cell list reveal api polish (#171104)

* CellReveal Sync.

* RevealCellAsync and RevealCellLineAsync

* RevealCellRangeAsync

* :polish: name

* Call reveal api directly on CellList

* Simplify cell reveal api in nb editor.

* Minimal ListViewInfoAccessor

* Replace RevealLine with RevealRange

* Group reveal cell methods.

* inline revealCellAsync

* drop revealElementInView
This commit is contained in:
Peng Lyu
2023-01-11 15:20:29 -08:00
committed by GitHub
parent 666415c90c
commit e3d247b542
5 changed files with 320 additions and 489 deletions
@@ -226,6 +226,7 @@ export interface ICellViewModel extends IGenericCellViewModel {
readonly onDidChangeCellStatusBarItems: Event<void>;
readonly onCellDecorationsChanged: Event<{ added: INotebookCellDecorationOptions[]; removed: INotebookCellDecorationOptions[] }>;
readonly onDidChangeState: Event<CellViewModelStateChangeEvent>;
readonly onDidChangeEditorAttachState: Event<void>;
readonly editStateSource: string;
readonly editorAttached: boolean;
isInputCollapsed: boolean;
@@ -313,6 +314,18 @@ export interface INotebookDeltaCellStatusBarItems {
readonly items: readonly INotebookCellStatusBarItem[];
}
export const enum CellRevealSyncType {
Default = 1,
Top = 2,
Center = 3,
CenterIfOutsideViewport = 4
}
export enum CellRevealRangeType {
Default = 1,
Center = 2,
CenterIfOutsideViewport = 3,
}
export enum CellRevealType {
NearTopIfOutsideViewport,
@@ -48,7 +48,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { contrastBorder, errorForeground, focusBorder, foreground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, transparent } from 'vs/platform/theme/common/colorRegistry';
import { EDITOR_PANE_BACKGROUND, PANEL_BORDER, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugColors';
import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellEditState, CellFindMatchWithIndex, CellFocusMode, CellLayoutContext, CellRevealRangeType, CellRevealSyncType, CellRevealType, IActiveNotebookEditorDelegate, IBaseCellEditorOptions, ICellOutputViewModel, ICellViewModel, ICommonCellInfo, IDisplayOutputLayoutUpdateRequest, IFocusNotebookCellOptions, IInsetRenderOutput, IModelDecorationsChangeAccessor, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorDelegate, INotebookEditorMouseEvent, INotebookEditorOptions, INotebookEditorViewState, INotebookViewCellsUpdateEvent, INotebookWebviewMessage, RenderOutputType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions';
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
import { notebookDebug } from 'vs/workbench/contrib/notebook/browser/notebookLogger';
@@ -1161,9 +1161,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
cell.focusMode = CellFocusMode.Editor;
await this.revealLineInCenterIfOutsideViewportAsync(cell, selection.startLineNumber);
} else if (options?.cellRevealType === CellRevealType.NearTopIfOutsideViewport) {
await this.revealNearTopIfOutsideViewportAync(cell);
await this._list.revealCellAsync(cell, CellRevealType.NearTopIfOutsideViewport);
} else {
await this.revealInCenterIfOutsideViewportAsync(cell);
await this._list.revealCellAsync(cell, CellRevealType.CenterIfOutsideViewport);
}
const editor = this._renderedEditors.get(cell)!;
@@ -1306,8 +1306,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
this._webview = this.instantiationService.createInstance(BackLayerWebView, {
get creationOptions() { return that.creationOptions; },
setScrollTop(scrollTop: number) { that._listViewInfoAccessor.setScrollTop(scrollTop); },
triggerScroll(event: IMouseWheelEvent) { that._listViewInfoAccessor.triggerScroll(event); },
setScrollTop(scrollTop: number) { that._list.scrollTop = scrollTop; },
triggerScroll(event: IMouseWheelEvent) { that._list.triggerScrollFromMouseWheelEvent(event); },
getCellByInfo: that.getCellByInfo.bind(that),
getCellById: that._getCellById.bind(that),
toggleNotebookCellSelection: that._toggleNotebookCellSelection.bind(that),
@@ -1950,63 +1950,55 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
}
scrollToBottom() {
this._listViewInfoAccessor.scrollToBottom();
this._list.scrollToBottom();
}
revealCellRangeInView(range: ICellRange) {
return this._listViewInfoAccessor.revealCellRangeInView(range);
return this._list.revealCellsInView(range);
}
revealInView(cell: ICellViewModel) {
this._listViewInfoAccessor.revealInView(cell);
this._list.revealCell(cell, CellRevealSyncType.Default);
}
revealInViewAtTop(cell: ICellViewModel) {
this._listViewInfoAccessor.revealInViewAtTop(cell);
}
revealInCenterIfOutsideViewport(cell: ICellViewModel) {
this._listViewInfoAccessor.revealInCenterIfOutsideViewport(cell);
}
async revealInCenterIfOutsideViewportAsync(cell: ICellViewModel) {
return this._listViewInfoAccessor.revealInCenterIfOutsideViewportAsync(cell);
this._list.revealCell(cell, CellRevealSyncType.Top);
}
revealInCenter(cell: ICellViewModel) {
this._listViewInfoAccessor.revealInCenter(cell);
this._list.revealCell(cell, CellRevealSyncType.Center);
}
revealNearTopIfOutsideViewportAync(cell: ICellViewModel) {
return this._listViewInfoAccessor.revealNearTopIfOutsideViewportAync(cell);
revealInCenterIfOutsideViewport(cell: ICellViewModel) {
this._list.revealCell(cell, CellRevealSyncType.CenterIfOutsideViewport);
}
async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise<void> {
return this._listViewInfoAccessor.revealLineInViewAsync(cell, line);
return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Default);
}
async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise<void> {
return this._listViewInfoAccessor.revealLineInCenterAsync(cell, line);
return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.Center);
}
async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise<void> {
return this._listViewInfoAccessor.revealLineInCenterIfOutsideViewportAsync(cell, line);
return this._list.revealCellRangeAsync(cell, new Range(line, 1, line, 1), CellRevealRangeType.CenterIfOutsideViewport);
}
async revealRangeInViewAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this._listViewInfoAccessor.revealRangeInViewAsync(cell, range);
return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Default);
}
async revealRangeInCenterAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this._listViewInfoAccessor.revealRangeInCenterAsync(cell, range);
return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.Center);
}
async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this._listViewInfoAccessor.revealRangeInCenterIfOutsideViewportAsync(cell, range);
return this._list.revealCellRangeAsync(cell, range, CellRevealRangeType.CenterIfOutsideViewport);
}
async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise<void> {
return this._listViewInfoAccessor.revealCellOffsetInCenterAsync(cell, offset);
return this._list.revealCellOffsetInCenterAsync(cell, offset);
}
getViewIndexByModelIndex(index: number): number {
@@ -2038,11 +2030,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD
}
setCellEditorSelection(cell: ICellViewModel, range: Range): void {
this._listViewInfoAccessor.setCellEditorSelection(cell, range);
this._list.setCellEditorSelection(cell, range);
}
setHiddenAreas(_ranges: ICellRange[]): boolean {
return this._listViewInfoAccessor.setHiddenAreas(_ranges);
return this._list.setHiddenAreas(_ranges, true);
}
getVisibleRangesPlusViewportAboveAndBelow(): ICellRange[] {
@@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { CursorAtBoundary, ICellViewModel, CellEditState, CellFocusMode, ICellOutputViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CursorAtBoundary, ICellViewModel, CellEditState, CellFocusMode, ICellOutputViewModel, CellRevealType, CellRevealSyncType, CellRevealRangeType } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
import { diff, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, SelectionStateType } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ICellRange, cellRangesToIndexes, reduceCellRanges, cellRangesEqual } from 'vs/workbench/contrib/notebook/common/notebookRange';
@@ -31,7 +31,7 @@ import { FastDomNode } from 'vs/base/browser/fastDomNode';
import { MarkupCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markupCellViewModel';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
const enum CellRevealType {
const enum CellEditorRevealType {
Line,
Range
}
@@ -222,7 +222,11 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
if (focus && focus.cellKind === CellKind.Markup && !focus.isInputCollapsed && !this._viewModel?.options.isReadOnly) {
// scroll the cell into view if out of viewport
this.revealElementInView(focus);
const focusedCellIndex = this._getViewIndexUpperBound(focus);
if (focusedCellIndex >= 0) {
this._revealInViewWithMinimalScrolling(focusedCellIndex);
}
focus.updateEditState(CellEditState.Editing, 'dbclick');
focus.focusMode = CellFocusMode.Editor;
}
@@ -735,7 +739,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
/**
* The range will be revealed with as little scrolling as possible.
*/
revealElementsInView(range: ICellRange) {
revealCellsInView(range: ICellRange) {
const startIndex = this._getViewIndexUpperBound2(range.start);
if (startIndex < 0) {
@@ -776,24 +780,16 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
}
}
this._revealInView(startIndex);
this._revealInViewWithMinimalScrolling(startIndex);
}
isScrolledToBottom() {
if (this.length === 0) {
return true;
private _revealInViewWithMinimalScrolling(viewIndex: number) {
const firstIndex = this.view.firstVisibleIndex;
if (viewIndex <= firstIndex) {
this._revealInternal(viewIndex, true, CellRevealPosition.Top);
} else {
this._revealInternal(viewIndex, true, CellRevealPosition.Bottom);
}
const last = this.length - 1;
const bottom = this.view.elementHeight(last) + this.view.elementTop(last);
const wrapperBottom = this.getViewScrollTop() + this.view.renderHeight;
if (bottom <= wrapperBottom) {
return true;
}
return false;
}
scrollToBottom() {
@@ -805,109 +801,292 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
this.view.setScrollTop(scrollHeight - (wrapperBottom - scrollTop) - topInsertToolbarHeight);
}
revealElementInView(cell: ICellViewModel) {
//#region Reveal Cell synchronously
revealCell(cell: ICellViewModel, revealType: CellRevealSyncType) {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
this._revealInView(index);
if (index < 0) {
return;
}
switch (revealType) {
case CellRevealSyncType.Top:
this._revealInternal(index, false, CellRevealPosition.Top);
break;
case CellRevealSyncType.Center:
this._revealInternal(index, false, CellRevealPosition.Center);
break;
case CellRevealSyncType.CenterIfOutsideViewport:
this._revealInternal(index, true, CellRevealPosition.Center);
break;
case CellRevealSyncType.Default:
this._revealInViewWithMinimalScrolling(index);
break;
}
}
revealElementInViewAtTop(cell: ICellViewModel) {
const index = this._getViewIndexUpperBound(cell);
private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) {
if (viewIndex >= this.view.length) {
return;
}
if (index >= 0) {
this._revealInternal(index, false, CellRevealPosition.Top);
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const elementBottom = this.view.elementHeight(viewIndex) + elementTop;
if (ignoreIfInsideViewport
&& elementTop >= scrollTop
&& elementBottom < wrapperBottom) {
if (revealPosition === CellRevealPosition.Center
&& elementBottom > wrapperBottom
&& elementTop > (scrollTop + wrapperBottom) / 2) {
// the element is partially visible and it's below the center of the viewport
} else {
return;
}
}
switch (revealPosition) {
case CellRevealPosition.Top:
this.view.setScrollTop(elementTop);
this.view.setScrollTop(this.view.elementTop(viewIndex));
break;
case CellRevealPosition.Center:
case CellRevealPosition.NearTop:
{
// reveal the cell top in the viewport center initially
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
// cell rendered already, we now have a more accurate cell height
const newElementTop = this.view.elementTop(viewIndex);
const newElementHeight = this.view.elementHeight(viewIndex);
const renderHeight = this.getViewScrollBottom() - this.getViewScrollTop();
if (newElementHeight >= renderHeight) {
// cell is larger than viewport, reveal top
this.view.setScrollTop(newElementTop);
} else if (revealPosition === CellRevealPosition.Center) {
this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2));
} else if (revealPosition === CellRevealPosition.NearTop) {
this.view.setScrollTop(newElementTop - (renderHeight / 5));
}
}
break;
case CellRevealPosition.Bottom:
this.view.setScrollTop(this.scrollTop + (elementBottom - wrapperBottom));
this.view.setScrollTop(this.scrollTop + (this.view.elementTop(viewIndex) + this.view.elementHeight(viewIndex) - this.getViewScrollBottom()));
break;
default:
break;
}
}
revealElementInCenterIfOutsideViewport(cell: ICellViewModel) {
//#endregion
//#region Reveal Cell asynchronously
async revealCellAsync(cell: ICellViewModel, revealType: CellRevealType) {
const viewIndex = this._getViewIndexUpperBound(cell);
if (viewIndex < 0) {
return;
}
const revealPosition = revealType === CellRevealType.NearTopIfOutsideViewport ? CellRevealPosition.NearTop : CellRevealPosition.Center;
this._revealInternal(viewIndex, true, revealPosition);
// wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor)
if (cell.getEditState() === CellEditState.Editing && !cell.editorAttached) {
return getEditorAttachedPromise(cell);
}
return;
}
//#endregion
//#region Reveal Cell Editor Range asynchronously
async revealCellRangeAsync(cell: ICellViewModel, range: Range, revealType: CellRevealRangeType): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
this._revealInCenterIfOutsideViewport(index);
if (index < 0) {
return;
}
switch (revealType) {
case CellRevealRangeType.Default:
return this._revealRangeInternalAsync(index, range, CellEditorRevealType.Range);
case CellRevealRangeType.Center:
return this._revealRangeInCenterInternalAsync(index, range, CellEditorRevealType.Range);
case CellRevealRangeType.CenterIfOutsideViewport:
return this._revealRangeInCenterIfOutsideViewportInternalAsync(index, range, CellEditorRevealType.Range);
}
}
revealElementInCenter(cell: ICellViewModel) {
const index = this._getViewIndexUpperBound(cell);
// List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done.
// For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view.
// To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport.
private async _revealRangeInternalAsync(viewIndex: number, range: Range, revealType: CellEditorRevealType): Promise<void> {
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const element = this.view.element(viewIndex);
if (index >= 0) {
this._revealInCenter(index);
if (element.editorAttached) {
this._revealRangeCommon(viewIndex, range, revealType, false, false);
} else {
const elementHeight = this.view.elementHeight(viewIndex);
let upwards = false;
if (elementTop + elementHeight < scrollTop) {
// scroll downwards
this.view.setScrollTop(elementTop);
upwards = false;
} else if (elementTop > wrapperBottom) {
// scroll upwards
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
upwards = true;
}
const editorAttachedPromise = new Promise<void>((resolve, reject) => {
element.onDidChangeEditorAttachState(() => {
element.editorAttached ? resolve() : reject();
});
});
return editorAttachedPromise.then(() => {
this._revealRangeCommon(viewIndex, range, revealType, true, upwards);
});
}
}
async revealElementInCenterIfOutsideViewportAsync(cell: ICellViewModel): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
private async _revealRangeInCenterInternalAsync(viewIndex: number, range: Range, revealType: CellEditorRevealType): Promise<void> {
const reveal = (viewIndex: number, range: Range, revealType: CellEditorRevealType) => {
const element = this.view.element(viewIndex);
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset;
this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2);
if (index >= 0) {
return this._revealIfOutsideViewportAsync(index, CellRevealPosition.Center);
if (revealType === CellEditorRevealType.Range) {
element.revealRangeInCenter(range);
}
};
const elementTop = this.view.elementTop(viewIndex);
const viewItemOffset = elementTop;
this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2);
const element = this.view.element(viewIndex);
if (!element.editorAttached) {
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
} else {
reveal(viewIndex, range, revealType);
}
}
async revealNearTopIfOutsideViewportAync(cell: ICellViewModel): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
private async _revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex: number, range: Range, revealType: CellEditorRevealType): Promise<void> {
const reveal = (viewIndex: number, range: Range, revealType: CellEditorRevealType) => {
const element = this.view.element(viewIndex);
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset;
this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2);
if (index >= 0) {
return this._revealIfOutsideViewportAsync(index, CellRevealPosition.NearTop);
if (revealType === CellEditorRevealType.Range) {
element.revealRangeInCenter(range);
}
};
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const viewItemOffset = elementTop;
const element = this.view.element(viewIndex);
const positionOffset = viewItemOffset + element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
if (positionOffset < scrollTop || positionOffset > wrapperBottom) {
// let it render
this.view.setScrollTop(positionOffset - this.view.renderHeight / 2);
// after rendering, it might be pushed down due to markdown cell dynamic height
const newPositionOffset = this.view.elementTop(viewIndex) + element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
this.view.setScrollTop(newPositionOffset - this.view.renderHeight / 2);
// reveal editor
if (!element.editorAttached) {
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
} else {
// for example markdown
}
} else {
if (element.editorAttached) {
element.revealRangeInCenter(range);
} else {
// for example, markdown cell in preview mode
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
}
}
}
async revealElementLineInViewAsync(cell: ICellViewModel, line: number): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
private _revealRangeCommon(viewIndex: number, range: Range, revealType: CellEditorRevealType, newlyCreated: boolean, alignToBottom: boolean) {
const element = this.view.element(viewIndex);
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const elementOriginalHeight = this.view.elementHeight(viewIndex);
if (positionOffset >= elementOriginalHeight) {
// we are revealing a range that is beyond current element height
// if we don't update the element height now, and directly `setTop` to reveal the range
// the element might be scrolled out of view
// next frame, when we update the element height, the element will never be scrolled back into view
const newTotalHeight = element.layoutInfo.totalHeight;
this.updateElementHeight(viewIndex, newTotalHeight);
}
const elementTop = this.view.elementTop(viewIndex);
const positionTop = elementTop + positionOffset;
if (index >= 0) {
return this._revealLineInViewAsync(index, line);
// TODO@rebornix 30 ---> line height * 1.5
if (positionTop < scrollTop) {
this.view.setScrollTop(positionTop - 30);
} else if (positionTop > wrapperBottom) {
this.view.setScrollTop(scrollTop + positionTop - wrapperBottom + 30);
} else if (newlyCreated) {
// newly scrolled into view
if (alignToBottom) {
// align to the bottom
this.view.setScrollTop(scrollTop + positionTop - wrapperBottom + 30);
} else {
// align to to top
this.view.setScrollTop(positionTop - 30);
}
}
if (revealType === CellEditorRevealType.Range) {
element.revealRangeInCenter(range);
}
}
//#endregion
//#region Reveal Cell offset
async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise<void> {
const viewIndex = this._getViewIndexUpperBound(cell);
if (viewIndex >= 0) {
const element = this.view.element(viewIndex);
const elementTop = this.view.elementTop(viewIndex);
if (element instanceof MarkupCellViewModel) {
return this._revealInCenterIfOutsideViewport(viewIndex);
} else {
const rangeOffset = element.layoutInfo.outputContainerOffset + offset;
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
this.view.setScrollTop(elementTop + rangeOffset - this.view.renderHeight / 2);
}
}
}
async revealElementLineInCenterAsync(cell: ICellViewModel, line: number): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealLineInCenterAsync(index, line);
}
private _revealInCenterIfOutsideViewport(viewIndex: number) {
this._revealInternal(viewIndex, true, CellRevealPosition.Center);
}
async revealElementLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealLineInCenterIfOutsideViewportAsync(index, line);
}
}
async revealElementRangeInViewAsync(cell: ICellViewModel, range: Range): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealRangeInView(index, range);
}
}
async revealElementRangeInCenterAsync(cell: ICellViewModel, range: Range): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealRangeInCenterAsync(index, range);
}
}
async revealElementRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealRangeInCenterIfOutsideViewportAsync(index, range);
}
}
async revealElementOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise<void> {
const index = this._getViewIndexUpperBound(cell);
if (index >= 0) {
return this._revealOffset(index, offset);
}
}
//#endregion
domElementOfElement(element: ICellViewModel): HTMLElement | null {
const index = this._getViewIndexUpperBound(element);
@@ -940,7 +1119,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
this.view.delegateVerticalScrollbarPointerDown(browserEvent);
}
isElementAboveViewport(index: number) {
private isElementAboveViewport(index: number) {
const elementTop = this.view.elementTop(index);
const elementBottom = elementTop + this.view.elementHeight(index);
@@ -1031,270 +1210,7 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
return this.getViewScrollTop() + this.view.renderHeight - topInsertToolbarHeight;
}
private _revealOffset(viewIndex: number, offset: number) {
const element = this.view.element(viewIndex);
const elementTop = this.view.elementTop(viewIndex);
if (element instanceof MarkupCellViewModel) {
return this._revealInCenterIfOutsideViewport(viewIndex);
} else {
const rangeOffset = element.layoutInfo.outputContainerOffset + offset;
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
this.view.setScrollTop(elementTop + rangeOffset - this.view.renderHeight / 2);
}
}
private _revealRange(viewIndex: number, range: Range, revealType: CellRevealType, newlyCreated: boolean, alignToBottom: boolean) {
const element = this.view.element(viewIndex);
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const elementOriginalHeight = this.view.elementHeight(viewIndex);
if (positionOffset >= elementOriginalHeight) {
// we are revealing a range that is beyond current element height
// if we don't update the element height now, and directly `setTop` to reveal the range
// the element might be scrolled out of view
// next frame, when we update the element height, the element will never be scrolled back into view
const newTotalHeight = element.layoutInfo.totalHeight;
this.updateElementHeight(viewIndex, newTotalHeight);
}
const elementTop = this.view.elementTop(viewIndex);
const positionTop = elementTop + positionOffset;
// TODO@rebornix 30 ---> line height * 1.5
if (positionTop < scrollTop) {
this.view.setScrollTop(positionTop - 30);
} else if (positionTop > wrapperBottom) {
this.view.setScrollTop(scrollTop + positionTop - wrapperBottom + 30);
} else if (newlyCreated) {
// newly scrolled into view
if (alignToBottom) {
// align to the bottom
this.view.setScrollTop(scrollTop + positionTop - wrapperBottom + 30);
} else {
// align to to top
this.view.setScrollTop(positionTop - 30);
}
}
if (revealType === CellRevealType.Range) {
element.revealRangeInCenter(range);
}
}
// List items have real dynamic heights, which means after we set `scrollTop` based on the `elementTop(index)`, the element at `index` might still be removed from the view once all relayouting tasks are done.
// For example, we scroll item 10 into the view upwards, in the first round, items 7, 8, 9, 10 are all in the viewport. Then item 7 and 8 resize themselves to be larger and finally item 10 is removed from the view.
// To ensure that item 10 is always there, we need to scroll item 10 to the top edge of the viewport.
private async _revealRangeInternalAsync(viewIndex: number, range: Range, revealType: CellRevealType): Promise<void> {
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const element = this.view.element(viewIndex);
if (element.editorAttached) {
this._revealRange(viewIndex, range, revealType, false, false);
} else {
const elementHeight = this.view.elementHeight(viewIndex);
let upwards = false;
if (elementTop + elementHeight < scrollTop) {
// scroll downwards
this.view.setScrollTop(elementTop);
upwards = false;
} else if (elementTop > wrapperBottom) {
// scroll upwards
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
upwards = true;
}
const editorAttachedPromise = new Promise<void>((resolve, reject) => {
element.onDidChangeEditorAttachState(() => {
element.editorAttached ? resolve() : reject();
});
});
return editorAttachedPromise.then(() => {
this._revealRange(viewIndex, range, revealType, true, upwards);
});
}
}
private async _revealLineInViewAsync(viewIndex: number, line: number): Promise<void> {
return this._revealRangeInternalAsync(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line);
}
private async _revealRangeInView(viewIndex: number, range: Range): Promise<void> {
return this._revealRangeInternalAsync(viewIndex, range, CellRevealType.Range);
}
private async _revealRangeInCenterInternalAsync(viewIndex: number, range: Range, revealType: CellRevealType): Promise<void> {
const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => {
const element = this.view.element(viewIndex);
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset;
this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2);
if (revealType === CellRevealType.Range) {
element.revealRangeInCenter(range);
}
};
const elementTop = this.view.elementTop(viewIndex);
const viewItemOffset = elementTop;
this.view.setScrollTop(viewItemOffset - this.view.renderHeight / 2);
const element = this.view.element(viewIndex);
if (!element.editorAttached) {
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
} else {
reveal(viewIndex, range, revealType);
}
}
private async _revealLineInCenterAsync(viewIndex: number, line: number): Promise<void> {
return this._revealRangeInCenterInternalAsync(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line);
}
private _revealRangeInCenterAsync(viewIndex: number, range: Range): Promise<void> {
return this._revealRangeInCenterInternalAsync(viewIndex, range, CellRevealType.Range);
}
private async _revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex: number, range: Range, revealType: CellRevealType): Promise<void> {
const reveal = (viewIndex: number, range: Range, revealType: CellRevealType) => {
const element = this.view.element(viewIndex);
const positionOffset = element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
const positionOffsetInView = this.view.elementTop(viewIndex) + positionOffset;
this.view.setScrollTop(positionOffsetInView - this.view.renderHeight / 2);
if (revealType === CellRevealType.Range) {
element.revealRangeInCenter(range);
}
};
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const viewItemOffset = elementTop;
const element = this.view.element(viewIndex);
const positionOffset = viewItemOffset + element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
if (positionOffset < scrollTop || positionOffset > wrapperBottom) {
// let it render
this.view.setScrollTop(positionOffset - this.view.renderHeight / 2);
// after rendering, it might be pushed down due to markdown cell dynamic height
const newPositionOffset = this.view.elementTop(viewIndex) + element.getPositionScrollTopOffset(range.startLineNumber, range.startColumn);
this.view.setScrollTop(newPositionOffset - this.view.renderHeight / 2);
// reveal editor
if (!element.editorAttached) {
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
} else {
// for example markdown
}
} else {
if (element.editorAttached) {
element.revealRangeInCenter(range);
} else {
// for example, markdown cell in preview mode
return getEditorAttachedPromise(element).then(() => reveal(viewIndex, range, revealType));
}
}
}
private async _revealIfOutsideViewportAsync(viewIndex: number, revealPosition: CellRevealPosition): Promise<void> {
this._revealInternal(viewIndex, true, revealPosition);
const element = this.view.element(viewIndex);
// wait for the editor to be created only if the cell is in editing mode (meaning it has an editor and will focus the editor)
if (element.getEditState() === CellEditState.Editing && !element.editorAttached) {
return getEditorAttachedPromise(element);
}
return;
}
private async _revealLineInCenterIfOutsideViewportAsync(viewIndex: number, line: number): Promise<void> {
return this._revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex, new Range(line, 1, line, 1), CellRevealType.Line);
}
private async _revealRangeInCenterIfOutsideViewportAsync(viewIndex: number, range: Range): Promise<void> {
return this._revealRangeInCenterIfOutsideViewportInternalAsync(viewIndex, range, CellRevealType.Range);
}
private _revealInternal(viewIndex: number, ignoreIfInsideViewport: boolean, revealPosition: CellRevealPosition) {
if (viewIndex >= this.view.length) {
return;
}
const scrollTop = this.getViewScrollTop();
const wrapperBottom = this.getViewScrollBottom();
const elementTop = this.view.elementTop(viewIndex);
const elementBottom = this.view.elementHeight(viewIndex) + elementTop;
if (ignoreIfInsideViewport
&& elementTop >= scrollTop
&& elementBottom < wrapperBottom) {
if (revealPosition === CellRevealPosition.Center
&& elementBottom > wrapperBottom
&& elementTop > (scrollTop + wrapperBottom) / 2) {
// the element is partially visible and it's below the center of the viewport
} else {
return;
}
}
switch (revealPosition) {
case CellRevealPosition.Top:
this.view.setScrollTop(elementTop);
this.view.setScrollTop(this.view.elementTop(viewIndex));
break;
case CellRevealPosition.Center:
case CellRevealPosition.NearTop:
{
// reveal the cell top in the viewport center initially
this.view.setScrollTop(elementTop - this.view.renderHeight / 2);
// cell rendered already, we now have a more accurate cell height
const newElementTop = this.view.elementTop(viewIndex);
const newElementHeight = this.view.elementHeight(viewIndex);
const renderHeight = this.getViewScrollBottom() - this.getViewScrollTop();
if (newElementHeight >= renderHeight) {
// cell is larger than viewport, reveal top
this.view.setScrollTop(newElementTop);
} else if (revealPosition === CellRevealPosition.Center) {
this.view.setScrollTop(newElementTop + (newElementHeight / 2) - (renderHeight / 2));
} else if (revealPosition === CellRevealPosition.NearTop) {
this.view.setScrollTop(newElementTop - (renderHeight / 5));
}
}
break;
case CellRevealPosition.Bottom:
this.view.setScrollTop(this.scrollTop + (elementBottom - wrapperBottom));
this.view.setScrollTop(this.scrollTop + (this.view.elementTop(viewIndex) + this.view.elementHeight(viewIndex) - this.getViewScrollBottom()));
break;
default:
break;
}
}
private _revealInView(viewIndex: number) {
const firstIndex = this.view.firstVisibleIndex;
if (viewIndex <= firstIndex) {
this._revealInternal(viewIndex, true, CellRevealPosition.Top);
} else {
this._revealInternal(viewIndex, true, CellRevealPosition.Bottom);
}
}
private _revealInCenter(viewIndex: number) {
this._revealInternal(viewIndex, false, CellRevealPosition.Center);
}
private _revealInCenterIfOutsideViewport(viewIndex: number) {
this._revealInternal(viewIndex, true, CellRevealPosition.Center);
}
setCellSelection(cell: ICellViewModel, range: Range) {
setCellEditorSelection(cell: ICellViewModel, range: Range) {
const element = cell as CellViewModel;
if (element.editorAttached) {
element.setSelection(range);
@@ -1303,7 +1219,6 @@ export class NotebookCellList extends WorkbenchList<CellViewModel> implements ID
}
}
override style(styles: IListStyles) {
const selectorSuffix = this.view.domId;
if (!this.styleElement) {
@@ -1449,74 +1364,6 @@ export class ListViewInfoAccessor extends Disposable {
super();
}
setScrollTop(scrollTop: number) {
this.list.scrollTop = scrollTop;
}
isScrolledToBottom() {
return this.list.isScrolledToBottom();
}
scrollToBottom() {
this.list.scrollToBottom();
}
revealCellRangeInView(range: ICellRange) {
return this.list.revealElementsInView(range);
}
revealInView(cell: ICellViewModel) {
this.list.revealElementInView(cell);
}
revealInViewAtTop(cell: ICellViewModel) {
this.list.revealElementInViewAtTop(cell);
}
revealInCenterIfOutsideViewport(cell: ICellViewModel) {
this.list.revealElementInCenterIfOutsideViewport(cell);
}
async revealInCenterIfOutsideViewportAsync(cell: ICellViewModel) {
return this.list.revealElementInCenterIfOutsideViewportAsync(cell);
}
revealInCenter(cell: ICellViewModel) {
this.list.revealElementInCenter(cell);
}
async revealNearTopIfOutsideViewportAync(cell: ICellViewModel) {
return this.list.revealNearTopIfOutsideViewportAync(cell);
}
async revealLineInViewAsync(cell: ICellViewModel, line: number): Promise<void> {
return this.list.revealElementLineInViewAsync(cell, line);
}
async revealLineInCenterAsync(cell: ICellViewModel, line: number): Promise<void> {
return this.list.revealElementLineInCenterAsync(cell, line);
}
async revealLineInCenterIfOutsideViewportAsync(cell: ICellViewModel, line: number): Promise<void> {
return this.list.revealElementLineInCenterIfOutsideViewportAsync(cell, line);
}
async revealRangeInViewAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this.list.revealElementRangeInViewAsync(cell, range);
}
async revealRangeInCenterAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this.list.revealElementRangeInCenterAsync(cell, range);
}
async revealRangeInCenterIfOutsideViewportAsync(cell: ICellViewModel, range: Range): Promise<void> {
return this.list.revealElementRangeInCenterIfOutsideViewportAsync(cell, range);
}
async revealCellOffsetInCenterAsync(cell: ICellViewModel, offset: number): Promise<void> {
return this.list.revealElementOffsetInCenterAsync(cell, offset);
}
getViewIndex(cell: ICellViewModel): number {
return this.list.getViewIndex(cell) ?? -1;
}
@@ -1569,24 +1416,12 @@ export class ListViewInfoAccessor extends Disposable {
return this.list.viewModel?.getCellsInRange(range) ?? [];
}
setCellEditorSelection(cell: ICellViewModel, range: Range): void {
this.list.setCellSelection(cell, range);
}
setHiddenAreas(_ranges: ICellRange[]): boolean {
return this.list.setHiddenAreas(_ranges, true);
}
getVisibleRangesPlusViewportAboveAndBelow(): ICellRange[] {
return this.list?.getVisibleRangesPlusViewportAboveAndBelow() ?? [];
}
triggerScroll(event: IMouseWheelEvent) {
this.list.triggerScrollFromMouseWheelEvent(event);
}
}
function getEditorAttachedPromise(element: CellViewModel) {
function getEditorAttachedPromise(element: ICellViewModel) {
return new Promise<void>((resolve, reject) => {
Event.once(element.onDidChangeEditorAttachState)(() => element.editorAttached ? resolve() : reject());
});
@@ -14,11 +14,12 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellRevealRangeType, CellRevealSyncType, CellRevealType, ICellOutputViewModel, ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellPartsCollection } from 'vs/workbench/contrib/notebook/browser/view/cellPart';
import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModelImpl';
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
export interface INotebookCellList {
isDisposed: boolean;
viewModel: NotebookViewModel | null;
@@ -58,22 +59,12 @@ export interface INotebookCellList {
selectElements(elements: ICellViewModel[]): void;
getFocusedElements(): ICellViewModel[];
getSelectedElements(): ICellViewModel[];
revealElementsInView(range: ICellRange): void;
isScrolledToBottom(): boolean;
revealCellsInView(range: ICellRange): void;
scrollToBottom(): void;
revealElementInView(element: ICellViewModel): void;
revealElementInViewAtTop(element: ICellViewModel): void;
revealElementInCenterIfOutsideViewport(element: ICellViewModel): void;
revealElementInCenter(element: ICellViewModel): void;
revealElementInCenterIfOutsideViewportAsync(element: ICellViewModel): Promise<void>;
revealNearTopIfOutsideViewportAync(element: ICellViewModel): Promise<void>;
revealElementLineInViewAsync(element: ICellViewModel, line: number): Promise<void>;
revealElementLineInCenterAsync(element: ICellViewModel, line: number): Promise<void>;
revealElementLineInCenterIfOutsideViewportAsync(element: ICellViewModel, line: number): Promise<void>;
revealElementRangeInViewAsync(element: ICellViewModel, range: Range): Promise<void>;
revealElementRangeInCenterAsync(element: ICellViewModel, range: Range): Promise<void>;
revealElementRangeInCenterIfOutsideViewportAsync(element: ICellViewModel, range: Range): Promise<void>;
revealElementOffsetInCenterAsync(element: ICellViewModel, offset: number): Promise<void>;
revealCell(cell: ICellViewModel, revealType: CellRevealSyncType): void;
revealCellAsync(cell: ICellViewModel, revealType: CellRevealType): Promise<void>;
revealCellRangeAsync(cell: ICellViewModel, range: Range, revealType: CellRevealRangeType): Promise<void>;
revealCellOffsetInCenterAsync(element: ICellViewModel, offset: number): Promise<void>;
setHiddenAreas(_ranges: ICellRange[], triggerViewUpdate: boolean): boolean;
domElementOfElement(element: ICellViewModel): HTMLElement | null;
focusView(): void;
@@ -82,7 +73,7 @@ export interface INotebookCellList {
updateElementHeight2(element: ICellViewModel, size: number, anchorElementIndex?: number | null): void;
domFocus(): void;
focusContainer(): void;
setCellSelection(element: ICellViewModel, range: Range): void;
setCellEditorSelection(element: ICellViewModel, range: Range): void;
style(styles: IListStyles): void;
getRenderHeight(): number;
getScrollHeight(): number;
@@ -60,17 +60,17 @@ suite('NotebookCellList', () => {
assert.deepStrictEqual(cellList.getViewScrollBottom(), 215);
// reveal cell 1, top 50, bottom 150, which is fully visible in the viewport
cellList.revealElementsInView({ start: 1, end: 2 });
cellList.revealCellsInView({ start: 1, end: 2 });
assert.deepStrictEqual(cellList.scrollTop, 5);
assert.deepStrictEqual(cellList.getViewScrollBottom(), 215);
// reveal cell 2, top 150, bottom 200, which is fully visible in the viewport
cellList.revealElementsInView({ start: 2, end: 3 });
cellList.revealCellsInView({ start: 2, end: 3 });
assert.deepStrictEqual(cellList.scrollTop, 5);
assert.deepStrictEqual(cellList.getViewScrollBottom(), 215);
// reveal cell 3, top 200, bottom 300, which is partially visible in the viewport
cellList.revealElementsInView({ start: 3, end: 4 });
cellList.revealCellsInView({ start: 3, end: 4 });
assert.deepStrictEqual(cellList.scrollTop, 90);
});
});
@@ -105,7 +105,7 @@ suite('NotebookCellList', () => {
assert.deepStrictEqual(cellList.getViewScrollBottom(), 210);
// reveal cell 3, top 200, bottom 300, which is partially visible in the viewport
cellList.revealElementsInView({ start: 3, end: 4 });
cellList.revealCellsInView({ start: 3, end: 4 });
assert.deepStrictEqual(cellList.scrollTop, 90);
// scroll to 5
@@ -114,7 +114,7 @@ suite('NotebookCellList', () => {
assert.deepStrictEqual(cellList.getViewScrollBottom(), 215);
// reveal cell 0, top 0, bottom 50
cellList.revealElementsInView({ start: 0, end: 1 });
cellList.revealCellsInView({ start: 0, end: 1 });
assert.deepStrictEqual(cellList.scrollTop, 0);
});
});
@@ -150,7 +150,7 @@ suite('NotebookCellList', () => {
assert.deepStrictEqual(cellList.scrollTop, 0);
assert.deepStrictEqual(cellList.getViewScrollBottom(), 210);
cellList.revealElementsInView({ start: 4, end: 5 });
cellList.revealCellsInView({ start: 4, end: 5 });
assert.deepStrictEqual(cellList.scrollTop, 140);
// assert.deepStrictEqual(cellList.getViewScrollBottom(), 330);
});