diff --git a/src/sql/base/browser/ui/table/media/saveMarkdown.svg b/src/sql/base/browser/ui/table/media/saveMarkdown.svg new file mode 100644 index 0000000000..88e75dd354 --- /dev/null +++ b/src/sql/base/browser/ui/table/media/saveMarkdown.svg @@ -0,0 +1,55 @@ + + diff --git a/src/sql/base/browser/ui/table/media/saveMarkdown_inverse.svg b/src/sql/base/browser/ui/table/media/saveMarkdown_inverse.svg new file mode 100644 index 0000000000..bf6adbdc32 --- /dev/null +++ b/src/sql/base/browser/ui/table/media/saveMarkdown_inverse.svg @@ -0,0 +1,55 @@ + + diff --git a/src/sql/base/browser/ui/table/media/slickColorTheme.css b/src/sql/base/browser/ui/table/media/slickColorTheme.css index 974446b2fb..d527387cbe 100644 --- a/src/sql/base/browser/ui/table/media/slickColorTheme.css +++ b/src/sql/base/browser/ui/table/media/slickColorTheme.css @@ -63,6 +63,11 @@ background-image: url("saveExcel.svg"); } +.vs .codicon.saveMarkdown { + /* ResultToMarkdown_16x_vscode */ + background-image: url("saveMarkdown.svg"); +} + .vs .codicon.saveXml { /* ResultToXML_16x_vscode */ background-image: url("saveXml.svg"); @@ -172,6 +177,12 @@ background-image: url("saveExcel_inverse.svg"); } +.vs-dark .codicon.saveMarkdown, +.hc-black .codicon.saveMarkdown { + /* ResultToMarkdown_16x_vscode_inverse.svg */ + background-image: url("saveMarkdown_inverse.svg"); +} + .vs-dark .codicon.saveXml, .hc-black .codicon.saveXml { /* ResultToXml_16x_vscode_inverse.svg */ diff --git a/src/sql/platform/query/common/query.ts b/src/sql/platform/query/common/query.ts index 5f0a0d0ff7..311898632b 100644 --- a/src/sql/platform/query/common/query.ts +++ b/src/sql/platform/query/common/query.ts @@ -15,6 +15,11 @@ export interface IQueryEditorConfiguration { readonly saveAsExcel: { readonly includeHeaders: boolean, }, + readonly saveAsMarkdown: { + readonly encoding: string, + readonly includeHeaders: boolean, + readonly lineSeparator: string, + } readonly saveAsXml: { readonly formatted: boolean, readonly encoding: string diff --git a/src/sql/workbench/contrib/editData/browser/gridActions.ts b/src/sql/workbench/contrib/editData/browser/gridActions.ts index 33f3e6fb1f..05acbe4e20 100644 --- a/src/sql/workbench/contrib/editData/browser/gridActions.ts +++ b/src/sql/workbench/contrib/editData/browser/gridActions.ts @@ -14,6 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti export const GRID_SAVECSV_ID = 'grid.saveAsCsv'; export const GRID_SAVEJSON_ID = 'grid.saveAsJson'; +export const GRID_SAVEMARKDOWN_ID = 'grid.saveAsMarkdown'; export const GRID_SAVEEXCEL_ID = 'grid.saveAsExcel'; export const GRID_SAVEXML_ID = 'grid.saveAsXml'; export const GRID_COPY_ID = 'grid.copySelection'; @@ -46,6 +47,7 @@ export class GridActionProvider { const actions: IAction[] = []; actions.push(this._instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveFormat.CSV, this._dataService)); actions.push(this._instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveFormat.JSON, this._dataService)); + actions.push(this._instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveFormat.MARKDOWN, this._dataService)); actions.push(this._instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveFormat.EXCEL, this._dataService)); actions.push(this._instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveFormat.XML, this._dataService)); actions.push(this._instantiationService.createInstance(SelectAllGridAction, SelectAllGridAction.ID, SelectAllGridAction.LABEL, this._selectAllCallback)); @@ -63,6 +65,9 @@ class SaveResultAction extends Action { public static SAVEJSON_ID = GRID_SAVEJSON_ID; public static SAVEJSON_LABEL = localize('saveAsJson', "Save As JSON"); + public static SAVEMARKDOWN_ID = GRID_SAVEMARKDOWN_ID; + public static SAVEMARKDOWN_LABEL = localize('saveAsMarkdown', "Save As Markdown"); + public static SAVEEXCEL_ID = GRID_SAVEEXCEL_ID; public static SAVEEXCEL_LABEL = localize('saveAsExcel', "Save As Excel"); diff --git a/src/sql/workbench/contrib/editData/browser/gridCommands.ts b/src/sql/workbench/contrib/editData/browser/gridCommands.ts index 0e62e74592..f32f294215 100644 --- a/src/sql/workbench/contrib/editData/browser/gridCommands.ts +++ b/src/sql/workbench/contrib/editData/browser/gridCommands.ts @@ -63,6 +63,10 @@ export const saveAsJson = (accessor: ServicesAccessor) => { runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsJSON); }; +export const saveAsMarkdown = (accessor: ServicesAccessor) => { + runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsMarkdown); +}; + export const saveAsExcel = (accessor: ServicesAccessor) => { runActionOnActiveResultsEditor(accessor, GridContentEvents.SaveAsExcel); }; diff --git a/src/sql/workbench/contrib/editData/browser/gridParentComponent.ts b/src/sql/workbench/contrib/editData/browser/gridParentComponent.ts index 52586a7713..d4a60b77f0 100644 --- a/src/sql/workbench/contrib/editData/browser/gridParentComponent.ts +++ b/src/sql/workbench/contrib/editData/browser/gridParentComponent.ts @@ -139,6 +139,9 @@ export abstract class GridParentComponent extends Disposable { case GridContentEvents.SaveAsJSON: self.sendSaveRequest(SaveFormat.JSON); break; + case GridContentEvents.SaveAsMarkdown: + self.sendSaveRequest(SaveFormat.MARKDOWN); + break; case GridContentEvents.SaveAsExcel: self.sendSaveRequest(SaveFormat.EXCEL); break; @@ -313,6 +316,9 @@ export abstract class GridParentComponent extends Disposable { 'SaveAsJSON': () => { this.sendSaveRequest(SaveFormat.JSON); }, + 'SaveAsMarkdown': () => { + this.sendSaveRequest(SaveFormat.MARKDOWN); + }, 'SaveAsExcel': () => { this.sendSaveRequest(SaveFormat.EXCEL); }, @@ -341,6 +347,9 @@ export abstract class GridParentComponent extends Disposable { case 'savejson': this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.JSON, selection: event.selection }); break; + case 'saveMarkdown': + this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.MARKDOWN, selection: event.selection }); + break; case 'saveexcel': this.dataService.sendSaveRequest({ batchIndex: event.batchId, resultSetNumber: event.resultId, format: SaveFormat.EXCEL, selection: event.selection }); break; 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 31230226fe..4afcb191bd 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -276,6 +276,7 @@ class DataResourceTable extends GridTableBase { this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), + this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveResultAction.SAVEMARKDOWN_ICON, SaveFormat.MARKDOWN), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), this.instantiationService.createInstance(NotebookChartAction, this) ]; diff --git a/src/sql/workbench/contrib/query/browser/actions.ts b/src/sql/workbench/contrib/query/browser/actions.ts index 4e737194ca..eb5da31267 100644 --- a/src/sql/workbench/contrib/query/browser/actions.ts +++ b/src/sql/workbench/contrib/query/browser/actions.ts @@ -52,6 +52,10 @@ export class SaveResultAction extends Action { public static SAVEJSON_LABEL = localize('saveAsJson', "Save As JSON"); public static SAVEJSON_ICON = 'saveJson'; + public static SAVEMARKDOWN_ID = 'grid.saveAsMarkdown'; + public static SAVEMARKDOWN_LABEL = localize('saveAsMarkdown', "Save As Markdown"); + public static SAVEMARKDOWN_ICON = 'saveMarkdown'; + public static SAVEEXCEL_ID = 'grid.saveAsExcel'; public static SAVEEXCEL_LABEL = localize('saveAsExcel', "Save As Excel"); public static SAVEEXCEL_ICON = 'saveExcel'; diff --git a/src/sql/workbench/contrib/query/browser/gridPanel.ts b/src/sql/workbench/contrib/query/browser/gridPanel.ts index 8bcff02ff8..7712830be9 100644 --- a/src/sql/workbench/contrib/query/browser/gridPanel.ts +++ b/src/sql/workbench/contrib/query/browser/gridPanel.ts @@ -953,6 +953,7 @@ class GridTable extends GridTableBase { this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), + this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveResultAction.SAVEMARKDOWN_ICON, SaveFormat.MARKDOWN), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), this.instantiationService.createInstance(ChartDataAction) ); @@ -969,6 +970,7 @@ class GridTable extends GridTableBase { this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVECSV_ID, SaveResultAction.SAVECSV_LABEL, SaveResultAction.SAVECSV_ICON, SaveFormat.CSV), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEEXCEL_ID, SaveResultAction.SAVEEXCEL_LABEL, SaveResultAction.SAVEEXCEL_ICON, SaveFormat.EXCEL), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEJSON_ID, SaveResultAction.SAVEJSON_LABEL, SaveResultAction.SAVEJSON_ICON, SaveFormat.JSON), + this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEMARKDOWN_ID, SaveResultAction.SAVEMARKDOWN_LABEL, SaveResultAction.SAVEMARKDOWN_ICON, SaveFormat.MARKDOWN), this.instantiationService.createInstance(SaveResultAction, SaveResultAction.SAVEXML_ID, SaveResultAction.SAVEXML_LABEL, SaveResultAction.SAVEXML_ICON, SaveFormat.XML), ]; } diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 73b2289ece..93507adba0 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -287,6 +287,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: gridCommands.saveAsJson }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: gridActions.GRID_SAVEMARKDOWN_ID, + weight: KeybindingWeight.EditorContrib, + when: ResultsGridFocusCondition, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyR, KeyMod.CtrlCmd | KeyCode.KeyM), + handler: gridCommands.saveAsMarkdown +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: gridActions.GRID_SAVEEXCEL_ID, weight: KeybindingWeight.EditorContrib, @@ -384,6 +392,21 @@ const queryEditorConfiguration: IConfigurationNode = { 'description': localize('queryEditor.results.saveAsCsv.encoding', "File encoding used when saving results as CSV"), 'default': 'utf-8' }, + 'queryEditor.results.saveAsMarkdown.encoding': { + 'type': 'string', + 'description': localize('queryEditor.results.saveAsMarkdown.encoding', "File encoding used when saving results as Markdown"), + 'default': 'utf-8' + }, + 'queryEditor.results.saveAsMarkdown.includeHeaders': { + 'type': 'boolean', + 'description': localize('queryEditor.results.saveAsMarkdown.includeHeaders', "When true, column headers are included when saving results as a Markdown file"), + 'default': true + }, + 'queryEditor.results.saveAsMarkdown.lineSeparator': { + 'type': 'string', + 'description': localize('queryEditor.results.saveAsMarkdown.lineSeparator', "Character(s) to use to separate lines when exporting to Markdown, defaults to system line endings"), + 'default': null + }, 'queryEditor.results.saveAsXml.formatted': { 'type': 'boolean', 'description': localize('queryEditor.results.saveAsXml.formatted', "When true, XML output will be formatted when saving results as XML"), diff --git a/src/sql/workbench/services/query/common/gridContentEvents.ts b/src/sql/workbench/services/query/common/gridContentEvents.ts index 5d39850433..61fba5e0f0 100644 --- a/src/sql/workbench/services/query/common/gridContentEvents.ts +++ b/src/sql/workbench/services/query/common/gridContentEvents.ts @@ -14,6 +14,7 @@ export const SelectAll = 'SelectAll'; export const SelectAllMessages = 'SelectAllMessages'; export const SaveAsCsv = 'SaveAsCSV'; export const SaveAsJSON = 'SaveAsJSON'; +export const SaveAsMarkdown = 'SaveAsMarkdown'; export const SaveAsExcel = 'SaveAsExcel'; export const SaveAsXML = 'SaveAsXML'; export const ViewAsChart = 'ViewAsChart'; diff --git a/src/sql/workbench/services/query/common/resultSerializer.ts b/src/sql/workbench/services/query/common/resultSerializer.ts index 18e9429dfb..d61428c249 100644 --- a/src/sql/workbench/services/query/common/resultSerializer.ts +++ b/src/sql/workbench/services/query/common/resultSerializer.ts @@ -38,6 +38,7 @@ export interface SaveResultsResponse { export enum SaveFormat { CSV = 'csv', JSON = 'json', + MARKDOWN = 'markdown', EXCEL = 'excel', XML = 'xml' } @@ -128,6 +129,9 @@ export class ResultSerializer { case SaveFormat.JSON: fileName = fileName + '.json'; break; + case SaveFormat.MARKDOWN: + fileName = fileName + '.md'; + break; case SaveFormat.EXCEL: fileName = fileName + '.xlsx'; break; @@ -153,6 +157,10 @@ export class ResultSerializer { fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionJSONTitle', "JSON"); fileFilter.extensions = ['json']; break; + case SaveFormat.MARKDOWN: + fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionMarkdownTitle', "Markdown"); + fileFilter.extensions = ['md']; + break; case SaveFormat.EXCEL: fileFilter.name = nls.localize('resultsSerializer.saveAsFileExtensionExcelTitle', "Excel Workbook"); fileFilter.extensions = ['xlsx']; @@ -170,22 +178,21 @@ export class ResultSerializer { return fileFilters; } - public getBasicSaveParameters(format: string): SaveResultsRequestParams { - let saveResultsParams: SaveResultsRequestParams; - - if (format === SaveFormat.CSV) { - saveResultsParams = this.getConfigForCsv(); - } else if (format === SaveFormat.JSON) { - saveResultsParams = this.getConfigForJson(); - } else if (format === SaveFormat.EXCEL) { - saveResultsParams = this.getConfigForExcel(); - } else if (format === SaveFormat.XML) { - saveResultsParams = this.getConfigForXml(); + public getBasicSaveParameters(format: SaveFormat): SaveResultsRequestParams { + switch (format) { + case SaveFormat.CSV: + return this.getConfigForCsv(); + case SaveFormat.EXCEL: + return this.getConfigForExcel(); + case SaveFormat.JSON: + return this.getConfigForJson(); + case SaveFormat.MARKDOWN: + return this.getConfigForMarkdown(); + case SaveFormat.XML: + return this.getConfigForXml(); } - return saveResultsParams!; // this could be unsafe } - private getConfigForCsv(): SaveResultsRequestParams { let saveResultsParams = { resultFormat: SaveFormat.CSV as string }; @@ -219,6 +226,26 @@ export class ResultSerializer { return { resultFormat: SaveFormat.JSON as string }; } + private getConfigForMarkdown(): SaveResultsRequestParams { + let saveResultsParams = { resultFormat: SaveFormat.MARKDOWN as string }; + + // Get config from VSCode config and build params with it if user has set config + const saveConfig = this._configurationService.getValue('queryEditor').results.saveAsMarkdown; + if (saveConfig) { + if (saveConfig.encoding) { + saveResultsParams.encoding = saveConfig.encoding; + } + if (saveConfig.includeHeaders !== undefined) { + saveResultsParams.includeHeaders = saveConfig.includeHeaders; + } + if (saveConfig.lineSeparator !== undefined) { + saveResultsParams.lineSeperator = saveConfig.lineSeparator; + } + } + + return saveResultsParams; + } + private getConfigForExcel(): SaveResultsRequestParams { let saveResultsParams = { resultFormat: SaveFormat.EXCEL as string }; @@ -260,7 +287,7 @@ export class ResultSerializer { filePath: URI, batchIndex: number, resultSetNo: number, - format: string, + format: SaveFormat, selection?: Slick.Range ): SaveResultsRequestParams { let saveResultsParams = this.getBasicSaveParameters(format);