diff --git a/src/sql/base/browser/ui/table/plugins/cellRangeSelector.ts b/src/sql/base/browser/ui/table/plugins/cellRangeSelector.ts index ed1523ee4a..3cdfc51c3b 100644 --- a/src/sql/base/browser/ui/table/plugins/cellRangeSelector.ts +++ b/src/sql/base/browser/ui/table/plugins/cellRangeSelector.ts @@ -28,6 +28,7 @@ export interface ICellRangeSelectorOptions { export interface ICellRangeSelector extends Slick.Plugin { onCellRangeSelected: Slick.Event; onBeforeCellRangeSelected: Slick.Event; + onAppendCellRangeSelected: Slick.Event; } export interface ICellRangeDecorator { @@ -45,6 +46,7 @@ export class CellRangeSelector implements ICellRangeSelector { public onBeforeCellRangeSelected = new Slick.Event(); public onCellRangeSelected = new Slick.Event(); + public onAppendCellRangeSelected = new Slick.Event(); constructor(private options: ICellRangeSelectorOptions) { require.__$__nodeRequire('slickgrid/plugins/slick.cellrangedecorator'); @@ -138,11 +140,18 @@ export class CellRangeSelector implements ICellRangeSelector { if (!dd || !dd.range || !dd.range.start || !dd.range.end) { return; } - this.onCellRangeSelected.notify(new Slick.Range( + + let newRange = new Slick.Range( dd.range.start.row, dd.range.start.cell, dd.range.end.row, dd.range.end.cell - )); + ); + + if (e.ctrlKey) { + this.onAppendCellRangeSelected.notify(newRange); + } else { + this.onCellRangeSelected.notify(newRange); + } } } diff --git a/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts b/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts index 698469604d..c57b1a669f 100644 --- a/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/cellSelectionModel.plugin.ts @@ -36,11 +36,14 @@ export class CellSelectionModel implements Slick.SelectionModel) { this.grid = grid; - this._handler.subscribe(this.grid.onActiveCellChanged, (e: Event, args: Slick.OnActiveCellChangedEventArgs) => this.handleActiveCellChange(e, args)); + this._handler.subscribe(this.grid.onClick, (e: MouseEvent, args: Slick.OnActiveCellChangedEventArgs) => this.handleActiveCellChange(e, args)); this._handler.subscribe(this.grid.onKeyDown, (e: KeyboardEvent) => this.handleKeyDown(e)); + this._handler.subscribe(this.grid.onClick, (e: MouseEvent, args: Slick.OnClickEventArgs) => this.handleIndividualCellSelection(e, args)); this._handler.subscribe(this.grid.onHeaderClick, (e: MouseEvent, args: Slick.OnHeaderClickEventArgs) => this.handleHeaderClick(e, args)); this.grid.registerPlugin(this.selector); - this._handler.subscribe(this.selector.onCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range)); + this._handler.subscribe(this.selector.onCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range, false)); + this._handler.subscribe(this.selector.onAppendCellRangeSelected, (e: Event, range: Slick.Range) => this.handleCellRangeSelected(e, range, true)); + this._handler.subscribe(this.selector.onBeforeCellRangeSelected, (e: Event, cell: Slick.Cell) => this.handleBeforeCellRangeSelected(e, cell)); } @@ -87,13 +90,18 @@ export class CellSelectionModel implements Slick.SelectionModel) { - if (this.options.selectActiveCell && !isUndefinedOrNull(args.row) && !isUndefinedOrNull(args.cell)) { + private handleActiveCellChange(e: MouseEvent, args: Slick.OnActiveCellChangedEventArgs) { + if (this.options.selectActiveCell && !isUndefinedOrNull(args.row) && !isUndefinedOrNull(args.cell) && !e.ctrlKey) { this.setSelectedRanges([new Slick.Range(args.row, args.cell)]); } else if (!this.options.selectActiveCell) { // clear the previous selection once the cell changes @@ -118,6 +126,119 @@ export class CellSelectionModel implements Slick.SelectionModel, range: Slick.Range) { + // New ranges selection + let newRanges: Array = []; + + // Have we handled this value + let handled = false; + for (let current of ranges) { + // We've already processed everything. Add everything left back to the list. + if (handled) { + newRanges.push(current); + continue; + } + let newRange: Slick.Range | undefined = undefined; + + // if the ranges are the same. + if (current.fromRow === range.fromRow && + current.fromCell === range.fromCell && + current.toRow === range.toRow && + current.toCell === range.toCell) { + // If we're actually not going to handle it during this loop + // this region will be added with the handled boolean check + continue; + } + + // Rows are the same - horizontal merging of the selection area + if (current.fromRow === range.fromRow && current.toRow === range.toRow) { + // Check if the new region is adjacent to the old selection group + if (range.toCell + 1 === current.fromCell || range.fromCell - 1 === current.toCell) { + handled = true; + let fromCell = Math.min(range.fromCell, current.fromCell, range.toCell, current.toCell); + let toCell = Math.max(range.fromCell, current.fromCell, range.toCell, current.toCell); + newRange = new Slick.Range(range.fromRow, fromCell, range.toRow, toCell); + } + // Cells are the same - vertical merging of the selection area + } else if (current.fromCell === range.fromCell && current.toCell === range.toCell) { + // Check if the new region is adjacent to the old selection group + if (range.toRow + 1 === current.fromRow || range.fromRow - 1 === current.toRow) { + handled = true; + let fromRow = Math.min(range.fromRow, current.fromRow, range.fromRow, current.fromRow); + let toRow = Math.max(range.toRow, current.toRow, range.toRow, current.toRow); + newRange = new Slick.Range(fromRow, range.fromCell, toRow, range.toCell); + } + } + + if (newRange) { + newRanges.push(newRange); + } else { + newRanges.push(current); + } + } + + if (!handled) { + newRanges.push(range); + } + + return { + newRanges, + handled + }; + } + + private insertIntoSelections(ranges: Array, range: Slick.Range): Array { + let result = this.mergeSelections(ranges, range); + let newRanges = result.newRanges; + + // Keep merging the rows until we stop having changes + let i = 0; + while (true) { + if (i++ > 10000) { + console.error('InsertIntoSelection infinite loop: Report this error on github'); + break; + } + let shouldContinue = false; + for (let current of newRanges) { + result = this.mergeSelections(newRanges, current); + if (result.handled) { + shouldContinue = true; + newRanges = result.newRanges; + break; + } + } + + if (shouldContinue) { + continue; + } + break; + } + + return newRanges; + } + + private handleIndividualCellSelection(e: MouseEvent, args: Slick.OnClickEventArgs) { + if (!e.ctrlKey) { + return; + } + + let ranges: Array; + + ranges = this.getSelectedRanges(); + ranges = this.insertIntoSelections(ranges, new Slick.Range(args.row, args.cell)); + + this.grid.setActiveCell(args.row, args.cell); + this.setSelectedRanges(ranges); + + e.preventDefault(); + e.stopImmediatePropagation(); + } + private handleKeyDown(e: KeyboardEvent) { /*** * Кey codes diff --git a/src/sql/platform/query/common/gridDataProvider.ts b/src/sql/platform/query/common/gridDataProvider.ts index 591bb1abba..602013986a 100644 --- a/src/sql/platform/query/common/gridDataProvider.ts +++ b/src/sql/platform/query/common/gridDataProvider.ts @@ -43,22 +43,27 @@ export interface IGridDataProvider { } export async function getResultsString(provider: IGridDataProvider, selection: Slick.Range[], includeHeaders?: boolean): Promise { + let headers: Map = new Map(); + let rows: Map> = new Map(); let copyTable: string[][] = []; const eol = provider.getEolString(); // create a mapping of the ranges to get promises let tasks = selection.map((range, i) => { return async () => { + let selectionsCopy = selection; + let startCol = range.fromCell; + let startRow = range.fromRow; + const result = await provider.getRowData(range.fromRow, range.toRow - range.fromRow + 1); // If there was a previous selection separate it with a line break. Currently // when there are multiple selections they are never on the same line - if (provider.shouldIncludeHeaders(includeHeaders)) { - let columnHeaders = provider.getColumnHeaders(range); - if (columnHeaders !== undefined) { - if (copyTable[0] === undefined) { - copyTable[0] = []; - } - copyTable[0].push(...columnHeaders); + let columnHeaders = provider.getColumnHeaders(range); + if (columnHeaders !== undefined) { + let idx = 0; + for (let header of columnHeaders) { + headers.set(startCol + idx, header); + idx++; } } // Iterate over the rows to paste into the copy string @@ -70,11 +75,17 @@ export async function getResultsString(provider: IGridDataProvider, selection: S ? cellObjects.map(x => removeNewLines(x.displayValue)) : cellObjects.map(x => x.displayValue); - let idx = rowIndex + 1; - if (copyTable[idx] === undefined) { - copyTable[idx] = []; + let idx = 0; + for (let cell of cells) { + let map = rows.get(rowIndex + startRow); + if (!map) { + map = new Map(); + rows.set(rowIndex + startRow, map); + } + + map.set(startCol + idx, cell); + idx++; } - copyTable[idx].push(...cells); } }; }); @@ -88,13 +99,23 @@ export async function getResultsString(provider: IGridDataProvider, selection: S } let copyString = ''; - copyTable.forEach((row) => { - if (row === undefined) { - return; + if (includeHeaders) { + copyString = [...headers.values()].join('\t').concat(eol); + } + + const rowKeys = [...headers.keys()]; + + for (let rowEntry of rows) { + let rowMap = rowEntry[1]; + for (let rowIdx of rowKeys) { + + let value = rowMap.get(rowIdx); + if (value) { + copyString = copyString.concat(value); + } + copyString = copyString.concat('\t'); } - copyString = copyString.concat(row.join('\t').concat(eol)); - }); - copyString = copyString.slice(0, -1 * eol.length); + } return copyString; }