diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index 749e565086e2..aef31fb1986f 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/common/codiconsLibrary.ts b/src/vs/base/common/codiconsLibrary.ts index 78e10aaa5435..9d0fb350bdae 100644 --- a/src/vs/base/common/codiconsLibrary.ts +++ b/src/vs/base/common/codiconsLibrary.ts @@ -708,4 +708,6 @@ export const codiconsLibrary = { positronOpenInEditor: register('positron-open-in-editor', 0xf290), positronOpenFolder: register('positron-open-folder', 0xf291), positronFormatDocument: register('positron-format-document', 0xf292), + positronPin: register('positron-pin', 0xf293), + positronUnpin: register('positron-unpin', 0xf294), } as const; diff --git a/src/vs/platform/positronActionBar/browser/positronActionBar.tsx b/src/vs/platform/positronActionBar/browser/positronActionBar.tsx index 438aec7fac6f..533c1570e2a4 100644 --- a/src/vs/platform/positronActionBar/browser/positronActionBar.tsx +++ b/src/vs/platform/positronActionBar/browser/positronActionBar.tsx @@ -8,12 +8,11 @@ import './positronActionBarVariables.css'; import './positronActionBar.css'; // React. -import React, { KeyboardEvent, PropsWithChildren, useEffect, useLayoutEffect, useRef } from 'react'; +import React, { KeyboardEvent, PropsWithChildren, useEffect, useRef } from 'react'; // Other dependencies. import * as DOM from '../../../base/browser/dom.js'; import { usePositronActionBarContext } from './positronActionBarContext.js'; -import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; import { optionalValue, positronClassNames } from '../../../base/common/positronUtilities.js'; /** @@ -111,25 +110,6 @@ export const PositronActionBar = (props: PropsWithChildren { - // Create the disposable store for cleanup. - const disposableStore = new DisposableStore(); - - // Allocate and initialize the resize observer. - const resizeObserver = new ResizeObserver(entries => { - }); - - // Start observing the size of the action bar. - resizeObserver.observe(ref.current); - - // Add the resize observer to the disposable store. - disposableStore.add(toDisposable(() => resizeObserver.disconnect())); - - // Return the cleanup function that will dispose of the disposables. - return () => disposableStore.dispose(); - }, [context]); - useEffect(() => { if (!props.nestedActionBar && prevIndex >= 0 && (focusedIndex !== prevIndex)) { const items = Array.from(context.focusableComponents); diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorDataGridInstance.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorDataGridInstance.tsx index a1e52239e8ca..ea27bb2f164e 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorDataGridInstance.tsx +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorDataGridInstance.tsx @@ -93,6 +93,8 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { defaultRowHeight: ROW_HEIGHT, columnResize: false, rowResize: false, + columnPinning: false, + rowPinning: false, horizontalScrollbar: false, verticalScrollbar: true, scrollbarThickness: 8, @@ -114,8 +116,11 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { this._columnSchemaCache = new ColumnSchemaCache(this._dataExplorerClientInstance) ); - // Set the initial layout entries in the row layout manager. - this._rowLayoutManager.setLayoutEntries(backendState.table_shape.num_columns); + // Set the column layout entries. There is always one column. + this._columnLayoutManager.setEntries(1); + + // Set the row layout entries. + this._rowLayoutManager.setEntries(backendState.table_shape.num_columns); /** * Updates the data grid instance. @@ -131,7 +136,7 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { this._backendState = backendState; // Set the layout entries in the row layout manager. - this._rowLayoutManager.setLayoutEntries(backendState.table_shape.num_columns); + this._rowLayoutManager.setEntries(backendState.table_shape.num_columns); // Scroll to the top. await this.setScrollOffsets(0, 0); @@ -146,21 +151,19 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { // Add the onDidDataUpdate event handler. this._register(this._dataExplorerClientInstance.onDidDataUpdate(async () => // Update the data grid instance. - updateDataGridInstance + updateDataGridInstance() )); // Add the onDidUpdateBackendState event handler. - this._register( - this._dataExplorerClientInstance.onDidUpdateBackendState(async backendState => - // Update the data grid instance. - updateDataGridInstance(backendState) - ) - ); + this._register(this._dataExplorerClientInstance.onDidUpdateBackendState(async backendState => + // Update the data grid instance. + updateDataGridInstance(backendState) + )); // Add the onDidUpdateCache event handler. this._register(this._columnSchemaCache.onDidUpdateCache(() => // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire() + this.fireOnDidUpdateEvent() )); } @@ -195,7 +198,8 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { override get firstColumn() { return { columnIndex: 0, - left: 0 + left: 0, + width: 0, }; } @@ -219,11 +223,13 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { } /** - * Gets the width of a column. + * Gets the custom width of a column. * @param columnIndex The column index. + * @returns The custom width of the column; otherwise, undefined. */ - override getColumnWidth(columnIndex: number): number { - return this.layoutWidth - 8; + override getCustomColumnWidth(columnIndex: number): number | undefined { + // Subtrack 8px for margins. + return columnIndex === 0 ? this.layoutWidth - 8 : undefined; } selectItem(rowIndex: number): void { diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.css b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.css index 1cfead0cb35c..4e47df3cd6df 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.css +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.css @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.tsx index ac6a8a7eb3a4..f3d21b0f65e5 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.tsx +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/addEditRowFilterModalPopup/components/columnSelectorModalPopup.tsx @@ -65,7 +65,7 @@ export const ColumnSelectorModalPopup = (props: ColumnSelectorModalPopupProps) = // Return the cleanup function that will dispose of the disposables. return () => disposableStore.dispose(); - }, [props, props.columnSelectorDataGridInstance]); + }, [props]); const onKeyDown = (evt: React.KeyboardEvent) => { if (evt.code === 'Enter' || evt.code === 'Space') { diff --git a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/dataExplorer.tsx b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/dataExplorer.tsx index 0f03b58ee808..cbb375776ca9 100644 --- a/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/dataExplorer.tsx +++ b/src/vs/workbench/browser/positronDataExplorer/components/dataExplorerPanel/components/dataExplorer.tsx @@ -237,7 +237,7 @@ export const DataExplorer = () => { return () => disposableStore.dispose(); }, [columnsCollapsed, services.accessibilityService, context.instance]); - // Automatic layout useEffect. + // Automatic layout useLayoutEffect. useLayoutEffect(() => { // Set the initial width. const initialWidth = dataExplorerRef.current.offsetWidth; diff --git a/src/vs/workbench/browser/positronDataGrid/classes/dataGridInstance.tsx b/src/vs/workbench/browser/positronDataGrid/classes/dataGridInstance.tsx index 6f29fa555876..1e2ca0f1e86a 100644 --- a/src/vs/workbench/browser/positronDataGrid/classes/dataGridInstance.tsx +++ b/src/vs/workbench/browser/positronDataGrid/classes/dataGridInstance.tsx @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -7,12 +7,12 @@ import { JSX } from 'react'; // Other dependencies. -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; import { IDataColumn } from '../interfaces/dataColumn.js'; +import { Emitter } from '../../../../base/common/event.js'; import { IColumnSortKey } from '../interfaces/columnSortKey.js'; +import { ILayoutEntry, LayoutManager } from './layoutManager.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { AnchorPoint } from '../../positronComponents/positronModalPopup/positronModalPopup.js'; -import { ILayoutEntry, LayoutManager } from '../../../services/positronDataExplorer/common/layoutManager.js'; import { PositronActionBarHoverManager } from '../../../../platform/positronActionBar/browser/positronActionBarHoverManager.js'; /** @@ -73,6 +73,28 @@ type RowResizeOptions = | { readonly maximumRowHeight: number; }; +/** + * ColumnPinningOptions type. + */ +type ColumnPinningOptions = | { + readonly columnPinning: false; + readonly maximumPinnedColumns?: never; +} | { + readonly columnPinning: true; + readonly maximumPinnedColumns: number; +} + +/** + * RowPinningOptions type. + */ +type RowPinningOptions = | { + readonly rowPinning: false; + readonly maximumPinnedRows?: never; +} | { + readonly rowPinning: true; + readonly maximumPinnedRows: number; +} + /** * ScrollbarOptions type. */ @@ -143,6 +165,8 @@ type DataGridOptions = DefaultSizeOptions & ColumnResizeOptions & RowResizeOptions & + ColumnPinningOptions & + RowPinningOptions & ScrollbarOptions & DisplayOptions & CursorOptions & @@ -155,6 +179,15 @@ type DataGridOptions = export interface ColumnDescriptor { readonly columnIndex: number; readonly left: number; + readonly width: number; +} + +/** + * ColumnDescriptors interface. + */ +export interface ColumnDescriptors { + pinnedColumnDescriptors: ColumnDescriptor[]; + unpinnedColumnDescriptors: ColumnDescriptor[]; } /** @@ -163,6 +196,15 @@ export interface ColumnDescriptor { export interface RowDescriptor { readonly rowIndex: number; readonly top: number; + readonly height: number; +} + +/** + * RowDescriptors interface. + */ +export interface RowDescriptors { + pinnedRowDescriptors: RowDescriptor[]; + unpinnedRowDescriptors: RowDescriptor[]; } /** @@ -202,7 +244,7 @@ export enum ColumnSelectionState { None = 0, Selected = 1, SelectedLeft = 2, - SelectedRight = 4 + SelectedRight = 4, } /** @@ -212,7 +254,7 @@ export enum RowSelectionState { None = 0, Selected = 1, SelectedTop = 8, - SelectedBottom = 16 + SelectedBottom = 16, } /** @@ -225,136 +267,107 @@ export enum MouseSelectionType { } /** - * CellSelectionRange interface. + * CellSelectionIndexes class. */ -class CellSelectionRange { +class CellSelectionIndexes { /** - * Constructor. - * @param firstColumnIndex The first column index. - * @param firstRowIndex The first row index. - * @param lastColumnIndex The last column index. - * @param lastRowIndex The last row index. + * Gets the column indexes set. */ - constructor( - public firstColumnIndex: number, - public firstRowIndex: number, - public lastColumnIndex: number, - public lastRowIndex: number - ) { } + private readonly _columnIndexesSet: Set; /** - * Returns a value which indicates whether the specified column index and row index is contained - * in the cell selection range - * @param columnIndex The column index. - * @param rowIndex The row index. - * @returns true if the column index and row index is contained in the cell selection range; - * otherwise, false. + * Gets the row indexes set. */ - contains(columnIndex: number, rowIndex: number) { - return columnIndex >= this.firstColumnIndex && columnIndex <= this.lastColumnIndex && - rowIndex >= this.firstRowIndex && rowIndex <= this.lastRowIndex; - } -} + private readonly _rowIndexesSet: Set; -/** - * SelectionRange interface. - */ -class SelectionRange { /** - * Constructor. - * @param firstIndex The first index. - * @param lastIndex The last index. + * Gets the first column index. */ - constructor(public firstIndex: number, public lastIndex: number) { } + get firstColumnIndex() { + return this.columnIndexes[0]; + } /** - * Returns a value which indicates whether the specified index is contained in the selection - * range. - * @param index The index. - * @returns true if the index is contained in the selection range; otherwise, false. + * Gets the last column index. */ - contains(index: number) { - return index >= this.firstIndex && index <= this.lastIndex; + get lastColumnIndex() { + return this.columnIndexes[this.columnIndexes.length - 1]; } - indexes() { - const indexes: number[] = [this.lastIndex - this.firstIndex]; - for (let index = this.firstIndex; index <= this.lastIndex; index++) { - indexes.push(index); - } - - return new SelectionIndexes(indexes); + /** + * Gets the first row index. + */ + get firstRowIndex() { + return this.rowIndexes[0]; } -} -/** - * SelectionIndexes class. - */ -class SelectionIndexes { /** - * Gets or sets the indexes. + * Gets the last row index. */ - readonly indexes = new Set(); + get lastRowIndex() { + return this.rowIndexes[this.rowIndexes.length - 1]; + } /** * Constructor. - * @param indexes The initial indexes. + * @param columnIndexes The column indices. + * @param rowIndexes The row indices. */ - constructor(indexes: number | number[]) { - if (Array.isArray(indexes)) { - indexes.forEach(index => this.indexes.add(index)); - } else { - this.indexes.add(indexes); - } + constructor(public readonly columnIndexes: number[], public readonly rowIndexes: number[]) { + this._columnIndexesSet = new Set(columnIndexes); + this._rowIndexesSet = new Set(rowIndexes); } /** - * Determines whether the selection indexes has the specified index. - * @param index The index. - * @returns true, if the selection indexes has the specified index; otherwise, false. + * Returns a value which indicates whether the specified column index and row index is contained + * in the cell selection. + * @param columnIndex The column index. + * @param rowIndex The row index. + * @returns true if the column index and row index is contained in the cell selection range; otherwise, false. */ - has(index: number) { - return this.indexes.has(index); + contains(columnIndex: number, rowIndex: number) { + return this._columnIndexesSet.has(columnIndex) && this._rowIndexesSet.has(rowIndex); } +} +/** + * SelectionIndexes class. + */ +class SelectionIndexes { /** - * Returns a value which indicates whether the selection indexes is empty. - * @returns true, if the selection indexes is empty; otherwise, false. + * Gets the indexes set. */ - isEmpty() { - return this.indexes.size === 0; - } + private readonly _indexesSet = new Set(); /** - * Adds the specified index to the selection indexes. - * @param index The index. + * Gets the first column index. */ - add(index: number) { - this.indexes.add(index); + get firstIndex() { + return this.indexes[0]; } /** - * Deletes the specified index from the set of selection indexes. - * @param index The index. + * Gets the last index. */ - delete(index: number) { - this.indexes.delete(index); + get lastIndex() { + return this.indexes[this.indexes.length - 1]; } /** - * Returns the max selection index. - * @returns The max selection index. + * Constructor. + * @param indexes The indexes. */ - max() { - return Math.max(...this.indexes); + constructor(public readonly indexes: number[]) { + this._indexesSet = new Set(indexes); } /** - * Returns the selection indexes as a sorted array. - * @returns The selection indexes as a sorted array. + * Determines whether the selection indexes contains the specified index. + * @param index The index. + * @returns true, if the selection indexes contains the specified index; otherwise, false. */ - sortedArray() { - return Array.from(this.indexes).sort((a, b) => a - b); + contains(index: number) { + return this._indexesSet.has(index); } } @@ -362,32 +375,26 @@ class SelectionIndexes { * ClipboardCell class. */ export class ClipboardCell { - constructor( - readonly columnIndex: number, - readonly rowIndex: number - ) { } -} - -/** - * ClipboardCellRange class. - */ -export class ClipboardCellRange { - constructor( - readonly firstColumnIndex: number, - readonly firstRowIndex: number, - readonly lastColumnIndex: number, - readonly lastRowIndex: number - ) { } + /** + * Constructor. + * @param columnIndex The column index. + * @param rowIndex The row index. + */ + constructor(readonly columnIndex: number, readonly rowIndex: number) { + } } /** - * ClipboardColumnRange class. + * ClipboardCellIndexes class. */ -export class ClipboardColumnRange { - constructor( - readonly firstColumnIndex: number, - readonly lastColumnIndex: number, - ) { } +export class ClipboardCellIndexes { + /** + * Constructor. + * @param columnIndexes The column indexes. + * @param rowIndexes The row indexes. + */ + constructor(readonly columnIndexes: number[], readonly rowIndexes: number[]) { + } } /** @@ -399,16 +406,6 @@ export class ClipboardColumnIndexes { ) { } } -/** - * ClipboardRowRange class. - */ -export class ClipboardRowRange { - constructor( - readonly firstRowIndex: number, - readonly lastRowIndex: number, - ) { } -} - /** * ClipboardRowIndexes class. */ @@ -423,10 +420,8 @@ export class ClipboardRowIndexes { */ export type ClipboardData = ClipboardCell | - ClipboardCellRange | - ClipboardColumnRange | + ClipboardCellIndexes | ClipboardColumnIndexes | - ClipboardRowRange | ClipboardRowIndexes; /** @@ -514,6 +509,14 @@ export class ColumnSortKeyDescriptor implements IColumnSortKey { //#endregion Public Properties } +/** + * Represents a position and its corresponding index. + */ +interface PositionIndex { + readonly position: number; + readonly index: number; +} + /** * DataGridInstance class. */ @@ -585,6 +588,26 @@ export abstract class DataGridInstance extends Disposable { */ private readonly _defaultRowHeight: number; + /** + * Gets a value which indicates whether column pinning is enabled. + */ + private readonly _columnPinning: boolean; + + /** + * Gets the maximum number of pinned columns. + */ + private readonly _maximumPinnedColumns: number; + + /** + * Gets a value which indicates whether row pinning is enabled. + */ + private readonly _rowPinning: boolean; + + /** + * Gets the maximum number of pinned rows. + */ + private readonly _maximumPinnedRows: number; + /** * Gets a value which indicates whether to show the horizontal scrollbar. */ @@ -669,16 +692,6 @@ export abstract class DataGridInstance extends Disposable { */ private _height = 0; - /** - * The horizontal scroll offset. - */ - protected _horizontalScrollOffset = 0; - - /** - * The vertical scroll offset. - */ - protected _verticalScrollOffset = 0; - /** * Gets or sets the cursor column index. */ @@ -690,14 +703,9 @@ export abstract class DataGridInstance extends Disposable { private _cursorRowIndex = 0; /** - * Gets or sets the cell selection range. - */ - private _cellSelectionRange?: CellSelectionRange; - - /** - * Gets or sets the column selection range. + * Gets or sets the cell selection indexes. */ - private _columnSelectionRange?: SelectionRange; + private _cellSelectionIndexes?: CellSelectionIndexes; /** * Gets the column selection indexes. @@ -705,49 +713,59 @@ export abstract class DataGridInstance extends Disposable { private _columnSelectionIndexes?: SelectionIndexes; /** - * Gets or sets the row selection range. + * Gets the row selection indexes. */ - private _rowSelectionRange?: SelectionRange; + private _rowSelectionIndexes?: SelectionIndexes; /** - * Gets the row selection indexes. + * A value which indicates that there is a pending onDidUpdate event. */ - private _rowSelectionIndexes?: SelectionIndexes; + private _pendingOnDidUpdateEvent = false; //#endregion Private Properties - //#region Protected Properties + //#region Private Events /** - * Gets the column layout manager. + * The onDidUpdate event emitter. */ - protected readonly _columnLayoutManager: LayoutManager; + private readonly _onDidUpdateEmitter = this._register(new Emitter); /** - * Gets the row layout manager. + * The onDidChangeColumnSorting event emitter. */ - protected readonly _rowLayoutManager: LayoutManager; + private readonly _onDidChangeColumnSortingEmitter = this._register(new Emitter); + + //#endregion Private Events + + //#region Protected Properties /** - * Gets the column sort keys. + * The horizontal scroll offset. */ - protected readonly _columnSortKeys = new Map(); + protected _horizontalScrollOffset = 0; - //#endregion Protected Properties + /** + * The vertical scroll offset. + */ + protected _verticalScrollOffset = 0; - //#region Protected Events + /** + * Gets the column layout manager. + */ + protected readonly _columnLayoutManager: LayoutManager; /** - * The onDidUpdate event emitter. + * Gets the row layout manager. */ - protected readonly _onDidUpdateEmitter = this._register(new Emitter); + protected readonly _rowLayoutManager: LayoutManager; /** - * The onDidChangeColumnSorting event emitter. + * Gets the column sort keys. Keyed by column index. */ - protected readonly _onDidChangeColumnSortingEmitter = this._register(new Emitter); + protected readonly _columnSortKeys = new Map(); - //#endregion Protected Events + //#endregion Protected Properties //#region Constructor & Dispose @@ -782,6 +800,14 @@ export abstract class DataGridInstance extends Disposable { this._minimumRowHeight = options.minimumRowHeight ?? options.defaultRowHeight; this._maximumRowHeight = options.maximumRowHeight ?? options.defaultRowHeight; + // ColumnPinningOptions. + this._columnPinning = options.columnPinning || false; + this._maximumPinnedColumns = this._columnPinning ? options.maximumPinnedColumns ?? 0 : 0; + + // RowPinningOptions. + this._rowPinning = options.rowPinning || false; + this._maximumPinnedRows = this._rowPinning ? options.maximumPinnedRows ?? 0 : 0; + // ScrollbarOptions. this._horizontalScrollbar = options.horizontalScrollbar || false; this._verticalScrollbar = options.verticalScrollbar || false; @@ -909,6 +935,34 @@ export abstract class DataGridInstance extends Disposable { return this._defaultRowHeight; } + /** + * Gets a value which indicates whether column pinning is enabled + */ + get columnPinning() { + return this._columnPinning; + } + + /** + * Gets the maximum number of pinned columns. + */ + get maximumPinnedColumns() { + return this._maximumPinnedColumns; + } + + /** + * Gets a value which indicates whether row pinning is enabled + */ + get rowPinning() { + return this._rowPinning; + } + + /** + * Gets the maximum number of pinned rows. + */ + get maximumPinnedRows() { + return this._maximumPinnedRows; + } + /** * Gets a value which indicates whether to show the horizontal scrollbar. */ @@ -1026,14 +1080,14 @@ export abstract class DataGridInstance extends Disposable { * Gets the scroll width. */ get scrollWidth() { - return this._columnLayoutManager.size + this._scrollbarOverscroll; + return this._columnLayoutManager.unpinnedLayoutEntriesSize + this._scrollbarOverscroll; } /** * Gets the scroll height. */ get scrollHeight() { - return (this._rowsMargin * 2) + this._rowLayoutManager.size + this._scrollbarOverscroll; + return (this._rowsMargin * 2) + this._rowLayoutManager.unpinnedLayoutEntriesSize + this._scrollbarOverscroll; } /** @@ -1054,16 +1108,25 @@ export abstract class DataGridInstance extends Disposable { * Gets the layout width. */ get layoutWidth() { - // Calculate the layout width. + // Set the layout width. let layoutWidth = this._width; + + // If row headers are enabled, subtract the row headers width. if (this.rowHeaders) { layoutWidth -= this._rowHeadersWidth; } + + // If column pinning is enabled, subtract the pinned columns width. + if (this.columnPinning) { + layoutWidth -= this._columnLayoutManager.pinnedLayoutEntriesSize; + } + + // If the vertical scrollbar is enabled, subtract the scrollbar width. if (this._verticalScrollbar) { layoutWidth -= this._scrollbarThickness; } - // Done. + // Return the layout width. return layoutWidth; } @@ -1078,16 +1141,25 @@ export abstract class DataGridInstance extends Disposable { * Gets the layout height. */ get layoutHeight() { - // Calculate the layout height. + // Set the layout height. let layoutHeight = this._height; + + // If column headers are enabled, subtract the column headers height. if (this.columnHeaders) { layoutHeight -= this._columnHeadersHeight; } + + // If row pinning is enabled, subtract the pinned rows height. + if (this.rowPinning) { + layoutHeight -= this._rowLayoutManager.pinnedLayoutEntriesSize; + } + + // If the horizontal scrollbar is enabled, subtract the scrollbar height. if (this._horizontalScrollbar) { layoutHeight -= this._scrollbarThickness; } - // Done. + // Return the layout height. return layoutHeight; } @@ -1135,7 +1207,7 @@ export abstract class DataGridInstance extends Disposable { */ get firstColumn(): ColumnDescriptor | undefined { // Get the first column layout entry. If it wasn't found, return undefined. - const layoutEntry = this._columnLayoutManager.findLayoutEntry(this.horizontalScrollOffset); + const layoutEntry = this._columnLayoutManager.findFirstUnpinnedLayoutEntry(this.horizontalScrollOffset); if (!layoutEntry) { return undefined; } @@ -1143,7 +1215,8 @@ export abstract class DataGridInstance extends Disposable { // Return the column descriptor for the first column. return { columnIndex: layoutEntry.index, - left: layoutEntry.start + left: layoutEntry.start, + width: layoutEntry.size, }; } @@ -1152,7 +1225,7 @@ export abstract class DataGridInstance extends Disposable { */ get firstRow(): RowDescriptor | undefined { // Get the first row layout entry. If it wasn't found, return undefined. - const layoutEntry = this._rowLayoutManager.findLayoutEntry(this.verticalScrollOffset); + const layoutEntry = this._rowLayoutManager.findFirstUnpinnedLayoutEntry(this.verticalScrollOffset); if (!layoutEntry) { return undefined; } @@ -1160,7 +1233,8 @@ export abstract class DataGridInstance extends Disposable { // Return the row descriptor for the first row. return { rowIndex: layoutEntry.index, - top: layoutEntry.start + top: layoutEntry.start, + height: layoutEntry.size, }; } @@ -1228,7 +1302,7 @@ export abstract class DataGridInstance extends Disposable { this._focused = focused; // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1250,56 +1324,93 @@ export abstract class DataGridInstance extends Disposable { } /** - * Gets a column descriptor. - * @param columnIndex The column index. - * @returns The column descriptor, if found; otherwise, undefined. - */ - getColumn(columnIndex: number): ColumnDescriptor | undefined { - // Get the column layout entry. If it wasn't found, return undefined. - const layoutEntry = this._columnLayoutManager.getLayoutEntry(columnIndex); - if (!layoutEntry) { - return undefined; - } + * Gets column descriptors. + * @param horizontalOffset The horizontal offset of the unpinned column descriptors to return. + * @param width The width of the unpinned column descriptors to return. + * @returns The column descriptors. + */ + getColumnDescriptors(horizontalOffset: number, width: number): ColumnDescriptors { + // Get the pinned column descriptors. + const pinnedLayoutEntries = this._columnLayoutManager.pinnedLayoutEntries(width); + const pinnedColumnDescriptors = pinnedLayoutEntries.map((pinnedLayoutEntry): ColumnDescriptor => ({ + columnIndex: pinnedLayoutEntry.index, + left: pinnedLayoutEntry.start, + width: pinnedLayoutEntry.size, + })); + + // Calculate the total width of the pinned column descriptors. + const pinnedColumnDescriptorsWidth = (() => { + if (!pinnedColumnDescriptors.length) { + return 0; + } else { + const lastPinnedColumnDescriptor = pinnedColumnDescriptors[pinnedColumnDescriptors.length - 1]; + return lastPinnedColumnDescriptor.left + lastPinnedColumnDescriptor.width; + } + })(); + + // Get the unpinned column descriptors. + const unpinnedLayoutEntries = this._columnLayoutManager.unpinnedLayoutEntries(horizontalOffset, width - pinnedColumnDescriptorsWidth); + const unpinnedColumnDescriptors = unpinnedLayoutEntries.map((pinnedLayoutEntry): ColumnDescriptor => ({ + columnIndex: pinnedLayoutEntry.index, + left: pinnedColumnDescriptorsWidth + pinnedLayoutEntry.start, + width: pinnedLayoutEntry.size, + })); - // Return the column descriptor for the column. + // Return the column descriptors. return { - columnIndex: layoutEntry.index, - left: layoutEntry.start + pinnedColumnDescriptors, + unpinnedColumnDescriptors, }; } /** - * Gets a row descriptor. - * @param columnIndex The row index. - * @returns The row descriptor, if found; otherwise, undefined. - */ - getRow(rowIndex: number): RowDescriptor | undefined { - // Get the row layout entry. If it wasn't found, return undefined. - const layoutEntry = this._rowLayoutManager.getLayoutEntry(rowIndex); - if (!layoutEntry) { - return undefined; - } + * Gets row descriptors. + * @param verticalOffset The vertical offset of the unpinned row descriptors to return. + * @param layoutHeight The height of the unpinned row descriptors to return. + * @returns The row descriptors. + */ + getRowDescriptors(verticalOffset: number, layoutHeight: number): RowDescriptors { + // Get the pinned row descriptors. + const pinnedLayoutEntries = this._rowLayoutManager.pinnedLayoutEntries(layoutHeight); + const pinnedRowDescriptors = pinnedLayoutEntries.map((pinnedLayoutEntry): RowDescriptor => ({ + rowIndex: pinnedLayoutEntry.index, + top: pinnedLayoutEntry.start, + height: pinnedLayoutEntry.size, + })) + + // Calculate the total height of the pinned row descriptors. + const pinnedRowDescriptorsHeight = (() => { + if (!pinnedRowDescriptors.length) { + return 0; + } else { + const lastPinnedRowDescriptor = pinnedRowDescriptors[pinnedRowDescriptors.length - 1]; + return lastPinnedRowDescriptor.top + lastPinnedRowDescriptor.height; + } + })(); - // Return the row descriptor for the row. + // Get the unpinned row descriptors. + const unpinnedLayoutEntries = this._rowLayoutManager.unpinnedLayoutEntries(verticalOffset, layoutHeight - pinnedRowDescriptorsHeight); + const unpinnedRowDescriptors = unpinnedLayoutEntries.map((pinnedLayoutEntry): RowDescriptor => ({ + rowIndex: pinnedLayoutEntry.index, + top: pinnedRowDescriptorsHeight + pinnedLayoutEntry.start, + height: pinnedLayoutEntry.size, + })); + + // Return the row descriptors. return { - rowIndex: layoutEntry.index, - top: layoutEntry.start + pinnedRowDescriptors, + unpinnedRowDescriptors, }; } /** - * Gets the width of a column. + * Gets the custom width of a column. This can be overridden by subclasses to provide + * custom column widths (e.g., for fixed-width columns). * @param columnIndex The column index. + * @returns The custom width of the column; otherwise, undefined. */ - getColumnWidth(columnIndex: number): number { - // Get the column layout entry. If it wasn't found, return 0. - const layoutEntry = this._columnLayoutManager.getLayoutEntry(columnIndex); - if (!layoutEntry) { - return 0; - } - - // Return the column width. - return layoutEntry.size; + getCustomColumnWidth(columnIndex: number): number | undefined { + return undefined; } /** @@ -1315,28 +1426,13 @@ export abstract class DataGridInstance extends Disposable { } // Set the column width override. - this._columnLayoutManager.setLayoutOverride(columnIndex, columnWidth); + this._columnLayoutManager.setSizeOverride(columnIndex, columnWidth); // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); - } - - /** - * Gets the height of a row. - * @param rowIndex The row index. - */ - getRowHeight(rowIndex: number) { - // Get the row layout entry. If it wasn't found, return 0. - const layoutEntry = this._rowLayoutManager.getLayoutEntry(rowIndex); - if (!layoutEntry) { - return undefined; - } - - // Return the row height. - return layoutEntry.size; + this.fireOnDidUpdateEvent(); } /** @@ -1352,13 +1448,13 @@ export abstract class DataGridInstance extends Disposable { } // Set the row height override. - this._rowLayoutManager.setLayoutOverride(rowIndex, rowHeight); + this._rowLayoutManager.setSizeOverride(rowIndex, rowHeight); // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } /** @@ -1366,40 +1462,59 @@ export abstract class DataGridInstance extends Disposable { * @returns A Promise that resolves when the operation is complete. */ async scrollPageUp() { - // Get the first row layout entry for the vertical scroll offset. - const firstLayoutEntry = this._rowLayoutManager.findLayoutEntry(this.verticalScrollOffset); - if (firstLayoutEntry && firstLayoutEntry.index > 1) { - // Find the layout entry that will be to first layout entry for the previous page. - let lastFullyVisibleLayoutEntry: ILayoutEntry | undefined = undefined; - for (let index = firstLayoutEntry.index - 1; index >= 0; index--) { - // Get the layout entry. - const layoutEntry = this._rowLayoutManager.getLayoutEntry(index); - if (layoutEntry) { - if (layoutEntry.start >= this.verticalScrollOffset - this.layoutHeight) { - lastFullyVisibleLayoutEntry = layoutEntry; - } else { - // Set the vertical scroll offset. - this.setVerticalScrollOffset( - lastFullyVisibleLayoutEntry?.start ?? layoutEntry.start - ); + // Get the first unpinned layout entry. + const firstUnpinnedLayoutEntry = this._rowLayoutManager.findFirstUnpinnedLayoutEntry(this.verticalScrollOffset); + if (firstUnpinnedLayoutEntry === undefined) { + return; + } + + // Get the first unpinned layout entry position. + const firstUnpinnedLayoutEntryPosition = this._rowLayoutManager.mapIndexToPosition(firstUnpinnedLayoutEntry.index); + if (firstUnpinnedLayoutEntryPosition === undefined) { + return; + } + + // Find the layout entry that will be the first layout entry for the previous page. + let lastFullyVisibleLayoutEntry: ILayoutEntry | undefined = undefined; + for (let position = firstUnpinnedLayoutEntryPosition - 1; position >= 0; position--) { + // Get the index of the position. + const index = this._rowLayoutManager.mapPositionToIndex(position); + if (index === undefined) { + return; + } + + // Get the layout entry. + const layoutEntry = this._rowLayoutManager.getLayoutEntry(index); + if (layoutEntry === undefined) { + return; + } - // Fetch data. - await this.fetchData(); + // Check if the layout entry is fully visible, note it; otherwise, scroll the viewport and return. + if (layoutEntry.start >= this.verticalScrollOffset - this.layoutHeight) { + lastFullyVisibleLayoutEntry = layoutEntry; + } else { + // Set the vertical scroll offset. + this.setVerticalScrollOffset(lastFullyVisibleLayoutEntry?.start ?? layoutEntry.start); - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + // Fetch data. + await this.fetchData(); - // Done. - return; - } - } + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + + // Done. + return; } } - // Scroll to the top. + // If we drop through to here, scroll to the top. this.setVerticalScrollOffset(0); + + // Fetch data. await this.fetchData(); - this._onDidUpdateEmitter.fire(); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } /** @@ -1407,39 +1522,56 @@ export abstract class DataGridInstance extends Disposable { * @returns A Promise that resolves when the operation is complete. */ async scrollPageDown() { - // Get the first row layout entry for the vertical scroll offset. - const firstLayoutEntry = this._rowLayoutManager.findLayoutEntry(this.verticalScrollOffset); - if (firstLayoutEntry && firstLayoutEntry.index < this.rows - 1) { - - // Find the layout entry that will be to first layout entry for the next page. - for (let index = firstLayoutEntry.index + 1; index < this.rows; index++) { - // Get the layout entry. - const layoutEntry = this._rowLayoutManager.getLayoutEntry(index); - if (layoutEntry) { - if (layoutEntry.end >= this.verticalScrollOffset + this.layoutHeight) { - // Set the vertical scroll offset. - this.setVerticalScrollOffset(Math.min( - layoutEntry.start, - this.maximumVerticalScrollOffset - )); - - // Fetch data. - await this.fetchData(); - - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); - - // Done. - return; - } - } + // Get the first unpinned layout entry. + const firstUnpinnedLayoutEntry = this._rowLayoutManager.findFirstUnpinnedLayoutEntry(this.verticalScrollOffset); + if (firstUnpinnedLayoutEntry === undefined) { + return; + } + + // Get the first unpinned layout entry position. + const firstUnpinnedLayoutEntryPosition = this._rowLayoutManager.mapIndexToPosition(firstUnpinnedLayoutEntry.index); + if (firstUnpinnedLayoutEntryPosition === undefined) { + return; + } + + // Scroll down to the next unpinned layout entry. + for (let position = firstUnpinnedLayoutEntryPosition + 1; position < this._rowLayoutManager.entryCount; position++) { + // Get the index of the position. + const index = this._rowLayoutManager.mapPositionToIndex(position); + if (index === undefined) { + return; + } + + // Get the layout entry. + const layoutEntry = this._rowLayoutManager.getLayoutEntry(index); + if (layoutEntry === undefined) { + return; + } + + // If the layout entry ends at or beyond the viewport, scroll to it. + if (layoutEntry.end >= this.verticalScrollOffset + this.layoutHeight) { + // Set the vertical scroll offset. + this.setVerticalScrollOffset(Math.min(layoutEntry.start, this.maximumVerticalScrollOffset)); + + // Fetch data. + await this.fetchData(); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + + // Done. + return; } } - // Scroll to the bottom. + // If we drop through to here, scroll to the bottom. this.setVerticalScrollOffset(this.maximumVerticalScrollOffset); + + // Fetch data. await this.fetchData(); - this._onDidUpdateEmitter.fire(); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } /** @@ -1467,11 +1599,14 @@ export abstract class DataGridInstance extends Disposable { return; } + // Clear selection. + this.clearSelection(); + // Fire the onDidChangeColumnSorting event. this._onDidChangeColumnSortingEmitter.fire(true); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); // Sort the data. await this.doSortData(); @@ -1499,11 +1634,14 @@ export abstract class DataGridInstance extends Disposable { } }); + // Clear selection. + this.clearSelection(); + // Fire the onDidChangeColumnSorting event. this._onDidChangeColumnSortingEmitter.fire(this._columnSortKeys.size > 0); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); // Sort the data. await this.doSortData(); @@ -1518,11 +1656,14 @@ export abstract class DataGridInstance extends Disposable { // Clear column sort keys. this._columnSortKeys.clear(); + // Clear selection. + this.clearSelection(); + // Fire the onDidChangeColumnSorting event. this._onDidChangeColumnSortingEmitter.fire(false); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); // Sort the data. await this.doSortData(); @@ -1543,7 +1684,7 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1570,7 +1711,7 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1596,7 +1737,7 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1614,7 +1755,7 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1632,26 +1773,23 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } - /** * Sets the cursor position. * @param cursorColumnIndex The cursor column index. * @param cursorRowIndex The cursor row index. */ setCursorPosition(cursorColumnIndex: number, cursorRowIndex: number) { - if (cursorColumnIndex !== this._cursorColumnIndex || - cursorRowIndex !== this._cursorRowIndex - ) { + if (cursorColumnIndex !== this._cursorColumnIndex || cursorRowIndex !== this._cursorRowIndex) { // Set the cursor position. this._cursorColumnIndex = cursorColumnIndex; this._cursorRowIndex = cursorRowIndex; // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1665,7 +1803,7 @@ export abstract class DataGridInstance extends Disposable { this._cursorColumnIndex = cursorColumnIndex; // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1679,10 +1817,190 @@ export abstract class DataGridInstance extends Disposable { this._cursorRowIndex = cursorRowIndex; // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } + /** + * Gets the first column index. + */ + get firstColumnIndex() { + return this._columnLayoutManager.firstIndex; + } + + /** + * Gets the last column index. + */ + get lastColummIndex() { + return this._columnLayoutManager.lastIndex; + } + + /** + * Gets the first row index. + */ + get firstRowIndex() { + return this._rowLayoutManager.firstIndex; + } + + /** + * Gets the last row index. + */ + get lastRowIndex() { + return this._rowLayoutManager.lastIndex; + } + + /** + * Returns a value which indicates whether the specified column is pinned. + * @param columnIndex The column index. + * @returns true if the specified column is pinned; otherwise, false. + */ + isColumnPinned(columnIndex: number) { + return this._columnLayoutManager.isPinnedIndex(columnIndex); + } + + /** + * Pins a column. + * @param columnIndex The index of the column to pin. + */ + pinColumn(columnIndex: number) { + // If column pinning is enabled, and the maximum pinned columns limit has not been reached, pin the column. + if (this._columnPinning && this._columnLayoutManager.pinnedIndexesCount < this._maximumPinnedColumns && this._columnLayoutManager.pinIndex(columnIndex)) { + this.clearSelection(); + this.fireOnDidUpdateEvent(); + } + } + + /** + * Unpins a column. + * @param columnIndex The index of the column to unpin. + */ + unpinColumn(columnIndex: number) { + // If column pinning is enabled, unpin the column. + if (this._columnPinning && this._columnLayoutManager.unpinIndex(columnIndex)) { + this.clearSelection(); + this.fireOnDidUpdateEvent(); + } + } + + /** + * Returns a value which indicates whether the specified row is pinned. + * @param rowIndex The row index. + * @returns true if the specified row is pinned; otherwise, false. + */ + isRowPinned(rowIndex: number) { + return this._rowLayoutManager.isPinnedIndex(rowIndex); + } + + /** + * Pins a row. + * @param rowIndex The index of the row to pin. + */ + pinRow(rowIndex: number) { + // If row pinning is enabled, and the maximum pinned rows limit has not been reached, pin the row. + if (this._rowPinning && this._rowLayoutManager.pinnedIndexesCount < this._maximumPinnedRows && this._rowLayoutManager.pinIndex(rowIndex)) { + this.clearSelection(); + this.fireOnDidUpdateEvent(); + } + } + + /** + * Unpins a row. + * @param rowIndex The index of the row to unpin. + */ + unpinRow(rowIndex: number) { + // If row pinning is enabled, unpin the row. + if (this._rowPinning && this._rowLayoutManager.unpinIndex(rowIndex)) { + this.clearSelection(); + this.fireOnDidUpdateEvent(); + } + } + + /** + * Moves the cursor up. + */ + moveCursorUp() { + // Get the previous row index using the row layout manager. + const previousRowIndex = this._rowLayoutManager.previousIndex(this._cursorRowIndex) + + // If the previous row index is undefined, this means that the cursor is already at the top row. + if (previousRowIndex === undefined) { + return; + } + + // Set the cursor row index to the previous row index and fire the onDidUpdate event. + this._cursorRowIndex = previousRowIndex; + + this.scrollToCursor() + + this.fireOnDidUpdateEvent(); + } + + /** + * Moves the cursor down. + */ + moveCursorDown() { + // Get the next row index using the row layout manager. + const nextRowIndex = this._rowLayoutManager.nextIndex(this._cursorRowIndex) + + // If the next row index is undefined, this means that the cursor is already at the bottom row. + if (nextRowIndex === undefined) { + return; + } + + // Set the cursor row index to the next row index and fire the onDidUpdate event. + this._cursorRowIndex = nextRowIndex; + + // Scroll to the cursor. + this.scrollToCursor() + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } + + /** + * Moves the cursor left. + */ + moveCursorLeft() { + // Get the previous column index using the column layout manager. + const previousColumnIndex = this._columnLayoutManager.previousIndex(this._cursorColumnIndex) + + // If the previous column index is undefined, this means that the cursor is already at the first column. + if (previousColumnIndex === undefined) { + return; + } + + // Set the cursor column index to the previous column index. + this._cursorColumnIndex = previousColumnIndex; + + // Scroll to the cursor column index. + this.scrollToColumn(this._cursorColumnIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } + + /** + * Moves the cursor right. + */ + moveCursorRight() { + // Get the next column index using the column layout manager. + const nextColumnIndex = this._columnLayoutManager.nextIndex(this._cursorColumnIndex) + + // If the next column index is undefined, this means that the cursor is already at the last column. + if (nextColumnIndex === undefined) { + return; + } + + // Set the cursor column index to the next column index. + this._cursorColumnIndex = nextColumnIndex; + + // Scroll to the cursor column index. + this.scrollToColumn(this._cursorColumnIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } + /** * Scrolls to the cursor. * @returns A Promise that resolves when the operation is complete. @@ -1729,7 +2047,7 @@ export abstract class DataGridInstance extends Disposable { this._verticalScrollOffset = rowLayoutEntry.start; scrollOffsetUpdated = true; } else if (rowLayoutEntry.end > this._verticalScrollOffset + this.layoutHeight) { - this._verticalScrollOffset = rowIndex === this.rows - 1 ? + this._verticalScrollOffset = rowIndex === this._rowLayoutManager.lastIndex ? this._verticalScrollOffset = this.maximumVerticalScrollOffset : this._verticalScrollOffset = rowLayoutEntry.end - this.layoutHeight; scrollOffsetUpdated = true; @@ -1741,7 +2059,7 @@ export abstract class DataGridInstance extends Disposable { await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } } @@ -1750,7 +2068,12 @@ export abstract class DataGridInstance extends Disposable { * @param columnIndex The column index. * @returns A Promise that resolves when the operation is complete. */ - async scrollToColumn(columnIndex: number): Promise { + async scrollToColumn(columnIndex: number) { + // If the column is pinned, return. + if (this._columnLayoutManager.isPinnedIndex(columnIndex)) { + return; + } + // Get the column layout entry. If it wasn't found, return. const columnLayoutEntry = this._columnLayoutManager.getLayoutEntry(columnIndex); if (!columnLayoutEntry) { @@ -1770,6 +2093,11 @@ export abstract class DataGridInstance extends Disposable { * @param rowIndex The row index. */ async scrollToRow(rowIndex: number) { + // If the row is pinned, return. + if (this._rowLayoutManager.isPinnedIndex(rowIndex)) { + return; + } + // Get the row layout entry. If it wasn't found, return. const rowLayoutEntry = this._rowLayoutManager.getLayoutEntry(rowIndex); if (!rowLayoutEntry) { @@ -1789,18 +2117,29 @@ export abstract class DataGridInstance extends Disposable { */ selectAll() { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; - // Clear column selection. - this._columnSelectionRange = undefined; + // Select all. this._columnSelectionIndexes = undefined; - - // Select all by selecting all rows. (We could have done this with selecting all columns.) this._rowSelectionIndexes = undefined; - this._rowSelectionRange = new SelectionRange(0, this.rows - 1); + + // Get the column indexes for all columns. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(0, this._columnLayoutManager.entryCount - 1); + if (columnIndexes === undefined) { + return; + } + + // Get the row indexes for all rows. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(0, this._rowLayoutManager.entryCount - 1); + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } /** @@ -1808,19 +2147,18 @@ export abstract class DataGridInstance extends Disposable { * @param columnIndex The column index. * @param rowIndex The row index. * @param selectionType The mouse selection type. - * @returns A Promise that resolves when the operation is complete. + * @returns A Promise that resolves when the operation is complete. */ async mouseSelectCell( columnIndex: number, rowIndex: number, + pinned: boolean, selectionType: MouseSelectionType - ) { + ): Promise { // Clear column selection. - this._columnSelectionRange = undefined; this._columnSelectionIndexes = undefined; // Clear row selection. - this._rowSelectionRange = undefined; this._rowSelectionIndexes = undefined; // Process the selection based on selection type. @@ -1828,38 +2166,81 @@ export abstract class DataGridInstance extends Disposable { // Single selection. case MouseSelectionType.Single: { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; - // Adjust the cursor and scroll to it. + // Adjust the cursor position. this.setCursorPosition(columnIndex, rowIndex); - await this.scrollToCursor(); + + // If the cursor position isn't pinned, scroll to it. + if (!pinned) { + await this.scrollToCursor(); + } // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Range selection. case MouseSelectionType.Range: { - // Create a new cell selection range. - this._cellSelectionRange = new CellSelectionRange( - Math.min(this._cursorColumnIndex, columnIndex), - Math.min(this._cursorRowIndex, rowIndex), - Math.max(this._cursorColumnIndex, columnIndex), - Math.max(this._cursorRowIndex, rowIndex), - ); + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // Get the column position. + const columnPosition = this._columnLayoutManager.mapIndexToPosition(columnIndex); + if (columnPosition === undefined) { + return; + } + + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined) { + return; + } + + // Get the row position. + const rowPosition = this._rowLayoutManager.mapIndexToPosition(rowIndex); + if (rowPosition === undefined) { + return; + } + + // Determine the first column position and the last column position. + const firstColumnPosition = Math.min(cursorColumnPosition, columnPosition); + const lastColumnPosition = Math.max(cursorColumnPosition, columnPosition); + + // Determine the first row position and the last row position. + const firstRowPosition = Math.min(cursorRowPosition, rowPosition); + const lastRowPosition = Math.max(cursorRowPosition, rowPosition); + + // Calculate the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition, lastColumnPosition); + if (columnIndexes === undefined) { + return; + } + + // Calculate the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition, lastRowPosition); + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); // Scroll the cell into view. await this.scrollToCell(columnIndex, rowIndex); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Multi selection. case MouseSelectionType.Multi: { - // Not supported at this time. Do nothing. + // Not supported at this time. Silently succeed. return; } } @@ -1871,18 +2252,16 @@ export abstract class DataGridInstance extends Disposable { */ selectColumn(columnIndex: number) { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; // Clear row selection. - this._rowSelectionRange = undefined; this._rowSelectionIndexes = undefined; // Single select the column. - this._columnSelectionRange = undefined; - this._columnSelectionIndexes = new SelectionIndexes(columnIndex); + this._columnSelectionIndexes = new SelectionIndexes([columnIndex]); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } /** @@ -1893,10 +2272,9 @@ export abstract class DataGridInstance extends Disposable { */ async mouseSelectColumn(columnIndex: number, selectionType: MouseSelectionType): Promise { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; // Clear row selection. - this._rowSelectionRange = undefined; this._rowSelectionIndexes = undefined; /** @@ -1914,76 +2292,90 @@ export abstract class DataGridInstance extends Disposable { // Single selection. case MouseSelectionType.Single: { // Single select the column. - this._columnSelectionRange = undefined; - this._columnSelectionIndexes = new SelectionIndexes(columnIndex); + this._columnSelectionIndexes = new SelectionIndexes([columnIndex]); - // Adjust the cursor and update the waffle. + // Adjust the cursor and scroll to the column. await adjustCursor(columnIndex); await this.scrollToColumn(columnIndex); + + // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Range selection. case MouseSelectionType.Range: { - // Clear individually-selected columns. - this._columnSelectionIndexes = undefined; + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // Get the column position. + const columnPosition = this._columnLayoutManager.mapIndexToPosition(columnIndex); + if (columnPosition === undefined) { + return; + } + + // Determine the first column position and the last column position. + const firstColumnPosition = Math.min(cursorColumnPosition, columnPosition); + const lastColumnPosition = Math.max(cursorColumnPosition, columnPosition); + + // Calculate the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition, lastColumnPosition); + if (columnIndexes === undefined) { + return; + } - // Set the column selection range. - this._columnSelectionRange = new SelectionRange( - Math.min(this._cursorColumnIndex, columnIndex), - Math.max(this._cursorColumnIndex, columnIndex) - ); + // Set the column selection indexes. + this._columnSelectionIndexes = new SelectionIndexes(columnIndexes); // Update the waffle. await this.scrollToColumn(columnIndex); + + // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Multi selection. case MouseSelectionType.Multi: { - // If the column index is part of the column selection range, ignore the event to - // preserve the user's selection. - if (this._columnSelectionRange?.contains(columnIndex)) { - return; - } - - // Multi select the column. - if (this._columnSelectionIndexes?.has(columnIndex)) { - // Unselect the column. - this._columnSelectionIndexes.delete(columnIndex); - if (this._columnSelectionIndexes.isEmpty()) { - this._columnSelectionIndexes = undefined; - } + // Build the column selection indexes. + let indexes: number[] = []; + if (this._columnSelectionIndexes === undefined) { + indexes.push(columnIndex); } else { - // Select the column. - if (this._columnSelectionIndexes) { - this._columnSelectionIndexes.add(columnIndex); + if (this._columnSelectionIndexes.contains(columnIndex)) { + indexes = this._columnSelectionIndexes.indexes.filter(index => index !== columnIndex); } else { - if (!this._columnSelectionRange) { - this._columnSelectionIndexes = new SelectionIndexes(columnIndex); - } else { - this._columnSelectionIndexes = this._columnSelectionRange.indexes(); - this._columnSelectionIndexes.add(columnIndex); - this._columnSelectionRange = undefined; - } + indexes = [...this._columnSelectionIndexes.indexes, columnIndex]; } + } - // Adjust the cursor and update the waffle. - await adjustCursor(columnIndex); - await this.scrollToColumn(columnIndex); - await this.fetchData(); + // Set the column selection indexes. + if (indexes.length === 0) { + this._columnSelectionIndexes = undefined; + } else { + this._columnSelectionIndexes = new SelectionIndexes(indexes); } + // Adjust the cursor. + await adjustCursor(columnIndex); + + // Scroll to the column. + await this.scrollToColumn(columnIndex); + + // Fetch data. + await this.fetchData(); + // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } } @@ -1995,18 +2387,16 @@ export abstract class DataGridInstance extends Disposable { */ selectRow(rowIndex: number) { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; // Clear column selection. - this._columnSelectionRange = undefined; this._columnSelectionIndexes = undefined; // Single select the row. - this._rowSelectionRange = undefined; - this._rowSelectionIndexes = new SelectionIndexes(rowIndex); + this._rowSelectionIndexes = new SelectionIndexes([rowIndex]); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } /** @@ -2017,10 +2407,9 @@ export abstract class DataGridInstance extends Disposable { */ async mouseSelectRow(rowIndex: number, selectionType: MouseSelectionType): Promise { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; // Clear column selection. - this._columnSelectionRange = undefined; this._columnSelectionIndexes = undefined; /** @@ -2038,76 +2427,90 @@ export abstract class DataGridInstance extends Disposable { // Single selection. case MouseSelectionType.Single: { // Single select the row. - this._rowSelectionRange = undefined; - this._rowSelectionIndexes = new SelectionIndexes(rowIndex); + this._rowSelectionIndexes = new SelectionIndexes([rowIndex]); - // Adjust the cursor and update the waffle. + // Adjust the cursor and scroll to the row. await adjustCursor(rowIndex); await this.scrollToRow(rowIndex); + + // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Range selection. case MouseSelectionType.Range: { - // Clear individually-selected rows. - this._rowSelectionIndexes = undefined; + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined) { + return; + } + + // Get the row position. + const rowPosition = this._rowLayoutManager.mapIndexToPosition(rowIndex); + if (rowPosition === undefined) { + return; + } - // Set the row selection range. - this._rowSelectionRange = new SelectionRange( - Math.min(this._cursorRowIndex, rowIndex), - Math.max(this._cursorRowIndex, rowIndex) - ); + // Determine the first row position and the last row position. + const firstRowPosition = Math.min(cursorRowPosition, rowPosition); + const lastRowPosition = Math.max(cursorRowPosition, rowPosition); - // Update the waffle. + // Calculate the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition, lastRowPosition); + if (rowIndexes === undefined) { + return; + } + + // Set the row selection indexes. + this._rowSelectionIndexes = new SelectionIndexes(rowIndexes); + + // Scroll to the row. await this.scrollToRow(rowIndex); + + // Fetch data. await this.fetchData(); // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } // Multi selection. case MouseSelectionType.Multi: { - // If the row index is part of the row selection range, ignore the event to preserve - // the user's selection. - if (this._rowSelectionRange?.contains(rowIndex)) { - return; - } - - // Multi select the row. - if (this._rowSelectionIndexes?.has(rowIndex)) { - // Unselect the row. - this._rowSelectionIndexes.delete(rowIndex); - if (this._rowSelectionIndexes.isEmpty()) { - this._rowSelectionIndexes = undefined; - } + // Build the row selection indexes. + let indexes: number[] = []; + if (this._rowSelectionIndexes === undefined) { + indexes.push(rowIndex); } else { - // Select the row. - if (this._rowSelectionIndexes) { - this._rowSelectionIndexes.add(rowIndex); + if (this._rowSelectionIndexes.contains(rowIndex)) { + indexes = this._rowSelectionIndexes.indexes.filter(index => index !== rowIndex); } else { - if (!this._rowSelectionRange) { - this._rowSelectionIndexes = new SelectionIndexes(rowIndex); - } else { - this._rowSelectionIndexes = this._rowSelectionRange.indexes(); - this._rowSelectionIndexes.add(rowIndex); - this._rowSelectionRange = undefined; - } + indexes = [...this._rowSelectionIndexes.indexes, rowIndex]; } + } - // Adjust the cursor and update the waffle. - await adjustCursor(rowIndex); - await this.scrollToRow(rowIndex); - await this.fetchData(); + // Set the row selection indexes. + if (indexes.length === 0) { + this._rowSelectionIndexes = undefined; + } else { + this._rowSelectionIndexes = new SelectionIndexes(indexes); } + // Adjust the cursor. + await adjustCursor(rowIndex); + + // Scroll to the row. + await this.scrollToRow(rowIndex); + + // Fetch data. + await this.fetchData(); + // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); break; } } @@ -2119,67 +2522,139 @@ export abstract class DataGridInstance extends Disposable { */ extendColumnSelectionLeft(extendColumnSelectionBy: ExtendColumnSelectionBy) { // If there is a row selection, do nothing. - if (this._rowSelectionRange || this._rowSelectionIndexes) { + if (this._rowSelectionIndexes) { return; } // Process extend selection left based on what is currently selected. if (this._columnSelectionIndexes) { - // Convert an individually selected column into a column selection range, if possible. - if (this._columnSelectionIndexes.has(this._cursorColumnIndex)) { - if (this._cursorColumnIndex > 0) { - // Clear the individually-selected columns. - this._columnSelectionIndexes = undefined; + // Extend column selection left. + if (this._columnSelectionIndexes.contains(this._cursorColumnIndex)) { + // Get the cursor column position. If it's undefined, or the first column position, return. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined || cursorColumnPosition === 0) { + return; + } - // Set the column selection range. - this._columnSelectionRange = new SelectionRange( - this._cursorColumnIndex - 1, - this._cursorColumnIndex - ); + // Get the previous column index. + const previousColumnIndex = this._columnLayoutManager.mapPositionToIndex(cursorColumnPosition - 1); + if (previousColumnIndex === undefined) { + return; + } - // Sroll to the column. - this.scrollToColumn(this._columnSelectionRange.firstIndex); + // Move the cursor to the previous column index. + this.setCursorColumn(previousColumnIndex); - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + // Update the column selection indexes. + if (!this._columnSelectionIndexes.contains(previousColumnIndex)) { + this._columnSelectionIndexes = new SelectionIndexes([previousColumnIndex, ...this._columnSelectionIndexes.indexes]); } + + // Sroll to the column. + this.scrollToColumn(previousColumnIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._columnSelectionRange) { - // Expand or contract the column selection range, if possible. - if (this._cursorColumnIndex === this._columnSelectionRange.lastIndex) { - if (this._columnSelectionRange.firstIndex > 0) { - this._columnSelectionRange.firstIndex--; - this.scrollToColumn(this._columnSelectionRange.firstIndex); - this._onDidUpdateEmitter.fire(); + } else if (this._cellSelectionIndexes) { + if (this._cursorColumnIndex === this._cellSelectionIndexes.lastColumnIndex) { + // Get the first column position. + const firstColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstColumnIndex); + if (!firstColumnPosition) { + return; } - } else if (this._cursorColumnIndex === this._columnSelectionRange.firstIndex) { - this._columnSelectionRange.lastIndex--; - this.scrollToColumn(this._columnSelectionRange.lastIndex); - this._onDidUpdateEmitter.fire(); - } - } else if (this._cellSelectionRange) { - // Expand or contract the cell selection range along the column axis, if possible. - if (this._cursorColumnIndex === this._cellSelectionRange.lastColumnIndex) { - if (this._cellSelectionRange.firstColumnIndex > 0) { - this._cellSelectionRange.firstColumnIndex--; - this.scrollToColumn(this._cellSelectionRange.firstColumnIndex); - this._onDidUpdateEmitter.fire(); + + // If the first column cannot be moved left, return. + if (!(firstColumnPosition > 0)) { + return; + } + + // Get the last column position. + const lastColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastColumnIndex); + if (lastColumnPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition - 1, lastColumnPosition); + if (columnIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, this._cellSelectionIndexes.rowIndexes); + + // Scroll to the column. + this.scrollToColumn(columnIndexes[0]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } else if (this._cursorColumnIndex === this._cellSelectionIndexes.firstColumnIndex) { + // Get the first column position. + const firstColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstColumnIndex); + if (firstColumnPosition === undefined) { + return; + } + + // Get the last column position. + const lastColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastColumnIndex); + if (lastColumnPosition === undefined) { + return; } - } else if (this._cursorColumnIndex === this._cellSelectionRange.firstColumnIndex) { - this._cellSelectionRange.lastColumnIndex--; - this.scrollToColumn(this._cellSelectionRange.lastColumnIndex); - this._onDidUpdateEmitter.fire(); + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition, lastColumnPosition - 1); + if (columnIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, this._cellSelectionIndexes.rowIndexes); + + // Scroll to the column. + this.scrollToColumn(columnIndexes[columnIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._cursorColumnIndex > 0) { - // Create a new cell selection range. - this._cellSelectionRange = new CellSelectionRange( - this._cursorColumnIndex - 1, - this._cursorRowIndex, - this._cursorColumnIndex, - this._cursorRowIndex - ); - this.scrollToCell(this._cellSelectionRange.firstColumnIndex, this._cursorRowIndex); - this._onDidUpdateEmitter.fire(); + } else { + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // If the cursor column position cannot be moved left, return. + if (!(cursorColumnPosition > 0)) { + return; + } + + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(cursorColumnPosition - 1, cursorColumnPosition) + if (columnIndexes === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(cursorRowPosition, cursorRowPosition) + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); + + // Scroll to the cell. + this.scrollToCell(columnIndexes[columnIndexes.length - 1], this._cursorRowIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } } @@ -2189,67 +2664,140 @@ export abstract class DataGridInstance extends Disposable { */ extendColumnSelectionRight(extendColumnSelectionBy: ExtendColumnSelectionBy) { // If there is a row selection, do nothing. - if (this._rowSelectionRange || this._rowSelectionIndexes) { + if (this._rowSelectionIndexes) { return; } // Process extend selection right based on what is currently selected. if (this._columnSelectionIndexes) { - // Convert an individually selected column into a column selection range, if possible. - if (this._columnSelectionIndexes.has(this._cursorColumnIndex)) { - if (this._cursorColumnIndex < this.columns - 1) { - // Clear the individually-selected columns. - this._columnSelectionIndexes = undefined; + // Extend column selection right. + if (this._columnSelectionIndexes.contains(this._cursorColumnIndex)) { + // Get the cursor column position. If it's undefined, or the last column position, return. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined || cursorColumnPosition === this._columnLayoutManager.entryCount - 1) { + return; + } - // Set the column selection range. - this._columnSelectionRange = new SelectionRange( - this._cursorColumnIndex, - this._cursorColumnIndex + 1 - ); + // Get the next column index. + const nextColumnIndex = this._columnLayoutManager.mapPositionToIndex(cursorColumnPosition + 1); + if (nextColumnIndex === undefined) { + return; + } - // Sroll to the column. - this.scrollToColumn(this._columnSelectionRange.lastIndex); + // Move the cursor to the next column index. + this.setCursorColumn(nextColumnIndex); - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + // Update the column selection indexes. + if (!this._columnSelectionIndexes.contains(nextColumnIndex)) { + this._columnSelectionIndexes = new SelectionIndexes([...this._columnSelectionIndexes.indexes, nextColumnIndex]); } + + // Sroll to the column. + this.scrollToColumn(nextColumnIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._columnSelectionRange) { - // Expand or contract the column selection range, if possible. - if (this._cursorColumnIndex === this._columnSelectionRange.firstIndex) { - if (this._columnSelectionRange.lastIndex < this.columns - 1) { - this._columnSelectionRange.lastIndex++; - this.scrollToColumn(this._columnSelectionRange.lastIndex); - this._onDidUpdateEmitter.fire(); - } - } else if (this._cursorColumnIndex === this._columnSelectionRange.lastIndex) { - this._columnSelectionRange.firstIndex++; - this.scrollToColumn(this._columnSelectionRange.firstIndex); - this._onDidUpdateEmitter.fire(); - } - } else if (this._cellSelectionRange) { + } else if (this._cellSelectionIndexes) { // Expand or contract the cell selection range along the column axis, if possible. - if (this._cursorColumnIndex === this._cellSelectionRange.firstColumnIndex) { - if (this._cellSelectionRange.lastColumnIndex < this.columns - 1) { - this._cellSelectionRange.lastColumnIndex++; - this.scrollToColumn(this._cellSelectionRange.lastColumnIndex); - this._onDidUpdateEmitter.fire(); + if (this._cursorColumnIndex === this._cellSelectionIndexes.firstColumnIndex) { + // Get the last column position. + const lastColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastColumnIndex); + if (!lastColumnPosition) { + return; + } + + // If the last column cannot be moved right, return. + if (!(lastColumnPosition < this._columnLayoutManager.entryCount - 1)) { + return; + } + + // Get the first column position. + const firstColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstColumnIndex); + if (firstColumnPosition === undefined) { + return; } - } else if (this._cursorColumnIndex === this._cellSelectionRange.lastColumnIndex) { - this._cellSelectionRange.firstColumnIndex++; - this.scrollToColumn(this._cellSelectionRange.firstColumnIndex); - this._onDidUpdateEmitter.fire(); + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition, lastColumnPosition + 1); + if (columnIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, this._cellSelectionIndexes.rowIndexes); + + // Scroll to the column. + this.scrollToColumn(columnIndexes[columnIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } else if (this._cursorColumnIndex === this._cellSelectionIndexes.lastColumnIndex) { + // Get the first column position. + const firstColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstColumnIndex); + if (firstColumnPosition === undefined) { + return; + } + + // Get the last column position. + const lastColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastColumnIndex); + if (lastColumnPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(firstColumnPosition + 1, lastColumnPosition); + if (columnIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, this._cellSelectionIndexes.rowIndexes); + + // Scroll to the column. + this.scrollToColumn(columnIndexes[columnIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._cursorColumnIndex < this.columns - 1) { - // Create a new cell selection range. - this._cellSelectionRange = new CellSelectionRange( - this._cursorColumnIndex, - this._cursorRowIndex, - this._cursorColumnIndex + 1, - this._cursorRowIndex - ); - this.scrollToCell(this._cellSelectionRange.lastColumnIndex, this._cursorRowIndex); - this._onDidUpdateEmitter.fire(); + } else { + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // If the cursor column position cannot be moved right, return. + if (!(cursorColumnPosition < this._columnLayoutManager.entryCount - 1)) { + return; + } + + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(cursorColumnPosition, cursorColumnPosition + 1) + if (columnIndexes === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(cursorRowPosition, cursorRowPosition) + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); + + // Scroll to the cell. + this.scrollToCell(columnIndexes[columnIndexes.length - 1], this._cursorRowIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } } @@ -2259,67 +2807,139 @@ export abstract class DataGridInstance extends Disposable { */ extendRowSelectionUp(extendRowSelectionBy: ExtendRowSelectionBy) { // If there is a column selection, do nothing. - if (this._columnSelectionRange || this._columnSelectionIndexes) { + if (this._columnSelectionIndexes) { return; } // Process extend selection up based on what is currently selected. if (this._rowSelectionIndexes) { - // Convert an individually selected row into a row selection range, if possible. - if (this._rowSelectionIndexes.has(this._cursorRowIndex)) { - if (this._cursorRowIndex > 0) { - // Clear the individually-selected rows. - this._rowSelectionIndexes = undefined; + // Extend row selection up. + if (this._rowSelectionIndexes.contains(this._cursorRowIndex)) { + // Get the cursor row position. If it's undefined, or the first row position, return. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined || cursorRowPosition === 0) { + return; + } - // Set the row selection range. - this._rowSelectionRange = new SelectionRange( - this._cursorRowIndex - 1, - this._cursorRowIndex - ); + // Get the previous row index. + const previousRowIndex = this._rowLayoutManager.mapPositionToIndex(cursorRowPosition - 1); + if (previousRowIndex === undefined) { + return; + } - // Scroll the row into view. - this.scrollToRow(this._rowSelectionRange.firstIndex); + // Move the cursor to the previous row index. + this.setCursorRow(previousRowIndex); - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + // Update the row selection indexes. + if (!this._rowSelectionIndexes.contains(previousRowIndex)) { + this._rowSelectionIndexes = new SelectionIndexes([previousRowIndex, ...this._rowSelectionIndexes.indexes]); } + + // Sroll to the row. + this.scrollToRow(previousRowIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._rowSelectionRange) { - // Expand or contract the row selection range, if possible. - if (this._cursorRowIndex === this._rowSelectionRange.lastIndex) { - if (this._rowSelectionRange.firstIndex > 0) { - this._rowSelectionRange.firstIndex--; - this.scrollToRow(this._rowSelectionRange.firstIndex); - this._onDidUpdateEmitter.fire(); + } else if (this._cellSelectionIndexes) { + if (this._cursorRowIndex === this._cellSelectionIndexes.lastRowIndex) { + // Get the first row position. + const firstRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstRowIndex); + if (!firstRowPosition) { + return; } - } else if (this._cursorRowIndex === this._rowSelectionRange.firstIndex) { - this._rowSelectionRange.lastIndex--; - this.scrollToRow(this._rowSelectionRange.lastIndex); - this._onDidUpdateEmitter.fire(); - } - } else if (this._cellSelectionRange) { - // Expand or contract the cell selection range along the row axis, if possible. - if (this._cursorRowIndex === this._cellSelectionRange.lastRowIndex) { - if (this._cellSelectionRange.firstRowIndex > 0) { - this._cellSelectionRange.firstRowIndex--; - this.scrollToRow(this._cellSelectionRange.firstRowIndex); - this._onDidUpdateEmitter.fire(); + + // If the first row cannot be moved up, return. + if (!(firstRowPosition > 0)) { + return; + } + + // Get the last row position. + const lastRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastRowIndex); + if (lastRowPosition === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition - 1, lastRowPosition); + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(this._cellSelectionIndexes.columnIndexes, rowIndexes); + + // Scroll to the row. + this.scrollToRow(rowIndexes[0]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } else if (this._cursorRowIndex === this._cellSelectionIndexes.firstRowIndex) { + // Get the first row position. + const firstRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstRowIndex); + if (firstRowPosition === undefined) { + return; + } + + // Get the last row position. + const lastRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastRowIndex); + if (lastRowPosition === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition, lastRowPosition - 1); + if (rowIndexes === undefined) { + return; } - } else if (this._cursorRowIndex === this._cellSelectionRange.firstRowIndex) { - this._cellSelectionRange.lastRowIndex--; - this.scrollToRow(this._cellSelectionRange.lastRowIndex); - this._onDidUpdateEmitter.fire(); + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(this._cellSelectionIndexes.columnIndexes, rowIndexes); + + // Scroll to the row. + this.scrollToRow(rowIndexes[rowIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._cursorRowIndex > 0) { - // Create a new cell selection range. - this._cellSelectionRange = new CellSelectionRange( - this._cursorColumnIndex, - this._cursorRowIndex - 1, - this._cursorColumnIndex, - this._cursorRowIndex - ); - this.scrollToCell(this._cursorColumnIndex, this._cellSelectionRange.firstRowIndex); - this._onDidUpdateEmitter.fire(); + } else { + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (!cursorRowPosition) { + return; + } + + // If the cursor row position cannot be moved up, return. + if (!(cursorRowPosition > 0)) { + return; + } + + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(cursorColumnPosition, cursorColumnPosition) + if (columnIndexes === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(cursorRowPosition - 1, cursorRowPosition) + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); + + // Scroll to the cell. + this.scrollToCell(this._cursorColumnIndex, this._cellSelectionIndexes.firstRowIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } } @@ -2329,67 +2949,140 @@ export abstract class DataGridInstance extends Disposable { */ extendRowSelectionDown(extendRowSelectionBy: ExtendRowSelectionBy) { // If there is a column selection, do nothing. - if (this._columnSelectionRange || this._columnSelectionIndexes) { + if (this._columnSelectionIndexes) { return; } // Process extend selection down based on what is currently selected. if (this._rowSelectionIndexes) { - // Convert an individually selected row into a row selection range, if possible. - if (this._rowSelectionIndexes.has(this._cursorRowIndex)) { - if (this._cursorRowIndex < this.rows - 1) { - // Clear the individually-selected rows. - this._rowSelectionIndexes = undefined; + // Extend row selection down. + if (this._rowSelectionIndexes.contains(this._cursorRowIndex)) { + // Get the cursor row position. If it's undefined, or the last row position, return. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined || cursorRowPosition === this._rowLayoutManager.entryCount - 1) { + return; + } - // Set the row selection range. - this._rowSelectionRange = new SelectionRange( - this._cursorRowIndex, - this._cursorRowIndex + 1 - ); + // Get the next row index. + const nextRowIndex = this._rowLayoutManager.mapPositionToIndex(cursorRowPosition + 1); + if (nextRowIndex === undefined) { + return; + } - // Scroll to the row. - this.scrollToRow(this._rowSelectionRange.lastIndex); + // Move the cursor to the next row index. + this.setCursorRow(nextRowIndex); - // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + // Update the row selection indexes. + if (!this._rowSelectionIndexes.contains(nextRowIndex)) { + this._rowSelectionIndexes = new SelectionIndexes([...this._rowSelectionIndexes.indexes, nextRowIndex]); } + + // Sroll to the row. + this.scrollToRow(nextRowIndex); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._rowSelectionRange) { - // Expand or contract the row selection range, if possible. - if (this._cursorRowIndex === this._rowSelectionRange.firstIndex) { - if (this._rowSelectionRange.lastIndex < this.rows - 1) { - this._rowSelectionRange.lastIndex++; - this.scrollToRow(this._rowSelectionRange.lastIndex); - this._onDidUpdateEmitter.fire(); + } else if (this._cellSelectionIndexes) { + // Expand or contract the row selection range along the row axis, if possible. + if (this._cursorRowIndex === this._cellSelectionIndexes.firstRowIndex) { + // Get the last row position. + const lastRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastRowIndex); + if (!lastRowPosition) { + return; } - } else if (this._cursorRowIndex === this._rowSelectionRange.lastIndex) { - this._rowSelectionRange.firstIndex++; - this.scrollToRow(this._rowSelectionRange.firstIndex); - this._onDidUpdateEmitter.fire(); - } - } else if (this._cellSelectionRange) { - // Expand or contract the cell selection range along the row axis, if possible. - if (this._cursorRowIndex === this._cellSelectionRange.firstRowIndex) { - if (this._cellSelectionRange.lastRowIndex < this.rows - 1) { - this._cellSelectionRange.lastRowIndex++; - this.scrollToRow(this._cellSelectionRange.lastRowIndex); - this._onDidUpdateEmitter.fire(); + + // If the last row cannot be moved down, return. + if (!(lastRowPosition < this._rowLayoutManager.entryCount - 1)) { + return; + } + + // Get the first row position. + const firstRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstRowIndex); + if (firstRowPosition === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition, lastRowPosition + 1); + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(this._cellSelectionIndexes.columnIndexes, rowIndexes); + + // Scroll to the row. + this.scrollToRow(rowIndexes[rowIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); + } else if (this._cursorRowIndex === this._cellSelectionIndexes.lastRowIndex) { + // Get the first row position. + const firstRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.firstRowIndex); + if (firstRowPosition === undefined) { + return; + } + + // Get the last row position. + const lastRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cellSelectionIndexes.lastRowIndex); + if (lastRowPosition === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(firstRowPosition + 1, lastRowPosition); + if (rowIndexes === undefined) { + return; } - } else if (this._cursorRowIndex === this._cellSelectionRange.lastRowIndex) { - this._cellSelectionRange.firstRowIndex++; - this.scrollToRow(this._cellSelectionRange.firstRowIndex); - this._onDidUpdateEmitter.fire(); + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(this._cellSelectionIndexes.columnIndexes, rowIndexes); + + // Scroll to the row. + this.scrollToRow(rowIndexes[0]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } - } else if (this._cursorRowIndex < this.rows - 1) { - // Create a new cell selection range. - this._cellSelectionRange = new CellSelectionRange( - this._cursorColumnIndex, - this._cursorRowIndex, - this._cursorColumnIndex, - this._cursorRowIndex + 1 - ); - this.scrollToCell(this._cursorColumnIndex, this._cellSelectionRange.lastRowIndex); - this._onDidUpdateEmitter.fire(); + } else { + // Get the cursor row position. + const cursorRowPosition = this._rowLayoutManager.mapIndexToPosition(this._cursorRowIndex); + if (cursorRowPosition === undefined) { + return; + } + + // If the cursor column position cannot be moved down, return. + if (!(cursorRowPosition < this._rowLayoutManager.entryCount - 1)) { + return; + } + + // Get the cursor column position. + const cursorColumnPosition = this._columnLayoutManager.mapIndexToPosition(this._cursorColumnIndex); + if (cursorColumnPosition === undefined) { + return; + } + + // Build the column indexes. + const columnIndexes = this._columnLayoutManager.mapPositionsToIndexes(cursorColumnPosition, cursorColumnPosition) + if (columnIndexes === undefined) { + return; + } + + // Build the row indexes. + const rowIndexes = this._rowLayoutManager.mapPositionsToIndexes(cursorRowPosition, cursorRowPosition + 1) + if (rowIndexes === undefined) { + return; + } + + // Set the cell selection indexes. + this._cellSelectionIndexes = new CellSelectionIndexes(columnIndexes, rowIndexes); + + // Scroll to the cell. + this.scrollToCell(this._cursorColumnIndex, rowIndexes[rowIndexes.length - 1]); + + // Fire the onDidUpdate event. + this.fireOnDidUpdateEvent(); } } @@ -2400,34 +3093,58 @@ export abstract class DataGridInstance extends Disposable { * @returns A CellSelectionState that represents the cell selection state. */ cellSelectionState(columnIndex: number, rowIndex: number) { - // If there isn't a cell selection range, return the column selection state and the row - // selection state. - if (!this._cellSelectionRange) { - return this.columnSelectionState(columnIndex) | this.rowSelectionState(rowIndex); + // If there isn't a cell selection, return the column selection state or the row selection state. + if (!this._cellSelectionIndexes) { + // Return the column selection state. + let columnSelectionState = this.columnSelectionState(columnIndex); + if (columnSelectionState !== ColumnSelectionState.None) { + // If the row index is the last index, set the selected bottom bit. + if (rowIndex === this._rowLayoutManager.lastIndex) { + columnSelectionState |= RowSelectionState.SelectedBottom; + } + + // Return the column selection state. + return columnSelectionState; + } + + // Return the row selection state. + const rowSelectionState = this.rowSelectionState(rowIndex); + if (rowSelectionState !== RowSelectionState.None) { + // If the column index is the last index, set the selected right bit. + if (columnIndex === this._columnLayoutManager.lastIndex) { + columnSelectionState |= ColumnSelectionState.SelectedRight; + } + + // Return the row selection state. + return rowSelectionState; + } + + // The cell is not selected. + return CellSelectionState.None; } // If the cell is selected, return the cell selection state. - if (this._cellSelectionRange?.contains(columnIndex, rowIndex)) { + if (this._cellSelectionIndexes.contains(columnIndex, rowIndex)) { // Set the selected bit. let cellSelectionState = CellSelectionState.Selected; // If the column index is the first selected column index, set the selected left bit. - if (columnIndex === this._cellSelectionRange.firstColumnIndex) { + if (columnIndex === this._cellSelectionIndexes.firstColumnIndex) { cellSelectionState |= CellSelectionState.SelectedLeft; } // If the column index is the last selected column index, set the selected right bit. - if (columnIndex === this._cellSelectionRange.lastColumnIndex) { + if (columnIndex === this._cellSelectionIndexes.lastColumnIndex) { cellSelectionState |= CellSelectionState.SelectedRight; } // If the row index is the first selected row index, set the selected top bit. - if (rowIndex === this._cellSelectionRange.firstRowIndex) { + if (rowIndex === this._cellSelectionIndexes.firstRowIndex) { cellSelectionState |= CellSelectionState.SelectedTop; } // If the row index is the last selected row index, set the selected bottom bit. - if (rowIndex === this._cellSelectionRange.lastRowIndex) { + if (rowIndex === this._cellSelectionIndexes.lastRowIndex) { cellSelectionState |= CellSelectionState.SelectedBottom; } @@ -2442,99 +3159,29 @@ export abstract class DataGridInstance extends Disposable { /** * Returns the column selection state. * @param columnIndex The column index. - * @returns A SelectionState that represents the column selection state. + * @returns A ColumnSelectionState that represents the column selection state. */ columnSelectionState(columnIndex: number) { - // If the column index is individually selected, return the appropriate selection state. - if (this._columnSelectionIndexes?.has(columnIndex)) { - // The column index is selected. - let selectionState = ColumnSelectionState.Selected; - - // See if the column index is the left selected column index in a range. - if (!this._columnSelectionIndexes.has(columnIndex - 1)) { - selectionState |= ColumnSelectionState.SelectedLeft; - } - - // See if the column index is the right selected column index in a range. - if (!this._columnSelectionIndexes.has(columnIndex + 1)) { - selectionState |= ColumnSelectionState.SelectedRight; - } - - // Return the selection state. - return selectionState; - } - - // See if the column index is in the column selection range. - if (this._columnSelectionRange && - columnIndex >= this._columnSelectionRange.firstIndex && - columnIndex <= this._columnSelectionRange.lastIndex) { - // The column index is selected. - let selectionState = ColumnSelectionState.Selected; - - // See if the column index is the first selected column index. - if (columnIndex === this._columnSelectionRange.firstIndex) { - selectionState |= ColumnSelectionState.SelectedLeft; - } - - // See if the column index is the last selected column index. - if (columnIndex === this._columnSelectionRange.lastIndex) { - selectionState |= ColumnSelectionState.SelectedRight; - } - - // Return the selection state. - return selectionState; + // If the column isn't selected, return none. + if (this._columnSelectionIndexes === undefined || !this._columnSelectionIndexes.contains(columnIndex)) { + return ColumnSelectionState.None; + } else { + return ColumnSelectionState.Selected | ColumnSelectionState.SelectedLeft | ColumnSelectionState.SelectedRight; } - - // The column is not selected. - return ColumnSelectionState.None; } /** * Returns the row selection state. * @param rowIndex The row index. - * @returns A SelectionState that represents the row selection state. + * @returns A RowSelectionState that represents the row selection state. */ rowSelectionState(rowIndex: number) { - // If the row index is individually selected, return the appropriate selection state. - if (this._rowSelectionIndexes?.has(rowIndex)) { - // The row index is selected. - let selectionState = RowSelectionState.Selected; - - // See if the row index is the first row index in a range. - if (!this._rowSelectionIndexes.has(rowIndex - 1)) { - selectionState |= RowSelectionState.SelectedTop; - } - - // See if the row index is the last row index in a range. - if (!this._rowSelectionIndexes.has(rowIndex + 1)) { - selectionState |= RowSelectionState.SelectedBottom; - } - - // Return the selection state. - return selectionState; - } - - // See if the row index is in the selection range. - if (this._rowSelectionRange?.contains(rowIndex)) { - // The row index is selected. - let selectionState = RowSelectionState.Selected; - - // See if the row index is the first selected row index. - if (rowIndex === this._rowSelectionRange.firstIndex) { - selectionState |= RowSelectionState.SelectedTop; - } - - // See if the row index is the last selected row index. - if (rowIndex === this._rowSelectionRange.lastIndex) { - selectionState |= RowSelectionState.SelectedBottom; - } - - // Return the selection state. - return selectionState; + // If the row isn't selected, return none. + if (this._rowSelectionIndexes === undefined || !this._rowSelectionIndexes.contains(rowIndex)) { + return RowSelectionState.None; + } else { + return RowSelectionState.Selected | RowSelectionState.SelectedTop | RowSelectionState.SelectedBottom; } - - // The row is not selected. - return RowSelectionState.None; } /** @@ -2542,18 +3189,16 @@ export abstract class DataGridInstance extends Disposable { */ clearSelection() { // Clear cell selection. - this._cellSelectionRange = undefined; + this._cellSelectionIndexes = undefined; // Clear column selection. - this._columnSelectionRange = undefined; this._columnSelectionIndexes = undefined; // Clear row selection. - this._rowSelectionRange = undefined; this._rowSelectionIndexes = undefined; // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } /** @@ -2562,47 +3207,65 @@ export abstract class DataGridInstance extends Disposable { */ getClipboardData(): ClipboardData | undefined { // Cell selection range. - if (this._cellSelectionRange) { - return new ClipboardCellRange( - this._cellSelectionRange.firstColumnIndex, - this._cellSelectionRange.firstRowIndex, - this._cellSelectionRange.lastColumnIndex, - this._cellSelectionRange.lastRowIndex - ); + if (this._cellSelectionIndexes) { + return new ClipboardCellIndexes(this._cellSelectionIndexes.columnIndexes, this._cellSelectionIndexes.rowIndexes); } - // Column selection range. - if (this._columnSelectionRange) { - return new ClipboardColumnRange( - this._columnSelectionRange.firstIndex, - this._columnSelectionRange.lastIndex - ); - } + /** + * Sorts selection indexes. + * @param selectionIndexes The selection indexes. + * @param layoutManager The layout manager. + * @returns The sorted selection indexes. + */ + const sortSelectionIndexesByPosition = (selectionIndexes: number[], layoutManager: LayoutManager) => { + // Order the selections. + const positionIndexes: PositionIndex[] = []; + for (let i = 0; i < selectionIndexes.length; i++) { + // Get the column index and column position. + const index = selectionIndexes[i]; + const position = layoutManager.mapIndexToPosition(index); + if (position === undefined) { + return selectionIndexes; + } + + // Push the position index. + positionIndexes.push({ + position, + index + }); + } + + // Return the sorted indexes. + return positionIndexes.sort((a, b) => a.position - b.position).map(positionIndex => positionIndex.index); + }; // Column selection indexes. if (this._columnSelectionIndexes) { - return new ClipboardColumnIndexes(this._columnSelectionIndexes.sortedArray()); - } + // Get the column selection indexes. + const columnSelectionIndexes = this._columnSelectionIndexes.indexes; + if (columnSelectionIndexes.length === 0) { + return; + } - // Row selection range. - if (this._rowSelectionRange) { - return new ClipboardRowRange( - this._rowSelectionRange.firstIndex, - this._rowSelectionRange.lastIndex - ); + // Return the sorted column selection indexes. + return new ClipboardColumnIndexes(sortSelectionIndexesByPosition(columnSelectionIndexes, this._columnLayoutManager)); } // Row selection indexes. if (this._rowSelectionIndexes) { - return new ClipboardRowIndexes(this._rowSelectionIndexes.sortedArray()); + // Get the row selection indexes. + const rowSelectionIndexes = this._rowSelectionIndexes.indexes; + if (rowSelectionIndexes.length === 0) { + return; + } + + // Return the sorted row selection indexes. + return new ClipboardColumnIndexes(sortSelectionIndexesByPosition(rowSelectionIndexes, this._columnLayoutManager)); } // Cursor cell. if (this._cursorColumnIndex >= 0 && this._cursorRowIndex >= 0) { - return new ClipboardCell( - this._cursorColumnIndex, - this._cursorRowIndex - ); + return new ClipboardCell(this._cursorColumnIndex, this._cursorRowIndex); } // Clipboard data isn't available. @@ -2633,7 +3296,7 @@ export abstract class DataGridInstance extends Disposable { /** * Gets a column. - * @param rowIndex The row index. + * @param columnIndex The column index. * @returns The row label. */ column(columnIndex: number): IDataColumn | undefined { @@ -2730,13 +3393,33 @@ export abstract class DataGridInstance extends Disposable { * Resets the selection of the data grid. */ protected resetSelection() { - this._cellSelectionRange = undefined; - this._columnSelectionRange = undefined; + this._cellSelectionIndexes = undefined; this._columnSelectionIndexes = undefined; - this._rowSelectionRange = undefined; this._rowSelectionIndexes = undefined; } + /** + * Fires the onDidUpdate event. + */ + protected fireOnDidUpdateEvent() { + // If the onDidUpdate event has already been fired, do nothing. + if (this._pendingOnDidUpdateEvent) { + return; + } + + // Set the pending flag. + this._pendingOnDidUpdateEvent = true; + + // Fire the event in a microtask. + Promise.resolve().then(() => { + // Clear the pending flag. + this._pendingOnDidUpdateEvent = false; + + // Fire the onDidUpdate event. + this._onDidUpdateEmitter.fire(); + }); + } + //#endregion Protected Methods //#region Private Methods diff --git a/src/vs/workbench/browser/positronDataGrid/classes/layoutManager.ts b/src/vs/workbench/browser/positronDataGrid/classes/layoutManager.ts new file mode 100644 index 000000000000..3cbe5b3aa799 --- /dev/null +++ b/src/vs/workbench/browser/positronDataGrid/classes/layoutManager.ts @@ -0,0 +1,1328 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * LayoutManager is used to manage the layout of columns and rows in a Data Grid. + * + * In this code: + * index - Represents the index of a column or row. + * start - Represents the X or Y coordinate of a column or row. + * size - Represents the width or height of a column or row. + * end - Represents the X or Y coordinate of the end of a column or row + * defaultSize - Represents the default width or height of a column or row. + * customSize - Represents the custom width or height of a column or row (as set by a user). + */ + +/** + * Maximum number of layout entries that supports advanced layout features. When this limit is + * exceeded, layout manager falls back to a simplified layout strategy. + */ +export const MAX_ADVANCED_LAYOUT_ENTRY_COUNT = 10_000_000; + +/** + * ILayoutEntry interface. + */ +export interface ILayoutEntry { + /** + * Gets the index. + */ + readonly index: number; + + /** + * Gets the start. + */ + readonly start: number; + + /** + * Gets the size. + */ + readonly size: number; + + /** + * Gets the end. + */ + readonly end: number; +} + +/** + * LayoutManager class. + */ +export class LayoutManager { + //#region Private Properties + + /** + * Gets the default size. + */ + private readonly _defaultSize: number = 0; + + /** + * Gets or sets the entry count. + */ + private _entryCount: number = 0; + + /** + * Gets or sets the entry sizes. + */ + private readonly _entrySizes = new Map(); + + /** + * Gets the custom entry sizes map. + */ + private readonly _customEntrySizes = new Map(); + + /** + * The entry map. Maps position to index. + */ + private _entryMap: number[] = []; + + /** + * The inverse entry map. Maps index to position. + */ + private readonly _inverseEntryMap = new Map(); + + /** + * Gets the pinned indexes. This is keyed by index. + */ + private readonly _pinnedIndexes = new Set(); + + /** + * Cached calculations below here. + */ + + /** + * Gets or sets the pinned layout entries size. + */ + private _pinnedLayoutEntriesSize: number | undefined; + + /** + * Gets or sets the unpinned layout entries size. + */ + private _unpinnedLayoutEntriesSize: number | undefined; + + //#endregion Private Properties + + //#region Constructor + + /** + * Constructor. + * @param defaultSize The default size. + */ + constructor(defaultSize: number = 0) { + this._defaultSize = defaultSize; + } + + //#endregion Constructor + + //#region Public Properties + + /** + * Gets the entry count. + */ + get entryCount() { + return this._entryCount; + } + + /** + * Gets the first index, if any; otherwise, -1. + */ + get firstIndex() { + // If there are no entries, return -1. + if (!this._entryCount) { + return -1; + } + + // If there are no pinned indexes, return the index at position 0. + if (!this._pinnedIndexes.size) { + return this.mapPositionToIndex(0) ?? -1; + } + + // Return the first pinned index, or -1 if for some reason none exists. + const firstIteratorResult = this._pinnedIndexes.values().next(); + return firstIteratorResult.done ? -1 : firstIteratorResult.value; + } + + /** + * Gets the last index, if any; otherwise, -1. + */ + get lastIndex() { + // If there are no entries, return -1. + if (!this._entryCount) { + return -1; + } + + // Find the last unpinned entry index. + for (let position = this._entryCount - 1; position >= 0; position--) { + // Map the position to an index. + const index = this.mapPositionToIndex(position); + if (index === undefined) { + return -1; + } + + // If the index is not pinned, return it. + if (!this.isPinnedIndex(index)) { + return index; + } + } + + // Find the last pinned index. + let lastPinnedIndex: number | undefined; + for (const pinned of this._pinnedIndexes) { + lastPinnedIndex = pinned; + } + + // Return the last pinned index. + return lastPinnedIndex ?? -1; + } + + /** + * Gets the pinned indexes count. + */ + get pinnedIndexesCount() { + return this._pinnedIndexes.size; + } + + /** + * Gets the pinned indexes. + */ + get pinnedIndexes() { + return Array.from(this._pinnedIndexes); + } + + /** + * Gets the pinned layout entries size. + */ + get pinnedLayoutEntriesSize() { + // If the pinned layout entries size is already calculated, return it. + if (this._pinnedLayoutEntriesSize !== undefined) { + return this._pinnedLayoutEntriesSize; + } + + // Calculate the pinned layout entries size. + let size = 0; + for (const index of this._pinnedIndexes) { + size += this.entrySize(index); + } + + // Cache the pinned layout entries size. + this._pinnedLayoutEntriesSize = size; + + // Return the pinned layout entries size. + return size; + } + + /** + * Gets the unpinned layout entries size. + */ + get unpinnedLayoutEntriesSize() { + // If the unpinned layout entries size is already calculated, return it. + if (this._unpinnedLayoutEntriesSize !== undefined) { + return this._unpinnedLayoutEntriesSize; + } + + // Calculate the default unpinned layout entries size. This accounts for all entries. + let size = this._entryCount * this._defaultSize; + + // Account for pinned indexes by subtracting the default size for each one. + for (const pinnedIndex of this._pinnedIndexes) { + if (pinnedIndex < this._entryCount) { + size -= this._defaultSize; + } + } + + // Account for custom entry sizes by subtracting the default size and adding the custom entry size for each one. + for (const [customEntrySizeIndex, customEntrySize] of this._customEntrySizes) { + if (!this.isPinnedIndex(customEntrySizeIndex)) { + size -= this._defaultSize; + size += customEntrySize; + } + } + + // Account for entry sizes by subtracting the default size and adding the entry size for each one. + for (const [entrySizeIndex, entrySize] of this._entrySizes) { + if (!this.isPinnedIndex(entrySizeIndex) && !this._customEntrySizes.has(entrySizeIndex)) { + size -= this._defaultSize; + size += entrySize; + } + } + + // Cache the unpinned layout entries size. + this._unpinnedLayoutEntriesSize = size; + + // Return the calculated unpinned layout entries size. + return size; + } + + //#endregion Public Properties + + //#region Public Methods + + /** + * Sets the entries. + * @param entryCount The entry count. + * @param entrySizes The entry sizes, if any. There must be exactly `entryCount` sizes. + * @param entryMap The entry map, if any. There must be exactly `entryCount` entries. + */ + setEntries(entryCount: number, entrySizes: number[] | undefined = undefined, entryMap: number[] | undefined = undefined) { + // Invalidate cached calculations. + this.invalidateCachedCalculations(); + + // Set the entry count. + this._entryCount = entryCount; + + // Reset advanced layout capabilities. + this._entrySizes.clear(); + this._entryMap = []; + this._inverseEntryMap.clear(); + + // Enable advanced layout capabilities, if we don't have too many entries. + if (this._entryCount <= MAX_ADVANCED_LAYOUT_ENTRY_COUNT) { + // Set the entry sizes, if they were provided and are valid (i.e., they have the correct length). + // This is unavoidably O(n) over entry sizes. + if (entrySizes?.length === this._entryCount) { + for (let i = 0; i < entrySizes.length; i++) { + this._entrySizes.set(i, entrySizes[i]); + } + } + + // Set the entry map and reverse entry map, if an entry map was provided and is valid (i.e., it has the correct length). + this._entryMap = entryMap?.length === this._entryCount ? entryMap : []; + if (this._entryMap.length !== 0) { + for (let position = 0; position < this._entryMap.length; position++) { + this._inverseEntryMap.set(this._entryMap[position], position); + } + } + } + + // Remove any pinned indexes are no longer valid. + for (const pinnedIndex of this._pinnedIndexes) { + const pinnedIndexPosition = this.mapIndexToPosition(pinnedIndex); + if (pinnedIndexPosition && (pinnedIndexPosition < 0 || pinnedIndexPosition >= this._entryCount)) { + this._pinnedIndexes.delete(pinnedIndex); + } + } + } + + /** + * Sets the pinned indices to the specified indices. + * Clears any previously pinned indices before pinning the new indices. + * + * @param pinnedIndexes The indices that should be pinned. + * The array is keyed by position and maps the position + * to index of data entry from the origina dataset + */ + setPinnedIndexes(pinnedIndexes: number[]) { + // Unpin all the current indexes. + this._pinnedIndexes.clear(); + + // Pin the new indexes. + for (const index of pinnedIndexes) { + this.pinIndex(index); + } + } + + /** + * Gets the layout indexes. + * @param layoutOffset The layout offset. + * @param layoutSize The layout size. + * @param overscanFactor The overscan factor (e.g. 2 or 3) + * @returns The layout indexes. + */ + getLayoutIndexes(layoutOffset: number, layoutSize: number, overscanFactor: number) { + // Validate the layout offset and layout size. + if (layoutOffset < 0 || layoutSize < 0) { + return []; + } + + // Create the layout indexes from the pinned indexes. These are always returned. + const layoutIndexes = this.pinnedIndexes; + + // Find the first unpinned layout entry that overlaps with the specified offset. + const firstUnpinnedLayoutEntry = this.findFirstUnpinnedLayoutEntry(layoutOffset); + if (firstUnpinnedLayoutEntry === undefined) { + return layoutIndexes; + } + + // Push the first unpinned layout entry. + layoutIndexes.push(firstUnpinnedLayoutEntry.index); + + // Get the first layout entry position. + const firstLayoutEntryPosition = this.mapIndexToPosition(firstUnpinnedLayoutEntry.index); + if (firstLayoutEntryPosition === undefined) { + return layoutIndexes; + } + + // Enumerate entries before the first layout entry. + const startOffset = layoutOffset - (layoutSize * overscanFactor); + let end = firstUnpinnedLayoutEntry.start; + for (let position = firstLayoutEntryPosition - 1; position >= 0 && end > startOffset; position--) { + // Get the index of the positon. + const index = this.mapPositionToIndex(position); + if (index === undefined) { + return []; + } + + // Skipped pinned indexes. + if (this.isPinnedIndex(index)) { + continue; + } + + // Push the layout index. + layoutIndexes.push(index); + + // Adjust the end. + end -= this.entrySize(index); + } + + // Enumerate entries after the first layout entry. + const endOffset = layoutOffset + layoutSize + (layoutSize * overscanFactor); + let start = firstUnpinnedLayoutEntry.end; + for (let position = firstLayoutEntryPosition + 1; position < this._entryCount && start < endOffset; position++) { + // Get the index of the positon. + const index = this.mapPositionToIndex(position); + if (index === undefined) { + return []; + } + + // Skipped pinned indexes. + if (this.isPinnedIndex(index)) { + continue; + } + + // Push the layout index. + layoutIndexes.push(index); + + // Adjust the end. + start += this.entrySize(index); + } + + // Return the sorted layout indexes. + return layoutIndexes; + } + + /** + * Maps a range of positions to their corresponding indexes. + * @param startingPosition The starting position, inclusive. + * @param endingPosition The ending position, inclusive. + * @returns An array of indexes corresponding to the specified positions, or undefined if the positions are invalid. + */ + mapPositionsToIndexes(startingPosition: number, endingPosition: number): number[] | undefined { + // Validate the starting position and ending position. + if (startingPosition < 0 || endingPosition < startingPosition || endingPosition >= this._entryCount) { + return undefined; + } + + // If there are no pinned indexes, positions map directly to indexes. This means we can simply + // enumerate the positions and return the indexes or the entry-mapped indexes. + if (this._pinnedIndexes.size === 0) { + // Build the indexes or the entry-mapped indexes. + const indexes: number[] = []; + if (this._entryMap.length === 0) { + // Build the indexes. + for (let index = startingPosition; index <= endingPosition; index++) { + indexes.push(index); + } + } else { + // Build the entry-mapped indexes. + for (let position = startingPosition; position <= endingPosition; position++) { + const entryMappedIndex = this._entryMap[position]; + if (entryMappedIndex === undefined) { + return undefined; + } else { + indexes.push(entryMappedIndex); + } + } + + // Return the indexes. + return indexes; + } + } + + // Add pinned indexes. + const indexes: number[] = []; + const pinnedIndexesArray = Array.from(this._pinnedIndexes); + while (startingPosition < pinnedIndexesArray.length && startingPosition <= endingPosition) { + indexes.push(pinnedIndexesArray[startingPosition++]); + } + + // Add unpinned indexes. + if (startingPosition <= endingPosition) { + /** + * Checks if a position is pinned. + * @param position The position to check. + * @returns true if the position is pinned; otherwise, false. + */ + const isPinnedPosition = (position: number) => { + if (this._entryMap.length === 0) { + return this.isPinnedIndex(position); + } else { + const entryMappedIndex = this._entryMap[position]; + return entryMappedIndex !== undefined && this.isPinnedIndex(entryMappedIndex); + } + }; + + // Compute the rank of the unpinned position within the unpinned indexes. + const rank = startingPosition - this._pinnedIndexes.size; + if (rank >= this._entryCount - this._pinnedIndexes.size) { + return undefined; + } + + // Binary search to the first candidate position. + const target = rank + 1; + let leftPosition = 0; + let rightPosition = this._entryCount - 1; + let candidatePosition = -1; + while (leftPosition <= rightPosition) { + // Calculate the middle position. + const middlePosition = (leftPosition + rightPosition) >>> 1; + + // Calculate the number of pinned positions at or before middle position. + const pinnedPositionsAtOrBeforeMiddlePosition = this.pinnedPositionsAtOrBefore(middlePosition); + if (pinnedPositionsAtOrBeforeMiddlePosition === undefined) { + return undefined; + } + + // Determine whether to search left or right. + if ((middlePosition + 1) - pinnedPositionsAtOrBeforeMiddlePosition >= target) { + candidatePosition = middlePosition; + rightPosition = middlePosition - 1; + } else { + leftPosition = middlePosition + 1; + } + } + + // Ensure that a candidate position was found. + if (candidatePosition === -1) { + return undefined; + } + + // The candidate position should be an unpinned position. If not, advance to the next unpinned position. + while (candidatePosition < this._entryCount && isPinnedPosition(candidatePosition)) { + candidatePosition++; + } + + // Add unpinned indexes. + while (startingPosition <= endingPosition) { + // If the candidate position is invalid, return undefined. + if (candidatePosition >= this._entryCount) { + return undefined; + } + + // Get the index of the candidate position. + const index = this._entryMap.length === 0 ? candidatePosition : this._entryMap[candidatePosition]; + if (index === undefined) { + return undefined; + } + + // Push the index. + indexes.push(index); + + // Advance to the next starting position and the next candidate position. + startingPosition++; + do { + candidatePosition++; + } while (candidatePosition < this._entryCount && isPinnedPosition(candidatePosition)); + } + } + + // Return the indexes. + return indexes; + } + + /** + * Maps a position to an index. + * @param position The position. + * @returns The index, or undefined if the position is invalid. + */ + mapPositionToIndex(position: number): number | undefined { + // If the position is invalid, return undefined. + if (position < 0 || position >= this._entryCount) { + return undefined; + } + + // If there are no pinned indexes, the position is the index. + if (this._pinnedIndexes.size === 0) { + // Return the index. + if (this._entryMap.length === 0) { + return position; + } + + // Return the entry-mapped index. + return this._entryMap[position]; + } + + // If the position is pinned, return its index. + if (position < this._pinnedIndexes.size) { + return Array.from(this._pinnedIndexes)[position]; + } + + // Compute the rank of the unpinned position within the unpinned indexes. + const rank = position - this._pinnedIndexes.size; + + // Compute the rank of the unpinned position within the unpinned indexes. + if (rank >= this._entryCount - this._pinnedIndexes.size) { + return undefined; + } + + // Binary search for the candidate position. + const target = rank + 1; + let leftPosition = 0; + let rightPosition = this._entryCount - 1; + let candidatePosition = -1; + while (leftPosition <= rightPosition) { + // Calculate the middle position. + const middlePosition = (leftPosition + rightPosition) >>> 1; + + // Calculate the number of pinned positions at or before middle position. + const pinnedPositionsAtOrBeforeMiddlePosition = this.pinnedPositionsAtOrBefore(middlePosition); + if (pinnedPositionsAtOrBeforeMiddlePosition === undefined) { + return undefined; + } + + // Determine whether to search left or right. + if ((middlePosition + 1) - pinnedPositionsAtOrBeforeMiddlePosition >= target) { + candidatePosition = middlePosition; + rightPosition = middlePosition - 1; + } else { + leftPosition = middlePosition + 1; + } + } + + // Return the index. + return candidatePosition === -1 ? undefined : this._entryMap.length !== 0 ? this._entryMap[candidatePosition] : candidatePosition; + } + + /** + * Maps an index to a position. + * @param index The index to map. + * @returns The position, or undefined if the index is invalid. + */ + mapIndexToPosition(index: number): number | undefined { + // If the index is invalid, return undefined. + if (index < 0) { + return undefined; + } + + // If there are no pinned indexes, the index is the position. + if (this._pinnedIndexes.size === 0) { + // If there is no entry map, the index is the position, so return it. + if (this._entryMap.length === 0) { + // If the index is invalid, return undefined. + if (index >= this._entryCount) { + return undefined; + } + + // Return the index. + return index; + } + + // Return the entry-mapped index. This will naturally return undefined, if the index is invalid. + return this._entryMap[index]; + } + + // If the index is pinned, return its position. + if (this._pinnedIndexes.has(index)) { + return Array.from(this._pinnedIndexes).indexOf(index); + } + + // Get the position of the index. + const position = this.positionOfIndex(index); + if (position === undefined) { + return undefined; + } + + // Calculate the number of pinned positions that occur before this position. + const pinnedPositionsBefore = position > 0 ? this.pinnedPositionsAtOrBefore(position - 1) : 0; + if (pinnedPositionsBefore === undefined) { + return undefined; + } + + // Return the adjusted position. + return this._pinnedIndexes.size + (position - pinnedPositionsBefore); + } + + /** + * Sets a size override. + * @param index The index to set the size override for. + * @param sizeOverride The size override to set. + */ + setSizeOverride(index: number, sizeOverride: number) { + // Validate the index and the size override. + if (!this.validateIndex(index) || sizeOverride <= 0) { + return; + } + + // If the size override is the same as the current size override, return. + if (this._customEntrySizes.get(index) === sizeOverride) { + return; + } + + // Set the size override. + this._customEntrySizes.set(index, sizeOverride); + + // Invalidate cached calculations. + this.invalidateCachedCalculations(); + } + + /** + * Clears a size override. + * @param index The index to clear the size override for. + */ + clearSizeOverride(index: number) { + // Validate the index. + if (!this.validateIndex(index)) { + return; + } + + // If there isn't a custom entry size for the index, return. + if (!this._customEntrySizes.has(index)) { + return; + } + + // Clear the size override. + this._customEntrySizes.delete(index); + + // Invalidate cached calculations. + this.invalidateCachedCalculations(); + } + + /** + * Checks if the given index is pinned. + * @param index The index to check. + * @returns true if the index is pinned, false otherwise. + */ + isPinnedIndex(index: number): boolean { + return this._pinnedIndexes.has(index); + } + + /** + * Pins an index. + * @param index The index to pin. + * @returns true if the index was pinned; otherwise, false. + */ + pinIndex(index: number) { + // Validate the index. + if (!this.validateIndex(index)) { + return false; + } + + // If the index is already pinned, return. + if (this.isPinnedIndex(index)) { + return false; + } + + // Pin the index. + this._pinnedIndexes.add(index); + + // Invalidate cached calculations. + this.invalidateCachedCalculations(); + + // Return true to indicate that the index was pinned. + return true; + } + + /** + * Unpins an index + * @param index The index to unpin. + * @returns true if the index was unpinned; otherwise, false. + */ + unpinIndex(index: number) { + // Validate the index. + if (!this.validateIndex(index)) { + return false; + } + + // If the index is not pinned, return. + if (!this.isPinnedIndex(index)) { + return false; + } + + // Unpin the index. + this._pinnedIndexes.delete(index); + + // Invalidate cached calculations. + this.invalidateCachedCalculations(); + + // Return true to indicate that the index was unpinned. + return true; + } + + /** + * Returns the pinned layout entries that fit within the specified layout size. + * @param layoutSize The layout size to fit the pinned entries within. + * @returns An array of the pinned layout entries, if any; otherwise, undefined. + */ + pinnedLayoutEntries(layoutSize: number) { + // Validate the layout size. + if (layoutSize <= 0) { + return []; + } + + // Enumerate the pinned indexes and build the pinned layout entries. + let start = 0; + const layoutEntries: ILayoutEntry[] = []; + for (const index of this._pinnedIndexes) { + // Create the layout entry. + const size = this.entrySize(index); + layoutEntries.push({ + index, + start, + size, + end: start + size + }); + + // Increment the start by the size of the layout entry. + start += size; + + // If the start exceeds the layout size, break. + if (start > layoutSize) { + break; + } + } + + // Return the pinned layout entries. + return layoutEntries; + } + + /** + * Returns the unpinned layout entries that overlap with the specified offset and size. + * @param layoutOffset The offset. + * @param layoutSize The size. + * @returns An array containing the unpinned layout entries, if any; otherwise, undefined. + */ + unpinnedLayoutEntries(layoutOffset: number, layoutSize: number): ILayoutEntry[] { + // Validate the layout offset and layout size. + if (layoutOffset < 0 || layoutSize < 0) { + return []; + } + + // Find the first unpinned layout entry that overlaps with the specified offset. + const firstLayoutEntry = this.findFirstUnpinnedLayoutEntry(layoutOffset); + if (!firstLayoutEntry) { + return []; + } + + // Create the layout entries array and add the first layout entry. + const layoutEntries: ILayoutEntry[] = [firstLayoutEntry]; + const layoutEnd = layoutOffset + layoutSize; + + // Enumerate indexes and build the layout entries until we exceed the layout size. + for (let index = this.nextIndex(firstLayoutEntry.index), start = firstLayoutEntry.end; index !== undefined && start < layoutEnd; index = this.nextIndex(index)) { + // Skip pinned indexes. + if (this.isPinnedIndex(index)) { + continue; + } + + // Create the layout entry. + const size = this.entrySize(index); + layoutEntries.push({ + index, + start, + size, + end: start + size + }); + + // Increment the start by the size of the layout entry. + start += size; + } + + // Return the layout entries. + return layoutEntries; + } + + /** + * Returns the previous index of the starting index. + * @param startingIndex The starting index to get the previous index for. + * @returns The previous index, if found; otherwise, undefined. + */ + previousIndex(startingIndex: number): number | undefined { + // If the index is pinned, return the previous pinned index, if there is one. + if (this.isPinnedIndex(startingIndex)) { + // Get the pinned indexes as an array. + const pinnedIndexesArray = Array.from(this._pinnedIndexes); + + // Get the pinned index position. + const pinnedIndexPosition = pinnedIndexesArray.indexOf(startingIndex); + + // If the pinned index position greater than zero, return the previous pinned index. + if (pinnedIndexPosition > 0) { + return pinnedIndexesArray[pinnedIndexPosition - 1]; + } + + // There is no previous pinned index. + return undefined; + } + + // When there isn't an entry map, return the previous unpinned index, if there is one; otherwise, + // return the previous unpinned entry map index, if there is one. + if (this._entryMap.length === 0) { + // Return the previous unpinned index. + for (let i = startingIndex - 1; i >= 0; i--) { + if (!this.isPinnedIndex(i)) { + return i; + } + } + } else { + // Get the position of the index in the entry map. + let position = this._inverseEntryMap.get(startingIndex); + if (position === undefined) { + return undefined; + } + + // Return the previous unpinned entry map index, if found. + while (--position >= 0) { + // Get the entry map index. + const entryMapIndex = this._entryMap[position]; + + // If the entry map index isn't pinned, return it. + if (!this.isPinnedIndex(entryMapIndex)) { + return entryMapIndex; + } + } + } + + // This may be a transition from unpinned indexes to pinned indexes. If so, get the last + // pinned index. If not, this will result in undefined. + let lastPinnedIndex: number | undefined; + for (const pinnedIndex of this._pinnedIndexes) { + lastPinnedIndex = pinnedIndex; + } + + // Return the last pinned index. + return lastPinnedIndex; + } + + /** + * Returns the next index after the starting index. + * @param startingIndex The starting index to get the next index for. + * @returns The next index, if found; otherwise, undefined. + */ + nextIndex(startingIndex: number): number | undefined { + // Validate the index. + if (!this.validateIndex(startingIndex)) { + return undefined; + } + + // If the index is pinned, return the next pinned index, if there is one; otherwise, return + // the first unpinned index, if there is one. + if (this.isPinnedIndex(startingIndex)) { + // Get the pinned indexes as an array. + const pinnedIndexesArray = Array.from(this._pinnedIndexes); + + // Get the pinned position. + const pinnedPosition = pinnedIndexesArray.indexOf(startingIndex); + if (pinnedPosition === -1) { + return undefined; + } + + // If the pinned position is not the last pinned position, return the next pinned index. + if (pinnedPosition < pinnedIndexesArray.length - 1) { + return pinnedIndexesArray[pinnedPosition + 1]; + } + + // The pinned position is the last pinned position, return the first unpinned index. + for (let position = 0; position < this._entryCount; position++) { + // Map the position to an index. + const index = this.mapPositionToIndex(position); + if (index === undefined) { + return undefined; + } + + // If the index is not pinned, return it. + if (!this.isPinnedIndex(index)) { + return index; + } + } + + // There are no unpinned indexes. + return undefined; + } + + // Return the next unpinned index. + if (this._entryMap.length === 0) { + for (let i = startingIndex + 1; i < this._entryCount; i++) { + if (!this.isPinnedIndex(i)) { + return i; + } + } + } else { + // Get the entry map position of the index. + let entryMapPosition = this._inverseEntryMap.get(startingIndex); + if (entryMapPosition === undefined) { + return undefined; + } + + // Return the next unpinned index, if found. + while (++entryMapPosition < this._entryMap.length) { + // Get the index at the position. + const indexAtPosition = this._entryMap[entryMapPosition]; + + // If the entry map index isn't pinned, return it. + if (!this.isPinnedIndex(indexAtPosition)) { + return indexAtPosition; + } + } + } + + // There is not a next index. + return undefined; + } + + /** + * Gets a layout entry by its index. + * @param layoutEntryIndex The index of the layout entry to get. + * @returns The layout entry, if found; otherwise, undefined. + */ + getLayoutEntry(layoutEntryIndex: number): ILayoutEntry | undefined { + // Validate the index. + if (!this.validateIndex(layoutEntryIndex)) { + return undefined; + } + + // If the index is pinned, return the pinned layout entry. + if (this.isPinnedIndex(layoutEntryIndex)) { + // Get the pinned indexes as an array. + const pinnedIndexesArray = Array.from(this._pinnedIndexes); + + // Get the pinned index position within the pinned indexes array. + const pinnedIndexPosition = pinnedIndexesArray.indexOf(layoutEntryIndex); + if (pinnedIndexPosition === -1) { + return undefined; + } + + // Compute the start of the pinned index. + let start = 0; + for (let position = 0; position < pinnedIndexPosition; position++) { + start += this.entrySize(pinnedIndexesArray[position]); + } + + // Return the pinned layout entry. + const size = this.entrySize(layoutEntryIndex); + return { + index: layoutEntryIndex, + start, + size, + end: start + size, + }; + } + + // Get the layout entry position. + const layoutEntryPosition = this.mapIndexToPosition(layoutEntryIndex); + if (layoutEntryPosition === undefined) { + return undefined; + } + + // Calculate the start as if there were no pinned indexes, no custom entry sizes, and no entry sizes. + let start = layoutEntryPosition * this._defaultSize; + + // Adjust the start to account for pinned indexes. This is unavoidably O(n) over pinned indexes. + for (const pinnedIndex of this._pinnedIndexes) { + // Get the pinned index position. + const pinnedIndexPosition = this.mapIndexToPosition(pinnedIndex); + if (pinnedIndexPosition === undefined) { + continue; + } + + // If the pinned index position is before the layout entry position, subtract the default size. + if (pinnedIndexPosition < layoutEntryPosition) { + start -= this._defaultSize; + } + } + + // Adjust the start to account for custom entry sizes. This is unavoidably O(n) over custom entry sizes. + for (const [customEntrySizeIndex, customEntrySize] of this._customEntrySizes) { + // If the custom entry size index is pinned, skip it. + if (this.isPinnedIndex(customEntrySizeIndex)) { + continue; + } + + // Get the custom entry size position. + const customEntrySizePosition = this.mapIndexToPosition(customEntrySizeIndex); + if (customEntrySizePosition === undefined) { + continue; + } + + // If the custom entry size position is before the layout entry position, adjust the start for it. + if (customEntrySizePosition < layoutEntryPosition) { + start -= this._defaultSize; + start += customEntrySize; + } + } + + // Adjust the start to account for entry sizes. This is unavoidably O(n) over entry sizes. + for (const [entrySizeIndex, entrySize] of this._entrySizes) { + // If the entry size index is pinned, or there's a custom entry size, skip it. + if (this.isPinnedIndex(entrySizeIndex) || this._customEntrySizes.has(entrySizeIndex)) { + continue; + } + + // Get the entry size position. + const entrySizePosition = this.mapIndexToPosition(entrySizeIndex); + if (entrySizePosition === undefined) { + continue; + } + + // If the entry size position is before the layout entry position, adjust the start for it. + if (entrySizePosition < layoutEntryPosition) { + start -= this._defaultSize; + start += entrySize; + } + } + + // Return the layout entry. + const size = this.entrySize(layoutEntryIndex); + return { + index: layoutEntryIndex, + start, + size, + end: start + size, + }; + } + + /** + * Finds the first unpinned layout entry that contains the given layout offset. + * @param layoutOffset The layout offset to find the first unpinned layout entry for. + * @returns The first unpinned layout entry that contains the layout offset, or undefined if none is found. + */ + findFirstUnpinnedLayoutEntry(layoutOffset: number): ILayoutEntry | undefined { + // Return undefined if there are no entries or the layout offset is invalid. + if (!this._entryCount || layoutOffset < 0) { + return undefined; + } + + // Shortcut for full-span layout: when the layout offset is 0, the default size is 0, + // and the entry count is 1, this represents a dynamically sized full-width column or + // full-height row. Return the only possible layout entry. + if (layoutOffset === 0 && this._defaultSize === 0 && this._entryCount === 1) { + return { + index: 0, + start: 0, + size: 0, + end: 0, + }; + } + + // Outside the binary search, get the pinned indexes, filtered custom entry sizes, and filtered entry sizes. + const pinnedIndexes = Array.from(this._pinnedIndexes); + const customEntrySizes = [...this._customEntrySizes.entries()].filter(([customEntrySizeIndex]) => !this.isPinnedIndex(customEntrySizeIndex)); + const entrySizes = [...this._entrySizes.entries()].filter(([entrySizeIndex]) => !this.isPinnedIndex(entrySizeIndex) && !this._customEntrySizes.has(entrySizeIndex)); + + // Binary search to find the first unpinned layout entry that contains the offset. + let leftPosition = 0; + let rightPosition = this._entryCount - 1; + while (leftPosition <= rightPosition) { + // Calculate the middle position. + const middlePosition = (leftPosition + rightPosition) >> 1; + + // Get the middle position. + const middleIndex = this.mapPositionToIndex(middlePosition); + if (middleIndex === undefined) { + return undefined; + } + + // Compute the start of the middle position as if there were no pinned indexes, no custom entry sizes, and no entry sizes. + let start = middlePosition * this._defaultSize; + + // Adjust the start to account for pinned indexes. This is unavoidably O(n) over pinned indexes. + for (let i = 0; i < pinnedIndexes.length; i++) { + const pinnedIndexPosition = this.mapIndexToPosition(pinnedIndexes[i]); + if (pinnedIndexPosition !== undefined && pinnedIndexPosition < middlePosition) { + start -= this._defaultSize; + } + } + + // Adjust the start to account for custom entry sizes. This is unavoidably O(n) over custom entry sizes. + for (const [customEntrySizeIndex, customEntrySize] of customEntrySizes) { + // Get the custom entry size position. + const customEntrySizePosition = this.mapIndexToPosition(customEntrySizeIndex); + if (customEntrySizePosition === undefined) { + continue; + } + + // If the custom entry size position is before the layout entry position, adjust the start for it. + if (customEntrySizePosition < middlePosition) { + start -= this._defaultSize; + start += customEntrySize; + } + } + + // Adjust the start to account for entry sizes. This is unavoidably O(n) over entry sizes. + for (const [entrySizeIndex, entrySize] of entrySizes) { + // Get the entry size position. + const entrySizePosition = this.mapIndexToPosition(entrySizeIndex); + if (entrySizePosition === undefined) { + continue; + } + + // If the entry size position is before the layout entry position, adjust the start for it. + if (entrySizePosition < middlePosition) { + start -= this._defaultSize; + start += entrySize; + } + } + + // If the layout offset is less than the start, search the left half. + if (layoutOffset < start) { + rightPosition = middlePosition - 1; + continue; + } + + // Now that we know the start, we can check if the layout offset is within the middle entry. + if (layoutOffset >= start && layoutOffset < start + this.entrySize(middleIndex)) { + // Set the first unpinned index. + let firstUnpinnedIndex = middleIndex; + + // If the first unpinned index is pinned, scan backwards and forwards to find the first unpinned index. + if (this.isPinnedIndex(firstUnpinnedIndex)) { + // Scan backward from the middle position for the first unpinned index. + let backwardScanPosition = middlePosition; + while (--backwardScanPosition >= 0) { + const index = this.mapPositionToIndex(backwardScanPosition); + if (index === undefined) { + return undefined; + } + + if (!this.isPinnedIndex(index)) { + firstUnpinnedIndex = index; + break; + } + } + + // If backward scan fails, scan forward from the middle position. + if (backwardScanPosition === -1) { + let forwardScanPosition = middlePosition; + while (++forwardScanPosition < this._entryCount) { + const index = this.mapPositionToIndex(forwardScanPosition); + if (index === undefined) { + return undefined; + } + + if (!this.isPinnedIndex(index)) { + firstUnpinnedIndex = index; + break; + } + } + + // If the first unpinned index was not found in either direction, return undefined. + if (forwardScanPosition === this._entryCount) { + return undefined; + } + } + } + + // Return the layout entry for the first unpinned index. + const size = this.entrySize(firstUnpinnedIndex); + return { + index: firstUnpinnedIndex, + start, + size, + end: start + size, + }; + } + + // Setup the next binary search. + leftPosition = middlePosition + 1; + } + + // The first layout entry was not found. + return undefined; + } + + //#endregion Public Methods + + //#region Private Methods + + /** + * Validates an index. + * @param index The index to validate. + * @returns true if the index is valid, false otherwise. + */ + private validateIndex(index: number) { + // If the number is not an integer or is negative, return false. + if (!Number.isInteger(index) || index < 0) { + return false; + } + + // Validate the index. + return this._entryMap.length !== 0 ? this._inverseEntryMap.has(index) : index < this._entryCount; + } + + /** + * Invalidates the cached layout entry sizes. + */ + private invalidateCachedCalculations() { + this._pinnedLayoutEntriesSize = undefined; + this._unpinnedLayoutEntriesSize = undefined; + } + + /** + * Gets the size of the entry at the specified index. + * @param index The index of the entry. + * @returns The size of the entry. + */ + private entrySize(index: number): number { + // If a custom size is set for the index, return it. + const customSize = this._customEntrySizes.get(index); + if (customSize !== undefined) { + return customSize; + } + + // If an entry size is set for the index, return it. + const entrySize = this._entrySizes.get(index); + if (entrySize !== undefined) { + return entrySize; + } + + // Return the default size. + return this._defaultSize; + } + + /** + * Returns the position of an index. + * @param index The index. + * @returns The index of the position. + */ + private positionOfIndex(index: number): number | undefined { + // If there is no entry map, the index is the position, so return it. + if (this._entryMap.length === 0) { + // Validate the index. + if (index >= this._entryCount) { + return undefined; + } + + // Return the index. + return index; + } + + // Return the inverse entry-mapped index. This will naturally return undefined, if the index is invalid. + return this._inverseEntryMap.get(index); + } + + /** + * Returns a count of the pinned positions at or before the given position. + * @param position The position. + * @returns The count of the pinned positions at or before the given position. + */ + private pinnedPositionsAtOrBefore(position: number): number | undefined { + // Count how many pinned positions are less than or equal to position. + let count = 0; + for (const pinnedIndex of this._pinnedIndexes) { + // Get the position of the pinned index. + const positionOfIndex = this.positionOfIndex(pinnedIndex); + if (positionOfIndex === undefined) { + return undefined; + } + + // If the position of the pinned index is less than or equal to the given position, increment the count. + if (positionOfIndex <= position) { + count++; + } + } + + // Return the count. + return count; + } + + //#endregion Private Methods +} diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.css index 4709afa9e85e..2b5658e434e1 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.css @@ -12,8 +12,24 @@ background-color: var(--vscode-positronDataGrid-contrastBackground); } -.data-grid-column-header -.border-overlay { +.data-grid-column-header:not(.pinned) { + z-index: 0; +} + +.data-grid-column-header.pinned { + z-index: 1; +} + +.data-grid-column-header .pinned-indicator { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background-color: var(--vscode-positronDataGrid-selectionBorder); +} + +.data-grid-column-header .border-overlay { top: 0; right: 0; bottom: 0; @@ -24,8 +40,7 @@ border-bottom: 1px solid var(--vscode-positronDataGrid-border); } -.data-grid-column-header -.selection-overlay { +.data-grid-column-header .selection-overlay { top: 0; right: 0; bottom: 0; @@ -37,28 +52,23 @@ border-bottom: 1px solid var(--vscode-positronDataGrid-selectionInnerBorder); } -.data-grid-column-header -.selection-overlay:not(.focused) { +.data-grid-column-header .selection-overlay:not(.focused) { opacity: 50%; } -.data-grid-column-header -.selection-overlay.selected-left { +.data-grid-column-header .selection-overlay.selected-left { border-left: 1px solid var(--vscode-positronDataGrid-selectionBorder); } -.data-grid-column-header -.selection-overlay.selected-right { +.data-grid-column-header .selection-overlay.selected-right { border-right: 1px solid var(--vscode-positronDataGrid-selectionBorder); } -.data-grid-column-header -.selection-overlay:not(.selected-right) { +.data-grid-column-header .selection-overlay:not(.selected-right) { border-right: 1px solid var(--vscode-positronDataGrid-selectionInnerBorder); } -.data-grid-column-header -.content { +.data-grid-column-header .content { display: grid; position: relative; align-items: center; @@ -66,17 +76,13 @@ grid-template-columns: [title-description] minmax(0, 1fr) [sort-indicator] min-content [button] 20px [button-end]; } -.data-grid-column-header -.content -.title-description { - margin-bottom: 1px; /* Adjust for the bottom border. */ +.data-grid-column-header .content .title-description { + /* Adjust for the bottom border. */ + margin-bottom: 1px; grid-column: title-description / sort-indicator; } -.data-grid-column-header -.content -.title-description -.title { +.data-grid-column-header .content .title-description .title { overflow: hidden; white-space: nowrap; line-height: normal; @@ -84,18 +90,11 @@ font-weight: var(--positron-data-grid-column-header-title-font-weight); } -.data-grid-column-header -.content -.title-description -.title -.whitespace { +.data-grid-column-header .content .title-description .title .whitespace { opacity: 50%; } -.data-grid-column-header -.content -.title-description -.description { +.data-grid-column-header .content .title-description .description { opacity: 80%; overflow: hidden; white-space: nowrap; @@ -104,38 +103,31 @@ font-size: var(--positron-data-grid-column-header-description-font-size); } -.data-grid-column-header -.content -.sort-indicator { +.data-grid-column-header .content .sort-indicator { display: flex; align-items: center; justify-content: center; } -.data-grid-column-header -.content -.sort-indicator -.sort-icon { +.data-grid-column-header .content .sort-indicator .sort-icon { margin: 0; } -.data-grid-column-header -.content -.sort-indicator -.sort-index { - margin: 0 3px; /* If this value is changed, columnHeaderWidthCalculator must be updated. */ +.data-grid-column-header .content .sort-indicator .sort-index { + /* If this value is changed, columnHeaderWidthCalculator must be updated. */ + margin: 0 3px; color: var(--vscode-positronDataGrid-sortIndexForeground); font-size: var(--positron-data-grid-column-header-sort-index-font-size); font-weight: var(--positron-data-grid-column-header-sort-index-font-weight); font-variant-numeric: var(--positron-data-grid-column-header-sort-index-font-variant-numeric); } -.data-grid-column-header -.content -.sort-button { +.data-grid-column-header .content .sort-button { z-index: 1; - width: 20px; /* If this value is changed, columnHeaderWidthCalculator must be updated. */ - height: 20px; /* If this value is changed, columnHeaderWidthCalculator must be updated. */ + /* If this value is changed, columnHeaderWidthCalculator must be updated. */ + width: 20px; + /* If this value is changed, columnHeaderWidthCalculator must be updated. */ + height: 20px; display: flex; cursor: pointer; border-radius: 4px; @@ -145,33 +137,24 @@ grid-column: button / button-end; } -.data-grid-column-header -.content -.sort-button:focus { +.data-grid-column-header .content .sort-button:focus { outline: none !important; } -.data-grid-column-header -.content -.sort-button:focus-visible { +.data-grid-column-header .content .sort-button:focus-visible { border-radius: 4px; outline: 1px solid var(--vscode-focusBorder) !important; } -.data-grid-column-header -.content -.sort-button:hover { +.data-grid-column-header .content .sort-button:hover { border: 1px solid var(--vscode-positronDataGrid-selectionBorder); } -.data-grid-column-header -.content -.sort-button:active { +.data-grid-column-header .content .sort-button:active { border: 1px solid var(--vscode-positronDataGrid-selectionBorder); background-color: var(--vscode-positronDataGrid-selectionBackground); } -.data-grid-column-header -.vertical-splitter { +.data-grid-column-header .vertical-splitter { grid-column: right-gutter / end; } diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.tsx index 3ce932d1311c..3b275d1f43c4 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeader.tsx @@ -10,13 +10,13 @@ import './dataGridColumnHeader.css'; import React, { MouseEvent, useRef } from 'react'; // Other dependencies. -import { positronClassNames } from '../../../../base/common/positronUtilities.js'; import { IDataColumn } from '../interfaces/dataColumn.js'; -import { Button, MouseTrigger } from '../../../../base/browser/ui/positronComponents/button/button.js'; import { selectionType } from '../utilities/mouseUtilities.js'; -import { VerticalSplitter } from '../../../../base/browser/ui/positronComponents/splitters/verticalSplitter.js'; import { ColumnSelectionState } from '../classes/dataGridInstance.js'; import { usePositronDataGridContext } from '../positronDataGridContext.js'; +import { positronClassNames } from '../../../../base/common/positronUtilities.js'; +import { Button, MouseTrigger } from '../../../../base/browser/ui/positronComponents/button/button.js'; +import { VerticalSplitter } from '../../../../base/browser/ui/positronComponents/splitters/verticalSplitter.js'; import { renderLeadingTrailingWhitespace } from '../../../services/positronDataExplorer/browser/components/tableDataCell.js'; /** @@ -31,6 +31,8 @@ interface DataGridColumnHeaderProps { column?: IDataColumn; columnIndex: number; left: number; + pinned: boolean; + width: number; } /** @@ -104,19 +106,21 @@ export const DataGridColumnHeader = (props: DataGridColumnHeaderProps) => { // Determine whether the column is selected. const selected = (columnSelectionState & ColumnSelectionState.Selected) !== 0; - const renderedColumn = renderLeadingTrailingWhitespace(props.column?.name); - // Render. return (
+ {props.pinned &&
} {context.instance.cellBorders && <>
@@ -140,7 +144,7 @@ export const DataGridColumnHeader = (props: DataGridColumnHeaderProps) => { }} >
-
{renderedColumn}
+
{renderLeadingTrailingWhitespace(props.column?.name)}
{props.column?.description &&
{props.column.description}
} @@ -170,13 +174,12 @@ export const DataGridColumnHeader = (props: DataGridColumnHeaderProps) => {
- {context.instance.columnResize && ({ minimumWidth: context.instance.minimumColumnWidth, maximumWidth: context.instance.maximumColumnWidth, - startingWidth: context.instance.getColumnWidth(props.columnIndex) + startingWidth: props.width })} onResize={async columnWidth => await context.instance.setColumnWidth(props.columnIndex, columnWidth) diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.css index b44584822fab..a8a4704b1e3f 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.css @@ -6,6 +6,6 @@ .data-grid-column-headers { overflow: hidden; position: relative; - grid-column: waffle / end; + grid-column: waffle / end-waffle; grid-row: headers / waffle; } diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.tsx index 2f583b92e6ad..773bba016c39 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridColumnHeaders.tsx @@ -11,6 +11,7 @@ import React, { JSX } from 'react'; // Other dependencies. import { DataGridColumnHeader } from './dataGridColumnHeader.js'; +import { ColumnDescriptors } from '../classes/dataGridInstance.js'; import { usePositronDataGridContext } from '../positronDataGridContext.js'; // Other dependencies. @@ -19,8 +20,9 @@ import { usePositronDataGridContext } from '../positronDataGridContext.js'; * DataGridColumnHeadersProps interface. */ interface DataGridColumnHeadersProps { - width: number; + columnDescriptors: ColumnDescriptors; height: number; + width: number; } /** @@ -32,19 +34,33 @@ export const DataGridColumnHeaders = (props: DataGridColumnHeadersProps) => { // Context hooks. const context = usePositronDataGridContext(); - // Create the data grid column headers. + // Create the pinned data grid column header elements. const dataGridColumnHeaders: JSX.Element[] = []; - for ( - let columnDescriptor = context.instance.firstColumn; - columnDescriptor && columnDescriptor.left < context.instance.layoutRight; - columnDescriptor = context.instance.getColumn(columnDescriptor.columnIndex + 1) - ) { + for (const pinnedColumnDescriptor of props.columnDescriptors.pinnedColumnDescriptors) { + // Push the pinned column header element to the array. + dataGridColumnHeaders.push( + + ); + } + + // Create the unpinned data grid column header elements. + for (const unpinnedColumnDescriptor of props.columnDescriptors.unpinnedColumnDescriptors) { + // Push the unpinned column header element to the array. dataGridColumnHeaders.push( ); } diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.css index f89379adff99..e2ff837bb8e5 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.css @@ -9,3 +9,11 @@ width: 100%; position: absolute; } + +.data-grid-row:not(.pinned) { + z-index: 0; +} + +.data-grid-row.pinned { + z-index: 1; +} diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.tsx index de2bd8b04f7c..f165118c0f0d 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRow.tsx @@ -11,15 +11,20 @@ import React, { JSX } from 'react'; // Other dependencies. import { DataGridRowCell } from './dataGridRowCell.js'; +import { ColumnDescriptors } from '../classes/dataGridInstance.js'; import { usePositronDataGridContext } from '../positronDataGridContext.js'; +import { positronClassNames } from '../../../../base/common/positronUtilities.js'; /** * DataGridRowProps interface. */ interface DataGridRowProps { - width: number; + columnDescriptors: ColumnDescriptors; + height: number; + pinned: boolean; rowIndex: number; top: number; + width: number; } /** @@ -31,18 +36,33 @@ export const DataGridRow = (props: DataGridRowProps) => { // Context hooks. const context = usePositronDataGridContext(); - // Create the data grid column headers. + // Render the pinned data grid row cells. const dataGridRowCells: JSX.Element[] = []; - for (let columnDescriptor = context.instance.firstColumn; - columnDescriptor && columnDescriptor.left < context.instance.layoutRight; - columnDescriptor = context.instance.getColumn(columnDescriptor.columnIndex + 1) - ) { + for (const pinnedColumnDescriptor of props.columnDescriptors.pinnedColumnDescriptors) { + dataGridRowCells.push( + + ); + } + + // Create the unpinned data grid column header elements. + for (const unpinnedColumnDescriptor of props.columnDescriptors.unpinnedColumnDescriptors) { dataGridRowCells.push( ); } @@ -50,10 +70,13 @@ export const DataGridRow = (props: DataGridRowProps) => { // Render. return (
{dataGridRowCells} diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.css index fad2409b392c..c67a8af8dd0e 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.css @@ -5,6 +5,15 @@ .data-grid-row-cell { position: absolute; + background-color: var(--vscode-positronDataGrid-background); +} + +.data-grid-row-cell:not(.pinned) { + z-index: 0; +} + +.data-grid-row-cell.pinned { + z-index: 1; } .data-grid-row-cell diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx index 1248040a9328..86f902a04c40 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowCell.tsx @@ -10,20 +10,23 @@ import './dataGridRowCell.css'; import React, { MouseEvent, useRef } from 'react'; // Other dependencies. -import { positronClassNames } from '../../../../base/common/positronUtilities.js'; import { selectionType } from '../utilities/mouseUtilities.js'; import { CellSelectionState } from '../classes/dataGridInstance.js'; +import { usePositronDataGridContext } from '../positronDataGridContext.js'; +import { positronClassNames } from '../../../../base/common/positronUtilities.js'; import { VerticalSplitter } from '../../../../base/browser/ui/positronComponents/splitters/verticalSplitter.js'; import { HorizontalSplitter } from '../../../../base/browser/ui/positronComponents/splitters/horizontalSplitter.js'; -import { usePositronDataGridContext } from '../positronDataGridContext.js'; /** * DataGridRowCellProps interface. */ interface DataGridRowCellProps { columnIndex: number; - rowIndex: number; + height: number; left: number; + pinned: boolean; + rowIndex: number; + width: number; } /** @@ -65,11 +68,14 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { await context.instance.mouseSelectCell( props.columnIndex, props.rowIndex, + props.pinned, selectionType(e) ); } else { // Scroll to the cell. - await context.instance.scrollToCell(props.columnIndex, props.rowIndex); + if (!props.pinned) { + await context.instance.scrollToCell(props.columnIndex, props.rowIndex); + } } } @@ -105,7 +111,6 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { props.columnIndex === context.instance.cursorColumnIndex && props.rowIndex === context.instance.cursorRowIndex; - /** * Cursor component. * @param dimmed A value which indicates whether the cursor component should be dimmed. @@ -132,11 +137,14 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { return (
@@ -176,7 +184,7 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { onBeginResize={() => ({ minimumWidth: context.instance.minimumColumnWidth, maximumWidth: context.instance.maximumColumnWidth, - startingWidth: context.instance.getColumnWidth(props.columnIndex) + startingWidth: props.width })} onResize={async columnWidth => await context.instance.setColumnWidth(props.columnIndex, columnWidth) @@ -188,7 +196,7 @@ export const DataGridRowCell = (props: DataGridRowCellProps) => { onBeginResize={() => ({ minimumHeight: context.instance.minimumRowHeight, maximumHeight: 90, - startingHeight: context.instance.getRowHeight(props.rowIndex)! + startingHeight: props.height })} onResize={async rowHeight => await context.instance.setRowHeight(props.rowIndex, rowHeight) diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.css index e99dd40d2950..079ba11038e9 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.css @@ -15,6 +15,23 @@ background-color: var(--vscode-positronDataGrid-contrastBackground); } +.data-grid-row-header:not(.pinned) { + z-index: 0; +} + +.data-grid-row-header.pinned { + z-index: 1; +} + +.data-grid-row-header .pinned-indicator { + position: absolute; + top: 0; + left: 0; + width: 2px; + height: 100%; + background-color: var(--vscode-positronDataGrid-selectionBorder); +} + .data-grid-row-header .border-overlay { top: 0; diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.tsx index dc0952024b53..4a100c8a590d 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeader.tsx @@ -21,6 +21,8 @@ import { usePositronDataGridContext } from '../positronDataGridContext.js'; * DataGridRowHeaderProps interface. */ interface DataGridRowHeaderProps { + height: number; + pinned: boolean; rowIndex: number; top: number; } @@ -88,13 +90,17 @@ export const DataGridRowHeader = (props: DataGridRowHeaderProps) => { return (
+ {props.pinned &&
} {context.instance.cellBorders && <>
@@ -128,7 +134,7 @@ export const DataGridRowHeader = (props: DataGridRowHeaderProps) => { onBeginResize={() => ({ minimumHeight: context.instance.minimumRowHeight, maximumHeight: 90, - startingHeight: context.instance.getRowHeight(props.rowIndex)! + startingHeight: props.height })} onResize={async rowHeight => await context.instance.setRowHeight(props.rowIndex, rowHeight) diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeaders.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeaders.tsx index 4e6733ddeb13..bc9959b9b45e 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeaders.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridRowHeaders.tsx @@ -12,12 +12,14 @@ import React, { JSX } from 'react'; // Other dependencies. import { DataGridRowHeader } from './dataGridRowHeader.js'; import { usePositronDataGridContext } from '../positronDataGridContext.js'; +import { RowDescriptors } from '../classes/dataGridInstance.js'; /** * DataGridRowHeadersProps interface. */ interface DataGridRowHeadersProps { height: number; + rowDescriptors: RowDescriptors; } /** @@ -27,22 +29,31 @@ interface DataGridRowHeadersProps { */ export const DataGridRowHeaders = (props: DataGridRowHeadersProps) => { // Context hooks. - // FALSE POSITIVE: The ESLint rule of hooks is incorrectly flagging this line as a violation of - // the rules of hooks. See: https://github.com/facebook/react/issues/31687 - // eslint-disable-next-line react-hooks/rules-of-hooks const context = usePositronDataGridContext(); - // Create the data grid rows headers. + // Create the pinned data grid row header elements. const dataGridRowHeaders: JSX.Element[] = []; - for (let rowDescriptor = context.instance.firstRow; - rowDescriptor && rowDescriptor.top < context.instance.layoutBottom; - rowDescriptor = context.instance.getRow(rowDescriptor.rowIndex + 1) - ) { + for (const pinnedRowDescriptor of props.rowDescriptors.pinnedRowDescriptors) { dataGridRowHeaders.push( + ); + } + + // Create the unpinned data grid row header elements. + for (const unpinnedRowDescriptor of props.rowDescriptors.unpinnedRowDescriptors) { + dataGridRowHeaders.push( + ); } diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.css b/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.css index fba8b1c85be2..3764fddfadf7 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.css +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.css @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -22,9 +22,14 @@ outline: none !important; } -.data-grid-rows { +.data-grid-rows-container { + overflow: hidden; position: relative; box-sizing: border-box; grid-row: waffle / end-waffle; grid-column: waffle / end-waffle; } + +.data-grid-rows { + position: relative; +} diff --git a/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.tsx b/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.tsx index b09d388e1310..cc91c5f0b99e 100644 --- a/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.tsx +++ b/src/vs/workbench/browser/positronDataGrid/components/dataGridWaffle.tsx @@ -166,7 +166,6 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { switch (e.code) { // Space key. case 'Space': { - // Make sure the cursor is showing. if (context.instance.showCursor()) { return; @@ -181,7 +180,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { context.instance.selectColumn(context.instance.cursorColumnIndex); } else if (e.shiftKey && !e.ctrlKey) { context.instance.selectRow(context.instance.cursorRowIndex); - } if (isMacintosh ? e.metaKey : e.ctrlKey && e.shiftKey) { + } else if (e.ctrlKey && e.shiftKey) { context.instance.selectAll(); } } @@ -219,14 +218,17 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { if (isMacintosh ? e.metaKey : e.ctrlKey) { context.instance.clearSelection(); await context.instance.setScrollOffsets(0, 0); - context.instance.setCursorPosition(0, 0); + context.instance.setCursorPosition( + context.instance.firstColumnIndex, + context.instance.firstRowIndex + ); return; } // Home clears the selection and positions the screen and cursor to the left. context.instance.clearSelection(); await context.instance.setHorizontalScrollOffset(0); - context.instance.setCursorColumn(0); + context.instance.setCursorColumn(context.instance.firstColumnIndex); break; } @@ -260,8 +262,8 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { context.instance.maximumVerticalScrollOffset ); context.instance.setCursorPosition( - context.instance.columns - 1, - context.instance.rows - 1 + context.instance.lastColummIndex, + context.instance.lastRowIndex ); return; } @@ -269,7 +271,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { // End clears the selection and positions the screen and cursor to the left. context.instance.clearSelection(); await context.instance.setHorizontalScrollOffset(context.instance.maximumHorizontalScrollOffset); - context.instance.setCursorColumn(context.instance.columns - 1); + context.instance.setCursorColumn(context.instance.lastColummIndex); break; } @@ -361,10 +363,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { } // Move the cursor up. - if (context.instance.cursorRowIndex > 0) { - context.instance.setCursorRow(context.instance.cursorRowIndex - 1); - context.instance.scrollToCursor(); - } + context.instance.moveCursorUp(); break; } @@ -396,10 +395,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { } // Move the cursor down. - if (context.instance.cursorRowIndex < context.instance.rows - 1) { - context.instance.setCursorRow(context.instance.cursorRowIndex + 1); - context.instance.scrollToCursor(); - } + context.instance.moveCursorDown(); break; } @@ -431,10 +427,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { } // Moves the cursor left. - if (context.instance.cursorColumnIndex > 0) { - context.instance.setCursorColumn(context.instance.cursorColumnIndex - 1); - context.instance.scrollToCursor(); - } + context.instance.moveCursorLeft(); break; } @@ -465,12 +458,8 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { context.instance.clearSelection(); } - // Move the cursor right. - context.instance.clearSelection(); - if (context.instance.cursorColumnIndex < context.instance.columns - 1) { - context.instance.setCursorColumn(context.instance.cursorColumnIndex + 1); - context.instance.scrollToCursor(); - } + // Moves the cursor right. + context.instance.moveCursorRight(); break; } } @@ -489,6 +478,21 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { let deltaX = e.deltaX; let deltaY = e.deltaY; + // Suppress jitter on the non-dominant wheel axis. + { + // This bias factor prevents minor input noise from falsely flipping axis dominance. + const bias = 1.1; + + // Zero out the non-dominant axis. + const absDeltaX = Math.abs(deltaX); + const absDeltaY = Math.abs(deltaY); + if (absDeltaX > absDeltaY * bias) { + deltaY = 0; + } else if (absDeltaY > absDeltaX * bias) { + deltaX = 0; + } + } + // When the user is holding the shift key, invert delta X and delta Y. if (e.shiftKey) { [deltaX, deltaY] = [deltaY, deltaX]; @@ -517,17 +521,42 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { ); }; - // Create the data grid rows. + // Get the column descriptors and row descriptors. + const columnDescriptors = context.instance.getColumnDescriptors( + context.instance.horizontalScrollOffset, + width + ); + const rowDescriptors = context.instance.getRowDescriptors( + context.instance.verticalScrollOffset, + height + ); + + // Create the pinned data grid row elements. const dataGridRows: JSX.Element[] = []; - for (let rowLayoutEntry = context.instance.firstRow; - rowLayoutEntry && rowLayoutEntry.top < context.instance.layoutBottom; - rowLayoutEntry = context.instance.getRow(rowLayoutEntry.rowIndex + 1) - ) { + for (const pinnedRowDescriptor of rowDescriptors.pinnedRowDescriptors) { dataGridRows.push( + ); + } + + // Create the unpinned data grid row elements. + for (const unpinnedRowDescriptor of rowDescriptors.unpinnedRowDescriptors) { + dataGridRows.push( + ); @@ -544,7 +573,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { onKeyDown={keyDownHandler} onWheel={wheelHandler} > - {context.instance.columnHeaders && context.instance.rowHeaders && + {context.instance.columnHeaders && context.instance.rowHeaders && context.instance.columns !== 0 && { await context.instance.setScrollOffsets(0, 0); @@ -553,6 +582,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { } {context.instance.columnHeaders && @@ -560,6 +590,7 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { {context.instance.rowHeaders && } {context.instance.horizontalScrollbar && @@ -612,17 +643,18 @@ export const DataGridWaffle = forwardRef((_: unknown, ref) => { }
-
+
{dataGridRows}
diff --git a/src/vs/workbench/browser/positronDataGrid/positronDataGridContext.tsx b/src/vs/workbench/browser/positronDataGrid/positronDataGridContext.tsx index 614a1f97c9aa..7ab517b816cb 100644 --- a/src/vs/workbench/browser/positronDataGrid/positronDataGridContext.tsx +++ b/src/vs/workbench/browser/positronDataGrid/positronDataGridContext.tsx @@ -32,9 +32,7 @@ const PositronDataGridContext = createContext(undefined!) * The useDataGridState custom hook. * @returns The hook. */ -const usePositronDataGridState = ( - configuration: PositronDataGridConfiguration -): PositronDataGridState => { +const usePositronDataGridState = (configuration: PositronDataGridConfiguration): PositronDataGridState => { // Add event handlers. useEffect(() => { // Create a disposable store for the event handlers we'll add. diff --git a/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts b/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts index 0ea7f872252f..d88c70d29ba5 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts +++ b/src/vs/workbench/services/positronDataExplorer/browser/positronDataExplorerInstance.ts @@ -14,11 +14,11 @@ import { TableSummaryDataGridInstance } from './tableSummaryDataGridInstance.js' import { Severity } from '../../../../platform/notification/common/notification.js'; import { PositronDataExplorerLayout } from './interfaces/positronDataExplorerService.js'; import { PositronReactServices } from '../../../../base/browser/positronReactServices.js'; +import { CodeSyntaxName } from '../../languageRuntime/common/positronDataExplorerComm.js'; import { IPositronDataExplorerInstance } from './interfaces/positronDataExplorerInstance.js'; import { DataExplorerClientInstance } from '../../languageRuntime/common/languageRuntimeDataExplorerClient.js'; import { DataExplorerSummaryCollapseEnabled, DefaultDataExplorerSummaryLayout } from './positronDataExplorerSummary.js'; -import { CodeSyntaxName } from '../../languageRuntime/common/positronDataExplorerComm.js'; -import { ClipboardCell, ClipboardCellRange, ClipboardColumnIndexes, ClipboardColumnRange, ClipboardRowIndexes, ClipboardRowRange } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; +import { ClipboardCell, ClipboardCellIndexes, ClipboardColumnIndexes, ClipboardRowIndexes } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; /** * Constants. @@ -327,18 +327,10 @@ export class PositronDataExplorerInstance extends Disposable implements IPositro let selectedClipboardCells; if (clipboardData instanceof ClipboardCell) { selectedClipboardCells = 1; - } else if (clipboardData instanceof ClipboardCellRange) { - const columns = Math.max(clipboardData.lastColumnIndex - clipboardData.firstColumnIndex, 1); - const rows = Math.max(clipboardData.lastRowIndex - clipboardData.firstRowIndex, 1); - selectedClipboardCells = columns * rows; - } else if (clipboardData instanceof ClipboardColumnRange) { - const columns = clipboardData.lastColumnIndex - clipboardData.firstColumnIndex; - selectedClipboardCells = columns * this._tableDataCache.rows; + } else if (clipboardData instanceof ClipboardCellIndexes) { + selectedClipboardCells = clipboardData.columnIndexes.length * clipboardData.rowIndexes.length; } else if (clipboardData instanceof ClipboardColumnIndexes) { selectedClipboardCells = clipboardData.indexes.length * this._tableDataCache.rows; - } else if (clipboardData instanceof ClipboardRowRange) { - const rows = clipboardData.lastRowIndex - clipboardData.firstRowIndex; - selectedClipboardCells = rows * this._tableDataCache.columns; } else if (clipboardData instanceof ClipboardRowIndexes) { selectedClipboardCells = clipboardData.indexes.length * this._tableDataCache.columns; } else { diff --git a/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx b/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx index fa3ddef4b6e0..53a32cb75ba9 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/tableDataDataGridInstance.tsx @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -22,10 +22,15 @@ import { PositronDataExplorerCommandId } from '../../../contrib/positronDataExpl import { InvalidateCacheFlags, TableDataCache, WidthCalculators } from '../common/tableDataCache.js'; import { CustomContextMenuEntry, showCustomContextMenu } from '../../../browser/positronComponents/customContextMenu/customContextMenu.js'; import { dataExplorerExperimentalFeatureEnabled } from '../common/positronDataExplorerExperimentalConfig.js'; -import { BackendState, ColumnSchema, DataSelectionCellRange, DataSelectionIndices, DataSelectionRange, DataSelectionSingleCell, ExportFormat, RowFilter, SupportStatus, TableSelection, TableSelectionKind } from '../../languageRuntime/common/positronDataExplorerComm.js'; -import { ClipboardCell, ClipboardCellRange, ClipboardColumnIndexes, ClipboardColumnRange, ClipboardData, ClipboardRowIndexes, ClipboardRowRange, ColumnSelectionState, ColumnSortKeyDescriptor, DataGridInstance, RowSelectionState } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; +import { BackendState, ColumnSchema, DataSelectionCellIndices, DataSelectionIndices, DataSelectionSingleCell, ExportFormat, RowFilter, SupportStatus, TableSelection, TableSelectionKind } from '../../languageRuntime/common/positronDataExplorerComm.js'; +import { ClipboardCell, ClipboardCellIndexes, ClipboardColumnIndexes, ClipboardData, ClipboardRowIndexes, ColumnSelectionState, ColumnSortKeyDescriptor, DataGridInstance, RowSelectionState } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; import { PositronReactServices } from '../../../../base/browser/positronReactServices.js'; +/** + * Constants. + */ +const OVERSCAN_FACTOR = 3; + /** * Localized strings. */ @@ -78,6 +83,10 @@ export class TableDataDataGridInstance extends DataGridInstance { minimumColumnWidth: 80, maximumColumnWidth: 800, rowResize: false, + columnPinning: true, + maximumPinnedColumns: 10, + rowPinning: true, + maximumPinnedRows: 10, horizontalScrollbar: true, verticalScrollbar: true, scrollbarThickness: 14, @@ -103,19 +112,15 @@ export class TableDataDataGridInstance extends DataGridInstance { state = await this._dataExplorerClientInstance.getBackendState(); } - // Calculate the layout entries. - const layoutEntries = await this._tableDataCache.calculateColumnLayoutEntries( + // Calculate column widths. + const columnWidths = await this._tableDataCache.calculateColumnWidths( this.minimumColumnWidth, this.maximumColumnWidth ); // Set the layout entries. - this._columnLayoutManager.setLayoutEntries( - layoutEntries ?? state.table_shape.num_columns - ); - this._rowLayoutManager.setLayoutEntries( - state.table_shape.num_rows - ); + this._columnLayoutManager.setEntries(state.table_shape.num_columns, columnWidths); + this._rowLayoutManager.setEntries(state.table_shape.num_rows); // For zero-row case (e.g., after filtering), ensure a full reset of scroll positions if (state.table_shape.num_rows === 0) { @@ -123,7 +128,7 @@ export class TableDataDataGridInstance extends DataGridInstance { this._horizontalScrollOffset = 0; // Force a layout recomputation and repaint this.softReset(); - this._onDidUpdateEmitter.fire(); + this.fireOnDidUpdateEvent(); } else { // Adjust the vertical scroll offset, if needed. if (!this.firstRow) { @@ -185,7 +190,7 @@ export class TableDataDataGridInstance extends DataGridInstance { // Add the table data cache onDidUpdate event handler. this._register(this._tableDataCache.onDidUpdate(() => // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire() + this.fireOnDidUpdateEvent() )); } @@ -240,10 +245,8 @@ export class TableDataDataGridInstance extends DataGridInstance { // Update the cache. await this._tableDataCache.update({ invalidateCache: InvalidateCacheFlags.Data, - firstColumnIndex: columnDescriptor.columnIndex, - screenColumns: this.screenColumns, - firstRowIndex: rowDescriptor.rowIndex, - screenRows: this.screenRows + columnIndices: this._columnLayoutManager.getLayoutIndexes(this.horizontalScrollOffset, this.layoutWidth, OVERSCAN_FACTOR), + rowIndices: this._rowLayoutManager.getLayoutIndexes(this.verticalScrollOffset, this.layoutHeight, OVERSCAN_FACTOR) }); } } @@ -257,12 +260,11 @@ export class TableDataDataGridInstance extends DataGridInstance { const columnDescriptor = this.firstColumn; const rowDescriptor = this.firstRow; if (columnDescriptor && rowDescriptor) { + // Update the cache. await this._tableDataCache.update({ invalidateCache: invalidateCacheFlags ?? InvalidateCacheFlags.None, - firstColumnIndex: columnDescriptor.columnIndex, - screenColumns: this.screenColumns, - firstRowIndex: rowDescriptor.rowIndex, - screenRows: this.screenRows + columnIndices: this._columnLayoutManager.getLayoutIndexes(this.horizontalScrollOffset, this.layoutWidth, OVERSCAN_FACTOR), + rowIndices: this._rowLayoutManager.getLayoutIndexes(this.verticalScrollOffset, this.layoutHeight, OVERSCAN_FACTOR) }); } } @@ -347,16 +349,15 @@ export class TableDataDataGridInstance extends DataGridInstance { anchorElement: HTMLElement, anchorPoint?: AnchorPoint ): Promise { - /** - * Get the column sort key for the column. - */ - const columnSortKey = this.columnSortKey(columnIndex); - + // Get the supported features. const features = this._dataExplorerClientInstance.getSupportedFeatures(); const copySupported = this.isFeatureEnabled(features.export_data_selection?.support_status); const sortSupported = this.isFeatureEnabled(features.set_sort_columns?.support_status); const filterSupported = this.isFeatureEnabled(features.set_row_filters?.support_status); + // Get the column sort key for the column. + const columnSortKey = sortSupported ? this.columnSortKey(columnIndex) : undefined; + // Build the entries. const entries: CustomContextMenuEntry[] = []; entries.push(new CustomContextMenuItem({ @@ -375,6 +376,25 @@ export class TableDataDataGridInstance extends DataGridInstance { disabled: this.columnSelectionState(columnIndex) !== ColumnSelectionState.None, onSelected: () => this.selectColumn(columnIndex) })); + if (this.columnPinning) { + entries.push(new CustomContextMenuSeparator()); + if (!this.isColumnPinned(columnIndex)) { + entries.push(new CustomContextMenuItem({ + checked: false, + disabled: false, + icon: 'positron-pin', + label: localize('positron.dataExplorer.pinColumn', "Pin Column"), + onSelected: () => this.pinColumn(columnIndex) + })); + } else { + entries.push(new CustomContextMenuItem({ + checked: false, + icon: 'positron-unpin', + label: localize('positron.dataExplorer.unpinColumn', "Unpin Column"), + onSelected: () => this.unpinColumn(columnIndex) + })); + } + } entries.push(new CustomContextMenuSeparator()); entries.push(new CustomContextMenuItem({ checked: columnSortKey !== undefined && columnSortKey.ascending, @@ -463,6 +483,25 @@ export class TableDataDataGridInstance extends DataGridInstance { disabled: this.rowSelectionState(rowIndex) !== RowSelectionState.None, onSelected: () => this.selectRow(rowIndex) })); + if (this.rowPinning) { + entries.push(new CustomContextMenuSeparator()); + if (!this.isRowPinned(rowIndex)) { + entries.push(new CustomContextMenuItem({ + checked: false, + disabled: false, + icon: 'positron-pin', + label: localize('positron.dataExplorer.pinRow', "Pin Row"), + onSelected: () => this.pinRow(rowIndex) + })); + } else { + entries.push(new CustomContextMenuItem({ + checked: false, + icon: 'positron-unpin', + label: localize('positron.dataExplorer.unpinRow', "Unpin Row"), + onSelected: () => this.unpinRow(rowIndex) + })); + } + } // Show the context menu. await showCustomContextMenu({ @@ -523,6 +562,45 @@ export class TableDataDataGridInstance extends DataGridInstance { disabled: this.rowSelectionState(rowIndex) !== RowSelectionState.None, onSelected: () => this.selectRow(rowIndex) })); + if (this.columnPinning) { + entries.push(new CustomContextMenuSeparator()); + if (!this.isColumnPinned(columnIndex)) { + entries.push(new CustomContextMenuItem({ + checked: false, + disabled: false, + icon: 'positron-pin', + label: localize('positron.dataExplorer.pinColumn', "Pin Column"), + onSelected: () => this.pinColumn(columnIndex) + })); + } else { + entries.push(new CustomContextMenuItem({ + checked: false, + icon: 'positron-unpin', + label: localize('positron.dataExplorer.unpinColumn', "Unpin Column"), + onSelected: () => this.unpinColumn(columnIndex) + })); + } + } + if (this.rowPinning) { + if (!this.columnPinning) { + entries.push(new CustomContextMenuSeparator()); + } + if (!this.isRowPinned(rowIndex)) { + entries.push(new CustomContextMenuItem({ + checked: false, + icon: 'positron-pin', + label: localize('positron.dataExplorer.pinRow', "Pin Row"), + onSelected: () => this.pinRow(rowIndex) + })); + } else { + entries.push(new CustomContextMenuItem({ + checked: false, + icon: 'positron-unpin', + label: localize('positron.dataExplorer.unpinRow', "Unpin Row"), + onSelected: () => this.unpinRow(rowIndex) + })); + } + } entries.push(new CustomContextMenuSeparator()); entries.push(new CustomContextMenuItem({ checked: columnSortKey !== undefined && columnSortKey.ascending, @@ -614,24 +692,13 @@ export class TableDataDataGridInstance extends DataGridInstance { kind: TableSelectionKind.SingleCell, selection }; - } else if (clipboardData instanceof ClipboardCellRange) { - const selection: DataSelectionCellRange = { - first_column_index: clipboardData.firstColumnIndex, - first_row_index: clipboardData.firstRowIndex, - last_column_index: clipboardData.lastColumnIndex, - last_row_index: clipboardData.lastRowIndex, + } else if (clipboardData instanceof ClipboardCellIndexes) { + const selection: DataSelectionCellIndices = { + column_indices: clipboardData.columnIndexes, + row_indices: clipboardData.rowIndexes }; dataSelection = { - kind: TableSelectionKind.CellRange, - selection - }; - } else if (clipboardData instanceof ClipboardColumnRange) { - const selection: DataSelectionRange = { - first_index: clipboardData.firstColumnIndex, - last_index: clipboardData.lastColumnIndex - }; - dataSelection = { - kind: TableSelectionKind.ColumnRange, + kind: TableSelectionKind.CellIndices, selection }; } else if (clipboardData instanceof ClipboardColumnIndexes) { @@ -642,15 +709,6 @@ export class TableDataDataGridInstance extends DataGridInstance { kind: TableSelectionKind.ColumnIndices, selection }; - } else if (clipboardData instanceof ClipboardRowRange) { - const selection: DataSelectionRange = { - first_index: clipboardData.firstRowIndex, - last_index: clipboardData.lastRowIndex - }; - dataSelection = { - kind: TableSelectionKind.RowRange, - selection - }; } else if (clipboardData instanceof ClipboardRowIndexes) { const selection: DataSelectionIndices = { indices: clipboardData.indexes @@ -694,10 +752,8 @@ export class TableDataDataGridInstance extends DataGridInstance { // Update the cache. await this._tableDataCache.update({ invalidateCache: InvalidateCacheFlags.Data, - firstColumnIndex: columnDescriptor.columnIndex, - screenColumns: this.screenColumns, - firstRowIndex: rowDescriptor.rowIndex, - screenRows: this.screenRows + columnIndices: this._columnLayoutManager.getLayoutIndexes(this.horizontalScrollOffset, this.layoutWidth, OVERSCAN_FACTOR), + rowIndices: this._rowLayoutManager.getLayoutIndexes(this.verticalScrollOffset, this.layoutHeight, OVERSCAN_FACTOR) }); } } diff --git a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx index b63636f14b4c..1c49fd9624b2 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx @@ -8,20 +8,20 @@ import React, { JSX } from 'react'; // Other dependencies. import { Emitter } from '../../../../base/common/event.js'; -import { DataGridInstance } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; import { TableSummaryCache } from '../common/tableSummaryCache.js'; import { ColumnSummaryCell } from './components/columnSummaryCell.js'; -import { BackendState, ColumnDisplayType, SearchSchemaSortOrder } from '../../languageRuntime/common/positronDataExplorerComm.js'; -import { DataExplorerClientInstance } from '../../languageRuntime/common/languageRuntimeDataExplorerClient.js'; import { COLUMN_PROFILE_DATE_LINE_COUNT } from './components/columnProfileDate.js'; import { COLUMN_PROFILE_NUMBER_LINE_COUNT } from './components/columnProfileNumber.js'; import { COLUMN_PROFILE_OBJECT_LINE_COUNT } from './components/columnProfileObject.js'; import { COLUMN_PROFILE_STRING_LINE_COUNT } from './components/columnProfileString.js'; import { COLUMN_PROFILE_BOOLEAN_LINE_COUNT } from './components/columnProfileBoolean.js'; -import { COLUMN_PROFILE_DATE_TIME_LINE_COUNT } from './components/columnProfileDatetime.js'; -import { PositronActionBarHoverManager } from '../../../../platform/positronActionBar/browser/positronActionBarHoverManager.js'; import { PositronReactServices } from '../../../../base/browser/positronReactServices.js'; +import { COLUMN_PROFILE_DATE_TIME_LINE_COUNT } from './components/columnProfileDatetime.js'; +import { DataGridInstance } from '../../../browser/positronDataGrid/classes/dataGridInstance.js'; +import { DataExplorerClientInstance } from '../../languageRuntime/common/languageRuntimeDataExplorerClient.js'; import { summaryPanelEnhancementsFeatureEnabled } from '../common/positronDataExplorerSummaryEnhancementsFeatureFlag.js'; +import { PositronActionBarHoverManager } from '../../../../platform/positronActionBar/browser/positronActionBarHoverManager.js'; +import { BackendState, ColumnDisplayType, SearchSchemaSortOrder } from '../../languageRuntime/common/positronDataExplorerComm.js'; /** * Constants. @@ -81,6 +81,8 @@ export class TableSummaryDataGridInstance extends DataGridInstance { defaultRowHeight: SUMMARY_HEIGHT, columnResize: false, rowResize: false, + columnPinning: false, + rowPinning: false, horizontalScrollbar: false, verticalScrollbar: true, scrollbarThickness: 14, @@ -93,7 +95,7 @@ export class TableSummaryDataGridInstance extends DataGridInstance { }); // Set the column layout entries. There is always one column. - this._columnLayoutManager.setLayoutEntries(1); + this._columnLayoutManager.setEntries(1); /** * Updates the layout entries. @@ -106,7 +108,7 @@ export class TableSummaryDataGridInstance extends DataGridInstance { } // Set the layout entries. - this._rowLayoutManager.setLayoutEntries(state.table_shape.num_columns); + this._rowLayoutManager.setEntries(state.table_shape.num_columns); // Adjust the vertical scroll offset, if needed. if (!this.firstRow) { @@ -152,7 +154,7 @@ export class TableSummaryDataGridInstance extends DataGridInstance { // Add the table summary cache onDidUpdate event handler. this._register(this._tableSummaryCache.onDidUpdate(() => // Fire the onDidUpdate event. - this._onDidUpdateEmitter.fire() + this.fireOnDidUpdateEvent() )); // Create the hover manager. @@ -162,7 +164,7 @@ export class TableSummaryDataGridInstance extends DataGridInstance { this._services.hoverService )); - // Show tooltip hovers right away + // Show tooltip hovers right away. this._hoverManager.setCustomHoverDelay(0); } @@ -211,7 +213,8 @@ export class TableSummaryDataGridInstance extends DataGridInstance { override get firstColumn() { return { columnIndex: 0, - left: 0 + left: 0, + width: 0, }; } @@ -245,11 +248,12 @@ export class TableSummaryDataGridInstance extends DataGridInstance { } /** - * Gets the width of a column. + * Gets the custom width of a column. * @param columnIndex The column index. + * @returns The custom width of the column; otherwise, undefined. */ - override getColumnWidth(columnIndex: number): number { - return this.layoutWidth; + override getCustomColumnWidth(columnIndex: number): number | undefined { + return columnIndex === 0 ? this.layoutWidth : undefined; } /** @@ -335,9 +339,9 @@ export class TableSummaryDataGridInstance extends DataGridInstance { */ async toggleExpandColumn(columnIndex: number) { if (this._tableSummaryCache.isColumnExpanded(columnIndex)) { - this._rowLayoutManager.clearLayoutOverride(columnIndex); + this._rowLayoutManager.clearSizeOverride(columnIndex); } else { - this._rowLayoutManager.setLayoutOverride(columnIndex, this.expandedRowHeight(columnIndex)); + this._rowLayoutManager.setSizeOverride(columnIndex, this.expandedRowHeight(columnIndex)); } return this._tableSummaryCache.toggleExpandColumn(columnIndex); } diff --git a/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts b/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts index 71c29ff35c60..e0b7ddf5d625 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts @@ -168,17 +168,18 @@ export class ColumnSchemaCache extends Disposable { this._searchText = searchText; // // Get the size of the data. - // const tableState = await this._dataExplorerClientInstance.getBackendState(); - // this._columns = tableState.table_shape.num_columns; + const tableState = await this._dataExplorerClientInstance.getBackendState(); + this._columns = tableState.table_shape.num_columns; // Set the start column index and the end column index of the columns to cache. const startColumnIndex = Math.max( firstColumnIndex - (visibleColumns * OVERSCAN_FACTOR), 0 ); - const endColumnIndex = startColumnIndex + - visibleColumns + - (visibleColumns * OVERSCAN_FACTOR * 2); + const endColumnIndex = Math.min( + startColumnIndex + visibleColumns + (visibleColumns * OVERSCAN_FACTOR), + this._columns - 1 + ); // Build an array of the column indices to cache. const columnIndices = arrayFromIndexRange(startColumnIndex, endColumnIndex); @@ -191,6 +192,41 @@ export class ColumnSchemaCache extends Disposable { // Initialize the cache updated flag. let cacheUpdated = false; + if (!searchText) { + // Load the column schema for the specified column indices. + const tableSchemaResult = await this._dataExplorerClientInstance.getSchema(columnSchemaIndices); + + // Set the columns. + this._columns = tableSchemaResult.columns.length; + + // Update the column schema cache, overwriting any entries we already have cached. + for (let i = 0; i < tableSchemaResult.columns.length; i++) { + this._columnSchemaCache.set(columnSchemaIndices[0] + i, tableSchemaResult.columns[i]); + } + } else { + // If there are column schema indices that need to be cached, cache them. + if (columnSchemaIndices.length) { + // Get the schema. + const tableSchemaSearchResult = await this._dataExplorerClientInstance.searchSchema({ + searchText, + startIndex: columnSchemaIndices[0], + numColumns: columnSchemaIndices[columnSchemaIndices.length - 1] - + columnSchemaIndices[0] + 1 + }); + + // Set the columns. + this._columns = tableSchemaSearchResult.matching_columns; + + // Update the column schema cache, overwriting any entries we already have cached. + for (let i = 0; i < tableSchemaSearchResult.columns.length; i++) { + this._columnSchemaCache.set(columnSchemaIndices[0] + i, tableSchemaSearchResult.columns[i]); + } + + // Update the cache updated flag. + cacheUpdated = true; + } + } + // If there are column schema indices that need to be cached, cache them. if (columnSchemaIndices.length) { // Get the schema. diff --git a/src/vs/workbench/services/positronDataExplorer/common/layoutManager.ts b/src/vs/workbench/services/positronDataExplorer/common/layoutManager.ts deleted file mode 100644 index 0db4348c84db..000000000000 --- a/src/vs/workbench/services/positronDataExplorer/common/layoutManager.ts +++ /dev/null @@ -1,477 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. - * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * ILayoutEntry interface. - */ -export interface ILayoutEntry { - /** - * Gets index of the column or row. - */ - readonly index: number; - - /** - * Gets the X or Y coordinate of the column or row. - */ - readonly start: number; - - /** - * Gets the width or the height of the column or row. - */ - readonly size: number; - - /** - * Gets the end of the column or row. - */ - readonly end: number; -} - -/** - * LayoutEntry class. - */ -class LayoutEntry implements ILayoutEntry { - //#region Public Properties - - /** - * Gets index of the column or row. - */ - readonly index: number; - - /** - * Gets or sets the X or Y coordinate of the column or row. - */ - start: number; - - /** - * Gets or sets the default width or the height of the column or row. - */ - defaultSize: number; - - /** - * Gets or sets the override width or the height of the column or row. - */ - overrideSize?: number; - - /** - * Gets the size of the column or row. - */ - get size() { - return this.overrideSize ?? this.defaultSize; - } - - /** - * Gets the end of the column or row. - */ - get end() { - return this.start + this.size; - } - - //#endregion Public Properties - - //#region Constructor - - /** - * Constructor. - * @param index The index of the column or row. - * @param start The X or Y coordinate of the column or row. - * @param defaultSize The default width or the height of the column or row. - * @param overrideSize The override width or the height of the column or row. - */ - constructor(index: number, start: number, defaultSize: number, overrideSize?: number) { - this.index = index; - this.start = start; - this.defaultSize = defaultSize; - this.overrideSize = overrideSize; - } - - //#endregion Constructor -} - -/** - * ILayoutOverride interface. - */ -interface ILayoutOverride { - /** - * Gets index of the column or row. - */ - readonly index: number; - - /** - * Gets the override size. - */ - readonly overrideSize: number; -} - -/** - * LayoutManager class. - */ -export class LayoutManager { - //#region Private Properties - - /** - * Gets the default size. - */ - private readonly _defaultSize: number = 0; - - /** - * Gets or sets the layout entries. This is either a count of the layout entries or an array of - * the layout entries. - */ - private _layoutEntries: number | LayoutEntry[] = 0; - - /** - * Gets or sets the layout overrides. - */ - private _layoutOverrides = new Map(); - - /** - * Gets or sets the cached layout entry. - */ - private _cachedLayoutEntry?: LayoutEntry; - - //#endregion Private Properties - - //#region Constructor - - /** - * Constructor. - * @param defaultSize The default size. - */ - constructor(defaultSize: number = 0) { - this._defaultSize = defaultSize; - } - - //#endregion Constructor - - //#region Public Properties - - /** - * Gets the size of the layout entries. - */ - get size() { - // If the layout entries is an array, return the end of the last layout entry. - if (Array.isArray(this._layoutEntries)) { - return this._layoutEntries[this._layoutEntries.length - 1].end; - } - - // Calculate the size of the layout entries. - let size = this._layoutEntries * this._defaultSize; - const sortedLayoutOverrides = this.getSortedLayoutOverrides(); - for (let index = 0; index < sortedLayoutOverrides.length; index++) { - const layoutOverride = sortedLayoutOverrides[index]; - if (layoutOverride.index < this._layoutEntries) { - size = size - this._defaultSize + layoutOverride.overrideSize; - } else { - break; - } - } - - // Return the size of the layout entries. - return size; - } - - //#endregion Public Properties - - //#region Public Methods - - /** - * Sets the layout entries. - * @param layoutEntries The layout entries. - */ - setLayoutEntries(layoutEntries: number | number[]) { - // Clear the cached layout entry. - this._cachedLayoutEntry = undefined; - - // If layout entries is a number, set it; otherwise, create and populate the layout entries - // array from the supplied layout entries. - if (!Array.isArray(layoutEntries)) { - this._layoutEntries = layoutEntries; - return; - } - - // Create the layout entries array. - this._layoutEntries = new Array(layoutEntries.length); - - // Set the layout entries in the layout entries array. - for (let index = 0, start = 0; index < layoutEntries.length; index++) { - // Create the layout entry. - const layoutEntry = new LayoutEntry( - index, - start, - layoutEntries[index], - this._layoutOverrides.get(index) - ); - - // Set the layout entry. - this._layoutEntries[index] = layoutEntry; - - // Update the start for the next layout entry. - start = layoutEntry.end; - } - } - - /** - * Clears a layout override. - * @param index The index of the layout override. - */ - clearLayoutOverride(index: number) { - // Discard the cached layout entry, if it exists and its index is greater than the index of - // the layout override being cleared. - if (this._cachedLayoutEntry && this._cachedLayoutEntry.index >= index) { - this._cachedLayoutEntry = undefined; - } - - // Clear the layout override. - this._layoutOverrides.delete(index); - - // Adjust the layout entries. - if (Array.isArray(this._layoutEntries) && index < this._layoutEntries.length) { - // Get the layout entry for the layout override being cleared and clear its override - // size. - const layoutEntry = this._layoutEntries[index]; - layoutEntry.overrideSize = undefined; - - // Adjust the start of the remaining layout entries. - for (let i = index + 1, start = layoutEntry.end; i < this._layoutEntries.length; i++) { - // Update the start of the layout entry. - const layoutEntry = this._layoutEntries[i]; - layoutEntry.start = start; - - // Adjust the start for the next layout entry. - start = layoutEntry.end; - } - } - } - - /** - * Sets a layout override. - * @param index The index of the layout entry. - * @param overrideSize The override size of the layout entry. - */ - setLayoutOverride(index: number, overrideSize: number) { - // Sanity check the index and size. - if (!Number.isInteger(index) || index < 0 || overrideSize <= 0) { - return; - } - - // Discard the cached layout entry, if it exists and its index is greater than the index of - // the layout override. - if (this._cachedLayoutEntry && this._cachedLayoutEntry.index >= index) { - this._cachedLayoutEntry = undefined; - } - - // Set the layout override. - this._layoutOverrides.set(index, overrideSize); - - // Adjust the layout entries. - if (Array.isArray(this._layoutEntries) && index < this._layoutEntries.length) { - // Get the layout entry that was overridden and set its override size. - const layoutEntry = this._layoutEntries[index]; - layoutEntry.overrideSize = overrideSize; - - // Adjust the start of the remaining layout entries. - for (let i = index + 1, start = layoutEntry.end; i < this._layoutEntries.length; i++) { - // Update the start of the layout entry. - const layoutEntry = this._layoutEntries[i]; - layoutEntry.start = start; - - // Adjust the start for the next layout entry. - start = layoutEntry.end; - } - } - } - - /** - * Gets a layout entry by index - * @param index The index. - * @returns The layout entry at the specified index, if found; otherwise, undefined. - */ - getLayoutEntry(index: number): ILayoutEntry | undefined { - // Sanity check the index. - if (index < 0) { - return undefined; - } - - // If we have the layout entry cached, return it. - if (this._cachedLayoutEntry && this._cachedLayoutEntry.index === index) { - return this._cachedLayoutEntry; - } - - // If layout entries is an array, return the layout entry at the specified index. - if (Array.isArray(this._layoutEntries)) { - // Sanity check the index. - if (index >= this._layoutEntries.length) { - return undefined; - } - - // Return the layout entry. - return this._layoutEntries[index]; - } - - // Sanity check the index. - if (index >= this._layoutEntries) { - return undefined; - } - - // If there are no layout overrides, we can calculate which layout entry to return. - // Cache and return the layout entry. - if (!this._layoutOverrides.size) { - // Return the layout entry. - return new LayoutEntry( - index, - index * this._defaultSize, - this._defaultSize - ); - } - - // Calculate the start and size of the layout entry to return. - const sortedLayoutOverrides = this.getSortedLayoutOverrides(); - let start = index * this._defaultSize; - sortedLayoutOverrides.some(layoutOverride => { - // If the layout override index is less than the index, adjust the start and return - // false to continue the search. - if (layoutOverride.index < index) { - start = start - this._defaultSize + layoutOverride.overrideSize; - return false; - } - - // Return true to stop the search. - return true; - }); - - // Return the layout entry. - return new LayoutEntry( - index, - start, - this._defaultSize, - this._layoutOverrides.get(index) - ); - } - - /** - * Finds a layout entry. - * @param offset The offset of the layout entry to find. - * @returns The layout entry, if found; otherwise, undefined. - */ - findLayoutEntry(offset: number): ILayoutEntry | undefined { - // Sanity check the offset. - if (offset < 0) { - return undefined; - } - - // See if the layout entry is cached. If it is, return it. - if (this._cachedLayoutEntry) { - if (offset >= this._cachedLayoutEntry.start && offset < this._cachedLayoutEntry.end) { - return this._cachedLayoutEntry; - } - } - - // Find the layout entry to return. - if (!Array.isArray(this._layoutEntries)) { - // If there are no layout overrides, we can calculate which layout entry to return. - if (!this._layoutOverrides.size) { - // Calculate the layout entry index to return. If it's beyond the number of layout - // entries, return undefined. - const index = Math.floor(offset / this._defaultSize); - if (index >= this._layoutEntries) { - return undefined; - } - - // Cache and return the layout entry. - return this._cachedLayoutEntry = new LayoutEntry( - index, - index * this._defaultSize, - this._defaultSize - ); - } - - // Binary search the layout entries. - let leftIndex = 0; - let rightIndex = this._layoutEntries - 1; - const sortedLayoutOverrides = this.getSortedLayoutOverrides(); - while (leftIndex <= rightIndex) { - // Calculate the middle index. - const middleIndex = Math.floor((leftIndex + rightIndex) / 2); - - // Calculate the start and size of the middle layout entry. - let start = middleIndex * this._defaultSize; - sortedLayoutOverrides.some(layoutOverride => { - // If the layout override index is less than the middle index, adjust the start - // and return false to continue the search. - if (layoutOverride.index < middleIndex) { - start = start - this._defaultSize + layoutOverride.overrideSize; - return false; - } - - // Return true to stop the search. - return true; - }); - - // Check if the middle layout entry contains the offset. If so, cache and return it. - if (offset >= start && - offset < start + (this._layoutOverrides.get(middleIndex) ?? this._defaultSize) - ) { - // Cache and return the layout entry. - return this._cachedLayoutEntry = new LayoutEntry( - middleIndex, - start, - this._defaultSize, - this._layoutOverrides.get(middleIndex) - ); - } - - // Setup the next binary chop. - if (start < offset) { - leftIndex = middleIndex + 1; - } else { - rightIndex = middleIndex - 1; - } - } - - // Not found. - return undefined; - } else { - // Binary search the array of layout entries. - let leftIndex = 0; - let rightIndex = this._layoutEntries.length - 1; - while (leftIndex <= rightIndex) { - // Calculate the middle index and get the middle layout entry to check. - const middleIndex = Math.floor((leftIndex + rightIndex) / 2); - const middleLayoutEntry = this._layoutEntries[middleIndex]; - - // Check if the middle layout entry contains the offset. If so, cache and return it. - if (offset >= middleLayoutEntry.start && offset < middleLayoutEntry.end) { - // Cache the layout entry and return its layout. - return this._cachedLayoutEntry = middleLayoutEntry; - } - - // Setup the next binary chop. - if (middleLayoutEntry.start < offset) { - leftIndex = middleIndex + 1; - } else { - rightIndex = middleIndex - 1; - } - } - - // Not found. - return undefined; - } - } - - //#endregion Public Methods - - //#region Private Methods - - /** - * Gets the sorted layout overrides. - */ - private getSortedLayoutOverrides(): ILayoutOverride[] { - return Array.from(this._layoutOverrides). - map(([index, size]): ILayoutOverride => ({ index, overrideSize: size })). - sort((a, b) => a.index - b.index); - } - - //#endregion Private Methods -} diff --git a/src/vs/workbench/services/positronDataExplorer/common/tableDataCache.ts b/src/vs/workbench/services/positronDataExplorer/common/tableDataCache.ts index fc35a5b68895..9e3b77f9af71 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/tableDataCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/tableDataCache.ts @@ -3,10 +3,10 @@ * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ +import { isContiguous } from './utils.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { pinToRange } from '../../../../base/common/positronUtilities.js'; -import { arrayFromIndexRange } from './utils.js'; import { DataExplorerClientInstance } from '../../languageRuntime/common/languageRuntimeDataExplorerClient.js'; import { ArraySelection, ColumnSchema, ColumnSelection, DataSelectionIndices, DataSelectionRange } from '../../languageRuntime/common/positronDataExplorerComm.js'; @@ -16,7 +16,6 @@ import { ArraySelection, ColumnSchema, ColumnSelection, DataSelectionIndices, Da const MAX_AUTO_SIZE_COLUMNS = 1_000; const AUTO_SIZE_COLUMNS_PAGE_SIZE = 250; const TRIM_CACHE_TIMEOUT = 3_000; -const OVERSCAN_FACTOR = 3; const CHUNK_SIZE = 4_096; /** @@ -42,10 +41,8 @@ export enum InvalidateCacheFlags { */ interface UpdateDescriptor { invalidateCache: InvalidateCacheFlags; - firstColumnIndex: number; - screenColumns: number; - firstRowIndex: number; - screenRows: number; + columnIndices: number[]; + rowIndices: number[]; } /** @@ -242,12 +239,12 @@ export class TableDataCache extends Disposable { } /** - * Calculates the column layout entries. + * Calculates the column widths. * @param minimumColumnWidth The minimum column width. * @param maximumColumnWidth The maximum column width. - * @returns An array of column layout layout entries, if successful; otherwise, undefined. + * @returns An array of column widths, if successful; otherwise, undefined. */ - async calculateColumnLayoutEntries( + async calculateColumnWidths( minimumColumnWidth: number, maximumColumnWidth: number ): Promise { @@ -362,58 +359,45 @@ export class TableDataCache extends Disposable { // Set the updating flag. this._updating = true; - // Destructure the update descriptor. - const { - invalidateCache, - firstColumnIndex, - screenColumns, - firstRowIndex, - screenRows - } = updateDescriptor; - - // Set the invalidate cache flags. - const invalidateColumnSchemaCache = (invalidateCache & InvalidateCacheFlags.ColumnSchema) - === InvalidateCacheFlags.ColumnSchema; - const invalidateDataCache = (invalidateCache & InvalidateCacheFlags.Data) - === InvalidateCacheFlags.Data; - - // Get the size of the data. + // Get the size of the data and whether it has row labels. const tableState = await this._dataExplorerClientInstance.getBackendState(); this._columns = tableState.table_shape.num_columns; this._rows = tableState.table_shape.num_rows; this._hasRowLabels = tableState.has_row_labels; - // If there are no rows (e.g., due to filtering), immediately fire an update and return - if (this._rows === 0) { - // Clear existing caches for a clean display + // If there are no rows or no columns (e.g., due to filtering), immediately fire an update and return. + if (this._rows === 0 || this._columns === 0) { + // Clear existing caches for a clean display. this._rowLabelCache.clear(); this._dataColumnCache.clear(); - // Fire the update event before returning to ensure UI updates even with zero rows + + // Fire the update event before returning to ensure UI updates even with zero rows. this._onDidUpdateEmitter.fire(); + + // Clear the updating flag. this._updating = false; + + // Return. return; } - // Set the start column index and the end column index of the columns to cache. - const overscanColumns = screenColumns * OVERSCAN_FACTOR; - const startColumnIndex = Math.max( - 0, - firstColumnIndex - overscanColumns - ); - const endColumnIndex = Math.min( - this._columns - 1, - firstColumnIndex + screenColumns + overscanColumns, - ); - - // Set the column indices of the column schema we need to load. + // Set the invalidate cache flags. + const invalidateColumnSchemaCache = (updateDescriptor.invalidateCache & InvalidateCacheFlags.ColumnSchema) === InvalidateCacheFlags.ColumnSchema; + const invalidateDataCache = (updateDescriptor.invalidateCache & InvalidateCacheFlags.Data) === InvalidateCacheFlags.Data; + + // Sort the column and row indices in the update descriptor. + updateDescriptor.columnIndices.sort((a, b) => a - b); + updateDescriptor.rowIndices.sort((a, b) => a - b); + + // Set the column indices of the table schema we need to load. let columnIndices: number[]; if (invalidateColumnSchemaCache) { - columnIndices = arrayFromIndexRange(startColumnIndex, endColumnIndex); + columnIndices = updateDescriptor.columnIndices; } else { columnIndices = []; - for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) { - if (!this._columnSchemaCache.has(columnIndex)) { - columnIndices.push(columnIndex); + for (const index of updateDescriptor.columnIndices) { + if (!this._columnSchemaCache.has(index)) { + columnIndices.push(index); } } } @@ -423,93 +407,84 @@ export class TableDataCache extends Disposable { this._columnSchemaCache.clear(); } - // Load the column schemas we need to load. + // Load the table schema we need to load. const tableSchema = await this._dataExplorerClientInstance.getSchema(columnIndices); // Cache the column schemas that were returned. - for (let i = 0; i < tableSchema.columns.length; i++) { - // Get the column schema and compute the column index. - const columnIndex = columnIndices[i]; - const columnSchema = tableSchema.columns[i]; - - // Cache the column schema. - this._columnSchemaCache.set(columnIndex, columnSchema); + for (const columnSchema of tableSchema.columns) { + this._columnSchemaCache.set(columnSchema.column_index, columnSchema); } // Fire the onDidUpdate event. this._onDidUpdateEmitter.fire(); - // Set the start row index and the end row index of the rows to cache. - const overscanRows = screenRows * OVERSCAN_FACTOR; - const startRowIndex = Math.max( - 0, - firstRowIndex - overscanRows - ); - const endRowIndex = Math.min( - this._rows - 1, - firstRowIndex + screenRows + overscanRows - ); + // Determine whether the row indices are contiguous. + const rowIndicesAreContiguous = isContiguous(updateDescriptor.rowIndices); + + // Create the array selection spec. + let spec: ArraySelection; + if (rowIndicesAreContiguous) { + spec = { + first_index: updateDescriptor.rowIndices[0], + last_index: updateDescriptor.rowIndices[updateDescriptor.rowIndices.length - 1], + }; + } else { + spec = { + indices: updateDescriptor.rowIndices, + }; + } // Build an array of the column selections to load. const columnSelections: ColumnSelection[] = []; if (invalidateDataCache) { // The data cache is being invalidated. Load everything. - for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) { + for (const columnIndex of updateDescriptor.columnIndices) { columnSelections.push({ column_index: columnIndex, - spec: { - first_index: startRowIndex, - last_index: endRowIndex - } + spec }); } } else { - // The cache is not being invalidated. Load only the cells that we don't have cached. - for (let columnIndex = startColumnIndex; columnIndex <= endColumnIndex; columnIndex++) { + // The data cache is not being invalidated. Load everything we don't have cached. + for (const columnIndex of updateDescriptor.columnIndices) { const dataColumn = this._dataColumnCache.get(columnIndex); if (!dataColumn) { // The data column isn't cached. Load it. columnSelections.push({ column_index: columnIndex, - spec: { - first_index: startRowIndex, - last_index: endRowIndex - } + spec }); } else { // The data column is cached. Load any cells that are not cached. let contiguous = true; - const indices: number[] = []; - for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) { + const rowIndices: number[] = []; + for (const rowIndex of updateDescriptor.rowIndices) { if (!dataColumn.has(rowIndex)) { // Add the index. - indices.push(rowIndex); + rowIndices.push(rowIndex); // Check whether the indices are contiguous. - if (contiguous && - indices.length > 1 && - indices[indices.length - 2] + 1 !== indices[indices.length - 1] - ) { + if (contiguous && rowIndices.length > 1 && rowIndices[rowIndices.length - 2] + 1 !== rowIndices[rowIndices.length - 1]) { contiguous = false; } } } // If there are cells that are not cached, add the column and its spec. - if (indices.length) { - if (!contiguous) { + if (rowIndices.length) { + if (contiguous) { columnSelections.push({ column_index: columnIndex, spec: { - indices: indices + first_index: rowIndices[0], + last_index: rowIndices[rowIndices.length - 1] } }); } else { columnSelections.push({ column_index: columnIndex, spec: { - first_index: indices[0], - last_index: indices[indices.length - 1] + indices: rowIndices } }); } @@ -524,45 +499,50 @@ export class TableDataCache extends Disposable { rowLabels = undefined; } else { if (invalidateDataCache) { - rowLabels = { first_index: startRowIndex, last_index: endRowIndex }; + if (rowIndicesAreContiguous) { + rowLabels = { + first_index: updateDescriptor.rowIndices[0], + last_index: updateDescriptor.rowIndices[updateDescriptor.rowIndices.length - 1], + }; + } else { + rowLabels = { + indices: updateDescriptor.rowIndices + }; + } } else { let contiguous = true; - const indices: number[] = []; - for (let rowIndex = startRowIndex; rowIndex <= endRowIndex; rowIndex++) { + const rowIndices: number[] = []; + for (const rowIndex of updateDescriptor.rowIndices) { if (!this._rowLabelCache.has(rowIndex)) { // Add the index. - indices.push(rowIndex); + rowIndices.push(rowIndex); // Check whether the indices are contiguous. - if (contiguous && - indices.length > 1 && - indices[indices.length - 2] + 1 !== indices[indices.length - 1] - ) { + if (contiguous && rowIndices.length > 1 && rowIndices[rowIndices.length - 2] + 1 !== rowIndices[rowIndices.length - 1]) { contiguous = false; } } } // If there are labels that are not cached, - if (!indices.length) { + if (!rowIndices.length) { rowLabels = undefined; } else { - if (!contiguous) { - rowLabels = { indices }; - } else { + if (contiguous) { rowLabels = { - first_index: indices[0], - last_index: indices[indices.length - 1] + first_index: rowIndices[0], + last_index: rowIndices[rowIndices.length - 1] }; + } else { + rowLabels = { indices: rowIndices }; } } } + } // Get the table row labels. - const tableRowLabels = !rowLabels ? - undefined : - await this._dataExplorerClientInstance.getRowLabels(rowLabels); + const tableRowLabels = !rowLabels ? undefined : await this._dataExplorerClientInstance.getRowLabels(rowLabels); // Clear the data cache, if we're supposed to. if (invalidateDataCache) { @@ -651,25 +631,21 @@ export class TableDataCache extends Disposable { } // Schedule trimming the cache. - if (invalidateCache !== InvalidateCacheFlags.All) { + if (updateDescriptor.invalidateCache !== InvalidateCacheFlags.All) { // Set the trim cache timeout. this._trimCacheTimeout = setTimeout(() => { // Release the trim cache timeout. this._trimCacheTimeout = undefined; // Trim the column schema cache, if it wasn't invalidated. + const columnIndices = new Set(updateDescriptor.columnIndices); if (!invalidateColumnSchemaCache) { - this.trimColumnSchemaCache(startColumnIndex, endColumnIndex); + this.trimColumnSchemaCache(columnIndices); } // Trim the data cache, if it wasn't invalidated. if (!invalidateDataCache) { - this.trimDataCache( - startColumnIndex, - endColumnIndex, - startRowIndex, - endRowIndex - ); + this.trimDataCache(columnIndices, new Set(updateDescriptor.rowIndices)); } }, TRIM_CACHE_TIMEOUT); } @@ -826,13 +802,12 @@ export class TableDataCache extends Disposable { /** * Trims the column schema cache. - * @param startColumnIndex The start column index. - * @param endColumnIndex The end column index. + * @param columnIndices The column indicies to keep cached. */ - private trimColumnSchemaCache(startColumnIndex: number, endColumnIndex: number) { + private trimColumnSchemaCache(columnIndices: Set) { // Trim the column schema cache. for (const columnIndex of this._columnSchemaCache.keys()) { - if (columnIndex < startColumnIndex || columnIndex > endColumnIndex) { + if (!columnIndices.has(columnIndex)) { this._columnSchemaCache.delete(columnIndex); } } @@ -840,33 +815,29 @@ export class TableDataCache extends Disposable { /** * Trims the data cache. - * @param startColumnIndex The start column index. - * @param endColumnIndex The end column index. - * @param startRowIndex The start row index. - * @param endRowIndex The end row index. + * @param columnIndices The column indicies to keep cached. + * @param rowIndices The row indicies to keep cached. */ private trimDataCache( - startColumnIndex: number, - endColumnIndex: number, - startRowIndex: number, - endRowIndex: number + columnIndices: Set, + rowIndices: Set ) { // Trim the row label cache. for (const rowIndex of this._rowLabelCache.keys()) { - if (rowIndex < startRowIndex || rowIndex > endRowIndex) { + if (!rowIndices.has(rowIndex)) { this._rowLabelCache.delete(rowIndex); } } // Trim the data column cache. for (const columnIndex of this._dataColumnCache.keys()) { - if (columnIndex < startColumnIndex || columnIndex > endColumnIndex) { + if (!columnIndices.has(columnIndex)) { this._dataColumnCache.delete(columnIndex); } else { const dataColumn = this._dataColumnCache.get(columnIndex); if (dataColumn) { for (const rowIndex of dataColumn.keys()) { - if (rowIndex < startRowIndex || rowIndex > endRowIndex) { + if (!rowIndices.has(rowIndex)) { dataColumn.delete(rowIndex); } } diff --git a/src/vs/workbench/services/positronDataExplorer/common/utils.ts b/src/vs/workbench/services/positronDataExplorer/common/utils.ts index 27b31f0db041..130c3d52d328 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/utils.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/utils.ts @@ -21,11 +21,24 @@ export const arrayFromIndexRange = (startIndex: number, endIndex: number) => Array.from({ length: endIndex - startIndex + 1 }, (_, i) => startIndex + i); /** - * Performs a linear conversion from one range to another range. - * @param value The value to convert. - * @param from The from range. - * @param to The to range. - * @returns The converted value. + * Determines whether an array of numbers is contiguous. + * @param array The array of numbers. + * @returns true if the array is contiguous; otherwise, false. */ -export const linearConversion = (value: number, from: Range, to: Range) => - ((value - from.min) / (from.max - from.min)) * (to.max - to.min) + to.min; +export const isContiguous = (array: number[]) => { + if (array.length === 0) { + return false; + } + + if (array.length === 1) { + return true; + } + + for (let i = 1; i < array.length; i++) { + if (array[i - 1] + 1 !== array[i]) { + return false; + } + } + + return true; +}; diff --git a/src/vs/workbench/test/common/layoutManager.test.ts b/src/vs/workbench/test/browser/layoutManager.test.ts similarity index 56% rename from src/vs/workbench/test/common/layoutManager.test.ts rename to src/vs/workbench/test/browser/layoutManager.test.ts index b9cdb19af333..5a58bb831df0 100644 --- a/src/vs/workbench/test/common/layoutManager.test.ts +++ b/src/vs/workbench/test/browser/layoutManager.test.ts @@ -1,11 +1,11 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { LayoutManager } from '../../browser/positronDataGrid/classes/layoutManager.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js'; -import { LayoutManager } from '../../services/positronDataExplorer/common/layoutManager.js'; /** * Tests the LayoutManager class. @@ -76,8 +76,8 @@ suite('LayoutManager', () => { verifyFixedSizedPredefinedEntries(10, 10); verifyFixedSizedPredefinedEntries(1, 1_000); verifyFixedSizedPredefinedEntries(19, 1_000); - verifyFixedSizedPredefinedEntries(127, 20_000); // Too big for CI. + // verifyFixedSizedPredefinedEntries(127, 20_000); // verifyFixedSizedPredefinedEntries(23, 500_000); }); @@ -87,8 +87,176 @@ suite('LayoutManager', () => { test('Randomly-Sized Predefined Entries', () => { verifyRandomlySizedPredefinedEntries(1); verifyRandomlySizedPredefinedEntries(10); - verifyRandomlySizedPredefinedEntries(1_000); - verifyRandomlySizedPredefinedEntries(20_000); + verifyRandomlySizedPredefinedEntries(100); + // Too big for CI. + //verifyRandomlySizedPredefinedEntries(1_000); + //verifyRandomlySizedPredefinedEntries(20_000); + }); + + /** + * Tests mapping positions to indexes and indexes to positions with no entry map and no pinned indexes. + */ + test('Map Position To Index - Map Index To Position - No Entry Map - No Pinned Indexes', () => { + // Create and initialize the layout manager. + const layoutManager = new LayoutManager(100); + layoutManager.setEntries(10); + + // Test mapping positions to indexes. + assert(layoutManager.mapPositionToIndex(-1) === undefined); + assert(layoutManager.mapPositionToIndex(-10) === undefined); + testMapPositionToIndex(layoutManager, 0, 0); + testMapPositionToIndex(layoutManager, 1, 1); + testMapPositionToIndex(layoutManager, 2, 2); + testMapPositionToIndex(layoutManager, 3, 3); + testMapPositionToIndex(layoutManager, 4, 4); + testMapPositionToIndex(layoutManager, 5, 5); + testMapPositionToIndex(layoutManager, 6, 6); + testMapPositionToIndex(layoutManager, 7, 7); + testMapPositionToIndex(layoutManager, 8, 8); + testMapPositionToIndex(layoutManager, 9, 9); + assert(layoutManager.mapPositionToIndex(10) === undefined); + assert(layoutManager.mapPositionToIndex(100) === undefined); + + // Test mapping indexes to positions. + assert(layoutManager.mapIndexToPosition(-1) === undefined); + assert(layoutManager.mapIndexToPosition(-10) === undefined); + testMapIndexToPosition(layoutManager, 0, 0); + testMapIndexToPosition(layoutManager, 1, 1); + testMapIndexToPosition(layoutManager, 2, 2); + testMapIndexToPosition(layoutManager, 3, 3); + testMapIndexToPosition(layoutManager, 4, 4); + testMapIndexToPosition(layoutManager, 5, 5); + testMapIndexToPosition(layoutManager, 6, 6); + testMapIndexToPosition(layoutManager, 7, 7); + testMapIndexToPosition(layoutManager, 8, 8); + testMapIndexToPosition(layoutManager, 9, 9); + assert(layoutManager.mapIndexToPosition(10) === undefined); + assert(layoutManager.mapIndexToPosition(100) === undefined); + }); + + /** + * Tests mapping positions to indexes and indexes to positions with an entry map and no pinned indexes. + */ + test('Map Position To Index - Map Index To Position - With Entry Map - No Pinned Indexes', () => { + // Create and initialize the layout manager. + const layoutManager = new LayoutManager(100); + layoutManager.setEntries(10, undefined, [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + + // Test mapping positions to indexes. + assert(layoutManager.mapPositionToIndex(-1) === undefined); + assert(layoutManager.mapPositionToIndex(-10) === undefined); + testMapPositionToIndex(layoutManager, 0, 9); + testMapPositionToIndex(layoutManager, 1, 8); + testMapPositionToIndex(layoutManager, 2, 7); + testMapPositionToIndex(layoutManager, 3, 6); + testMapPositionToIndex(layoutManager, 4, 5); + testMapPositionToIndex(layoutManager, 5, 4); + testMapPositionToIndex(layoutManager, 6, 3); + testMapPositionToIndex(layoutManager, 7, 2); + testMapPositionToIndex(layoutManager, 8, 1); + testMapPositionToIndex(layoutManager, 9, 0); + assert(layoutManager.mapPositionToIndex(10) === undefined); + assert(layoutManager.mapPositionToIndex(100) === undefined); + + // Test mapping indexes to positions. + assert(layoutManager.mapIndexToPosition(-1) === undefined); + assert(layoutManager.mapIndexToPosition(-10) === undefined); + testMapIndexToPosition(layoutManager, 9, 0); + testMapIndexToPosition(layoutManager, 8, 1); + testMapIndexToPosition(layoutManager, 7, 2); + testMapIndexToPosition(layoutManager, 6, 3); + testMapIndexToPosition(layoutManager, 5, 4); + testMapIndexToPosition(layoutManager, 4, 5); + testMapIndexToPosition(layoutManager, 3, 6); + testMapIndexToPosition(layoutManager, 2, 7); + testMapIndexToPosition(layoutManager, 1, 8); + testMapIndexToPosition(layoutManager, 0, 9); + assert(layoutManager.mapIndexToPosition(10) === undefined); + assert(layoutManager.mapIndexToPosition(100) === undefined); + }); + + /** + * Tests mapping positions to indexes and indexes to positions with an entry map and no pinned indexes. + */ + test('Map Position To Index - Map Index To Position - With No Entry Map - With Pinned Indexes', () => { + // Create and initialize the layout manager. + const layoutManager = new LayoutManager(100); + layoutManager.setEntries(10); + layoutManager.setPinnedIndexes([3, 2, 0]); + + // Test mapping positions to indexes. + assert(layoutManager.mapPositionToIndex(-1) === undefined); + assert(layoutManager.mapPositionToIndex(-10) === undefined); + testMapPositionToIndex(layoutManager, 0, 3); + testMapPositionToIndex(layoutManager, 1, 2); + testMapPositionToIndex(layoutManager, 2, 0); + testMapPositionToIndex(layoutManager, 3, 1); + testMapPositionToIndex(layoutManager, 4, 4); + testMapPositionToIndex(layoutManager, 5, 5); + testMapPositionToIndex(layoutManager, 6, 6); + testMapPositionToIndex(layoutManager, 7, 7); + testMapPositionToIndex(layoutManager, 8, 8); + testMapPositionToIndex(layoutManager, 9, 9); + assert(layoutManager.mapPositionToIndex(10) === undefined); + assert(layoutManager.mapPositionToIndex(100) === undefined); + + // Test mapping indexes to positions. + assert(layoutManager.mapIndexToPosition(-1) === undefined); + assert(layoutManager.mapIndexToPosition(-10) === undefined); + testMapIndexToPosition(layoutManager, 0, 2); + testMapIndexToPosition(layoutManager, 1, 3); + testMapIndexToPosition(layoutManager, 2, 1); + testMapIndexToPosition(layoutManager, 3, 0); + testMapIndexToPosition(layoutManager, 4, 4); + testMapIndexToPosition(layoutManager, 5, 5); + testMapIndexToPosition(layoutManager, 6, 6); + testMapIndexToPosition(layoutManager, 7, 7); + testMapIndexToPosition(layoutManager, 8, 8); + testMapIndexToPosition(layoutManager, 9, 9); + assert(layoutManager.mapIndexToPosition(10) === undefined); + assert(layoutManager.mapIndexToPosition(100) === undefined); + }); + + /** + * Tests mapping positions to indexes and indexes to positions with an entry map and no pinned indexes. + */ + test('Map Position To Index - Map Index To Position - With Entry Map - With Pinned Indexes', () => { + // Create and initialize the layout manager. + const layoutManager = new LayoutManager(100); + layoutManager.setEntries(10, undefined, [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]); + layoutManager.setPinnedIndexes([3, 2, 0]); + + // Test mapping positions to indexes. + assert(layoutManager.mapPositionToIndex(-1) === undefined); + assert(layoutManager.mapPositionToIndex(-10) === undefined); + testMapPositionToIndex(layoutManager, 0, 3); + testMapPositionToIndex(layoutManager, 1, 2); + testMapPositionToIndex(layoutManager, 2, 0); + testMapPositionToIndex(layoutManager, 3, 9); + testMapPositionToIndex(layoutManager, 4, 8); + testMapPositionToIndex(layoutManager, 5, 7); + testMapPositionToIndex(layoutManager, 6, 6); + testMapPositionToIndex(layoutManager, 7, 5); + testMapPositionToIndex(layoutManager, 8, 4); + testMapPositionToIndex(layoutManager, 9, 1); + assert(layoutManager.mapPositionToIndex(10) === undefined); + assert(layoutManager.mapPositionToIndex(100) === undefined); + + // Test mapping indexes to positions. + assert(layoutManager.mapIndexToPosition(-1) === undefined); + assert(layoutManager.mapIndexToPosition(-10) === undefined); + testMapIndexToPosition(layoutManager, 3, 0); + testMapIndexToPosition(layoutManager, 2, 1); + testMapIndexToPosition(layoutManager, 0, 2); + testMapIndexToPosition(layoutManager, 9, 3); + testMapIndexToPosition(layoutManager, 8, 4); + testMapIndexToPosition(layoutManager, 7, 5); + testMapIndexToPosition(layoutManager, 6, 6); + testMapIndexToPosition(layoutManager, 5, 7); + testMapIndexToPosition(layoutManager, 4, 8); + testMapIndexToPosition(layoutManager, 1, 9); + assert(layoutManager.mapIndexToPosition(10) === undefined); + assert(layoutManager.mapIndexToPosition(100) === undefined); }); /** @@ -97,28 +265,28 @@ suite('LayoutManager', () => { const verifySizeOfDefaultSizedEntries = (defaultSize: number, entries: number) => { // Create and initialize the layout manager. const layoutManager = new LayoutManager(defaultSize); - layoutManager.setLayoutEntries(entries); + layoutManager.setEntries(entries); const size = defaultSize * entries; // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); // Add a layout override that will affect the size, and one that will not, for coverage. - layoutManager.setLayoutOverride(0, defaultSize * 2); - layoutManager.setLayoutOverride(entries, defaultSize * 2); + layoutManager.setSizeOverride(0, defaultSize * 2); + layoutManager.setSizeOverride(entries, defaultSize * 2); // Verify size. - assert.strictEqual(layoutManager.size, size + defaultSize); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size + defaultSize); // Get a layout entry cached, for coverage. - layoutManager.findLayoutEntry(0); + layoutManager.findFirstUnpinnedLayoutEntry(0); // Clear layout overrides. - layoutManager.clearLayoutOverride(0); - layoutManager.clearLayoutOverride(entries); + layoutManager.clearSizeOverride(0); + layoutManager.clearSizeOverride(entries); // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); }; /** @@ -126,29 +294,26 @@ suite('LayoutManager', () => { */ const verifySizeOfFixedSizedEntries = (entrySize: number, entries: number) => { // Create and initialize the layout manager. - const layoutManager = new LayoutManager(0); - layoutManager.setLayoutEntries(Array.from({ length: entries }, (_, i) => entrySize)); + const layoutManager = new LayoutManager(100); + layoutManager.setEntries(entries, Array.from({ length: entries }, (_, i) => entrySize)); const size = entrySize * entries; // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); // Add a layout override that will affect the size, and one that will not, for coverage. - layoutManager.setLayoutOverride(0, entrySize * 2); - layoutManager.setLayoutOverride(entries, entrySize * 2); + layoutManager.setSizeOverride(0, entrySize * 2); + layoutManager.setSizeOverride(entries, entrySize * 2); // Verify size. - assert.strictEqual(layoutManager.size, size + entrySize); - - // Get a layout entry cached, for coverage. - layoutManager.findLayoutEntry(0); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size + entrySize); // Clear layout overrides. - layoutManager.clearLayoutOverride(0); - layoutManager.clearLayoutOverride(entries); + layoutManager.clearSizeOverride(0); + layoutManager.clearSizeOverride(entries); // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); }; /** @@ -156,32 +321,29 @@ suite('LayoutManager', () => { */ const verifySizeOfRandomlySizedEntries = (entries: number) => { // Create the layout manager. - const layoutManager = new LayoutManager(0); - const layoutEntries = Array.from({ length: entries }, (_, i) => - getRandomIntInclusive(1, 4096) + const layoutManager = new LayoutManager(100); + const entrySizes = Array.from({ length: entries }, (_, i) => + getRandomIntInclusive(1, 400) ); - layoutManager.setLayoutEntries(layoutEntries); - const size = layoutEntries.reduce((size, randomSize) => size + randomSize, 0); + layoutManager.setEntries(entries, entrySizes); + const size = entrySizes.reduce((size, randomSize) => size + randomSize, 0); // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); // Add a layout override that will affect the size, and one that will not, for coverage. - layoutManager.setLayoutOverride(0, layoutEntries[0] * 2); - layoutManager.setLayoutOverride(entries, 354); + layoutManager.setSizeOverride(0, entrySizes[0] * 2); + layoutManager.setSizeOverride(entries, 354); // Verify size. - assert.strictEqual(layoutManager.size, size + layoutEntries[0]); - - // Get a layout entry cached, for coverage. - layoutManager.findLayoutEntry(0); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size + entrySizes[0]); // Clear layout overrides. - layoutManager.clearLayoutOverride(0); - layoutManager.clearLayoutOverride(entries); + layoutManager.clearSizeOverride(0); + layoutManager.clearSizeOverride(entries); // Verify size. - assert.strictEqual(layoutManager.size, size); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, size); }; /** @@ -192,7 +354,7 @@ suite('LayoutManager', () => { const verifyGetLayoutEntryOfDefaultSizedEntries = (defaultSize: number, entries: number) => { // Create and initialize the layout manager. const layoutManager = new LayoutManager(defaultSize); - layoutManager.setLayoutEntries(entries); + layoutManager.setEntries(entries); // Verify getting the first layout entry. let layoutEntry = layoutManager.getLayoutEntry(0); @@ -209,7 +371,7 @@ suite('LayoutManager', () => { assert.strictEqual(layoutEntry.size, defaultSize); // Add a layout override. - layoutManager.setLayoutOverride(0, defaultSize * 2); + layoutManager.setSizeOverride(0, defaultSize * 2); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); @@ -228,7 +390,7 @@ suite('LayoutManager', () => { } // Clear the layout override. - layoutManager.clearLayoutOverride(0); + layoutManager.clearSizeOverride(0); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); @@ -252,8 +414,8 @@ suite('LayoutManager', () => { */ const verifyGetLayoutEntryOfFixedSizedEntries = (entrySize: number, entries: number) => { // Create and initialize the layout manager. - const layoutManager = new LayoutManager(0); - layoutManager.setLayoutEntries(Array.from({ length: entries }, (_, i) => entrySize)); + const layoutManager = new LayoutManager(entrySize); + layoutManager.setEntries(entries, Array.from({ length: entries }, (_, i) => entrySize)); // Verify getting the first layout entry. let layoutEntry = layoutManager.getLayoutEntry(0); @@ -270,7 +432,7 @@ suite('LayoutManager', () => { assert.strictEqual(layoutEntry.size, entrySize); // Add a layout override. - layoutManager.setLayoutOverride(0, entrySize * 2); + layoutManager.setSizeOverride(0, entrySize * 2); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); @@ -289,7 +451,7 @@ suite('LayoutManager', () => { } // Clear the layout override. - layoutManager.clearLayoutOverride(0); + layoutManager.clearSizeOverride(0); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); @@ -313,62 +475,62 @@ suite('LayoutManager', () => { */ const verifyGetLayoutEntryOfRandomlySizedEntries = (entries: number) => { // Create the layout manager. - const layoutManager = new LayoutManager(0); - const layoutEntries = Array.from({ length: entries }, (_, i) => - getRandomIntInclusive(1, 4096) + const layoutManager = new LayoutManager(50); + const entrySizes = Array.from({ length: entries }, (_, i) => + getRandomIntInclusive(1, 400) ); - layoutManager.setLayoutEntries(layoutEntries); - const size = layoutEntries.reduce((size, randomSize) => size + randomSize, 0); + layoutManager.setEntries(entries, entrySizes); + const size = entrySizes.reduce((size, randomSize) => size + randomSize, 0); // Verify getting the first layout entry. let layoutEntry = layoutManager.getLayoutEntry(0); assert(layoutEntry); assert.strictEqual(layoutEntry.index, 0); assert.strictEqual(layoutEntry.start, 0); - assert.strictEqual(layoutEntry.size, layoutEntries[0]); + assert.strictEqual(layoutEntry.size, entrySizes[0]); // Verify getting the last layout entry. layoutEntry = layoutManager.getLayoutEntry(entries - 1); assert(layoutEntry); assert.strictEqual(layoutEntry.index, entries - 1); - assert.strictEqual(layoutEntry.start, size - layoutEntries[entries - 1]); - assert.strictEqual(layoutEntry.size, layoutEntries[entries - 1]); + assert.strictEqual(layoutEntry.start, size - entrySizes[entries - 1]); + assert.strictEqual(layoutEntry.size, entrySizes[entries - 1]); // Add a layout override. - layoutManager.setLayoutOverride(0, layoutEntries[0] * 2); + layoutManager.setSizeOverride(0, entrySizes[0] * 2); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); assert(layoutEntry); assert.strictEqual(layoutEntry.index, 0); assert.strictEqual(layoutEntry.start, 0); - assert.strictEqual(layoutEntry.size, layoutEntries[0] * 2); + assert.strictEqual(layoutEntry.size, entrySizes[0] * 2); // Verify getting the last layout entry. if (entries > 1) { layoutEntry = layoutManager.getLayoutEntry(entries - 1); assert(layoutEntry); assert.strictEqual(layoutEntry.index, entries - 1); - assert.strictEqual(layoutEntry.start, size + layoutEntries[0] - layoutEntries[entries - 1]); - assert.strictEqual(layoutEntry.size, layoutEntries[entries - 1]); + assert.strictEqual(layoutEntry.start, size + entrySizes[0] - entrySizes[entries - 1]); + assert.strictEqual(layoutEntry.size, entrySizes[entries - 1]); } // Add a layout override. - layoutManager.clearLayoutOverride(0); + layoutManager.clearSizeOverride(0); // Verify getting the first layout entry. layoutEntry = layoutManager.getLayoutEntry(0); assert(layoutEntry); assert.strictEqual(layoutEntry.index, 0); assert.strictEqual(layoutEntry.start, 0); - assert.strictEqual(layoutEntry.size, layoutEntries[0]); + assert.strictEqual(layoutEntry.size, entrySizes[0]); // Verify getting the last layout entry. layoutEntry = layoutManager.getLayoutEntry(entries - 1); assert(layoutEntry); assert.strictEqual(layoutEntry.index, entries - 1); - assert.strictEqual(layoutEntry.start, size - layoutEntries[entries - 1]); - assert.strictEqual(layoutEntry.size, layoutEntries[entries - 1]); + assert.strictEqual(layoutEntry.start, size - entrySizes[entries - 1]); + assert.strictEqual(layoutEntry.size, entrySizes[entries - 1]); }; /** @@ -379,14 +541,14 @@ suite('LayoutManager', () => { const verifyDefaultSizedEntries = (defaultSize: number, entries: number) => { // Create the layout manager. const layoutManager = new LayoutManager(defaultSize); - layoutManager.setLayoutEntries(entries); + layoutManager.setEntries(entries); // Verify that every entry is correct. for (let entry = 0; entry < entries; entry++) { // Verify that every offset for every entry is correct. for (let offset = 0; offset < defaultSize; offset++) { const start = defaultSize * entry; - const layoutEntry = layoutManager.findLayoutEntry(start + offset); + const layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + offset); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, entry); assert.strictEqual(layoutEntry!.start, start); @@ -416,25 +578,25 @@ suite('LayoutManager', () => { // Create the layout manager. const layoutManager = new LayoutManager(defaultSize); - layoutManager.setLayoutEntries(entries); + layoutManager.setEntries(entries); // Add bogus layout overrides. - layoutManager.setLayoutOverride(100.1, 1); - layoutManager.setLayoutOverride(1, 100.1); - layoutManager.setLayoutOverride(-1, 1); - layoutManager.setLayoutOverride(1, -1); + layoutManager.setSizeOverride(100.1, 1); + layoutManager.setSizeOverride(1, 100.1); + layoutManager.setSizeOverride(-1, 1); + layoutManager.setSizeOverride(1, -1); // Add the layout overrides in reverse order for better coverage. for (let i = overrideEntries - 1; i >= 0; i--) { - layoutManager.setLayoutOverride(overridesStartAt + i, overrideSize); + layoutManager.setSizeOverride(overridesStartAt + i, overrideSize); } // Add a layout override beyond the end for coverage. - layoutManager.setLayoutOverride(entries, overrideSize); + layoutManager.setSizeOverride(entries, overrideSize); // Add and remove a layout override. - layoutManager.setLayoutOverride(1, 1); - layoutManager.clearLayoutOverride(1); + layoutManager.setSizeOverride(1, 1); + layoutManager.clearSizeOverride(1); /** * Verifies a layout entry before the overrides. @@ -445,14 +607,14 @@ suite('LayoutManager', () => { assert(index < overridesStartAt); // Verify the layout entry. - let layoutEntry = layoutManager.findLayoutEntry(defaultSize * index); + let layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(defaultSize * index); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, index); assert.strictEqual(layoutEntry!.start, defaultSize * index); assert.strictEqual(layoutEntry!.size, defaultSize); // Verify the layout entry. - layoutEntry = layoutManager.findLayoutEntry( + layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry( (defaultSize * index) + Math.floor(defaultSize / 2) ); assert(layoutEntry); @@ -483,14 +645,14 @@ suite('LayoutManager', () => { // Verify the layout entry. const start = startingOffset + (testIndex * overrideSize); - let layoutEntry = layoutManager.findLayoutEntry(start); + let layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, index); assert.strictEqual(layoutEntry!.start, start); assert.strictEqual(layoutEntry!.size, overrideSize); // Verify the layout entry. - layoutEntry = layoutManager.findLayoutEntry(start + Math.floor(overrideSize / 2)); + layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + Math.floor(overrideSize / 2)); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, index); assert.strictEqual(layoutEntry!.start, start); @@ -519,14 +681,14 @@ suite('LayoutManager', () => { // Verify the layout entry. const start = startingOffset + (defaultSize * testIndex); - let layoutEntry = layoutManager.findLayoutEntry(start); + let layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, index); assert.strictEqual(layoutEntry!.start, start); assert.strictEqual(layoutEntry!.size, defaultSize); // Verify the layout entry. - layoutEntry = layoutManager.findLayoutEntry(start + Math.floor(defaultSize / 2)); + layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + Math.floor(defaultSize / 2)); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, index); assert.strictEqual(layoutEntry!.start, start); @@ -543,8 +705,8 @@ suite('LayoutManager', () => { verifyLayoutEntryAfterOverrides(entries - overridesStartAt - overrideEntries - 1); // Verify finding a layout entry that should not be found. - assert(!layoutManager.findLayoutEntry(Number.MAX_SAFE_INTEGER)); - assert(!layoutManager.findLayoutEntry(Number.MIN_SAFE_INTEGER)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry(Number.MAX_SAFE_INTEGER)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry(Number.MIN_SAFE_INTEGER)); // Verify getting the layout entry past the last index, for coverage. assert(!layoutManager.getLayoutEntry(entries)); @@ -557,15 +719,15 @@ suite('LayoutManager', () => { */ const verifyFixedSizedPredefinedEntries = (entrySize: number, entries: number) => { // Create the layout manager. - const layoutManager = new LayoutManager(0); - layoutManager.setLayoutEntries(Array.from({ length: entries }, (_, i) => entrySize)); + const layoutManager = new LayoutManager(entrySize); + layoutManager.setEntries(entries, Array.from({ length: entries }, (_, i) => entrySize)); // Verify that every entry is correct. for (let entry = 0; entry < entries; entry++) { // Verify that every offset for every entry is correct. for (let offset = 0; offset < entrySize; offset++) { const start = entry * entrySize; - const layoutEntry = layoutManager.findLayoutEntry(start + offset); + const layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + offset); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, entry); assert.strictEqual(layoutEntry!.start, start); @@ -581,8 +743,8 @@ suite('LayoutManager', () => { { index: 0, start: 0, - defaultSize: entrySize, - overrideSize: undefined + size: entrySize, + end: entrySize } ); assert.deepEqual( @@ -590,20 +752,20 @@ suite('LayoutManager', () => { { index: entries - 1, start: (entries - 1) * entrySize, - defaultSize: entrySize, - overrideSize: undefined + size: entrySize, + end: entries * entrySize } ); // Override the first entry. const layoutOverride = Math.ceil(entrySize / 2); - layoutManager.setLayoutOverride(0, layoutOverride); + layoutManager.setSizeOverride(0, layoutOverride); // Verify entries that should not be found. verifyEntriesThatShouldNotBeFound(layoutManager, entries, entrySize); // Verify the size. - assert.strictEqual(layoutManager.size, (entrySize * (entries - 1)) + layoutOverride); + assert.strictEqual(layoutManager.unpinnedLayoutEntriesSize, (entrySize * (entries - 1)) + layoutOverride); }; /** @@ -612,20 +774,20 @@ suite('LayoutManager', () => { */ const verifyRandomlySizedPredefinedEntries = (entries: number) => { // Create the layout manager. - const layoutManager = new LayoutManager(0); - const layoutEntries = Array.from({ length: entries }, (_, i) => - getRandomIntInclusive(1, 100) + const layoutManager = new LayoutManager(getRandomIntInclusive(20, 400)); + const entrySizes = Array.from({ length: entries }, (_, i) => + getRandomIntInclusive(1, 400) ); - layoutManager.setLayoutEntries(layoutEntries); + layoutManager.setEntries(entries, entrySizes); // Verify that every entry is correct. for (let entry = 0, start = 0; entry < entries; entry++) { // Get the size of the entry. - const size = layoutEntries[entry]; + const size = entrySizes[entry]; // Verify that every offset for every entry is correct. for (let offset = 0; offset < size; offset++) { - const layoutEntry = layoutManager.findLayoutEntry(start + offset); + const layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + offset); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, entry); assert.strictEqual(layoutEntry!.start, start); @@ -637,16 +799,16 @@ suite('LayoutManager', () => { } // Override the first entry. - layoutManager.setLayoutOverride(0, 10); + layoutManager.setSizeOverride(0, 10); // Verify that every entry is correct. for (let entry = 0, start = 0; entry < entries; entry++) { // Get the size of the entry. - const size = !entry ? 10 : layoutEntries[entry]; + const size = !entry ? 10 : entrySizes[entry]; // Verify that every offset for every entry is correct. for (let offset = 0; offset < size; offset++) { - const layoutEntry = layoutManager.findLayoutEntry(start + offset); + const layoutEntry = layoutManager.findFirstUnpinnedLayoutEntry(start + offset); assert(layoutEntry); assert.strictEqual(layoutEntry!.index, entry); assert.strictEqual(layoutEntry!.start, start); @@ -670,10 +832,10 @@ suite('LayoutManager', () => { size: number ) => { // Verify that entries outside the range are not found. - assert(!layoutManager.findLayoutEntry(-1)); - assert(!layoutManager.findLayoutEntry(entries * size)); - assert(!layoutManager.findLayoutEntry((entries * size) + 100)); - assert(!layoutManager.findLayoutEntry((entries * size) + 1000)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry(-1)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry(entries * size)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry((entries * size) + 100)); + assert(!layoutManager.findFirstUnpinnedLayoutEntry((entries * size) + 1000)); }; /** @@ -685,6 +847,30 @@ suite('LayoutManager', () => { return Math.floor(Math.random() * (max - min + 1)) + min; }; + /** + * Tests mapping a position to an index. + * @param layoutManager The layout manager. + * @param position The position to test. + * @param expectedIndex The expected index. + */ + const testMapPositionToIndex = (layoutManager: LayoutManager, position: number, expectedIndex: number) => { + const index = layoutManager.mapPositionToIndex(position); + assert(index !== undefined); + assert.strictEqual(index, expectedIndex); + }; + + /** + * Tests mapping an index to a position. + * @param layoutManager The layout manager. + * @param index The index to test. + * @param expectedPosition The expected position. + */ + const testMapIndexToPosition = (layoutManager: LayoutManager, index: number, expectedPosition: number) => { + const position = layoutManager.mapIndexToPosition(index); + assert(position !== undefined); + assert.strictEqual(position, expectedPosition); + }; + // Ensure that all disposables are cleaned up. ensureNoDisposablesAreLeakedInTestSuite(); }); diff --git a/test/e2e/pages/dataExplorer.ts b/test/e2e/pages/dataExplorer.ts index 5e99297adc1c..f6f069c57bb6 100644 --- a/test/e2e/pages/dataExplorer.ts +++ b/test/e2e/pages/dataExplorer.ts @@ -1,5 +1,5 @@ /*--------------------------------------------------------------------------------------------- - * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved. * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. *--------------------------------------------------------------------------------------------*/ @@ -9,7 +9,7 @@ import { Workbench } from '../infra/workbench'; const COLUMN_HEADERS = '.data-explorer-panel .right-column .data-grid-column-headers'; const HEADER_TITLES = '.data-grid-column-header .title-description .title'; -const DATA_GRID_ROWS = '.data-explorer-panel .right-column .data-grid-rows'; +const DATA_GRID_ROWS = '.data-explorer-panel .right-column .data-grid-rows-container'; const DATA_GRID_ROW = '.data-grid-row'; const SCROLLBAR_LOWER_RIGHT_CORNER = '.data-grid-scrollbar-corner';