From 30edf8d57b08b5a4b0f35b5c31799a052e79f8a2 Mon Sep 17 00:00:00 2001 From: Aasim Khan Date: Wed, 31 May 2023 22:25:41 -0700 Subject: [PATCH] Fixing copy headers (#23280) --- .../browser/outputs/gridOutput.component.ts | 13 ++++- .../contrib/query/browser/actions.ts | 12 ++--- .../services/query/common/gridDataProvider.ts | 49 +++++++++++++++++-- .../services/query/common/queryRunner.ts | 13 ++++- 4 files changed, 69 insertions(+), 18 deletions(-) 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 d99a978073..c1956c59ac 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -6,7 +6,7 @@ import { OnInit, Component, Input, Inject, ViewChild, ElementRef, ChangeDetectorRef, forwardRef } from '@angular/core'; import * as azdata from 'azdata'; -import { IGridDataProvider, getResultsString } from 'sql/workbench/services/query/common/gridDataProvider'; +import { IGridDataProvider, getResultsString, getTableHeaderString } from 'sql/workbench/services/query/common/gridDataProvider'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -421,7 +421,16 @@ export class DataResourceDataProvider implements IGridDataProvider { let results = await getResultsString(this, selection, includeHeaders, tableView); this._clipboardService.writeText(results); } catch (error) { - this._notificationService.error(localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error))); + this._notificationService.error(localize('copyFailed', "Copy failed with error: {0}", getErrorMessage(error))); + } + } + + async copyHeaders(selection: Slick.Range[]): Promise { + try { + const results = getTableHeaderString(this, selection); + await this._clipboardService.writeText(results); + } catch (error) { + this._notificationService.error(localize('copyFailed', "Copy failed with error: {0}", getErrorMessage(error))); } } diff --git a/src/sql/workbench/contrib/query/browser/actions.ts b/src/sql/workbench/contrib/query/browser/actions.ts index d9ecbea720..7768667f64 100644 --- a/src/sql/workbench/contrib/query/browser/actions.ts +++ b/src/sql/workbench/contrib/query/browser/actions.ts @@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStorageService } from 'vs/platform/storage/common/storage'; import { getChartMaxRowCount, notifyMaxRowCountExceeded } from 'sql/workbench/contrib/charts/browser/utils'; import { IEncodingSupport } from 'vs/workbench/services/textfile/common/textfiles'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; export interface IGridActionContext { gridDataProvider: IGridDataProvider; @@ -127,19 +126,14 @@ export class CopyHeadersAction extends Action { public static ID = 'grid.copyHeaders'; private static LABEL = localize('copyHeaders', 'Copy Headers'); - constructor( - @IClipboardService private clipboardService: IClipboardService - ) { + constructor() { super(CopyHeadersAction.ID, CopyHeadersAction.LABEL); } public override async run(context: IGridActionContext): Promise { - // Starting at index 1 to ignore the first column of row numbers - const columnHeaders = context.table.columns.slice(1, context.table.columns.length) - .map(c => c.name ? c.name : '') - .join(','); + const selection = mapForNumberColumn(context.selection); + await context.gridDataProvider.copyHeaders(selection); - await this.clipboardService.writeText(columnHeaders); } } diff --git a/src/sql/workbench/services/query/common/gridDataProvider.ts b/src/sql/workbench/services/query/common/gridDataProvider.ts index 98eaa85168..9caa8a0b9b 100644 --- a/src/sql/workbench/services/query/common/gridDataProvider.ts +++ b/src/sql/workbench/services/query/common/gridDataProvider.ts @@ -25,6 +25,12 @@ export interface IGridDataProvider { */ copyResults(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise; + /** + * Sends a copy request to copy table headers to the clipboard + * @param selection The selection range to copy + */ + copyHeaders(selection: Slick.Range[]): Promise; + /** * Gets the EOL terminator to use for this data type. */ @@ -100,11 +106,8 @@ export async function getResultsString(provider: IGridDataProvider, selection: S // Make sure all these tasks have executed await Promise.all(actionedTasks); - const sortResults = (e1: [number, any], e2: [number, any]) => { - return e1[0] - e2[0]; - }; - headers = new Map([...headers].sort(sortResults)); - rows = new Map([...rows].sort(sortResults)); + headers = sortMapEntriesByColumnOrder(headers); + rows = sortMapEntriesByColumnOrder(rows); let copyString = ''; if (includeHeaders) { @@ -132,6 +135,42 @@ export async function getResultsString(provider: IGridDataProvider, selection: S return copyString; } +export function getTableHeaderString(provider: IGridDataProvider, selection: Slick.Range[]): string { + let headers: Map = new Map(); // Maps a column index -> header + + selection.forEach((range) => { + let startCol = range.fromCell; + let columnHeaders = provider.getColumnHeaders(range); + if (columnHeaders !== undefined) { + let idx = 0; + for (let header of columnHeaders) { + headers.set(startCol + idx, header); + idx++; + } + } + }); + + headers = sortMapEntriesByColumnOrder(headers) + + const copyString = Array.from(headers.values()) + .map(colHeader => colHeader ? colHeader : '') + .join('\t'); + + return copyString; +} + +/** + * Ensures that table entries in the map appear in column order instead of the order that they were selected. + * @param map Contains the entries selected in a table + * @returns Sorted map with entries appearing in column order. + */ +function sortMapEntriesByColumnOrder(map: Map): Map { + const leftToRight = (e1: [number, any], e2: [number, any]) => { + return e1[0] - e2[0]; + }; + + return new Map([...map].sort(leftToRight)); +} function removeNewLines(inputString: string): string { // This regex removes all newlines in all OS types diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index 608dc72830..cf5895cf26 100644 --- a/src/sql/workbench/services/query/common/queryRunner.ts +++ b/src/sql/workbench/services/query/common/queryRunner.ts @@ -21,7 +21,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { URI } from 'vs/base/common/uri'; import * as perf from 'vs/base/common/performance'; import { mssqlProviderName } from 'sql/platform/connection/common/constants'; -import { IGridDataProvider, getResultsString } from 'sql/workbench/services/query/common/gridDataProvider'; +import { IGridDataProvider, getResultsString, getTableHeaderString } from 'sql/workbench/services/query/common/gridDataProvider'; import { getErrorMessage } from 'vs/base/common/errors'; import { ILogService } from 'vs/platform/log/common/log'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -564,7 +564,16 @@ export class QueryGridDataProvider implements IGridDataProvider { const results = await getResultsString(this, selection, includeHeaders, tableView); await this._clipboardService.writeText(results); } catch (error) { - this._notificationService.error(nls.localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error))); + this._notificationService.error(nls.localize('copyFailed', "Copy failed with error: {0}", getErrorMessage(error))); + } + } + + async copyHeaders(selection: Slick.Range[]): Promise { + try { + const results = getTableHeaderString(this, selection); + await this._clipboardService.writeText(results); + } catch (error) { + this._notificationService.error(nls.localize('copyFailed', "Copy failed with error: {0}", getErrorMessage(error))); } }