diff --git a/.vscode/launch.json b/.vscode/launch.json index 59f5253d51..9e339c88f8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -294,6 +294,31 @@ "group": "5_samples" }, "timeout": 30000 + }, + { + "name": "Run Sample Notebook Provider Extension", + "type": "sqlopsExtensionHost", + "request": "launch", + "windows": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.bat" + }, + "osx": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" + }, + "linux": { + "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" + }, + "args": [ + "--extensionDevelopmentPath=${workspaceRoot}/samples/sample-notebook-provider" + ], + "outFiles": [ + "${workspaceRoot}/samples/sample-notebook-provider/out/**/*.js" + ], + "preLaunchTask": "Watch sample-notebook-provider", + "presentation": { + "group": "5_samples" + }, + "timeout": 30000 } ], "compounds": [ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bad21a1087..c8f5b7d7ab 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -243,6 +243,18 @@ "reveal": "never" }, "group": "build" + }, + { + "type": "npm", + "script": "watch", + "label": "Watch sample-notebook-provider", + "path": "./samples/sample-notebook-provider/package.json", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": "build" } ] } diff --git a/samples/sample-notebook-provider/.gitignore b/samples/sample-notebook-provider/.gitignore new file mode 100644 index 0000000000..1fcb1529f8 --- /dev/null +++ b/samples/sample-notebook-provider/.gitignore @@ -0,0 +1 @@ +out diff --git a/samples/sample-notebook-provider/.yarnrc b/samples/sample-notebook-provider/.yarnrc new file mode 100644 index 0000000000..f757a6ac58 --- /dev/null +++ b/samples/sample-notebook-provider/.yarnrc @@ -0,0 +1 @@ +--ignore-engines true \ No newline at end of file diff --git a/samples/sample-notebook-provider/README.md b/samples/sample-notebook-provider/README.md new file mode 100644 index 0000000000..84352009f3 --- /dev/null +++ b/samples/sample-notebook-provider/README.md @@ -0,0 +1,3 @@ +# Notebook Provider Samples + +This extension provides examples of how to provide a new Notebook serializer and controller for handling loading and running a custom Notebook. diff --git a/samples/sample-notebook-provider/images/computer-cat.gif b/samples/sample-notebook-provider/images/computer-cat.gif new file mode 100644 index 0000000000..ca2398a552 Binary files /dev/null and b/samples/sample-notebook-provider/images/computer-cat.gif differ diff --git a/samples/sample-notebook-provider/images/sqlserver.png b/samples/sample-notebook-provider/images/sqlserver.png new file mode 100644 index 0000000000..d884faa14a Binary files /dev/null and b/samples/sample-notebook-provider/images/sqlserver.png differ diff --git a/samples/sample-notebook-provider/package.json b/samples/sample-notebook-provider/package.json new file mode 100644 index 0000000000..9683c489ce --- /dev/null +++ b/samples/sample-notebook-provider/package.json @@ -0,0 +1,54 @@ +{ + "name": "sample-notebook-provider", + "displayName": "%extension-display-name%", + "description": "%extension-description%", + "version": "0.0.1", + "publisher": "Microsoft", + "preview": true, + "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/main/LICENSE.txt", + "icon": "images/sqlserver.png", + "engines": { + "vscode": "*", + "azdata": ">=1.34.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "extensionDependencies": [ + "microsoft.notebook" + ], + "main": "./out/extension", + "scripts": { + "compile": "node node_modules/typescript/bin/tsc -p ./tsconfig.json", + "watch": "node node_modules/typescript/bin/tsc --watch -p ./tsconfig.json" + }, + "contributes": { + "notebooks": [ + { + "id": "my-notebook", + "type": "my-notebook", + "displayName": "My Custom Notebook", + "selector": [ + { + "filenamePattern": "*.mynotebook" + } + ] + } + ] + }, + "dependencies": { + }, + "devDependencies": { + "@types/azdata": "^1.34.0", + "@types/vscode": "^1.61.0", + "@types/node": "14.x", + "typescript": "^4.4.0-dev.20210607" + } +} diff --git a/samples/sample-notebook-provider/package.nls.json b/samples/sample-notebook-provider/package.nls.json new file mode 100644 index 0000000000..4903df70b6 --- /dev/null +++ b/samples/sample-notebook-provider/package.nls.json @@ -0,0 +1,4 @@ +{ + "extension-display-name": "Sample Notebook provider extension for Azure Data Studio", + "extension-description": "Demonstrates contributing a custom Notebook provider which allows users to contribute their own custom Notebooks." +} diff --git a/samples/sample-notebook-provider/src/extension.ts b/samples/sample-notebook-provider/src/extension.ts new file mode 100644 index 0000000000..2018cb1715 --- /dev/null +++ b/samples/sample-notebook-provider/src/extension.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { SampleController } from './sampleController'; +import { SampleSerializer } from './sampleSerializer'; + +export function activate(context: vscode.ExtensionContext) { + // Create and register the serializer and controller with Azure Data Studio + context.subscriptions.push( + vscode.workspace.registerNotebookSerializer('my-notebook', new SampleSerializer()) + ); + context.subscriptions.push(new SampleController(context)); +} + +export function deactivate() { } diff --git a/samples/sample-notebook-provider/src/sampleController.ts b/samples/sample-notebook-provider/src/sampleController.ts new file mode 100644 index 0000000000..8fa060098b --- /dev/null +++ b/samples/sample-notebook-provider/src/sampleController.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as fs from 'fs/promises'; +import * as path from 'path'; + +/** + * A sample Notebook controller which handles creating a new controller and registering it with Azure Data Studio + */ +export class SampleController { + // The unique ID of the controller + readonly controllerId = 'my-notebook-controller-id'; + // The type of the notebook, must be the same as defined in the package.json contribution + readonly notebookType = 'my-notebook'; + // Label to display in the UI when choosing a Notebook provider + readonly label = 'My Notebook'; + // The languages this Notebook supports for code cells + readonly supportedLanguages = ['python']; + + private readonly _controller: vscode.NotebookController; + private _executionOrder = 0; + + constructor(private context: vscode.ExtensionContext) { + this._controller = vscode.notebooks.createNotebookController( + this.controllerId, + this.notebookType, + this.label + ); + + this._controller.supportedLanguages = this.supportedLanguages; + this._controller.supportsExecutionOrder = true; + this._controller.executeHandler = this._execute.bind(this); + } + + dispose() { } + + private async _execute( + cells: vscode.NotebookCell[], + _notebook: vscode.NotebookDocument, + _controller: vscode.NotebookController + ): Promise { + for (let cell of cells) { + await this._doExecution(cell); + } + } + + private async _doExecution(cell: vscode.NotebookCell): Promise { + // First we create an execution object for the cell and start it + const execution = this._controller.createNotebookCellExecution(cell); + execution.executionOrder = ++this._executionOrder; + execution.start(); + + // This logic can be whatever you want - typically you would use the contents of the cell and do something + // with that (such as executing a query) but you can also run whatever code you want to and send outputs + // to be displayed. + const image = await fs.readFile(path.join(this.context.extensionPath, 'images', 'computer-cat.gif')); + + // Header output that includes the original text of the cell, formatted as markdown + const outputHeader = new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(`**Running code: ${cell.document.getText()}**`, 'text/markdown'), + ]); + // Simple text output + const output = new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text('Finding the cat'), + ]); + // Initial set of messages + await execution.replaceOutput([ + outputHeader, + output + ]); + + // Show replacing the output items to incrementally update a specific output + for (let i = 1; i < 8; i++) { + await new Promise(resolve => setTimeout(resolve, 500)); + await execution.replaceOutputItems([ + vscode.NotebookCellOutputItem.text('Finding the cat' + '.'.repeat(i)) + ], output); + } + // End by replacing the text with a gif + await execution.replaceOutputItems([ + vscode.NotebookCellOutputItem.text(image.toString('base64'), 'image/gif') + ], output); + + // And finally append a new output to the existing ones + await execution.appendOutput(new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text('Cat found!')]), cell); + + // Signal execution end + execution.end(true); + } +} diff --git a/samples/sample-notebook-provider/src/sampleSerializer.ts b/samples/sample-notebook-provider/src/sampleSerializer.ts new file mode 100644 index 0000000000..bb92bc4fba --- /dev/null +++ b/samples/sample-notebook-provider/src/sampleSerializer.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +/** + * Sample hardcoded contents for the Notebook. + */ +const presetNotebookData: vscode.NotebookData = { + cells: [{ + kind: vscode.NotebookCellKind.Markup, + value: 'Sample markup cell', + languageId: 'Markup' + }, { + kind: vscode.NotebookCellKind.Code, + value: '1+1', + languageId: 'Python' + }] +}; + +const presetNotebookBytes = new TextEncoder().encode(JSON.stringify(presetNotebookData)); + +/** + * A sample Notebook serializer which handles serializing/deserializing the Notebook from/into a byte array for storage. + */ +export class SampleSerializer implements vscode.NotebookSerializer { + deserializeNotebook(content: Uint8Array, token: vscode.CancellationToken): vscode.NotebookData | Thenable { + return presetNotebookData; + } + serializeNotebook(data: vscode.NotebookData, token: vscode.CancellationToken): Uint8Array | Thenable { + return presetNotebookBytes; + } +} diff --git a/samples/sample-notebook-provider/tsconfig.json b/samples/sample-notebook-provider/tsconfig.json new file mode 100644 index 0000000000..bb0a62b5b4 --- /dev/null +++ b/samples/sample-notebook-provider/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "sourceMap": true, + "rootDir": "src", + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/samples/sample-notebook-provider/yarn.lock b/samples/sample-notebook-provider/yarn.lock new file mode 100644 index 0000000000..b8f788b178 --- /dev/null +++ b/samples/sample-notebook-provider/yarn.lock @@ -0,0 +1,25 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/azdata@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@types/azdata/-/azdata-1.34.0.tgz#8e48249e5ccb5e2b927e1176cb578822391e04e3" + integrity sha512-0vSuYYnUhHd7D68uL6/prQAdAMBEnG6i5Nu3Dt9LOFsURgW365EbDBqmGtZ96Q3x87F+DVC14hI+15J7Mtx/Yw== + dependencies: + "@types/vscode" "*" + +"@types/node@14.x": + version "14.17.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" + integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== + +"@types/vscode@*", "@types/vscode@^1.61.0": + version "1.61.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.61.0.tgz#c54335b6f84c19c69b1435b17cc0ce3b2cecfeec" + integrity sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg== + +typescript@^4.4.0-dev.20210607: + version "4.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c" + integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==