diff --git a/src/sql/base/browser/ui/scrollableView/scrollableView.ts b/src/sql/base/browser/ui/scrollableView/scrollableView.ts index d5096a433f..f5c3eb7800 100644 --- a/src/sql/base/browser/ui/scrollableView/scrollableView.ts +++ b/src/sql/base/browser/ui/scrollableView/scrollableView.ts @@ -21,11 +21,13 @@ export interface IScrollableViewOptions { smoothScrolling?: boolean; verticalScrollMode?: ScrollbarVisibility; additionalScrollHeight?: number; + scrollDebouce?: number; } const DefaultOptions: IScrollableViewOptions = { useShadows: true, - verticalScrollMode: ScrollbarVisibility.Auto + verticalScrollMode: ScrollbarVisibility.Auto, + scrollDebouce: 25 }; export interface IView { @@ -83,7 +85,7 @@ export class ScrollableView extends Disposable { this.domNode.appendChild(this.scrollableElement.getDomNode()); container.appendChild(this.domNode); - this._register(Event.debounce(this.scrollableElement.onScroll, (l, e) => e, 25)(this.onScroll, this)); + this._register(Event.debounce(this.scrollableElement.onScroll, (l, e) => e, getOrDefault(options, o => o.scrollDebouce, DefaultOptions.scrollDebouce))(this.onScroll, this)); // Prevent the monaco-scrollable-element from scrolling // https://github.com/Microsoft/vscode/issues/44181 diff --git a/src/sql/base/browser/ui/table/highPerf/cellCache.ts b/src/sql/base/browser/ui/table/highPerf/cellCache.ts new file mode 100644 index 0000000000..38576b46f9 --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/cellCache.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from 'vs/base/common/lifecycle'; +import { ITableRenderer } from 'sql/base/browser/ui/table/highPerf/table'; + +import { $, removeClass } from 'vs/base/browser/dom'; +export interface ICell { + domNode: HTMLElement | null; + templateData: any; + templateId: string; +} + +function removeFromParent(element: HTMLElement): void { + try { + if (element.parentElement) { + element.parentElement.removeChild(element); + } + } catch (e) { + // this will throw if this happens due to a blur event, nasty business + } +} + +export class CellCache implements IDisposable { + + private cache = new Map(); + + constructor(private renderers: Map>) { } + + alloc(templateId: string): ICell { + let result = this.getTemplateCache(templateId).pop(); + + if (!result) { + const domNode = $('.monaco-perftable-cell'); + const renderer = this.getRenderer(templateId); + const templateData = renderer.renderTemplate(domNode); + result = { domNode, templateId, templateData }; + } + + return result; + } + + private getTemplateCache(templateId: string): ICell[] { + let result = this.cache.get(templateId); + + if (!result) { + result = []; + this.cache.set(templateId, result); + } + + return result; + } + + private getRenderer(templateId: string): ITableRenderer { + const renderer = this.renderers.get(templateId); + if (!renderer) { + throw new Error(`No renderer found for ${templateId}`); + } + return renderer; + } + + release(cell: ICell) { + const { domNode, templateId } = cell; + if (domNode) { + removeClass(domNode, 'scrolling'); + removeFromParent(domNode); + } + + const cache = this.getTemplateCache(templateId); + cache.push(cell); + } + + private garbageCollect(): void { + if (!this.renderers) { + return; + } + + this.cache.forEach((cachedRows, templateId) => { + for (const cachedRow of cachedRows) { + const renderer = this.getRenderer(templateId); + renderer.disposeTemplate(cachedRow.templateData); + cachedRow.domNode = null; + cachedRow.templateData = null; + } + }); + + this.cache.clear(); + } + + dispose(): void { + this.garbageCollect(); + this.cache.clear(); + this.renderers = null!; // StrictNullOverride: nulling out ok in dispose + } +} diff --git a/src/sql/workbench/contrib/query/browser/media/sidebysideEditor.css b/src/sql/base/browser/ui/table/highPerf/rowCache.ts similarity index 65% rename from src/sql/workbench/contrib/query/browser/media/sidebysideEditor.css rename to src/sql/base/browser/ui/table/highPerf/rowCache.ts index 8dc1e4e111..d45dac4c01 100644 --- a/src/sql/workbench/contrib/query/browser/media/sidebysideEditor.css +++ b/src/sql/base/browser/ui/table/highPerf/rowCache.ts @@ -3,10 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.vs-dark .side-by-side-editor > .master-editor-container { - box-shadow: -6px 0 5px -5px black; +import { IDisposable } from 'vs/base/common/lifecycle'; + + +export class RowCache implements IDisposable { + dispose(): void { + + } } -.side-by-side-editor > .master-editor-container { - box-shadow: -6px 0 5px -5px #DDD; -} \ No newline at end of file +export interface IRow { + domNode: HTMLElement | null; +} diff --git a/src/sql/base/browser/ui/table/highPerf/table.css b/src/sql/base/browser/ui/table/highPerf/table.css new file mode 100644 index 0000000000..ca16e81a90 --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/table.css @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-perftable { + position: relative; + height: 100%; + width: 100%; + white-space: nowrap; + overflow: hidden; +} + +.monaco-perftable.mouse-support { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: -moz-none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.monaco-perftable > .monaco-scrollable-element { + height: calc(100% - 22px); +} + +.monaco-perftable .monaco-perftable-cell { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + box-sizing: border-box; +} + +.monaco-perftable-rows { + position: relative; + width: 100%; + height: 100%; +} + +.monaco-perftable.horizontal-scrolling .monaco-perftable-rows { + width: auto; + min-width: 100%; +} + +.monaco-perftable-row { + position: absolute; + -moz-box-sizing: border-box; + -o-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + overflow: hidden; + width: 100%; +} + +.monaco-perftable.mouse-support .monaco-perftable-row { + cursor: pointer; + touch-action: none; +} + +/* for OS X balperftableic scrolling */ +.monaco-perftable-row.scrolling { + display: none !important; +} + +/* Focus */ +.monaco-perftable.element-focused, .monaco-perftable.selection-single, .monaco-perftable.selection-multiple { + outline: 0 !important; +} + +.monaco-perftable-header { + position: relative; + width: 100%; + overflow: hidden; +} + +.monaco-perftable-header-cell { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + position: absolute; + height: 100%; + padding-left: 3px; + box-sizing: border-box; +} + +.monaco-perftable .sash-container { + position: absolute; + width: 100%; + height: 100%; + pointer-events: none; +} + +.monaco-perftable .sash-container > .monaco-sash { + pointer-events: initial; +} + +.monaco-perftable .monaco-perftable-cell * { + text-overflow: ellipsis; + overflow: hidden; + padding-left: 3px; +} + +.monaco-perftable .monaco-perftable-cell .row-count { + text-align: center; + padding-left: 0; +} diff --git a/src/sql/base/browser/ui/table/highPerf/table.ts b/src/sql/base/browser/ui/table/highPerf/table.ts new file mode 100644 index 0000000000..4108d07c7d --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/table.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IGridRange } from 'sql/base/common/gridRange'; +import { IGridPosition } from 'sql/base/common/gridPosition'; + +export interface ITableRenderer { + renderTemplate(container: HTMLElement): TTemplateData; + renderCell(element: T, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void; + disposeCell?(element: T, index: number, cell: number, olumnId: string, templateData: TTemplateData, width: number | undefined): void; + disposeTemplate(templateData: TTemplateData): void; +} + +export interface IStaticTableRenderer extends ITableRenderer { + renderCell(element: T | undefined, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void; + disposeCell?(element: T | undefined, index: number, cell: number, columnId: string, templateData: TTemplateData, width: number | undefined): void; +} + +export class TableError extends Error { + + constructor(user: string, message: string) { + super(`TableError [${user}] ${message}`); + } +} + +export interface ITableDataSource { + getRow(index: number): Promise; +} + +export interface ITableEvent { + elements: T[]; + indexes: IGridRange[]; + browserEvent?: UIEvent; +} + +export interface ITableMouseEvent { + browserEvent: MouseEvent; + buttons: number; + element: T | undefined; + index: IGridPosition | undefined; +} + +export interface ITableContextMenuEvent { + browserEvent: UIEvent; + element: T | undefined; + index: IGridPosition | undefined; + anchor: HTMLElement | { x: number; y: number; }; +} + +export interface ITableDragEvent { + start: IGridPosition; + current: IGridPosition; +} + +export interface ITableColumn { + /** + * Renderer associated with this column + */ + renderer: ITableRenderer | IStaticTableRenderer; + /** + * Initial width of this column + */ + width?: number; + /** + * Minimum allowed width of this column + */ + minWidth?: number; + /** + * Is this column resizable? + */ + resizeable?: boolean; + /** + * This string will be added to the cell as a class + * Useful for styling specific columns + */ + cellClass?: string; + /** + * Specifies this column doesn't need data to render + * Useful when you don't need to wait for data you render a column + */ + static?: boolean; + id: string; + /** + * Name to display in the column header + */ + name: string; +} + +export interface IStaticColumn extends ITableColumn { + /** + * Renderer associated with this column + */ + renderer: IStaticTableRenderer; +} diff --git a/src/sql/base/browser/ui/table/highPerf/tableView.ts b/src/sql/base/browser/ui/table/highPerf/tableView.ts new file mode 100644 index 0000000000..0dc1383de9 --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/tableView.ts @@ -0,0 +1,727 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./table'; + +import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle'; +import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Event } from 'vs/base/common/event'; +import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions } from 'vs/base/common/scrollable'; +import * as DOM from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import { isWindows } from 'vs/base/common/platform'; +import * as browser from 'vs/base/browser/browser'; +import { Range, IRange } from 'vs/base/common/range'; +import { getOrDefault } from 'vs/base/common/objects'; +import { memoize } from 'vs/base/common/decorators'; +import { Sash, Orientation, ISashEvent as IBaseSashEvent } from 'vs/base/browser/ui/sash/sash'; +import { firstIndex } from 'vs/base/common/arrays'; + +import { CellCache, ICell } from 'sql/base/browser/ui/table/highPerf/cellCache'; +import { ITableRenderer, ITableDataSource, ITableMouseEvent, IStaticTableRenderer, ITableColumn } from 'sql/base/browser/ui/table/highPerf/table'; +import { GridPosition } from 'sql/base/common/gridPosition'; + +export interface IAriaSetProvider { + getSetSize(element: T, index: number, listLength: number): number; + getPosInSet(element: T, index: number): number; +} + +export interface ITableViewOptions { + rowHeight?: number; + mouseSupport?: boolean; + initialLength?: number; + rowCountColumn?: boolean; + headerHeight?: number; +} + +const DefaultOptions = { + rowHeight: 22, + columnWidth: 120, + minWidth: 20, + resizeable: true, + headerHeight: 22 +}; + +interface IInternalColumn extends ITableColumn { + domNode?: HTMLElement; + left?: number; +} + +interface IInternalStaticColumn extends IInternalColumn { + renderer: IStaticTableRenderer; +} + +interface ISashItem { + sash: Sash; + disposable: IDisposable; +} + +interface ISashDragState { + current: number; + index: number; + start: number; + sizes: Array; + lefts: Array; +} + +interface ISashEvent { + sash: Sash; + column: IInternalColumn; + start: number; + current: number; +} + +function removeFromParent(element: HTMLElement): void { + try { + if (element.parentElement) { + element.parentElement.removeChild(element); + } + } catch (e) { + // this will throw if this happens due to a blur event, nasty business + } +} + +interface IAsyncRowItem { + readonly id: string; + element: T | undefined; + row: HTMLElement | null; + cells: ICell[] | null; + size: number; + datapromise: CancelablePromise | null; +} + +export class TableView implements IDisposable { + private static InstanceCount = 0; + readonly domId = `table_id_${++TableView.InstanceCount}`; + + readonly domNode = DOM.$('.monaco-perftable'); + + private visibleRows: IAsyncRowItem[] = []; + private cache: CellCache; + private renderers = new Map>(); + private lastRenderTop = 0; + private lastRenderHeight = 0; + private readonly rowsContainer = DOM.$('.monaco-perftable-rows'); + private scrollableElement: ScrollableElement; + private _scrollHeight: number = 0; + private _scrollWidth: number = 0; + private scrollableElementUpdateDisposable: IDisposable | null = null; + // private ariaSetProvider: IAriaSetProvider; + private canUseTranslate3d: boolean | undefined = undefined; + public readonly rowHeight: number; + private _length: number = 0; + + private columns: IInternalColumn[]; + private staticColumns: IInternalStaticColumn[]; + private columnSashs: ISashItem[] = []; + private sashDragState?: ISashDragState; + private headerContainer!: HTMLElement; + + private scheduledRender?: IDisposable; + private bigNumberDelta = 0; + + private headerHeight: number; + + private disposables: IDisposable[]; + + get contentHeight(): number { return this.length * this.rowHeight; } + + get onDidScroll(): Event { return this.scrollableElement.onScroll; } + + private _orthogonalStartSash: Sash | undefined; + get orthogonalStartSash(): Sash | undefined { return this._orthogonalStartSash; } + set orthogonalStartSash(sash: Sash | undefined) { + for (const sashItem of this.columnSashs) { + sashItem.sash.orthogonalStartSash = sash; + } + + this._orthogonalStartSash = sash; + } + + private _orthogonalEndSash: Sash | undefined; + get orthogonalEndSash(): Sash | undefined { return this._orthogonalEndSash; } + set orthogonalEndSash(sash: Sash | undefined) { + for (const sashItem of this.columnSashs) { + sashItem.sash.orthogonalEndSash = sash; + } + + this._orthogonalEndSash = sash; + } + + get sashes(): Sash[] { + return this.columnSashs.map(s => s.sash); + } + + constructor( + container: HTMLElement, + columns: ITableColumn[], + private readonly dataSource: ITableDataSource, + options: ITableViewOptions = DefaultOptions as ITableViewOptions, + ) { + for (const column of columns) { + this.renderers.set(column.id, column.renderer); + } + + this.columns = columns.slice(); + this.staticColumns = this.columns.filter(c => c.static); + + this.cache = new CellCache(this.renderers); + + this.domNode.setAttribute('role', 'grid'); + this.domNode.setAttribute('aria-rowcount', '0'); + this.domNode.setAttribute('aria-readonly', 'true'); + + DOM.addClass(this.domNode, this.domId); + this.domNode.tabIndex = 0; + + DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true); + + // this.ariaSetProvider = { getSetSize: (e, i, length) => length, getPosInSet: (_, index) => index + 1 }; + + this.rowHeight = getOrDefault(options, o => o.rowHeight, DefaultOptions.rowHeight); + this.headerHeight = getOrDefault(options, o => o.headerHeight, DefaultOptions.headerHeight); + + let left = 0; + this.columns = this.columns.map(c => { + c.width = getOrDefault(c, o => o.width, DefaultOptions.columnWidth); + c.minWidth = getOrDefault(c, o => o.minWidth, DefaultOptions.minWidth); + c.resizeable = getOrDefault(c, c => c.resizeable, DefaultOptions.resizeable); + c.left = left; + left += c.width; + return c; + }); + + this.rowsContainer.setAttribute('role', 'rowgroup'); + + this.scrollableElement = new ScrollableElement(this.rowsContainer, { + horizontal: ScrollbarVisibility.Auto, + vertical: ScrollbarVisibility.Auto, + useShadows: true + }); + + this.renderHeader(this.domNode); + + this.domNode.appendChild(this.scrollableElement.getDomNode()); + container.appendChild(this.domNode); + + this.disposables = [/*this.gesture,*/ this.scrollableElement, this.cache]; + + this.scrollableElement.onScroll(this.onScroll, this, this.disposables); + + // Prevent the monaco-scrollable-element from scrolling + // https://github.com/Microsoft/vscode/issues/44181 + domEvent(this.scrollableElement.getDomNode(), 'scroll') + (e => (e.target as HTMLElement).scrollTop = 0, null, this.disposables); + + this.updateScrollWidth(); + this.layout(); + } + + private renderHeader(container: HTMLElement): void { + this.headerContainer = DOM.append(container, DOM.$('.monaco-perftable-header')); + const sashContainer = DOM.append(this.headerContainer, DOM.$('.sash-container')); + this.headerContainer.style.height = this.headerHeight + 'px'; + this.headerContainer.style.lineHeight = this.headerHeight + 'px'; + this.headerContainer.setAttribute('role', 'rowgroup'); + const headerCellContainer = DOM.append(this.headerContainer, DOM.$('.monaco-perftable-header-cell-container')); + headerCellContainer.setAttribute('role', 'row'); + + for (const column of this.columns) { + this.createHeaderSash(sashContainer, column); + const domNode = DOM.append(headerCellContainer, DOM.$('.monaco-perftable-header-cell')); + domNode.setAttribute('role', 'columnheader'); + domNode.style.width = column.width + 'px'; + domNode.style.left = column.left + 'px'; + domNode.innerText = column.name; + column.domNode = domNode; + } + } + + private createHeaderSash(sashContainer: HTMLElement, column: ITableColumn): void { + const layoutProvider = { + getVerticalSashLeft: (sash: Sash) => { + let left = 0; + for (const c of this.columns) { + left += c.width!; + if (column === c) { + break; + } + } + return left; + } + }; + const sash = new Sash(sashContainer, layoutProvider, { + orientation: Orientation.VERTICAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }); + + const sashEventMapper = (e: IBaseSashEvent) => ({ sash, column, start: e.startX, current: e.currentX }); + + const onStart = Event.map(sash.onDidStart, sashEventMapper); + const onStartDisposable = onStart(this.onSashStart, this); + const onChange = Event.map(sash.onDidChange, sashEventMapper); + const onChangeDisposable = onChange(this.onSashChange, this); + // const onEnd = Event.map(sash.onDidEnd, () => firstIndex(this.columnSashs, item => item.sash === sash)); + // const onEndDisposable = onEnd(this.onSashEnd, this); + // const onDidReset = Event.map(sash.onDidEnd, () => firstIndex(this.columnSashs, item => item.sash === sash)); + // const onDidResetDisposable = onDidReset(this.onDidSashReset, this); + + const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, /*onEndDisposable, onDidResetDisposable, */sash); + const sashItem: ISashItem = { sash, disposable }; + this.columnSashs.push(sashItem); + if (!column.resizeable) { + sash.hide(); + } + } + + private onSashStart({ sash, start }: ISashEvent): void { + const index = firstIndex(this.columnSashs, item => item.sash === sash); + const sizes = this.columns.map(i => i.width!); + const lefts = this.columns.map(i => i.left!); + this.sashDragState = { start, current: start, index, sizes, lefts }; + } + + private onSashChange({ column, current }: ISashEvent): void { + const { index, start, sizes, lefts } = this.sashDragState!; + this.sashDragState!.current = current; + + const delta = current - start; + const adjustedDelta = sizes[index] + delta < column.minWidth! ? column.minWidth! - sizes[index] : delta; + + column.width = sizes[index] + adjustedDelta; + column.domNode!.style.width = column.width + 'px'; + for (let i = index + 1; i < this.columns.length; i++) { + const resizeColumn = this.columns[i]; + resizeColumn.left = lefts[i] + adjustedDelta; + resizeColumn.domNode!.style.left = resizeColumn.left + 'px'; + } + for (const [index, row] of this.visibleRows.entries()) { + if (row) { + this.updateRowInDOM(row, index); + } + } + this.updateScrollWidth(); + this.layout(); + } + + private eventuallyUpdateScrollDimensions(): void { + this._scrollHeight = this.contentHeight; + this.rowsContainer.style.height = `${this._scrollHeight}px`; + + if (!this.scrollableElementUpdateDisposable) { + this.scrollableElementUpdateDisposable = DOM.scheduleAtNextAnimationFrame(() => { + this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); + this.updateScrollWidth(); + this.scrollableElementUpdateDisposable = null; + }); + } + } + + private updateScrollWidth(): void { + this._scrollWidth = this.columns.reduce((p, c) => p += c.width!, 0); + this.rowsContainer.style.width = `${this.scrollWidth}px`; + this.headerContainer.style.width = `${this.scrollWidth}px`; + this.scrollableElement.setScrollDimensions({ scrollWidth: this.scrollWidth }); + } + + private onScroll(e: ScrollEvent): void { + if (this.scheduledRender) { + this.scheduledRender.dispose(); + } + this.scheduledRender = DOM.runAtThisOrScheduleAtNextAnimationFrame(() => { + this.render(e.scrollTop, e.height, e.scrollLeft, e.scrollWidth); + }); + } + + private getRenderRange(renderTop: number, renderHeight: number): IRange { + const start = Math.floor(renderTop / this.rowHeight); + const end = Math.ceil((renderTop + renderHeight) / this.rowHeight); + return { + start, + end + }; + } + + private getNextToLastElement(ranges: IRange[]): HTMLElement | null { + const lastRange = ranges[ranges.length - 1]; + + if (!lastRange) { + return null; + } + + const nextToLastItem = this.visibleRows[lastRange.end]; + + if (!nextToLastItem) { + return null; + } + + if (!nextToLastItem.row) { + return null; + } + + return nextToLastItem.row; + } + + private render(renderTop: number, renderHeight: number, renderLeft: number, scrollWidth: number): void { + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const renderRange = this.getRenderRange(renderTop, renderHeight); + renderRange.end = renderRange.end > this.length ? this.length : renderRange.end; + + // IE (all versions) cannot handle units above about 1,533,908 px, so every 500k pixels bring numbers down + const STEP_SIZE = 500000; + this.bigNumberDelta = 0; + if (renderTop >= STEP_SIZE) { + // Compute a delta that guarantees that lines are positioned at `lineHeight` increments + this.bigNumberDelta = Math.floor(renderTop / STEP_SIZE) * STEP_SIZE; + this.bigNumberDelta = Math.floor(this.bigNumberDelta / this.rowHeight) * this.rowHeight; + const rangesToUpdate = Range.intersect(previousRenderRange, renderRange); + for (let i = rangesToUpdate.start; i < rangesToUpdate.end; i++) { + this.updateRowInDOM(this.visibleRows[i], i); + } + } + + const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange); + const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange); + const beforeElement = this.getNextToLastElement(rangesToInsert); + + for (const range of rangesToInsert) { + for (let i = range.start; i < range.end; i++) { + this.insertRowInDOM(i, beforeElement); + } + } + + for (const range of rangesToRemove) { + for (let i = range.start; i < range.end; i++) { + this.removeRowFromDOM(i); + } + } + + const canUseTranslate3d = !isWindows && !browser.isFirefox && browser.getZoomLevel() === 0; + + if (canUseTranslate3d) { + const transform = `translate3d(-${renderLeft}px, -${renderTop - this.bigNumberDelta}px, 0px)`; + this.rowsContainer.style.transform = transform; + this.rowsContainer.style.webkitTransform = transform; + this.headerContainer.style.transform = `translate3d(-${renderLeft}px, 0px, 0px)`; + this.headerContainer.style.webkitTransform = `translate3d(-${renderLeft}px, 0px, 0px)`; + + if (canUseTranslate3d !== this.canUseTranslate3d) { + this.rowsContainer.style.left = '0'; + this.headerContainer.style.left = '0'; + this.rowsContainer.style.top = '0'; + } + } else { + this.rowsContainer.style.left = `-${renderLeft}px`; + this.headerContainer.style.left = `-${renderLeft}px`; + this.rowsContainer.style.top = `-${renderTop - this.bigNumberDelta}px`; + + if (canUseTranslate3d !== this.canUseTranslate3d) { + this.rowsContainer.style.transform = ''; + this.rowsContainer.style.webkitTransform = ''; + } + } + + this.canUseTranslate3d = canUseTranslate3d; + + this.lastRenderTop = renderTop; + this.lastRenderHeight = renderHeight; + } + + public layout(height?: number, width?: number): void { + const scrollDimensions: INewScrollDimensions = { + height: typeof height === 'number' ? height : DOM.getContentHeight(this.domNode) + }; + scrollDimensions.height = scrollDimensions.height! - this.headerHeight; + if (this.scrollableElementUpdateDisposable) { + this.scrollableElementUpdateDisposable.dispose(); + this.scrollableElementUpdateDisposable = null; + scrollDimensions.scrollHeight = this.scrollHeight; + } + + this.scrollableElement.setScrollDimensions(scrollDimensions); + + this.scrollableElement.setScrollDimensions({ + width: typeof width === 'number' ? width : DOM.getContentWidth(this.domNode) + }); + + this.columnSashs.forEach(s => s.sash.layout()); + } + + getScrollTop(): number { + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollTop; + } + + getScrollLeft(): number { + const scrollPosition = this.scrollableElement.getScrollPosition(); + return scrollPosition.scrollLeft; + } + + setScrollTop(scrollTop: number): void { + if (this.scrollableElementUpdateDisposable) { + this.scrollableElementUpdateDisposable.dispose(); + this.scrollableElementUpdateDisposable = null; + this.scrollableElement.setScrollDimensions({ scrollHeight: this.scrollHeight }); + } + + this.scrollableElement.setScrollPosition({ scrollTop }); + } + + get scrollTop(): number { + return this.getScrollTop(); + } + + set scrollTop(scrollTop: number) { + this.setScrollTop(scrollTop); + } + + get scrollHeight(): number { + return this._scrollHeight + 10; + } + + get scrollWidth(): number { + return this._scrollWidth + 10; + } + + get scrollLeft(): number { + return this.getScrollLeft(); + } + + domElement(index: number, column: number): HTMLElement | null { + const row = this.visibleRows[index]; + const cell = row && row.cells && row.cells[column]; + return cell && cell.domNode; + } + + element(index: number): T | undefined { + return this.visibleRows[index].element; + } + + column(index: number): ITableColumn | undefined { + return this.columns[index]; + } + + indexOfColumn(columnId: string): number | undefined { + return firstIndex(this.columns, v => v.id === columnId); + } + + get renderHeight(): number { + const scrollDimensions = this.scrollableElement.getScrollDimensions(); + return scrollDimensions.height; + } + + private insertRowInDOM(index: number, beforeElement: HTMLElement | null): void { + let row = this.visibleRows[index]; + + if (!row) { + row = { + id: String(index), + element: undefined, + row: null, + size: this.rowHeight, + cells: null, + datapromise: null + }; + row.datapromise = createCancelablePromise(token => { + return this.dataSource.getRow(index).then(d => { + row.element = d; + }); + }); + row.datapromise.finally(() => row.datapromise = null); + this.visibleRows[index] = row; + } + + if (!row.row) { + this.allocRow(row, index); + row.row!.setAttribute('role', 'treeitem'); + } + + if (!row.row!.parentElement) { + if (beforeElement) { + this.rowsContainer.insertBefore(row.row!, beforeElement); + } else { + this.rowsContainer.appendChild(row.row!); + } + } + + this.updateRowInDOM(row, index); + + if (row.datapromise) { + row.datapromise.then(() => this.renderRow(row, index)); + // in this case we can special case the row count column + for (const [i, column] of this.staticColumns.entries()) { + const cell = row.cells![i]; + column.renderer.renderCell(undefined, index, i, column.id, cell.templateData, column.width); + } + } else { + this.renderRow(row, index); + } + } + + private allocRow(row: IAsyncRowItem, index: number): void { + row.cells = new Array(); + row.row = DOM.$('.monaco-perftable-row'); + for (const [index, column] of this.columns.entries()) { + const cell = this.cache.alloc(column.id); + row.cells[index] = cell; + if (column.cellClass) { + DOM.addClass(cell.domNode!, column.cellClass); + } + row.row.appendChild(cell.domNode!); + } + } + + private renderRow(row: IAsyncRowItem, index: number): void { + for (const [i, column] of this.columns.entries()) { + const cell = row.cells![i]; + column.renderer.renderCell(row.element!, index, i, column.id, cell.templateData, column.width); + } + } + + private updateRowInDOM(row: IAsyncRowItem, index: number): void { + row.row!.style.top = `${this.elementTop(index)}px`; + row.row!.style.height = `${row.size}px`; + + for (const [columnIndex, column] of this.columns.entries()) { + const cell = row.cells![columnIndex].domNode; + cell!.style.width = `${column.width}px`; + cell!.style.left = `${column.left}px`; + cell!.style.height = `${row.size}px`; + cell!.style.lineHeight = `${row.size}px`; + cell!.setAttribute('data-column-id', `${columnIndex}`); + cell!.setAttribute('role', 'gridcell'); + row.row!.setAttribute('id', this.getElementDomId(index, columnIndex)); + } + + row.row!.setAttribute('data-index', `${index}`); + row.row!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false'); + row.row!.setAttribute('role', 'row'); + // row.row!.setAttribute('aria-setsize', String(this.ariaSetProvider.getSetSize(row.element, index, this.length))); + // row.row!.setAttribute('aria-posinset', String(this.ariaSetProvider.getPosInSet(row.element, index))); + row.row!.setAttribute('id', this.getElementDomId(index)); + } + + private removeRowFromDOM(index: number): void { + const item = this.visibleRows[index]; + + if (!item) { + return; + } + + let canceled = false; + if (item.datapromise) { + item.datapromise.cancel(); + canceled = true; + } + + for (const [i, column] of this.columns.entries()) { + const cell = item.cells![i]; + + if (!canceled) { + const renderer = column.renderer; + if (renderer && renderer.disposeCell) { + renderer.disposeCell(item.element!, index, i, column.id, cell.templateData, column.width); + } + } + + this.cache.release(cell!); + } + + removeFromParent(item.row!); + + delete this.visibleRows[index]; + } + + @memoize get onMouseClick(): Event> { return Event.map(domEvent(this.domNode, 'click'), e => this.toMouseEvent(e)); } + @memoize get onMouseDblClick(): Event> { return Event.map(domEvent(this.domNode, 'dblclick'), e => this.toMouseEvent(e)); } + @memoize get onMouseMiddleClick(): Event> { return Event.filter(Event.map(domEvent(this.domNode, 'auxclick'), e => this.toMouseEvent(e as MouseEvent)), e => e.browserEvent.button === 1); } + @memoize get onMouseUp(): Event> { return Event.map(domEvent(this.domNode, 'mouseup'), e => this.toMouseEvent(e)); } + @memoize get onMouseDown(): Event> { return Event.map(domEvent(this.domNode, 'mousedown'), e => this.toMouseEvent(e)); } + @memoize get onMouseOver(): Event> { return Event.map(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); } + @memoize get onMouseMove(): Event> { return Event.map(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); } + @memoize get onMouseOut(): Event> { return Event.map(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); } + @memoize get onContextMenu(): Event> { return Event.map(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); } + + public toMouseEvent(browserEvent: MouseEvent): ITableMouseEvent { + const index = this.getItemIndexFromEventTarget(browserEvent.target || null); + const item = typeof index === 'undefined' ? undefined : this.visibleRows[index.row]; + const element = item && item.element; + const buttons = browserEvent.buttons; + return { browserEvent, buttons, index, element }; + } + + public getItemIndexFromEventTarget(target: EventTarget | null): GridPosition | undefined { + let element: HTMLElement | null = target as (HTMLElement | null); + + while (element instanceof HTMLElement && element !== this.rowsContainer) { + const rawColumn = element.getAttribute('data-column-id'); + + if (rawColumn) { + const column = Number(rawColumn); + + if (!isNaN(column)) { + while (element instanceof HTMLElement && element !== this.rowsContainer) { + const rawIndex = element.getAttribute('data-index'); + + if (rawIndex) { + const row = Number(rawIndex); + + if (!isNaN(row)) { + return new GridPosition(row, column); + } + } + + element = element.parentElement; + } + } + } + + element = element!.parentElement; + } + + return undefined; + } + + elementTop(index: number): number { + return Math.floor(index * this.rowHeight) - this.bigNumberDelta; + } + + getElementDomId(index: number, column?: number): string { + if (column) { + return `${this.domId}_${index}_${column}`; + } else { + return `${this.domId}_${index}`; + } + } + + get length(): number { + return this._length; + } + + set length(length: number) { + this.domNode.setAttribute('aria-rowcount', `${length}`); + const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight); + const potentialRerenderRange = { start: this.length, end: length }; + const rerenderRange = Range.intersect(potentialRerenderRange, previousRenderRange); + this._length = length; + for (let i = rerenderRange.start; i < rerenderRange.end; i++) { + if (this.visibleRows[i]) { + this.removeRowFromDOM(i); + } + this.insertRowInDOM(i, null); + } + this.eventuallyUpdateScrollDimensions(); + } + + get columnLength(): number { + return this.columns.length; + } + + dispose(): void { + dispose(this.disposables); + } +} diff --git a/src/sql/base/browser/ui/table/highPerf/tableWidget.ts b/src/sql/base/browser/ui/table/highPerf/tableWidget.ts new file mode 100644 index 0000000000..670921962d --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/tableWidget.ts @@ -0,0 +1,1060 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITableEvent, ITableRenderer, ITableMouseEvent, ITableContextMenuEvent, ITableDataSource, IStaticTableRenderer, IStaticColumn, ITableColumn, TableError } from 'sql/base/browser/ui/table/highPerf/table'; + +import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { memoize } from 'vs/base/common/decorators'; +import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; +import { firstIndex, find } from 'vs/base/common/arrays'; +import * as DOM from 'vs/base/browser/dom'; +import { TableView, ITableViewOptions } from 'sql/base/browser/ui/table/highPerf/tableView'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { domEvent } from 'vs/base/browser/event'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import * as platform from 'vs/base/common/platform'; +import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { Color } from 'vs/base/common/color'; +import { getOrDefault } from 'vs/base/common/objects'; +import { isNumber } from 'vs/base/common/types'; +import { clamp } from 'vs/base/common/numbers'; +import { GlobalMouseMoveMonitor } from 'vs/base/browser/globalMouseMoveMonitor'; +import { GridPosition } from 'sql/base/common/gridPosition'; +import { GridRange, IGridRange } from 'sql/base/common/gridRange'; + +interface ITraitChangeEvent { + indexes: IGridRange[]; + browserEvent?: UIEvent; +} + +type ITraitTemplateData = HTMLElement; + +interface IRenderedContainer { + templateData: ITraitTemplateData; + index: GridPosition; +} + +class TraitRenderer implements ITableRenderer +{ + private renderedElements: IRenderedContainer[] = []; + + constructor(private trait: Trait) { } + + get templateId(): string { + return `template:${this.trait.trait}`; + } + + renderTemplate(container: HTMLElement): ITraitTemplateData { + return container; + } + + renderCell(element: T, row: number, cell: number, columnId: string, templateData: ITraitTemplateData): void { + const renderedElementIndex = firstIndex(this.renderedElements, el => el.templateData === templateData); + + if (renderedElementIndex >= 0) { + const rendered = this.renderedElements[renderedElementIndex]; + this.trait.unrender(templateData); + rendered.index = new GridPosition(row, cell); + } else { + const rendered = { index: new GridPosition(row, cell), templateData }; + this.renderedElements.push(rendered); + } + + this.trait.renderIndex(new GridPosition(row, cell), templateData); + } + + renderIndexes(indexes: IGridRange[]): void { + for (const { index, templateData } of this.renderedElements) { + if (!!find(indexes, v => GridRange.containsPosition(v, index))) { + this.trait.renderIndex(index, templateData); + } + } + } + + disposeTemplate(templateData: ITraitTemplateData): void { + const index = firstIndex(this.renderedElements, el => el.templateData === templateData); + + if (index < 0) { + return; + } + + this.renderedElements.splice(index, 1); + } +} + +class DOMFocusController implements IDisposable { + + private disposables: IDisposable[] = []; + + constructor( + private list: Table, + private view: TableView + ) { + this.disposables = []; + + const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown')) + .map(e => new StandardKeyboardEvent(e)); + + onKeyDown.filter(e => e.keyCode === KeyCode.Tab && !e.ctrlKey && !e.metaKey && !e.shiftKey && !e.altKey) + .on(this.onTab, this, this.disposables); + } + + private onTab(e: StandardKeyboardEvent): void { + if (e.target !== this.view.domNode) { + return; + } + + const focus = this.list.getFocus(); + + if (focus.length === 0) { + return; + } + + const focusedDomElement = this.view.domElement(focus[0].startRow, focus[0].startColumn); + + if (!focusedDomElement) { + return; + } + + const tabIndexElement = focusedDomElement.querySelector('[tabIndex]'); + + if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) { + return; + } + + const style = window.getComputedStyle(tabIndexElement); + if (style.visibility === 'hidden' || style.display === 'none') { + return; + } + + e.preventDefault(); + e.stopPropagation(); + tabIndexElement.focus(); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +class Trait implements IDisposable { + + private indexes: Array = []; + private sortedIndexes: Array = []; + + private _onChange = new Emitter(); + get onChange(): Event { return this._onChange.event; } + + get trait(): string { return this._trait; } + + @memoize + get renderer(): TraitRenderer { + return new TraitRenderer(this); + } + + constructor(private _trait: string) { } + + renderIndex(index: GridPosition, container: HTMLElement): void { + DOM.toggleClass(container, this._trait, this.contains(index)); + } + + unrender(container: HTMLElement): void { + DOM.removeClass(container, this._trait); + } + + /** + * Sets the indexes which should have this trait. + * + * @param indexes Indexes which should have this trait. + * @return The old indexes which had this trait. + */ + set(indexes: Array, browserEvent?: UIEvent): Array { + return this._set(indexes, indexes, browserEvent); + } + + private _set(indexes: Array, sortedIndexes: Array, browserEvent?: UIEvent): Array { + const result = this.indexes; + const sortedResult = this.sortedIndexes; + + this.indexes = indexes; + this.sortedIndexes = sortedIndexes; + + // const toRender = disjunction(sortedResult, indexes); + this.renderer.renderIndexes(indexes.concat(sortedResult)); + + this._onChange.fire({ indexes, browserEvent }); + return result; + } + + get(): Array { + return this.indexes; + } + + contains(index: GridPosition): boolean { + return !!find(this.indexes, v => GridRange.containsPosition(v, index)); + } + + dispose() { + this._onChange = dispose(this._onChange); + } +} + +class FocusTrait extends Trait { + + constructor() { + super('focused'); + } + + renderIndex(index: GridPosition, container: HTMLElement): void { + super.renderIndex(index, container); + + if (this.contains(index)) { + container.setAttribute('aria-selected', 'true'); + } else { + container.removeAttribute('aria-selected'); + } + } +} + +class PipelineRenderer implements ITableRenderer { + + constructor( + private renderers: ITableRenderer[] + ) { } + + renderTemplate(container: HTMLElement): any[] { + return this.renderers.map(r => r.renderTemplate(container)); + } + + renderCell(element: T, index: number, cell: number, columnId: string, templateData: any[], height: number | undefined): void { + let i = 0; + + for (const renderer of this.renderers) { + renderer.renderCell(element, index, cell, columnId, templateData[i++], height); + } + } + + disposeCell(element: T, index: number, cell: number, columnId: string, templateData: any[], height: number | undefined): void { + let i = 0; + + for (const renderer of this.renderers) { + if (renderer.disposeCell) { + renderer.disposeCell(element, index, cell, columnId, templateData[i], height); + } + + i += 1; + } + } + + disposeTemplate(templateData: any[]): void { + let i = 0; + + for (const renderer of this.renderers) { + renderer.disposeTemplate(templateData[i++]); + } + } +} + +export function isSelectionSingleChangeEvent(event: ITableMouseEvent): boolean { + return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey; +} + +export function isSelectionRangeChangeEvent(event: ITableMouseEvent): boolean { + return event.browserEvent.shiftKey; +} + +function isMouseRightClick(event: UIEvent): boolean { + return event instanceof MouseEvent && event.button === 2; +} + +const DefaultMultipleSelectionController = { + isSelectionSingleChangeEvent, + isSelectionRangeChangeEvent +}; + +export interface IOpenController { + shouldOpen(event: UIEvent): boolean; +} + +const DefaultOpenController: IOpenController = { + shouldOpen: (event: UIEvent) => { + if (event instanceof MouseEvent) { + return !isMouseRightClick(event); + } + + return true; + } +}; + +export interface IMultipleSelectionController { + isSelectionSingleChangeEvent(event: ITableMouseEvent): boolean; + isSelectionRangeChangeEvent(event: ITableMouseEvent): boolean; +} + +class RowCountRenderer implements IStaticTableRenderer { + renderTemplate(container: HTMLElement): HTMLElement { + return DOM.append(container, DOM.$('.row-count')); + } + + renderCell(element: undefined, index: number, ccell: number, olumnId: string, templateData: HTMLElement, width: number): void { + templateData.innerText = `${index}`; + } + + disposeTemplate(templateData: HTMLElement): void { + throw new Error('Method not implemented.'); + } +} + +const rowCountColumnDef: IStaticColumn = { + id: 'rowCount', + name: '', + renderer: new RowCountRenderer(), + cellClass: 'row-count-cell', + static: true, + width: 30, + resizeable: false +}; + +function rowCountFilter(column: ITableColumn): boolean { + return column.id !== rowCountColumnDef.id; +} + +class KeyboardController implements IDisposable { + + private disposables: IDisposable[]; + // private openController: IOpenController; + + constructor( + private table: Table, + private view: TableView, + options?: ITableOptions + ) { + // const multipleSelectionSupport = !(options.multipleSelectionSupport === false); + this.disposables = []; + + // this.openController = options.openController || DefaultOpenController; + + const onKeyDown = Event.chain(domEvent(view.domNode, 'keydown')) + // .filter(e => !isInputElement(e.target as HTMLElement)) + .map(e => new StandardKeyboardEvent(e)); + + onKeyDown.filter(e => e.keyCode === KeyCode.Enter).on(this.onEnter, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.UpArrow).on(this.onUpArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.DownArrow).on(this.onDownArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.PageUp).on(this.onPageUpArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.PageDown).on(this.onPageDownArrow, this, this.disposables); + onKeyDown.filter(e => e.keyCode === KeyCode.Escape).on(this.onEscape, this, this.disposables); + + // if (multipleSelectionSupport) { + onKeyDown.filter(e => (platform.isMacintosh ? e.metaKey : e.ctrlKey) && e.keyCode === KeyCode.KEY_A).on(this.onCtrlA, this, this.disposables); + // } + } + + private onEnter(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.setSelection(this.table.getFocus(), e.browserEvent); + + // if (this.openController.shouldOpen(e.browserEvent)) { + // this.list.open(this.list.getFocus(), e.browserEvent); + // } + } + + private onUpArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.focusPreviousRow(1, false, e.browserEvent); + this.table.reveal(this.table.getFocus()[0].startRow); + this.view.domNode.focus(); + } + + private onDownArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.focusNextRow(1, false, e.browserEvent); + this.table.reveal(this.table.getFocus()[0].startRow); + this.view.domNode.focus(); + } + + private onRightArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.focusNextCell(1, false, e.browserEvent); + this.table.reveal(this.table.getFocus()[0].startRow); + this.view.domNode.focus(); + } + + private onLeftArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.focusPreviousCell(1, false, e.browserEvent); + this.table.reveal(this.table.getFocus()[0].startRow); + this.view.domNode.focus(); + } + + private onPageUpArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + // this.table.focusPreviousPage(e.browserEvent); + // this.table.reveal(this.table.getFocus()[0]); + this.view.domNode.focus(); + } + + private onPageDownArrow(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + // this.table.focusNextPage(e.browserEvent); + // this.table.reveal(this.table.getFocus()[0]); + this.view.domNode.focus(); + } + + private onCtrlA(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + // this.table.setSelection(range(this.table.length), e.browserEvent); + this.view.domNode.focus(); + } + + private onEscape(e: StandardKeyboardEvent): void { + e.preventDefault(); + e.stopPropagation(); + this.table.setSelection([], e.browserEvent); + this.view.domNode.focus(); + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +export class MouseController implements IDisposable { + + private multipleSelectionSupport: boolean; + readonly multipleSelectionController?: IMultipleSelectionController; + private openController: IOpenController; + private disposables: IDisposable[] = []; + private readonly _mouseMoveMonitor = new GlobalMouseMoveMonitor>(); + + private startMouseEvent?: ITableMouseEvent; + + constructor(protected table: Table, protected view: TableView) { + this.multipleSelectionSupport = true; + + if (this.multipleSelectionSupport) { + this.multipleSelectionController = DefaultMultipleSelectionController; + } + + this.openController = DefaultOpenController; + + this.disposables.push(this._mouseMoveMonitor); + + table.onMouseDown(this.onMouseDown, this, this.disposables); + // table.onMouseClick(this.onPointer, this, this.disposables); + table.onContextMenu(this.onContextMenu, this, this.disposables); + } + + protected isSelectionSingleChangeEvent(event: ITableMouseEvent): boolean { + if (this.multipleSelectionController) { + return this.multipleSelectionController.isSelectionSingleChangeEvent(event); + } + + return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey; + } + + protected isSelectionRangeChangeEvent(event: ITableMouseEvent): boolean { + if (this.multipleSelectionController) { + return this.multipleSelectionController.isSelectionRangeChangeEvent(event); + } + + return event.browserEvent.shiftKey; + } + + private isSelectionChangeEvent(event: ITableMouseEvent): boolean { + return this.isSelectionSingleChangeEvent(event) || this.isSelectionRangeChangeEvent(event); + } + + private onMouseDown(e: ITableMouseEvent): void { + if (document.activeElement !== e.browserEvent.target) { + this.table.domFocus(); + } + const merger = (lastEvent: ITableMouseEvent, currentEvent: MouseEvent): ITableMouseEvent => { + return this.view.toMouseEvent(currentEvent); + }; + this._mouseMoveMonitor.startMonitoring(e.browserEvent.target as HTMLElement, e.buttons, merger, e => this.onMouseMove(e), () => this.onMouseStop()); + this.onPointer(e); + } + + private onContextMenu(e: ITableContextMenuEvent): void { + const focus = typeof e.index === 'undefined' ? [] : [new GridRange(e.index.row, e.index.column)]; + this.table.setFocus(focus, e.browserEvent); + } + + protected onMouseMove(event: ITableMouseEvent): void { + if (event.index) { + this.startMouseEvent = this.startMouseEvent || event; + this.table.setSelection([new GridRange(this.startMouseEvent.index!.row, this.startMouseEvent.index!.column, event.index.row, event.index.column)]); + } + } + + protected onMouseStop(): void { + this.startMouseEvent = undefined; + } + + protected onPointer(e: ITableMouseEvent): void { + + let reference = this.table.getFocus(); + const selection = this.table.getSelection(); + reference = reference === undefined ? selection : reference; + + const focus = e.index; + + if (typeof focus === 'undefined') { + this.table.setFocus([], e.browserEvent); + this.table.setSelection([], e.browserEvent); + return; + } + + if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) { + return this.changeSelection(e, reference); + } + + if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) { + return this.changeSelection(e, reference); + } + + this.table.setFocus([new GridRange(focus.row, focus.column)], e.browserEvent); + + if (!isMouseRightClick(e.browserEvent)) { + this.table.setSelection([new GridRange(focus.row, focus.column)], e.browserEvent); + + if (this.openController.shouldOpen(e.browserEvent)) { + // this.table.open([focus], e.browserEvent); + } + } + } + + private changeSelection(e: ITableMouseEvent, reference: IGridRange[] | undefined): void { + const focus = e.index!; + + if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) { + const selection = this.table.getSelection(); + const lastSelection = selection.pop(); + if (lastSelection) { + this.table.setSelection([...selection, GridRange.plusRange(lastSelection, new GridRange(focus.row, focus.column))]); + } else { + this.table.setSelection([...selection, new GridRange(focus.row, focus.column)]); + } + } else if (this.isSelectionSingleChangeEvent(e)) { + const selection = this.table.getSelection(); + selection.push(new GridRange(focus.row, focus.column)); + this.table.setSelection(selection); + } + } + + dispose() { + this.disposables = dispose(this.disposables); + } +} + +export interface ITableOptions extends ITableViewOptions { + keyboardSupport?: boolean; + dnd?: boolean; +} + +export interface ITableStyles extends IListStyles { + cellOutlineColor?: Color; + tableHeaderAndRowCountColor?: Color; +} + +export class DefaultStyleController implements IStyleController { + + constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { } + + style(styles: ITableStyles): void { + const suffix = this.selectorSuffix ? `.${this.selectorSuffix}` : ''; + const content: string[] = []; + + if (styles.listFocusBackground) { + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listFocusForeground) { + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { color: ${styles.listFocusForeground}; }`); + } + + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-perftable${suffix}:focus .monaco-perftable-cell.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listFocusAndSelectionBackground) { + content.push(` + .monaco-drag-image, + .monaco-perftable${suffix}:focus .monaco-perftable-cell.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } + `); + } + + if (styles.listFocusAndSelectionForeground) { + content.push(` + .monaco-drag-image, + .monaco-perftable${suffix}:focus .monaco-perftable-cell.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } + `); + } + + if (styles.listInactiveFocusBackground) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused { background-color: ${styles.listInactiveFocusBackground}; }`); + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionBackground) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { background-color: ${styles.listInactiveSelectionBackground}; }`); + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionForeground) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { color: ${styles.listInactiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-perftable${suffix}:not(.drop-target) .monaco-perftable-cell:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + } + + if (styles.listHoverForeground) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); + } + + if (styles.listSelectionOutline) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); + } + + if (styles.listFocusOutline) { + content.push(` + .monaco-drag-image, + .monaco-perftable${suffix}:focus .monaco-perftable-cell.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } + `); + } + + if (styles.listInactiveFocusOutline) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); + } + + if (styles.listHoverOutline) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + if (styles.listDropBackground) { + content.push(` + .monaco-perftable${suffix}.drop-target, + .monaco-perftable${suffix} .monaco-perftable-cell.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } + `); + } + + if (styles.listFilterWidgetBackground) { + content.push(`.monaco-perftable-type-filter { background-color: ${styles.listFilterWidgetBackground} }`); + } + + if (styles.listFilterWidgetOutline) { + content.push(`.monaco-perftable-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`); + } + + if (styles.listFilterWidgetNoMatchesOutline) { + content.push(`.monaco-perftable-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`); + } + + if (styles.listMatchesShadow) { + content.push(`.monaco-perftable-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); + } + + if (styles.cellOutlineColor) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell { border: 1px solid ${styles.cellOutlineColor}; }`); + content.push(`.monaco-perftable${suffix} .monaco-perftable-header-cell { border: 1px solid ${styles.cellOutlineColor}; }`); + } + + if (styles.tableHeaderAndRowCountColor) { + content.push(`.monaco-perftable${suffix} .monaco-perftable-header-cell { background-color: ${styles.tableHeaderAndRowCountColor}; }`); + content.push(`.monaco-perftable${suffix} .monaco-perftable-cell.row-count-cell { background-color: ${styles.tableHeaderAndRowCountColor}; }`); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.styleElement.innerHTML) { + this.styleElement.innerHTML = newStyles; + } + } +} + +const DefaultOptions = { + rowCountColumn: true, +}; + +export class Table implements IDisposable { + + private focus: Trait; + private selection: Trait; + private eventBufferer = new EventBufferer(); + private view: TableView; + private styleElement: HTMLStyleElement; + private styleController: IStyleController; + + public inDrag = false; + + protected readonly disposables = new DisposableStore(); + + @memoize get onFocusChange(): Event> { + return Event.map(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toTableEvent(e)); + } + + @memoize get onSelectionChange(): Event> { + return Event.map(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toTableEvent(e)); + } + + private toTableEvent({ indexes, browserEvent }: ITraitChangeEvent) { + return { indexes, elements: indexes.map(i => this.view.element(i.startRow)!), browserEvent }; + } + + get onDidScroll(): Event { return this.view.onDidScroll; } + get onMouseClick(): Event> { return this.view.onMouseClick; } + get onMouseDblClick(): Event> { return this.view.onMouseDblClick; } + get onMouseMiddleClick(): Event> { return this.view.onMouseMiddleClick; } + get onMouseUp(): Event> { return this.view.onMouseUp; } + get onMouseDown(): Event> { return this.view.onMouseDown; } + get onMouseOver(): Event> { return this.view.onMouseOver; } + get onMouseMove(): Event> { return this.view.onMouseMove; } + get onMouseOut(): Event> { return this.view.onMouseOut; } + + private didJustPressContextMenuKey: boolean = false; + @memoize get onContextMenu(): Event> { + const fromKeydown = Event.chain(domEvent(this.view.domNode, 'keydown')) + .map(e => new StandardKeyboardEvent(e)) + .filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10)) + .filter(e => { e.preventDefault(); e.stopPropagation(); return false; }) + .event as Event; + + const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup')) + .filter(() => { + const didJustPressContextMenuKey = this.didJustPressContextMenuKey; + this.didJustPressContextMenuKey = false; + return didJustPressContextMenuKey; + }) + .filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0].startRow, this.getFocus()[0].startColumn)) + .map(browserEvent => { + const index = this.getFocus()[0]; + const element = this.view.element(index.startRow); + const anchor = this.view.domElement(index.startRow, index.startColumn) as HTMLElement; + return { index: GridRange.lift(index).getStartPosition(), element, anchor, browserEvent }; + }) + .event; + + const fromMouse = Event.chain(this.view.onContextMenu) + .filter(() => !this.didJustPressContextMenuKey) + .map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent })) + .event; + + return Event.any>(fromKeydown, fromKeyup, fromMouse); + } + + get onKeyDown(): Event { return domEvent(this.view.domNode, 'keydown'); } + get onKeyUp(): Event { return domEvent(this.view.domNode, 'keyup'); } + get onKeyPress(): Event { return domEvent(this.view.domNode, 'keypress'); } + + readonly onDidFocus: Event; + readonly onDidBlur: Event; + + private readonly _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; + + constructor( + private readonly user: string, + container: HTMLElement, + columns: ITableColumn[], + dataSource: ITableDataSource, + options: ITableOptions = DefaultOptions + ) { + this.focus = new FocusTrait(); + this.selection = new Trait('selected'); + + const baseRenderers: ITableRenderer[] = [this.focus.renderer, this.selection.renderer]; + + columns = columns.map(r => { + r.renderer = new PipelineRenderer([...baseRenderers, r.renderer]); + return r; + }); + + options.rowCountColumn = getOrDefault(options, o => o.rowCountColumn, DefaultOptions.rowCountColumn); + + if (options.rowCountColumn) { + columns.unshift(rowCountColumnDef); + } + + this.view = new TableView(container, columns, dataSource, options); + + this.view.domNode.setAttribute('aria-multiselectable', 'true'); + + this.styleElement = DOM.createStyleSheet(this.view.domNode); + + this.styleController = new DefaultStyleController(this.styleElement, this.view.domId); + + this.disposables.add(new DOMFocusController(this, this.view)); + + if (!options || typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) { + const controller = new KeyboardController(this, this.view, options); + this.disposables.add(controller); + } + + this.onDidFocus = Event.map(domEvent(this.view.domNode, 'focus', true), () => null!); + this.onDidBlur = Event.map(domEvent(this.view.domNode, 'blur', true), () => null!); + + this.disposables.add(this.createMouseController()); + + this.onFocusChange(this._onFocusChange, this, this.disposables); + this.onSelectionChange(this._onSelectionChange, this, this.disposables); + } + + protected createMouseController(): MouseController { + return new MouseController(this, this.view); + } + + get length(): number { + return this.view.length; + } + + set length(length: number) { + this.view.length = length; + } + + get columnLength(): number { + return this.view.columnLength; + } + + layout(height?: number, width?: number): void { + this.view.layout(height, width); + } + + domFocus(): void { + this.view.domNode.focus(); + } + + setSelection(indexes: IGridRange[], browserEvent?: UIEvent): void { + // for (const index of indexes) { + // if (index < 0 || index >= this.length) { + // throw new Error(`Invalid index ${index}`); + // } + // } + + this.selection.set(indexes, browserEvent); + } + + getSelection(): IGridRange[] { + return this.selection.get(); + } + + setFocus(indexes: IGridRange[], browserEvent?: UIEvent): void { + // for (const index of indexes) { + // if (index < 0 || index >= this.length) { + // throw new Error(`Invalid index ${index}`); + // } + // } + + this.focus.set(indexes, browserEvent); + this.selection.set(indexes, browserEvent); + } + + reveal(index: number, relativeTop?: number): void { + if (index < 0 || index >= this.length) { + throw new TableError(this.user, `Invalid index ${index}`); + } + + const scrollTop = this.view.getScrollTop(); + const elementTop = this.view.elementTop(index); + const elementHeight = this.view.rowHeight; + + if (isNumber(relativeTop)) { + // y = mx + b + const m = elementHeight - this.view.renderHeight; + this.view.setScrollTop(m * clamp(relativeTop, 0, 1) + elementTop); + } else { + const viewItemBottom = elementTop + elementHeight; + const wrapperBottom = scrollTop + this.view.renderHeight; + + if (elementTop < scrollTop) { + this.view.setScrollTop(elementTop); + } else if (viewItemBottom >= wrapperBottom) { + this.view.setScrollTop(viewItemBottom - this.view.renderHeight); + } + } + } + + + focusNextCell(n = 1, loop = false, browserEvent?: UIEvent, filter: (column: ITableColumn) => boolean = rowCountFilter): void { + if (this.length === 0) { return; } + + const focus = this.focus.get(); + const cellIndex = focus.length > 0 ? focus[0].startColumn! : 0; + const targetColumn = this.findNextColumn(cellIndex + n, loop, filter); + const targetRow = focus.length > 0 ? focus[0].startRow : 0; + + if (targetColumn > -1) { + this.setFocus([new GridRange(targetRow, targetColumn)], browserEvent); + } + } + + focusPreviousCell(n = 1, loop = false, browserEvent?: UIEvent, filter: (column: ITableColumn) => boolean = rowCountFilter): void { + if (this.length === 0) { return; } + + const focus = this.focus.get(); + const cellIndex = focus.length > 0 ? focus[0].startColumn! : 0; + const targetColumn = this.findPreviousColumn(cellIndex - n, loop, filter); + const targetRow = focus.length > 0 ? focus[0].startRow : 0; + + if (targetColumn > -1) { + this.setFocus([new GridRange(targetRow, targetColumn)], browserEvent); + } + } + + focusNextRow(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void { + if (this.length === 0) { return; } + + const focus = this.focus.get(); + const index = this.findNextRowIndex(focus.length > 0 ? focus[0].startRow + n : 0, loop, filter); + + const targetColumn = focus.length > 0 ? focus[0].startColumn : 0; + + if (index > -1) { + this.setFocus([new GridRange(index, targetColumn)], browserEvent); + } + } + + focusPreviousRow(n = 1, loop = false, browserEvent?: UIEvent, filter?: (element: T) => boolean): void { + if (this.length === 0) { return; } + + const focus = this.focus.get(); + const index = this.findPreviousRowIndex(focus.length > 0 ? focus[0].startRow - n : 0, loop, filter); + + const targetColumn = focus.length > 0 ? focus[0].startColumn : 0; + + if (index > -1) { + this.setFocus([new GridRange(index, targetColumn)], browserEvent); + } + } + + private findNextColumn(index: number, loop = false, filter?: (column: ITableColumn) => boolean): number { + for (let i = 0; i < this.columnLength; i++) { + if (index >= this.columnLength && !loop) { + return -1; + } + + index = index % this.columnLength; + + if (!filter || filter(this.view.column(index)!)) { + return index; + } + + index++; + } + + return -1; + } + + private findPreviousColumn(index: number, loop = false, filter?: (column: ITableColumn) => boolean): number { + for (let i = 0; i < this.columnLength; i++) { + if (index < 0 && !loop) { + return -1; + } + + index = (this.columnLength + (index % this.columnLength)) % this.columnLength; + + if (!filter || filter(this.view.column(index)!)) { + return index; + } + + index--; + } + + return -1; + } + + private findNextRowIndex(index: number, loop = false, filter?: (element: T) => boolean): number { + for (let i = 0; i < this.length; i++) { + if (index >= this.length && !loop) { + return -1; + } + + index = index % this.length; + + // if (!filter || filter(this.view.element(index))) { + return index; + // } + + index++; + } + + return -1; + } + + private findPreviousRowIndex(index: number, loop = false, filter?: (element: T) => boolean): number { + for (let i = 0; i < this.length; i++) { + if (index < 0 && !loop) { + return -1; + } + + index = (this.length + (index % this.length)) % this.length; + + // if (!filter || filter(this.view.element(index))) { + return index; + // } + + index--; + } + + return -1; + } + + getFocus(): Array { + return this.focus.get(); + } + + style(styles: ITableStyles): void { + this.styleController.style(styles); + } + + getHTMLElement(): HTMLElement { + return this.view.domNode; + } + + private _onFocusChange(): void { + const focus = this.focus.get(); + + if (focus.length > 0) { + this.view.domNode.setAttribute('aria-activedescendant', this.view.getElementDomId(focus[0].startRow, focus[0].startColumn)); + } else { + this.view.domNode.removeAttribute('aria-activedescendant'); + } + + this.view.domNode.setAttribute('role', 'tree'); + DOM.toggleClass(this.view.domNode, 'element-focused', focus.length > 0); + } + + private _onSelectionChange(): void { + const selection = this.selection.get(); + + DOM.toggleClass(this.view.domNode, 'selection-none', selection.length === 0); + DOM.toggleClass(this.view.domNode, 'selection-single', selection.length === 1); + DOM.toggleClass(this.view.domNode, 'selection-multiple', selection.length > 1); + } + + dispose(): void { + this._onDidDispose.fire(); + this.disposables.dispose(); + + this._onDidDispose.dispose(); + } +} diff --git a/src/sql/base/browser/ui/table/highPerf/virtualizedWindow.ts b/src/sql/base/browser/ui/table/highPerf/virtualizedWindow.ts new file mode 100644 index 0000000000..da91941260 --- /dev/null +++ b/src/sql/base/browser/ui/table/highPerf/virtualizedWindow.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; + +class DataWindow { + private _data: T[] | undefined; + private _length: number = 0; + private _offsetFromDataSource: number = -1; + + private dataReady?: CancelablePromise; + + constructor( + private loadFunction: (offset: number, count: number) => Promise + ) { } + + dispose() { + this._data = undefined; + if (this.dataReady) { + this.dataReady.cancel(); + } + } + + get start(): number { + return this._offsetFromDataSource; + } + + get end(): number { + return this._offsetFromDataSource + this._length; + } + + get length(): number { + return this._length; + } + + public contains(dataSourceIndex: number): boolean { + return dataSourceIndex >= this.start && dataSourceIndex < this.end; + } + + public getItem(index: number): Promise { + return this.dataReady!.then(() => this._data![index - this._offsetFromDataSource]); + } + + public positionWindow(offset: number, length: number): void { + this._offsetFromDataSource = offset; + this._length = length; + this._data = undefined; + + if (this.dataReady) { + this.dataReady.cancel(); + } + + if (length === 0) { + return; + } + + this.dataReady = createCancelablePromise(token => { + return this.loadFunction(offset, length).then(data => { + if (!token.isCancellationRequested) { + this._data = data; + } + }); + }); + } +} + +export class VirtualizedWindow { + private _bufferWindowBefore: DataWindow; + private _window: DataWindow; + private _bufferWindowAfter: DataWindow; + + constructor( + private readonly windowSize: number, + private _length: number, + loadFn: (offset: number, count: number) => Promise + ) { + + this._bufferWindowBefore = new DataWindow(loadFn); + this._window = new DataWindow(loadFn); + this._bufferWindowAfter = new DataWindow(loadFn); + } + + dispose() { + this._bufferWindowAfter.dispose(); + this._bufferWindowBefore.dispose(); + this._window.dispose(); + } + + get length(): number { + return this._length; + } + + set length(length: number) { + if (this.length !== length) { + const oldLength = this.length; + this._length = length; + if (this._window.length !== this.windowSize) { + this.resetWindowsAroundIndex(oldLength); + } else if (this._bufferWindowAfter.length !== this.windowSize) { + this._bufferWindowAfter.positionWindow(this._bufferWindowAfter.start, Math.min(this._bufferWindowAfter.start + this.windowSize, this.length)); + } + } + } + + public getIndex(index: number): Promise { + + if (index < this._bufferWindowBefore.start || index >= this._bufferWindowAfter.end) { + this.resetWindowsAroundIndex(index); + } + // scrolling up + else if (this._bufferWindowBefore.contains(index)) { + const beforeWindow = this._bufferWindowAfter; + this._bufferWindowAfter = this._window; + this._window = this._bufferWindowBefore; + this._bufferWindowBefore = beforeWindow; + // ensure we aren't buffer invalid data + const beforeStart = Math.max(0, this._window.start - this.windowSize); + // ensure if we got hinder in our start index that we update out length to not overlap + const beforeLength = this._window.start - beforeStart; + this._bufferWindowBefore.positionWindow(beforeStart, beforeLength); + } + // scroll down + else if (this._bufferWindowAfter.contains(index)) { + const afterWindow = this._bufferWindowBefore; + this._bufferWindowBefore = this._window; + this._window = this._bufferWindowAfter; + this._bufferWindowAfter = afterWindow; + // ensure we aren't buffer invalid data + const afterStart = this._window.end; + // ensure if we got hinder in our start index that we update out length to not overlap + const afterLength = afterStart + this.windowSize > this.length ? this.length - afterStart : this.windowSize; + this._bufferWindowAfter.positionWindow(afterStart, afterLength); + } + + // at this point we know the current window will have the index + return this._window.getItem(index); + } + + private resetWindowsAroundIndex(index: number): void { + + let bufferWindowBeforeStart = Math.max(0, index - this.windowSize * 1.5); + let bufferWindowBeforeEnd = Math.max(0, index - this.windowSize / 2); + this._bufferWindowBefore.positionWindow(bufferWindowBeforeStart, bufferWindowBeforeEnd - bufferWindowBeforeStart); + + let mainWindowStart = bufferWindowBeforeEnd; + let mainWindowEnd = Math.min(mainWindowStart + this.windowSize, this.length); + this._window.positionWindow(mainWindowStart, mainWindowEnd - mainWindowStart); + + let bufferWindowAfterStart = mainWindowEnd; + let bufferWindowAfterEnd = Math.min(bufferWindowAfterStart + this.windowSize, this.length); + this._bufferWindowAfter.positionWindow(bufferWindowAfterStart, bufferWindowAfterEnd - bufferWindowAfterStart); + } +} diff --git a/src/sql/base/common/gridPosition.ts b/src/sql/base/common/gridPosition.ts new file mode 100644 index 0000000000..c638b82611 --- /dev/null +++ b/src/sql/base/common/gridPosition.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * A position in a grid. This interface is suitable for serialization. + */ +export interface IGridPosition { + /** + * line number (starts at 1) + */ + readonly row: number; + /** + * column + */ + readonly column: number; +} + +/** + * A position in a grid. + */ +export class GridPosition { + /** + * row (starts at 1) + */ + public readonly row: number; + /** + * column + */ + public readonly column: number; + + constructor(row: number, column: number) { + this.row = row; + this.column = column; + } + + /** + * Create a new postion from this position. + * + * @param newRow new row + * @param newColumn new column + */ + with(newRow: number = this.row, newColumn: number = this.column): GridPosition { + if (newRow === this.row && newColumn === this.column) { + return this; + } else { + return new GridPosition(newRow, newColumn); + } + } + + /** + * Derive a new grid position from this grid position. + * + * @param deltaRow row delta + * @param deltaColumn column delta + */ + delta(deltaRow: number = 0, deltaColumn: number = 0): GridPosition { + return this.with(this.row + deltaRow, this.column + deltaColumn); + } + + /** + * Test if this grid position equals other grid position + */ + public equals(other: IGridPosition): boolean { + return GridPosition.equals(this, other); + } + + /** + * Test if grid position `a` equals grid position `b` + */ + public static equals(a: IGridPosition | null, b: IGridPosition | null): boolean { + if (!a && !b) { + return true; + } + return ( + !!a && + !!b && + a.row === b.row && + a.column === b.column + ); + } + + /** + * Test if this grid position is before other grid position. + * If the two grid positions are equal, the result will be false. + */ + public isBefore(other: IGridPosition): boolean { + return GridPosition.isBefore(this, other); + } + + /** + * Test if grid position `a` is before grid position `b`. + * If the two grid positions are equal, the result will be false. + */ + public static isBefore(a: IGridPosition, b: IGridPosition): boolean { + if (a.row < b.row) { + return true; + } + if (b.row < a.row) { + return false; + } + return a.column < b.column; + } + + /** + * Test if this grid position is before other grid position. + * If the two grid positions are equal, the result will be true. + */ + public isBeforeOrEqual(other: IGridPosition): boolean { + return GridPosition.isBeforeOrEqual(this, other); + } + + /** + * Test if grid position `a` is before grid position `b`. + * If the two grid positions are equal, the result will be true. + */ + public static isBeforeOrEqual(a: IGridPosition, b: IGridPosition): boolean { + if (a.row < b.row) { + return true; + } + if (b.row < a.row) { + return false; + } + return a.column <= b.column; + } + + /** + * A function that compares grid positions, useful for sorting + */ + public static compare(a: IGridPosition, b: IGridPosition): number { + let aRow = a.row | 0; + let bRow = b.row | 0; + + if (aRow === bRow) { + let aColumn = a.column | 0; + let bColumn = b.column | 0; + return aColumn - bColumn; + } + + return aRow - bRow; + } + + /** + * Clone this grid position. + */ + public clone(): GridPosition { + return new GridPosition(this.row, this.column); + } + + /** + * Convert to a human-readable representation. + */ + public toString(): string { + return '(' + this.row + ',' + this.column + ')'; + } + + // --- + + /** + * Create a `GridPosition` from an `IGridPosition`. + */ + public static lift(pos: IGridPosition): GridPosition { + return new GridPosition(pos.row, pos.column); + } + + /** + * Test if `obj` is an `IGridPosition`. + */ + public static isIGridPosition(obj: any): obj is IGridPosition { + return ( + obj + && (typeof obj.row === 'number') + && (typeof obj.column === 'number') + ); + } +} diff --git a/src/sql/base/common/gridRange.ts b/src/sql/base/common/gridRange.ts new file mode 100644 index 0000000000..a397158b9c --- /dev/null +++ b/src/sql/base/common/gridRange.ts @@ -0,0 +1,361 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IGridPosition, GridPosition } from 'sql/base/common/gridPosition'; +import { isNumber } from 'vs/base/common/types'; + +/** + * A range in a grid. This interface is suitable for serialization. + */ +export interface IGridRange { + /** + * Row on which the range starts (starts at 1). + */ + readonly startRow: number; + /** + * Column on which the range starts in line `startRow` (starts at 1). + */ + readonly startColumn: number; + /** + * Row on which the range ends. + */ + readonly endRow: number; + /** + * Column on which the range ends in line `endRow`. + */ + readonly endColumn: number; +} + +/** + * A range in a grid. (startRow,startColumn) is <= (endRow,endColumn) + */ +export class GridRange { + + /** + * Row on which the range starts (starts at 1). + */ + public readonly startRow: number; + /** + * Column on which the range starts in line `startRow` (starts at 1). + */ + public readonly startColumn: number; + /** + * Row on which the range ends. + */ + public readonly endRow: number; + /** + * Column on which the range ends in line `endRow`. + */ + public readonly endColumn: number; + + constructor(startRow: number, startColumn: number, endRow?: number, endColumn?: number) { + this.startRow = isNumber(endRow) ? Math.min(startRow, endRow) : startRow; + this.startColumn = isNumber(endColumn) ? Math.min(startColumn, endColumn) : startColumn; + this.endRow = isNumber(endRow) ? Math.max(endRow, startRow) : startRow; + this.endColumn = isNumber(endColumn) ? Math.max(endColumn, startColumn) : startColumn; + } + + /** + * Test if position is in this range. If the position is at the edges, will return true. + */ + public containsPosition(position: IGridPosition): boolean { + return GridRange.containsPosition(this, position); + } + + /** + * Test if `position` is in `range`. If the position is at the edges, will return true. + */ + public static containsPosition(range: IGridRange, position: IGridPosition): boolean { + return position.row >= range.startRow + && position.row <= range.endRow + && position.column >= range.startColumn + && position.column <= range.endColumn; + } + + /** + * Test if range is in this range. If the range is equal to this range, will return true. + */ + public containsRange(range: IGridRange): boolean { + return GridRange.containsRange(this, range); + } + + /** + * Test if `otherRange` is in `range`. If the ranges are equal, will return true. + */ + public static containsRange(range: IGridRange, otherRange: IGridRange): boolean { + if (otherRange.startRow < range.startRow || otherRange.endRow < range.startRow) { + return false; + } + if (otherRange.startRow > range.endRow || otherRange.endRow > range.endRow) { + return false; + } + if (otherRange.startRow === range.startRow && otherRange.startColumn < range.startColumn) { + return false; + } + if (otherRange.endRow === range.endRow && otherRange.endColumn > range.endColumn) { + return false; + } + return true; + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IGridRange): GridRange { + return GridRange.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IGridRange, b: IGridRange): GridRange { + let startRow = Math.min(a.startRow, b.startRow); + let startColumn = Math.min(a.startColumn, b.startColumn); + let endRow = Math.max(a.endRow, b.endRow); + let endColumn = Math.max(a.endColumn, b.endColumn); + + return new GridRange(startRow, startColumn, endRow, endColumn); + } + + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IGridRange): GridRange | null { + return GridRange.intersectRanges(this, range); + } + + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IGridRange, b: IGridRange): GridRange | null { + let resultStartRow = a.startRow; + let resultStartColumn = a.startColumn; + let resultEndRow = a.endRow; + let resultEndColumn = a.endColumn; + let otherStartRow = b.startRow; + let otherStartColumn = b.startColumn; + let otherEndRow = b.endRow; + let otherEndColumn = b.endColumn; + + if (resultStartRow < otherStartRow) { + resultStartRow = otherStartRow; + resultStartColumn = otherStartColumn; + } else if (resultStartRow === otherStartRow) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); + } + + if (resultEndRow > otherEndRow) { + resultEndRow = otherEndRow; + resultEndColumn = otherEndColumn; + } else if (resultEndRow === otherEndRow) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); + } + + // Check if selection is now empty + if (resultStartRow > resultEndRow) { + return null; + } + if (resultStartRow === resultEndRow && resultStartColumn > resultEndColumn) { + return null; + } + return new GridRange(resultStartRow, resultStartColumn, resultEndRow, resultEndColumn); + } + + /** + * Test if this range equals other. + */ + public equalsRange(other: IGridRange | null): boolean { + return GridRange.equalsRange(this, other); + } + + /** + * Test if range `a` equals `b`. + */ + public static equalsRange(a: IGridRange | null, b: IGridRange | null): boolean { + return ( + !!a && + !!b && + a.startRow === b.startRow && + a.startColumn === b.startColumn && + a.endRow === b.endRow && + a.endColumn === b.endColumn + ); + } + + /** + * Return the end position (which will be after or equal to the start position) + */ + public getEndPosition(): GridPosition { + return new GridPosition(this.endRow, this.endColumn); + } + + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): GridPosition { + return new GridPosition(this.startRow, this.startColumn); + } + + /** + * Transform to a user presentable string representation. + */ + public toString(): string { + return '[' + this.startRow + ',' + this.startColumn + ' -> ' + this.endRow + ',' + this.endColumn + ']'; + } + + /** + * Create a new range using this range's start position, and using endRow and endColumn as the end position. + */ + public setEndPosition(endRow: number, endColumn: number): GridRange { + return new GridRange(this.startRow, this.startColumn, endRow, endColumn); + } + + /** + * Create a new range using this range's end position, and using startRow and startColumn as the start position. + */ + public setStartPosition(startRow: number, startColumn: number): GridRange { + return new GridRange(startRow, startColumn, this.endRow, this.endColumn); + } + + /** + * Create a new empty range using this range's start position. + */ + public collapseToStart(): GridRange { + return GridRange.collapseToStart(this); + } + + /** + * Create a new empty range using this range's start position. + */ + public static collapseToStart(range: IGridRange): GridRange { + return new GridRange(range.startRow, range.startColumn, range.startRow, range.startColumn); + } + + // --- + + public static fromPositions(start: IGridPosition, end: IGridPosition = start): GridRange { + return new GridRange(start.row, start.column, end.row, end.column); + } + + /** + * Create a `GridRange` from an `IGridRange`. + */ + public static lift(range: undefined | null): null; + public static lift(range: IGridRange): GridRange; + public static lift(range: IGridRange | undefined | null): GridRange | null { + if (!range) { + return null; + } + return new GridRange(range.startRow, range.startColumn, range.endRow, range.endColumn); + } + + /** + * Test if `obj` is an `IGridRange`. + */ + public static isIRange(obj: any): obj is IGridRange { + return ( + obj + && (typeof obj.startRow === 'number') + && (typeof obj.startColumn === 'number') + && (typeof obj.endRow === 'number') + && (typeof obj.endColumn === 'number') + ); + } + + /** + * Test if the two ranges are touching in any way. + */ + public static areIntersectingOrTouching(a: IGridRange, b: IGridRange): boolean { + // Check if `a` is before `b` + if (a.endRow < b.startRow || (a.endRow === b.startRow && a.endColumn < b.startColumn)) { + return false; + } + + // Check if `b` is before `a` + if (b.endRow < a.startRow || (b.endRow === a.startRow && b.endColumn < a.startColumn)) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * Test if the two ranges are intersecting. If the ranges are touching it returns true. + */ + public static areIntersecting(a: IGridRange, b: IGridRange): boolean { + // Check if `a` is before `b` + if (a.endRow < b.startRow || (a.endRow === b.startRow && a.endColumn <= b.startColumn)) { + return false; + } + + // Check if `b` is before `a` + if (b.endRow < a.startRow || (b.endRow === a.startRow && b.endColumn <= a.startColumn)) { + return false; + } + + // These ranges must intersect + return true; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the startPosition and then on the endPosition + */ + public static compareRangesUsingStarts(a: IGridRange | null | undefined, b: IGridRange | null | undefined): number { + if (a && b) { + const aStartRow = a.startRow | 0; + const bStartRow = b.startRow | 0; + + if (aStartRow === bStartRow) { + const aStartColumn = a.startColumn | 0; + const bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + const aEndRow = a.endRow | 0; + const bEndRow = b.endRow | 0; + + if (aEndRow === bEndRow) { + const aEndColumn = a.endColumn | 0; + const bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; + } + return aEndRow - bEndRow; + } + return aStartColumn - bStartColumn; + } + return aStartRow - bStartRow; + } + const aExists = (a ? 1 : 0); + const bExists = (b ? 1 : 0); + return aExists - bExists; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the endPosition and then on the startPosition + */ + public static compareRangesUsingEnds(a: IGridRange, b: IGridRange): number { + if (a.endRow === b.endRow) { + if (a.endColumn === b.endColumn) { + if (a.startRow === b.startRow) { + return a.startColumn - b.startColumn; + } + return a.startRow - b.startRow; + } + return a.endColumn - b.endColumn; + } + return a.endRow - b.endRow; + } + + /** + * Test if the range spans multiple lines. + */ + public static spansMultipleLines(range: IGridRange): boolean { + return range.endRow > range.startRow; + } +} diff --git a/src/sql/platform/query/common/query.ts b/src/sql/platform/query/common/query.ts index 1506323bb5..2b9f88c02c 100644 --- a/src/sql/platform/query/common/query.ts +++ b/src/sql/platform/query/common/query.ts @@ -18,7 +18,8 @@ export interface IQueryEditorConfiguration { }, readonly streaming: boolean, readonly copyIncludeHeaders: boolean, - readonly copyRemoveNewLine: boolean + readonly copyRemoveNewLine: boolean, + readonly optimizedTable: boolean }, readonly messages: { readonly showBatchTime: boolean, diff --git a/src/sql/platform/table/browser/tableService.ts b/src/sql/platform/table/browser/tableService.ts new file mode 100644 index 0000000000..9512e2268f --- /dev/null +++ b/src/sql/platform/table/browser/tableService.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Table, DefaultStyleController, ITableOptions } from 'sql/base/browser/ui/table/highPerf/tableWidget'; +import { RawContextKey, IContextKey, ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { createStyleSheet } from 'vs/base/browser/dom'; +import { attachHighPerfTableStyler as attachTableStyler, defaultHighPerfTableStyles } from 'sql/platform/theme/common/styler'; +import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ITableDataSource, ITableColumn } from 'sql/base/browser/ui/table/highPerf/table'; +import { IColorMapping, computeStyles } from 'vs/platform/theme/common/styler'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export const ITableService = createDecorator('tableService'); + +export type TableWidget = Table; + +export interface ITableService { + + _serviceBrand: undefined; + + /** + * Returns the currently focused table widget if any. + */ + readonly lastFocusedTable: TableWidget | undefined; +} + +interface IRegisteredTable { + widget: TableWidget; + extraContextKeys?: (IContextKey)[]; +} + +export class TableService implements ITableService { + + _serviceBrand: undefined; + + private disposables = new DisposableStore(); + private tables: IRegisteredTable[] = []; + private _lastFocusedWidget: TableWidget | undefined = undefined; + + get lastFocusedTable(): TableWidget | undefined { + return this._lastFocusedWidget; + } + + constructor(@IThemeService themeService: IThemeService) { + // create a shared default tree style sheet for performance reasons + const styleController = new DefaultStyleController(createStyleSheet(), ''); + this.disposables.add(attachTableStyler(styleController, themeService)); + } + + register(widget: TableWidget, extraContextKeys?: (IContextKey)[]): IDisposable { + if (this.tables.some(l => l.widget === widget)) { + throw new Error('Cannot register the same widget multiple times'); + } + + // Keep in our tables table + const registeredTable: IRegisteredTable = { widget, extraContextKeys }; + this.tables.push(registeredTable); + + // Check for currently being focused + if (widget.getHTMLElement() === document.activeElement) { + this._lastFocusedWidget = widget; + } + + return combinedDisposable( + widget.onDidFocus(() => this._lastFocusedWidget = widget), + toDisposable(() => this.tables.splice(this.tables.indexOf(registeredTable), 1)), + widget.onDidDispose(() => { + this.tables = this.tables.filter(l => l !== registeredTable); + if (this._lastFocusedWidget === widget) { + this._lastFocusedWidget = undefined; + } + }) + ); + } + + dispose(): void { + this.disposables.dispose(); + } +} + +const RawWorkbenchTableFocusContextKey = new RawContextKey('tableFocus', true); +export const WorkbenchTableFocusContextKey = ContextKeyExpr.and(RawWorkbenchTableFocusContextKey, ContextKeyExpr.not(InputFocusedContextKey)); +export const WorkbenchTableHasSelectionOrFocus = new RawContextKey('tableHasSelectionOrFocus', false); +export const WorkbenchTableDoubleSelection = new RawContextKey('tableDoubleSelection', false); +export const WorkbenchTableMultiSelection = new RawContextKey('tableMultiSelection', false); +export const WorkbenchTableSupportsKeyboardNavigation = new RawContextKey('tableSupportsKeyboardNavigation', true); +export const WorkbenchTableAutomaticKeyboardNavigationKey = 'tableAutomaticKeyboardNavigation'; +export const WorkbenchTableAutomaticKeyboardNavigation = new RawContextKey(WorkbenchTableAutomaticKeyboardNavigationKey, true); +export let didBindWorkbenchTableAutomaticKeyboardNavigation = false; + +function createScopedContextKeyService(contextKeyService: IContextKeyService, widget: TableWidget): IContextKeyService { + const result = contextKeyService.createScoped(widget.getHTMLElement()); + RawWorkbenchTableFocusContextKey.bindTo(result); + return result; +} + +export const multiSelectModifierSettingKey = 'workbench.table.multiSelectModifier'; +export const openModeSettingKey = 'workbench.table.openMode'; +export const horizontalScrollingKey = 'workbench.table.horizontalScrolling'; +export const keyboardNavigationSettingKey = 'workbench.table.keyboardNavigation'; +export const automaticKeyboardNavigationSettingKey = 'workbench.table.automaticKeyboardNavigation'; + +function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean { + return configurationService.getValue(multiSelectModifierSettingKey) === 'alt'; +} + +function toWorkbenchTableOptions(options: ITableOptions): [ITableOptions, IDisposable] { + const disposables = new DisposableStore(); + const result = { ...options }; + + return [result, disposables]; +} + +export interface IWorkbenchTableOptions extends ITableOptions { + readonly overrideStyles?: IColorMapping; +} + +export class WorkbenchTable extends Table { + + readonly contextKeyService: IContextKeyService; + private readonly configurationService: IConfigurationService; + + private tableHasSelectionOrFocus: IContextKey; + private tableDoubleSelection: IContextKey; + private tableMultiSelection: IContextKey; + + private _useAltAsMultipleSelectionModifier: boolean; + + constructor( + user: string, + container: HTMLElement, + columns: ITableColumn[], + dataSource: ITableDataSource, + options: IWorkbenchTableOptions, + @IContextKeyService contextKeyService: IContextKeyService, + @ITableService tableService: ITableService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService + ) { + const [workbenchTableOptions, workbenchTableOptionsDisposable] = toWorkbenchTableOptions(options); + + super(user, container, columns, dataSource, + { + keyboardSupport: false, + ...computeStyles(themeService.getColorTheme(), defaultHighPerfTableStyles), + ...workbenchTableOptions + } + ); + + this.disposables.add(workbenchTableOptionsDisposable); + + this.contextKeyService = createScopedContextKeyService(contextKeyService, this); + this.configurationService = configurationService; + + this.tableHasSelectionOrFocus = WorkbenchTableHasSelectionOrFocus.bindTo(this.contextKeyService); + this.tableDoubleSelection = WorkbenchTableDoubleSelection.bindTo(this.contextKeyService); + this.tableMultiSelection = WorkbenchTableMultiSelection.bindTo(this.contextKeyService); + + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); + + this.disposables.add(this.contextKeyService); + this.disposables.add((tableService as TableService).register(this)); + + if (options.overrideStyles) { + this.disposables.add(attachTableStyler(this, themeService, options.overrideStyles)); + } + + this.disposables.add(this.onSelectionChange(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.tableHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + this.tableMultiSelection.set(selection.length > 1); + this.tableDoubleSelection.set(selection.length === 2); + })); + this.disposables.add(this.onFocusChange(() => { + const selection = this.getSelection(); + const focus = this.getFocus(); + + this.tableHasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); + })); + + this.registerListeners(); + } + + private registerListeners(): void { + this.disposables.add(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(multiSelectModifierSettingKey)) { + this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(this.configurationService); + } + })); + } + + get useAltAsMultipleSelectionModifier(): boolean { + return this._useAltAsMultipleSelectionModifier; + } +} + +registerSingleton(ITableService, TableService, true); diff --git a/src/sql/platform/theme/common/colors.ts b/src/sql/platform/theme/common/colors.ts index 103b2dd31e..478fab523d 100644 --- a/src/sql/platform/theme/common/colors.ts +++ b/src/sql/platform/theme/common/colors.ts @@ -9,12 +9,14 @@ import * as nls from 'vs/nls'; export const tableHeaderBackground = registerColor('table.headerBackground', { dark: new Color(new RGBA(51, 51, 52)), light: new Color(new RGBA(245, 245, 245)), hc: '#333334' }, nls.localize('tableHeaderBackground', "Table header background color")); export const tableHeaderForeground = registerColor('table.headerForeground', { dark: new Color(new RGBA(229, 229, 229)), light: new Color(new RGBA(16, 16, 16)), hc: '#e5e5e5' }, nls.localize('tableHeaderForeground', "Table header foreground color")); +export const listFocusAndSelectionBackground = registerColor('list.focusAndSelectionBackground', { dark: '#2c3295', light: '#2c3295', hc: null }, nls.localize('listFocusAndSelectionBackground', "List/Table background color for the selected and focus item when the list/table is active")); +export const tableCellOutline = registerColor('table.cell.outline', { dark: '#e3e4e229', light: '#33333333', hc: '#e3e4e229' }, nls.localize('tableCellOutline', 'Color of the outline of a cell.')); + export const disabledInputBackground = registerColor('input.disabled.background', { dark: '#444444', light: '#dcdcdc', hc: Color.black }, nls.localize('disabledInputBoxBackground', "Disabled Input box background.")); export const disabledInputForeground = registerColor('input.disabled.foreground', { dark: '#888888', light: '#888888', hc: foreground }, nls.localize('disabledInputBoxForeground', "Disabled Input box foreground.")); export const buttonFocusOutline = registerColor('button.focusOutline', { dark: '#eaeaea', light: '#666666', hc: null }, nls.localize('buttonFocusOutline', "Button outline color when focused.")); export const disabledCheckboxForeground = registerColor('checkbox.disabled.foreground', { dark: '#888888', light: '#888888', hc: Color.black }, nls.localize('disabledCheckboxforeground', "Disabled checkbox foreground.")); -export const listFocusAndSelectionBackground = registerColor('list.focusAndSelectionBackground', { dark: '#2c3295', light: '#2c3295', hc: null }, nls.localize('listFocusAndSelectionBackground', "List/Table background color for the selected and focus item when the list/table is active")); // SQL Agent Colors export const tableBackground = registerColor('agent.tableBackground', { light: '#fffffe', dark: '#333333', hc: Color.black }, nls.localize('agentTableBackground', "SQL Agent Table background color.")); diff --git a/src/sql/platform/theme/common/styler.ts b/src/sql/platform/theme/common/styler.ts index 0f76f6731e..39c823701a 100644 --- a/src/sql/platform/theme/common/styler.ts +++ b/src/sql/platform/theme/common/styler.ts @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as sqlcolors from './colors'; +import * as colors from './colors'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import * as cr from 'vs/platform/theme/common/colorRegistry'; -import { attachStyler } from 'vs/platform/theme/common/styler'; +import { attachStyler, IColorMapping, IStyleOverrides } from 'vs/platform/theme/common/styler'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemable } from 'vs/base/common/styler'; @@ -29,7 +29,7 @@ export function attachDropdownStyler(widget: IThemable, themeService: IThemeServ buttonBackground: (style && style.buttonBackground) || cr.buttonBackground, buttonHoverBackground: (style && style.buttonHoverBackground) || cr.buttonHoverBackground, buttonBorder: cr.contrastBorder, - buttonFocusOutline: (style && style.buttonFocusOutline) || sqlcolors.buttonFocusOutline + buttonFocusOutline: (style && style.buttonFocusOutline) || colors.buttonFocusOutline }, widget); } @@ -50,8 +50,8 @@ export function attachInputBoxStyler(widget: IThemable, themeService: IThemeServ return attachStyler(themeService, { inputBackground: (style && style.inputBackground) || cr.inputBackground, inputForeground: (style && style.inputForeground) || cr.inputForeground, - disabledInputBackground: (style && style.disabledInputBackground) || sqlcolors.disabledInputBackground, - disabledInputForeground: (style && style.disabledInputForeground) || sqlcolors.disabledInputForeground, + disabledInputBackground: (style && style.disabledInputBackground) || colors.disabledInputBackground, + disabledInputForeground: (style && style.disabledInputForeground) || colors.disabledInputForeground, inputBorder: (style && style.inputBorder) || cr.inputBorder, inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || cr.inputValidationInfoBorder, inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || cr.inputValidationInfoBackground, @@ -88,8 +88,8 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer selectListBackground: (style && style.selectListBackground) || cr.selectListBackground, selectForeground: (style && style.selectForeground) || cr.selectForeground, selectBorder: (style && style.selectBorder) || cr.selectBorder, - disabledSelectBackground: (style && style.disabledSelectBackground) || sqlcolors.disabledInputBackground, - disabledSelectForeground: (style && style.disabledSelectForeground) || sqlcolors.disabledInputForeground, + disabledSelectBackground: (style && style.disabledSelectBackground) || colors.disabledInputBackground, + disabledSelectForeground: (style && style.disabledSelectForeground) || colors.disabledInputForeground, inputValidationInfoBorder: (style && style.inputValidationInfoBorder) || cr.inputValidationInfoBorder, inputValidationInfoBackground: (style && style.inputValidationInfoBackground) || cr.inputValidationInfoBackground, inputValidationWarningBorder: (style && style.inputValidationWarningBorder) || cr.inputValidationWarningBorder, @@ -156,7 +156,7 @@ export function attachTableStyler(widget: IThemable, themeService: IThemeService listFocusForeground: (style && style.listFocusForeground) || cr.listFocusForeground, listActiveSelectionBackground: (style && style.listActiveSelectionBackground) || cr.listActiveSelectionBackground, listActiveSelectionForeground: (style && style.listActiveSelectionForeground) || cr.listActiveSelectionForeground, - listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || sqlcolors.listFocusAndSelectionBackground, + listFocusAndSelectionBackground: style && style.listFocusAndSelectionBackground || colors.listFocusAndSelectionBackground, listFocusAndSelectionForeground: (style && style.listFocusAndSelectionForeground) || cr.listActiveSelectionForeground, listInactiveFocusBackground: (style && style.listInactiveFocusBackground), listInactiveSelectionBackground: (style && style.listInactiveSelectionBackground) || cr.listInactiveSelectionBackground, @@ -168,11 +168,60 @@ export function attachTableStyler(widget: IThemable, themeService: IThemeService listSelectionOutline: (style && style.listSelectionOutline) || cr.activeContrastBorder, listHoverOutline: (style && style.listHoverOutline) || cr.activeContrastBorder, listInactiveFocusOutline: style && style.listInactiveFocusOutline, - tableHeaderBackground: (style && style.tableHeaderBackground) || sqlcolors.tableHeaderBackground, - tableHeaderForeground: (style && style.tableHeaderForeground) || sqlcolors.tableHeaderForeground + tableHeaderBackground: (style && style.tableHeaderBackground) || colors.tableHeaderBackground, + tableHeaderForeground: (style && style.tableHeaderForeground) || colors.tableHeaderForeground }, widget); } +export interface ITableStyleOverrides extends IStyleOverrides { + listFocusBackground?: cr.ColorIdentifier, + listFocusForeground?: cr.ColorIdentifier, + listActiveSelectionBackground?: cr.ColorIdentifier, + listActiveSelectionForeground?: cr.ColorIdentifier, + listFocusAndSelectionBackground?: cr.ColorIdentifier, + listFocusAndSelectionForeground?: cr.ColorIdentifier, + listInactiveFocusBackground?: cr.ColorIdentifier, + listInactiveSelectionBackground?: cr.ColorIdentifier, + listInactiveSelectionForeground?: cr.ColorIdentifier, + listHoverBackground?: cr.ColorIdentifier, + listHoverForeground?: cr.ColorIdentifier, + listDropBackground?: cr.ColorIdentifier, + listFocusOutline?: cr.ColorIdentifier, + listInactiveFocusOutline?: cr.ColorIdentifier, + listSelectionOutline?: cr.ColorIdentifier, + listHoverOutline?: cr.ColorIdentifier, + tableHeaderBackground?: cr.ColorIdentifier, + tableHeaderForeground?: cr.ColorIdentifier, + cellOutlineColor?: cr.ColorIdentifier, + tableHeaderAndRowCountColor?: cr.ColorIdentifier +} + +export function attachHighPerfTableStyler(widget: IThemable, themeService: IThemeService, overrides?: IColorMapping): IDisposable { + return attachStyler(themeService, { ...defaultHighPerfTableStyles, ...(overrides || {}) }, widget); +} + +export const defaultHighPerfTableStyles: IColorMapping = { + listFocusBackground: cr.listFocusBackground, + listFocusForeground: cr.listFocusForeground, + listActiveSelectionBackground: cr.listActiveSelectionBackground, + listActiveSelectionForeground: cr.listActiveSelectionForeground, + listFocusAndSelectionBackground: colors.listFocusAndSelectionBackground, + listFocusAndSelectionForeground: cr.listActiveSelectionForeground, + listInactiveFocusBackground: cr.listInactiveFocusBackground, + listInactiveSelectionBackground: cr.listInactiveSelectionBackground, + listInactiveSelectionForeground: cr.listInactiveSelectionForeground, + listHoverBackground: cr.listHoverBackground, + listHoverForeground: cr.listHoverForeground, + listDropBackground: cr.listDropBackground, + listFocusOutline: cr.activeContrastBorder, + listSelectionOutline: cr.activeContrastBorder, + listHoverOutline: cr.activeContrastBorder, + tableHeaderBackground: colors.tableHeaderBackground, + tableHeaderForeground: colors.tableHeaderForeground, + cellOutlineColor: colors.tableCellOutline, + tableHeaderAndRowCountColor: colors.tableCellOutline +}; + export function attachEditableDropdownStyler(widget: IThemable, themeService: IThemeService, style?: { listFocusBackground?: cr.ColorIdentifier, listFocusForeground?: cr.ColorIdentifier, @@ -245,13 +294,13 @@ export function attachButtonStyler(widget: IThemable, themeService: IThemeServic buttonBackground: (style && style.buttonBackground) || cr.buttonBackground, buttonHoverBackground: (style && style.buttonHoverBackground) || cr.buttonHoverBackground, buttonBorder: cr.contrastBorder, - buttonFocusOutline: (style && style.buttonFocusOutline) || sqlcolors.buttonFocusOutline + buttonFocusOutline: (style && style.buttonFocusOutline) || colors.buttonFocusOutline }, widget); } export function attachCheckboxStyler(widget: IThemable, themeService: IThemeService, style?: { disabledCheckboxForeground?: cr.ColorIdentifier }) : IDisposable { return attachStyler(themeService, { - disabledCheckboxForeground: (style && style.disabledCheckboxForeground) || sqlcolors.disabledCheckboxForeground + disabledCheckboxForeground: (style && style.disabledCheckboxForeground) || colors.disabledCheckboxForeground }, widget); } diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index ac8f8b9490..22fcd55ca9 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -135,7 +135,7 @@ class DataResourceTable extends GridTableBase { @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, @IConfigurationService configurationService: IConfigurationService ) { - super(state, createResultSet(source), contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); + super(state, createResultSet(source), undefined, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); this._gridDataProvider = this.instantiationService.createInstance(DataResourceDataProvider, source, this.resultSet, this.cellModel.notebookModel.notebookUri.toString()); this._chart = this.instantiationService.createInstance(ChartView, false); diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index a57ad3f0ce..e29b7ae9c8 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -5,6 +5,7 @@ import 'vs/css!./media/gridPanel'; +import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; import { attachTableStyler } from 'sql/platform/theme/common/styler'; import QueryRunner, { QueryGridDataProvider } from 'sql/workbench/services/query/common/queryRunner'; import { ResultSetSummary, IColumn } from 'sql/workbench/services/query/common/query'; @@ -17,9 +18,9 @@ import { CellSelectionModel } from 'sql/base/browser/ui/table/plugins/cellSelect import { RowNumberColumn } from 'sql/base/browser/ui/table/plugins/rowNumberColumn.plugin'; import { escape } from 'sql/base/common/strings'; import { hyperLinkFormatter, textFormatter } from 'sql/base/browser/ui/table/formatters'; -import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin'; import { AdditionalKeyBindings } from 'sql/base/browser/ui/table/plugins/additionalKeyBindings.plugin'; -import { ITableStyles, ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces'; +import { CopyKeybind } from 'sql/base/browser/ui/table/plugins/copyKeybind.plugin'; +import { GridTable as HighPerfGridTable } from 'sql/workbench/contrib/query/browser/highPerfGridPanel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -27,9 +28,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Emitter, Event } from 'vs/base/common/event'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { range, find } from 'vs/base/common/arrays'; -import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { Disposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; +import { range, find } from 'vs/base/common/arrays'; import { generateUuid } from 'vs/base/common/uuid'; import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; import { isInDOM, Dimension } from 'vs/base/browser/dom'; @@ -47,6 +47,7 @@ import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer import { Progress } from 'vs/platform/progress/common/progress'; import { ScrollableView, IView } from 'sql/base/browser/ui/scrollableView/scrollableView'; import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; +import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; const ROW_HEIGHT = 29; const HEADER_HEIGHT = 26; @@ -64,24 +65,25 @@ const MIN_GRID_HEIGHT = (MIN_GRID_HEIGHT_ROWS * ROW_HEIGHT) + HEADER_HEIGHT + ES export class GridPanel extends Disposable { private container = document.createElement('div'); private scrollableView: ScrollableView; - private tables: GridTable[] = []; + private tables: Array | HighPerfGridTable> = []; private tableDisposable = this._register(new DisposableStore()); private queryRunnerDisposables = this._register(new DisposableStore()); - private currentHeight: number; private runner: QueryRunner; - private maximizedGrid: GridTable; + private maximizedGrid: GridTable | HighPerfGridTable; private _state: GridPanelState | undefined; + private readonly optimized = this.configurationService.getValue('queryEditor').results.optimizedTable; + constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IThemeService private readonly themeService: IThemeService, ) { super(); - this.scrollableView = new ScrollableView(this.container); + this.scrollableView = new ScrollableView(this.container, { scrollDebouce: this.optimized ? 0 : undefined }); this.scrollableView.onDidScroll(e => { if (this.state && this.scrollableView.length !== 0) { this.state.scrollPosition = e.scrollTop; @@ -97,12 +99,7 @@ export class GridPanel extends Disposable { } public layout(size: Dimension): void { - this.scrollableView.layout(size.height); - // if the size hasn't change it won't layout our table so we have to do it manually - if (size.height === this.currentHeight) { - this.tables.map(e => e.layout()); - } - this.currentHeight = size.height; + this.scrollableView.layout(size.height, size.width); } public focus(): void { @@ -203,9 +200,9 @@ export class GridPanel extends Disposable { } private addResultSet(resultSet: ResultSetSummary[]) { - let tables: GridTable[] = []; + const tables: Array | HighPerfGridTable> = []; - for (let set of resultSet) { + for (const set of resultSet) { // ensure we aren't adding a resultSet that is already visible if (find(this.tables, t => t.resultSet.batchId === set.batchId && t.resultSet.id === set.id)) { continue; @@ -220,7 +217,12 @@ export class GridPanel extends Disposable { this.state.tableStates.push(tableState); } } - let table = this.instantiationService.createInstance(GridTable, this.runner, set, tableState); + let table: GridTable | HighPerfGridTable; + if (this.optimized) { + table = this.instantiationService.createInstance(HighPerfGridTable, this.runner, set, tableState); + } else { + table = this.instantiationService.createInstance(GridTable, this.runner, set, tableState); + } this.tableDisposable.add(tableState.onMaximizedChange(e => { if (e) { this.maximizeTable(table.id); @@ -365,12 +367,12 @@ export abstract class GridTableBase extends Disposable implements IView { constructor( state: GridTableState, protected _resultSet: ResultSetSummary, - protected contextMenuService: IContextMenuService, - protected instantiationService: IInstantiationService, - protected editorService: IEditorService, - protected untitledEditorService: IUntitledTextEditorService, - protected configurationService: IConfigurationService, - private readonly options: IGridTableOptions = { actionOrientation: ActionsOrientation.VERTICAL } + private readonly options: IGridTableOptions = { actionOrientation: ActionsOrientation.VERTICAL }, + @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IEditorService private readonly editorService: IEditorService, + @IUntitledTextEditorService private readonly untitledEditorService: IUntitledTextEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid'); @@ -768,7 +770,7 @@ class GridTable extends GridTableBase { @IUntitledTextEditorService untitledEditorService: IUntitledTextEditorService, @IConfigurationService configurationService: IConfigurationService ) { - super(state, resultSet, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); + super(state, resultSet, undefined, contextMenuService, instantiationService, editorService, untitledEditorService, configurationService); this._gridDataProvider = this.instantiationService.createInstance(QueryGridDataProvider, this._runner, resultSet.batchId, resultSet.id); } diff --git a/src/sql/workbench/contrib/query/browser/highPerfGridPanel.ts b/src/sql/workbench/contrib/query/browser/highPerfGridPanel.ts new file mode 100644 index 0000000000..1eed32cf43 --- /dev/null +++ b/src/sql/workbench/contrib/query/browser/highPerfGridPanel.ts @@ -0,0 +1,165 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/optimizedGridPanel'; + +import { ITableRenderer, ITableColumn } from 'sql/base/browser/ui/table/highPerf/table'; +import { VirtualizedWindow } from 'sql/base/browser/ui/table/highPerf/virtualizedWindow'; +import { attachHighPerfTableStyler } from 'sql/platform/theme/common/styler'; +import QueryRunner from 'sql/workbench/services/query/common/queryRunner'; +import { GridTableState } from 'sql/workbench/common/editor/query/gridTableState'; +import { ResultSetSummary } from 'sql/workbench/services/query/common/query'; + +import { append, $ } from 'vs/base/browser/dom'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Emitter, Event } from 'vs/base/common/event'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { WorkbenchTable } from 'sql/platform/table/browser/tableService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IView } from 'sql/base/browser/ui/scrollableView/scrollableView'; + +type ICellTemplate = HTMLElement; + +class TableFormatter implements ITableRenderer { + renderTemplate(container: HTMLElement): ICellTemplate { + return append(container, $('.cell')); + } + + renderCell(element: T, index: number, cellIndex: number, columnId: string, templateData: ICellTemplate, width: number): void { + templateData.innerText = element[columnId]; + } + + disposeCell?(element: T, index: number, cellIndex: number, columnId: string, templateData: ICellTemplate, width: number): void { + templateData.innerText = ''; + } + + disposeTemplate(templateData: ICellTemplate): void { + } + +} + +const ROW_HEIGHT = 22; +const HEADER_HEIGHT = 22; +const MIN_GRID_HEIGHT_ROWS = 8; +const ESTIMATED_SCROLL_BAR_SIZE = 10; +const BOTTOM_PADDING = 15; + +// this handles min size if rows is greater than the min grid visible rows +const MIN_GRID_HEIGHT = (MIN_GRID_HEIGHT_ROWS * ROW_HEIGHT) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_SIZE; + +export class GridTable extends Disposable implements IView { + + private _onDidChange = new Emitter(); + public readonly onDidChange: Event = this._onDidChange.event; + + private virtWindow: VirtualizedWindow; + private table: WorkbenchTable; + private tableContainer: HTMLElement; + private columns: ITableColumn[]; + + public id = generateUuid(); + readonly element = $('.grid-panel.optimized'); + + private _state: GridTableState; + + private rowHeight: number; + + public get resultSet(): ResultSetSummary { + return this._resultSet; + } + + // this handles if the row count is small, like 4-5 rows + private get maxSize(): number { + return ((this.resultSet.rowCount) * this.rowHeight) + HEADER_HEIGHT + ESTIMATED_SCROLL_BAR_SIZE; + } + + // worthless for this table + public isOnlyTable: boolean; + + constructor( + private readonly runner: QueryRunner, + private _resultSet: ResultSetSummary, + state: GridTableState, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.tableContainer = append(this.element, $('.table-panel')); + let config = this.configurationService.getValue<{ rowHeight: number }>('resultsGrid'); + this.rowHeight = config && config.rowHeight ? config.rowHeight : ROW_HEIGHT; + this.state = state; + + this.columns = this.resultSet.columnInfo.map>((c, i) => ({ + id: i.toString(), + name: c.columnName === 'Microsoft SQL Server 2005 XML Showplan' + ? 'XML Showplan' + : escape(c.columnName), + renderer: new TableFormatter(), + width: this.state.columnSizes && this.state.columnSizes[i] ? this.state.columnSizes[i] : undefined + })); + + this.virtWindow = new VirtualizedWindow(50, this.resultSet.rowCount, (offset, count) => { + return Promise.resolve(this.runner.getQueryRows(offset, count, this._resultSet.batchId, this._resultSet.id).then(r => { + return r.rows.map(c => c.reduce((p, c, i) => { + p[this.columns[i].id] = c.displayValue; + return p; + }, Object.create(null))); + })); + }); + + this.table = this._register(this.instantiationService.createInstance(WorkbenchTable, 'gridPanel', this.tableContainer, this.columns, { + getRow: index => this.virtWindow.getIndex(index) + }, { rowHeight: this.rowHeight, headerHeight: HEADER_HEIGHT, rowCountColumn: false }) as WorkbenchTable); + + this.table.length = this.resultSet.rowCount; + + this._register(attachHighPerfTableStyler(this.table, this.themeService)); + } + + public get state(): GridTableState { + return this._state; + } + + public set state(val: GridTableState) { + this._state = val; + } + + public updateResult(resultSet: ResultSetSummary) { + this._resultSet = resultSet; + if (this.table) { + this.virtWindow.length = resultSet.rowCount; + this.table.length = resultSet.rowCount; + } + this._onDidChange.fire(undefined); + } + + public layout(height: number, width: number): void { + this.tableContainer.style.width = `${width - ESTIMATED_SCROLL_BAR_SIZE}px`; + this.table.layout(height, width - ESTIMATED_SCROLL_BAR_SIZE); + } + + public get minimumSize(): number { + // clamp between ensuring we can show the actionbar, while also making sure we don't take too much space + // if there is only one table then allow a minimum size of ROW_HEIGHT + return Math.max(Math.min(this.maxSize, MIN_GRID_HEIGHT), BOTTOM_PADDING); + } + + public get maximumSize(): number { + return Math.max(this.maxSize, BOTTOM_PADDING); + } + + public dispose() { + this.element.remove(); + super.dispose(); + } + + public style(): void { + } + + public focus() { } +} diff --git a/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css b/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css deleted file mode 100644 index 74a4e1f99f..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .binarydiff-left { - float: left; -} - -.monaco-workbench .binarydiff-right { - border-left: 3px solid #DDD; -} - -.monaco-workbench.vs-dark .binarydiff-right { - border-left: 3px solid rgb(20, 20, 20); -} - -.monaco-workbench.hc-black .binarydiff-right { - border-left: 3px solid #6FC3DF; -} diff --git a/src/sql/workbench/contrib/query/browser/media/close-dirty-inverse.svg b/src/sql/workbench/contrib/query/browser/media/close-dirty-inverse.svg deleted file mode 100644 index 02dafab76f..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/close-dirty-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/close-dirty.svg b/src/sql/workbench/contrib/query/browser/media/close-dirty.svg deleted file mode 100644 index 409e5fa539..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/close-dirty.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/close-inverse.svg b/src/sql/workbench/contrib/query/browser/media/close-inverse.svg deleted file mode 100644 index 751e89b3b0..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/close-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/close.svg b/src/sql/workbench/contrib/query/browser/media/close.svg deleted file mode 100644 index fde34404d4..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/close.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css b/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css deleted file mode 100644 index c2f3548ea3..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css +++ /dev/null @@ -1,152 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench.vs > .editor > .content.drag { - background-color: #ECECEC; -} - -.monaco-workbench.vs-dark > .editor > .content.drag { - background-color: #2D2D2D; -} - -.monaco-workbench > .editor > .content.dragging > .monaco-sash { - display: none; /* hide sashes while dragging editors around */ -} - -#monaco-workbench-editor-move-overlay, -#monaco-workbench-editor-drop-overlay { - position: absolute; - left: 0; - width: 100%; - z-index: 10000; -} - -#monaco-workbench-editor-drop-overlay { - opacity: 0; /* initially not visible until moving around */ -} - -.vs #monaco-workbench-editor-drop-overlay, -.monaco-workbench.vs > .editor.empty > .content.dropfeedback { - background-color: rgba(51,153,255, 0.18); -} - -.vs-dark #monaco-workbench-editor-drop-overlay, -.monaco-workbench.vs-dark > .editor.empty > .content.dropfeedback { - background-color: rgba(83, 89, 93, 0.5); -} - -.hc-black #monaco-workbench-editor-drop-overlay, -.monaco-workbench.hc-black > .editor.empty > .content.dropfeedback { - background: none !important; - outline: 2px dashed #f38518; - outline-offset: -2px; -} - -.monaco-workbench > .editor > .content > .one-editor-silo { - position: absolute; - box-sizing: border-box; /* use border box to be able to draw a border as separator between editors */ -} - -.monaco-workbench > .editor > .content > .one-editor-silo.editor-one { - left: 0; - top: 0; -} - -.monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three { - right: 0; -} - -.monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { - bottom: 0; -} - -.monaco-workbench > .editor > .content > .one-editor-silo.dragging { - z-index: 70; - box-sizing: content-box; -} - -.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.dragging { - border-left: 1px solid #E7E7E7; - border-right: 1px solid #E7E7E7; -} - -.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.dragging { - border-top: 1px solid #E7E7E7; - border-bottom: 1px solid #E7E7E7; -} - -.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.editor-three { - border-left: 1px solid #E7E7E7; -} - -.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { - border-top: 1px solid #E7E7E7; -} - -.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.dragging { - border-left: 1px solid #444; - border-right: 1px solid #444; -} - -.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.dragging { - border-top: 1px solid #444; - border-bottom: 1px solid #444; -} - -.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.editor-three { - border-left: 1px solid #444; -} - -.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { - border-top: 1px solid #444; -} - -.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.dragging { - border-left: 1px solid #6FC3DF; - border-right: 1px solid #6FC3DF; -} - -.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.dragging { - border-top: 1px solid #6FC3DF; - border-bottom: 1px solid #6FC3DF; -} - -.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.editor-three { - border-left: 1px solid #6FC3DF; -} - -.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { - border-top: 1px solid #6FC3DF; -} - -.monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.draggedunder { - transition: left 200ms ease-out; -} - -.monaco-workbench > .editor > .content.vertical-layout > .editor-three.draggedunder { - transition-property: right; -} - -.monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.draggedunder { - transition: top 200ms ease-out; -} - -.monaco-workbench > .editor > .content.horizontal-layout > .editor-three.draggedunder { - transition-property: bottom; -} - -.monaco-workbench > .editor > .content > .one-editor-silo > .container { - height: 100%; -} - -.monaco-workbench > .editor > .content > .one-editor-silo > .container > .editor-container { - height: calc(100% - 35px); /* Editor is below editor title */ -} diff --git a/src/sql/workbench/contrib/query/browser/media/editorpart.css b/src/sql/workbench/contrib/query/browser/media/editorpart.css deleted file mode 100644 index 4489ddaef0..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/editorpart.css +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench.vs .monaco-editor-background { - background-color: white; -} - -.monaco-workbench.vs-dark .monaco-editor-background { - background-color: #1E1E1E; -} - -.monaco-workbench.hc-black .monaco-editor-background { - background-color: #000; -} - -.monaco-workbench .part.editor { - background-repeat: no-repeat; - background-position: 50% 50%; -} - -.monaco-workbench .part.editor.empty { - background-image: url('letterpress.svg'); -} - -.monaco-workbench.vs-dark .part.editor.empty { - background-image: url('letterpress-dark.svg'); -} - -.monaco-workbench.hc-black .part.editor.empty { - background-image: url('letterpress-hc.svg'); -} - -@media -(-webkit-min-device-pixel-ratio: 2), -(min-resolution: 192dppx) { - .monaco-workbench .part.editor { - background-size: 260px 260px; - } -} diff --git a/src/sql/workbench/contrib/query/browser/media/editorstatus.css b/src/sql/workbench/contrib/query/browser/media/editorstatus.css deleted file mode 100644 index b889f317b8..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/editorstatus.css +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .editor-statusbar-item > a:not(:first-child) { - margin-left: 5px; -} - -.monaco-workbench .editor-statusbar-item > .editor-status-mode, -.monaco-workbench .editor-statusbar-item > .editor-status-encoding, -.monaco-workbench .editor-statusbar-item > .editor-status-eol, -.monaco-workbench .editor-statusbar-item > .editor-status-selection, -.monaco-workbench .editor-statusbar-item > .editor-status-indentation, -.monaco-workbench .editor-statusbar-item > .editor-status-metadata { - padding: 0 5px 0 5px; -} - -.monaco-workbench .editor-statusbar-item > .editor-status-metadata { - cursor: default; -} - -.monaco-workbench .editor-statusbar-item > .editor-status-tabfocusmode { - padding: 0 5px 0 5px; - background-color: brown !important; -} \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/gridPanel.css b/src/sql/workbench/contrib/query/browser/media/gridPanel.css index b35e0900cf..45c8406f22 100644 --- a/src/sql/workbench/contrib/query/browser/media/gridPanel.css +++ b/src/sql/workbench/contrib/query/browser/media/gridPanel.css @@ -13,3 +13,8 @@ width : 100%; display: flex; } + +.grid-panel .cell { + text-overflow: ellipsis; + overflow: hidden; +} diff --git a/src/sql/workbench/contrib/query/browser/media/letterpress-dark.svg b/src/sql/workbench/contrib/query/browser/media/letterpress-dark.svg deleted file mode 100644 index 5cd4a656db..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/letterpress-dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/letterpress-hc.svg b/src/sql/workbench/contrib/query/browser/media/letterpress-hc.svg deleted file mode 100644 index 38b46ee704..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/letterpress-hc.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/letterpress.svg b/src/sql/workbench/contrib/query/browser/media/letterpress.svg deleted file mode 100644 index 41a76de75b..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/letterpress.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/next-diff-inverse.svg b/src/sql/workbench/contrib/query/browser/media/next-diff-inverse.svg deleted file mode 100644 index 16a6dc9063..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/next-diff-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/next-diff.svg b/src/sql/workbench/contrib/query/browser/media/next-diff.svg deleted file mode 100644 index 8befbea7ba..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/next-diff.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/notabstitle.css b/src/sql/workbench/contrib/query/browser/media/notabstitle.css deleted file mode 100644 index b33a7276ce..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/notabstitle.css +++ /dev/null @@ -1,38 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Title Label */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label { - line-height: 35px; - overflow: hidden; - text-overflow: ellipsis; - position: relative; - padding-left: 20px; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .monaco-icon-label::before { - height: 35px; /* tweak the icon size of the editor labels when icons are enabled */ -} - -/* Title Actions */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions { - display: flex; - flex: initial; - opacity: 0.5; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-actions { - opacity: 1; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { - background: url('close-dirty.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { - background: url('close-dirty-inverse.svg') center center no-repeat; -} diff --git a/src/sql/workbench/contrib/query/browser/media/editorpicker.css b/src/sql/workbench/contrib/query/browser/media/optimizedGridPanel.css similarity index 73% rename from src/sql/workbench/contrib/query/browser/media/editorpicker.css rename to src/sql/workbench/contrib/query/browser/media/optimizedGridPanel.css index c02d6e8c0a..90e9c91421 100644 --- a/src/sql/workbench/contrib/query/browser/media/editorpicker.css +++ b/src/sql/workbench/contrib/query/browser/media/optimizedGridPanel.css @@ -3,6 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .quick-open-widget .quick-open-tree .quick-open-entry.editor-preview { - font-style: italic; +.grid-panel.optimized { + width: 100%; + height: 100%; +} + +.grid-panel.optimized > .table-panel { + width: 100%; + height: 100%; } \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/parseQuery.svg b/src/sql/workbench/contrib/query/browser/media/parseQuery.svg deleted file mode 100644 index 8237d08f43..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/parseQuery.svg +++ /dev/null @@ -1 +0,0 @@ -ParseQuery_16x \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/previous-diff-inverse.svg b/src/sql/workbench/contrib/query/browser/media/previous-diff-inverse.svg deleted file mode 100644 index db3d321841..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/previous-diff-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/previous-diff.svg b/src/sql/workbench/contrib/query/browser/media/previous-diff.svg deleted file mode 100644 index 508d57b2f1..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/previous-diff.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/runWithoutDebug.svg b/src/sql/workbench/contrib/query/browser/media/runWithoutDebug.svg deleted file mode 100644 index e223af6c57..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/runWithoutDebug.svg +++ /dev/null @@ -1 +0,0 @@ -StartWithoutDebug@2x \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal-inverse.svg b/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal-inverse.svg deleted file mode 100644 index 4969d2e785..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal.svg b/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal.svg deleted file mode 100644 index c307f0142b..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/split-editor-horizontal.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/split-editor-vertical-inverse.svg b/src/sql/workbench/contrib/query/browser/media/split-editor-vertical-inverse.svg deleted file mode 100644 index 4eab753669..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/split-editor-vertical-inverse.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/split-editor-vertical.svg b/src/sql/workbench/contrib/query/browser/media/split-editor-vertical.svg deleted file mode 100644 index 3eeaf7c536..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/split-editor-vertical.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/sql/workbench/contrib/query/browser/media/stackview-inverse.svg b/src/sql/workbench/contrib/query/browser/media/stackview-inverse.svg deleted file mode 100644 index 8d9a603ae8..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/stackview-inverse.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/sql/workbench/contrib/query/browser/media/stackview.svg b/src/sql/workbench/contrib/query/browser/media/stackview.svg deleted file mode 100644 index 9ea38002d9..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/stackview.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/sql/workbench/contrib/query/browser/media/tabstitle.css b/src/sql/workbench/contrib/query/browser/media/tabstitle.css deleted file mode 100644 index 7e5756ef18..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/tabstitle.css +++ /dev/null @@ -1,220 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Title Container */ - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.tabs { - background: #F3F3F3; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.tabs { - background: #252526; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs > .monaco-scrollable-element { - flex: 1; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs > .monaco-scrollable-element .scrollbar { - z-index: 3; /* on top of tabs */ - cursor: default; -} - -/* Tabs Container */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container { - display: flex; - height: 35px; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.scroll { - overflow: scroll !important; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container::-webkit-scrollbar { - display: none; -} - -/* Tab */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { - display: flex; - width: 120px; - min-width: fit-content; - white-space: nowrap; - cursor: pointer; - height: 35px; - box-sizing: border-box; - border: 1px solid transparent; - padding-left: 10px; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { - background-color: #ECECEC; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { - background-color: #2D2D2D; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { - border-left-color: #F3F3F3; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { - border-right-color: #F3F3F3; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { - border-left-color: #252526; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { - border-right-color: #252526; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child, -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child { - border-left-color: transparent; -} - -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { - border-left-color: #6FC3DF; -} - -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active { - outline: 2px solid #f38518; - outline-offset: -1px; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { - background-color: #DDECFF; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { - background-color: #383B3D; -} - -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { - background: none !important; - outline: 2px dashed #f38518; - outline-offset: -2px; -} - -/* Tab Label */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { - margin-top: auto; - margin-bottom: auto; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .monaco-icon-label::before { - height: 16px; /* tweak the icon size of the editor labels when icons are enabled */ -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { - opacity: 0.7 !important; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { - opacity: 0.5 !important; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active .tab-label, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback .tab-label { - opacity: 1 !important; -} - -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { - opacity: 1 !important; -} - -/* Tab Close */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab > .tab-close { - margin-top: auto; - margin-bottom: auto; - width: 28px; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button > .tab-close { - display: none; /* hide the close action bar when we are configured to hide it */ -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.active > .tab-close .action-label, /* always show it for active tab */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab > .tab-close .action-label:focus, /* always show it on focus */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab:hover > .tab-close .action-label, /* always show it on hover */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.active:hover > .tab-close .action-label, /* always show it on hover */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab.dirty > .tab-close .action-label { /* always show it for dirty tabs */ - opacity: 1; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:hover > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty > .tab-close .action-label, /* show dimmed for inactive group */ -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:hover > .tab-close .action-label { /* show dimmed for inactive group */ - opacity: 0.5; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab > .tab-close .action-label { - opacity: 0; - display: block; - height: 16px; - width: 16px; - background-size: 16px; - background-position: center center; - background-repeat: no-repeat; - margin-right: 0.5em; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { - background: url('close-dirty.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { - background: url('close-dirty-inverse.svg') center center no-repeat; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { - background: url('close.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { - background: url('close-inverse.svg') center center no-repeat; -} - -/* No Tab Close Button */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button { - padding-right: 28px; /* make room for dirty indication when we are running without close button */ -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { - background-repeat: no-repeat; - background-position-y: center; - background-position-x: calc(100% - 6px); /* to the right of the tab label */ -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { - background-image: url('close-dirty.svg'); -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { - background-image: url('close-dirty-inverse.svg'); -} - -/* Editor Actions */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions { - cursor: default; - flex: initial; - padding-left: 4px; -} diff --git a/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css b/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css deleted file mode 100644 index 7d59b5590d..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench.vs .textdiff-editor-action.next { - background: url('next-diff.svg') center center no-repeat; -} - -.monaco-workbench.vs .textdiff-editor-action.previous { - background: url('previous-diff.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark .textdiff-editor-action.next, -.monaco-workbench.hc-black .textdiff-editor-action.next { - background: url('next-diff-inverse.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark .textdiff-editor-action.previous, -.monaco-workbench.hc-black .textdiff-editor-action.previous { - background: url('previous-diff-inverse.svg') center center no-repeat; -} diff --git a/src/sql/workbench/contrib/query/browser/media/titlecontrol.css b/src/sql/workbench/contrib/query/browser/media/titlecontrol.css deleted file mode 100644 index a6a275ae7c..0000000000 --- a/src/sql/workbench/contrib/query/browser/media/titlecontrol.css +++ /dev/null @@ -1,127 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/* Editor Label */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { - white-space: nowrap; - flex: 1; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { - text-decoration: none; - font-size: 13px; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label span, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label span { - cursor: pointer; -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { - color: rgba(51, 51, 51, 0.5); -} - -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, -.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { - color: #333333; -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { - color: rgba(255, 255, 255, 0.5); -} - -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, -.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { - color: white; -} - -/* Title Actions */ - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label { - display: block; - height: 35px; - line-height: 35px; - min-width: 28px; - background-size: 16px; - background-position: center center; - background-repeat: no-repeat; -} - -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label, -.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label { - line-height: initial; -} - -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label .label, -.monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label .label { - display: none; -} - -/* Drag Cursor */ -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title.tabs .scrollbar .slider, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .title-label a, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo > .container > .title .title-label span { - cursor: -webkit-grab; -} - -#monaco-workbench-editor-move-overlay, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title.tabs .scrollbar .slider, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .monaco-icon-label::before, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .title-label a, -.monaco-workbench > .part.editor > .content.multiple-editors > .one-editor-silo.drag > .container > .title .title-label span { - cursor: -webkit-grabbing; -} - -/* Actions */ - -.monaco-workbench .close-editor-action { - background: url('close.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark .close-editor-action, -.monaco-workbench.hc-black .close-editor-action { - background: url('close-inverse.svg') center center no-repeat; -} - -.monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action { - background: url('split-editor-vertical.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action, -.monaco-workbench.hc-black > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action { - background: url('split-editor-vertical-inverse.svg') center center no-repeat; -} - -.monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action { - background: url('split-editor-horizontal.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action, -.monaco-workbench.hc-black > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action { - background: url('split-editor-horizontal-inverse.svg') center center no-repeat; -} - -.monaco-workbench .show-group-editors-action { - background: url('stackview.svg') center center no-repeat; -} - -.monaco-workbench.vs-dark .show-group-editors-action, -.monaco-workbench.hc-black .show-group-editors-action { - background: url('stackview-inverse.svg') center center no-repeat; -} diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 75c425d702..39e06a419c 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -365,6 +365,11 @@ const queryEditorConfiguration: IConfigurationNode = { 'description': localize('queryEditor.results.copyRemoveNewLine', "Configuration options for copying multi-line results from the Results View"), 'default': true }, + 'queryEditor.results.optimizedTable': { + 'type': 'boolean', + 'description': localize('queryEditor.results.optimizedTable', "(Experimental) Use a optimized table in the results out. Some functionality might be missing and in the works."), + 'default': false + }, 'queryEditor.messages.showBatchTime': { 'type': 'boolean', 'description': localize('queryEditor.messages.showBatchTime', "Should execution time be shown for individual batches"),