mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-29 01:25:37 -05:00
Share Notebook grid rendering with Query editor (#6241)
This is a staged refactor to use the exact same grid logic in the Notebook and query editors, including context menu support, font settings, and sizing logic. The goal long term is: - As the core Query grid is updated, Notebook can benefit from the changes - As we add in support for contributions like new buttons & actions working on the grid, can share the logic - Ideally if and when we refactor things like the action bar for grid results, we can apply in both places though this is TBD. Fixes a number of issues: - Fixes #5755 Grids don't respond to font settings. @anthonydresser can we remove setting from each query results editor and just use Notebook Styles since these are global (not scoped) settings? - Fixes #5501 Copy from grid settings. - Fixes #4938 SQL Notebook result sets are missing the actions provide for SQL File results sets. this now has the core ability to solve this, and separate work items for specific asks (serialization, charting) are tracked. Currently hidden: - Save as... options in context menu - All right toolbar actions (save as, chart). Remaining issues to address in future commits: - Need to implement support for serialization (#5137). - Need to add charting support - Need to solve the layout of buttons on the right hand side when a small number of columns are output. It doesn't look right that buttons are so far away from the results - Will work with UX on this. For now, mitigating this by hiding all buttons, but will need to solve in the future - Would like to make buttons contributable via extension, but need to refactor similar to ObjectExplorer context menu so that we can serialize context menu options across to extension host while still having internal actions with full support
This commit is contained in:
@@ -26,6 +26,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { ITextResourcePropertiesService } from 'vs/editor/common/services/resourceConfiguration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { mssqlProviderName } from 'sql/platform/connection/common/constants';
|
||||
import { IGridDataProvider, getResultsString } from 'sql/platform/query/common/gridDataProvider';
|
||||
import { getErrorMessage } from 'sql/workbench/parts/notebook/notebookUtils';
|
||||
|
||||
export interface IEditSessionReadyEvent {
|
||||
ownerUri: string;
|
||||
@@ -98,7 +100,6 @@ export default class QueryRunner extends Disposable {
|
||||
@IQueryManagementService private _queryManagementService: IQueryManagementService,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IInstantiationService private instantiationService: IInstantiationService,
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
@@ -212,7 +213,7 @@ export default class QueryRunner extends Disposable {
|
||||
|
||||
private handleFailureRunQueryResult(error: any) {
|
||||
// Attempting to launch the query failed, show the error message
|
||||
const eol = this.getEolString();
|
||||
const eol = getEolString(this._textResourcePropertiesService, this.uri);
|
||||
if (error instanceof Error) {
|
||||
error = error.message;
|
||||
}
|
||||
@@ -437,10 +438,7 @@ export default class QueryRunner extends Disposable {
|
||||
|
||||
// TODO issue #228 add statusview callbacks here
|
||||
this._isExecuting = false;
|
||||
this._notificationService.notify({
|
||||
severity: Severity.Error,
|
||||
message: nls.localize('query.initEditExecutionFailed', 'Init Edit Execution failed: ') + error
|
||||
});
|
||||
this._notificationService.error(nls.localize('query.initEditExecutionFailed', "Initialize edit data session failed: ") + error);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -541,76 +539,12 @@ export default class QueryRunner extends Disposable {
|
||||
* @param includeHeaders [Optional]: Should column headers be included in the copy selection
|
||||
*/
|
||||
copyResults(selection: Slick.Range[], batchId: number, resultId: number, includeHeaders?: boolean): void {
|
||||
const self = this;
|
||||
let copyString = '';
|
||||
const eol = this.getEolString();
|
||||
|
||||
// create a mapping of the ranges to get promises
|
||||
let tasks = selection.map((range, i) => {
|
||||
return () => {
|
||||
return self.getQueryRows(range.fromRow, range.toRow - range.fromRow + 1, batchId, resultId).then((result) => {
|
||||
// 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 (i > 0) {
|
||||
copyString += eol;
|
||||
}
|
||||
|
||||
if (self.shouldIncludeHeaders(includeHeaders)) {
|
||||
let columnHeaders = self.getColumnHeaders(batchId, resultId, range);
|
||||
if (columnHeaders !== undefined) {
|
||||
copyString += columnHeaders.join('\t') + eol;
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the rows to paste into the copy string
|
||||
for (let rowIndex: number = 0; rowIndex < result.resultSubset.rows.length; rowIndex++) {
|
||||
let row = result.resultSubset.rows[rowIndex];
|
||||
let cellObjects = row.slice(range.fromCell, (range.toCell + 1));
|
||||
// Remove newlines if requested
|
||||
let cells = self.shouldRemoveNewLines()
|
||||
? cellObjects.map(x => self.removeNewLines(x.displayValue))
|
||||
: cellObjects.map(x => x.displayValue);
|
||||
copyString += cells.join('\t');
|
||||
if (rowIndex < result.resultSubset.rows.length - 1) {
|
||||
copyString += eol;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
if (tasks.length > 0) {
|
||||
let p = tasks[0]();
|
||||
for (let i = 1; i < tasks.length; i++) {
|
||||
p = p.then(tasks[i]);
|
||||
}
|
||||
p.then(() => {
|
||||
this._clipboardService.writeText(copyString);
|
||||
});
|
||||
}
|
||||
let provider = this.getGridDataProvider(batchId, resultId);
|
||||
provider.copyResults(selection, includeHeaders);
|
||||
}
|
||||
|
||||
private getEolString(): string {
|
||||
return this._textResourcePropertiesService.getEOL(URI.parse(this.uri), 'sql');
|
||||
}
|
||||
|
||||
private shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||
if (includeHeaders !== undefined) {
|
||||
// Respect the value explicity passed into the method
|
||||
return includeHeaders;
|
||||
}
|
||||
// else get config option from vscode config
|
||||
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.copyIncludeHeaders);
|
||||
return !!includeHeaders;
|
||||
}
|
||||
|
||||
private shouldRemoveNewLines(): boolean {
|
||||
// get config copyRemoveNewLine option from vscode config
|
||||
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configCopyRemoveNewLine);
|
||||
return !!removeNewLines;
|
||||
}
|
||||
|
||||
private getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
||||
public getColumnHeaders(batchId: number, resultId: number, range: Slick.Range): string[] {
|
||||
let headers: string[] = undefined;
|
||||
let batchSummary: azdata.BatchSummary = this._batchSets[batchId];
|
||||
if (batchSummary !== undefined) {
|
||||
@@ -622,19 +556,6 @@ export default class QueryRunner extends Disposable {
|
||||
return headers;
|
||||
}
|
||||
|
||||
private removeNewLines(inputString: string): string {
|
||||
// This regex removes all newlines in all OS types
|
||||
// Windows(CRLF): \r\n
|
||||
// Linux(LF)/Modern MacOS: \n
|
||||
// Old MacOs: \r
|
||||
if (types.isUndefinedOrNull(inputString)) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
let outputString: string = inputString.replace(/(\r\n|\n|\r)/gm, '');
|
||||
return outputString;
|
||||
}
|
||||
|
||||
private sendBatchTimeMessage(batchId: number, executionTime: string): void {
|
||||
// get config copyRemoveNewLine option from vscode config
|
||||
let showBatchTime: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(this._configurationService, Constants.configShowBatchTime);
|
||||
@@ -654,4 +575,80 @@ export default class QueryRunner extends Disposable {
|
||||
public serializeResults(batchId: number, resultSetId: number, format: SaveFormat, selection: Slick.Range[]) {
|
||||
return this.instantiationService.createInstance(ResultSerializer).saveResults(this.uri, { selection, format, batchIndex: batchId, resultSetNumber: resultSetId });
|
||||
}
|
||||
|
||||
public getGridDataProvider(batchId: number, resultSetId: number): IGridDataProvider {
|
||||
return this.instantiationService.createInstance(QueryGridDataProvider, this, batchId, resultSetId);
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryGridDataProvider implements IGridDataProvider {
|
||||
|
||||
constructor(
|
||||
private queryRunner: QueryRunner,
|
||||
private batchId: number,
|
||||
private resultSetId: number,
|
||||
@INotificationService private _notificationService: INotificationService,
|
||||
@IClipboardService private _clipboardService: IClipboardService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@ITextResourcePropertiesService private _textResourcePropertiesService: ITextResourcePropertiesService
|
||||
) {
|
||||
}
|
||||
|
||||
getRowData(rowStart: number, numberOfRows: number): Thenable<azdata.QueryExecuteSubsetResult> {
|
||||
return this.queryRunner.getQueryRows(rowStart, numberOfRows, this.batchId, this.resultSetId);
|
||||
}
|
||||
|
||||
copyResults(selection: Slick.Range[], includeHeaders?: boolean): void {
|
||||
this.copyResultsAsync(selection, includeHeaders);
|
||||
}
|
||||
|
||||
private async copyResultsAsync(selection: Slick.Range[], includeHeaders?: boolean): Promise<void> {
|
||||
try {
|
||||
let results = await getResultsString(this, selection, includeHeaders);
|
||||
this._clipboardService.writeText(results);
|
||||
} catch (error) {
|
||||
this._notificationService.error(nls.localize('copyFailed', "Copy failed with error {0}", getErrorMessage(error)));
|
||||
}
|
||||
}
|
||||
getEolString(): string {
|
||||
return getEolString(this._textResourcePropertiesService, this.queryRunner.uri);
|
||||
}
|
||||
shouldIncludeHeaders(includeHeaders: boolean): boolean {
|
||||
return shouldIncludeHeaders(includeHeaders, this._configurationService);
|
||||
}
|
||||
shouldRemoveNewLines(): boolean {
|
||||
return shouldRemoveNewLines(this._configurationService);
|
||||
}
|
||||
getColumnHeaders(range: Slick.Range): string[] {
|
||||
return this.queryRunner.getColumnHeaders(this.batchId, this.resultSetId, range);
|
||||
}
|
||||
|
||||
get canSerialize(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
serializeResults(format: SaveFormat, selection: Slick.Range[]): Thenable<void> {
|
||||
return this.queryRunner.serializeResults(this.batchId, this.resultSetId, format, selection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getEolString(textResourcePropertiesService: ITextResourcePropertiesService, uri: string): string {
|
||||
return textResourcePropertiesService.getEOL(URI.parse(uri), 'sql');
|
||||
}
|
||||
|
||||
export function shouldIncludeHeaders(includeHeaders: boolean, configurationService: IConfigurationService): boolean {
|
||||
if (includeHeaders !== undefined) {
|
||||
// Respect the value explicity passed into the method
|
||||
return includeHeaders;
|
||||
}
|
||||
// else get config option from vscode config
|
||||
includeHeaders = WorkbenchUtils.getSqlConfigValue<boolean>(configurationService, Constants.copyIncludeHeaders);
|
||||
return !!includeHeaders;
|
||||
}
|
||||
|
||||
export function shouldRemoveNewLines(configurationService: IConfigurationService): boolean {
|
||||
// get config copyRemoveNewLine option from vscode config
|
||||
let removeNewLines: boolean = WorkbenchUtils.getSqlConfigValue<boolean>(configurationService, Constants.configCopyRemoveNewLine);
|
||||
return !!removeNewLines;
|
||||
}
|
||||
Reference in New Issue
Block a user