Skip to content

Commit

Permalink
Fix copy behavior for sorted and/or filtered grid (#18695)
Browse files Browse the repository at this point in the history
* fix copy behavior for sorted grid

* cleanup

* refactor pr comments

* assign copy string

* refactor

* add comments

* remove console.log
  • Loading branch information
cssuh committed Feb 20, 2025
1 parent 957204c commit 2033148
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 56 deletions.
174 changes: 142 additions & 32 deletions src/controllers/queryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,33 +616,14 @@ export default class QueryRunner {
): Promise<void> {
let copyString = "";

// add the column headers
if (this.shouldIncludeHeaders(includeHeaders)) {
let firstCol: number;
let lastCol: number;
for (let range of selection) {
if (firstCol === undefined || range.fromCell < firstCol) {
firstCol = range.fromCell;
}
if (lastCol === undefined || range.toCell > lastCol) {
lastCol = range.toCell;
}
}
let columnRange: ISlickRange = {
fromCell: firstCol,
toCell: lastCol,
fromRow: undefined,
toRow: undefined,
};
let columnHeaders = this.getColumnHeaders(
copyString = this.addHeadersToCopyString(
copyString,
batchId,
resultId,
columnRange,
selection,
);
copyString += columnHeaders.join("\t");
copyString += os.EOL;
}

// sort the selections by row to maintain copy order
selection.sort((a, b) => a.fromRow - b.fromRow);

Expand All @@ -659,16 +640,12 @@ export default class QueryRunner {
batchId,
resultId,
);
for (let row of result.resultSubset.rows) {
let rowNumber = row[0].rowId + range.fromRow;
if (rowIdToSelectionMap.has(rowNumber)) {
let rowSelection = rowIdToSelectionMap.get(rowNumber);
rowSelection.push(range);
} else {
rowIdToSelectionMap.set(rowNumber, [range]);
}
rowIdToRowMap.set(rowNumber, row);
}
this.getRowMappings(
result.resultSubset.rows,
range,
rowIdToSelectionMap,
rowIdToRowMap,
);
};
});

Expand All @@ -679,6 +656,95 @@ export default class QueryRunner {
}
await p;

copyString = this.constructCopyString(
copyString,
rowIdToRowMap,
rowIdToSelectionMap,
);

await this.writeStringToClipboard(copyString);
}

public async exportCellsToClipboard(
data: DbCellValue[][],
batchId: number,
resultId: number,
selection: ISlickRange[],
headersFlag,
) {
let copyString = "";
if (headersFlag) {
copyString = this.addHeadersToCopyString(
copyString,
batchId,
resultId,
selection,
);
}

// create a mapping of rows to selections
let rowIdToSelectionMap = new Map<number, ISlickRange[]>();
let rowIdToRowMap = new Map<number, DbCellValue[]>();

// create a mapping of the ranges to get promises
let tasks = selection.map((range) => {
return async () => {
const result = data;
this.getRowMappings(
result,
range,
rowIdToSelectionMap,
rowIdToRowMap,
);
};
});
let p = tasks[0]();
for (let i = 1; i < tasks.length; i++) {
p = p.then(tasks[i]);
}
await p;

copyString = this.constructCopyString(
copyString,
rowIdToRowMap,
rowIdToSelectionMap,
);

await this.writeStringToClipboard(copyString);
}

/**
* Construct the row mappings, which contain the row data and selection data and are used to construct the copy string
* @param data
* @param range
* @param rowIdToSelectionMap
* @param rowIdToRowMap
*/
private getRowMappings(
data: DbCellValue[][],
range: ISlickRange,
rowIdToSelectionMap,
rowIdToRowMap,
) {
let count = 0;
for (let row of data) {
let rowNumber = count + range.fromRow;
if (rowIdToSelectionMap.has(rowNumber)) {
let rowSelection = rowIdToSelectionMap.get(rowNumber);
rowSelection.push(range);
} else {
rowIdToSelectionMap.set(rowNumber, [range]);
}
rowIdToRowMap.set(rowNumber, row);
count += 1;
}
}

private constructCopyString(
copyString: string,
rowIdToRowMap: Map<number, DbCellValue[]>,
rowIdToSelectionMap: Map<number, ISlickRange[]>,
) {
// Go through all rows and get selections for them
let allRowIds = rowIdToRowMap.keys();
const endColumns = this.getSelectionEndColumns(
Expand Down Expand Up @@ -735,7 +801,51 @@ export default class QueryRunner {
copyString.length - os.EOL.length,
);
}
return copyString;
}

/**
* Add the column headers to the copy string
* @param copyString
* @param batchId
* @param resultId
* @param selection
* @returns
*/
public addHeadersToCopyString(
copyString: string,
batchId: number,
resultId: number,
selection: ISlickRange[],
): string {
// add the column headers
let firstCol: number;
let lastCol: number;
for (let range of selection) {
if (firstCol === undefined || range.fromCell < firstCol) {
firstCol = range.fromCell;
}
if (lastCol === undefined || range.toCell > lastCol) {
lastCol = range.toCell;
}
}
let columnRange: ISlickRange = {
fromCell: firstCol,
toCell: lastCol,
fromRow: undefined,
toRow: undefined,
};
let columnHeaders = this.getColumnHeaders(
batchId,
resultId,
columnRange,
);
copyString += columnHeaders.join("\t");
copyString += os.EOL;
return copyString;
}

public async writeStringToClipboard(copyString: string): Promise<void> {
let oldLang: string;
if (process.platform === "darwin") {
oldLang = process.env["LANG"];
Expand Down
19 changes: 19 additions & 0 deletions src/models/sqlOutputContentProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,25 @@ export class SqlOutputContentProvider {
);
}

public sendToClipboard(
uri: string,
data: Array<any>,
batchId: number,
resultId: number,
selection: ISlickRange[],
headersFlag: boolean,
): void {
void this._queryResultsMap
.get(uri)
.queryRunner.exportCellsToClipboard(
data,
batchId,
resultId,
selection,
headersFlag,
);
}

public editorSelectionRequestHandler(
uri: string,
selection: ISelectionData,
Expand Down
24 changes: 24 additions & 0 deletions src/queryResult/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@ export function registerCommonRequestHandlers(
message.selection,
);
});

webviewController.registerRequestHandler(
"sendToClipboard",
async (message) => {
sendActionEvent(
TelemetryViews.QueryResult,
TelemetryActions.CopyResults,
{
correlationId: correlationId,
},
);
return webviewViewController
.getSqlOutputContentProvider()
.sendToClipboard(
message.uri,
message.data,
message.batchId,
message.resultId,
message.selection,
message.headersFlag,
);
},
);

webviewController.registerRequestHandler(
"copySelection",
async (message) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {
} from "../../../../../sharedInterfaces/queryResult";
import { locConstants } from "../../../../common/locConstants";
import { VscodeWebviewContext } from "../../../../common/vscodeWebviewProvider";
import { IDisposableDataProvider } from "../dataProvider";
import { HybridDataProvider } from "../hybridDataProvider";
import { tryCombineSelectionsForResults } from "../utils";
import { selectionToRange, tryCombineSelectionsForResults } from "../utils";
import "./contextMenu.css";

export class ContextMenu<T extends Slick.SlickData> {
Expand All @@ -32,6 +33,7 @@ export class ContextMenu<T extends Slick.SlickData> {
QueryResultWebviewState,
QueryResultReducers
>,
private dataProvider: IDisposableDataProvider<T>,
) {
this.uri = uri;
this.resultSetSummary = resultSetSummary;
Expand Down Expand Up @@ -116,24 +118,90 @@ export class ContextMenu<T extends Slick.SlickData> {
]);
break;
case "copy":
await this.webViewState.extensionRpc.call("copySelection", {
uri: this.uri,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
});
if (this.dataProvider.isDataInMemory) {
let range = selectionToRange(selection[0]);
let data = await this.dataProvider.getRangeAsync(
range.start,
range.length,
);
const dataArray = data.map((map) => {
const maxKey = Math.max(
...Array.from(Object.keys(map)).map(Number),
); // Get the maximum key
return Array.from(
{ length: maxKey + 1 },
(_, index) => ({
rowId: index,
displayValue: map[index].displayValue || null,
}),
);
});

await this.webViewState.extensionRpc.call(
"sendToClipboard",
{
uri: this.uri,
data: dataArray,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
headersFlag: false,
},
);
} else {
await this.webViewState.extensionRpc.call("copySelection", {
uri: this.uri,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
});
}

console.log("Copy action triggered");
break;
case "copy-with-headers":
await this.webViewState.extensionRpc.call("copyWithHeaders", {
uri: this.uri,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
});
if (this.dataProvider.isDataInMemory) {
let range = selectionToRange(selection[0]);
let data = await this.dataProvider.getRangeAsync(
range.start,
range.length,
);
const dataArray = data.map((map) => {
const maxKey = Math.max(
...Array.from(Object.keys(map)).map(Number),
); // Get the maximum key
return Array.from(
{ length: maxKey + 1 },
(_, index) => ({
rowId: index,
displayValue: map[index].displayValue || null,
}),
);
});

await this.webViewState.extensionRpc.call(
"sendToClipboard",
{
uri: this.uri,
data: dataArray,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
headersFlag: true,
},
);
} else {
await this.webViewState.extensionRpc.call(
"copyWithHeaders",
{
uri: this.uri,
batchId: this.resultSetSummary.batchId,
resultId: this.resultSetSummary.id,
selection: selection,
},
);
}

console.log("Copy with Headers action triggered");
break;
case "copy-headers":
await this.webViewState.extensionRpc.call("copyHeaders", {
Expand Down
Loading

0 comments on commit 2033148

Please sign in to comment.