diff --git a/src/sql/base/browser/ui/table/plugins/autoSizeColumns.plugin.ts b/src/sql/base/browser/ui/table/plugins/autoSizeColumns.plugin.ts index 4b1b171b7f..6c6e3fd390 100644 --- a/src/sql/base/browser/ui/table/plugins/autoSizeColumns.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/autoSizeColumns.plugin.ts @@ -14,6 +14,10 @@ const defaultOptions: IAutoColumnSizeOptions = { autoSizeOnRender: false }; +// Set the max number of rows to scan to 10 since the result grid viewport in query editor is usually around 10 rows but can go up to 20 rows for notebooks. +// In most cases, 10 rows are enough to get a reasonable width and will cut down measuring costs for large notebooks. +const MAX_ROWS_TO_SCAN = 10; + export class AutoColumnSize implements Slick.Plugin { private _grid!: Slick.Grid; private _$container!: JQuery; @@ -78,20 +82,33 @@ export class AutoColumnSize implements Slick.Plugin[] = []; + let colIndices: number[] = []; + for (let i = 0; i <= headerColumns.children.length; i++) { let headerEl = jQuery(headerColumns.children.item(i)!); let columnDef = headerEl.data('column'); if (columnDef) { - let headerWidth = this.getElementWidth(headerEl[0]); - let colIndex = this._grid.getColumnIndex(columnDef.id); - let column = allColumns[colIndex]; - let autoSizeWidth = Math.max(headerWidth, this.getMaxColumnTextWidth(columnDef, colIndex)) + 1; - if (autoSizeWidth !== column.width) { - allColumns[colIndex].width = autoSizeWidth; - change = true; - } + headerElements.push(headerEl[0]); + columnDefs.push(columnDef); + colIndices.push(this._grid.getColumnIndex(columnDef.id)); } } + + let headerWidths: number[] = this.getElementWidths(headerElements); + let maxColumnTextWidths: number[] = this.getMaxColumnTextWidths(columnDefs, colIndices); + + for (let i = 0; i < columnDefs.length; i++) { + let colIndex: number = colIndices[i]; + let column: Slick.Column = allColumns[colIndex]; + let autoSizeWidth: number = Math.max(headerWidths[i], maxColumnTextWidths[i]) + 1; + if (autoSizeWidth !== column.width) { + allColumns[colIndex].width = autoSizeWidth; + change = true; + } + } + if (change) { this.onPostEventHandler.unsubscribeAll(); this._grid.setColumns(allColumns); @@ -115,7 +132,7 @@ export class AutoColumnSize implements Slick.Plugin) { - let headerWidth = this.getElementWidth(headerEl[0]); + let headerWidth = this.getElementWidths([headerEl[0]])[0]; let colIndex = this._grid.getColumnIndex(columnDef.id!); let origCols = this._grid.getColumns(); let allColumns = deepClone(origCols); @@ -134,27 +151,69 @@ export class AutoColumnSize implements Slick.Plugin[], colIndices: number[]): number[] { + let data = this._grid.getData() as Slick.DataProvider; + let viewPort = this._grid.getViewport(); + let start = Math.max(0, viewPort.top); + let end = Math.min(data.getLength(), MAX_ROWS_TO_SCAN); + let allTexts: Array[] = []; + let rowElements: JQuery[] = []; + + columnDefs.forEach((columnDef) => { + let texts: Array = []; + for (let i = start; i < end; i++) { + texts.push(data.getItem(i)[columnDef.field!]); + } + allTexts.push(texts); + let rowEl = this.createRow(); + rowElements.push(rowEl); + }); + + let templates = this.getMaxTextTemplates(allTexts, columnDefs, colIndices, data, rowElements); + + let widths = this.getTemplateWidths(rowElements, templates); + + return widths.map((width) => Math.min(this._options.maxWidth, width)); + } + private getMaxColumnTextWidth(columnDef: Slick.Column, colIndex: number): number { let texts: Array = []; let rowEl = this.createRow(); let data = this._grid.getData() as Slick.DataProvider; let viewPort = this._grid.getViewport(); let start = Math.max(0, viewPort.top); - let end = Math.min(data.getLength(), viewPort.bottom); + let end = Math.min(data.getLength(), MAX_ROWS_TO_SCAN); for (let i = start; i < end; i++) { texts.push(data.getItem(i)[columnDef.field!]); } let template = this.getMaxTextTemplate(texts, columnDef, colIndex, data, rowEl); - let width = this.getTemplateWidth(rowEl, template); + let width = this.getTemplateWidths([rowEl], [template])[0]; this.deleteRow(rowEl); return width > this._options.maxWidth! ? this._options.maxWidth! : width; } - private getTemplateWidth(rowEl: JQuery, template: JQuery | HTMLElement | string): number { - let cell = jQuery(rowEl.find('.slick-cell')); - cell.append(template); - jQuery(cell).find('*').css('position', 'relative'); - return cell.outerWidth() + 1; + private getTemplateWidths(rowElements: JQuery[], templates: (JQuery | HTMLElement | string)[]): number[] { + // Write all changes first then read all widths to prevent layout thrashing + // (https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing) + const cells: JQuery[] = templates.map((template, index) => { + let rowEl = rowElements[index]; + let cell = jQuery(rowEl.find('.slick-cell')); + cell.append(template); + jQuery(cell).find('*').css('position', 'relative'); + return cell; + }); + + return cells.map(cell => cell.outerWidth() + 1); + } + + private getMaxTextTemplates(allTexts: string[][], columnDefs: Slick.Column[], colIndices: number[], data: Slick.DataProvider, rowElements: JQuery[]): (JQuery | HTMLElement | string)[] { + return columnDefs.map((columnDef, index) => this.getMaxTextTemplate(allTexts[index], columnDef, colIndices[index], data, rowElements[index])); } private getMaxTextTemplate(texts: string[], columnDef: Slick.Column, colIndex: number, data: Slick.DataProvider, rowEl: JQuery): JQuery | HTMLElement | string { @@ -192,13 +251,28 @@ export class AutoColumnSize implements Slick.Plugin { + let clone = element.cloneNode(true) as HTMLElement; + clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;'; + element.parentNode!.insertBefore(clone, element); + clones.push(clone); + }); + + clones.forEach(clone => { + widths.push(clone.offsetWidth); + }); + + clones.forEach(clone => { + clone.parentNode!.removeChild(clone); + }); + + return widths; } private getElementWidthUsingCanvas(element: JQuery, text: string): number {