Better table implementation (#11781)

* wip

* wip

* weird splitview scrolling stuff

* working table

* remove spliceable table

* handling resizing columns

* get perf table integrated into grid

* make more improvments to table view

* testing

* wip

* wip

* fix async data window; add more optimization to scrolling

* work on scrolling

* fix column resizing

* start working on table widget

* inital work to get table widget working with styles and mouse controls

* fix unrendering selection; fix sizes of cells

* support high perf table option; remove unused files; add cell borders to high perf

* add accessibility tags

* handle borders and row count

* more styling changfes

* fix strict null checks

* adding inital keyboard navigation

* center row count; add padding left to rows

* inital drag selection

* remove drag implementation; it can be done better utilizing the global mouse monitor object

* range logic

* create custom grid range

* work with new range

* remove unused code

* fix how plus range works

* add drag selection; change focus to set selection; fix problem with creating a range with inverse start and end

* code cleanup

* fix strict-null-checks

* fix up perf table

* fix layering

* inital table service

* finish table service

* fix some compile errors

* fix compile

* fix compile

* fix up for use

* fix layering

* remove console use

* fix strict nulls
This commit is contained in:
Anthony Dresser
2020-08-18 12:10:05 -07:00
committed by GitHub
parent 17856855f6
commit c4b524237c
47 changed files with 3276 additions and 753 deletions

View File

@@ -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')
);
}
}

View File

@@ -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;
}
}