diff --git a/.github/workflows/updateNotebookApi.yml b/.github/workflows/updateNotebookApi.yml new file mode 100644 index 0000000000..b4919e0741 --- /dev/null +++ b/.github/workflows/updateNotebookApi.yml @@ -0,0 +1,101 @@ +name: "Update Notebook API" + +on: + push: + pull_request: + schedule: + - cron: 0 22 * * * + +jobs: + Update-Notebook-Api: + + runs-on: ubuntu-latest + defaults: + run: + shell: pwsh + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Setup Node.js environment + uses: actions/setup-node@v2.1.0 + + - name: Rename proposed dts to old + run: Move-Item ./vscode.proposed.d.ts ./old.vscode.proposed.d.ts + + - name: npm install + run: npm install + + - name: Get latest proposed dts + run: npm run download-api + + - name: Generate new dts and compare it with the old one + run: | + # This will contain the content of our new file + $fullFile = [System.Collections.Generic.List[string]]@() + $dts = Get-Content ./vscode.proposed.d.ts + + # First add everything up to the declare statement + $index = 0 + while ($dts[$index] -notmatch "declare module 'vscode' {") { + $fullFile += $dts[$index] + $index++ + } + + # Add the declare statement + $fullFile += $dts[$index] + + # Find the Notebook region start index + for ( $i = $index; $i -lt $dts.Length; $i++) { + if($dts[$i] -match '//#region @rebornix: Notebook') { + $index = $i + break + } + } + + # Add everything until the endregion to the new file + while ($dts[$index] -notmatch "//#endregion") { + $fullFile += $dts[$index] + $index++ + } + + # Add the endregion line and ending brace line + $fullFile += $dts[$index] + $fullFile += '}' + + # Overwrite the file with the new content + $fullFile | Set-Content ./vscode.proposed.d.ts + + # Get the old and new files' raw text + $oldFile = Get-Content ./old.vscode.proposed.d.ts -Raw + $newFile = Get-Content ./vscode.proposed.d.ts -Raw + + # Compare them and log if they are different + if($oldFile -ne $newFile) { + Write-Host "New changes detected!" + } + + # Remove the old file so it doesn't get picked up by tsc + Remove-Item ./old.vscode.proposed.d.ts -Force + + - name: Compile the TypeScript to check for errors + run: npm run compile + + - name: Create Pull Request + if: github.event_name == 'schedule' + id: cpr + uses: peter-evans/create-pull-request@v2 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + with: + commit-message: "Update Notebook dts" + committer: GitHub + author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com> + title: "Update Notebook dts" + assignees: TylerLeonhardt + reviewers: TylerLeonhardt + base: master + draft: false + branch: powershell-notebook-patch-${{ github.run_id }} + labels: Created_by_Action diff --git a/.gitignore b/.gitignore index fab6d5d7bd..a49ef7fae2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ npm-debug.log .vscode-test/ *.DS_Store test-results.xml +vscode.d.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 3d821aa8b5..2e8ab9f8a0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,8 +29,12 @@ "request": "launch", "runtimeExecutable": "${execPath}", "args": [ + "--disable-extensions", + "--enable-proposed-api", "ms-vscode.powershell-preview", "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test/testRunner.js" ], + "--extensionTestsPath=${workspaceFolder}/out/test/testRunner.js", + "${workspaceFolder}/test" + ], "outFiles": ["${workspaceFolder}/out/**/*.js"], "preLaunchTask": "Build" } diff --git a/package-lock.json b/package-lock.json index 60a479ec69..f0332b3285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -115,9 +115,9 @@ } }, "@types/node": { - "version": "12.12.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.39.tgz", - "integrity": "sha512-pADGfwnDkr6zagDwEiCVE4yQrv7XDkoeVa4OfA9Ju/zRTk6YNDLGtQbkdL4/56mCQQCs4AhNrBIag6jrp7ZuOg==", + "version": "14.0.22", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/@types/node/-/@types/node-14.0.22.tgz", + "integrity": "sha1-I+pNiBic7H1Y+ea2b3hrIV62G9w=", "dev": true }, "@types/node-fetch": { @@ -167,9 +167,9 @@ "dev": true }, "@types/vscode": { - "version": "1.43.0", - "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/@types/vscode/-/@types/vscode-1.43.0.tgz", - "integrity": "sha1-IiduYANMaTszEX8QaP+qwOiVIts=", + "version": "1.44.0", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/@types/vscode/-/@types/vscode-1.44.0.tgz", + "integrity": "sha1-Yuz+PQ44lC/OVWV02lTuEBPHdbc=", "dev": true }, "acorn": { @@ -1207,6 +1207,12 @@ "integrity": "sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==", "dev": true }, + "kleur": { + "version": "3.0.3", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha1-p5yezIbuHOP6YgbRIWxQHxR/wH4=", + "dev": true + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -1583,6 +1589,16 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "prompts": { + "version": "2.3.2", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/prompts/-/prompts-2.3.2.tgz", + "integrity": "sha1-SAVy2J7POVZtK9P+LJ/Mt8TAsGg=", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -1773,6 +1789,12 @@ } } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha1-E01oEpd1ZDfMBcoBNw06elcQde0=", + "dev": true + }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -2149,6 +2171,48 @@ } } }, + "vscode-dts": { + "version": "0.3.1", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/vscode-dts/-/vscode-dts-0.3.1.tgz", + "integrity": "sha1-BwVUp/yar16FRmoFGb8yBnMIzfQ=", + "dev": true, + "requires": { + "minimist": "^1.2.0", + "prompts": "^2.1.0", + "rimraf": "^3.0.0" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/glob/-/glob-7.1.6.tgz", + "integrity": "sha1-FB8zuBp8JJLhJVlDB0gMRmeSeKY=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha1-Z9ZgFLZqaoqqDAg8X9WN9OTpdgI=", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://botbuilder.myget.org/F/botframework-cli/npm/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, "vscode-extension-telemetry": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.6.tgz", diff --git a/package.json b/package.json index 9178125be0..c43b88cb87 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "publisher": "ms-vscode", "description": "(Preview) Develop PowerShell scripts in Visual Studio Code!", "engines": { - "vscode": "^1.43.0" + "vscode": "^1.44.0" }, "license": "SEE LICENSE IN LICENSE.txt", "homepage": "https://github.com/PowerShell/vscode-powershell/blob/master/README.md", @@ -26,6 +26,7 @@ "url": "https://github.com/PowerShell/vscode-powershell.git" }, "main": "./out/src/main", + "enableProposedApi": true, "activationEvents": [ "onDebugInitialConfigurations", "onDebugResolve:PowerShell", @@ -43,7 +44,8 @@ "onCommand:PowerShell.RegisterExternalExtension", "onCommand:PowerShell.UnregisterExternalExtension", "onCommand:PowerShell.GetPowerShellVersionDetails", - "onView:PowerShellCommands" + "onView:PowerShellCommands", + "onNotebookEditor:PowerShellNotebookMode" ], "dependencies": { "node-fetch": "^2.6.0", @@ -56,13 +58,13 @@ "@types/glob": "^7.1.2", "@types/mocha": "~7.0.2", "@types/mock-fs": "~4.10.0", - "@types/node": "~12.12.39", + "@types/node": "~14.0.22", "@types/node-fetch": "~2.5.7", "@types/rewire": "~2.5.28", "@types/semver": "~7.2.0", "@types/sinon": "~9.0.4", "@types/uuid": "^8.0.0", - "@types/vscode": "1.43.0", + "@types/vscode": "1.44.0", "mocha": "~5.2.0", "mocha-junit-reporter": "~2.0.0", "mocha-multi-reporters": "~1.1.7", @@ -72,7 +74,8 @@ "tslint": "~6.1.2", "typescript": "~3.9.3", "vsce": "~1.77.0", - "vscode-test": "~1.4.0" + "vscode-test": "~1.4.0", + "vscode-dts": "~0.3.1" }, "extensionDependencies": [ "vscode.powershell" @@ -80,7 +83,8 @@ "scripts": { "compile": "tsc -v && tsc -p ./ && tslint -p ./", "compile-watch": "tsc -watch -p ./", - "test": "node ./out/test/runTests.js" + "test": "node ./out/test/runTests.js", + "download-api": "vscode-dts dev" }, "contributes": { "breakpoints": [ @@ -106,6 +110,18 @@ } ] }, + "notebookProvider": [ + { + "viewType": "PowerShellNotebookMode", + "displayName": "Powershell Notebook", + "selector": [ + { + "filenamePattern": "*.ps1" + } + ], + "priority": "option" + } + ], "keybindings": [ { "command": "PowerShell.ShowHelp", @@ -201,6 +217,24 @@ "dark": "resources/dark/play.svg" } }, + { + "command": "PowerShell.ShowNotebookMode", + "title": "(Preview) Show Notebook Mode", + "category": "PowerShell", + "icon": { + "light": "resources/light/book.svg", + "dark": "resources/dark/book.svg" + } + }, + { + "command": "PowerShell.HideNotebookMode", + "title": "Show Text Editor", + "category": "PowerShell", + "icon": { + "light": "resources/light/file-code.svg", + "dark": "resources/dark/file-code.svg" + } + }, { "command": "PowerShell.RestartSession", "title": "Restart Current Session", @@ -376,6 +410,16 @@ "when": "editorLangId == powershell && config.powershell.buttons.showRunButtons", "command": "PowerShell.RunSelection", "group": "navigation@101" + }, + { + "when": "editorLangId == powershell && config.powershell.notebooks.showToggleButton", + "command": "PowerShell.ShowNotebookMode", + "group": "navigation@102" + }, + { + "when": "resourceLangId == powershell && notebookEditorFocused", + "command": "PowerShell.HideNotebookMode", + "group": "navigation@102" } ], "editor/title/context": [ @@ -881,6 +925,20 @@ "type": "boolean", "default": false, "description": "Show buttons in the editor titlebar for moving the panel around." + }, + "powershell.notebooks.showToggleButton": { + "type": "boolean", + "default": false, + "description": "Controls whether we show or hide the buttons to toggle Notebook mode in the top right." + }, + "powershell.notebooks.saveMarkdownCellsAs": { + "type": "string", + "enum": [ + "BlockComment", + "LineComment" + ], + "default": "BlockComment", + "description": "Controls what new markdown cells in Notebook Mode get saved as in the PowerShell file." } } }, diff --git a/resources/dark/book.svg b/resources/dark/book.svg new file mode 100644 index 0000000000..4d433d56e3 --- /dev/null +++ b/resources/dark/book.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/dark/file-code.svg b/resources/dark/file-code.svg new file mode 100644 index 0000000000..d2a7c4eab0 --- /dev/null +++ b/resources/dark/file-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/light/book.svg b/resources/light/book.svg new file mode 100644 index 0000000000..95a115b2e0 --- /dev/null +++ b/resources/light/book.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/light/file-code.svg b/resources/light/file-code.svg new file mode 100644 index 0000000000..cb42220622 --- /dev/null +++ b/resources/light/file-code.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/controls/animatedStatusBar.ts b/src/controls/animatedStatusBar.ts index dc4709a56d..a76ca302b6 100644 --- a/src/controls/animatedStatusBar.ts +++ b/src/controls/animatedStatusBar.ts @@ -7,7 +7,8 @@ import { StatusBarAlignment, StatusBarItem, ThemeColor, - window} from "vscode"; + window, + Command} from "vscode"; export function showAnimatedStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable { const animatedStatusBarItem: AnimatedStatusBarItem = new AnimatedStatusBarItem(text); @@ -58,11 +59,11 @@ class AnimatedStatusBarItem implements StatusBarItem { this.statusBarItem.color = value; } - public get command(): string { + public get command(): string | Command { return this.statusBarItem.command; } - public set command(value: string) { + public set command(value: string | Command) { this.statusBarItem.command = value; } diff --git a/src/features/HelpCompletion.ts b/src/features/HelpCompletion.ts index 97255bfc67..8395ad058c 100644 --- a/src/features/HelpCompletion.ts +++ b/src/features/HelpCompletion.ts @@ -33,7 +33,7 @@ export class HelpCompletionFeature implements IFeature { constructor(private log: Logger) { this.settings = Settings.load(); - if (this.settings.helpCompletion !== Settings.HelpCompletion.Disabled) { + if (this.settings.helpCompletion !== Settings.CommentType.Disabled) { this.helpCompletionProvider = new HelpCompletionProvider(); const subscriptions = []; workspace.onDidChangeTextDocument(this.onEvent, this, subscriptions); @@ -166,7 +166,7 @@ class HelpCompletionProvider { const result = await this.langClient.sendRequest(CommentHelpRequestType, { documentUri: doc.uri.toString(), triggerPosition: triggerStartPos, - blockComment: this.settings.helpCompletion === Settings.HelpCompletion.BlockComment, + blockComment: this.settings.helpCompletion === Settings.CommentType.BlockComment, }); if (!(result && result.content)) { diff --git a/src/features/PowerShellNotebooks.ts b/src/features/PowerShellNotebooks.ts new file mode 100644 index 0000000000..36fc7281cb --- /dev/null +++ b/src/features/PowerShellNotebooks.ts @@ -0,0 +1,253 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as vscode from "vscode"; +import { CommentType } from "../settings"; +import { IFeature, LanguageClient } from "../feature"; +import { EvaluateRequestType } from "./Console"; +import Settings = require("../settings"); +import { ILogger } from "../logging"; + +export class PowerShellNotebooksFeature implements vscode.NotebookContentProvider, vscode.NotebookKernel, IFeature { + + private readonly showNotebookModeCommand: vscode.Disposable; + private readonly hideNotebookModeCommand: vscode.Disposable; + private languageClient: LanguageClient; + + private _onDidChangeNotebook = new vscode.EventEmitter(); + public onDidChangeNotebook: vscode.Event = this._onDidChangeNotebook.event; + public kernel?: vscode.NotebookKernel; + + public label: string = "PowerShell"; + public preloads?: vscode.Uri[]; + + public constructor(private logger: ILogger, skipRegisteringCommands?: boolean) { + // VS Code Notebook API uses this property for handling cell execution. + this.kernel = this; + + if(!skipRegisteringCommands) { + this.showNotebookModeCommand = vscode.commands.registerCommand( + "PowerShell.ShowNotebookMode", + PowerShellNotebooksFeature.showNotebookMode); + + this.hideNotebookModeCommand = vscode.commands.registerCommand( + "PowerShell.HideNotebookMode", + PowerShellNotebooksFeature.hideNotebookMode); + } + } + + public async openNotebook(uri: vscode.Uri, context: vscode.NotebookDocumentOpenContext): Promise { + // load from backup if needed. + const actualUri = context.backupId ? vscode.Uri.parse(context.backupId) : uri; + this.logger.writeDiagnostic(`Opening Notebook: ${uri.toString()}`); + + const data = (await vscode.workspace.fs.readFile(actualUri)).toString(); + const lines = data.split(/\r\n|\r|\n/g); + + const notebookData: vscode.NotebookData = { + languages: ["powershell"], + cells: [], + metadata: {} + }; + + let currentCellSource: string[] = []; + let cellKind: vscode.CellKind | undefined; + let insideBlockComment: boolean = false; + + // Iterate through all lines in a document (aka ps1 file) and group the lines + // into cells (markdown or code) that will be rendered in Notebook mode. + // tslint:disable-next-line: prefer-for-of + for (let i = 0; i < lines.length; i++) { + // Handle block comments + if (insideBlockComment) { + if (lines[i] === "#>") { + // We've reached the end of a block comment, + // push a markdown cell. + insideBlockComment = false; + + notebookData.cells.push({ + cellKind: vscode.CellKind.Markdown, + language: "markdown", + outputs: [], + source: currentCellSource.join("\n"), + metadata: { + custom: { + commentType: CommentType.BlockComment + } + } + }); + + currentCellSource = []; + cellKind = null; + continue; + } + + // If we're still in a block comment, push the line and continue. + currentCellSource.push(lines[i]); + continue; + } else if (lines[i] === "<#") { + // If we found the start of a block comment, + // insert what we saw leading up to this. + // If cellKind is null/undefined, that means we + // are starting the file with a BlockComment. + if (cellKind) { + notebookData.cells.push({ + cellKind, + language: cellKind === vscode.CellKind.Markdown ? "markdown" : "powershell", + outputs: [], + source: currentCellSource.join("\n"), + metadata: { + custom: { + commentType: cellKind === vscode.CellKind.Markdown ? CommentType.LineComment : CommentType.Disabled, + } + } + }); + } + + // reset state because we're starting a new Markdown cell. + currentCellSource = []; + cellKind = vscode.CellKind.Markdown; + insideBlockComment = true; + continue; + } + + // Handle everything else (regular comments and code) + // If a line starts with # it's a comment + const kind: vscode.CellKind = lines[i].startsWith("#") ? vscode.CellKind.Markdown : vscode.CellKind.Code; + + // If this line is a continuation of the previous cell type, then add this line to the current cell source. + if (kind === cellKind) { + currentCellSource.push(kind === vscode.CellKind.Markdown && !insideBlockComment ? lines[i].replace(/^\#\s*/, "") : lines[i]); + } else { + // If cellKind has a value, then we can add the cell we've just computed. + if (cellKind) { + notebookData.cells.push({ + cellKind: cellKind!, + language: cellKind === vscode.CellKind.Markdown ? "markdown" : "powershell", + outputs: [], + source: currentCellSource.join("\n"), + metadata: { + custom: { + commentType: cellKind === vscode.CellKind.Markdown ? CommentType.LineComment : CommentType.Disabled, + } + } + }); + } + + // set initial new cell state + currentCellSource = []; + cellKind = kind; + currentCellSource.push(kind === vscode.CellKind.Markdown ? lines[i].replace(/^\#\s*/, "") : lines[i]); + } + } + + // If we have some leftover lines that have not been added (for example, + // when there is only the _start_ of a block comment but not an _end_.) + // add the appropriate cell. + if (currentCellSource.length) { + notebookData.cells.push({ + cellKind: cellKind!, + language: cellKind === vscode.CellKind.Markdown ? "markdown" : "powershell", + outputs: [], + source: currentCellSource.join("\n"), + metadata: { + custom: { + commentType: cellKind === vscode.CellKind.Markdown ? CommentType.LineComment : CommentType.Disabled, + } + } + }); + } + + return notebookData; + } + + public resolveNotebook(document: vscode.NotebookDocument, webview: { readonly onDidReceiveMessage: vscode.Event; postMessage(message: any): Thenable; asWebviewUri(localResource: vscode.Uri): vscode.Uri; }): Promise { + // We don't need to do anything here because our Notebooks are backed by files. + return; + } + + public saveNotebook(document: vscode.NotebookDocument, cancellation: vscode.CancellationToken): Promise { + return this._save(document, document.uri, cancellation); + } + + public saveNotebookAs(targetResource: vscode.Uri, document: vscode.NotebookDocument, cancellation: vscode.CancellationToken): Promise { + return this._save(document, targetResource, cancellation); + } + + public async backupNotebook(document: vscode.NotebookDocument, context: vscode.NotebookDocumentBackupContext, cancellation: vscode.CancellationToken): Promise { + await this._save(document, context.destination, cancellation); + + return { + id: context.destination.toString(), + delete: () => { + vscode.workspace.fs.delete(context.destination); + } + }; + } + + public dispose() { + this.showNotebookModeCommand.dispose(); + this.hideNotebookModeCommand.dispose(); + } + + public setLanguageClient(languageClient: LanguageClient) { + this.languageClient = languageClient; + } + + private async _save(document: vscode.NotebookDocument, targetResource: vscode.Uri, _token: vscode.CancellationToken): Promise { + this.logger.writeDiagnostic(`Saving Notebook: ${targetResource.toString()}`); + + const retArr: string[] = []; + for (const cell of document.cells) { + if (cell.cellKind === vscode.CellKind.Code) { + retArr.push(...cell.document.getText().split(/\r|\n|\r\n/)); + } else { + // First honor the comment type of the cell if it already has one. + // If not, use the user setting. + const commentKind = cell.metadata.custom?.commentType || Settings.load().notebooks.saveMarkdownCellsAs; + + if (commentKind === CommentType.BlockComment) { + retArr.push("<#"); + retArr.push(...cell.document.getText().split(/\r|\n|\r\n/)); + retArr.push("#>"); + } else { + retArr.push(...cell.document.getText().split(/\r|\n|\r\n/).map((line) => `# ${line}`)); + } + } + } + + await vscode.workspace.fs.writeFile(targetResource, new TextEncoder().encode(retArr.join("\n"))); + } + + private static async showNotebookMode() { + const uri = vscode.window.activeTextEditor.document.uri; + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await vscode.commands.executeCommand("vscode.openWith", uri, "PowerShellNotebookMode"); + } + + private static async hideNotebookMode() { + const uri = vscode.notebook.activeNotebookEditor.document.uri; + await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await vscode.commands.executeCommand("vscode.openWith", uri, "default"); + } + + /* + `vscode.NotebookKernel` implementations + */ + public async executeAllCells(document: vscode.NotebookDocument, token: vscode.CancellationToken): Promise { + for (const cell of document.cells) { + await this.executeCell(document, cell, token); + } + } + + public async executeCell(document: vscode.NotebookDocument, cell: vscode.NotebookCell | undefined, token: vscode.CancellationToken): Promise { + if (token.isCancellationRequested) { + return; + } + + await this.languageClient.sendRequest(EvaluateRequestType, { + expression: cell.document.getText(), + }); + } +} diff --git a/src/main.ts b/src/main.ts index b8c681973b..d4da95a233 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,6 +34,8 @@ import { Logger, LogLevel } from "./logging"; import { SessionManager } from "./session"; import Settings = require("./settings"); import { PowerShellLanguageId } from "./utils"; +import utils = require("./utils"); +import { PowerShellNotebooksFeature } from "./features/PowerShellNotebooks"; // The most reliable way to get the name and version of the current extension. // tslint:disable-next-line: no-var-requires @@ -159,6 +161,20 @@ export function activate(context: vscode.ExtensionContext): void { new ExternalApiFeature(sessionManager, logger) ]; + // Notebook UI is only supported in VS Code Insiders. + if(vscode.env.uriScheme === "vscode-insiders") { + const powerShellNotebooksFeature = new PowerShellNotebooksFeature(logger); + + try { + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider("PowerShellNotebookMode", powerShellNotebooksFeature)); + extensionFeatures.push(powerShellNotebooksFeature); + } catch (e) { + // This would happen if VS Code changes their API. + powerShellNotebooksFeature.dispose(); + logger.writeVerbose("Failed to register NotebookContentProvider", e); + } + } + sessionManager.setExtensionFeatures(extensionFeatures); if (extensionSettings.startAutomatically) { diff --git a/src/settings.ts b/src/settings.ts index 9cd5883084..6271eb1274 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -21,7 +21,7 @@ enum PipelineIndentationStyle { None, } -export enum HelpCompletion { +export enum CommentType { Disabled = "Disabled", BlockComment = "BlockComment", LineComment = "LineComment", @@ -102,6 +102,7 @@ export interface ISettings { pester?: IPesterSettings; buttons?: IButtonSettings; cwd?: string; + notebooks?: INotebooksSettings; } export interface IStartAsLoginShellSettings { @@ -132,6 +133,10 @@ export interface IButtonSettings { showPanelMovementButtons?: boolean; } +export interface INotebooksSettings { + saveMarkdownCellsAs?: CommentType; +} + export function load(): ISettings { const configuration: vscode.WorkspaceConfiguration = vscode.workspace.getConfiguration( @@ -210,6 +215,10 @@ export function load(): ISettings { debugOutputVerbosity: "Diagnostic", }; + const defaultNotebooksSettings: INotebooksSettings = { + saveMarkdownCellsAs: CommentType.BlockComment, + }; + return { startAutomatically: configuration.get("startAutomatically", true), @@ -230,7 +239,7 @@ export function load(): ISettings { enableProfileLoading: configuration.get("enableProfileLoading", false), helpCompletion: - configuration.get("helpCompletion", HelpCompletion.BlockComment), + configuration.get("helpCompletion", CommentType.BlockComment), scriptAnalysis: configuration.get("scriptAnalysis", defaultScriptAnalysisSettings), debugging: @@ -251,6 +260,8 @@ export function load(): ISettings { configuration.get("pester", defaultPesterSettings), buttons: configuration.get("buttons", defaultButtonSettings), + notebooks: + configuration.get("notebooks", defaultNotebooksSettings), startAsLoginShell: // tslint:disable-next-line // We follow the same convention as VS Code - https://github.com/microsoft/vscode/blob/ff00badd955d6cfcb8eab5f25f3edc86b762f49f/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts#L105-L107 diff --git a/test/features/PowerShellNotebooks.test.ts b/test/features/PowerShellNotebooks.test.ts new file mode 100644 index 0000000000..800b97fb39 --- /dev/null +++ b/test/features/PowerShellNotebooks.test.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +import * as assert from "assert"; +import * as path from "path"; +import * as vscode from "vscode"; +import { PowerShellNotebooksFeature } from "../../src/features/PowerShellNotebooks"; +import os = require("os"); +import { readFileSync } from "fs"; +import { CommentType } from "../../src/settings"; +import * as utils from "../../src/utils"; +import { MockLogger } from "../test_utils"; + +const notebookDir = [ + __dirname, + "..", + "..", + "..", + "test", + "features", + "testNotebookFiles" +]; + +const notebookOnlyCode = vscode.Uri.file( + path.join(...notebookDir, "onlyCode.ps1")); +const notebookOnlyMarkdown = vscode.Uri.file( + path.join(...notebookDir,"onlyMarkdown.ps1")); +const notebookSimpleBlockComments = vscode.Uri.file( + path.join(...notebookDir,"simpleBlockComments.ps1")); +const notebookSimpleLineComments = vscode.Uri.file( + path.join(...notebookDir,"simpleLineComments.ps1")); +const notebookSimpleMixedComments = vscode.Uri.file( + path.join(...notebookDir,"simpleMixedComments.ps1")); + +const notebookTestData = new Map(); + +function readBackingFile(uri: vscode.Uri): string { + return readFileSync(uri.fsPath).toString(); +} + +function compareCells(actualCells: vscode.NotebookCellData[], expectedCells: vscode.NotebookCellData[]) : void { + assert.deepStrictEqual(actualCells.length, expectedCells.length); + + // Compare cell metadata + for (let i = 0; i < actualCells.length; i++) { + assert.deepStrictEqual( + actualCells[i].metadata.custom, + expectedCells[i].metadata.custom + ); + } +} + +suite("PowerShellNotebooks tests", () => { + notebookTestData.set(notebookOnlyCode, [ + { + cellKind: vscode.CellKind.Code, + language: "powershell", + source: readBackingFile(notebookOnlyCode), + outputs: [], + metadata: { + custom: { + commentType: CommentType.Disabled, + } + } + } + ]); + + notebookTestData.set(notebookOnlyMarkdown, [ + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: readBackingFile(notebookOnlyMarkdown), + outputs: [], + metadata: { + custom: { + commentType: CommentType.LineComment, + } + } + } + ]); + + let content = readBackingFile(notebookSimpleBlockComments).split(os.EOL); + notebookTestData.set(notebookSimpleBlockComments, [ + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(0, 5).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.BlockComment, + } + } + }, + { + cellKind: vscode.CellKind.Code, + language: "powershell", + source: content.slice(5, 6).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.Disabled, + } + } + }, + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(6, 11).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.BlockComment, + } + } + }, + ]); + + content = readBackingFile(notebookSimpleLineComments).split(os.EOL); + notebookTestData.set(notebookSimpleLineComments, [ + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(0, 3).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.LineComment, + } + } + }, + { + cellKind: vscode.CellKind.Code, + language: "powershell", + source: content.slice(3, 4).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.Disabled, + } + } + }, + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(4, 7).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.LineComment, + } + } + }, + ]); + + content = readBackingFile(notebookSimpleMixedComments).split(os.EOL); + notebookTestData.set(notebookSimpleMixedComments, [ + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(0, 3).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.LineComment, + } + } + }, + { + cellKind: vscode.CellKind.Code, + language: "powershell", + source: content.slice(3, 4).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.Disabled, + } + } + }, + { + cellKind: vscode.CellKind.Markdown, + language: "markdown", + source: content.slice(4, 9).join(os.EOL), + outputs: [], + metadata: { + custom: { + commentType: CommentType.BlockComment, + } + } + }, + ]); + + const feature = new PowerShellNotebooksFeature(new MockLogger(), true); + + for (const [uri, expectedCells] of notebookTestData) { + test(`Can open a notebook with expected cells - ${uri.fsPath}`, async () => { + const actualNotebookData = await feature.openNotebook(uri, {}); + compareCells(actualNotebookData.cells, expectedCells); + }); + } + + test("Can save a new notebook with expected cells and metadata", async () => { + const uri = vscode.Uri.file(path.join(__dirname, "testFile.ps1")); + try { + await vscode.workspace.fs.delete(uri); + } catch { + // If the file doesn't exist that's fine. + } + + // Open an existing notebook ps1. + await vscode.commands.executeCommand("vscode.openWith", notebookSimpleMixedComments, "PowerShellNotebookMode"); + + // Allow some time to pass to render the Notebook + await utils.sleep(5000); + assert.strictEqual( + vscode.notebook.activeNotebookEditor.document.uri.toString(), + notebookSimpleMixedComments.toString()); + + // Save it as testFile.ps1 and reopen it using the feature. + await feature.saveNotebookAs(uri, vscode.notebook.activeNotebookEditor.document, null); + const newNotebook = await feature.openNotebook(uri, {}); + + // Compare that saving as a file results in the same cell data as the existing one. + const expectedCells = notebookTestData.get(notebookSimpleMixedComments); + compareCells(newNotebook.cells, expectedCells); + }).timeout(20000); +}); diff --git a/test/features/testNotebookFiles/onlyCode.ps1 b/test/features/testNotebookFiles/onlyCode.ps1 new file mode 100644 index 0000000000..916d38265f --- /dev/null +++ b/test/features/testNotebookFiles/onlyCode.ps1 @@ -0,0 +1 @@ +Get-ChildItem diff --git a/test/features/testNotebookFiles/onlyMarkdown.ps1 b/test/features/testNotebookFiles/onlyMarkdown.ps1 new file mode 100644 index 0000000000..fa2d22dd81 --- /dev/null +++ b/test/features/testNotebookFiles/onlyMarkdown.ps1 @@ -0,0 +1,3 @@ +# # h1 +# **bold** +# text \ No newline at end of file diff --git a/test/features/testNotebookFiles/simpleBlockComments.ps1 b/test/features/testNotebookFiles/simpleBlockComments.ps1 new file mode 100644 index 0000000000..5183f4e135 --- /dev/null +++ b/test/features/testNotebookFiles/simpleBlockComments.ps1 @@ -0,0 +1,11 @@ +<# +Foo +bar +baz +#> +Get-ChildItem +<# +Foo +bar +baz +#> \ No newline at end of file diff --git a/test/features/testNotebookFiles/simpleLineComments.ps1 b/test/features/testNotebookFiles/simpleLineComments.ps1 new file mode 100644 index 0000000000..39943e3e04 --- /dev/null +++ b/test/features/testNotebookFiles/simpleLineComments.ps1 @@ -0,0 +1,7 @@ +# Foo +# bar +# baz +Get-ChildItem +# Foo +# bar +# baz \ No newline at end of file diff --git a/test/features/testNotebookFiles/simpleMixedComments.ps1 b/test/features/testNotebookFiles/simpleMixedComments.ps1 new file mode 100644 index 0000000000..48a855d9bc --- /dev/null +++ b/test/features/testNotebookFiles/simpleMixedComments.ps1 @@ -0,0 +1,9 @@ +# Foo +# bar +# baz +Get-ChildItem +<# +Foo +bar +baz +#> \ No newline at end of file diff --git a/test/runTests.ts b/test/runTests.ts index 34d5f724bd..283ba84f46 100644 --- a/test/runTests.ts +++ b/test/runTests.ts @@ -17,7 +17,16 @@ async function main() { const extensionTestsPath = path.resolve(__dirname, "./testRunner"); // Download VS Code, unzip it and run the integration test from the local directory. - await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: ["."] }); + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [ + "--disable-extensions", + "--enable-proposed-api", "ms-vscode.powershell-preview", + "./test" + ], + version: "insiders" + }); } catch (err) { // tslint:disable-next-line:no-console console.error(err); diff --git a/tsconfig.json b/tsconfig.json index eeb3c3222e..c2fa369ef5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,10 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": [ + "es2017", + "DOM" + ], "sourceMap": true }, "exclude": [ diff --git a/vscode.proposed.d.ts b/vscode.proposed.d.ts new file mode 100644 index 0000000000..84b8b36b9c --- /dev/null +++ b/vscode.proposed.d.ts @@ -0,0 +1,541 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This is the place for API experiments and proposals. + * These API are NOT stable and subject to change. They are only available in the Insiders + * distribution and CANNOT be used in published extensions. + * + * To test these API in local environment: + * - Use Insiders release of VS Code. + * - Add `"enableProposedApi": true` to your package.json. + * - Copy this file to your project. + */ + +declare module 'vscode' { + //#region @rebornix: Notebook + + export enum CellKind { + Markdown = 1, + Code = 2 + } + + export enum CellOutputKind { + Text = 1, + Error = 2, + Rich = 3 + } + + export interface CellStreamOutput { + outputKind: CellOutputKind.Text; + text: string; + } + + export interface CellErrorOutput { + outputKind: CellOutputKind.Error; + /** + * Exception Name + */ + ename: string; + /** + * Exception Value + */ + evalue: string; + /** + * Exception call stack + */ + traceback: string[]; + } + + export interface NotebookCellOutputMetadata { + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; + } + + export interface CellDisplayOutput { + outputKind: CellOutputKind.Rich; + /** + * { mime_type: value } + * + * Example: + * ```json + * { + * "outputKind": vscode.CellOutputKind.Rich, + * "data": { + * "text/html": [ + * "

Hello

" + * ], + * "text/plain": [ + * "" + * ] + * } + * } + */ + data: { [key: string]: any; }; + + readonly metadata?: NotebookCellOutputMetadata; + } + + export type CellOutput = CellStreamOutput | CellErrorOutput | CellDisplayOutput; + + export enum NotebookCellRunState { + Running = 1, + Idle = 2, + Success = 3, + Error = 4 + } + + export interface NotebookCellMetadata { + /** + * Controls if the content of a cell is editable or not. + */ + editable?: boolean; + + /** + * Controls if the cell is executable. + * This metadata is ignored for markdown cell. + */ + runnable?: boolean; + + /** + * Controls if the cell has a margin to support the breakpoint UI. + * This metadata is ignored for markdown cell. + */ + breakpointMargin?: boolean; + + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; + + /** + * The order in which this cell was executed. + */ + executionOrder?: number; + + /** + * A status message to be shown in the cell's status bar + */ + statusMessage?: string; + + /** + * The cell's current run state + */ + runState?: NotebookCellRunState; + + /** + * If the cell is running, the time at which the cell started running + */ + runStartTime?: number; + + /** + * The total duration of the cell's last run + */ + lastRunDuration?: number; + + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; + } + + export interface NotebookCell { + readonly notebook: NotebookDocument; + readonly uri: Uri; + readonly cellKind: CellKind; + readonly document: TextDocument; + language: string; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; + } + + export interface NotebookDocumentMetadata { + /** + * Controls if users can add or delete cells + * Defaults to true + */ + editable?: boolean; + + /** + * Controls whether the full notebook can be run at once. + * Defaults to true + */ + runnable?: boolean; + + /** + * Default value for [cell editable metadata](#NotebookCellMetadata.editable). + * Defaults to true. + */ + cellEditable?: boolean; + + /** + * Default value for [cell runnable metadata](#NotebookCellMetadata.runnable). + * Defaults to true. + */ + cellRunnable?: boolean; + + /** + * Default value for [cell hasExecutionOrder metadata](#NotebookCellMetadata.hasExecutionOrder). + * Defaults to true. + */ + cellHasExecutionOrder?: boolean; + + displayOrder?: GlobPattern[]; + + /** + * Additional attributes of the document metadata. + */ + custom?: { [key: string]: any }; + } + + export interface NotebookDocument { + readonly uri: Uri; + readonly fileName: string; + readonly viewType: string; + readonly isDirty: boolean; + readonly cells: NotebookCell[]; + languages: string[]; + displayOrder?: GlobPattern[]; + metadata: NotebookDocumentMetadata; + } + + export interface NotebookConcatTextDocument { + isClosed: boolean; + dispose(): void; + onDidChange: Event; + version: number; + getText(): string; + getText(range: Range): string; + + offsetAt(position: Position): number; + positionAt(offset: number): Position; + validateRange(range: Range): Range; + validatePosition(position: Position): Position; + + locationAt(positionOrRange: Position | Range): Location; + positionAt(location: Location): Position; + contains(uri: Uri): boolean + } + + export interface NotebookEditorCellEdit { + insert(index: number, content: string | string[], language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): void; + delete(index: number): void; + } + + export interface NotebookEditor { + /** + * The document associated with this notebook editor. + */ + readonly document: NotebookDocument; + + /** + * The primary selected cell on this notebook editor. + */ + readonly selection?: NotebookCell; + + /** + * The column in which this editor shows. + */ + viewColumn?: ViewColumn; + + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel is disposed. + */ + readonly onDidDispose: Event; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + + edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; + } + + export interface NotebookOutputSelector { + mimeTypes?: string[]; + } + + export interface NotebookRenderRequest { + output: CellDisplayOutput; + mimeType: string; + outputId: string; + } + + export interface NotebookOutputRenderer { + /** + * + * @returns HTML fragment. We can probably return `CellOutput` instead of string ? + * + */ + render(document: NotebookDocument, request: NotebookRenderRequest): string; + + /** + * Call before HTML from the renderer is executed, and will be called for + * every editor associated with notebook documents where the renderer + * is or was used. + * + * The communication object will only send and receive messages to the + * render API, retrieved via `acquireNotebookRendererApi`, acquired with + * this specific renderer's ID. + * + * If you need to keep an association between the communication object + * and the document for use in the `render()` method, you can use a WeakMap. + */ + resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; + + readonly preloads?: Uri[]; + } + + export interface NotebookCellsChangeData { + readonly start: number; + readonly deletedCount: number; + readonly deletedItems: NotebookCell[]; + readonly items: NotebookCell[]; + } + + export interface NotebookCellsChangeEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly changes: ReadonlyArray; + } + + export interface NotebookCellMoveEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly index: number; + readonly newIndex: number; + } + + export interface NotebookCellOutputsChangeEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cells: NotebookCell[]; + } + + export interface NotebookCellLanguageChangeEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly language: string; + } + + export interface NotebookCellData { + readonly cellKind: CellKind; + readonly source: string; + language: string; + outputs: CellOutput[]; + metadata: NotebookCellMetadata; + } + + export interface NotebookData { + readonly cells: NotebookCellData[]; + readonly languages: string[]; + readonly metadata: NotebookDocumentMetadata; + } + + interface NotebookDocumentContentChangeEvent { + + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + } + + interface NotebookDocumentEditEvent { + + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable | void; + + /** + * Display name describing the edit. + * + * This will be shown to users in the UI for undo/redo operations. + */ + readonly label?: string; + } + + interface NotebookDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openCustomDocument` when opening a custom editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; + } + + interface NotebookDocumentBackupContext { + readonly destination: Uri; + } + + interface NotebookDocumentOpenContext { + readonly backupId?: string; + } + + /** + * Communication object passed to the {@link NotebookContentProvider} and + * {@link NotebookOutputRenderer} to communicate with the webview. + */ + export interface NotebookCommunication { + /** + * ID of the editor this object communicates with. A single notebook + * document can have multiple attached webviews and editors, when the + * notebook is split for instance. The editor ID lets you differentiate + * between them. + */ + readonly editorId: string; + + /** + * Fired when the output hosting webview posts a message. + */ + readonly onDidReceiveMessage: Event; + /** + * Post a message to the output hosting webview. + * + * Messages are only delivered if the editor is live. + * + * @param message Body of the message. This must be a string or other json serilizable object. + */ + postMessage(message: any): Thenable; + + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + } + + export interface NotebookContentProvider { + /** + * Content providers should always use [file system providers](#FileSystemProvider) to + * resolve the raw content for `uri` as the resouce is not necessarily a file on disk. + */ + openNotebook(uri: Uri, openContext: NotebookDocumentOpenContext): NotebookData | Promise; + resolveNotebook(document: NotebookDocument, webview: NotebookCommunication): Promise; + saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; + saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; + readonly onDidChangeNotebook: Event; + backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; + + kernel?: NotebookKernel; + } + + export interface NotebookKernel { + label: string; + preloads?: Uri[]; + executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise; + executeAllCells(document: NotebookDocument, token: CancellationToken): Promise; + } + + export namespace notebook { + export function registerNotebookContentProvider( + notebookType: string, + provider: NotebookContentProvider + ): Disposable; + + export function registerNotebookKernel( + id: string, + selectors: GlobPattern[], + kernel: NotebookKernel + ): Disposable; + + export function registerNotebookOutputRenderer( + id: string, + outputSelector: NotebookOutputSelector, + renderer: NotebookOutputRenderer + ): Disposable; + + export const onDidOpenNotebookDocument: Event; + export const onDidCloseNotebookDocument: Event; + + /** + * All currently known notebook documents. + */ + export const notebookDocuments: ReadonlyArray; + + export let visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event; + + export let activeNotebookEditor: NotebookEditor | undefined; + export const onDidChangeActiveNotebookEditor: Event; + export const onDidChangeNotebookCells: Event; + export const onDidChangeCellOutputs: Event; + export const onDidChangeCellLanguage: Event; + /** + * Create a document that is the concatenation of all notebook cells. By default all code-cells are included + * but a selector can be provided to narrow to down the set of cells. + * + * @param notebook + * @param selector + */ + export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; + } + + //#endregion +}