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 ea27bb2f164e..127eba4a3996 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 @@ -18,9 +18,15 @@ import { DataExplorerClientInstance } from '../../../../../../../services/langua * Constants. */ const ROW_HEIGHT = 26; +const OVERSCAN_FACTOR = 3 /** * ColumnSelectorDataGridInstance class. + * + * This class is used to display a list of the column names from a dataset + * in the column selector modal popup. The column selector modal popup is + * a DataGrid component. This instance manages the list of columns and + * supports searching for columns by name. */ export class ColumnSelectorDataGridInstance extends DataGridInstance { //#region Private Properties @@ -117,48 +123,39 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { ); // Set the column layout entries. There is always one column. + // The single column contains all the column names. this._columnLayoutManager.setEntries(1); // Set the row layout entries. this._rowLayoutManager.setEntries(backendState.table_shape.num_columns); - /** - * Updates the data grid instance. - * @param backendState The backend state, if known; otherwise, undefined. - */ - const updateDataGridInstance = async (backendState?: BackendState) => { - // Get the backend state, if it was not supplied. - if (!backendState) { - backendState = await this._dataExplorerClientInstance.getBackendState(); - } - - // Update the backend state. - this._backendState = backendState; - - // Set the layout entries in the row layout manager. - this._rowLayoutManager.setEntries(backendState.table_shape.num_columns); + // Add the onDidSchemaUpdate event handler. + this._register(this._dataExplorerClientInstance.onDidSchemaUpdate(async () => { + // Update the layout entries. + await this.updateLayoutEntries() - // Scroll to the top. - await this.setScrollOffsets(0, 0); - }; + // Perform a soft reset. + this.softReset(); - // Add the onDidSchemaUpdate event handler. - this._register(this._dataExplorerClientInstance.onDidSchemaUpdate(async () => - // Update the data grid instance. - updateDataGridInstance() - )); + // Fetch data. + await this.fetchData(true); + })); // Add the onDidDataUpdate event handler. - this._register(this._dataExplorerClientInstance.onDidDataUpdate(async () => - // Update the data grid instance. - updateDataGridInstance() - )); + this._register(this._dataExplorerClientInstance.onDidDataUpdate(async () => { + // Update the layout entries. + await this.updateLayoutEntries() + + // Fetch data. + await this.fetchData(true); + })); // Add the onDidUpdateBackendState event handler. - this._register(this._dataExplorerClientInstance.onDidUpdateBackendState(async backendState => + this._register(this._dataExplorerClientInstance.onDidUpdateBackendState(async backendState => { // Update the data grid instance. - updateDataGridInstance(backendState) - )); + await this.updateLayoutEntries(backendState); + await this.fetchData(true); + })); // Add the onDidUpdateCache event handler. this._register(this._columnSchemaCache.onDidUpdateCache(() => @@ -209,16 +206,20 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { /** * Fetches data. + * @param invalidateCache A value which indicates whether to invalidate the cache. * @returns A Promise that resolves when the operation is complete. */ - override async fetchData() { + override async fetchData(invalidateCache?: boolean) { const rowDescriptor = this.firstRow; if (rowDescriptor) { - await this._columnSchemaCache.update({ - searchText: this._searchText, - firstColumnIndex: rowDescriptor.rowIndex, - visibleColumns: this.screenRows - }); + // Get the layout indices for visible data. + const columnIndices = this._rowLayoutManager.getLayoutIndexes(this.verticalScrollOffset, this.layoutHeight, OVERSCAN_FACTOR); + if (columnIndices.length > 0 || invalidateCache) { + await this._columnSchemaCache.update({ + columnIndices, + invalidateCache: !!invalidateCache + }); + } } } @@ -232,10 +233,23 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { return columnIndex === 0 ? this.layoutWidth - 8 : undefined; } + /** + * Select the column schema at the visual index provided. + * @param rowIndex The row index (visual positional) of the selected item. + */ selectItem(rowIndex: number): void { - // Get the column schema for the row index. - const columnSchema = this._columnSchemaCache.getColumnSchema(rowIndex); - if (!columnSchema) { return; } + // The row index is the visible row index, so we need to map it to the actual index. + // For example, if the user has searched for a column name, the visible row index + // may not match the actual index in the dataset. + const index = this._rowLayoutManager.mapPositionToIndex(rowIndex); + if (index === undefined) { + return; + } + // Get the column schema using the actual index in the dataset + const columnSchema = this._columnSchemaCache.getColumnSchema(index); + if (!columnSchema) { + return; + } this._onDidSelectColumnEmitter.fire(columnSchema); } @@ -243,7 +257,7 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { /** * Gets a cell. * @param columnIndex The column index. - * @param rowIndex The row index. + * @param rowIndex The row index from the original dataset. * @returns The cell. */ cell(columnIndex: number, rowIndex: number): JSX.Element | undefined { @@ -252,16 +266,19 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { return undefined; } - // Get the column schema for the row index. + // Get the column schema for the row index from the original dataset. const columnSchema = this._columnSchemaCache.getColumnSchema(rowIndex); if (!columnSchema) { return undefined; } + // Get the visual position for the row index from the original dataset. + const visualPosition = this._rowLayoutManager.mapIndexToPosition(rowIndex); + // Return the cell. return ( this._onDidSelectColumnEmitter.fire(columnSchema)} @@ -290,16 +307,79 @@ export class ColumnSelectorDataGridInstance extends DataGridInstance { // Set the search text and fetch data. this._searchText = searchText; - await this.fetchData(); + await this.updateLayoutEntries(); + // Always invalidate the cache when search text changes, + // so the layout manager and cache are in sync. + await this.fetchData(true); - // select the first available row after fetching so that users cat hit "enter" + // select the first available row after fetching so that users can hit "enter" // to make an immediate confirmation on what they were searching for if (this.rows > 0) { this.showCursor(); this.setCursorRow(0); } + + // Force a re-render when the search changes + this.fireOnDidUpdateEvent(); } } //#endregion Public Methods + + //#region Private Methods + + /** + * Updates the layout entries to render. + * @param backendState The backend state, if known; otherwise, undefined. + */ + private async updateLayoutEntries(backendState?: BackendState) { + if (!this._searchText) { + // Get the backend state, if it was not supplied. + if (!backendState) { + backendState = await this._dataExplorerClientInstance.getBackendState(); + } + this._rowLayoutManager.setEntries(backendState.table_shape.num_columns); + } else { + const searchResults = await this._dataExplorerClientInstance.searchSchema2({ + searchText: this._searchText, + }); + this._rowLayoutManager.setEntries(searchResults.matches.length, undefined, searchResults.matches); + } + } + + //#endregion Private Methods + + /** + * Moves the cursor down. + * Override to work with visual positions instead of data indices. + */ + override moveCursorDown() { + // Calculate the next visual position + const nextRowIndex = this.cursorRowIndex + 1; + // Check if we're at the last row + if (nextRowIndex >= this.rows) { + return; + } + // Set the cursor row index to the next visual position + this.setCursorRow(nextRowIndex); + // Scroll to the cursor + this.scrollToCursor(); + } + + /** + * Moves the cursor up. + * Override to work with visual positions instead of data indices. + */ + override moveCursorUp() { + // Calculate the previous visual position + const prevRowIndex = this.cursorRowIndex - 1; + // Check if we're at the first row + if (prevRowIndex < 0) { + return; + } + // Set the cursor row index to the previous visual position + this.setCursorRow(prevRowIndex); + // Scroll to the cursor + this.scrollToCursor(); + } } 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 f3d21b0f65e5..fd6a16b0a3ea 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 @@ -113,7 +113,7 @@ export const ColumnSelectorModalPopup = (props: ColumnSelectorModalPopupProps) = initialSearchText={props.searchInput} onConfirmSearch={() => { props.columnSelectorDataGridInstance.selectItem( - props.columnSelectorDataGridInstance.cursorColumnIndex + props.columnSelectorDataGridInstance.cursorRowIndex ); }} onNavigateOut={() => { diff --git a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx index 14036e2aa2e7..f8d7f4cccd4e 100644 --- a/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx +++ b/src/vs/workbench/services/positronDataExplorer/browser/tableSummaryDataGridInstance.tsx @@ -31,6 +31,10 @@ const OVERSCAN_FACTOR = 3 /** * TableSummaryDataGridInstance class. + * + * This class is used to display a summary of each column in the dataset. + * This instance manages represents a column from the dataset and displays + * summary information such as data type, null count, and summary statistics. */ export class TableSummaryDataGridInstance extends DataGridInstance { //#region Private Properties diff --git a/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts b/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts index e0b7ddf5d625..4265bf9d2359 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/columnSchemaCache.ts @@ -11,24 +11,14 @@ import { DataExplorerClientInstance } from '../../languageRuntime/common/languag /** * Constants. */ -const OVERSCAN_FACTOR = 3; - -/** - * Creates an array from an index range. - * @param startIndex The start index. - * @param endIndex The end index. - * @returns An array with the specified index range. - */ -const arrayFromIndexRange = (startIndex: number, endIndex: number) => - Array.from({ length: endIndex - startIndex + 1 }, (_, i) => startIndex + i); +const TRIM_CACHE_TIMEOUT = 3000; // 3 seconds /** * CacheUpdateDescriptor interface. */ interface CacheUpdateDescriptor { - searchText?: string; - firstColumnIndex: number; - visibleColumns: number; + columnIndices: number[]; + invalidateCache: boolean; } /** @@ -45,12 +35,12 @@ export class ColumnSchemaCache extends Disposable { /** * Gets or sets the cache update descriptor. */ - private _cacheUpdateDescriptor?: CacheUpdateDescriptor; + private _pendingCacheUpdateDescriptor?: CacheUpdateDescriptor; /** - * The search text. + * Gets or sets the trim cache timeout. */ - private _searchText?: string; + private _trimCacheTimeout?: Timeout; /** * Gets or sets the columns. @@ -86,6 +76,17 @@ export class ColumnSchemaCache extends Disposable { )); } + /** + * Dispose method + */ + override dispose(): void { + // Clear the pending trim cache timeout + this.clearTrimCacheTimeout(); + + // Call the base class's dispose method. + super.dispose(); + } + //#endregion Constructor & Dispose //#region Public Properties @@ -111,160 +112,132 @@ export class ColumnSchemaCache extends Disposable { //#region Public Methods /** - * Updates the cache. - * @param cacheUpdateDescriptor The cache update descriptor. + * Updates the cache with the specified column indices. + * @param param0 The column indices. * @returns A Promise that resolves when the update is complete. */ async update(cacheUpdateDescriptor: CacheUpdateDescriptor): Promise { - // Update the cache. - await this.doUpdateCache(cacheUpdateDescriptor); + // Clear the trim cache timeout. + this.clearTrimCacheTimeout(); - // Fire the onDidUpdateCache event. - this._onDidUpdateCacheEmitter.fire(); - } - - /** - * Gets the column schema for the specified column index. - * @param columnIndex The column index. - * @returns The column schema for the specified column index. - */ - getColumnSchema(columnIndex: number) { - return this._columnSchemaCache.get(columnIndex); - } - - //#endregion Public Methods - - //#region Private Methods + // If there are no column indices, return. + if (cacheUpdateDescriptor.columnIndices.length === 0 && !cacheUpdateDescriptor.invalidateCache) { + return; + } - /** - * Updates the cache. - * @param cacheUpdateDescriptor The cache update descriptor. - */ - private async doUpdateCache(cacheUpdateDescriptor: CacheUpdateDescriptor): Promise { // If a cache update is already in progress, set the pending cache update descriptor and // return. This allows cache updates that are happening in rapid succession to overwrite one // another so that only the last one gets processed. (For example, this happens when a user // drags a scrollbar rapidly.) if (this._updatingCache) { - this._cacheUpdateDescriptor = cacheUpdateDescriptor; + this._pendingCacheUpdateDescriptor = cacheUpdateDescriptor; return; } // Set the updating cache flag. this._updatingCache = true; - // Destructure the cache update descriptor. - const { - searchText, - firstColumnIndex, - visibleColumns, - } = cacheUpdateDescriptor; - - // If the search text has changed, clear the column schema cache. - if (searchText !== this._searchText) { - this._columnSchemaCache.clear(); - } - - this._searchText = searchText; - - // // Get the size of the data. + // Get the size of the data. 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 = Math.min( - startColumnIndex + visibleColumns + (visibleColumns * OVERSCAN_FACTOR), - this._columns - 1 - ); - - // Build an array of the column indices to cache. - const columnIndices = arrayFromIndexRange(startColumnIndex, endColumnIndex); - - // Build an array of the column schema indices that need to be cached. - const columnSchemaIndices = columnIndices.filter(columnIndex => - !this._columnSchemaCache.has(columnIndex) - ); - - // 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]); - } + // Set the column indices of the column schema we need to load. + let columnIndices: number[]; + if (cacheUpdateDescriptor.invalidateCache) { + columnIndices = cacheUpdateDescriptor.columnIndices; } 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]); + columnIndices = []; + for (const index of cacheUpdateDescriptor.columnIndices) { + if (!this._columnSchemaCache.has(index)) { + columnIndices.push(index); } - - // 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. - 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]); - } + // Load the column schema. + const tableSchema = await this._dataExplorerClientInstance.getSchema(columnIndices); - // Update the cache updated flag. - cacheUpdated = true; + // Invalidate the cache, if we're supposed to. + if (cacheUpdateDescriptor.invalidateCache) { + this._columnSchemaCache.clear(); } - // If the cache was updated, fire the onDidUpdateCache event. - if (cacheUpdated) { - this._onDidUpdateCacheEmitter.fire(); + // Cache the column schema that was returned. + for (const columnSchema of tableSchema.columns) { + this._columnSchemaCache.set(columnSchema.column_index, columnSchema); } + // Fire the onDidUpdateCache event. + this._onDidUpdateCacheEmitter.fire(); + // Clear the updating cache flag. this._updatingCache = false; // If there is a pending cache update descriptor, update the cache for it. - if (this._cacheUpdateDescriptor) { + if (this._pendingCacheUpdateDescriptor) { // Get the pending cache update descriptor and clear it. - const pendingCacheUpdateDescriptor = this._cacheUpdateDescriptor; - this._cacheUpdateDescriptor = undefined; + const pendingCacheUpdateDescriptor = this._pendingCacheUpdateDescriptor; + this._pendingCacheUpdateDescriptor = undefined; // Update the cache for the pending cache update descriptor. - await this.doUpdateCache(pendingCacheUpdateDescriptor); + await this.update(pendingCacheUpdateDescriptor); + } + + // Schedule trimming the cache if we didn't already invalidate the cache and we have + // column indices to keep. We don't want to schedule a trim if columnIndices is empty + // which can happen during UI rendering transitions (e.g.during resizing when layoutHeight + // is 0) because that would clear all cached data. + if (!cacheUpdateDescriptor.invalidateCache && cacheUpdateDescriptor.columnIndices.length) { + // Clear previously scheduled trim calls before scheduling a new one + // to prevent previously scheduled trim calls from clearing data that + // is now visible and should be in the cache. This can happen when a + // user is scrolling rapidly. + this.clearTrimCacheTimeout(); + + // Set the trim cache timeout. + this._trimCacheTimeout = setTimeout(() => { + // Release the trim cache timeout. + this._trimCacheTimeout = undefined; + // Trim the cache. + this.trimCache(new Set(cacheUpdateDescriptor.columnIndices)); + }, TRIM_CACHE_TIMEOUT); + } + } + + /** + * Gets the column schema for the specified column index. + * @param columnIndex The column index. + * @returns The column schema for the specified column index. + */ + getColumnSchema(columnIndex: number) { + return this._columnSchemaCache.get(columnIndex); + } + + //#endregion Public Methods + + //#region Private Methods + + /** + * Clears the trim cache timeout. + */ + private clearTrimCacheTimeout() { + // If there is a trim cache timeout scheduled, clear it. + if (this._trimCacheTimeout) { + clearTimeout(this._trimCacheTimeout); + this._trimCacheTimeout = undefined; + } + } + + /** + * Trims the data in the cache if the key is not in the provided list. + * @param columnIndicesToKeep The array of column indices to keep in the cache. + */ + private trimCache(columnIndices: Set) { + // Trim the column schema cache. + for (const columnIndex of this._columnSchemaCache.keys()) { + if (!columnIndices.has(columnIndex)) { + this._columnSchemaCache.delete(columnIndex); + } } } diff --git a/src/vs/workbench/services/positronDataExplorer/common/tableSummaryCache.ts b/src/vs/workbench/services/positronDataExplorer/common/tableSummaryCache.ts index 99c384eb7a27..653ba67dc213 100644 --- a/src/vs/workbench/services/positronDataExplorer/common/tableSummaryCache.ts +++ b/src/vs/workbench/services/positronDataExplorer/common/tableSummaryCache.ts @@ -260,16 +260,22 @@ export class TableSummaryCache extends Disposable { return this.update(pendingUpdateDescriptor); } - // Schedule trimming the cache if we have actual column indices to preserve. - // This prevents accidentally clearing all cached data when columnIndices is an empty array - // which happens during UI state transitions (e.g. during resizing when layoutHeight is 0). - if (!updateDescriptor.invalidateCache && columnIndices.length) { + // Schedule trimming the cache if we didn't already invalidate the cache and we have + // column indices to keep. We don't want to schedule a trim if columnIndices is empty + // which can happen during UI rendering transitions (e.g.during resizing when layoutHeight + // is 0) because that would clear all cached data. + if (!updateDescriptor.invalidateCache && updateDescriptor.columnIndices.length) { + // Clear previously scheduled trim calls before scheduling a new one + // to prevent previously scheduled trim calls from clearing data that + // is now visible and should be in the cache. This can happen when a + // user is scrolling rapidly. + this.clearTrimCacheTimeout(); // Set the trim cache timeout. this._trimCacheTimeout = setTimeout(() => { // Release the trim cache timeout. this._trimCacheTimeout = undefined; // Trim the cache. - this.trimCache(new Set(columnIndices)); + this.trimCache(new Set(updateDescriptor.columnIndices)); }, TRIM_CACHE_TIMEOUT); } }