diff --git a/extensions/mssql/config.json b/extensions/mssql/config.json index ced60839a1..66ca52d736 100644 --- a/extensions/mssql/config.json +++ b/extensions/mssql/config.json @@ -1,6 +1,6 @@ { "downloadUrl": "https://github.com/Microsoft/sqltoolsservice/releases/download/v{#version#}/microsoft.sqltools.servicelayer-{#fileName#}", - "version": "3.0.0-release.8", + "version": "3.0.0-release.11", "downloadFileNames": { "Windows_86": "win-x86-netcoreapp3.1.zip", "Windows_64": "win-x64-netcoreapp3.1.zip", diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index d54920288e..a509b6b62e 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -21,6 +21,18 @@ }, "contributes": { "commands": [ + { + "command": "mssql.exportSqlAsNotebook", + "title": "%mssql.exportSqlAsNotebook%" + }, + { + "command": "mssql.exportNotebookToSql", + "title": "%mssql.exportNotebookToSql%", + "icon": { + "dark": "resources/dark/export_blue_dark.svg", + "light": "resources/light/export_blue_light.svg" + } + }, { "command": "mssqlCluster.uploadFiles", "title": "%mssqlCluster.uploadFiles%" @@ -355,6 +367,14 @@ }, "menus": { "commandPalette": [ + { + "command": "mssql.exportSqlAsNotebook", + "when": "false" + }, + { + "command": "mssql.exportNotebookToSql", + "when": "false" + }, { "command": "mssqlCluster.uploadFiles", "when": "false" @@ -450,6 +470,12 @@ "when": "nodeType == mssqlCluster:file && nodeSubType =~/:spark:/", "group": "1mssqlCluster@6" } + ], + "notebook/toolbar": [ + { + "command": "mssql.exportNotebookToSql", + "when": "providerId == sql" + } ] }, "dashboard": { diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 93ef9ae583..e0127e10a5 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -39,6 +39,8 @@ "mssql.disabled": "Disabled", "mssql.enabled": "Enabled", + "mssql.exportNotebookToSql": "Export Notebook as SQL", + "mssql.exportSqlAsNotebook": "Export SQL as Notebook", "mssql.configuration.title": "MSSQL configuration", "mssql.query.displayBitAsNumber": "Should BIT columns be displayed as numbers (1 or 0)? If false, BIT columns will be displayed as 'true' or 'false'", "mssql.query.maxXmlCharsToStore": "Number of XML characters to store after running a query", diff --git a/extensions/mssql/resources/dark/export_blue_dark.svg b/extensions/mssql/resources/dark/export_blue_dark.svg new file mode 100644 index 0000000000..5f9f477494 --- /dev/null +++ b/extensions/mssql/resources/dark/export_blue_dark.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/extensions/mssql/resources/light/export_blue_light.svg b/extensions/mssql/resources/light/export_blue_light.svg new file mode 100644 index 0000000000..09c9df4d2b --- /dev/null +++ b/extensions/mssql/resources/light/export_blue_light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/extensions/mssql/src/constants.ts b/extensions/mssql/src/constants.ts index 4a4ac34575..c43f1ea4f7 100644 --- a/extensions/mssql/src/constants.ts +++ b/extensions/mssql/src/constants.ts @@ -31,6 +31,8 @@ export const hdfsRootPath = '/'; export const clusterEndpointsProperty = 'clusterEndpoints'; export const isBigDataClusterProperty = 'isBigDataCluster'; +export const ViewType = 'view'; + // SERVICE NAMES ////////////////////////////////////////////////////////// export const ObjectExplorerService = 'objectexplorer'; export const CmsService = 'cmsService'; @@ -38,8 +40,8 @@ export const DacFxService = 'dacfxService'; export const SchemaCompareService = 'schemaCompareService'; export const LanguageExtensionService = 'languageExtensionService'; export const objectExplorerPrefix: string = 'objectexplorer://'; -export const ViewType = 'view'; export const SqlAssessmentService = 'sqlAssessmentService'; +export const NotebookConvertService = 'notebookConvertService'; export enum BuiltInCommands { SetContext = 'setContext' diff --git a/extensions/mssql/src/contracts.ts b/extensions/mssql/src/contracts.ts index 268925bbfa..43460dcaf9 100644 --- a/extensions/mssql/src/contracts.ts +++ b/extensions/mssql/src/contracts.ts @@ -747,3 +747,33 @@ export class CompletionExtensionParams { export namespace CompletionExtLoadRequest { export const type = new RequestType('completion/extLoad'); } + +// ------------------------------- < Load Completion Extension Request > ------------------------------------ + +/// ------------------------------- ----------------------------- + +export interface ConvertNotebookToSqlParams { + content: string; +} + +export namespace ConvertNotebookToSqlRequest { + export const type = new RequestType('notebookconvert/convertnotebooktosql'); +} + +export interface ConvertNotebookToSqlResult extends azdata.ResultStatus { + content: string; +} + +export interface ConvertSqlToNotebookParams { + clientUri: string; +} + +export namespace ConvertSqlToNotebookRequest { + export const type = new RequestType('notebookconvert/convertsqltonotebook'); +} + +export interface ConvertSqlToNotebookResult extends azdata.ResultStatus { + content: string; +} + +// ------------------------------- ----------------------------- diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 5d28005e2a..506d0c9ab9 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -30,6 +30,7 @@ import { SqlToolsServer } from './sqlToolsServer'; import { promises as fs } from 'fs'; import { IconPathHelper } from './iconHelper'; import * as nls from 'vscode-nls'; +import { INotebookConvertService } from './notebookConvert/notebookConvertService'; const localize = nls.loadMessageBundle(); const msgSampleCodeDataFrame = localize('msgSampleCodeDataFrame', "This sample code loads the file into a data frame and shows the first 10 results."); @@ -79,6 +80,24 @@ export async function activate(context: vscode.ExtensionContext): Promise { + const result = await appContext.getService(Constants.NotebookConvertService).convertSqlToNotebook(uri.toString()); + const title = findNextUntitledEditorName(); + const untitledUri = vscode.Uri.parse(`untitled:${title}`); + await azdata.nb.showNotebookDocument(untitledUri, { initialContent: result.content }); + }); + + vscode.commands.registerCommand('mssql.exportNotebookToSql', async (uri: vscode.Uri) => { + // SqlToolsService doesn't currently store anything about Notebook documents so we have to pass the raw JSON to it directly + // We use vscode.workspace.textDocuments here because the azdata.nb.notebookDocuments don't actually contain their contents + // (they're left out for perf purposes) + const doc = vscode.workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString()); + const result = await appContext.getService(Constants.NotebookConvertService).convertNotebookToSql(doc.getText()); + + const sqlDoc = await vscode.workspace.openTextDocument({ language: 'sql', content: result.content }); + await vscode.commands.executeCommand('vscode.open', sqlDoc.uri); + }); + return createMssqlApi(appContext); } diff --git a/extensions/mssql/src/notebookConvert/notebookConvertService.ts b/extensions/mssql/src/notebookConvert/notebookConvertService.ts new file mode 100644 index 0000000000..1b2e13a09c --- /dev/null +++ b/extensions/mssql/src/notebookConvert/notebookConvertService.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AppContext } from '../appContext'; +import { SqlOpsDataClient, ISqlOpsFeature } from 'dataprotocol-client'; +import { ClientCapabilities } from 'vscode-languageclient'; +import * as constants from '../constants'; +import * as contracts from '../contracts'; + +export interface INotebookConvertService { + convertNotebookToSql(content: string): Promise; + convertSqlToNotebook(content: string): Promise; +} + +export class NotebookConvertService implements INotebookConvertService { + public static asFeature(context: AppContext): ISqlOpsFeature { + return class extends NotebookConvertService { + constructor(client: SqlOpsDataClient) { + super(context, client); + } + + fillClientCapabilities(capabilities: ClientCapabilities): void { + } + + initialize(): void { + } + }; + } + + private constructor(context: AppContext, protected readonly client: SqlOpsDataClient) { + context.registerService(constants.NotebookConvertService, this); + } + + async convertNotebookToSql(content: string): Promise { + let params: contracts.ConvertNotebookToSqlParams = { content: content }; + try { + return this.client.sendRequest(contracts.ConvertNotebookToSqlRequest.type, params); + } + catch (e) { + this.client.logFailedRequest(contracts.ConvertNotebookToSqlRequest.type, e); + } + + return undefined; + } + async convertSqlToNotebook(content: string): Promise { + let params: contracts.ConvertSqlToNotebookParams = { clientUri: content }; + try { + return this.client.sendRequest(contracts.ConvertSqlToNotebookRequest.type, params); + } + catch (e) { + this.client.logFailedRequest(contracts.ConvertSqlToNotebookRequest.type, e); + } + + return undefined; + } +} diff --git a/extensions/mssql/src/sqlToolsServer.ts b/extensions/mssql/src/sqlToolsServer.ts index cb03a7cb54..0d0979ccda 100644 --- a/extensions/mssql/src/sqlToolsServer.ts +++ b/extensions/mssql/src/sqlToolsServer.ts @@ -23,6 +23,7 @@ import { promises as fs } from 'fs'; import * as nls from 'vscode-nls'; import { LanguageExtensionService } from './languageExtension/languageExtensionService'; import { SqlAssessmentService } from './sqlAssessment/sqlAssessmentService'; +import { NotebookConvertService } from './notebookConvert/notebookConvertService'; const localize = nls.loadMessageBundle(); const outputChannel = vscode.window.createOutputChannel(Constants.serviceName); @@ -160,7 +161,8 @@ function getClientOptions(context: AppContext): ClientOptions { LanguageExtensionService.asFeature(context), DacFxService.asFeature(context), CmsService.asFeature(context), - SqlAssessmentService.asFeature(context) + SqlAssessmentService.asFeature(context), + NotebookConvertService.asFeature(context) ], outputChannel: new CustomOutputChannel() }; diff --git a/src/sql/base/browser/ui/taskbar/media/icons.css b/src/sql/base/browser/ui/taskbar/media/icons.css index deb3395318..6f736a2e05 100644 --- a/src/sql/base/browser/ui/taskbar/media/icons.css +++ b/src/sql/base/browser/ui/taskbar/media/icons.css @@ -118,3 +118,9 @@ background-image: url('disable_sqlcmd_inverse.svg'); background-repeat: no-repeat; } + +.carbon-taskbar .codicon.export { + background-origin: initial; + background-position: left; + background-size: 11px +} diff --git a/src/sql/media/icons/common-icons.css b/src/sql/media/icons/common-icons.css index 4175f3604b..a22bdbbd17 100644 --- a/src/sql/media/icons/common-icons.css +++ b/src/sql/media/icons/common-icons.css @@ -287,6 +287,15 @@ background-image: url("stop_inverse.svg"); } +.hc-black .codicon.export, +.vs-dark .codicon.export { + background: url("export_inverse.svg") center center no-repeat; +} + +.vs .codicon.export { + background: url("export.svg") center center no-repeat; +} + /* Notebook cells */ .codicon.toolbarIconRunInactive { background-image: url("execute_cell_grey.svg"); diff --git a/src/sql/media/icons/export.svg b/src/sql/media/icons/export.svg new file mode 100644 index 0000000000..719d989fcd --- /dev/null +++ b/src/sql/media/icons/export.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sql/media/icons/export_inverse.svg b/src/sql/media/icons/export_inverse.svg new file mode 100644 index 0000000000..1dee89afda --- /dev/null +++ b/src/sql/media/icons/export_inverse.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts index a70a06f86f..0bb919cf09 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.component.ts @@ -553,7 +553,7 @@ export class NotebookComponent extends AngularDisposable implements OnInit, OnDe // This is similar behavior that exists in MenuItemActionItem if (action instanceof MenuItemAction) { - if (action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) { + if ((action.item.id.includes('jupyter.cmd') && this.previewFeaturesEnabled) || action.item.id.includes('mssql')) { action.tooltip = action.label; action.label = ''; } diff --git a/src/sql/workbench/contrib/query/browser/queryActions.ts b/src/sql/workbench/contrib/query/browser/queryActions.ts index 85e1f8e897..3a13a76fb4 100644 --- a/src/sql/workbench/contrib/query/browser/queryActions.ts +++ b/src/sql/workbench/contrib/query/browser/queryActions.ts @@ -36,7 +36,7 @@ import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/q import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { getCurrentGlobalConnection } from 'sql/workbench/browser/taskUtilities'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { OEAction } from 'sql/workbench/services/objectExplorer/browser/objectExplorerActions'; import { TreeViewItemHandleArg } from 'sql/workbench/common/views'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; @@ -813,3 +813,27 @@ export class ListDatabasesActionItem extends Disposable implements IActionViewIt } } + +/** + * Action class that sends the request to convert the contents of the sql editor + * into a Notebook document + */ +export class ExportAsNotebookAction extends QueryTaskbarAction { + + public static IconClass = 'export'; + public static ID = 'exportAsNotebookAction'; + + constructor( + editor: QueryEditor, + @IConnectionManagementService connectionManagementService: IConnectionManagementService, + @ICommandService private _commandService: ICommandService + ) { + super(connectionManagementService, editor, ConnectDatabaseAction.ID, ExportAsNotebookAction.IconClass); + + this.label = nls.localize('queryEditor.exportSqlAsNotebook', "Export as Notebook"); + } + + public async run(): Promise { + this._commandService.executeCommand('mssql.exportSqlAsNotebook', this.editor.input.uri); + } +} diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index 5e2177b486..b9b12cc487 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -86,6 +86,7 @@ export class QueryEditor extends BaseEditor { private _actualQueryPlanAction: actions.ActualQueryPlanAction; private _listDatabasesActionItem: actions.ListDatabasesActionItem; private _toggleSqlcmdMode: actions.ToggleSqlCmdModeAction; + private _exportAsNotebookAction: actions.ExportAsNotebookAction; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -183,6 +184,7 @@ export class QueryEditor extends BaseEditor { this._estimatedQueryPlanAction = this.instantiationService.createInstance(actions.EstimatedQueryPlanAction, this); this._actualQueryPlanAction = this.instantiationService.createInstance(actions.ActualQueryPlanAction, this); this._toggleSqlcmdMode = this.instantiationService.createInstance(actions.ToggleSqlCmdModeAction, this, false); + this._exportAsNotebookAction = this.instantiationService.createInstance(actions.ExportAsNotebookAction, this); this.setTaskbarContent(); @@ -266,13 +268,14 @@ export class QueryEditor extends BaseEditor { { action: this._listDatabasesAction }, { element: separator }, { action: this._estimatedQueryPlanAction }, - { action: this._toggleSqlcmdMode } + { action: this._toggleSqlcmdMode }, + { action: this._exportAsNotebookAction } ]; // Remove the estimated query plan action if preview features are not enabled let previewFeaturesEnabled = this.configurationService.getValue('workbench')['enablePreviewFeatures']; if (!previewFeaturesEnabled) { - content = content.slice(0, -2); + content.splice(7, 1); } this.taskbar.setContent(content);