From f53119c2a6f1d6f0e02fca3c703e73445c916cac Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Thu, 23 Feb 2023 16:22:46 -0800 Subject: [PATCH] Enable VS Code notebooks with a built-in SQL kernel. (#21995) --- build/filters.js | 2 + build/gulpfile.extensions.js | 1 - build/lib/extensions.js | 2 +- build/lib/extensions.ts | 3 +- build/lib/locFunc.js | 1 + build/lib/locFunc.ts | 1 + build/npm/dirs.js | 2 + extensions/ipynb/.gitignore | 4 + extensions/ipynb/.vscodeignore | 9 + extensions/ipynb/README.md | 9 + .../ipynb/extension-browser.webpack.config.js | 22 + extensions/ipynb/extension.webpack.config.js | 20 + extensions/ipynb/package.json | 90 ++ extensions/ipynb/package.nls.json | 4 + extensions/ipynb/src/cellIdService.ts | 77 ++ extensions/ipynb/src/common.ts | 62 + extensions/ipynb/src/constants.ts | 6 + extensions/ipynb/src/deserializers.ts | 359 ++++++ extensions/ipynb/src/ipynbMain.ts | 116 ++ extensions/ipynb/src/notebookSerializer.ts | 108 ++ extensions/ipynb/src/serializers.ts | 421 +++++++ extensions/ipynb/src/test/index.ts | 40 + extensions/ipynb/src/test/serializers.test.ts | 637 ++++++++++ extensions/ipynb/src/types.d.ts | 6 + extensions/ipynb/tsconfig.json | 15 + extensions/ipynb/yarn.lock | 35 + extensions/mssql/package.json | 9 + extensions/mssql/package.nls.json | 1 + extensions/mssql/src/main.ts | 4 + .../src/sqlNotebook/sqlNotebookController.ts | 329 +++++ extensions/mssql/src/typings/refs.d.ts | 1 + extensions/notebook-renderers/.gitignore | 1 + extensions/notebook-renderers/.vscodeignore | 6 + extensions/notebook-renderers/README.md | 9 + extensions/notebook-renderers/esbuild.js | 44 + extensions/notebook-renderers/package.json | 57 + .../notebook-renderers/package.nls.json | 4 + extensions/notebook-renderers/src/ansi.ts | 447 +++++++ extensions/notebook-renderers/src/color.ts | 1055 +++++++++++++++++ extensions/notebook-renderers/src/colorMap.ts | 62 + extensions/notebook-renderers/src/index.ts | 281 +++++ extensions/notebook-renderers/src/linkify.ts | 181 +++ .../notebook-renderers/src/textHelper.ts | 51 + extensions/notebook-renderers/tsconfig.json | 15 + extensions/notebook-renderers/yarn.lock | 8 + scripts/test-integration.bat | 6 +- scripts/test-integration.sh | 3 +- src/sql/base/common/locConstants.ts | 25 + .../api/common/sqlExtHost.protocol.ts | 2 +- .../workbench/browser/actions.contribution.ts | 13 + src/sql/workbench/common/constants.ts | 1 + .../notebook/browser/notebookActions.ts | 25 +- .../api/browser/mainThreadNotebook.ts | 10 +- .../mainThreadNotebookDocumentsAndEditors.ts | 4 +- .../api/browser/mainThreadNotebookKernels.ts | 6 +- .../browser/mainThreadNotebookProxyKernels.ts | 6 +- .../browser/mainThreadNotebookRenderers.ts | 6 +- .../workbench/api/common/extHost.api.impl.ts | 126 +- .../breakpoints/notebookBreakpoints.ts | 3 +- .../notebook/browser/notebook.contribution.ts | 321 +++-- .../notebook/browser/notebookServiceImpl.ts | 77 +- .../extensions/common/extensionHostManager.ts | 7 - test/automation/src/code.ts | 6 - test/automation/src/electron.ts | 9 - test/automation/src/notebook.ts | 5 +- test/automation/src/workbench.ts | 2 +- 66 files changed, 4962 insertions(+), 318 deletions(-) create mode 100644 extensions/ipynb/.gitignore create mode 100644 extensions/ipynb/.vscodeignore create mode 100644 extensions/ipynb/README.md create mode 100644 extensions/ipynb/extension-browser.webpack.config.js create mode 100644 extensions/ipynb/extension.webpack.config.js create mode 100644 extensions/ipynb/package.json create mode 100644 extensions/ipynb/package.nls.json create mode 100644 extensions/ipynb/src/cellIdService.ts create mode 100644 extensions/ipynb/src/common.ts create mode 100644 extensions/ipynb/src/constants.ts create mode 100644 extensions/ipynb/src/deserializers.ts create mode 100644 extensions/ipynb/src/ipynbMain.ts create mode 100644 extensions/ipynb/src/notebookSerializer.ts create mode 100644 extensions/ipynb/src/serializers.ts create mode 100644 extensions/ipynb/src/test/index.ts create mode 100644 extensions/ipynb/src/test/serializers.test.ts create mode 100644 extensions/ipynb/src/types.d.ts create mode 100644 extensions/ipynb/tsconfig.json create mode 100644 extensions/ipynb/yarn.lock create mode 100644 extensions/mssql/src/sqlNotebook/sqlNotebookController.ts create mode 100644 extensions/notebook-renderers/.gitignore create mode 100644 extensions/notebook-renderers/.vscodeignore create mode 100644 extensions/notebook-renderers/README.md create mode 100644 extensions/notebook-renderers/esbuild.js create mode 100644 extensions/notebook-renderers/package.json create mode 100644 extensions/notebook-renderers/package.nls.json create mode 100644 extensions/notebook-renderers/src/ansi.ts create mode 100644 extensions/notebook-renderers/src/color.ts create mode 100644 extensions/notebook-renderers/src/colorMap.ts create mode 100644 extensions/notebook-renderers/src/index.ts create mode 100644 extensions/notebook-renderers/src/linkify.ts create mode 100644 extensions/notebook-renderers/src/textHelper.ts create mode 100644 extensions/notebook-renderers/tsconfig.json create mode 100644 extensions/notebook-renderers/yarn.lock diff --git a/build/filters.js b/build/filters.js index 2d3a6cca20..6573eb4678 100644 --- a/build/filters.js +++ b/build/filters.js @@ -47,6 +47,7 @@ module.exports.unicodeFilter = [ '!build/win32/**', '!extensions/markdown-language-features/notebook-out/*.js', '!extensions/markdown-math/notebook-out/**', + '!extensions/notebook-renderers/renderer-out/**', '!extensions/php-language-features/src/features/phpGlobalFunctions.ts', '!extensions/typescript-language-features/test-workspace/**', '!extensions/vscode-api-tests/testWorkspace/**', @@ -130,6 +131,7 @@ module.exports.indentationFilter = [ '!extensions/markdown-language-features/media/*.js', '!extensions/markdown-language-features/notebook-out/*.js', '!extensions/markdown-math/notebook-out/*.js', + '!extensions/notebook-renderers/renderer-out/*.js', '!extensions/simple-browser/media/*.js', // {{SQL CARBON EDIT}} Except for our stuff diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index dacb0916c8..fe6272b7a9 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -69,7 +69,6 @@ const compilations = [ 'vscode-api-tests/tsconfig.json', 'vscode-colorize-tests/tsconfig.json', 'vscode-custom-editor-tests/tsconfig.json', - 'vscode-notebook-tests/tsconfig.json', 'vscode-test-resolver/tsconfig.json' ]; */ diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 98f96e429d..fa8db80a5a 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -474,7 +474,7 @@ const esbuildMediaScripts = [ 'markdown-language-features/esbuild-notebook.js', 'markdown-language-features/esbuild-preview.js', 'markdown-math/esbuild.js', - // 'notebook-renderers/esbuild.js', {{SQL CARBON EDIT}} We don't have this extension + 'notebook-renderers/esbuild.js', 'simple-browser/esbuild-preview.js', ]; async function webpackExtensions(taskName, isWatch, webpackConfigLocations) { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 71728fe3e8..50092a8576 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -285,7 +285,6 @@ const excludedExtensions = [ 'vscode-test-resolver', 'ms-vscode.node-debug', 'ms-vscode.node-debug2', - 'vscode-notebook-tests', 'vscode-custom-editor-tests', 'integration-tests', // {{SQL CARBON EDIT}} ]; @@ -572,7 +571,7 @@ const esbuildMediaScripts = [ 'markdown-language-features/esbuild-notebook.js', 'markdown-language-features/esbuild-preview.js', 'markdown-math/esbuild.js', - // 'notebook-renderers/esbuild.js', {{SQL CARBON EDIT}} We don't have this extension + 'notebook-renderers/esbuild.js', 'simple-browser/esbuild-preview.js', ]; diff --git a/build/lib/locFunc.js b/build/lib/locFunc.js index 9f10993ac2..45eb781d7c 100644 --- a/build/lib/locFunc.js +++ b/build/lib/locFunc.js @@ -188,6 +188,7 @@ const VSCODEExtensions = [ "markdown-math", "merge-conflict", "microsoft-authentication", + "notebook-renderers", "powershell", "python", "r", diff --git a/build/lib/locFunc.ts b/build/lib/locFunc.ts index d30d5ae66d..7921032dbe 100644 --- a/build/lib/locFunc.ts +++ b/build/lib/locFunc.ts @@ -206,6 +206,7 @@ const VSCODEExtensions = [ "markdown-math", "merge-conflict", "microsoft-authentication", + "notebook-renderers", "powershell", "python", "r", diff --git a/build/npm/dirs.js b/build/npm/dirs.js index 7edd0cfe3e..65fd2e1ea4 100644 --- a/build/npm/dirs.js +++ b/build/npm/dirs.js @@ -29,6 +29,7 @@ exports.dirs = [ 'extensions/image-preview', 'extensions/import', 'extensions/integration-tests', + 'extensions/ipynb', 'extensions/json-language-features', 'extensions/json-language-features/server', 'extensions/kusto', @@ -39,6 +40,7 @@ exports.dirs = [ 'extensions/microsoft-authentication', 'extensions/mssql', 'extensions/notebook', + 'extensions/notebook-renderers', 'extensions/profiler', 'extensions/python', 'extensions/query-history', diff --git a/extensions/ipynb/.gitignore b/extensions/ipynb/.gitignore new file mode 100644 index 0000000000..170d890802 --- /dev/null +++ b/extensions/ipynb/.gitignore @@ -0,0 +1,4 @@ +out +dist +node_modules +*.vsix diff --git a/extensions/ipynb/.vscodeignore b/extensions/ipynb/.vscodeignore new file mode 100644 index 0000000000..08245c0148 --- /dev/null +++ b/extensions/ipynb/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +src/** +out/** +tsconfig.json +extension.webpack.config.js +extension-browser.webpack.config.js +yarn.lock +.gitignore + diff --git a/extensions/ipynb/README.md b/extensions/ipynb/README.md new file mode 100644 index 0000000000..767c8d7af4 --- /dev/null +++ b/extensions/ipynb/README.md @@ -0,0 +1,9 @@ +# Jupyter for Azure Data Studio + +**Notice:** This extension is bundled with Azure Data Studio. It can be disabled but not uninstalled. + +## Features + +This extension provides the following Jupyter-related features for Azure Data Studio: + +- Open, edit and save .ipynb files diff --git a/extensions/ipynb/extension-browser.webpack.config.js b/extensions/ipynb/extension-browser.webpack.config.js new file mode 100644 index 0000000000..b43f295c43 --- /dev/null +++ b/extensions/ipynb/extension-browser.webpack.config.js @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +const config = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/ipynbMain.ts' + }, + output: { + filename: 'ipynbMain.js' + } +}); + +module.exports = config; diff --git a/extensions/ipynb/extension.webpack.config.js b/extensions/ipynb/extension.webpack.config.js new file mode 100644 index 0000000000..88ea1ee468 --- /dev/null +++ b/extensions/ipynb/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/ipynbMain.ts', + }, + output: { + filename: 'ipynbMain.js' + } +}); diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json new file mode 100644 index 0000000000..c8feabc17a --- /dev/null +++ b/extensions/ipynb/package.json @@ -0,0 +1,90 @@ +{ + "name": "ipynb", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "version": "1.0.0", + "license": "MIT", + "engines": { + "vscode": "^1.57.0" + }, + "enabledApiProposals": [ + "notebookEditor", + "notebookEditorEdit" + ], + "activationEvents": [ + "*" + ], + "extensionKind": [ + "workspace", + "ui" + ], + "main": "./out/ipynbMain.js", + "browser": "./dist/browser/ipynbMain.js", + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "commands": [ + { + "command": "ipynb.newUntitledIpynb", + "title": "New Jupyter Notebook", + "shortTitle": "Jupyter Notebook", + "category": "Create" + }, + { + "command": "ipynb.openIpynbInNotebookEditor", + "title": "Open ipynb file in notebook editor" + } + ], + "notebooks": [ + { + "type": "jupyter-notebook", + "displayName": "Jupyter Notebook", + "selector": [ + { + "filenamePattern": "*.ipynb" + } + ], + "priority": "default" + } + ], + "menus": { + "file/newFile": [ + { + "command": "ipynb.newUntitledIpynb", + "group": "notebook" + } + ], + "commandPalette": [ + { + "command": "ipynb.newUntitledIpynb" + }, + { + "command": "ipynb.openIpynbInNotebookEditor", + "when": "false" + } + ] + } + }, + "scripts": { + "compile": "npx gulp compile-extension:ipynb", + "watch": "npx gulp watch-extension:ipynb" + }, + "dependencies": { + "@enonic/fnv-plus": "^1.3.0", + "detect-indent": "^6.0.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@jupyterlab/nbformat": "^3.2.9", + "@types/uuid": "^8.3.1" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json new file mode 100644 index 0000000000..6f2f7e47c0 --- /dev/null +++ b/extensions/ipynb/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": ".ipynb support", + "description": "Provides basic support for opening and reading Jupyter's .ipynb notebook files" +} diff --git a/extensions/ipynb/src/cellIdService.ts b/extensions/ipynb/src/cellIdService.ts new file mode 100644 index 0000000000..6a0e5a8acf --- /dev/null +++ b/extensions/ipynb/src/cellIdService.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext, NotebookDocument, NotebookDocumentChangeEvent, workspace, WorkspaceEdit } from 'vscode'; +import { v4 as uuid } from 'uuid'; +import { getCellMetadata } from './serializers'; +import { CellMetadata } from './common'; +import { getNotebookMetadata } from './notebookSerializer'; +import * as nbformat from '@jupyterlab/nbformat'; + +/** + * Ensure all new cells in notebooks with nbformat >= 4.5 have an id. + * Details of the spec can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html# + */ +export function ensureAllNewCellsHaveCellIds(context: ExtensionContext) { + workspace.onDidChangeNotebookDocument(onDidChangeNotebookCells, undefined, context.subscriptions); +} + +function onDidChangeNotebookCells(e: NotebookDocumentChangeEvent) { + const nbMetadata = getNotebookMetadata(e.notebook); + if (!isCellIdRequired(nbMetadata)) { + return; + } + e.contentChanges.forEach(change => { + change.addedCells.forEach(cell => { + const cellMetadata = getCellMetadata(cell); + if (cellMetadata?.id) { + return; + } + const id = generateCellId(e.notebook); + const edit = new WorkspaceEdit(); + // Don't edit the metadata directly, always get a clone (prevents accidental singletons and directly editing the objects). + const updatedMetadata: CellMetadata = { ...JSON.parse(JSON.stringify(cellMetadata || {})) }; + updatedMetadata.id = id; + edit.replaceNotebookCellMetadata(cell.notebook.uri, cell.index, { ...(cell.metadata), custom: updatedMetadata }); + workspace.applyEdit(edit); + }); + }); +} + +/** + * Cell ids are required in notebooks only in notebooks with nbformat >= 4.5 + */ +function isCellIdRequired(metadata: Pick, 'nbformat' | 'nbformat_minor'>) { + if ((metadata.nbformat || 0) >= 5) { + return true; + } + if ((metadata.nbformat || 0) === 4 && (metadata.nbformat_minor || 0) >= 5) { + return true; + } + return false; +} + +function generateCellId(notebook: NotebookDocument) { + while (true) { + // Details of the id can be found here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#adding-an-id-field, + // & here https://jupyter.org/enhancement-proposals/62-cell-id/cell-id.html#updating-older-formats + const id = uuid().replace(/-/g, '').substring(0, 8); + let duplicate = false; + for (let index = 0; index < notebook.cellCount; index++) { + const cell = notebook.cellAt(index); + const existingId = getCellMetadata(cell)?.id; + if (!existingId) { + continue; + } + if (existingId === id) { + duplicate = true; + break; + } + } + if (!duplicate) { + return id; + } + } +} diff --git a/extensions/ipynb/src/common.ts b/extensions/ipynb/src/common.ts new file mode 100644 index 0000000000..b343037aa5 --- /dev/null +++ b/extensions/ipynb/src/common.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nbformat from '@jupyterlab/nbformat'; + +/** + * Metadata we store in VS Code cell output items. + * This contains the original metadata from the Jupyter outputs. + */ +export interface CellOutputMetadata { + /** + * Cell output metadata. + */ + metadata?: any; + + /** + * Transient data from Jupyter. + */ + transient?: { + /** + * This is used for updating the output in other cells. + * We don't know of other properties, but this is definitely used. + */ + display_id?: string; + } & any; + + /** + * Original cell output type + */ + outputType: nbformat.OutputType | string; + + executionCount?: nbformat.IExecuteResult['ExecutionCount']; + + /** + * Whether the original Mime data is JSON or not. + * This properly only exists in metadata for NotebookCellOutputItems + * (this is something we have added) + */ + __isJson?: boolean; +} + + +/** + * Metadata we store in VS Code cells. + * This contains the original metadata from the Jupyuter cells. + */ +export interface CellMetadata { + /** + * Cell id for notebooks created with the new 4.5 version of nbformat. + */ + id?: string; + /** + * Stores attachments for cells. + */ + attachments?: nbformat.IAttachments; + /** + * Stores cell metadata. + */ + metadata?: Partial; +} diff --git a/extensions/ipynb/src/constants.ts b/extensions/ipynb/src/constants.ts new file mode 100644 index 0000000000..53d52e0858 --- /dev/null +++ b/extensions/ipynb/src/constants.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const defaultNotebookFormat = { major: 4, minor: 2 }; diff --git a/extensions/ipynb/src/deserializers.ts b/extensions/ipynb/src/deserializers.ts new file mode 100644 index 0000000000..52b307eaa9 --- /dev/null +++ b/extensions/ipynb/src/deserializers.ts @@ -0,0 +1,359 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nbformat from '@jupyterlab/nbformat'; +import { extensions, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode'; +import { CellMetadata, CellOutputMetadata } from './common'; + +const jupyterLanguageToMonacoLanguageMapping = new Map([ + ['c#', 'csharp'], + ['f#', 'fsharp'], + ['q#', 'qsharp'], + ['c++11', 'c++'], + ['c++12', 'c++'], + ['c++14', 'c++'] +]); + +export function getPreferredLanguage(metadata?: nbformat.INotebookMetadata) { + const jupyterLanguage = + metadata?.language_info?.name || + (metadata?.kernelspec as any)?.language; + + // Default to python language only if the Python extension is installed. + const defaultLanguage = + extensions.getExtension('ms-python.python') + ? 'python' + : (extensions.getExtension('ms-dotnettools.dotnet-interactive-vscode') ? 'csharp' : 'python'); + + // Note, whatever language is returned here, when the user selects a kernel, the cells (of blank documents) get updated based on that kernel selection. + return translateKernelLanguageToMonaco(jupyterLanguage || defaultLanguage); +} + +function translateKernelLanguageToMonaco(language: string): string { + language = language.toLowerCase(); + if (language.length === 2 && language.endsWith('#')) { + return `${language.substring(0, 1)}sharp`; + } + return jupyterLanguageToMonacoLanguageMapping.get(language) || language; +} + +const orderOfMimeTypes = [ + 'application/vnd.*', + 'application/vdom.*', + 'application/geo+json', + 'application/x-nteract-model-debug+json', + 'text/html', + 'application/javascript', + 'image/gif', + 'text/latex', + 'text/markdown', + 'image/png', + 'image/svg+xml', + 'image/jpeg', + 'application/json', + 'text/plain' +]; + +function isEmptyVendoredMimeType(outputItem: NotebookCellOutputItem) { + if (outputItem.mime.startsWith('application/vnd.')) { + try { + return outputItem.data.byteLength === 0 || Buffer.from(outputItem.data).toString().length === 0; + } catch { } + } + return false; +} +function isMimeTypeMatch(value: string, compareWith: string) { + if (value.endsWith('.*')) { + value = value.substr(0, value.indexOf('.*')); + } + return compareWith.startsWith(value); +} + +function sortOutputItemsBasedOnDisplayOrder(outputItems: NotebookCellOutputItem[]): NotebookCellOutputItem[] { + return outputItems + .map(item => { + let index = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(mime, item.mime)); + // Sometimes we can have mime types with empty data, e.g. when using holoview we can have `application/vnd.holoviews_load.v0+json` with empty value. + // & in these cases we have HTML/JS and those take precedence. + // https://github.com/microsoft/vscode-jupyter/issues/6109 + if (isEmptyVendoredMimeType(item)) { + index = -1; + } + index = index === -1 ? 100 : index; + return { + item, index + }; + }) + .sort((outputItemA, outputItemB) => outputItemA.index - outputItemB.index).map(item => item.item); +} + + +enum CellOutputMimeTypes { + error = 'application/vnd.code.notebook.error', + stderr = 'application/vnd.code.notebook.stderr', + stdout = 'application/vnd.code.notebook.stdout' +} + +export const textMimeTypes = ['text/plain', 'text/markdown', 'text/latex', CellOutputMimeTypes.stderr, CellOutputMimeTypes.stdout]; + +function concatMultilineString(str: string | string[], trim?: boolean): string { + const nonLineFeedWhiteSpaceTrim = /(^[\t\f\v\r ]+|[\t\f\v\r ]+$)/g; + if (Array.isArray(str)) { + let result = ''; + for (let i = 0; i < str.length; i += 1) { + const s = str[i]; + if (i < str.length - 1 && !s.endsWith('\n')) { + result = result.concat(`${s}\n`); + } else { + result = result.concat(s); + } + } + + // Just trim whitespace. Leave \n in place + return trim ? result.replace(nonLineFeedWhiteSpaceTrim, '') : result; + } + return trim ? str.toString().replace(nonLineFeedWhiteSpaceTrim, '') : str.toString(); +} + +function convertJupyterOutputToBuffer(mime: string, value: unknown): NotebookCellOutputItem { + if (!value) { + return NotebookCellOutputItem.text('', mime); + } + try { + if ( + (mime.startsWith('text/') || textMimeTypes.includes(mime)) && + (Array.isArray(value) || typeof value === 'string') + ) { + const stringValue = Array.isArray(value) ? concatMultilineString(value) : value; + return NotebookCellOutputItem.text(stringValue, mime); + } else if (mime.startsWith('image/') && typeof value === 'string' && mime !== 'image/svg+xml') { + // Images in Jupyter are stored in base64 encoded format. + // VS Code expects bytes when rendering images. + if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') { + return new NotebookCellOutputItem(Buffer.from(value, 'base64'), mime); + } else { + const data = Uint8Array.from(atob(value), c => c.charCodeAt(0)); + return new NotebookCellOutputItem(data, mime); + } + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return NotebookCellOutputItem.text(JSON.stringify(value), mime); + } else { + // For everything else, treat the data as strings (or multi-line strings). + value = Array.isArray(value) ? concatMultilineString(value) : value; + return NotebookCellOutputItem.text(value as string, mime); + } + } catch (ex) { + return NotebookCellOutputItem.error(ex); + } +} + +function getNotebookCellMetadata(cell: nbformat.IBaseCell): CellMetadata { + // We put this only for VSC to display in diff view. + // Else we don't use this. + const propertiesToClone: (keyof CellMetadata)[] = ['metadata', 'attachments']; + const custom: CellMetadata = {}; + propertiesToClone.forEach((propertyToClone) => { + if (cell[propertyToClone]) { + custom[propertyToClone] = JSON.parse(JSON.stringify(cell[propertyToClone])); + } + }); + if ('id' in cell && typeof cell.id === 'string') { + custom.id = cell.id; + } + return custom; +} +function getOutputMetadata(output: nbformat.IOutput): CellOutputMetadata { + // Add on transient data if we have any. This should be removed by our save functions elsewhere. + const metadata: CellOutputMetadata = { + outputType: output.output_type + }; + if (output.transient) { + metadata.transient = output.transient; + } + + switch (output.output_type as nbformat.OutputType) { + case 'display_data': + case 'execute_result': + case 'update_display_data': { + metadata.executionCount = output.execution_count; + metadata.metadata = output.metadata ? JSON.parse(JSON.stringify(output.metadata)) : {}; + break; + } + default: + break; + } + + return metadata; +} + + +function translateDisplayDataOutput( + output: nbformat.IDisplayData | nbformat.IDisplayUpdate | nbformat.IExecuteResult +): NotebookCellOutput { + // Metadata could be as follows: + // We'll have metadata specific to each mime type as well as generic metadata. + /* + IDisplayData = { + output_type: 'display_data', + data: { + 'image/jpg': '/////' + 'image/png': '/////' + 'text/plain': '/////' + }, + metadata: { + 'image/png': '/////', + 'background': true, + 'xyz': '/// + } + } + */ + const metadata = getOutputMetadata(output); + const items: NotebookCellOutputItem[] = []; + if (output.data) { + for (const key in output.data) { + items.push(convertJupyterOutputToBuffer(key, output.data[key])); + } + } + + return new NotebookCellOutput(sortOutputItemsBasedOnDisplayOrder(items), metadata); +} + +function translateErrorOutput(output?: nbformat.IError): NotebookCellOutput { + output = output || { output_type: 'error', ename: '', evalue: '', traceback: [] }; + return new NotebookCellOutput( + [ + NotebookCellOutputItem.error({ + name: output?.ename || '', + message: output?.evalue || '', + stack: (output?.traceback || []).join('\n') + }) + ], + { ...getOutputMetadata(output), originalError: output } + ); +} + +function translateStreamOutput(output: nbformat.IStream): NotebookCellOutput { + const value = concatMultilineString(output.text); + const item = output.name === 'stderr' ? NotebookCellOutputItem.stderr(value) : NotebookCellOutputItem.stdout(value); + return new NotebookCellOutput([item], getOutputMetadata(output)); +} + +const cellOutputMappers = new Map NotebookCellOutput>(); +cellOutputMappers.set('display_data', translateDisplayDataOutput); +cellOutputMappers.set('execute_result', translateDisplayDataOutput); +cellOutputMappers.set('update_display_data', translateDisplayDataOutput); +cellOutputMappers.set('error', translateErrorOutput); +cellOutputMappers.set('stream', translateStreamOutput); + +export function jupyterCellOutputToCellOutput(output: nbformat.IOutput): NotebookCellOutput { + /** + * Stream, `application/x.notebook.stream` + * Error, `application/x.notebook.error-traceback` + * Rich, { mime: value } + * + * outputs: [ + new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('application/x.notebook.stream', 2), + new vscode.NotebookCellOutputItem('application/x.notebook.stream', 3), + ]), + new vscode.NotebookCellOutput([ + new vscode.NotebookCellOutputItem('text/markdown', '## header 2'), + new vscode.NotebookCellOutputItem('image/svg+xml', [ + "\n", + " \n", + " \n", + " SVG\n", + "" + ]), + ]), + ] + * + */ + const fn = cellOutputMappers.get(output.output_type as nbformat.OutputType); + let result: NotebookCellOutput; + if (fn) { + result = fn(output); + } else { + result = translateDisplayDataOutput(output as any); + } + return result; +} + +function createNotebookCellDataFromRawCell(cell: nbformat.IRawCell): NotebookCellData { + const cellData = new NotebookCellData(NotebookCellKind.Code, concatMultilineString(cell.source), 'raw'); + cellData.outputs = []; + cellData.metadata = { custom: getNotebookCellMetadata(cell) }; + return cellData; +} +function createNotebookCellDataFromMarkdownCell(cell: nbformat.IMarkdownCell): NotebookCellData { + const cellData = new NotebookCellData( + NotebookCellKind.Markup, + concatMultilineString(cell.source), + 'markdown' + ); + cellData.outputs = []; + cellData.metadata = { custom: getNotebookCellMetadata(cell) }; + return cellData; +} +function createNotebookCellDataFromCodeCell(cell: nbformat.ICodeCell, cellLanguage: string): NotebookCellData { + const cellOutputs = Array.isArray(cell.outputs) ? cell.outputs : []; + const outputs = cellOutputs.map(jupyterCellOutputToCellOutput); + const hasExecutionCount = typeof cell.execution_count === 'number' && cell.execution_count > 0; + + const source = concatMultilineString(cell.source); + + const executionSummary: NotebookCellExecutionSummary = hasExecutionCount + ? { executionOrder: cell.execution_count as number } + : {}; + + const vscodeCustomMetadata = cell.metadata['vscode'] as { [key: string]: any } | undefined; + const cellLanguageId = vscodeCustomMetadata && vscodeCustomMetadata.languageId && typeof vscodeCustomMetadata.languageId === 'string' ? vscodeCustomMetadata.languageId : cellLanguage; + const cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguageId); + + cellData.outputs = outputs; + cellData.metadata = { custom: getNotebookCellMetadata(cell) }; + cellData.executionSummary = executionSummary; + return cellData; +} + +function createNotebookCellDataFromJupyterCell( + cellLanguage: string, + cell: nbformat.IBaseCell +): NotebookCellData | undefined { + switch (cell.cell_type) { + case 'raw': { + return createNotebookCellDataFromRawCell(cell as nbformat.IRawCell); + } + case 'markdown': { + return createNotebookCellDataFromMarkdownCell(cell as nbformat.IMarkdownCell); + } + case 'code': { + return createNotebookCellDataFromCodeCell(cell as nbformat.ICodeCell, cellLanguage); + } + } + + return; +} + +/** + * Converts a NotebookModel into VS Code format. + */ +export function jupyterNotebookModelToNotebookData( + notebookContent: Partial, + preferredLanguage: string +): NotebookData { + const notebookContentWithoutCells = { ...notebookContent, cells: [] }; + if (!notebookContent.cells || notebookContent.cells.length === 0) { + throw new Error('Notebook content is missing cells'); + } + + const cells = notebookContent.cells + .map(cell => createNotebookCellDataFromJupyterCell(preferredLanguage, cell)) + .filter((item): item is NotebookCellData => !!item); + + const notebookData = new NotebookData(cells); + notebookData.metadata = { custom: notebookContentWithoutCells }; + return notebookData; +} diff --git a/extensions/ipynb/src/ipynbMain.ts b/extensions/ipynb/src/ipynbMain.ts new file mode 100644 index 0000000000..d111446b9f --- /dev/null +++ b/extensions/ipynb/src/ipynbMain.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { ensureAllNewCellsHaveCellIds } from './cellIdService'; +import { NotebookSerializer } from './notebookSerializer'; + +// From {nbformat.INotebookMetadata} in @jupyterlab/coreutils +type NotebookMetadata = { + kernelspec?: { + name: string; + display_name: string; + [propName: string]: unknown; + }; + language_info?: { + name: string; + codemirror_mode?: string | {}; + file_extension?: string; + mimetype?: string; + pygments_lexer?: string; + [propName: string]: unknown; + }; + orig_nbformat: number; + [propName: string]: unknown; +}; + +export function activate(context: vscode.ExtensionContext) { + const serializer = new NotebookSerializer(context); + ensureAllNewCellsHaveCellIds(context); + context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jupyter-notebook', serializer, { + transientOutputs: false, + transientCellMetadata: { + breakpointMargin: true, + custom: false + } + })); + + vscode.languages.registerCodeLensProvider({ pattern: '**/*.ipynb' }, { + provideCodeLenses: (document) => { + if ( + document.uri.scheme === 'vscode-notebook-cell' || + document.uri.scheme === 'vscode-notebook-cell-metadata' || + document.uri.scheme === 'vscode-notebook-cell-output' + ) { + return []; + } + const codelens = new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), { title: 'Open in Notebook Editor', command: 'ipynb.openIpynbInNotebookEditor', arguments: [document.uri] }); + return [codelens]; + } + }); + + context.subscriptions.push(vscode.commands.registerCommand('ipynb.newUntitledIpynb', async () => { + const language = 'python'; + const cell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, '', language); + const data = new vscode.NotebookData([cell]); + data.metadata = { + custom: { + cells: [], + metadata: { + orig_nbformat: 4 + }, + nbformat: 4, + nbformat_minor: 2 + } + }; + const doc = await vscode.workspace.openNotebookDocument('jupyter-notebook', data); + await vscode.window.showNotebookDocument(doc); + })); + + context.subscriptions.push(vscode.commands.registerCommand('ipynb.openIpynbInNotebookEditor', async (uri: vscode.Uri) => { + if (vscode.window.activeTextEditor?.document.uri.toString() === uri.toString()) { + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + } + const document = await vscode.workspace.openNotebookDocument(uri); + await vscode.window.showNotebookDocument(document); + })); + + // Update new file contribution + vscode.extensions.onDidChange(() => { + vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); + }); + vscode.commands.executeCommand('setContext', 'jupyterEnabled', vscode.extensions.getExtension('ms-toolsai.jupyter')); + + return { + exportNotebook: (notebook: vscode.NotebookData): string => { + return exportNotebook(notebook, serializer); + }, + setNotebookMetadata: async (resource: vscode.Uri, metadata: Partial): Promise => { + const document = vscode.workspace.notebookDocuments.find(doc => doc.uri.toString() === resource.toString()); + if (!document) { + return false; + } + + const edit = new vscode.WorkspaceEdit(); + edit.replaceNotebookMetadata(resource, { + ...document.metadata, + custom: { + ...(document.metadata.custom ?? {}), + metadata: { + ...(document.metadata.custom?.metadata ?? {}), + ...metadata + }, + } + }); + return vscode.workspace.applyEdit(edit); + }, + }; +} + +function exportNotebook(notebook: vscode.NotebookData, serializer: NotebookSerializer): string { + return serializer.serializeNotebookToString(notebook); +} + +export function deactivate() { } diff --git a/extensions/ipynb/src/notebookSerializer.ts b/extensions/ipynb/src/notebookSerializer.ts new file mode 100644 index 0000000000..cb70f57ff2 --- /dev/null +++ b/extensions/ipynb/src/notebookSerializer.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nbformat from '@jupyterlab/nbformat'; +import * as detectIndent from 'detect-indent'; +import * as vscode from 'vscode'; +import { defaultNotebookFormat } from './constants'; +import { getPreferredLanguage, jupyterNotebookModelToNotebookData } from './deserializers'; +import { createJupyterCellFromNotebookCell, pruneCell, sortObjectPropertiesRecursively } from './serializers'; +import * as fnv from '@enonic/fnv-plus'; + +export class NotebookSerializer implements vscode.NotebookSerializer { + constructor(readonly context: vscode.ExtensionContext) { + } + + public async deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): Promise { + let contents = ''; + try { + contents = new TextDecoder().decode(content); + } catch { + } + + let json = contents && /\S/.test(contents) ? (JSON.parse(contents) as Partial) : {}; + + if (json.__webview_backup) { + const backupId = json.__webview_backup; + const uri = this.context.globalStorageUri; + const folder = uri.with({ path: this.context.globalStorageUri.path.replace('vscode.ipynb', 'ms-toolsai.jupyter') }); + const fileHash = fnv.fast1a32hex(backupId) as string; + const fileName = `${fileHash}.ipynb`; + const file = vscode.Uri.joinPath(folder, fileName); + const data = await vscode.workspace.fs.readFile(file); + json = data ? JSON.parse(data.toString()) : {}; + + if (json.contents && typeof json.contents === 'string') { + contents = json.contents; + json = JSON.parse(contents) as Partial; + } + } + + if (json.nbformat && json.nbformat < 4) { + throw new Error('Only Jupyter notebooks version 4+ are supported'); + } + + // Then compute indent from the contents (only use first 1K characters as a perf optimization) + const indentAmount = contents ? detectIndent(contents.substring(0, 1_000)).indent : ' '; + + const preferredCellLanguage = getPreferredLanguage(json.metadata); + // Ensure we always have a blank cell. + if ((json.cells || []).length === 0) { + json.cells = [ + { + cell_type: 'code', + execution_count: null, + metadata: {}, + outputs: [], + source: '' + } + ]; + } + + // For notebooks without metadata default the language in metadata to the preferred language. + if (!json.metadata || (!json.metadata.kernelspec && !json.metadata.language_info)) { + json.metadata = json.metadata || { orig_nbformat: defaultNotebookFormat.major }; + json.metadata.language_info = json.metadata.language_info || { name: preferredCellLanguage }; + } + + const data = jupyterNotebookModelToNotebookData( + json, + preferredCellLanguage + ); + data.metadata = data.metadata || {}; + data.metadata.indentAmount = indentAmount; + + return data; + } + + public serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array { + return new TextEncoder().encode(this.serializeNotebookToString(data)); + } + + public serializeNotebookToString(data: vscode.NotebookData): string { + const notebookContent = getNotebookMetadata(data); + // use the preferred language from document metadata or the first cell language as the notebook preferred cell language + const preferredCellLanguage = notebookContent.metadata?.language_info?.name ?? data.cells[0].languageId; + + notebookContent.cells = data.cells + .map(cell => createJupyterCellFromNotebookCell(cell, preferredCellLanguage)) + .map(pruneCell); + + const indentAmount = data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string' ? + data.metadata.indentAmount : + ' '; + // ipynb always ends with a trailing new line (we add this so that SCMs do not show unnecesary changes, resulting from a missing trailing new line). + return JSON.stringify(sortObjectPropertiesRecursively(notebookContent), undefined, indentAmount) + '\n'; + } +} + +export function getNotebookMetadata(document: vscode.NotebookDocument | vscode.NotebookData) { + const notebookContent: Partial = document.metadata?.custom || {}; + notebookContent.cells = notebookContent.cells || []; + notebookContent.nbformat = notebookContent.nbformat || 4; + notebookContent.nbformat_minor = notebookContent.nbformat_minor ?? 2; + notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 }; + return notebookContent; +} diff --git a/extensions/ipynb/src/serializers.ts b/extensions/ipynb/src/serializers.ts new file mode 100644 index 0000000000..8606a52a68 --- /dev/null +++ b/extensions/ipynb/src/serializers.ts @@ -0,0 +1,421 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nbformat from '@jupyterlab/nbformat'; +import { NotebookCell, NotebookCellData, NotebookCellKind, NotebookCellOutput } from 'vscode'; +import { CellMetadata, CellOutputMetadata } from './common'; +import { textMimeTypes } from './deserializers'; + +const textDecoder = new TextDecoder(); + +enum CellOutputMimeTypes { + error = 'application/vnd.code.notebook.error', + stderr = 'application/vnd.code.notebook.stderr', + stdout = 'application/vnd.code.notebook.stdout' +} + +export function createJupyterCellFromNotebookCell( + vscCell: NotebookCellData, + preferredLanguage: string | undefined +): nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell { + let cell: nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell; + if (vscCell.kind === NotebookCellKind.Markup) { + cell = createMarkdownCellFromNotebookCell(vscCell); + } else if (vscCell.languageId === 'raw') { + cell = createRawCellFromNotebookCell(vscCell); + } else { + cell = createCodeCellFromNotebookCell(vscCell, preferredLanguage); + } + return cell; +} + + +/** + * Sort the JSON to minimize unnecessary SCM changes. + * Jupyter notbeooks/labs sorts the JSON keys in alphabetical order. + * https://github.com/microsoft/vscode-python/issues/13155 + */ +export function sortObjectPropertiesRecursively(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(sortObjectPropertiesRecursively); + } + if (obj !== undefined && obj !== null && typeof obj === 'object' && Object.keys(obj).length > 0) { + return ( + Object.keys(obj) + .sort() + .reduce>((sortedObj, prop) => { + sortedObj[prop] = sortObjectPropertiesRecursively(obj[prop]); + return sortedObj; + }, {}) as any + ); + } + return obj; +} + +export function getCellMetadata(cell: NotebookCell | NotebookCellData) { + return cell.metadata?.custom as CellMetadata | undefined; +} +function createCodeCellFromNotebookCell(cell: NotebookCellData, preferredLanguage: string | undefined): nbformat.ICodeCell { + const cellMetadata = getCellMetadata(cell); + let metadata = cellMetadata?.metadata || {}; // This cannot be empty. + if (cell.languageId !== preferredLanguage) { + metadata = { + ...metadata, + vscode: { + languageId: cell.languageId + } + }; + } else { + // cell current language is the same as the preferred cell language in the document, flush the vscode custom language id metadata + metadata.vscode = undefined; + } + + const codeCell: any = { + cell_type: 'code', + execution_count: cell.executionSummary?.executionOrder ?? null, + source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), + outputs: (cell.outputs || []).map(translateCellDisplayOutput), + metadata: metadata + }; + if (cellMetadata?.id) { + codeCell.id = cellMetadata.id; + } + return codeCell; +} + +function createRawCellFromNotebookCell(cell: NotebookCellData): nbformat.IRawCell { + const cellMetadata = getCellMetadata(cell); + const rawCell: any = { + cell_type: 'raw', + source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), + metadata: cellMetadata?.metadata || {} // This cannot be empty. + }; + if (cellMetadata?.attachments) { + rawCell.attachments = cellMetadata.attachments; + } + if (cellMetadata?.id) { + rawCell.id = cellMetadata.id; + } + return rawCell; +} + +function splitMultilineString(source: nbformat.MultilineString): string[] { + if (Array.isArray(source)) { + return source as string[]; + } + const str = source.toString(); + if (str.length > 0) { + // Each line should be a separate entry, but end with a \n if not last entry + const arr = str.split('\n'); + return arr + .map((s, i) => { + if (i < arr.length - 1) { + return `${s}\n`; + } + return s; + }) + .filter(s => s.length > 0); // Skip last one if empty (it's the only one that could be length 0) + } + return []; +} + +function translateCellDisplayOutput(output: NotebookCellOutput): JupyterOutput { + const customMetadata = output.metadata as CellOutputMetadata | undefined; + let result: JupyterOutput; + // Possible some other extension added some output (do best effort to translate & save in ipynb). + // In which case metadata might not contain `outputType`. + const outputType = customMetadata?.outputType as nbformat.OutputType; + switch (outputType) { + case 'error': { + result = translateCellErrorOutput(output); + break; + } + case 'stream': { + result = convertStreamOutput(output); + break; + } + case 'display_data': { + result = { + output_type: 'display_data', + data: output.items.reduce((prev: any, curr) => { + prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array); + return prev; + }, {}), + metadata: customMetadata?.metadata || {} // This can never be undefined. + }; + break; + } + case 'execute_result': { + result = { + output_type: 'execute_result', + data: output.items.reduce((prev: any, curr) => { + prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array); + return prev; + }, {}), + metadata: customMetadata?.metadata || {}, // This can never be undefined. + execution_count: + typeof customMetadata?.executionCount === 'number' ? customMetadata?.executionCount : null // This can never be undefined, only a number or `null`. + }; + break; + } + case 'update_display_data': { + result = { + output_type: 'update_display_data', + data: output.items.reduce((prev: any, curr) => { + prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array); + return prev; + }, {}), + metadata: customMetadata?.metadata || {} // This can never be undefined. + }; + break; + } + default: { + const isError = + output.items.length === 1 && output.items.every((item) => item.mime === CellOutputMimeTypes.error); + const isStream = output.items.every( + (item) => item.mime === CellOutputMimeTypes.stderr || item.mime === CellOutputMimeTypes.stdout + ); + + if (isError) { + return translateCellErrorOutput(output); + } + + // In the case of .NET & other kernels, we need to ensure we save ipynb correctly. + // Hence if we have stream output, save the output as Jupyter `stream` else `display_data` + // Unless we already know its an unknown output type. + const outputType: nbformat.OutputType = + customMetadata?.outputType || (isStream ? 'stream' : 'display_data'); + let unknownOutput: nbformat.IUnrecognizedOutput | nbformat.IDisplayData | nbformat.IStream; + if (outputType === 'stream') { + // If saving as `stream` ensure the mandatory properties are set. + unknownOutput = convertStreamOutput(output); + } else if (outputType === 'display_data') { + // If saving as `display_data` ensure the mandatory properties are set. + const displayData: nbformat.IDisplayData = { + data: {}, + metadata: {}, + output_type: 'display_data' + }; + unknownOutput = displayData; + } else { + unknownOutput = { + output_type: outputType + }; + } + if (customMetadata?.metadata) { + unknownOutput.metadata = customMetadata.metadata; + } + if (output.items.length > 0) { + unknownOutput.data = output.items.reduce((prev: any, curr) => { + prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array); + return prev; + }, {}); + } + result = unknownOutput; + break; + } + } + + // Account for transient data as well + // `transient.display_id` is used to update cell output in other cells, at least thats one use case we know of. + if (result && customMetadata && customMetadata.transient) { + result.transient = customMetadata.transient; + } + return result; +} + +function translateCellErrorOutput(output: NotebookCellOutput): nbformat.IError { + // it should have at least one output item + const firstItem = output.items[0]; + // Bug in VS Code. + if (!firstItem.data) { + return { + output_type: 'error', + ename: '', + evalue: '', + traceback: [] + }; + } + const originalError: undefined | nbformat.IError = output.metadata?.originalError; + const value: Error = JSON.parse(textDecoder.decode(firstItem.data)); + return { + output_type: 'error', + ename: value.name, + evalue: value.message, + // VS Code needs an `Error` object which requires a `stack` property as a string. + // Its possible the format could change when converting from `traceback` to `string` and back again to `string` + // When .NET stores errors in output (with their .NET kernel), + // stack is empty, hence store the message instead of stack (so that somethign gets displayed in ipynb). + traceback: originalError?.traceback || splitMultilineString(value.stack || value.message || '') + }; +} + + +function getOutputStreamType(output: NotebookCellOutput): string | undefined { + if (output.items.length > 0) { + return output.items[0].mime === CellOutputMimeTypes.stderr ? 'stderr' : 'stdout'; + } + + return; +} + +type JupyterOutput = + | nbformat.IUnrecognizedOutput + | nbformat.IExecuteResult + | nbformat.IDisplayData + | nbformat.IStream + | nbformat.IError; + +function convertStreamOutput(output: NotebookCellOutput): JupyterOutput { + const outputs: string[] = []; + output.items + .filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout) + .map((opit) => textDecoder.decode(opit.data)) + .forEach(value => { + // Ensure each line is a seprate entry in an array (ending with \n). + const lines = value.split('\n'); + // If the last item in `outputs` is not empty and the first item in `lines` is not empty, then concate them. + // As they are part of the same line. + if (outputs.length && lines.length && lines[0].length > 0) { + outputs[outputs.length - 1] = `${outputs[outputs.length - 1]}${lines.shift()!}`; + } + for (const line of lines) { + outputs.push(line); + } + }); + + for (let index = 0; index < (outputs.length - 1); index++) { + outputs[index] = `${outputs[index]}\n`; + } + + // Skip last one if empty (it's the only one that could be length 0) + if (outputs.length && outputs[outputs.length - 1].length === 0) { + outputs.pop(); + } + + const streamType = getOutputStreamType(output) || 'stdout'; + + return { + output_type: 'stream', + name: streamType, + text: outputs + }; +} + +function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) { + if (!value) { + return ''; + } + try { + if (mime === CellOutputMimeTypes.error) { + const stringValue = textDecoder.decode(value); + return JSON.parse(stringValue); + } else if (mime.startsWith('text/') || textMimeTypes.includes(mime)) { + const stringValue = textDecoder.decode(value); + return splitMultilineString(stringValue); + } else if (mime.startsWith('image/') && mime !== 'image/svg+xml') { + // Images in Jupyter are stored in base64 encoded format. + // VS Code expects bytes when rendering images. + if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') { + return Buffer.from(value).toString('base64'); + } else { + return btoa(value.reduce((s: string, b: number) => s + String.fromCharCode(b), '')); + } + } else if (mime.toLowerCase().includes('json')) { + const stringValue = textDecoder.decode(value); + return stringValue.length > 0 ? JSON.parse(stringValue) : stringValue; + } else { + const stringValue = textDecoder.decode(value); + return stringValue; + } + } catch (ex) { + return ''; + } +} + +function createMarkdownCellFromNotebookCell(cell: NotebookCellData): nbformat.IMarkdownCell { + const cellMetadata = getCellMetadata(cell); + const markdownCell: any = { + cell_type: 'markdown', + source: splitMultilineString(cell.value.replace(/\r\n/g, '\n')), + metadata: cellMetadata?.metadata || {} // This cannot be empty. + }; + if (cellMetadata?.attachments) { + markdownCell.attachments = cellMetadata.attachments; + } + if (cellMetadata?.id) { + markdownCell.id = cellMetadata.id; + } + return markdownCell; +} + +export function pruneCell(cell: nbformat.ICell): nbformat.ICell { + // Source is usually a single string on input. Convert back to an array + const result = { + ...cell, + source: splitMultilineString(cell.source) + } as nbformat.ICell; + + // Remove outputs and execution_count from non code cells + if (result.cell_type !== 'code') { + delete (result).outputs; + delete (result).execution_count; + } else { + // Clean outputs from code cells + result.outputs = result.outputs ? (result.outputs as nbformat.IOutput[]).map(fixupOutput) : []; + } + + return result; +} +const dummyStreamObj: nbformat.IStream = { + output_type: 'stream', + name: 'stdout', + text: '' +}; +const dummyErrorObj: nbformat.IError = { + output_type: 'error', + ename: '', + evalue: '', + traceback: [''] +}; +const dummyDisplayObj: nbformat.IDisplayData = { + output_type: 'display_data', + data: {}, + metadata: {} +}; +const dummyExecuteResultObj: nbformat.IExecuteResult = { + output_type: 'execute_result', + name: '', + execution_count: 0, + data: {}, + metadata: {} +}; +const AllowedCellOutputKeys = { + ['stream']: new Set(Object.keys(dummyStreamObj)), + ['error']: new Set(Object.keys(dummyErrorObj)), + ['display_data']: new Set(Object.keys(dummyDisplayObj)), + ['execute_result']: new Set(Object.keys(dummyExecuteResultObj)) +}; + +function fixupOutput(output: nbformat.IOutput): nbformat.IOutput { + let allowedKeys: Set; + switch (output.output_type) { + case 'stream': + case 'error': + case 'execute_result': + case 'display_data': + allowedKeys = AllowedCellOutputKeys[output.output_type]; + break; + default: + return output; + } + const result = { ...output }; + for (const k of Object.keys(output)) { + if (!allowedKeys.has(k)) { + delete result[k]; + } + } + return result; +} diff --git a/extensions/ipynb/src/test/index.ts b/extensions/ipynb/src/test/index.ts new file mode 100644 index 0000000000..38ad28d2d0 --- /dev/null +++ b/extensions/ipynb/src/test/index.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('../../../../test/integration/electron/testrunner'); + +const options: any = { + ui: 'tdd', + color: true, + timeout: 60000 +}; + +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration .ipynb Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration .ipynb Tests'; +} else { + suite = 'Integration .ipynb Tests'; +} + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/ipynb/src/test/serializers.test.ts b/extensions/ipynb/src/test/serializers.test.ts new file mode 100644 index 0000000000..84df4eec0b --- /dev/null +++ b/extensions/ipynb/src/test/serializers.test.ts @@ -0,0 +1,637 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nbformat from '@jupyterlab/nbformat'; +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { jupyterCellOutputToCellOutput, jupyterNotebookModelToNotebookData } from '../deserializers'; + +function deepStripProperties(obj: any, props: string[]) { + for (let prop in obj) { + if (obj[prop]) { + delete obj[prop]; + } else if (typeof obj[prop] === 'object') { + deepStripProperties(obj[prop], props); + } + } +} + +suite('ipynb serializer', () => { + const base64EncodedImage = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOUlZL6DwAB/wFSU1jVmgAAAABJRU5ErkJggg=='; + test('Deserialize', async () => { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs: [], + source: 'print(1)', + metadata: {} + }, + { + cell_type: 'markdown', + source: '# HEAD', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + assert.ok(notebook); + + const expectedCodeCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Code, 'print(1)', 'python'); + expectedCodeCell.outputs = []; + expectedCodeCell.metadata = { custom: { metadata: {} } }; + expectedCodeCell.executionSummary = { executionOrder: 10 }; + + const expectedMarkdownCell = new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, '# HEAD', 'markdown'); + expectedMarkdownCell.outputs = []; + expectedMarkdownCell.metadata = { + custom: { metadata: {} } + }; + + assert.deepStrictEqual(notebook.cells, [expectedCodeCell, expectedMarkdownCell]); + }); + suite('Outputs', () => { + function validateCellOutputTranslation( + outputs: nbformat.IOutput[], + expectedOutputs: vscode.NotebookCellOutput[], + propertiesToExcludeFromComparison: string[] = [] + ) { + const cells: nbformat.ICell[] = [ + { + cell_type: 'code', + execution_count: 10, + outputs, + source: 'print(1)', + metadata: {} + } + ]; + const notebook = jupyterNotebookModelToNotebookData({ cells }, 'python'); + + // OutputItems contain an `id` property generated by VSC. + // Exclude that property when comparing. + const propertiesToExclude = propertiesToExcludeFromComparison.concat(['id']); + const actualOuts = notebook.cells[0].outputs; + deepStripProperties(actualOuts, propertiesToExclude); + deepStripProperties(expectedOutputs, propertiesToExclude); + assert.deepStrictEqual(actualOuts, expectedOutputs); + } + + test('Empty output', () => { + validateCellOutputTranslation([], []); + }); + + test('Stream output', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stderr', + text: 'Error' + }, + { + output_type: 'stream', + name: 'stdout', + text: 'NoError' + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('Error')], { + outputType: 'stream' + }), + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('NoError')], { + outputType: 'stream' + }) + ] + ); + }); + test('Stream output and line endings', () => { + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stdout', + text: [ + 'Line1\n', + '\n', + 'Line3\n', + 'Line4' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Line1\n\nLine3\nLine4')], { + outputType: 'stream' + }) + ] + ); + validateCellOutputTranslation( + [ + { + output_type: 'stream', + name: 'stdout', + text: [ + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n', + 'Hello\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout('Hello\nHello\nHello\nHello\nHello\nHello\n')], { + outputType: 'stream' + }) + ] + ); + }); + test('Multi-line Stream output', () => { + validateCellOutputTranslation( + [ + { + name: 'stdout', + output_type: 'stream', + text: [ + 'Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stdout(['Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n'].join(''))], { + outputType: 'stream' + }) + ] + ); + }); + + test('Multi-line Stream output (last empty line should not be saved in ipynb)', () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + output_type: 'stream', + text: [ + 'Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n' + ] + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr(['Epoch 1/5\n', + '...\n', + 'Epoch 2/5\n', + '...\n', + 'Epoch 3/5\n', + '...\n', + 'Epoch 4/5\n', + '...\n', + 'Epoch 5/5\n', + '...\n', + // This last empty line should not be saved in ipynb. + '\n'].join(''))], { + outputType: 'stream' + }) + ] + ); + }); + + test('Streamed text with Ansi characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); + + test('Streamed text with angle bracket characters', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.stderr('1 is < 2')], { + outputType: 'stream' + }) + ] + ); + }); + + test('Streamed text with angle bracket characters and ansi chars', async () => { + validateCellOutputTranslation( + [ + { + name: 'stderr', + text: '1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n', + output_type: 'stream' + } + ], + [ + new vscode.NotebookCellOutput( + [vscode.NotebookCellOutputItem.stderr('1 is < 2\u001b[K\u001b[33m✅ \u001b[0m Loading\n')], + { + outputType: 'stream' + } + ) + ] + ); + }); + + test('Error', async () => { + validateCellOutputTranslation( + [ + { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + ], + [ + new vscode.NotebookCellOutput( + [ + vscode.NotebookCellOutputItem.error({ + name: 'Error Name', + message: 'Error Value', + stack: ['stack1', 'stack2', 'stack3'].join('\n') + }) + ], + { + outputType: 'error', + originalError: { + ename: 'Error Name', + evalue: 'Error Value', + traceback: ['stack1', 'stack2', 'stack3'], + output_type: 'error' + } + } + ) + ] + ); + }); + + ['display_data', 'execute_result'].forEach(output_type => { + suite(`Rich output for output_type = ${output_type}`, () => { + // Properties to exclude when comparing. + let propertiesToExcludeFromComparison: string[] = []; + setup(() => { + if (output_type === 'display_data') { + // With display_data the execution_count property will never exist in the output. + // We can ignore that (as it will never exist). + // But we leave it in the case of `output_type === 'execute_result'` + propertiesToExcludeFromComparison = ['execution_count', 'executionCount']; + } + }); + + test('Text mimeType output', async () => { + validateCellOutputTranslation( + [ + { + data: { + 'text/plain': 'Hello World!' + }, + output_type, + metadata: {}, + execution_count: 1 + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from('Hello World!', 'utf8'), 'text/plain')], + { + outputType: output_type, + metadata: {}, // display_data & execute_result always have metadata. + executionCount: 1 + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png,jpeg images', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage, + 'image/jpeg': base64EncodedImage + }, + metadata: {}, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [ + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png'), + new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/jpeg') + ], + { + executionCount: 1, + outputType: output_type, + metadata: {} // display_data & execute_result always have metadata. + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with a light background', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + needs_background: 'light' + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'light' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with a dark background', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + needs_background: 'dark' + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + needs_background: 'dark' + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png image with custom dimensions', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + 'image/png': { height: '111px', width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + + test('png allowed to scroll', async () => { + validateCellOutputTranslation( + [ + { + execution_count: 1, + data: { + 'image/png': base64EncodedImage + }, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + output_type + } + ], + [ + new vscode.NotebookCellOutput( + [new vscode.NotebookCellOutputItem(Buffer.from(base64EncodedImage, 'base64'), 'image/png')], + { + executionCount: 1, + metadata: { + unconfined: true, + 'image/png': { width: '999px' } + }, + outputType: output_type + } + ) + ], + propertiesToExcludeFromComparison + ); + }); + }); + }); + }); + + suite('Output Order', () => { + test('Verify order of outputs', async () => { + const dataAndExpectedOrder: { output: nbformat.IDisplayData; expectedMimeTypesOrder: string[] }[] = [ + { + output: { + data: { + 'application/vnd.vegalite.v4+json': 'some json', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['application/vnd.vegalite.v4+json', 'text/html'] + }, + { + output: { + data: { + 'application/vnd.vegalite.v4+json': 'some json', + 'application/javascript': 'some js', + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: [ + 'application/vnd.vegalite.v4+json', + 'text/html', + 'application/javascript', + 'text/plain' + ] + }, + { + output: { + data: { + 'application/vnd.vegalite.v4+json': '', // Empty, should give preference to other mimetypes. + 'application/javascript': 'some js', + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: [ + 'text/html', + 'application/javascript', + 'text/plain', + 'application/vnd.vegalite.v4+json' + ] + }, + { + output: { + data: { + 'text/plain': 'some text', + 'text/html': 'Hello' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['text/html', 'text/plain'] + }, + { + output: { + data: { + 'application/javascript': 'some js', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['application/javascript', 'text/plain'] + }, + { + output: { + data: { + 'image/svg+xml': 'some svg', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['image/svg+xml', 'text/plain'] + }, + { + output: { + data: { + 'text/latex': 'some latex', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['text/latex', 'text/plain'] + }, + { + output: { + data: { + 'application/vnd.jupyter.widget-view+json': 'some widget', + 'text/plain': 'some text' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['application/vnd.jupyter.widget-view+json', 'text/plain'] + }, + { + output: { + data: { + 'text/plain': 'some text', + 'image/svg+xml': 'some svg', + 'image/png': 'some png' + }, + metadata: {}, + output_type: 'display_data' + }, + expectedMimeTypesOrder: ['image/png', 'image/svg+xml', 'text/plain'] + } + ]; + + dataAndExpectedOrder.forEach(({ output, expectedMimeTypesOrder }) => { + const sortedOutputs = jupyterCellOutputToCellOutput(output); + const mimeTypes = sortedOutputs.items.map((item) => item.mime).join(','); + assert.equal(mimeTypes, expectedMimeTypesOrder.join(',')); + }); + }); + }); +}); diff --git a/extensions/ipynb/src/types.d.ts b/extensions/ipynb/src/types.d.ts new file mode 100644 index 0000000000..73424d54b5 --- /dev/null +++ b/extensions/ipynb/src/types.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module '@enonic/fnv-plus'; diff --git a/extensions/ipynb/tsconfig.json b/extensions/ipynb/tsconfig.json new file mode 100644 index 0000000000..178e86493b --- /dev/null +++ b/extensions/ipynb/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "lib": [ + "dom" + ] + }, + "include": [ + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.notebookEditor.d.ts", + "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts" + ] +} diff --git a/extensions/ipynb/yarn.lock b/extensions/ipynb/yarn.lock new file mode 100644 index 0000000000..c932570ecc --- /dev/null +++ b/extensions/ipynb/yarn.lock @@ -0,0 +1,35 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@enonic/fnv-plus@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@enonic/fnv-plus/-/fnv-plus-1.3.0.tgz#be65a7b128a3b544f60aea3ef978d938e85869f3" + integrity sha512-BCN9uNWH8AmiP7BXBJqEinUY9KXalmRzo+L0cB/mQsmFfzODxwQrbvxCHXUNH2iP+qKkWYtB4vyy8N62PViMFw== + +"@jupyterlab/nbformat@^3.2.9": + version "3.2.9" + resolved "https://registry.yarnpkg.com/@jupyterlab/nbformat/-/nbformat-3.2.9.tgz#e7d854719612133498af4280d9a8caa0873205b0" + integrity sha512-WSf9OQo8yfFjyodbXRdFoaNwMkaAL5jFZiD6V2f8HqI380ipansWrrV7R9CGzPfgKHpUGZMO1tYKmUwzMhvZ4w== + dependencies: + "@lumino/coreutils" "^1.5.3" + +"@lumino/coreutils@^1.5.3": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@lumino/coreutils/-/coreutils-1.12.0.tgz#fbdef760f736eaf2bd396a5c6fc3a68a4b449b15" + integrity sha512-DSglh4ylmLi820CNx9soJmDJCpUgymckdWeGWuN0Ash5g60oQvrQDfosVxEhzmNvtvXv45WZEqSBzDP6E5SEmQ== + +"@types/uuid@^8.3.1": + version "8.3.1" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f" + integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg== + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index cda087c904..28064befaa 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -67,6 +67,11 @@ "category": "MSSQL", "title": "%title.designTable%" }, + { + "command": "mssql.changeNotebookConnection", + "category": "MSSQL", + "title": "%title.changeNotebookConnection%" + }, { "command": "mssql.newLogin", "category": "MSSQL", @@ -418,6 +423,10 @@ "command": "mssql.designTable", "when": "false" }, + { + "command": "mssql.changeNotebookConnection", + "when": "false" + }, { "command": "mssql.newServerRole", "when": "false" diff --git a/extensions/mssql/package.nls.json b/extensions/mssql/package.nls.json index 8a9b70468a..9bfc2521be 100644 --- a/extensions/mssql/package.nls.json +++ b/extensions/mssql/package.nls.json @@ -176,6 +176,7 @@ "objectsListProperties.name": "Name", "title.newTable": "New Table", "title.designTable": "Design", + "title.changeNotebookConnection": "Change SQL Notebook Connection", "mssql.parallelMessageProcessing" : "[Experimental] Whether the requests to the SQL Tools Service should be handled in parallel. This is introduced to discover the issues there might be when handling all requests in parallel. The default value is false. Relaunch of ADS is required when the value is changed.", "mssql.tableDesigner.preloadDatabaseModel": "Whether to preload the database model when the database node in the object explorer is expanded. When enabled, the loading time of table designer can be reduced. Note: You might see higher than normal memory usage if you need to expand a lot of database nodes.", "mssql.objectExplorer.groupBySchema": "When enabled, the database objects in Object Explorer will be categorized by schema.", diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 1dc101c905..8360595ebc 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -22,6 +22,7 @@ import { IconPathHelper } from './iconHelper'; import * as nls from 'vscode-nls'; import { INotebookConvertService } from './notebookConvert/notebookConvertService'; import { registerTableDesignerCommands } from './tableDesigner/tableDesigner'; +import { SqlNotebookController } from './sqlNotebook/sqlNotebookController'; import { registerObjectManagementCommands } from './objectManagement/commands'; import { TelemetryActions, TelemetryReporter, TelemetryViews } from './telemetry'; @@ -114,7 +115,10 @@ export async function activate(context: vscode.ExtensionContext): Promise void +} + +interface QueryMessageHandler { + ownerUri: string; + handler: (results: azdata.QueryExecuteMessageParams) => void +} + +export class SqlNotebookController implements vscode.Disposable { + private readonly _cellUriScheme = 'vscode-notebook-cell'; + private readonly _connectionLabel = (serverName: string) => localize('notebookConnection', 'Connected to: {0}', serverName); + private readonly _disconnectedLabel = localize('notebookDisconnected', 'Disconnected'); + + private readonly _disposables = new Array(); + private readonly _controller: vscode.NotebookController; + private readonly _connectionsMap = new Map(); + private readonly _executionOrderMap = new Map(); + private readonly _queryProvider: azdata.QueryProvider; + private readonly _connProvider: azdata.ConnectionProvider; + private readonly _connectionLabelItem: vscode.StatusBarItem; + + private _queryCompleteHandler: QueryCompletionHandler; + private _queryMessageHandler: QueryMessageHandler; + private _activeCellUri: string; + + constructor() { + this._controller = vscode.notebooks.createNotebookController('sql-controller-id', 'jupyter-notebook', 'SQL'); + + this._controller.supportedLanguages = ['sql']; + this._controller.supportsExecutionOrder = true; + this._controller.executeHandler = this.execute.bind(this); + + const sqlProvider = 'MSSQL'; + this._queryProvider = azdata.dataprotocol.getProvider(sqlProvider, azdata.DataProviderType.QueryProvider); + this._queryProvider.registerOnQueryComplete(result => this.handleQueryComplete(result)); + this._queryProvider.registerOnMessage(message => this.handleQueryMessage(message)); + + this._connProvider = azdata.dataprotocol.getProvider(sqlProvider, azdata.DataProviderType.ConnectionProvider); + + const commandName = 'mssql.changeNotebookConnection'; + let changeConnectionCommand = vscode.commands.registerCommand(commandName, async () => await this.changeConnection()); + this._disposables.push(changeConnectionCommand); + + this._connectionLabelItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right); + this._connectionLabelItem.text = this._disconnectedLabel; + this._connectionLabelItem.tooltip = localize('changeNotebookConnection', 'Change SQL Notebook Connection'); + this._connectionLabelItem.command = commandName; + this._disposables.push(this._connectionLabelItem); + + // Show connection status if there's a notebook already open when ADS starts + if (vscode.window.activeTextEditor?.document.notebook) { + this._connectionLabelItem.show(); + } + + let editorChangedEvent = vscode.window.onDidChangeActiveTextEditor(async editor => await this.handleActiveEditorChanged(editor)); + this._disposables.push(editorChangedEvent); + + let docClosedEvent = vscode.workspace.onDidCloseTextDocument(document => this.handleDocumentClosed(document)); + this._disposables.push(docClosedEvent); + } + + private handleQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void { + if (this._queryCompleteHandler && this._queryCompleteHandler.ownerUri === result.ownerUri) { // Check if handler is undefined separately in case the result URI is also undefined + this._queryCompleteHandler.handler(result.batchSummaries); + } + } + + private handleQueryMessage(message: azdata.QueryExecuteMessageParams): void { + if (this._queryMessageHandler && this._queryMessageHandler.ownerUri === message.ownerUri) { // Check if handler is undefined separately in case the result URI is also undefined + this._queryMessageHandler.handler(message); + } + } + + private async handleActiveEditorChanged(editor: vscode.TextEditor): Promise { + let notebook = editor?.document.notebook; + if (!notebook) { + // Hide status bar item if the current editor isn't a notebook + this._connectionLabelItem.hide(); + } else { + let connection = this._connectionsMap.get(notebook.uri); + if (connection) { + this._connectionLabelItem.text = this._connectionLabel(connection.options['server']); + + // If this editor is for a cell, then update the connection for it + this.updateCellConnection(notebook.uri, connection); + } else { + this._connectionLabelItem.text = this._disconnectedLabel; + } + this._connectionLabelItem.show(); + } + } + + public getConnectionProfile(connection: azdata.connection.Connection): azdata.IConnectionProfile { + let connectionProfile: azdata.IConnectionProfile = { + connectionName: connection.options.connectionName, + serverName: connection.options.server, + databaseName: connection.options.database, + userName: connection.options.user, + password: connection.options.password, + authenticationType: connection.options.authenticationType, + savePassword: connection.options.savePassword, + groupFullName: undefined, + groupId: undefined, + providerName: connection.providerName, + saveProfile: false, + id: connection.connectionId, + options: connection.options + }; + return connectionProfile; + } + + private handleDocumentClosed(editor: vscode.TextDocument): void { + // Have to check isClosed here since this event is also emitted on doc language changes + if (editor.notebook && editor.isClosed) { + // Remove the connection & execution associations if the doc is closed, but don't close the connection since it might be re-used elsewhere + this._connectionsMap.delete(editor.notebook.uri); + this._executionOrderMap.delete(editor.notebook.uri); + } + } + + private updateCellConnection(notebookUri: vscode.Uri, connection: azdata.connection.Connection): void { + let docUri = vscode.window.activeTextEditor?.document.uri; + if (docUri?.scheme === this._cellUriScheme && docUri?.path === notebookUri.path) { + if (this._activeCellUri) { + this._connProvider.disconnect(this._activeCellUri).then(() => undefined, error => console.log(error)); + } + this._activeCellUri = docUri.toString(); + // Delay connecting in case user is clicking between cells a lot + setTimeout(() => { + if (this._activeCellUri === docUri.toString()) { + let profile = this.getConnectionProfile(connection); + this._connProvider.connect(docUri.toString(), profile).then( + connected => { + if (!connected) { + console.log(`Failed to update cell connection for cell: ${docUri.toString()}`); + } + }, + error => { + console.log(error); + }); + } + }, 200); + } + } + + private async changeConnection(notebook?: vscode.NotebookDocument): Promise { + let connection: azdata.connection.Connection; + let notebookUri = notebook?.uri ?? vscode.window.activeTextEditor?.document.notebook?.uri; + if (notebookUri) { + connection = await azdata.connection.openConnectionDialog(['MSSQL']); + if (connection) { + this._connectionsMap.set(notebookUri, connection); + this._connectionLabelItem.text = this._connectionLabel(connection.options['server']); + + // Connect current notebook cell, if there is one + this.updateCellConnection(notebookUri, connection); + } else { + this._connectionLabelItem.text = this._disconnectedLabel; + } + this._connectionLabelItem.show(); + } + return connection; + } + + private async execute(cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController): Promise { + if (this._queryCompleteHandler) { + throw new Error(localize('queryInProgressError', 'Another query is currently in progress. Please wait for that query to complete before running these cells.')); + } + + let connection = this._connectionsMap.get(notebook.uri); + if (!connection) { + connection = await this.changeConnection(notebook); + } + + let executionOrder = this._executionOrderMap.get(notebook.uri) ?? 0; + for (let cell of cells) { + await this.doExecution(cell, connection, ++executionOrder); + } + this._executionOrderMap.set(notebook.uri, executionOrder); + } + + private async doExecution(cell: vscode.NotebookCell, connection: azdata.connection.Connection | undefined, executionOrder: number): Promise { + const execution = this._controller.createNotebookCellExecution(cell); + execution.executionOrder = executionOrder; + execution.start(Date.now()); + await execution.clearOutput(); + if (!connection) { + await execution.appendOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(localize('noConnectionError', 'No connection provided.')) + ]) + ]); + execution.end(false, Date.now()); + return; + } + + let cancelHandler: vscode.Disposable; + try { + const ownerUri = await azdata.connection.getUriForConnection(connection.connectionId); + await this._queryProvider.runQueryString(ownerUri, cell.document.getText()); + cancelHandler = execution.token.onCancellationRequested(async () => await this._queryProvider.cancelQuery(ownerUri)); + + let queryComplete = new Promise(resolve => { + let queryCompleteHandler = async (batchSummaries: azdata.BatchSummary[]) => { + let tableHtmlEntries: string[] = []; + for (let batchSummary of batchSummaries) { + if (execution.token.isCancellationRequested) { + break; + } + + for (let resultSummary of batchSummary.resultSetSummaries) { + if (execution.token.isCancellationRequested) { + break; + } + + if (resultSummary.rowCount > 0) { + // Add column headers + let tableHtml = + ` + `; + for (let column of resultSummary.columnInfo) { + tableHtml += ``; + } + tableHtml += ''; + + // Add rows and cells + let subsetResult = await this._queryProvider.getQueryRows({ + ownerUri: ownerUri, + batchIndex: batchSummary.id, + resultSetIndex: resultSummary.id, + rowsStartIndex: 0, + rowsCount: resultSummary.rowCount + }); + tableHtml += ''; + for (let row of subsetResult.resultSubset.rows) { + tableHtml += ''; + for (let cell of row) { + tableHtml += ``; + } + tableHtml += ''; + } + tableHtml += '
${htmlEscape(column.columnName)}
${htmlEscape(cell.displayValue)}
'; + tableHtmlEntries.push(tableHtml); + } + } + } + + if (execution.token.isCancellationRequested) { + await execution.appendOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(localize('cellExecutionCancelled', 'Cell execution cancelled.')) + ]) + ]); + execution.end(false, Date.now()); + } else { + await execution.appendOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(tableHtmlEntries.join('

'), 'text/html') + ]) + ]); + execution.end(true, Date.now()); + } + resolve(); + }; + this._queryCompleteHandler = { ownerUri: ownerUri, handler: queryCompleteHandler }; + }); + + this._queryMessageHandler = { + ownerUri: ownerUri, + handler: async message => { + await execution.appendOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.text(message.message.message) + ]) + ]); + } + }; + + await queryComplete; + } catch (error) { + await execution.appendOutput([ + new vscode.NotebookCellOutput([ + vscode.NotebookCellOutputItem.error(error) + ]) + ]); + execution.end(false, Date.now()); + } finally { + if (cancelHandler) { + cancelHandler.dispose(); + } + this._queryCompleteHandler = undefined; + this._queryMessageHandler = undefined; + } + } + + dispose() { + this._disposables.forEach(d => d.dispose()); + } +} + +function htmlEscape(html: string): string { + return html.replace(/[<|>|&|"]/g, function (match) { + switch (match) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + case '"': return '"'; + case '\'': return '''; + default: return match; + } + }); +} diff --git a/extensions/mssql/src/typings/refs.d.ts b/extensions/mssql/src/typings/refs.d.ts index 062ae88d5c..5b9c233d88 100644 --- a/extensions/mssql/src/typings/refs.d.ts +++ b/extensions/mssql/src/typings/refs.d.ts @@ -6,4 +6,5 @@ /// /// /// +/// /// diff --git a/extensions/notebook-renderers/.gitignore b/extensions/notebook-renderers/.gitignore new file mode 100644 index 0000000000..e81e8443dd --- /dev/null +++ b/extensions/notebook-renderers/.gitignore @@ -0,0 +1 @@ +renderer-out diff --git a/extensions/notebook-renderers/.vscodeignore b/extensions/notebook-renderers/.vscodeignore new file mode 100644 index 0000000000..e168400f68 --- /dev/null +++ b/extensions/notebook-renderers/.vscodeignore @@ -0,0 +1,6 @@ +src/** +notebook/** +tsconfig.json +.gitignore +esbuild.js +src/** diff --git a/extensions/notebook-renderers/README.md b/extensions/notebook-renderers/README.md new file mode 100644 index 0000000000..c21a357435 --- /dev/null +++ b/extensions/notebook-renderers/README.md @@ -0,0 +1,9 @@ +# Builtin Notebook Output Renderers for Azure Data Studio + +**Notice:** This extension is bundled with Azure Data Studio. It can be disabled but not uninstalled. + +## Features + +This extension provides the following notebook renderers for Azure Data Studio: + +- Image renderer for png, jpeg and gif diff --git a/extensions/notebook-renderers/esbuild.js b/extensions/notebook-renderers/esbuild.js new file mode 100644 index 0000000000..b80133933a --- /dev/null +++ b/extensions/notebook-renderers/esbuild.js @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +const path = require('path'); +const esbuild = require('esbuild'); + +const args = process.argv.slice(2); + +const isWatch = args.indexOf('--watch') >= 0; + +let outputRoot = __dirname; +const outputRootIndex = args.indexOf('--outputRoot'); +if (outputRootIndex >= 0) { + outputRoot = args[outputRootIndex + 1]; +} + +const srcDir = path.join(__dirname, 'src'); +const outDir = path.join(outputRoot, 'renderer-out'); + +function build() { + return esbuild.build({ + entryPoints: [ + path.join(srcDir, 'index.ts'), + ], + bundle: true, + minify: false, + sourcemap: false, + format: 'esm', + outdir: outDir, + platform: 'browser', + target: ['es2020'], + }); +} + +build().catch(() => process.exit(1)); + +if (isWatch) { + const watcher = require('@parcel/watcher'); + watcher.subscribe(srcDir, () => { + return build(); + }); +} diff --git a/extensions/notebook-renderers/package.json b/extensions/notebook-renderers/package.json new file mode 100644 index 0000000000..15b6751e20 --- /dev/null +++ b/extensions/notebook-renderers/package.json @@ -0,0 +1,57 @@ +{ + "name": "builtin-notebook-renderers", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "version": "1.0.0", + "license": "MIT", + "engines": { + "vscode": "^1.57.0" + }, + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "notebookRenderer": [ + { + "id": "vscode-builtin-notebook-renderer", + "entrypoint": "./renderer-out/index.js", + "displayName": "VS Code Builtin Notebook Output Renderer", + "requiresMessaging": "never", + "mimeTypes": [ + "image/gif", + "image/png", + "image/jpeg", + "image/git", + "image/svg+xml", + "text/html", + "application/javascript", + "application/vnd.code.notebook.error", + "application/vnd.code.notebook.stdout", + "application/x.notebook.stdout", + "application/x.notebook.stream", + "application/vnd.code.notebook.stderr", + "application/x.notebook.stderr", + "text/plain" + ] + } + ] + }, + "scripts": { + "compile": "npm run build-notebook", + "watch": "node ./esbuild --watch", + "build-notebook": "node ./esbuild" + }, + "dependencies": { + }, + "devDependencies": { + "@types/vscode-notebook-renderer": "^1.60.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/vscode.git" + } +} diff --git a/extensions/notebook-renderers/package.nls.json b/extensions/notebook-renderers/package.nls.json new file mode 100644 index 0000000000..70b82dbe8c --- /dev/null +++ b/extensions/notebook-renderers/package.nls.json @@ -0,0 +1,4 @@ +{ + "displayName": "Builtin Notebook Output Renderers", + "description": "Provides basic output renderers for notebooks" +} diff --git a/extensions/notebook-renderers/src/ansi.ts b/extensions/notebook-renderers/src/ansi.ts new file mode 100644 index 0000000000..760d50c614 --- /dev/null +++ b/extensions/notebook-renderers/src/ansi.ts @@ -0,0 +1,447 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RGBA, Color } from './color'; +import { ansiColorIdentifiers } from './colorMap'; +import { linkify } from './linkify'; + + +export function handleANSIOutput(text: string): HTMLSpanElement { + let workspaceFolder = undefined; + + const root: HTMLSpanElement = document.createElement('span'); + const textLength: number = text.length; + + let styleNames: string[] = []; + let customFgColor: RGBA | string | undefined; + let customBgColor: RGBA | string | undefined; + let customUnderlineColor: RGBA | string | undefined; + let colorsInverted: boolean = false; + let currentPos: number = 0; + let buffer: string = ''; + + while (currentPos < textLength) { + + let sequenceFound: boolean = false; + + // Potentially an ANSI escape sequence. + // See http://ascii-table.com/ansi-escape-sequences.php & https://en.wikipedia.org/wiki/ANSI_escape_code + if (text.charCodeAt(currentPos) === 27 && text.charAt(currentPos + 1) === '[') { + + const startPos: number = currentPos; + currentPos += 2; // Ignore 'Esc[' as it's in every sequence. + + let ansiSequence: string = ''; + + while (currentPos < textLength) { + const char: string = text.charAt(currentPos); + ansiSequence += char; + + currentPos++; + + // Look for a known sequence terminating character. + if (char.match(/^[ABCDHIJKfhmpsu]$/)) { + sequenceFound = true; + break; + } + + } + + if (sequenceFound) { + + // Flush buffer with previous styles. + appendStylizedStringToContainer(root, buffer, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor); + + buffer = ''; + + /* + * Certain ranges that are matched here do not contain real graphics rendition sequences. For + * the sake of having a simpler expression, they have been included anyway. + */ + if (ansiSequence.match(/^(?:[34][0-8]|9[0-7]|10[0-7]|[0-9]|2[1-5,7-9]|[34]9|5[8,9]|1[0-9])(?:;[349][0-7]|10[0-7]|[013]|[245]|[34]9)?(?:;[012]?[0-9]?[0-9])*;?m$/)) { + + const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character. + .split(';') // Separate style codes. + .filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', '']. + .map(elem => parseInt(elem, 10)); // Convert to numbers. + + if (styleCodes[0] === 38 || styleCodes[0] === 48 || styleCodes[0] === 58) { + // Advanced color code - can't be combined with formatting codes like simple colors can + // Ignores invalid colors and additional info beyond what is necessary + const colorType = (styleCodes[0] === 38) ? 'foreground' : ((styleCodes[0] === 48) ? 'background' : 'underline'); + + if (styleCodes[1] === 5) { + set8BitColor(styleCodes, colorType); + } else if (styleCodes[1] === 2) { + set24BitColor(styleCodes, colorType); + } + } else { + setBasicFormatters(styleCodes); + } + + } else { + // Unsupported sequence so simply hide it. + } + + } else { + currentPos = startPos; + } + } + + if (sequenceFound === false) { + buffer += text.charAt(currentPos); + currentPos++; + } + } + + // Flush remaining text buffer if not empty. + if (buffer) { + appendStylizedStringToContainer(root, buffer, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor); + } + + return root; + + /** + * Change the foreground or background color by clearing the current color + * and adding the new one. + * @param colorType If `'foreground'`, will change the foreground color, if + * `'background'`, will change the background color, and if `'underline'` + * will set the underline color. + * @param color Color to change to. If `undefined` or not provided, + * will clear current color without adding a new one. + */ + function changeColor(colorType: 'foreground' | 'background' | 'underline', color?: RGBA | string | undefined): void { + if (colorType === 'foreground') { + customFgColor = color; + } else if (colorType === 'background') { + customBgColor = color; + } else if (colorType === 'underline') { + customUnderlineColor = color; + } + styleNames = styleNames.filter(style => style !== `code-${colorType}-colored`); + if (color !== undefined) { + styleNames.push(`code-${colorType}-colored`); + } + } + + /** + * Swap foreground and background colors. Used for color inversion. Caller should check + * [] flag to make sure it is appropriate to turn ON or OFF (if it is already inverted don't call + */ + function reverseForegroundAndBackgroundColors(): void { + const oldFgColor: RGBA | string | undefined = customFgColor; + changeColor('foreground', customBgColor); + changeColor('background', oldFgColor); + } + + /** + * Calculate and set basic ANSI formatting. Supports ON/OFF of bold, italic, underline, + * double underline, crossed-out/strikethrough, overline, dim, blink, rapid blink, + * reverse/invert video, hidden, superscript, subscript and alternate font codes, + * clearing/resetting of foreground, background and underline colors, + * setting normal foreground and background colors, and bright foreground and + * background colors. Not to be used for codes containing advanced colors. + * Will ignore invalid codes. + * @param styleCodes Array of ANSI basic styling numbers, which will be + * applied in order. New colors and backgrounds clear old ones; new formatting + * does not. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#SGR } + */ + function setBasicFormatters(styleCodes: number[]): void { + for (const code of styleCodes) { + switch (code) { + case 0: { // reset (everything) + styleNames = []; + customFgColor = undefined; + customBgColor = undefined; + break; + } + case 1: { // bold + styleNames = styleNames.filter(style => style !== `code-bold`); + styleNames.push('code-bold'); + break; + } + case 2: { // dim + styleNames = styleNames.filter(style => style !== `code-dim`); + styleNames.push('code-dim'); + break; + } + case 3: { // italic + styleNames = styleNames.filter(style => style !== `code-italic`); + styleNames.push('code-italic'); + break; + } + case 4: { // underline + styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`)); + styleNames.push('code-underline'); + break; + } + case 5: { // blink + styleNames = styleNames.filter(style => style !== `code-blink`); + styleNames.push('code-blink'); + break; + } + case 6: { // rapid blink + styleNames = styleNames.filter(style => style !== `code-rapid-blink`); + styleNames.push('code-rapid-blink'); + break; + } + case 7: { // invert foreground and background + if (!colorsInverted) { + colorsInverted = true; + reverseForegroundAndBackgroundColors(); + } + break; + } + case 8: { // hidden + styleNames = styleNames.filter(style => style !== `code-hidden`); + styleNames.push('code-hidden'); + break; + } + case 9: { // strike-through/crossed-out + styleNames = styleNames.filter(style => style !== `code-strike-through`); + styleNames.push('code-strike-through'); + break; + } + case 10: { // normal default font + styleNames = styleNames.filter(style => !style.startsWith('code-font')); + break; + } + case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: { // font codes (and 20 is 'blackletter' font code) + styleNames = styleNames.filter(style => !style.startsWith('code-font')); + styleNames.push(`code-font-${code - 10}`); + break; + } + case 21: { // double underline + styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`)); + styleNames.push('code-double-underline'); + break; + } + case 22: { // normal intensity (bold off and dim off) + styleNames = styleNames.filter(style => (style !== `code-bold` && style !== `code-dim`)); + break; + } + case 23: { // Neither italic or blackletter (font 10) + styleNames = styleNames.filter(style => (style !== `code-italic` && style !== `code-font-10`)); + break; + } + case 24: { // not underlined (Neither singly nor doubly underlined) + styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`)); + break; + } + case 25: { // not blinking + styleNames = styleNames.filter(style => (style !== `code-blink` && style !== `code-rapid-blink`)); + break; + } + case 27: { // not reversed/inverted + if (colorsInverted) { + colorsInverted = false; + reverseForegroundAndBackgroundColors(); + } + break; + } + case 28: { // not hidden (reveal) + styleNames = styleNames.filter(style => style !== `code-hidden`); + break; + } + case 29: { // not crossed-out + styleNames = styleNames.filter(style => style !== `code-strike-through`); + break; + } + case 53: { // overlined + styleNames = styleNames.filter(style => style !== `code-overline`); + styleNames.push('code-overline'); + break; + } + case 55: { // not overlined + styleNames = styleNames.filter(style => style !== `code-overline`); + break; + } + case 39: { // default foreground color + changeColor('foreground', undefined); + break; + } + case 49: { // default background color + changeColor('background', undefined); + break; + } + case 59: { // default underline color + changeColor('underline', undefined); + break; + } + case 73: { // superscript + styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`)); + styleNames.push('code-superscript'); + break; + } + case 74: { // subscript + styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`)); + styleNames.push('code-subscript'); + break; + } + case 75: { // neither superscript or subscript + styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`)); + break; + } + default: { + setBasicColor(code); + break; + } + } + } + } + + /** + * Calculate and set styling for complicated 24-bit ANSI color codes. + * @param styleCodes Full list of integer codes that make up the full ANSI + * sequence, including the two defining codes and the three RGB codes. + * @param colorType If `'foreground'`, will set foreground color, if + * `'background'`, will set background color, and if it is `'underline'` + * will set the underline color. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit } + */ + function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void { + if (styleCodes.length >= 5 && + styleCodes[2] >= 0 && styleCodes[2] <= 255 && + styleCodes[3] >= 0 && styleCodes[3] <= 255 && + styleCodes[4] >= 0 && styleCodes[4] <= 255) { + const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]); + changeColor(colorType, customColor); + } + } + + /** + * Calculate and set styling for advanced 8-bit ANSI color codes. + * @param styleCodes Full list of integer codes that make up the ANSI + * sequence, including the two defining codes and the one color code. + * @param colorType If `'foreground'`, will set foreground color, if + * `'background'`, will set background color and if it is `'underline'` + * will set the underline color. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } + */ + function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void { + let colorNumber = styleCodes[2]; + const color = calcANSI8bitColor(colorNumber); + + if (color) { + changeColor(colorType, color); + } else if (colorNumber >= 0 && colorNumber <= 15) { + if (colorType === 'underline') { + // for underline colors we just decode the 0-15 color number to theme color, set and return + changeColor(colorType, ansiColorIdentifiers[colorNumber].colorValue); + return; + } + // Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107) + colorNumber += 30; + if (colorNumber >= 38) { + // Bright colors + colorNumber += 52; + } + if (colorType === 'background') { + colorNumber += 10; + } + setBasicColor(colorNumber); + } + } + + /** + * Calculate and set styling for basic bright and dark ANSI color codes. Uses + * theme colors if available. Automatically distinguishes between foreground + * and background colors; does not support color-clearing codes 39 and 49. + * @param styleCode Integer color code on one of the following ranges: + * [30-37, 90-97, 40-47, 100-107]. If not on one of these ranges, will do + * nothing. + */ + function setBasicColor(styleCode: number): void { + // const theme = themeService.getColorTheme(); + let colorType: 'foreground' | 'background' | undefined; + let colorIndex: number | undefined; + + if (styleCode >= 30 && styleCode <= 37) { + colorIndex = styleCode - 30; + colorType = 'foreground'; + } else if (styleCode >= 90 && styleCode <= 97) { + colorIndex = (styleCode - 90) + 8; // High-intensity (bright) + colorType = 'foreground'; + } else if (styleCode >= 40 && styleCode <= 47) { + colorIndex = styleCode - 40; + colorType = 'background'; + } else if (styleCode >= 100 && styleCode <= 107) { + colorIndex = (styleCode - 100) + 8; // High-intensity (bright) + colorType = 'background'; + } + + if (colorIndex !== undefined && colorType) { + changeColor(colorType, ansiColorIdentifiers[colorIndex]?.colorValue); + } + } +} + +export function appendStylizedStringToContainer( + root: HTMLElement, + stringContent: string, + cssClasses: string[], + workspaceFolder: string | undefined, + customTextColor?: RGBA | string, + customBackgroundColor?: RGBA | string, + customUnderlineColor?: RGBA | string +): void { + if (!root || !stringContent) { + return; + } + + const container = linkify(stringContent, true, workspaceFolder); + + container.className = cssClasses.join(' '); + if (customTextColor) { + container.style.color = typeof customTextColor === 'string' ? customTextColor : Color.Format.CSS.formatRGB(new Color(customTextColor)); + } + if (customBackgroundColor) { + container.style.backgroundColor = typeof customBackgroundColor === 'string' ? customBackgroundColor : Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); + } + if (customUnderlineColor) { + container.style.textDecorationColor = typeof customUnderlineColor === 'string' ? customUnderlineColor : Color.Format.CSS.formatRGB(new Color(customUnderlineColor)); + } + root.appendChild(container); +} + +/** + * Calculate the color from the color set defined in the ANSI 8-bit standard. + * Standard and high intensity colors are not defined in the standard as specific + * colors, so these and invalid colors return `undefined`. + * @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info. + * @param colorNumber The number (ranging from 16 to 255) referring to the color + * desired. + */ +export function calcANSI8bitColor(colorNumber: number): RGBA | undefined { + if (colorNumber % 1 !== 0) { + // Should be integer + return; + } if (colorNumber >= 16 && colorNumber <= 231) { + // Converts to one of 216 RGB colors + colorNumber -= 16; + + let blue: number = colorNumber % 6; + colorNumber = (colorNumber - blue) / 6; + let green: number = colorNumber % 6; + colorNumber = (colorNumber - green) / 6; + let red: number = colorNumber; + + // red, green, blue now range on [0, 5], need to map to [0,255] + const convFactor: number = 255 / 5; + blue = Math.round(blue * convFactor); + green = Math.round(green * convFactor); + red = Math.round(red * convFactor); + + return new RGBA(red, green, blue); + } else if (colorNumber >= 232 && colorNumber <= 255) { + // Converts to a grayscale value + colorNumber -= 232; + const colorLevel: number = Math.round(colorNumber / 23 * 255); + return new RGBA(colorLevel, colorLevel, colorLevel); + } else { + return; + } +} diff --git a/extensions/notebook-renderers/src/color.ts b/extensions/notebook-renderers/src/color.ts new file mode 100644 index 0000000000..60784226e0 --- /dev/null +++ b/extensions/notebook-renderers/src/color.ts @@ -0,0 +1,1055 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030C, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031B, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033D, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS + U_MACRON = 0x00AF, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00B8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02D8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE + U_OGONEK = 0x02DB, // U+02DB OGONEK + U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300C, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300D, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + + U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xFF1B, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xFF0C, // U+FF0C FULLWIDTH COMMA +} + +function roundFloat(number: number, decimalPoints: number): number { + const decimal = Math.pow(10, decimalPoints); + return Math.round(number * decimal) / decimal; +} + +export class RGBA { + _rgbaBrand: void = undefined; + + /** + * Red: integer in [0-255] + */ + readonly r: number; + + /** + * Green: integer in [0-255] + */ + readonly g: number; + + /** + * Blue: integer in [0-255] + */ + readonly b: number; + + /** + * Alpha: float in [0-1] + */ + readonly a: number; + + constructor(r: number, g: number, b: number, a: number = 1) { + this.r = Math.min(255, Math.max(0, r)) | 0; + this.g = Math.min(255, Math.max(0, g)) | 0; + this.b = Math.min(255, Math.max(0, b)) | 0; + this.a = roundFloat(Math.max(Math.min(1, a), 0), 3); + } + + static equals(a: RGBA, b: RGBA): boolean { + return a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a; + } +} + +export class HSLA { + + _hslaBrand: void = undefined; + + /** + * Hue: integer in [0, 360] + */ + readonly h: number; + + /** + * Saturation: float in [0, 1] + */ + readonly s: number; + + /** + * Luminosity: float in [0, 1] + */ + readonly l: number; + + /** + * Alpha: float in [0, 1] + */ + readonly a: number; + + constructor(h: number, s: number, l: number, a: number) { + this.h = Math.max(Math.min(360, h), 0) | 0; + this.s = roundFloat(Math.max(Math.min(1, s), 0), 3); + this.l = roundFloat(Math.max(Math.min(1, l), 0), 3); + this.a = roundFloat(Math.max(Math.min(1, a), 0), 3); + } + + static equals(a: HSLA, b: HSLA): boolean { + return a.h === b.h && a.s === b.s && a.l === b.l && a.a === b.a; + } + + /** + * Converts an RGB color value to HSL. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes r, g, and b are contained in the set [0, 255] and + * returns h in the set [0, 360], s, and l in the set [0, 1]. + */ + static fromRGBA(rgba: RGBA): HSLA { + const r = rgba.r / 255; + const g = rgba.g / 255; + const b = rgba.b / 255; + const a = rgba.a; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s = 0; + const l = (min + max) / 2; + const chroma = max - min; + + if (chroma > 0) { + s = Math.min((l <= 0.5 ? chroma / (2 * l) : chroma / (2 - (2 * l))), 1); + + switch (max) { + case r: h = (g - b) / chroma + (g < b ? 6 : 0); break; + case g: h = (b - r) / chroma + 2; break; + case b: h = (r - g) / chroma + 4; break; + } + + h *= 60; + h = Math.round(h); + } + return new HSLA(h, s, l, a); + } + + private static _hue2rgb(p: number, q: number, t: number): number { + if (t < 0) { + t += 1; + } + if (t > 1) { + t -= 1; + } + if (t < 1 / 6) { + return p + (q - p) * 6 * t; + } + if (t < 1 / 2) { + return q; + } + if (t < 2 / 3) { + return p + (q - p) * (2 / 3 - t) * 6; + } + return p; + } + + /** + * Converts an HSL color value to RGB. Conversion formula + * adapted from http://en.wikipedia.org/wiki/HSL_color_space. + * Assumes h in the set [0, 360] s, and l are contained in the set [0, 1] and + * returns r, g, and b in the set [0, 255]. + */ + static toRGBA(hsla: HSLA): RGBA { + const h = hsla.h / 360; + const { s, l, a } = hsla; + let r: number, g: number, b: number; + + if (s === 0) { + r = g = b = l; // achromatic + } else { + const q = l < 0.5 ? l * (1 + s) : l + s - l * s; + const p = 2 * l - q; + r = HSLA._hue2rgb(p, q, h + 1 / 3); + g = HSLA._hue2rgb(p, q, h); + b = HSLA._hue2rgb(p, q, h - 1 / 3); + } + + return new RGBA(Math.round(r * 255), Math.round(g * 255), Math.round(b * 255), a); + } +} + +export class HSVA { + + _hsvaBrand: void = undefined; + + /** + * Hue: integer in [0, 360] + */ + readonly h: number; + + /** + * Saturation: float in [0, 1] + */ + readonly s: number; + + /** + * Value: float in [0, 1] + */ + readonly v: number; + + /** + * Alpha: float in [0, 1] + */ + readonly a: number; + + constructor(h: number, s: number, v: number, a: number) { + this.h = Math.max(Math.min(360, h), 0) | 0; + this.s = roundFloat(Math.max(Math.min(1, s), 0), 3); + this.v = roundFloat(Math.max(Math.min(1, v), 0), 3); + this.a = roundFloat(Math.max(Math.min(1, a), 0), 3); + } + + static equals(a: HSVA, b: HSVA): boolean { + return a.h === b.h && a.s === b.s && a.v === b.v && a.a === b.a; + } + + // from http://www.rapidtables.com/convert/color/rgb-to-hsv.htm + static fromRGBA(rgba: RGBA): HSVA { + const r = rgba.r / 255; + const g = rgba.g / 255; + const b = rgba.b / 255; + const cmax = Math.max(r, g, b); + const cmin = Math.min(r, g, b); + const delta = cmax - cmin; + const s = cmax === 0 ? 0 : (delta / cmax); + let m: number; + + if (delta === 0) { + m = 0; + } else if (cmax === r) { + m = ((((g - b) / delta) % 6) + 6) % 6; + } else if (cmax === g) { + m = ((b - r) / delta) + 2; + } else { + m = ((r - g) / delta) + 4; + } + + return new HSVA(Math.round(m * 60), s, cmax, rgba.a); + } + + // from http://www.rapidtables.com/convert/color/hsv-to-rgb.htm + static toRGBA(hsva: HSVA): RGBA { + const { h, s, v, a } = hsva; + const c = v * s; + const x = c * (1 - Math.abs((h / 60) % 2 - 1)); + const m = v - c; + let [r, g, b] = [0, 0, 0]; + + if (h < 60) { + r = c; + g = x; + } else if (h < 120) { + r = x; + g = c; + } else if (h < 180) { + g = c; + b = x; + } else if (h < 240) { + g = x; + b = c; + } else if (h < 300) { + r = x; + b = c; + } else if (h <= 360) { + r = c; + b = x; + } + + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + + return new RGBA(r, g, b, a); + } +} + +export class Color { + + static fromHex(hex: string): Color { + return Color.Format.CSS.parseHex(hex) || Color.red; + } + + readonly rgba: RGBA; + private _hsla?: HSLA; + get hsla(): HSLA { + if (this._hsla) { + return this._hsla; + } else { + return HSLA.fromRGBA(this.rgba); + } + } + + private _hsva?: HSVA; + get hsva(): HSVA { + if (this._hsva) { + return this._hsva; + } + return HSVA.fromRGBA(this.rgba); + } + + constructor(arg: RGBA | HSLA | HSVA) { + if (!arg) { + throw new Error('Color needs a value'); + } else if (arg instanceof RGBA) { + this.rgba = arg; + } else if (arg instanceof HSLA) { + this._hsla = arg; + this.rgba = HSLA.toRGBA(arg); + } else if (arg instanceof HSVA) { + this._hsva = arg; + this.rgba = HSVA.toRGBA(arg); + } else { + throw new Error('Invalid color ctor argument'); + } + } + + equals(other: Color | null): boolean { + return !!other && RGBA.equals(this.rgba, other.rgba) && HSLA.equals(this.hsla, other.hsla) && HSVA.equals(this.hsva, other.hsva); + } + + /** + * http://www.w3.org/TR/WCAG20/#relativeluminancedef + * Returns the number in the set [0, 1]. O => Darkest Black. 1 => Lightest white. + */ + getRelativeLuminance(): number { + const R = Color._relativeLuminanceForComponent(this.rgba.r); + const G = Color._relativeLuminanceForComponent(this.rgba.g); + const B = Color._relativeLuminanceForComponent(this.rgba.b); + const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B; + + return roundFloat(luminance, 4); + } + + private static _relativeLuminanceForComponent(color: number): number { + const c = color / 255; + return (c <= 0.03928) ? c / 12.92 : Math.pow(((c + 0.055) / 1.055), 2.4); + } + + /** + * http://www.w3.org/TR/WCAG20/#contrast-ratiodef + * Returns the contrast ration number in the set [1, 21]. + */ + getContrastRatio(another: Color): number { + const lum1 = this.getRelativeLuminance(); + const lum2 = another.getRelativeLuminance(); + return lum1 > lum2 ? (lum1 + 0.05) / (lum2 + 0.05) : (lum2 + 0.05) / (lum1 + 0.05); + } + + /** + * http://24ways.org/2010/calculating-color-contrast + * Return 'true' if darker color otherwise 'false' + */ + isDarker(): boolean { + const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000; + return yiq < 128; + } + + /** + * http://24ways.org/2010/calculating-color-contrast + * Return 'true' if lighter color otherwise 'false' + */ + isLighter(): boolean { + const yiq = (this.rgba.r * 299 + this.rgba.g * 587 + this.rgba.b * 114) / 1000; + return yiq >= 128; + } + + isLighterThan(another: Color): boolean { + const lum1 = this.getRelativeLuminance(); + const lum2 = another.getRelativeLuminance(); + return lum1 > lum2; + } + + isDarkerThan(another: Color): boolean { + const lum1 = this.getRelativeLuminance(); + const lum2 = another.getRelativeLuminance(); + return lum1 < lum2; + } + + lighten(factor: number): Color { + return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l + this.hsla.l * factor, this.hsla.a)); + } + + darken(factor: number): Color { + return new Color(new HSLA(this.hsla.h, this.hsla.s, this.hsla.l - this.hsla.l * factor, this.hsla.a)); + } + + transparent(factor: number): Color { + const { r, g, b, a } = this.rgba; + return new Color(new RGBA(r, g, b, a * factor)); + } + + isTransparent(): boolean { + return this.rgba.a === 0; + } + + isOpaque(): boolean { + return this.rgba.a === 1; + } + + opposite(): Color { + return new Color(new RGBA(255 - this.rgba.r, 255 - this.rgba.g, 255 - this.rgba.b, this.rgba.a)); + } + + blend(c: Color): Color { + const rgba = c.rgba; + + // Convert to 0..1 opacity + const thisA = this.rgba.a; + const colorA = rgba.a; + + const a = thisA + colorA * (1 - thisA); + if (a < 1e-6) { + return Color.transparent; + } + + const r = this.rgba.r * thisA / a + rgba.r * colorA * (1 - thisA) / a; + const g = this.rgba.g * thisA / a + rgba.g * colorA * (1 - thisA) / a; + const b = this.rgba.b * thisA / a + rgba.b * colorA * (1 - thisA) / a; + + return new Color(new RGBA(r, g, b, a)); + } + + makeOpaque(opaqueBackground: Color): Color { + if (this.isOpaque() || opaqueBackground.rgba.a !== 1) { + // only allow to blend onto a non-opaque color onto a opaque color + return this; + } + + const { r, g, b, a } = this.rgba; + + // https://stackoverflow.com/questions/12228548/finding-equivalent-color-with-opacity + return new Color(new RGBA( + opaqueBackground.rgba.r - a * (opaqueBackground.rgba.r - r), + opaqueBackground.rgba.g - a * (opaqueBackground.rgba.g - g), + opaqueBackground.rgba.b - a * (opaqueBackground.rgba.b - b), + 1 + )); + } + + flatten(...backgrounds: Color[]): Color { + const background = backgrounds.reduceRight((accumulator, color) => { + return Color._flatten(color, accumulator); + }); + return Color._flatten(this, background); + } + + private static _flatten(foreground: Color, background: Color) { + const backgroundAlpha = 1 - foreground.rgba.a; + return new Color(new RGBA( + backgroundAlpha * background.rgba.r + foreground.rgba.a * foreground.rgba.r, + backgroundAlpha * background.rgba.g + foreground.rgba.a * foreground.rgba.g, + backgroundAlpha * background.rgba.b + foreground.rgba.a * foreground.rgba.b + )); + } + + private _toString?: string; + toString(): string { + if (!this._toString) { + this._toString = Color.Format.CSS.format(this); + } + return this._toString; + } + + static getLighterColor(of: Color, relative: Color, factor?: number): Color { + if (of.isLighterThan(relative)) { + return of; + } + factor = factor ? factor : 0.5; + const lum1 = of.getRelativeLuminance(); + const lum2 = relative.getRelativeLuminance(); + factor = factor * (lum2 - lum1) / lum2; + return of.lighten(factor); + } + + static getDarkerColor(of: Color, relative: Color, factor?: number): Color { + if (of.isDarkerThan(relative)) { + return of; + } + factor = factor ? factor : 0.5; + const lum1 = of.getRelativeLuminance(); + const lum2 = relative.getRelativeLuminance(); + factor = factor * (lum1 - lum2) / lum1; + return of.darken(factor); + } + + static readonly white = new Color(new RGBA(255, 255, 255, 1)); + static readonly black = new Color(new RGBA(0, 0, 0, 1)); + static readonly red = new Color(new RGBA(255, 0, 0, 1)); + static readonly blue = new Color(new RGBA(0, 0, 255, 1)); + static readonly green = new Color(new RGBA(0, 255, 0, 1)); + static readonly cyan = new Color(new RGBA(0, 255, 255, 1)); + static readonly lightgrey = new Color(new RGBA(211, 211, 211, 1)); + static readonly transparent = new Color(new RGBA(0, 0, 0, 0)); +} + +export namespace Color { + export namespace Format { + export namespace CSS { + + export function formatRGB(color: Color): string { + if (color.rgba.a === 1) { + return `rgb(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b})`; + } + + return Color.Format.CSS.formatRGBA(color); + } + + export function formatRGBA(color: Color): string { + return `rgba(${color.rgba.r}, ${color.rgba.g}, ${color.rgba.b}, ${+(color.rgba.a).toFixed(2)})`; + } + + export function formatHSL(color: Color): string { + if (color.hsla.a === 1) { + return `hsl(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(2)}%)`; + } + + return Color.Format.CSS.formatHSLA(color); + } + + export function formatHSLA(color: Color): string { + return `hsla(${color.hsla.h}, ${(color.hsla.s * 100).toFixed(2)}%, ${(color.hsla.l * 100).toFixed(2)}%, ${color.hsla.a.toFixed(2)})`; + } + + function _toTwoDigitHex(n: number): string { + const r = n.toString(16); + return r.length !== 2 ? '0' + r : r; + } + + /** + * Formats the color as #RRGGBB + */ + export function formatHex(color: Color): string { + return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(color.rgba.b)}`; + } + + /** + * Formats the color as #RRGGBBAA + * If 'compact' is set, colors without transparancy will be printed as #RRGGBB + */ + export function formatHexA(color: Color, compact = false): string { + if (compact && color.rgba.a === 1) { + return Color.Format.CSS.formatHex(color); + } + + return `#${_toTwoDigitHex(color.rgba.r)}${_toTwoDigitHex(color.rgba.g)}${_toTwoDigitHex(color.rgba.b)}${_toTwoDigitHex(Math.round(color.rgba.a * 255))}`; + } + + /** + * The default format will use HEX if opaque and RGBA otherwise. + */ + export function format(color: Color): string { + if (color.isOpaque()) { + return Color.Format.CSS.formatHex(color); + } + + return Color.Format.CSS.formatRGBA(color); + } + + /** + * Converts an Hex color value to a Color. + * returns r, g, and b are contained in the set [0, 255] + * @param hex string (#RGB, #RGBA, #RRGGBB or #RRGGBBAA). + */ + export function parseHex(hex: string): Color | null { + const length = hex.length; + + if (length === 0) { + // Invalid color + return null; + } + + if (hex.charCodeAt(0) !== CharCode.Hash) { + // Does not begin with a # + return null; + } + + if (length === 7) { + // #RRGGBB format + const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2)); + const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4)); + const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6)); + return new Color(new RGBA(r, g, b, 1)); + } + + if (length === 9) { + // #RRGGBBAA format + const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2)); + const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4)); + const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6)); + const a = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8)); + return new Color(new RGBA(r, g, b, a / 255)); + } + + if (length === 4) { + // #RGB format + const r = _parseHexDigit(hex.charCodeAt(1)); + const g = _parseHexDigit(hex.charCodeAt(2)); + const b = _parseHexDigit(hex.charCodeAt(3)); + return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b)); + } + + if (length === 5) { + // #RGBA format + const r = _parseHexDigit(hex.charCodeAt(1)); + const g = _parseHexDigit(hex.charCodeAt(2)); + const b = _parseHexDigit(hex.charCodeAt(3)); + const a = _parseHexDigit(hex.charCodeAt(4)); + return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b, (16 * a + a) / 255)); + } + + // Invalid color + return null; + } + + function _parseHexDigit(charCode: CharCode): number { + switch (charCode) { + case CharCode.Digit0: return 0; + case CharCode.Digit1: return 1; + case CharCode.Digit2: return 2; + case CharCode.Digit3: return 3; + case CharCode.Digit4: return 4; + case CharCode.Digit5: return 5; + case CharCode.Digit6: return 6; + case CharCode.Digit7: return 7; + case CharCode.Digit8: return 8; + case CharCode.Digit9: return 9; + case CharCode.a: return 10; + case CharCode.A: return 10; + case CharCode.b: return 11; + case CharCode.B: return 11; + case CharCode.c: return 12; + case CharCode.C: return 12; + case CharCode.d: return 13; + case CharCode.D: return 13; + case CharCode.e: return 14; + case CharCode.E: return 14; + case CharCode.f: return 15; + case CharCode.F: return 15; + } + return 0; + } + } + } +} diff --git a/extensions/notebook-renderers/src/colorMap.ts b/extensions/notebook-renderers/src/colorMap.ts new file mode 100644 index 0000000000..a58e7ff0e9 --- /dev/null +++ b/extensions/notebook-renderers/src/colorMap.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export const ansiColorIdentifiers: { colorName: string; colorValue: string }[] = []; +export const ansiColorMap: { [key: string]: { index: number } } = { + 'terminal.ansiBlack': { + index: 0, + }, + 'terminal.ansiRed': { + index: 1, + }, + 'terminal.ansiGreen': { + index: 2, + }, + 'terminal.ansiYellow': { + index: 3, + }, + 'terminal.ansiBlue': { + index: 4, + }, + 'terminal.ansiMagenta': { + index: 5, + }, + 'terminal.ansiCyan': { + index: 6, + }, + 'terminal.ansiWhite': { + index: 7, + }, + 'terminal.ansiBrightBlack': { + index: 8, + }, + 'terminal.ansiBrightRed': { + index: 9, + }, + 'terminal.ansiBrightGreen': { + index: 10, + }, + 'terminal.ansiBrightYellow': { + index: 11, + }, + 'terminal.ansiBrightBlue': { + index: 12, + }, + 'terminal.ansiBrightMagenta': { + index: 13, + }, + 'terminal.ansiBrightCyan': { + index: 14, + }, + 'terminal.ansiBrightWhite': { + index: 15, + } +}; + +for (const id in ansiColorMap) { + const entry = ansiColorMap[id]; + const colorName = id.substring(13); + ansiColorIdentifiers[entry.index] = { colorName, colorValue: 'var(--vscode-' + id.replace('.', '-') + ')' }; +} diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts new file mode 100644 index 0000000000..ae457fcaec --- /dev/null +++ b/extensions/notebook-renderers/src/index.ts @@ -0,0 +1,281 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer'; +import { truncatedArrayOfString } from './textHelper'; + +interface IDisposable { + dispose(): void; +} + +function clearContainer(container: HTMLElement) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } +} + + +function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable { + const blob = new Blob([outputInfo.data()], { type: outputInfo.mime }); + const src = URL.createObjectURL(blob); + const disposable = { + dispose: () => { + URL.revokeObjectURL(src); + } + }; + + const image = document.createElement('img'); + image.src = src; + const display = document.createElement('div'); + display.classList.add('display'); + display.appendChild(image); + element.appendChild(display); + + return disposable; +} + +const ttPolicy = window.trustedTypes?.createPolicy('notebookRenderer', { + createHTML: value => value, + createScript: value => value, +}); + +const preservedScriptAttributes: (keyof HTMLScriptElement)[] = [ + 'type', 'src', 'nonce', 'noModule', 'async', +]; + +const domEval = (container: Element) => { + const arr = Array.from(container.getElementsByTagName('script')); + for (let n = 0; n < arr.length; n++) { + const node = arr[n]; + const scriptTag = document.createElement('script'); + const trustedScript = ttPolicy?.createScript(node.innerText) ?? node.innerText; + scriptTag.text = trustedScript as string; + for (const key of preservedScriptAttributes) { + const val = node[key] || node.getAttribute && node.getAttribute(key); + if (val) { + scriptTag.setAttribute(key, val as any); + } + } + + // TODO@connor4312: should script with src not be removed? + container.appendChild(scriptTag).parentNode!.removeChild(scriptTag); + } +}; + +function renderHTML(outputInfo: OutputItem, container: HTMLElement): void { + clearContainer(container); + const htmlContent = outputInfo.text(); + const element = document.createElement('div'); + const trustedHtml = ttPolicy?.createHTML(htmlContent) ?? htmlContent; + element.innerHTML = trustedHtml as string; + container.appendChild(element); + domEval(element); +} + +function renderJavascript(outputInfo: OutputItem, container: HTMLElement): void { + const str = outputInfo.text(); + const scriptVal = ``; + const element = document.createElement('div'); + const trustedHtml = ttPolicy?.createHTML(scriptVal) ?? scriptVal; + element.innerHTML = trustedHtml as string; + container.appendChild(element); + domEval(element); +} + +function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext & { readonly settings: { readonly lineLimit: number } }): void { + const element = document.createElement('div'); + container.appendChild(element); + type ErrorLike = Partial; + + let err: ErrorLike; + try { + err = JSON.parse(outputInfo.text()); + } catch (e) { + console.log(e); + return; + } + + if (err.stack) { + const stack = document.createElement('pre'); + stack.classList.add('traceback'); + stack.style.margin = '8px 0'; + const element = document.createElement('span'); + truncatedArrayOfString(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, element); + stack.appendChild(element); + container.appendChild(stack); + } else { + const header = document.createElement('div'); + const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message; + if (headerMessage) { + header.innerText = headerMessage; + container.appendChild(header); + } + } + + container.classList.add('error'); +} + +function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext & { readonly settings: { readonly lineLimit: number } }): void { + const outputContainer = container.parentElement; + if (!outputContainer) { + // should never happen + return; + } + + const prev = outputContainer.previousSibling; + if (prev) { + // OutputItem in the same cell + // check if the previous item is a stream + const outputElement = (prev.firstChild as HTMLElement | null); + if (outputElement && outputElement.getAttribute('output-mime-type') === outputInfo.mime) { + // same stream + const text = outputInfo.text(); + + const element = document.createElement('span'); + truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element); + outputElement.appendChild(element); + return; + } + } + + const element = document.createElement('span'); + element.classList.add('output-stream'); + + const text = outputInfo.text(); + truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element); + while (container.firstChild) { + container.removeChild(container.firstChild); + } + container.appendChild(element); + container.setAttribute('output-mime-type', outputInfo.mime); + if (error) { + container.classList.add('error'); + } +} + +function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext & { readonly settings: { readonly lineLimit: number } }): void { + clearContainer(container); + const contentNode = document.createElement('div'); + contentNode.classList.add('output-plaintext'); + const text = outputInfo.text(); + truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode); + container.appendChild(contentNode); + +} + +export const activate: ActivationFunction = (ctx) => { + const disposables = new Map(); + const latestContext = ctx as (RendererContext & { readonly settings: { readonly lineLimit: number } }); + + const style = document.createElement('style'); + style.textContent = ` + .output-plaintext, + .output-stream, + .traceback { + line-height: var(--notebook-cell-output-line-height); + font-family: var(--notebook-cell-output-font-family); + white-space: pre-wrap; + word-wrap: break-word; + + font-size: var(--notebook-cell-output-font-size); + user-select: text; + -webkit-user-select: text; + -ms-user-select: text; + cursor: auto; + } + span.output-stream { + display: inline-block; + } + .output-plaintext .code-bold, + .output-stream .code-bold, + .traceback .code-bold { + font-weight: bold; + } + .output-plaintext .code-italic, + .output-stream .code-italic, + .traceback .code-italic { + font-style: italic; + } + .output-plaintext .code-strike-through, + .output-stream .code-strike-through, + .traceback .code-strike-through { + text-decoration: line-through; + } + .output-plaintext .code-underline, + .output-stream .code-underline, + .traceback .code-underline { + text-decoration: underline; + } + `; + document.body.appendChild(style); + return { + renderOutputItem: (outputInfo, element) => { + switch (outputInfo.mime) { + case 'text/html': + case 'image/svg+xml': + { + if (!ctx.workspace.isTrusted) { + return; + } + + renderHTML(outputInfo, element); + } + break; + case 'application/javascript': + { + if (!ctx.workspace.isTrusted) { + return; + } + + renderJavascript(outputInfo, element); + } + break; + case 'image/gif': + case 'image/png': + case 'image/jpeg': + case 'image/git': + { + const disposable = renderImage(outputInfo, element); + disposables.set(outputInfo.id, disposable); + } + break; + case 'application/vnd.code.notebook.error': + { + renderError(outputInfo, element, latestContext); + } + break; + case 'application/vnd.code.notebook.stdout': + case 'application/x.notebook.stdout': + case 'application/x.notebook.stream': + { + renderStream(outputInfo, element, false, latestContext); + } + break; + case 'application/vnd.code.notebook.stderr': + case 'application/x.notebook.stderr': + { + renderStream(outputInfo, element, true, latestContext); + } + break; + case 'text/plain': + { + renderText(outputInfo, element, latestContext); + } + break; + default: + break; + } + + + }, + disposeOutputItem: (id: string | undefined) => { + if (id) { + disposables.get(id)?.dispose(); + } else { + disposables.forEach(d => d.dispose()); + } + } + }; +}; diff --git a/extensions/notebook-renderers/src/linkify.ts b/extensions/notebook-renderers/src/linkify.ts new file mode 100644 index 0000000000..68d0031920 --- /dev/null +++ b/extensions/notebook-renderers/src/linkify.ts @@ -0,0 +1,181 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const CONTROL_CODES = '\\u0000-\\u0020\\u007f-\\u009f'; +const WEB_LINK_REGEX = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s' + CONTROL_CODES + '"]{2,}[^\\s' + CONTROL_CODES + '"\')}\\],:;.!?]', 'ug'); + +const WIN_ABSOLUTE_PATH = /(?:[a-zA-Z]:(?:(?:\\|\/)[\w\.-]*)+)/; +const WIN_RELATIVE_PATH = /(?:(?:\~|\.)(?:(?:\\|\/)[\w\.-]*)+)/; +const WIN_PATH = new RegExp(`(${WIN_ABSOLUTE_PATH.source}|${WIN_RELATIVE_PATH.source})`); +const POSIX_PATH = /((?:\~|\.)?(?:\/[\w\.-]*)+)/; +const LINE_COLUMN = /(?:\:([\d]+))?(?:\:([\d]+))?/; +const isWindows = navigator.userAgent.indexOf('Windows') >= 0; +const PATH_LINK_REGEX = new RegExp(`${isWindows ? WIN_PATH.source : POSIX_PATH.source}${LINE_COLUMN.source}`, 'g'); + +const MAX_LENGTH = 2000; + +type LinkKind = 'web' | 'path' | 'text'; +type LinkPart = { + kind: LinkKind; + value: string; + captures: string[]; +}; + +export class LinkDetector { + constructor( + ) { + // noop + } + + /** + * Matches and handles web urls, absolute and relative file links in the string provided. + * Returns element that wraps the processed string, where matched links are replaced by . + * 'onclick' event is attached to all anchored links that opens them in the editor. + * When splitLines is true, each line of the text, even if it contains no links, is wrapped in a + * and added as a child of the returned . + */ + linkify(text: string, splitLines?: boolean, workspaceFolder?: string): HTMLElement { + if (splitLines) { + const lines = text.split('\n'); + for (let i = 0; i < lines.length - 1; i++) { + lines[i] = lines[i] + '\n'; + } + if (!lines[lines.length - 1]) { + // Remove the last element ('') that split added. + lines.pop(); + } + const elements = lines.map(line => this.linkify(line, false, workspaceFolder)); + if (elements.length === 1) { + // Do not wrap single line with extra span. + return elements[0]; + } + const container = document.createElement('span'); + elements.forEach(e => container.appendChild(e)); + return container; + } + + const container = document.createElement('span'); + for (const part of this.detectLinks(text)) { + try { + switch (part.kind) { + case 'text': + container.appendChild(document.createTextNode(part.value)); + break; + case 'web': + container.appendChild(this.createWebLink(part.value)); + break; + case 'path': { + container.appendChild(document.createTextNode(part.value)); + + // const path = part.captures[0]; + // const lineNumber = part.captures[1] ? Number(part.captures[1]) : 0; + // const columnNumber = part.captures[2] ? Number(part.captures[2]) : 0; + // container.appendChild(this.createPathLink(part.value, path, lineNumber, columnNumber, workspaceFolder)); + break; + } + } + } catch (e) { + container.appendChild(document.createTextNode(part.value)); + } + } + return container; + } + + private createWebLink(url: string): Node { + const link = this.createLink(url); + + return link; + } + + // private createPathLink(text: string, path: string, lineNumber: number, columnNumber: number, workspaceFolder: string | undefined): Node { + // if (path[0] === '/' && path[1] === '/') { + // // Most likely a url part which did not match, for example ftp://path. + // return document.createTextNode(text); + // } + + // const options = { selection: { startLineNumber: lineNumber, startColumn: columnNumber } }; + // if (path[0] === '.') { + // if (!workspaceFolder) { + // return document.createTextNode(text); + // } + // const uri = workspaceFolder.toResource(path); + // const link = this.createLink(text); + // this.decorateLink(link, uri, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); + // return link; + // } + + // if (path[0] === '~') { + // const userHome = this.pathService.resolvedUserHome; + // if (userHome) { + // path = osPath.join(userHome.fsPath, path.substring(1)); + // } + // } + + // const link = this.createLink(text); + // link.tabIndex = 0; + // const uri = URI.file(osPath.normalize(path)); + // this.fileService.resolve(uri).then(stat => { + // if (stat.isDirectory) { + // return; + // } + // this.decorateLink(link, uri, (preserveFocus: boolean) => this.editorService.openEditor({ resource: uri, options: { ...options, preserveFocus } })); + // }).catch(() => { + // // If the uri can not be resolved we should not spam the console with error, remain quite #86587 + // }); + // return link; + // } + + private createLink(text: string): HTMLElement { + const link = document.createElement('a'); + link.textContent = text; + return link; + } + + private detectLinks(text: string): LinkPart[] { + if (text.length > MAX_LENGTH) { + return [{ kind: 'text', value: text, captures: [] }]; + } + + const regexes: RegExp[] = [WEB_LINK_REGEX, PATH_LINK_REGEX]; + const kinds: LinkKind[] = ['web', 'path']; + const result: LinkPart[] = []; + + const splitOne = (text: string, regexIndex: number) => { + if (regexIndex >= regexes.length) { + result.push({ value: text, kind: 'text', captures: [] }); + return; + } + const regex = regexes[regexIndex]; + let currentIndex = 0; + let match; + regex.lastIndex = 0; + while ((match = regex.exec(text)) !== null) { + const stringBeforeMatch = text.substring(currentIndex, match.index); + if (stringBeforeMatch) { + splitOne(stringBeforeMatch, regexIndex + 1); + } + const value = match[0]; + result.push({ + value: value, + kind: kinds[regexIndex], + captures: match.slice(1) + }); + currentIndex = match.index + value.length; + } + const stringAfterMatches = text.substring(currentIndex); + if (stringAfterMatches) { + splitOne(stringAfterMatches, regexIndex + 1); + } + }; + + splitOne(text, 0); + return result; + } +} + +const linkDetector = new LinkDetector(); +export function linkify(text: string, splitLines?: boolean, workspaceFolder?: string) { + return linkDetector.linkify(text, splitLines, workspaceFolder); +} diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts new file mode 100644 index 0000000000..f4072e918d --- /dev/null +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { handleANSIOutput } from './ansi'; + +function generateViewMoreElement(outputId: string) { + const container = document.createElement('span'); + const first = document.createElement('span'); + first.textContent = 'Output exceeds the '; + const second = document.createElement('a'); + second.textContent = 'size limit'; + second.href = `command:workbench.action.openSettings?["notebook.output.textLineLimit"]`; + const third = document.createElement('span'); + third.textContent = '. Open the full output data'; + const forth = document.createElement('a'); + forth.textContent = ' in a text editor'; + forth.href = `command:workbench.action.openLargeOutput?${outputId}`; + container.appendChild(first); + container.appendChild(second); + container.appendChild(third); + container.appendChild(forth); + return container; +} + +export function truncatedArrayOfString(id: string, outputs: string[], linesLimit: number, container: HTMLElement) { + let buffer = outputs.join('\n').split(/\r|\n|\r\n/g); + let lineCount = buffer.length; + + if (lineCount < linesLimit) { + const spanElement = handleANSIOutput(buffer.slice(0, linesLimit).join('\n')); + container.appendChild(spanElement); + return; + } + + container.appendChild(generateViewMoreElement(id)); + + const div = document.createElement('div'); + container.appendChild(div); + div.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'))); + + // view more ... + const viewMoreSpan = document.createElement('span'); + viewMoreSpan.innerText = '...'; + container.appendChild(viewMoreSpan); + + const div2 = document.createElement('div'); + container.appendChild(div2); + div2.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n'))); +} diff --git a/extensions/notebook-renderers/tsconfig.json b/extensions/notebook-renderers/tsconfig.json new file mode 100644 index 0000000000..2032bf87b0 --- /dev/null +++ b/extensions/notebook-renderers/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out", + "lib": [ + "dom" + ] + }, + "include": [ + "src/**/*", + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.notebookEditor.d.ts", + "../../src/vscode-dts/vscode.proposed.notebookEditorEdit.d.ts", + ] +} diff --git a/extensions/notebook-renderers/yarn.lock b/extensions/notebook-renderers/yarn.lock new file mode 100644 index 0000000000..06ac4e135c --- /dev/null +++ b/extensions/notebook-renderers/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/vscode-notebook-renderer@^1.60.0": + version "1.60.0" + resolved "https://registry.yarnpkg.com/@types/vscode-notebook-renderer/-/vscode-notebook-renderer-1.60.0.tgz#8a67d561f48ddf46a95dfa9f712a79c72c7b8f7a" + integrity sha512-u7TD2uuEZTVuitx0iijOJdKI0JLiQP6PsSBSRy2XmHXUOXcp5p1S56NrjOEDoF+PIHd3NL3eO6KTRSf5nukDqQ== diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 858724207e..1a793d4a5b 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -23,13 +23,12 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( :: and the build bundles extensions into .build webpacked :: {{SQL CARBON EDIT}} Don't compile unused extensions call yarn gulp compile-extension:azurecore^ - compile-extension:git + compile-extension:git^ :: compile-extension:vscode-api-tests^ :: compile-extension:vscode-colorize-tests^ :: compile-extension:markdown-language-features^ :: compile-extension:typescript-language-features^ :: compile-extension:vscode-custom-editor-tests^ - :: compile-extension:vscode-notebook-tests^ :: compile-extension:emmet^ :: compile-extension:css-language-features-server^ :: compile-extension:html-language-features-server^ @@ -77,9 +76,6 @@ set ALL_PLATFORMS_API_TESTS_EXTRA_ARGS=--disable-telemetry --skip-welcome --skip :: call "%INTEGRATION_TEST_ELECTRON_PATH%" $%~dp0\..\extensions\emmet\out\test\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% . :: if %errorlevel% neq 0 exit /b %errorlevel% -:: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% -:: if %errorlevel% neq 0 exit /b %errorlevel% - :: call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\emmet\test-workspace --extensionDevelopmentPath=%~dp0\..\extensions\emmet --extensionTestsPath=%~dp0\..\extensions\emmet\out\test %ALL_PLATFORMS_API_TESTS_EXTRA_ARGS% :: if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index ed9e742dfc..4465737f64 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -34,11 +34,10 @@ else # and the build bundles extensions into .build webpacked # {{SQL CARBON EDIT}} Don't compile unused extensions yarn gulp compile-extension:azurecore \ - compile-extension:git + compile-extension:git \ # compile-extension:vscode-api-tests \ # compile-extension:vscode-colorize-tests \ # compile-extension:vscode-custom-editor-tests \ - # compile-extension:vscode-notebook-tests \ # compile-extension:markdown-language-features \ # compile-extension:typescript-language-features \ # compile-extension:emmet \ diff --git a/src/sql/base/common/locConstants.ts b/src/sql/base/common/locConstants.ts index 0e397e1821..288f9d7d80 100644 --- a/src/sql/base/common/locConstants.ts +++ b/src/sql/base/common/locConstants.ts @@ -67,3 +67,28 @@ export const terminalIntegratedEnvOsxDescription = localize('terminal.integrated export const terminalIntegratedEnvLinuxDescription = localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the Azure Data Studio process to be used by the terminal on Linux. Set to `null` to delete the environment variable.") export const terminalIntegratedEnvWindowsDescription = localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the Azure Data Studio process to be used by the terminal on Windows. Set to `null` to delete the environment variable.") export const terminalIntegratedInheritEnvDescription = localize('terminal.integrated.inheritEnv', "Whether new shells should inherit their environment from Azure Data Studio, which may source a login shell to ensure $PATH and other development variables are initialized. This has no effect on Windows.") + +//#region VS Code Notebook settings +export const displayOrderDescription = localize('notebook.displayOrder.description', "Priority list for output mime types. (for VS Code Notebooks only)"); +export const cellToolbarLocationDescription = localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden. (for VS Code Notebooks only)"); +export const showCellStatusbarDescription = localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown. (for VS Code Notebooks only)"); +export const diffEnablePreviewDescription = localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook. (for VS Code Notebooks only)"); +export const cellToolbarVisibilityDescription = localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click. (for VS Code Notebooks only)"); +export const undoRedoPerCellDescription = localize('notebook.undoRedoPerCell.description', "Whether to use separate undo/redo stack for each cell. (for VS Code Notebooks only)"); +export const compactViewDescription = localize('notebook.compactView.description', "Control whether the notebook editor should be rendered in a compact form. For example, when turned on, it will decrease the left margin width. (for VS Code Notebooks only)"); +export const focusIndicatorDescription = localize('notebook.focusIndicator.description', "Controls where the focus indicator is rendered, either along the cell borders or on the left gutter. (for VS Code Notebooks only)"); +export const insertToolbarPositionDescription = localize('notebook.insertToolbarPosition.description', "Control where the insert cell actions should appear. (for VS Code Notebooks only)"); +export const globalToolbarDescription = localize('notebook.globalToolbar.description', "Control whether to render a global toolbar inside the notebook editor. (for VS Code Notebooks only)"); +export const consolidatedOutputButtonDescription = localize('notebook.consolidatedOutputButton.description', "Control whether outputs action should be rendered in the output toolbar. (for VS Code Notebooks only)"); +export const showFoldingControlsDescription = localize('notebook.showFoldingControls.description', "Controls when the Markdown header folding arrow is shown. (for VS Code Notebooks only)"); +export const dragAndDropDescription = localize('notebook.dragAndDrop.description', "Control whether the notebook editor should allow moving cells through drag and drop. (for VS Code Notebooks only)"); +export const consolidatedRunButtonDescription = localize('notebook.consolidatedRunButton.description', "Control whether extra actions are shown in a dropdown next to the run button. (for VS Code Notebooks only)"); +export const globalToolbarShowLabelDescription = localize('notebook.globalToolbarShowLabel', "Control whether the actions on the notebook toolbar should render label or not. (for VS Code Notebooks only)"); +export const textOutputLineLimitDescription = localize('notebook.textOutputLineLimit', "Control how many lines of text in a text output is rendered. (for VS Code Notebooks only)"); +export const markupFontSizeDescription = localize('notebook.markup.fontSize', "Controls the font size in pixels of rendered markup in notebooks. When set to `0`, 120% of `#editor.fontSize#` is used. (for VS Code Notebooks only)"); +export const interactiveWindowCollapseCodeCellsDescription = localize('notebook.interactiveWindow.collapseCodeCells', "Controls whether code cells in the interactive window are collapsed by default. (for VS Code Notebooks only)"); +export const outputLineHeightDescription = localize('notebook.outputLineHeight', "Line height of the output text for notebook cells.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values. (for VS Code Notebooks only)"); +export const outputFontSizeDescription = localize('notebook.outputFontSize', "Font size for the output text for notebook cells. When set to 0 `#editor.fontSize#` is used. (for VS Code Notebooks only)"); +export const outputFontFamilyDescription = localize('notebook.outputFontFamily', "The font family for the output text for notebook cells. When set to empty, the `#editor.fontFamily#` is used. (for VS Code Notebooks only)"); +export const experimentalCustomizationDescription = localize('notebook.editorOptions.experimentalCustomization', 'Settings for code editors used in notebooks. This can be used to customize most editor.* settings. (for VS Code Notebooks only)'); +//#endregion diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index fd11e57951..9d93750d1e 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -727,7 +727,7 @@ export interface MainThreadConnectionManagementShape extends IDisposable { $listDatabases(connectionId: string): Thenable; $getConnectionString(connectionId: string, includePassword: boolean): Thenable; $getUriForConnection(connectionId: string): Thenable; - $connect(connectionProfile: azdata.IConnectionProfile, saveConnection: boolean, showDashboard: boolean): Thenable; + $connect(connectionProfile: azdata.IConnectionProfile, saveConnection: boolean, showDashboard: boolean, ownerUri?: string): Thenable; } export interface MainThreadCredentialManagementShape extends IDisposable { diff --git a/src/sql/workbench/browser/actions.contribution.ts b/src/sql/workbench/browser/actions.contribution.ts index 7454b08197..f993614c5f 100644 --- a/src/sql/workbench/browser/actions.contribution.ts +++ b/src/sql/workbench/browser/actions.contribution.ts @@ -51,3 +51,16 @@ Registry.as(ConfigExtensions.Configuration).registerConf } } }); + +Registry.as(ConfigExtensions.Configuration).registerConfiguration({ + 'id': 'useVSCodeNotebooks', + 'title': nls.localize('useVSCodeNotebooksTitle', "Use VS Code notebooks"), + 'type': 'object', + 'properties': { + 'workbench.useVSCodeNotebooks': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('useVSCodeNotebooks', "(Preview) Use VS Code notebooks as the default notebook experience. Note: Azure Data Studio will need to be restarted to enable this setting.") + } + } +}); diff --git a/src/sql/workbench/common/constants.ts b/src/sql/workbench/common/constants.ts index 182eb81824..55a207f1a0 100644 --- a/src/sql/workbench/common/constants.ts +++ b/src/sql/workbench/common/constants.ts @@ -27,6 +27,7 @@ export const ToggleRegexCommandId = 'toggleSearchRegex'; export const AddCursorsAtSearchResults = 'addCursorsAtSearchResults'; export const CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES = 'workbench.enablePreviewFeatures'; +export const CONFIG_WORKBENCH_USEVSCODENOTEBOOKS = 'workbench.useVSCodeNotebooks'; export const SearchViewFocusedKey = new RawContextKey('notebookSearchViewletFocus', false); export const InputBoxFocusedKey = new RawContextKey('inputBoxFocus', false); diff --git a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts index 701ab0374b..bb197d31a3 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookActions.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookActions.ts @@ -37,6 +37,8 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { KernelsLanguage } from 'sql/workbench/services/notebook/common/notebookConstants'; import { INotebookViews } from 'sql/workbench/services/notebook/browser/notebookViews/notebookViews'; import { Schemas } from 'vs/base/common/network'; +import { CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES, CONFIG_WORKBENCH_USEVSCODENOTEBOOKS } from 'sql/workbench/common/constants'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const msgLoading = localize('loading', "Loading kernels..."); export const msgChanging = localize('changing', "Changing kernel..."); @@ -844,6 +846,8 @@ export class NewNotebookAction extends Action { @IObjectExplorerService private objectExplorerService: IObjectExplorerService, @IAdsTelemetryService private _telemetryService: IAdsTelemetryService, @INotebookService private _notebookService: INotebookService, + @IConfigurationService private _configurationService: IConfigurationService, + @ICommandService private _commandService: ICommandService, ) { super(id, label); this.class = 'notebook-action new-notebook'; @@ -853,14 +857,21 @@ export class NewNotebookAction extends Action { this._telemetryService.createActionEvent(TelemetryKeys.TelemetryView.Notebook, TelemetryKeys.NbTelemetryAction.NewNotebookFromConnections) .withConnectionInfo(context?.connectionProfile) .send(); - let connProfile: azdata.IConnectionProfile; - if (context && context.nodeInfo) { - let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath); - connProfile = TreeUpdateUtils.getConnectionProfile(node).toIConnectionProfile(); - } else if (context && context.connectionProfile) { - connProfile = context.connectionProfile; + + const usePreviewFeatures = this._configurationService.getValue(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES); + const useVSCodeNotebooks = this._configurationService.getValue(CONFIG_WORKBENCH_USEVSCODENOTEBOOKS); + if (usePreviewFeatures && useVSCodeNotebooks) { + await this._commandService.executeCommand('ipynb.newUntitledIpynb'); + } else { + let connProfile: azdata.IConnectionProfile; + if (context && context.nodeInfo) { + let node = await this.objectExplorerService.getTreeNode(context.connectionProfile.id, context.nodeInfo.nodePath); + connProfile = TreeUpdateUtils.getConnectionProfile(node).toIConnectionProfile(); + } else if (context && context.connectionProfile) { + connProfile = context.connectionProfile; + } + await this._notebookService.openNotebook(URI.from({ scheme: 'untitled' }), { connectionProfile: connProfile }); } - await this._notebookService.openNotebook(URI.from({ scheme: 'untitled' }), { connectionProfile: connProfile }); } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 912d3a3cde..2656e4e90a 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -9,18 +9,18 @@ import { Emitter } from 'vs/base/common/event'; import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; -import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; // {{SQL CARBON EDIT}} Remove unused +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; import { INotebookCellStatusBarItemProvider, INotebookContributionData, NotebookData as NotebookData, NotebookExtensionDescription, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookContentProvider, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { ExtHostContext, ExtHostNotebookShape, MainThreadNotebookShape } from '../common/extHost.protocol'; // {{SQL CARBON EDIT}} Remove unused +import { ExtHostContext, ExtHostNotebookShape, MainContext, MainThreadNotebookShape } from '../common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; import { StopWatch } from 'vs/base/common/stopwatch'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { assertType } from 'vs/base/common/types'; -// @extHostNamedCustomer(MainContext.MainThreadNotebook) {{SQL CARBON EDIT}} Disable VS Code notebooks +@extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks implements MainThreadNotebookShape { private readonly _disposables = new DisposableStore(); @@ -196,7 +196,7 @@ CommandsRegistry.registerCommand('_executeDataToNotebook', async (accessor, ...a const notebookService = accessor.get(INotebookService); const info = await notebookService.withNotebookDataProvider(notebookType); if (!(info instanceof SimpleNotebookProviderInfo)) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} strict nulls } const dto = await info.serializer.dataToNotebook(bytes); @@ -212,7 +212,7 @@ CommandsRegistry.registerCommand('_executeNotebookToData', async (accessor, ...a const notebookService = accessor.get(INotebookService); const info = await notebookService.withNotebookDataProvider(notebookType); if (!(info instanceof SimpleNotebookProviderInfo)) { - return undefined; + return undefined; // {{SQL CARBON EDIT}} strict nulls } const data = NotebookDto.fromNotebookDataDto(dto.value); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 8048550643..5724961b3c 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -10,7 +10,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { MainThreadNotebookDocuments } from 'vs/workbench/api/browser/mainThreadNotebookDocuments'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; import { MainThreadNotebookEditors } from 'vs/workbench/api/browser/mainThreadNotebookEditors'; -import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { extHostCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { editorGroupToColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { getNotebookEditorFromEditorPane, IActiveNotebookEditor, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; @@ -69,7 +69,7 @@ class NotebookAndEditorState { } } -// @extHostCustomer {{SQL CARBON EDIT}} Disable VS Code notebooks +@extHostCustomer export class MainThreadNotebooksAndEditors { // private readonly _onDidAddNotebooks = new Emitter(); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index a87ab33edf..d7b2e9d88e 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -11,13 +11,13 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { NotebookDto } from 'vs/workbench/api/browser/mainThreadNotebookDto'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; import { IResolvedNotebookKernel, INotebookKernelChangeEvent, INotebookKernelService, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; -import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; abstract class MainThreadKernel implements IResolvedNotebookKernel { readonly type: NotebookKernelType.Resolved = NotebookKernelType.Resolved; @@ -98,7 +98,7 @@ abstract class MainThreadKernel implements IResolvedNotebookKernel { abstract cancelNotebookCellExecution(uri: URI, cellHandles: number[]): Promise; } -// @extHostNamedCustomer(MainContext.MainThreadNotebookKernels) {{SQL CARBON EDIT}} Disable VS Code notebooks +@extHostNamedCustomer(MainContext.MainThreadNotebookKernels) export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape { private readonly _editors = new Map(); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts index e822e03d3c..2139666ec5 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookProxyKernels.ts @@ -6,9 +6,9 @@ import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; // {{SQL CARBON EDIT}} Remove unused +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookKernelService, INotebookProxyKernel, INotebookProxyKernelChangeEvent, ProxyKernelState, NotebookKernelType } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { ExtHostContext, ExtHostNotebookProxyKernelsShape, INotebookProxyKernelDto, MainThreadNotebookProxyKernelsShape } from '../common/extHost.protocol'; // {{SQL CARBON EDIT}} Remove unused +import { ExtHostContext, ExtHostNotebookProxyKernelsShape, INotebookProxyKernelDto, MainContext, MainThreadNotebookProxyKernelsShape } from '../common/extHost.protocol'; import { onUnexpectedError } from 'vs/base/common/errors'; abstract class MainThreadProxyKernel implements INotebookProxyKernel { @@ -64,7 +64,7 @@ abstract class MainThreadProxyKernel implements INotebookProxyKernel { abstract resolveKernel(): Promise; } -// @extHostNamedCustomer(MainContext.MainThreadNotebookProxyKernels) {{SQL CARBON EDIT}} Disable VS Code notebooks +@extHostNamedCustomer(MainContext.MainThreadNotebookProxyKernels) export class MainThreadNotebookProxyKernels implements MainThreadNotebookProxyKernelsShape { private readonly _disposables = new DisposableStore(); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts index cd08896f17..86ad26aad5 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookRenderers.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from 'vs/base/common/lifecycle'; -import { ExtHostContext, ExtHostNotebookRenderersShape, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; -import { IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; +import { ExtHostContext, ExtHostNotebookRenderersShape, MainContext, MainThreadNotebookRenderersShape } from 'vs/workbench/api/common/extHost.protocol'; +import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; import { INotebookRendererMessagingService } from 'vs/workbench/contrib/notebook/common/notebookRendererMessagingService'; -// @extHostNamedCustomer(MainContext.MainThreadNotebookRenderers) {{SQL CARBON EDIT}} Disable VS Code notebooks +@extHostNamedCustomer(MainContext.MainThreadNotebookRenderers) export class MainThreadNotebookRenderers extends Disposable implements MainThreadNotebookRenderersShape { private readonly proxy: ExtHostNotebookRenderersShape; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 00d7025bd6..4aa7f8c865 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -64,7 +64,7 @@ import { ILoggerService, ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; -// import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; {{SQL CARBON EDIT}} Disable VS Code notebooks +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; @@ -82,19 +82,19 @@ import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener'; import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; -// import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; {{SQL CARBON EDIT}} Disable VS Code notebooks +import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes'; -// import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; {{SQL CARBON EDIT}} Disable VS Code notebooks +import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers'; import { Schemas } from 'vs/base/common/network'; import { matchesScheme } from 'vs/platform/opener/common/opener'; -// import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookEditors'; {{SQL CARBON EDIT}} Disable VS Code notebooks -// import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; {{SQL CARBON EDIT}} Disable VS Code notebooks +import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookEditors'; +import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments'; // import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive'; {{SQL CARBON EDIT}} Remove until we need it import { combinedDisposable } from 'vs/base/common/lifecycle'; import { checkProposedApiEnabled, ExtensionIdentifierSet, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug'; - -import { notebooksNotSupportedError } from 'sql/base/common/locConstants'; +import { ExtHostNotebookProxyKernels } from 'vs/workbench/api/common/extHostNotebookProxyKernels'; +import { CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES, CONFIG_WORKBENCH_USEVSCODENOTEBOOKS } from 'sql/workbench/common/constants'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -158,15 +158,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadBulkEdits))); - /* {{SQL CARBON EDIT }} Disable VS Code notebooks const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, extHostDocuments, extensionStoragePaths)); const extHostNotebookDocuments = rpcProtocol.set(ExtHostContext.ExtHostNotebookDocuments, new ExtHostNotebookDocuments(extHostNotebook)); const extHostNotebookEditors = rpcProtocol.set(ExtHostContext.ExtHostNotebookEditors, new ExtHostNotebookEditors(extHostLogService, rpcProtocol, extHostNotebook)); - const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); - const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); + const extHostNotebookKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookKernels, new ExtHostNotebookKernels(rpcProtocol, initData, extHostNotebook, extHostCommands, extHostLogService)); const extHostNotebookProxyKernels = rpcProtocol.set(ExtHostContext.ExtHostNotebookProxyKernels, new ExtHostNotebookProxyKernels(rpcProtocol, extHostNotebookKernels, extHostLogService)); - */ - + const extHostNotebookRenderers = rpcProtocol.set(ExtHostContext.ExtHostNotebookRenderers, new ExtHostNotebookRenderers(rpcProtocol, extHostNotebook)); const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.remote)); @@ -195,13 +192,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // {{SQL CARBON EDIT}} filter out the services we don't expose const filteredProxies: Set> = new Set([ ExtHostContext.ExtHostDebugService, - ExtHostContext.ExtHostNotebook, - ExtHostContext.ExtHostNotebookDocuments, - ExtHostContext.ExtHostNotebookEditors, - ExtHostContext.ExtHostNotebookKernels, - ExtHostContext.ExtHostNotebookProxyKernels, - ExtHostContext.ExtHostNotebookRenderers, - ExtHostContext.ExtHostNotebookProxyKernels, ExtHostContext.ExtHostInteractive ]); const expected: ProxyIdentifier[] = values(ExtHostContext).filter(v => !filteredProxies.has(v)); @@ -219,6 +209,15 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExtHostApiCommands.register(extHostCommands); return function (extension: IExtensionDescription, extensionInfo: IExtensionRegistries, configProvider: ExtHostConfigProvider): typeof vscode { + // {{SQL CARBON EDIT}} + const checkVSCodeNotebooksEnabled = (extension: IExtensionDescription) => { + const usePreviewFeatures = configProvider.getConfiguration(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES); + const useVSCodeNotebooks = configProvider.getConfiguration(CONFIG_WORKBENCH_USEVSCODENOTEBOOKS); + const notebooksEnabled = usePreviewFeatures && useVSCodeNotebooks; + if (!notebooksEnabled) { + throw new Error(`Notebook extension '${extension.identifier.value}' is not supported. VS Code notebook functionality is currently disabled.`); + } + } // Check document selectors for being overly generic. Technically this isn't a problem but // in practice many extensions say they support `fooLang` but need fs-access to do so. Those @@ -775,32 +774,33 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostWebviewViews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions); }, get activeNotebookEditor(): vscode.NotebookEditor | undefined { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebook.activeNotebookEditor; }, onDidChangeActiveNotebookEditor(listener, thisArgs?, disposables?) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebook.onDidChangeActiveNotebookEditor(listener, thisArgs, disposables); }, get visibleNotebookEditors() { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - return undefined; + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebook.visibleNotebookEditors; }, get onDidChangeVisibleNotebookEditors() { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - return undefined; + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebook.onDidChangeVisibleNotebookEditors; }, onDidChangeNotebookEditorSelection(listener, thisArgs?, disposables?) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebookEditors.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); }, onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookEditor'); + return extHostNotebookEditors.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); }, - showNotebookDocument(uriOrDocument: URI | vscode.NotebookDocument, options?: vscode.NotebookDocumentShowOptions): Thenable { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + showNotebookDocument(uriOrDocument, options?) { + checkProposedApiEnabled(extension, 'notebookEditor'); + checkVSCodeNotebooksEnabled(extension); // {{SQL CARBON EDIT}} + return extHostNotebook.showNotebookDocument(uriOrDocument, options); }, registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) { checkProposedApiEnabled(extension, 'externalUriOpener'); @@ -925,36 +925,39 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostDocumentSaveParticipant.getOnWillSaveTextDocumentEvent(extension)(listener, thisArgs, disposables); }, get notebookDocuments(): vscode.NotebookDocument[] { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebook.notebookDocuments.map(d => d.apiNotebook); }, - async openNotebookDocument(uriOrType?: URI | string, content?: vscode.NotebookData): Promise { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + async openNotebookDocument(uriOrType?: URI | string, content?: vscode.NotebookData) { + checkVSCodeNotebooksEnabled(extension); // {{SQL CARBON EDIT}} + let uri: URI; + if (URI.isUri(uriOrType)) { + uri = uriOrType; + await extHostNotebook.openNotebookDocument(uriOrType); + } else if (typeof uriOrType === 'string') { + uri = URI.revive(await extHostNotebook.createNotebookDocument({ viewType: uriOrType, content })); + } else { + throw new Error('Invalid arguments'); + } + return extHostNotebook.getNotebookDocument(uri).apiNotebook; }, onDidSaveNotebookDocument(listener, thisArg, disposables) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebookDocuments.onDidSaveNotebookDocument(listener, thisArg, disposables); }, onDidChangeNotebookDocument(listener, thisArg, disposables) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebookDocuments.onDidChangeNotebookDocument(listener, thisArg, disposables); }, get onDidOpenNotebookDocument(): Event { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebook.onDidOpenNotebookDocument; }, get onDidCloseNotebookDocument(): Event { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebook.onDidCloseNotebookDocument; }, registerNotebookSerializer(viewType: string, serializer: vscode.NotebookSerializer, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebook.registerNotebookSerializer(extension, viewType, serializer, options, isProposedApiEnabled(extension, 'notebookLiveShare') ? registration : undefined); }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: vscode.NotebookDocumentContentOptions, registration?: vscode.NotebookRegistrationData) => { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookContentProvider'); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options, isProposedApiEnabled(extension, 'notebookLiveShare') ? registration : undefined); }, onDidChangeConfiguration: (listener: (_: any) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { return configProvider.onDidChangeConfiguration(listener, thisArgs, disposables); @@ -1191,28 +1194,25 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // namespace: notebook const notebooks: typeof vscode.notebooks = { createNotebookController(id: string, notebookType: string, label: string, handler?, rendererScripts?: vscode.NotebookRendererScript[]) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebookKernels.createNotebookController(extension, id, notebookType, label, handler, isProposedApiEnabled(extension, 'notebookMessaging') ? rendererScripts : undefined); }, registerNotebookCellStatusBarItemProvider: (notebookType: string, provider: vscode.NotebookCellStatusBarItemProvider) => { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, notebookType, provider); }, createNotebookEditorDecorationType(options: vscode.NotebookDecorationRenderOptions): vscode.NotebookEditorDecorationType { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookEditorDecorationType'); + return extHostNotebookEditors.createNotebookEditorDecorationType(options); }, createRendererMessaging(rendererId) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + return extHostNotebookRenderers.createRendererMessaging(extension, rendererId); }, onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookCellExecutionState'); + return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); }, createNotebookProxyController(id: string, notebookType: string, label: string, handler: () => vscode.NotebookController | string | Thenable) { - // {{SQL CARBON EDIT}} Disable VS Code notebooks - throw new Error(notebooksNotSupportedError); + checkProposedApiEnabled(extension, 'notebookProxyController'); + return extHostNotebookProxyKernels.createNotebookProxyController(extension, id, notebookType, label, handler); } }; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/breakpoints/notebookBreakpoints.ts b/src/vs/workbench/contrib/notebook/browser/contrib/breakpoints/notebookBreakpoints.ts index ae76043d2a..d81af9b4f2 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/breakpoints/notebookBreakpoints.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/breakpoints/notebookBreakpoints.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - +/* {{SQL CARBON EDIT}} Disable notebook breakpoints because the debug service is not enabled import { RunOnceScheduler } from 'vs/base/common/async'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; @@ -197,3 +197,4 @@ class NotebookCellPausing extends Disposable implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookCellPausing, LifecyclePhase.Restored); +*/ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 8fad6a5aa8..65e5e84af9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -15,7 +15,7 @@ import { IModelService } from 'vs/editor/common/services/model'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import * as nls from 'vs/nls'; -import { Extensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; // {{SQL CARBON EDIT}} Remove unused +import { Extensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -105,7 +105,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { NotebookInfo } from 'vs/editor/common/languageFeatureRegistry'; import { COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentReply'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; - +import * as locConstants from 'sql/base/common/locConstants'; /*--------------------------------------------------------------------------------------------- */ @@ -728,9 +728,9 @@ for (const editorOption of editorOptionsRegistry) { } } -/* {{SQL CARBON EDIT}} Remove VS Notebook configurations +// {{SQL CARBON EDIT}} Add updated description const editorOptionsCustomizationSchema: IConfigurationPropertySchema = { - description: nls.localize('notebook.editorOptions.experimentalCustomization', 'Settings for code editors used in notebooks. This can be used to customize most editor.* settings.'), + description: locConstants.experimentalCustomizationDescription, default: {}, allOf: [ { @@ -748,8 +748,8 @@ const editorOptionsCustomizationSchema: IConfigurationPropertySchema = { ], tags: ['notebookLayout'] }; -*/ +// {{SQL CARBON EDIT}} Add updated descriptions const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ id: 'notebook', @@ -757,161 +757,160 @@ configurationRegistry.registerConfiguration({ title: nls.localize('notebookConfigurationTitle', "Notebook"), type: 'object', properties: { - // {{SQL CARBON EDIT}} Remove unused VS Code Notebook configurations - // [DisplayOrderKey]: { - // description: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), - // type: ['array'], - // items: { - // type: 'string' - // }, - // default: [] - // }, - // [NotebookSetting.cellToolbarLocation]: { - // description: nls.localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden."), - // type: 'object', - // additionalProperties: { - // markdownDescription: nls.localize('notebook.cellToolbarLocation.viewType', "Configure the cell toolbar position for for specific file types"), - // type: 'string', - // enum: ['left', 'right', 'hidden'] - // }, - // default: { - // 'default': 'right' - // }, - // tags: ['notebookLayout'] - // }, - // [ShowCellStatusBar]: { - // description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown."), - // type: 'string', - // enum: ['hidden', 'visible', 'visibleAfterExecute'], - // enumDescriptions: [ - // nls.localize('notebook.showCellStatusbar.hidden.description', "The cell Status bar is always hidden."), - // nls.localize('notebook.showCellStatusbar.visible.description', "The cell Status bar is always visible."), - // nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell Status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], - // default: 'visible', - // tags: ['notebookLayout'] - // }, - // [NotebookTextDiffEditorPreview]: { - // description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [CellToolbarVisibility]: { - // markdownDescription: nls.localize('notebook.cellToolbarVisibility.description', "Whether the cell toolbar should appear on hover or click."), - // type: 'string', - // enum: ['hover', 'click'], - // default: 'click', - // tags: ['notebookLayout'] - // }, - // [UndoRedoPerCell]: { - // description: nls.localize('notebook.undoRedoPerCell.description', "Whether to use separate undo/redo stack for each cell."), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [CompactView]: { - // description: nls.localize('notebook.compactView.description', "Control whether the notebook editor should be rendered in a compact form. "), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [FocusIndicator]: { - // description: nls.localize('notebook.focusIndicator.description', "Controls where the focus indicator is rendered, either along the cell borders or on the left gutter"), - // type: 'string', - // enum: ['border', 'gutter'], - // default: 'gutter', - // tags: ['notebookLayout'] - // }, - // [InsertToolbarLocation]: { - // description: nls.localize('notebook.insertToolbarPosition.description', "Control where the insert cell actions should appear."), - // type: 'string', - // enum: ['betweenCells', 'notebookToolbar', 'both', 'hidden'], - // enumDescriptions: [ - // nls.localize('insertToolbarLocation.betweenCells', "A toolbar that appears on hover between cells."), - // nls.localize('insertToolbarLocation.notebookToolbar', "The toolbar at the top of the notebook editor."), - // nls.localize('insertToolbarLocation.both', "Both toolbars."), - // nls.localize('insertToolbarLocation.hidden', "The insert actions don't appear anywhere."), - // ], - // default: 'both', - // tags: ['notebookLayout'] - // }, - // [GlobalToolbar]: { - // description: nls.localize('notebook.globalToolbar.description', "Control whether to render a global toolbar inside the notebook editor."), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [ConsolidatedOutputButton]: { - // description: nls.localize('notebook.consolidatedOutputButton.description', "Control whether outputs action should be rendered in the output toolbar."), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [ShowFoldingControls]: { - // description: nls.localize('notebook.showFoldingControls.description', "Controls when the Markdown header folding arrow is shown."), - // type: 'string', - // enum: ['always', 'mouseover'], - // enumDescriptions: [ - // nls.localize('showFoldingControls.always', "The folding controls are always visible."), - // nls.localize('showFoldingControls.mouseover', "The folding controls are visible only on mouseover."), - // ], - // default: 'mouseover', - // tags: ['notebookLayout'] - // }, - // [DragAndDropEnabled]: { - // description: nls.localize('notebook.dragAndDrop.description', "Control whether the notebook editor should allow moving cells through drag and drop."), - // type: 'boolean', - // default: true, - // tags: ['notebookLayout'] - // }, - // [ConsolidatedRunButton]: { - // description: nls.localize('notebook.consolidatedRunButton.description', "Control whether extra actions are shown in a dropdown next to the run button."), - // type: 'boolean', - // default: false, - // tags: ['notebookLayout'] - // }, - // [GlobalToolbarShowLabel]: { - // description: nls.localize('notebook.globalToolbarShowLabel', "Control whether the actions on the notebook toolbar should render label or not."), - // type: 'boolean', - // enum: ['always', 'never', 'dynamic'], - // default: true, - // tags: ['notebookLayout'] - // }, - // [TextOutputLineLimit]: { - // description: nls.localize('notebook.textOutputLineLimit', "Control how many lines of text in a text output is rendered."), - // type: 'number', - // default: 30, - // tags: ['notebookLayout'] - // }, - // [NotebookSetting.markupFontSize]: { - // markdownDescription: nls.localize('notebook.markup.fontSize', "Controls the font size of rendered markup in notebooks. When set to `0`, 120% of `#editor.fontSize#` is used."), - // type: 'number', - // default: 0, - // tags: ['notebookLayout'] - // }, - // [NotebookSetting.cellEditorOptionsCustomizations]: editorOptionsCustomizationSchema - // [NotebookSetting.interactiveWindowCollapseCodeCells]: { - // markdownDescription: nls.localize('notebook.interactiveWindow.collapseCodeCells', "Controls whether code cells in the interactive window are collapsed by default."), - // type: 'string', - // enum: ['always', 'never', 'fromEditor'], - // default: 'fromEditor' - // }, - // [NotebookSetting.outputLineHeight]: { - // markdownDescription: nls.localize('notebook.outputLineHeight', "Line height of the output text for notebook cells.\n - Values between 0 and 8 will be used as a multiplier with the font size.\n - Values greater than or equal to 8 will be used as effective values."), - // type: 'number', - // default: 22, - // tags: ['notebookLayout'] - // }, - // [NotebookSetting.outputFontSize]: { - // markdownDescription: nls.localize('notebook.outputFontSize', "Font size for the output text for notebook cells. When set to 0 `#editor.fontSize#` is used."), - // type: 'number', - // default: 0, - // tags: ['notebookLayout'] - // }, - // [NotebookSetting.outputFontFamily]: { - // markdownDescription: nls.localize('notebook.outputFontFamily', "The font family for the output text for notebook cells. When set to empty, the `#editor.fontFamily#` is used."), - // type: 'string', - // tags: ['notebookLayout'] - // }, + [NotebookSetting.displayOrder]: { + description: locConstants.displayOrderDescription, + type: 'array', + items: { + type: 'string' + }, + default: [] + }, + [NotebookSetting.cellToolbarLocation]: { + description: locConstants.cellToolbarLocationDescription, + type: 'object', + additionalProperties: { + markdownDescription: nls.localize('notebook.cellToolbarLocation.viewType', "Configure the cell toolbar position for for specific file types"), + type: 'string', + enum: ['left', 'right', 'hidden'] + }, + default: { + 'default': 'right' + }, + tags: ['notebookLayout'] + }, + [NotebookSetting.showCellStatusBar]: { + description: locConstants.showCellStatusbarDescription, + type: 'string', + enum: ['hidden', 'visible', 'visibleAfterExecute'], + enumDescriptions: [ + nls.localize('notebook.showCellStatusbar.hidden.description', "The cell Status bar is always hidden."), + nls.localize('notebook.showCellStatusbar.visible.description', "The cell Status bar is always visible."), + nls.localize('notebook.showCellStatusbar.visibleAfterExecute.description', "The cell Status bar is hidden until the cell has executed. Then it becomes visible to show the execution status.")], + default: 'visible', + tags: ['notebookLayout'] + }, + [NotebookSetting.textDiffEditorPreview]: { + description: locConstants.diffEnablePreviewDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.cellToolbarVisibility]: { + markdownDescription: locConstants.cellToolbarVisibilityDescription, + type: 'string', + enum: ['hover', 'click'], + default: 'click', + tags: ['notebookLayout'] + }, + [NotebookSetting.undoRedoPerCell]: { + description: locConstants.undoRedoPerCellDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.compactView]: { + description: locConstants.compactViewDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.focusIndicator]: { + description: locConstants.focusIndicatorDescription, + type: 'string', + enum: ['border', 'gutter'], + default: 'gutter', + tags: ['notebookLayout'] + }, + [NotebookSetting.insertToolbarLocation]: { + description: locConstants.insertToolbarPositionDescription, + type: 'string', + enum: ['betweenCells', 'notebookToolbar', 'both', 'hidden'], + enumDescriptions: [ + nls.localize('insertToolbarLocation.betweenCells', "A toolbar that appears on hover between cells."), + nls.localize('insertToolbarLocation.notebookToolbar', "The toolbar at the top of the notebook editor."), + nls.localize('insertToolbarLocation.both', "Both toolbars."), + nls.localize('insertToolbarLocation.hidden', "The insert actions don't appear anywhere."), + ], + default: 'both', + tags: ['notebookLayout'] + }, + [NotebookSetting.globalToolbar]: { + description: locConstants.globalToolbarDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.consolidatedOutputButton]: { + description: locConstants.consolidatedOutputButtonDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.showFoldingControls]: { + description: locConstants.showFoldingControlsDescription, + type: 'string', + enum: ['always', 'mouseover'], + enumDescriptions: [ + nls.localize('showFoldingControls.always', "The folding controls are always visible."), + nls.localize('showFoldingControls.mouseover', "The folding controls are visible only on mouseover."), + ], + default: 'mouseover', + tags: ['notebookLayout'] + }, + [NotebookSetting.dragAndDropEnabled]: { + description: locConstants.dragAndDropDescription, + type: 'boolean', + default: true, + tags: ['notebookLayout'] + }, + [NotebookSetting.consolidatedRunButton]: { + description: locConstants.consolidatedRunButtonDescription, + type: 'boolean', + default: false, + tags: ['notebookLayout'] + }, + [NotebookSetting.globalToolbarShowLabel]: { + description: locConstants.globalToolbarShowLabelDescription, + type: 'string', + enum: ['always', 'never', 'dynamic'], + default: 'always', + tags: ['notebookLayout'] + }, + [NotebookSetting.textOutputLineLimit]: { + description: locConstants.textOutputLineLimitDescription, + type: 'number', + default: 30, + tags: ['notebookLayout'] + }, + [NotebookSetting.markupFontSize]: { + markdownDescription: locConstants.markupFontSizeDescription, + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, + [NotebookSetting.cellEditorOptionsCustomizations]: editorOptionsCustomizationSchema, + [NotebookSetting.interactiveWindowCollapseCodeCells]: { + markdownDescription: locConstants.interactiveWindowCollapseCodeCellsDescription, + type: 'string', + enum: ['always', 'never', 'fromEditor'], + default: 'fromEditor' + }, + [NotebookSetting.outputLineHeight]: { + markdownDescription: locConstants.outputLineHeightDescription, + type: 'number', + default: 22, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontSize]: { + markdownDescription: locConstants.outputFontSizeDescription, + type: 'number', + default: 0, + tags: ['notebookLayout'] + }, + [NotebookSetting.outputFontFamily]: { + markdownDescription: locConstants.outputFontFamilyDescription, + type: 'string', + tags: ['notebookLayout'] + }, } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 060151a42b..d4437a6274 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES, CONFIG_WORKBENCH_USEVSCODENOTEBOOKS } from 'sql/workbench/common/constants'; import { PixelRatio } from 'vs/base/browser/browser'; import { Emitter, Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; @@ -16,7 +17,6 @@ import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -// import { localize } from 'vs/nls'; {{SQL CARBON EDIT}} Disable VS Code notebooks import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -24,12 +24,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Memento } from 'vs/workbench/common/memento'; -import { notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; // {{SQL CARBON EDIT}} Remove INotebookEditorContribution, notebooksExtensionPoint +import { INotebookEditorContribution, notebookRendererExtensionPoint, notebooksExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; import { INotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookData, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, TransientOptions, NotebookExtensionDescription } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellUri, NotebookSetting, INotebookContributionData, INotebookExclusiveDocumentFilter, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, IOutputDto, MimeTypeDisplayOrder, NotebookData, NotebookEditorPriority, NotebookRendererMatch, NOTEBOOK_DISPLAY_ORDER, RENDERER_EQUIVALENT_EXTENSIONS, RENDERER_NOT_AVAILABLE, TransientOptions, NotebookExtensionDescription } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { updateEditorTopPadding } from 'vs/workbench/contrib/notebook/common/notebookOptions'; @@ -38,7 +38,7 @@ import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/con import { ComplexNotebookProviderInfo, INotebookContentProvider, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { DiffEditorInputFactoryFunction, EditorInputFactoryFunction, IEditorResolverService, IEditorType, RegisteredEditorInfo, RegisteredEditorPriority, UntitledEditorInputFactoryFunction } from 'vs/workbench/services/editor/common/editorResolverService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -// import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; {{SQL CARBON EDIT}} Disable VS Code notebooks +import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export class NotebookProviderInfoStore extends Disposable { @@ -79,7 +79,12 @@ export class NotebookProviderInfoStore extends Disposable { } })); - // notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); {{SQL CARBON EDIT}} Disable VS Code notebooks + // {{SQL CARBON EDIT}} Disable file associations here if we're not using VS Code notebooks by default + const usePreviewFeatures = this._configurationService.getValue(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES); + const useVSCodeNotebooks = this._configurationService.getValue(CONFIG_WORKBENCH_USEVSCODENOTEBOOKS); + if (usePreviewFeatures && useVSCodeNotebooks) { + notebooksExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); + } } override dispose(): void { @@ -87,7 +92,6 @@ export class NotebookProviderInfoStore extends Disposable { super.dispose(); } - /* // {{SQL CARBON EDIT}} Disable VS Code notebooks private _setupHandler(extensions: readonly IExtensionPointUser[]) { this._handled = true; const builtins: NotebookProviderInfo[] = [...this._contributedEditors.values()].filter(info => !info.extension); @@ -153,7 +157,6 @@ export class NotebookProviderInfoStore extends Disposable { return RegisteredEditorPriority.option; } - */ private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): IDisposable { @@ -221,6 +224,7 @@ export class NotebookProviderInfoStore extends Disposable { return disposables; } + private _clear(): void { this._contributedEditors.clear(); this._contributedEditorDisposables.clear(); @@ -421,35 +425,40 @@ export class NotebookService extends Disposable implements INotebookService { ) { super(); - notebookRendererExtensionPoint.setHandler((renderers) => { - this._notebookRenderersInfoStore.clear(); + // {{SQL CARBON EDIT}} Disable renderer associations here if we're not using VS Code notebooks by default + const usePreviewFeatures = this._configurationService.getValue(CONFIG_WORKBENCH_ENABLEPREVIEWFEATURES); + const useVSCodeNotebooks = this._configurationService.getValue(CONFIG_WORKBENCH_USEVSCODENOTEBOOKS); + if (usePreviewFeatures && useVSCodeNotebooks) { + notebookRendererExtensionPoint.setHandler((renderers) => { + this._notebookRenderersInfoStore.clear(); - for (const extension of renderers) { - for (const notebookContribution of extension.value) { - if (!notebookContribution.entrypoint) { // avoid crashing - extension.collector.error(`Notebook renderer does not specify entry point`); - continue; + for (const extension of renderers) { + for (const notebookContribution of extension.value) { + if (!notebookContribution.entrypoint) { // avoid crashing + extension.collector.error(`Notebook renderer does not specify entry point`); + continue; + } + + const id = notebookContribution.id; + if (!id) { + extension.collector.error(`Notebook renderer does not specify id-property`); + continue; + } + + this._notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ + id, + extension: extension.description, + entrypoint: notebookContribution.entrypoint, + displayName: notebookContribution.displayName, + mimeTypes: notebookContribution.mimeTypes || [], + dependencies: notebookContribution.dependencies, + optionalDependencies: notebookContribution.optionalDependencies, + requiresMessaging: notebookContribution.requiresMessaging, + })); } - - const id = notebookContribution.id; - if (!id) { - extension.collector.error(`Notebook renderer does not specify id-property`); - continue; - } - - this._notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ - id, - extension: extension.description, - entrypoint: notebookContribution.entrypoint, - displayName: notebookContribution.displayName, - mimeTypes: notebookContribution.mimeTypes || [], - dependencies: notebookContribution.dependencies, - optionalDependencies: notebookContribution.optionalDependencies, - requiresMessaging: notebookContribution.requiresMessaging, - })); } - } - }); + }); + } const updateOrder = () => { this._displayOrder = new MimeTypeDisplayOrder( @@ -518,7 +527,7 @@ export class NotebookService extends Disposable implements INotebookService { } clearEditorCache(): void { - // this.notebookProviderInfoStore.clearEditorCache(); // {{SQL CARBON EDIT}} - method disabled + this.notebookProviderInfoStore.clearEditorCache(); } private _postDocumentOpenActivation(viewType: string) { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index b80c8708cd..a00f781ca5 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -313,13 +313,6 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { // {{SQL CARBON EDIT}} filter out services we don't expose const filtered: ProxyIdentifier[] = [ MainContext.MainThreadDebugService, - MainContext.MainThreadNotebook, - MainContext.MainThreadNotebookDocuments, - MainContext.MainThreadNotebookEditors, - MainContext.MainThreadNotebookKernels, - MainContext.MainThreadNotebookProxyKernels, - MainContext.MainThreadNotebookRenderers, - MainContext.MainThreadNotebookProxyKernels, MainContext.MainThreadInteractive ]; const expected: ProxyIdentifier[] = Object.keys(MainContext).map((key) => (MainContext)[key]).filter(v => !filtered.some(x => x === v)); diff --git a/test/automation/src/code.ts b/test/automation/src/code.ts index 403cfb6e44..2c8d1b641c 100644 --- a/test/automation/src/code.ts +++ b/test/automation/src/code.ts @@ -3,20 +3,16 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { join } from 'path'; import * as os from 'os'; import * as cp from 'child_process'; import { IElement, ILocalizedStrings, ILocaleInfo } from './driver'; import { launch as launchPlaywrightBrowser } from './playwrightBrowser'; import { launch as launchPlaywrightElectron } from './playwrightElectron'; import { Logger, measureAndLog } from './logger'; -import { copyExtension } from './extensions'; import * as treekill from 'tree-kill'; import { teardown } from './processes'; import { PlaywrightDriver } from './playwrightDriver'; -const rootPath = join(__dirname, '../../..'); - export interface LaunchOptions { codePath?: string; readonly workspacePath: string; @@ -75,8 +71,6 @@ export async function launch(options: LaunchOptions): Promise { throw new Error('Smoke test process has terminated, refusing to spawn Code'); } - await measureAndLog(copyExtension(rootPath, options.extensionsPath, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', options.logger); - // Browser smoke tests if (options.web) { const { serverProcess, driver } = await measureAndLog(launchPlaywrightBrowser(options), 'launch playwright (browser)', options.logger); diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index c44a1105e1..b5dc6fe019 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -57,13 +57,6 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom const remoteDataDir = `${userDataDir}-server`; mkdirp.sync(remoteDataDir); - if (codePath) { - // running against a build: copy the test resolver extension into remote extensions dir - const remoteExtensionsDir = join(remoteDataDir, 'extensions'); - mkdirp.sync(remoteExtensionsDir); - await measureAndLog(copyExtension(root, remoteExtensionsDir, 'vscode-notebook-tests'), 'copyExtension(vscode-notebook-tests)', logger); - } - env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; env['TESTRESOLVER_LOGS_FOLDER'] = join(logsPath, 'server'); if (options.verbose) { @@ -71,8 +64,6 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom } } - args.push('--enable-proposed-api=vscode.vscode-notebook-tests'); - if (!codePath) { args.unshift(root); } diff --git a/test/automation/src/notebook.ts b/test/automation/src/notebook.ts index f7ba38eef3..bca8f73938 100644 --- a/test/automation/src/notebook.ts +++ b/test/automation/src/notebook.ts @@ -5,6 +5,7 @@ import { Code } from './code'; import { QuickAccess } from './quickaccess'; +import { QuickInput } from './quickinput'; const activeRowSelector = `.notebook-editor .monaco-list-row.focused`; @@ -12,11 +13,13 @@ export class Notebook { constructor( private readonly quickAccess: QuickAccess, + private readonly quickInput: QuickInput, private readonly code: Code) { } async openNotebook() { - await this.quickAccess.runCommand('vscode-notebook-tests.createNewNotebook'); + await this.quickAccess.openFileQuickAccessAndWait('notebook.ipynb', 1); + await this.quickInput.selectQuickInputElement(0); await this.code.waitForElement(activeRowSelector); await this.focusFirstCell(); await this.waitForActiveCellEditorContents('code()'); diff --git a/test/automation/src/workbench.ts b/test/automation/src/workbench.ts index e861c27c2d..5032a3850c 100644 --- a/test/automation/src/workbench.ts +++ b/test/automation/src/workbench.ts @@ -103,7 +103,7 @@ export class Workbench { this.addRemoteBookDialog = new AddRemoteBookDialog(code); this.taskPanel = new TaskPanel(code, this.quickaccess); // {{END}} - this.notebook = new Notebook(this.quickaccess, code); + this.notebook = new Notebook(this.quickaccess, this.quickinput, code); this.localization = new Localization(code); } }