diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 7f299f04c7..2073ca1adc 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -67,6 +67,7 @@ sqltoolsservice: https://github.com/Microsoft/sqltoolsservice svg.js: https://github.com/svgdotjs/svg.js systemjs: https://github.com/systemjs/systemjs temp-write: https://github.com/sindresorhus/temp-write +textcopy: https://github.com/CopyText/TextCopy turndown: https://github.com/domchristie/turndown turndown-plugin-gfm: https://github.com/domchristie/turndown-plugin-gfm underscore: https://github.com/jashkenas/underscore @@ -3351,3 +3352,30 @@ SOFTWARE. -------------------------------END OF THIRD-PARTY NOTICES------------------------------------------- ========================================= END OF Microsoft.ProgramSynthesis.Detection NOTICES AND INFORMATION + + +%% TextCopy NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT License + +Copyright (c) 2018 Simon Cropp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +========================================= +END OF TextCopy NOTICES AND INFORMATION \ No newline at end of file diff --git a/extensions/azuremonitor/package.json b/extensions/azuremonitor/package.json index 5741af2bcf..7ec1fe6b76 100644 --- a/extensions/azuremonitor/package.json +++ b/extensions/azuremonitor/package.json @@ -209,7 +209,7 @@ "update-grammar": "node ../../build/npm/update-grammar.js Microsoft/vscode-azuremonitor ./syntaxes/azuremonitor.tmLanguage" }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "figures": "^2.0.0", "find-remove": "1.2.1", "@microsoft/ads-service-downloader": "^1.2.1", diff --git a/extensions/azuremonitor/yarn.lock b/extensions/azuremonitor/yarn.lock index f48821629e..ca34325f0c 100644 --- a/extensions/azuremonitor/yarn.lock +++ b/extensions/azuremonitor/yarn.lock @@ -75,9 +75,9 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/extensions/datavirtualization/package.json b/extensions/datavirtualization/package.json index de257586a9..9344793cfd 100644 --- a/extensions/datavirtualization/package.json +++ b/extensions/datavirtualization/package.json @@ -107,7 +107,7 @@ "dependencies": { "@microsoft/ads-extension-telemetry": "^3.0.1", "@microsoft/ads-service-downloader": "^1.2.1", - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "vscode-nls": "^5.2.0" }, "devDependencies": { diff --git a/extensions/datavirtualization/yarn.lock b/extensions/datavirtualization/yarn.lock index e1efb4ac61..3dd76305a9 100644 --- a/extensions/datavirtualization/yarn.lock +++ b/extensions/datavirtualization/yarn.lock @@ -489,9 +489,9 @@ crypt@0.0.2: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/extensions/import/package.json b/extensions/import/package.json index 11adb767a8..bbd0d647d6 100644 --- a/extensions/import/package.json +++ b/extensions/import/package.json @@ -77,7 +77,7 @@ } }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "htmlparser2": "^3.10.1", "@microsoft/ads-service-downloader": "^1.2.1", "@microsoft/ads-extension-telemetry": "^3.0.1", diff --git a/extensions/import/yarn.lock b/extensions/import/yarn.lock index 7863f1d21a..a647c024c6 100644 --- a/extensions/import/yarn.lock +++ b/extensions/import/yarn.lock @@ -486,9 +486,9 @@ crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/extensions/kusto/package.json b/extensions/kusto/package.json index 44d07d608a..63e84dff12 100644 --- a/extensions/kusto/package.json +++ b/extensions/kusto/package.json @@ -427,7 +427,7 @@ } }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "figures": "^2.0.0", "find-remove": "1.2.1", "@microsoft/ads-service-downloader": "^1.2.1", diff --git a/extensions/kusto/yarn.lock b/extensions/kusto/yarn.lock index b5722faebd..dc80384d39 100644 --- a/extensions/kusto/yarn.lock +++ b/extensions/kusto/yarn.lock @@ -124,9 +124,9 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index 6046df85ba..853e4db085 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "4.8.0.17", + "version": "4.8.0.20", "downloadFileNames": { "Windows_86": "win-x86-net7.0.zip", "Windows_64": "win-x64-net7.0.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index bcf6487087..8923e007eb 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -806,6 +806,7 @@ "providerId": "MSSQL", "displayName": "%mssql.provider.displayName%", "isExecutionPlanProvider": true, + "supportCopyResultsToClipboard": true, "azureResource": "Sql", "supportedExecutionPlanFileExtensions": [ "sqlplan" @@ -1458,7 +1459,7 @@ "dependencies": { "@microsoft/ads-extension-telemetry": "^3.0.1", "@microsoft/ads-service-downloader": "^1.2.1", - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "find-remove": "1.2.1", "vscode-languageclient": "5.2.1", "vscode-nls": "^4.0.0" diff --git a/extensions/mssql/yarn.lock b/extensions/mssql/yarn.lock index 21a7187226..480bde03a8 100644 --- a/extensions/mssql/yarn.lock +++ b/extensions/mssql/yarn.lock @@ -425,9 +425,9 @@ crypt@~0.0.1: resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/extensions/sql-migration/package.json b/extensions/sql-migration/package.json index 8da19b9009..32b26a1e92 100644 --- a/extensions/sql-migration/package.json +++ b/extensions/sql-migration/package.json @@ -162,7 +162,7 @@ ] }, "dependencies": { - "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.4", + "dataprotocol-client": "github:Microsoft/sqlops-dataprotocolclient#1.3.5", "@microsoft/ads-service-downloader": "^1.2.1", "@microsoft/ads-extension-telemetry": "^3.0.1", "uuid": "^8.3.2", diff --git a/extensions/sql-migration/yarn.lock b/extensions/sql-migration/yarn.lock index 9b34b1edac..649ca461e4 100644 --- a/extensions/sql-migration/yarn.lock +++ b/extensions/sql-migration/yarn.lock @@ -62,9 +62,9 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== -"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.4": - version "1.3.4" - resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/fed4e05caadd89e1f635cc247b82a96a10bc837d" +"dataprotocol-client@github:Microsoft/sqlops-dataprotocolclient#1.3.5": + version "1.3.5" + resolved "https://codeload.github.com/Microsoft/sqlops-dataprotocolclient/tar.gz/23e1f6ea61f10a3b52d4be6f786685a55999fd9a" dependencies: vscode-languageclient "5.2.1" diff --git a/src/sql/platform/capabilities/common/capabilitiesService.ts b/src/sql/platform/capabilities/common/capabilitiesService.ts index 42ef4f4dd8..78775bf205 100644 --- a/src/sql/platform/capabilities/common/capabilitiesService.ts +++ b/src/sql/platform/capabilities/common/capabilitiesService.ts @@ -108,6 +108,13 @@ export interface ConnectionProviderProperties { * Connection string options for the connection provider */ connectionStringOptions?: ConnectionStringOptions; + + /** + * Indicates whether the provider support copy results to clipboard. Default value is false. + * If true, the copy results to clipboard will be delegated to the provider to avoid passing large amount of data using the RPC channel. + * Otherwise ADS will handle the copy request on the UI side. + */ + supportCopyResultsToClipboard?: boolean; } export interface ProviderFeatures { diff --git a/src/sql/platform/query/common/query.ts b/src/sql/platform/query/common/query.ts index 1aec4b4a84..d734bd1192 100644 --- a/src/sql/platform/query/common/query.ts +++ b/src/sql/platform/query/common/query.ts @@ -31,6 +31,7 @@ export interface IQueryEditorConfiguration { readonly inMemoryDataProcessingThreshold: number; readonly openAfterSave: boolean; readonly showActionBar: boolean; + readonly preferProvidersCopyHandler: boolean; }, readonly messages: { readonly showBatchTime: boolean; diff --git a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts index 2453c403ee..ceb75a55ac 100644 --- a/src/sql/workbench/api/browser/mainThreadDataProtocol.ts +++ b/src/sql/workbench/api/browser/mainThreadDataProtocol.ts @@ -159,6 +159,9 @@ export class MainThreadDataProtocol extends Disposable implements MainThreadData return Promise.resolve(self._serializationService.saveAs(requestParams.resultFormat, requestParams.filePath, undefined, true)); } }, + copyResults(requestParams: azdata.CopyResultsRequestParams): Promise { + return Promise.resolve(self._proxy.$copyResults(handle, requestParams)); + }, initializeEdit(ownerUri: string, schemaName: string, objectName: string, objectType: string, rowLimit: number, queryString: string): Promise { return Promise.resolve(self._proxy.$initializeEdit(handle, ownerUri, schemaName, objectName, objectType, rowLimit, queryString)); }, diff --git a/src/sql/workbench/api/common/extHostDataProtocol.ts b/src/sql/workbench/api/common/extHostDataProtocol.ts index 8357dd6b13..6529e04db4 100644 --- a/src/sql/workbench/api/common/extHostDataProtocol.ts +++ b/src/sql/workbench/api/common/extHostDataProtocol.ts @@ -414,6 +414,15 @@ export class ExtHostDataProtocol extends ExtHostDataProtocolShape { return this._resolveProvider(handle).saveResults(requestParams); } + override $copyResults(handle: number, requestParams: azdata.CopyResultsRequestParams): Thenable { + const provider = this._resolveProvider(handle); + if (provider.copyResults) { + return provider.copyResults(requestParams); + } else { + throw new Error(`copyResults() is not implemented by the provider`); + } + } + // Edit Data handlers override $commitEdit(handle: number, ownerUri: string): Thenable { return this._resolveProvider(handle).commitEdit(ownerUri); diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index 227bfc4636..4d6b4185d8 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -251,6 +251,11 @@ export abstract class ExtHostDataProtocolShape { */ $saveResults(handle: number, requestParams: azdata.SaveResultsRequestParams): Thenable { throw ni(); } + /** + * Copies the selected data to clipboard. + */ + $copyResults(handle: number, requestParams: azdata.CopyResultsRequestParams): Thenable { throw ni(); } + /** * Commits all pending edits in an edit session */ diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index f8ce9c454e..a0e0965bd6 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -404,6 +404,11 @@ const queryEditorConfiguration: IConfigurationNode = { 'description': localize('queryEditor.results.copyRemoveNewLine', "Configuration options for copying multi-line results from the Results View"), 'default': true }, + 'queryEditor.results.preferProvidersCopyHandler': { + 'type': 'boolean', + 'description': localize('queryEditor.results.preferProvidersCopyHandler', "Whether the copy result request should be handled by the query provider when it is supported. The default value is true, set this to false to force all copy handling to be done by Azure Data Studio."), + 'default': true + }, 'queryEditor.results.inMemoryDataProcessingThreshold': { 'type': 'number', 'default': 5000, diff --git a/src/sql/workbench/services/query/common/gridDataProvider.ts b/src/sql/workbench/services/query/common/gridDataProvider.ts index f4e8db6806..4218ea9b30 100644 --- a/src/sql/workbench/services/query/common/gridDataProvider.ts +++ b/src/sql/workbench/services/query/common/gridDataProvider.ts @@ -7,7 +7,7 @@ import * as types from 'vs/base/common/types'; import { SaveFormat } from 'sql/workbench/services/query/common/resultSerializer'; import { ICellValue, ResultSetSubset } from 'sql/workbench/services/query/common/query'; import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification'; import * as nls from 'vs/nls'; import { toAction } from 'vs/base/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -79,97 +79,112 @@ function mergeRanges(ranges: Range[]): Range[] { return mergedRanges; } -export async function copySelectionToClipboard(clipboardService: IClipboardService, notificationService: INotificationService, provider: IGridDataProvider, selections: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { - const batchSize = 100; - const eol = provider.getEolString(); - const valueSeparator = '\t'; - const shouldRemoveNewLines = provider.shouldRemoveNewLines(); - - // Merge the selections to get the columns and rows. - const columnRanges: Range[] = mergeRanges(selections.map(selection => { return { start: selection.fromCell, end: selection.toCell }; })); +export async function executeCopyWithNotification(notificationService: INotificationService, selections: Slick.Range[], isCancelable: boolean, copyHandler: (notification: INotificationHandle, rowCount: number) => Promise, onCanceled?: () => void): Promise { const rowRanges: Range[] = mergeRanges(selections.map(selection => { return { start: selection.fromRow, end: selection.toRow }; })); - - const totalRows = rowRanges.map(range => range.end - range.start + 1).reduce((p, c) => p + c); - - let processedRows = 0; - const getMessageText = (): string => { - return nls.localize('gridDataProvider.loadingRowsInProgress', "Loading the rows to be copied ({0}/{1})...", processedRows, totalRows); - }; - + const rowCount = rowRanges.map(range => range.end - range.start + 1).reduce((p, c) => p + c); let isCanceled = false; - const notificationHandle = notificationService.notify({ - message: getMessageText(), + message: nls.localize('gridDataProvider.copying', "Copying..."), severity: Severity.Info, progress: { infinite: true }, actions: { - primary: [ + primary: isCancelable ? [ toAction({ id: 'cancelCopyResults', label: nls.localize('gridDataProvider.cancelCopyResults', "Cancel"), run: () => { isCanceled = true; + onCanceled!(); notificationHandle.close(); } - })] + })] : [] } }); - - let resultString = ''; - if (includeHeaders) { - const headers: string[] = []; - columnRanges.forEach(range => { - headers.push(...provider.getColumnHeaders({ - fromCell: range.start, - toCell: range.end - })); - }); - resultString = Array.from(headers.values()).join(valueSeparator).concat(eol); - } - - const batchResult: string[] = []; - for (const range of rowRanges) { - if (tableView && tableView.isDataInMemory) { - const rangeLength = range.end - range.start + 1; - // If the data is sorted/filtered in memory, we need to get the data that is currently being displayed - const tableData = await tableView.getRangeAsync(range.start, rangeLength); - const rowSet = tableData.map(item => Object.keys(item).map(key => item[key])); - batchResult.push(getStringValueForRowSet(rowSet, columnRanges, selections, range.start, eol, valueSeparator, shouldRemoveNewLines)); - processedRows += rangeLength; - notificationHandle.updateMessage(getMessageText()); - } else { - let start = range.start; - do { - const end = Math.min(start + batchSize - 1, range.end); - const batchLength = end - start + 1 - const rowSet = (await provider.getRowData(start, batchLength)).rows; - batchResult.push(getStringValueForRowSet(rowSet, columnRanges, selections, range.start, eol, valueSeparator, shouldRemoveNewLines)); - start = end + 1; - processedRows = processedRows + batchLength; - if (!isCanceled) { - notificationHandle.updateMessage(getMessageText()); - } - } while (start < range.end && !isCanceled) + try { + await copyHandler(notificationHandle, rowCount); + if (!isCanceled) { + notificationHandle.progress.done(); + notificationHandle.updateActions({ + primary: [ + toAction({ + id: 'closeCopyResultsNotification', + label: nls.localize('gridDataProvider.closeNotification', "Close"), + run: () => { notificationHandle.close(); } + })] + }); + notificationHandle.updateMessage(nls.localize('gridDataProvider.copyResultsCompleted', "Selected data has been copied to the clipboard. Row count: {0}.", rowCount)); } } - if (!isCanceled) { - resultString += batchResult.join(eol); - notificationHandle.progress.done(); - notificationHandle.updateActions({ - primary: [ - toAction({ - id: 'closeCopyResultsNotification', - label: nls.localize('gridDataProvider.closeNotification', "Close"), - run: () => { notificationHandle.close(); } - })] - }); - await clipboardService.writeText(resultString); - notificationHandle.updateMessage(nls.localize('gridDataProvider.copyResultsCompleted', "Selected data has been copied to the clipboard. Row count: {0}.", totalRows)); + catch (err) { + notificationHandle.close(); + throw err; } } +export async function copySelectionToClipboard(clipboardService: IClipboardService, notificationService: INotificationService, provider: IGridDataProvider, selections: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { + let isCanceled = false; + await executeCopyWithNotification(notificationService, selections, true, async (notificationHandle, rowCount) => { + const batchSize = 100; + const eol = provider.getEolString(); + const valueSeparator = '\t'; + const shouldRemoveNewLines = provider.shouldRemoveNewLines(); + + // Merge the selections to get the columns and rows. + const columnRanges: Range[] = mergeRanges(selections.map(selection => { return { start: selection.fromCell, end: selection.toCell }; })); + const rowRanges: Range[] = mergeRanges(selections.map(selection => { return { start: selection.fromRow, end: selection.toRow }; })); + + let processedRows = 0; + const getMessageText = (): string => { + return nls.localize('gridDataProvider.loadingRowsInProgress', "Loading the rows to be copied ({0}/{1})...", processedRows, rowCount); + }; + let resultString = ''; + if (includeHeaders) { + const headers: string[] = []; + columnRanges.forEach(range => { + headers.push(...provider.getColumnHeaders({ + fromCell: range.start, + toCell: range.end + })); + }); + resultString = Array.from(headers.values()).join(valueSeparator).concat(eol); + } + + const batchResult: string[] = []; + for (const range of rowRanges) { + if (tableView && tableView.isDataInMemory) { + const rangeLength = range.end - range.start + 1; + // If the data is sorted/filtered in memory, we need to get the data that is currently being displayed + const tableData = await tableView.getRangeAsync(range.start, rangeLength); + const rowSet = tableData.map(item => Object.keys(item).map(key => item[key])); + batchResult.push(getStringValueForRowSet(rowSet, columnRanges, selections, range.start, eol, valueSeparator, shouldRemoveNewLines)); + processedRows += rangeLength; + notificationHandle.updateMessage(getMessageText()); + } else { + let start = range.start; + do { + const end = Math.min(start + batchSize - 1, range.end); + const batchLength = end - start + 1 + const rowSet = (await provider.getRowData(start, batchLength)).rows; + batchResult.push(getStringValueForRowSet(rowSet, columnRanges, selections, range.start, eol, valueSeparator, shouldRemoveNewLines)); + start = end + 1; + processedRows = processedRows + batchLength; + if (!isCanceled) { + notificationHandle.updateMessage(getMessageText()); + } + } while (start < range.end && !isCanceled) + } + } + if (!isCanceled) { + resultString += batchResult.join(eol); + await clipboardService.writeText(resultString); + } + }, () => { + isCanceled = true; + }); +} + function getStringValueForRowSet(rows: ICellValue[][], columnRanges: Range[], selections: Slick.Range[], rowSetStartIndex: number, eol: string, valueSeparator: string, shouldRemoveNewLines: boolean): string { let rowStrings: string[] = []; rows.forEach((values, index) => { diff --git a/src/sql/workbench/services/query/common/queryManagement.ts b/src/sql/workbench/services/query/common/queryManagement.ts index f0dbbdd7bf..31a6b77d77 100644 --- a/src/sql/workbench/services/query/common/queryManagement.ts +++ b/src/sql/workbench/services/query/common/queryManagement.ts @@ -55,6 +55,7 @@ export interface IQueryManagementService { changeConnectionUri(newUri: string, oldUri: string): Promise; saveResults(requestParams: azdata.SaveResultsRequestParams): Promise; setQueryExecutionOptions(uri: string, options: azdata.QueryExecutionOptions): Promise; + copyResults(params: azdata.CopyResultsRequestParams): Promise; // Callbacks onQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void; @@ -93,6 +94,7 @@ export interface IQueryRequestHandler { disposeQuery(ownerUri: string): Promise; connectionUriChanged(newUri: string, oldUri: string): Promise; saveResults(requestParams: azdata.SaveResultsRequestParams): Promise; + copyResults(requestParams: azdata.CopyResultsRequestParams): Promise; setQueryExecutionOptions(ownerUri: string, options: azdata.QueryExecutionOptions): Promise; // Edit Data actions @@ -311,6 +313,12 @@ export class QueryManagementService implements IQueryManagementService { }); } + public copyResults(requestParams: azdata.CopyResultsRequestParams): Promise { + return this._runAction(requestParams.ownerUri, (runner) => { + return runner.copyResults(requestParams); + }); + } + public onQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void { this._notify(result.ownerUri, (runner: QueryRunner) => { runner.handleQueryComplete(result.batchSummaries.map(s => ({ ...s, range: selectionDataToRange(s.selection) }))); diff --git a/src/sql/workbench/services/query/common/queryRunner.ts b/src/sql/workbench/services/query/common/queryRunner.ts index 5f120e6db5..ad0eb0b7da 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, copySelectionToClipboard, getTableHeaderString } from 'sql/workbench/services/query/common/gridDataProvider'; +import { IGridDataProvider, copySelectionToClipboard, executeCopyWithNotification, 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'; @@ -29,6 +29,7 @@ import { BatchSummary, IQueryMessage, ResultSetSummary, QueryExecuteSubsetParams import { IQueryEditorConfiguration } from 'sql/platform/query/common/query'; import { IDisposableDataProvider } from 'sql/base/common/dataProvider'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfiguration'; +import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; /* * Query Runner class which handles running a query, reports the results to the content manager, @@ -460,17 +461,30 @@ export default class QueryRunner extends Disposable { /** * Sends a copy request - * @param selection The selection range to copy + * @param selections The selection range to copy * @param batchId The batch id of the result to copy from * @param resultId The result id of the result to copy from + * @param removeNewLines Whether to remove line breaks from values. * @param includeHeaders [Optional]: Should column headers be included in the copy selection */ - async copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): Promise { - let provider = this.getGridDataProvider(batchId, resultId); - return provider.copyResults(selection, includeHeaders); + async copyResults(selections: Slick.Range[], batchId: number, resultId: number, removeNewLines: boolean, includeHeaders?: boolean): Promise { + await this.queryManagementService.copyResults({ + ownerUri: this.uri, + batchIndex: batchId, + resultSetIndex: resultId, + removeNewLines: removeNewLines, + includeHeaders: includeHeaders, + selections: selections.map(selection => { + return { + fromRow: selection.fromRow, + toRow: selection.toRow, + fromColumn: selection.fromCell, + toColumn: selection.toCell + }; + }) + }); } - public getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] | undefined { let headers: string[] | undefined = undefined; let batchSummary: BatchSummary = this._batchSets[batchId]; @@ -547,7 +561,8 @@ export class QueryGridDataProvider implements IGridDataProvider { @INotificationService private _notificationService: INotificationService, @IClipboardService private _clipboardService: IClipboardService, @IConfigurationService private _configurationService: IConfigurationService, - @ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService + @ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService, + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService ) { } @@ -559,14 +574,27 @@ export class QueryGridDataProvider implements IGridDataProvider { return this.copyResultsAsync(selection, includeHeaders, tableView); } - private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { + private async copyResultsAsync(selections: Slick.Range[], includeHeaders?: boolean, tableView?: IDisposableDataProvider): Promise { try { - await copySelectionToClipboard(this._clipboardService, this._notificationService, this, selection, includeHeaders, tableView); + const providerId = this.queryRunner.getProviderId(); + const providerSupportCopyResults = this._capabilitiesService.getCapabilities(providerId).connection.supportCopyResultsToClipboard; + const preferProvidersCopyHandler = this._configurationService.getValue('queryEditor').results.preferProvidersCopyHandler; + if (preferProvidersCopyHandler && providerSupportCopyResults && (tableView === undefined || !tableView.isDataInMemory)) { + await this.handleCopyRequestByProvider(selections, includeHeaders); + } else { + await copySelectionToClipboard(this._clipboardService, this._notificationService, this, selections, includeHeaders, tableView); + } } catch (error) { this._notificationService.error(nls.localize('copyFailed', "Copy failed with error: {0}", getErrorMessage(error))); } } + private async handleCopyRequestByProvider(selections: Slick.Range[], includeHeaders?: boolean): Promise { + executeCopyWithNotification(this._notificationService, selections, false, async () => { + await this.queryRunner.copyResults(selections, this.batchId, this.resultSetId, this.shouldRemoveNewLines(), this.shouldIncludeHeaders(includeHeaders)); + }); + } + async copyHeaders(selection: Slick.Range[]): Promise { try { const results = getTableHeaderString(this, selection); diff --git a/src/sql/workbench/services/query/test/common/testQueryManagementService.ts b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts index 973a25f1c7..cd0f253e6b 100644 --- a/src/sql/workbench/services/query/test/common/testQueryManagementService.ts +++ b/src/sql/workbench/services/query/test/common/testQueryManagementService.ts @@ -62,6 +62,9 @@ export class TestQueryManagementService implements IQueryManagementService { saveResults(requestParams: azdata.SaveResultsRequestParams): Promise { throw new Error('Method not implemented.'); } + copyResults(params: azdata.CopyResultsRequestParams): Promise { + throw new Error('Method not implemented.'); + } setQueryExecutionOptions(uri: string, options: azdata.QueryExecutionOptions): Promise { throw new Error('Method not implemented.'); }