From 4a201b1031dbd76bc38e893a612957c73e3007de Mon Sep 17 00:00:00 2001 From: Monica Gupta Date: Wed, 25 Nov 2020 21:08:29 -0800 Subject: [PATCH] Copy clipboard command in ADS (html/plain text supported) (#13527) * draft commit * few changes * Changes to copy query with results in plain and html formatting * undo changes * undo unintended change * remove comments * Addressed comments * Some clean up Co-authored-by: Monica Gupta --- .../clipboard/browser/clipboardService.ts | 6 +- .../query/browser/keyboardQueryActions.ts | 70 +++++++++++++++++++ .../query/browser/query.contribution.ts | 12 +++- .../clipboard/browser/clipboardService.ts | 7 +- .../clipboard/common/clipboardService.ts | 12 ++++ src/vs/platform/native/common/native.ts | 1 + .../electron-main/nativeHostMainService.ts | 5 ++ .../electron-sandbox/clipboardService.ts | 7 +- .../electron-browser/workbenchTestServices.ts | 1 + 9 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/sql/platform/clipboard/browser/clipboardService.ts b/src/sql/platform/clipboard/browser/clipboardService.ts index 6a724631ca9b..a615f4f6f881 100644 --- a/src/sql/platform/clipboard/browser/clipboardService.ts +++ b/src/sql/platform/clipboard/browser/clipboardService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IClipboardService } from 'sql/platform/clipboard/common/clipboardService'; -import { IClipboardService as vsIClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ClipboardData, IClipboardService as vsIClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; @@ -25,6 +25,10 @@ export class BrowserClipboardService implements IClipboardService { this._notificationService.info(localize('imageCopyingNotSupported', "Copying images is not supported")); } + write(data: ClipboardData): Promise { + return this._vsClipboardService.write(data); + } + writeText(text: string): Promise { return this._vsClipboardService.writeText(text); } diff --git a/src/sql/workbench/contrib/query/browser/keyboardQueryActions.ts b/src/sql/workbench/contrib/query/browser/keyboardQueryActions.ts index fd0fd3624ffa..f448dddf5690 100644 --- a/src/sql/workbench/contrib/query/browser/keyboardQueryActions.ts +++ b/src/sql/workbench/contrib/query/browser/keyboardQueryActions.ts @@ -9,6 +9,7 @@ import { Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import * as azdata from 'azdata'; +import { escape } from 'sql/base/common/strings'; import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; @@ -21,6 +22,7 @@ import { EditDataEditor } from 'sql/workbench/contrib/editData/browser/editDataE import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { QueryEditorInput } from 'sql/workbench/common/editor/query/queryEditorInput'; +import { ClipboardData, IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; const singleQuote = '\''; @@ -131,6 +133,74 @@ export class RunCurrentQueryKeyboardAction extends Action { } } +export class CopyQueryWithResultsKeyboardAction extends Action { + public static ID = 'copyQueryWithResultsKeyboardAction'; + public static LABEL = nls.localize('copyQueryWithResultsKeyboardAction', "Copy Query With Results"); + + constructor( + id: string, + label: string, + @IEditorService private _editorService: IEditorService, + @IClipboardService private _clipboardService: IClipboardService, + @IQueryModelService protected readonly queryModelService: IQueryModelService, + ) { + super(id, label); + } + + public async getFormattedResults(editor): Promise { + let queryRunner = this.queryModelService.getQueryRunner(editor.input.uri); + let allResults = ''; + let allHtmlResults = ''; + + if (queryRunner && queryRunner.batchSets.length > 0) { + for (let i = 0; i < queryRunner.batchSets[0].resultSetSummaries.length; i++) { + let resultSummary = queryRunner.batchSets[0].resultSetSummaries[i]; + let result = await queryRunner.getQueryRows(0, resultSummary.rowCount, resultSummary.batchId, resultSummary.id); + let tableHeaders = resultSummary.columnInfo.map((col, i) => (col.columnName)); + let htmlTableHeaders = resultSummary.columnInfo.map((col, i) => (`${escape(col.columnName)}`)); + let copyString = '\n'; + let htmlCopyString = ''; + + for (let rowEntry of result.rows) { + for (let colIdx = 0; colIdx < rowEntry.length; colIdx++) { + let value = rowEntry[colIdx].displayValue; + if (value) { + copyString = `${copyString}${value}\t`; + htmlCopyString = `${htmlCopyString}${escape(value)}`; + } + } + // Removes the tab seperator from the end of a row + copyString = copyString.slice(0, -1 * '\t'.length) + '\n'; + htmlCopyString = htmlCopyString + ''; + } + + allResults = `${allResults}${tableHeaders.join('\t')}${copyString}\n`; + allHtmlResults = `${allHtmlResults}


+ + ${htmlTableHeaders.join('')}${htmlCopyString} +
`; + } + } + + return { text: allResults, html: allHtmlResults }; + } + + public async run(): Promise { + const editor = this._editorService.activeEditorPane; + if (editor instanceof QueryEditor) { + let allResults = await this.getFormattedResults(editor); + let queryText = editor.getAllText(); + + let data = { + text: `${queryText}\n\n${allResults.text}`, + html: `${escape(queryText).replace(/\r\n|\n|\r/gm, '
')}${allResults.html}` + }; + + await this._clipboardService.write(data); + } + } +} + export class RunCurrentQueryWithActualPlanKeyboardAction extends Action { public static ID = 'runCurrentQueryWithActualPlanKeyboardAction'; public static LABEL = nls.localize('runCurrentQueryWithActualPlanKeyboardAction', "Run Current Query with Actual Plan"); diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 6773c696112f..a1c62291ff8b 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -19,7 +19,7 @@ import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResult import * as queryContext from 'sql/workbench/contrib/query/common/queryContext'; import { RunQueryKeyboardAction, RunCurrentQueryKeyboardAction, CancelQueryKeyboardAction, RefreshIntellisenseKeyboardAction, ToggleQueryResultsKeyboardAction, - RunQueryShortcutAction, RunCurrentQueryWithActualPlanKeyboardAction, FocusOnCurrentQueryKeyboardAction, ParseSyntaxAction + RunQueryShortcutAction, RunCurrentQueryWithActualPlanKeyboardAction, CopyQueryWithResultsKeyboardAction, FocusOnCurrentQueryKeyboardAction, ParseSyntaxAction } from 'sql/workbench/contrib/query/browser/keyboardQueryActions'; import * as gridActions from 'sql/workbench/contrib/editData/browser/gridActions'; import * as gridCommands from 'sql/workbench/contrib/editData/browser/gridCommands'; @@ -139,6 +139,16 @@ actionRegistry.registerWorkbenchAction( RunCurrentQueryWithActualPlanKeyboardAction.LABEL ); +actionRegistry.registerWorkbenchAction( + SyncActionDescriptor.create( + CopyQueryWithResultsKeyboardAction, + CopyQueryWithResultsKeyboardAction.ID, + CopyQueryWithResultsKeyboardAction.LABEL, + { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C } + ), + CopyQueryWithResultsKeyboardAction.LABEL +); + actionRegistry.registerWorkbenchAction( SyncActionDescriptor.create( CancelQueryKeyboardAction, diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 11b3e009a349..792f0545930a 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ClipboardData, IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // {{SQL CARBON EDIT}} import { URI } from 'vs/base/common/uri'; import { $ } from 'vs/base/browser/dom'; @@ -13,6 +13,11 @@ export class BrowserClipboardService implements IClipboardService { private readonly mapTextToType = new Map(); // unsupported in web (only in-memory) + // {{SQL CARBON EDIT}} + async write(data: ClipboardData, type?: string): Promise { + throw new Error('Not Implemented'); + } + async writeText(text: string, type?: string): Promise { // With type: only in-memory is supported diff --git a/src/vs/platform/clipboard/common/clipboardService.ts b/src/vs/platform/clipboard/common/clipboardService.ts index eb3b4bffce14..413b76af4242 100644 --- a/src/vs/platform/clipboard/common/clipboardService.ts +++ b/src/vs/platform/clipboard/common/clipboardService.ts @@ -8,9 +8,21 @@ import { URI } from 'vs/base/common/uri'; export const IClipboardService = createDecorator('clipboardService'); +// Added type https://www.electronjs.org/docs/api/clipboard#clipboardwritedata-type +export interface ClipboardData { // {{SQL CARBON EDIT}} + text?: string; + html?: string; + rtf?: string; + bookmark?: string; +} + export interface IClipboardService { readonly _serviceBrand: undefined; + /** + * Writes data to the system clipboard. + */ + write(data: ClipboardData, type?: string): Promise; // {{SQL CARBON EDIT}} /** * Writes text to the system clipboard. diff --git a/src/vs/platform/native/common/native.ts b/src/vs/platform/native/common/native.ts index 84c8a79b854c..1f5773ceeb27 100644 --- a/src/vs/platform/native/common/native.ts +++ b/src/vs/platform/native/common/native.ts @@ -112,6 +112,7 @@ export interface ICommonNativeHostService { writeClipboardText(text: string, type?: 'selection' | 'clipboard'): Promise; readClipboardFindText(): Promise; writeClipboardFindText(text: string): Promise; + writeClipboardData(data: any, type?: 'selection' | 'clipboard'): Promise; // {{SQL CARBON EDIT}} writeClipboardBuffer(format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise; readClipboardBuffer(format: string): Promise; hasClipboard(format: string, type?: 'selection' | 'clipboard'): Promise; diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 2716e3f52117..f7b91f6bf843 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -477,6 +477,11 @@ export class NativeHostMainService extends Disposable implements INativeHostMain return clipboard.writeText(text, type); } + // {{SQL CARBON EDIT}} + async writeClipboardData(windowId: number | undefined, data: any, type?: 'selection' | 'clipboard'): Promise { + return clipboard.write(data, type); + } + async readClipboardFindText(windowId: number | undefined,): Promise { return clipboard.readFindText(); } diff --git a/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts b/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts index 38003a54ab78..cfde1f875b26 100644 --- a/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts +++ b/src/vs/workbench/services/clipboard/electron-sandbox/clipboardService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ClipboardData, IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; // {{SQL CARBON EDIT}} import { URI } from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -20,6 +20,11 @@ export class NativeClipboardService implements IClipboardService { @INativeHostService private readonly nativeHostService: INativeHostService ) { } + // {{SQL CARBON EDIT}} + async write(data: ClipboardData, type?: 'selection' | 'clipboard'): Promise { + return this.electronService.writeClipboardData(data, type); + } + async writeText(text: string, type?: 'selection' | 'clipboard'): Promise { return this.nativeHostService.writeClipboardText(text, type); } diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 1180f987284e..e527fb156b03 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -227,6 +227,7 @@ export class TestNativeHostService implements INativeHostService { async toggleDevTools(): Promise { } async resolveProxy(url: string): Promise { return undefined; } async readClipboardText(type?: 'selection' | 'clipboard' | undefined): Promise { return ''; } + async writeClipboardData(data: any, type?: 'selection' | 'clipboard' | undefined): Promise { } // {{SQL CARBON EDIT}} async writeClipboardText(text: string, type?: 'selection' | 'clipboard' | undefined): Promise { } async readClipboardFindText(): Promise { return ''; } async writeClipboardFindText(text: string): Promise { }