Rename serializer extension and set up build scripts

This commit is contained in:
Rob Lourens 2021-07-20 14:59:35 -07:00
parent 58cc6980d6
commit 5793ae026a
22 changed files with 1109 additions and 5543 deletions

View file

@ -45,6 +45,7 @@ const compilations = [
'html-language-features/client/tsconfig.json',
'html-language-features/server/tsconfig.json',
'image-preview/tsconfig.json',
'ipynb/tsconfig.json',
'jake/tsconfig.json',
'json-language-features/client/tsconfig.json',
'json-language-features/server/tsconfig.json',

View file

@ -24,6 +24,7 @@ exports.dirs = [
'extensions/html-language-features',
'extensions/html-language-features/server',
'extensions/image-preview',
'extensions/ipynb',
'extensions/jake',
'extensions/json-language-features',
'extensions/json-language-features/server',

View file

@ -1,5 +1,4 @@
out
dist
node_modules
.vscode-test-web/
*.vsix

View file

@ -0,0 +1,6 @@
.vscode/**
src/**
out/**
tsconfig.json
extension.webpack.config.js
yarn.lock

View file

@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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;

View file

@ -7,11 +7,14 @@
'use strict';
const withDefaults = require('../../shared.webpack.config');
const withDefaults = require('../shared.webpack.config');
module.exports = withDefaults({
context: __dirname,
entry: {
extension: './src/extension.ts'
extension: './src/ipynbMain.ts',
},
output: {
filename: 'ipynbMain.js'
}
});

View file

@ -0,0 +1,66 @@
{
"name": "ipynb",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"version": "1.0.0",
"license": "MIT",
"engines": {
"vscode": "^1.57.0"
},
"activationEvents": [
"onNotebook:jupyter-notebook"
],
"extensionKind": [
"web",
"ui",
"workspace"
],
"main": "./out/ipynbMain.js",
"browser": "./dist/browser/ipynbMain.js",
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"languages": [
{
"id": "jupyter",
"aliases": [
"Jupyter"
],
"extensions": [
".ipynb"
]
}
],
"notebooks": [
{
"type": "jupyter-notebook",
"displayName": "Jupyter Notebook",
"selector": [
{
"filenamePattern": "*.ipynb"
}
],
"priority": "default"
}
]
},
"scripts": {
"compile": "npx gulp compile-extension:ipynb",
"watch": "npx gulp watch-extension:ipynb"
},
"dependencies": {
"detect-indent": "^6.0.0"
},
"devDependencies": {
"@jupyterlab/coreutils": "^3.1.0"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/vscode.git"
}
}

View file

@ -0,0 +1,4 @@
{
"displayName": ".ipynb support",
"description": "Provides basic support for opening and reading Jupyter's .ipynb notebook files"
}

View file

@ -0,0 +1,733 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nbformat } from '@jupyterlab/coreutils';
import { extensions, NotebookCell, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode';
export 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' : 'plaintext';
// Note, what ever 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);
}
export 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/svg+xml',
'image/png',
'image/jpeg',
'application/json',
'text/plain'
];
function sortOutputItemsBasedOnDisplayOrder(outputItems: NotebookCellOutputItem[]): NotebookCellOutputItem[] {
return outputItems.sort((outputItemA, outputItemB) => {
const isMimeTypeMatch = (value: string, compareWith: string) => {
if (value.endsWith('.*')) {
value = value.substr(0, value.indexOf('.*'));
}
return compareWith.startsWith(value);
};
const indexOfMimeTypeA = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(outputItemA.mime, mime));
const indexOfMimeTypeB = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(outputItemB.mime, mime));
return indexOfMimeTypeA - indexOfMimeTypeB;
});
}
export enum CellOutputMimeTypes {
error = 'application/vnd.code.notebook.error',
stderr = 'application/vnd.code.notebook.stderr',
stdout = 'application/vnd.code.notebook.stdout'
}
const textMimeTypes = ['text/plain', 'text/markdown', CellOutputMimeTypes.stderr, CellOutputMimeTypes.stdout];
export function concatMultilineString(str: string | string[], trim?: boolean): string {
const nonLineFeedWhiteSpaceTrim = /(^[\t\f\v\r ]+|[\t\f\v\r ]+$)/g; // Local var so don't have to reset the lastIndex.
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.
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);
}
}
export function createJupyterCellFromVSCNotebookCell(
vscCell: NotebookCell | NotebookCellData
): nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell {
let cell: nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell;
if (vscCell.kind === NotebookCellKind.Markup) {
cell = createMarkdownCellFromNotebookCell(vscCell);
} else if (
('document' in vscCell && vscCell.document.languageId === 'raw') ||
('languageId' in vscCell && vscCell.languageId === 'raw')
) {
cell = createRawCellFromNotebookCell(vscCell);
} else {
cell = createCodeCellFromNotebookCell(vscCell);
}
return cell;
}
function createCodeCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.ICodeCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const code = 'document' in cell ? cell.document.getText() : cell.value;
const codeCell: any = {
cell_type: 'code',
execution_count: cell.executionSummary?.executionOrder ?? null,
source: splitMultilineString(code),
outputs: (cell.outputs || []).map(translateCellDisplayOutput),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
return codeCell;
}
function createRawCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.IRawCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const rawCell: any = {
cell_type: 'raw',
source: splitMultilineString('document' in cell ? cell.document.getText() : cell.value),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
if (cellMetadata?.attachments) {
rawCell.attachments = cellMetadata.attachments;
}
return rawCell;
}
export function splitMultilineString(source: nbformat.MultilineString): string[] {
// Make sure a multiline string is back the way Jupyter expects it
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 [];
}
/**
* Metadata we store in VS Code cell output items.
* This contains the original metadata from the Jupyuter Outputs.
*/
export type CellOutputMetadata = {
/**
* Cell output metadata.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: any;
/**
* Transient data from Jupyter.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transient?: {
/**
* This is used for updating the output in other cells.
* We don't know of others properties, but this is definitely used.
*/
display_id?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} & 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;
};
export 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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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 =
<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) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unknownOutput.data = output.items.reduceRight((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;
}
export 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(new TextDecoder().decode(firstItem.data.buffer.slice(firstItem.data.byteOffset)));
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 || '')
};
}
export 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 = output.items
.filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout)
.map((opit) => convertOutputMimeToJupyterOutput(opit.mime, opit.data as Uint8Array) as string)
.reduceRight<string[]>((prev, curr) => (Array.isArray(curr) ? prev.concat(...curr) : prev.concat(curr)), []);
const streamType = getOutputStreamType(output) || 'stdout';
return {
output_type: 'stream',
name: streamType,
text: splitMultilineString(outputs.join(''))
};
}
function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) {
if (!value) {
return '';
}
try {
const stringValue = new TextDecoder().decode(value.buffer.slice(value.byteOffset));
if (mime === CellOutputMimeTypes.error) {
return JSON.parse(stringValue);
} else if (mime.startsWith('text/') || textMimeTypes.includes(mime)) {
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.
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_1_%E2%80%93_escaping_the_string_before_encoding_it
return btoa(encodeURIComponent(stringValue).replace(/%([0-9A-F]{2})/g, function (_match, p1) {
return String.fromCharCode(Number.parseInt('0x' + p1));
}));
} else if (mime.toLowerCase().includes('json')) {
return stringValue.length > 0 ? JSON.parse(stringValue) : stringValue;
} else {
return stringValue;
}
} catch (ex) {
return '';
}
}
function createMarkdownCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.IMarkdownCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const markdownCell: any = {
cell_type: 'markdown',
source: splitMultilineString('document' in cell ? cell.document.getText() : cell.value),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
if (cellMetadata?.attachments) {
markdownCell.attachments = cellMetadata.attachments;
}
return markdownCell;
}
/**
* Metadata we store in VS Code cells.
* This contains the original metadata from the Jupyuter cells.
*/
export type CellMetadata = {
/**
* Stores attachments for cells.
*/
attachments?: nbformat.IAttachments;
/**
* Stores cell metadata.
*/
metadata?: Partial<nbformat.ICellMetadata>;
};
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)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as nbformat.ICell; // nyc (code coverage) barfs on this so just trick it.
// Remove outputs and execution_count from non code cells
if (result.cell_type !== 'code') {
// Map to any so nyc will build.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (<any>result).outputs;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (<any>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: {}
};
export 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<string>;
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;
}
export 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]));
}
});
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[] = [];
// eslint-disable-next-line
const data: Record<string, any> = output.data || {};
// eslint-disable-next-line
for (const key in data) {
items.push(convertJupyterOutputToBuffer(key, data[key]));
}
return new NotebookCellOutput(sortOutputItemsBasedOnDisplayOrder(items), metadata);
}
export 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 factoryFn = output.name === 'stderr' ? NotebookCellOutputItem.stderr : NotebookCellOutputItem.stdout;
return new NotebookCellOutput([factoryFn(value)], getOutputMetadata(output));
}
const cellOutputMappers = new Map<nbformat.OutputType, (output: nbformat.IOutput) => NotebookCellOutput>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('display_data', translateDisplayDataOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('error', translateErrorOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('execute_result', translateDisplayDataOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('stream', translateStreamOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('update_display_data', translateDisplayDataOutput as any);
export function cellOutputToVSCCellOutput(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', [
"<svg baseProfile=\"full\" height=\"200\" version=\"1.1\" width=\"300\" xmlns=\"http://www.w3.org/2000/svg\">\n",
" <rect fill=\"blue\" height=\"100%\" width=\"100%\"/>\n",
" <circle cx=\"150\" cy=\"100\" fill=\"green\" r=\"80\"/>\n",
" <text fill=\"white\" font-size=\"60\" text-anchor=\"middle\" x=\"150\" y=\"125\">SVG</text>\n",
"</svg>"
]),
]),
]
*
*/
const fn = cellOutputMappers.get(output.output_type as nbformat.OutputType);
let result: NotebookCellOutput;
if (fn) {
result = fn(output);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = translateDisplayDataOutput(output as any);
}
return result;
}
export function createVSCCellOutputsFromOutputs(outputs?: nbformat.IOutput[]): NotebookCellOutput[] {
const cellOutputs: nbformat.IOutput[] = Array.isArray(outputs) ? (outputs as []) : [];
return cellOutputs.map(cellOutputToVSCCellOutput);
}
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 {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cellOutputs: nbformat.IOutput[] = Array.isArray(cell.outputs) ? cell.outputs : [];
const outputs = createVSCCellOutputsFromOutputs(cellOutputs);
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 cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguage);
cellData.outputs = outputs;
cellData.metadata = { custom: getNotebookCellMetadata(cell) };
cellData.executionSummary = executionSummary;
return cellData;
}
export function createVSCNotebookCellDataFromCell(
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);
}
default: {
}
}
return;
}
/**
* Converts a NotebookModel into VSCode friendly format.
*/
export function notebookModelToVSCNotebookData(
notebookContentWithoutCells: Exclude<Partial<nbformat.INotebookContent>, 'cells'>,
nbCells: nbformat.IBaseCell[],
preferredLanguage: string,
originalJson: Partial<nbformat.INotebookContent>
): NotebookData {
const cells = nbCells
.map((cell) => createVSCNotebookCellDataFromCell(preferredLanguage, cell))
.filter((item) => !!item)
.map((item) => item!);
if (cells.length === 0 && Object.keys(originalJson).length === 0) {
cells.push(new NotebookCellData(NotebookCellKind.Code, '', preferredLanguage));
}
const notebookData = new NotebookData(cells);
notebookData.metadata = { custom: notebookContentWithoutCells };
return notebookData;
}

View file

@ -10,4 +10,4 @@ export function activate(context: vscode.ExtensionContext) {
registerNotebookSerializer(context);
}
export function deactivate() {}
export function deactivate() { }

View file

@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { nbformat } from '@jupyterlab/coreutils';
import detectIndent = require('detect-indent');
import * as vscode from 'vscode';
import { defaultNotebookFormat } from './constants';
import { createJupyterCellFromVSCNotebookCell, getPreferredLanguage, notebookModelToVSCNotebookData, pruneCell } from './helpers';
export function registerNotebookSerializer(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jupyter-notebook', new NotebookSerializer(), {
transientOutputs: false,
transientCellMetadata: {
breakpointMargin: true,
inputCollapsed: true,
outputCollapsed: true,
custom: false
}
}));
}
export class NotebookSerializer implements vscode.NotebookSerializer {
public deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData {
let contents = '';
try {
contents = new TextDecoder().decode(content.buffer.slice(content.byteOffset));
} catch {
}
const json = contents ? (JSON.parse(contents) as Partial<nbformat.INotebookContent>) : {};
// Then compute indent. It's computed from the contents
const indentAmount = contents ? detectIndent(contents).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 = notebookModelToVSCNotebookData(
{ ...json, cells: [] },
json?.cells || [],
preferredCellLanguage,
json || {}
);
data.metadata = data.metadata || {};
data.metadata.indentAmount = indentAmount;
return data;
}
public serializeNotebookDocument(data: vscode.NotebookDocument): string {
return this.serialize(data);
}
public serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array {
return new TextEncoder().encode(this.serialize(data));
}
private serialize(data: vscode.NotebookDocument | vscode.NotebookData): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const notebookContent: Partial<nbformat.INotebookContent> = (data.metadata?.custom as any) || {};
notebookContent.cells = notebookContent.cells || [];
notebookContent.nbformat = notebookContent.nbformat || 4;
notebookContent.nbformat_minor = notebookContent.nbformat_minor || 2;
notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 };
// Override with what ever is in the metadata.
const indentAmount =
data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string'
? data.metadata.indentAmount
: ' ';
if ('notebookType' in data) {
notebookContent.cells = data
.getCells()
.map((cell) => createJupyterCellFromVSCNotebookCell(cell))
.map(pruneCell);
} else {
notebookContent.cells = data.cells.map((cell) => createJupyterCellFromVSCNotebookCell(cell)).map(pruneCell);
}
return JSON.stringify(notebookContent, undefined, indentAmount);
}
}

8
extensions/ipynb/src/types.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../src/vs/vscode.proposed.d.ts'/>

View file

@ -0,0 +1,12 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out",
"lib": [
"dom"
]
},
"include": [
"src/**/*"
]
}

154
extensions/ipynb/yarn.lock Normal file
View file

@ -0,0 +1,154 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@jupyterlab/coreutils@^3.1.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@jupyterlab/coreutils/-/coreutils-3.2.0.tgz#dd4d887bdedfea4c8545d46d297531749cb13724"
integrity sha512-LATiUsHuwze/h3JC2EZOBV+kGBoUKO3npqw/Pcgge4bz09xF/oTDrx4G8jl5eew3w1dCUNp9eLduNh8Orrw7xQ==
dependencies:
"@phosphor/commands" "^1.7.0"
"@phosphor/coreutils" "^1.3.1"
"@phosphor/disposable" "^1.3.0"
"@phosphor/properties" "^1.1.3"
"@phosphor/signaling" "^1.3.0"
ajv "^6.5.5"
json5 "^2.1.0"
minimist "~1.2.0"
moment "^2.24.0"
path-posix "~1.0.0"
url-parse "~1.4.3"
"@phosphor/algorithm@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@phosphor/algorithm/-/algorithm-1.2.0.tgz#4a19aa59261b7270be696672dc3f0663f7bef152"
integrity sha512-C9+dnjXyU2QAkWCW6QVDGExk4hhwxzAKf5/FIuYlHAI9X5vFv99PYm0EREDxX1PbMuvfFBZhPNu0PvuSDQ7sFA==
"@phosphor/commands@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@phosphor/commands/-/commands-1.7.2.tgz#df724f2896ae43c4a3a9e2b5a6445a15e0d60487"
integrity sha512-iSyBIWMHsus323BVEARBhuVZNnVel8USo+FIPaAxGcq+icTSSe6+NtSxVQSmZblGN6Qm4iw6I6VtiSx0e6YDgQ==
dependencies:
"@phosphor/algorithm" "^1.2.0"
"@phosphor/coreutils" "^1.3.1"
"@phosphor/disposable" "^1.3.1"
"@phosphor/domutils" "^1.1.4"
"@phosphor/keyboard" "^1.1.3"
"@phosphor/signaling" "^1.3.1"
"@phosphor/coreutils@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@phosphor/coreutils/-/coreutils-1.3.1.tgz#441e34f42340f7faa742a88b2a181947a88d7226"
integrity sha512-9OHCn8LYRcPU/sbHm5v7viCA16Uev3gbdkwqoQqlV+EiauDHl70jmeL7XVDXdigl66Dz0LI11C99XOxp+s3zOA==
"@phosphor/disposable@^1.3.0", "@phosphor/disposable@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@phosphor/disposable/-/disposable-1.3.1.tgz#be98fe12bd8c9a4600741cb83b0a305df28628f3"
integrity sha512-0NGzoTXTOizWizK/brKKd5EjJhuuEH4903tLika7q6wl/u0tgneJlTh7R+MBVeih0iNxtuJAfBa3IEY6Qmj+Sw==
dependencies:
"@phosphor/algorithm" "^1.2.0"
"@phosphor/signaling" "^1.3.1"
"@phosphor/domutils@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@phosphor/domutils/-/domutils-1.1.4.tgz#4c6aecf7902d3793b45db325319340e0a0b5543b"
integrity sha512-ivwq5TWjQpKcHKXO8PrMl+/cKqbgxPClPiCKc1gwbMd+6hnW5VLwNG0WBzJTxCzXK43HxX18oH+tOZ3E04wc3w==
"@phosphor/keyboard@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@phosphor/keyboard/-/keyboard-1.1.3.tgz#e5fd13af0479034ef0b5fffcf43ef2d4a266b5b6"
integrity sha512-dzxC/PyHiD6mXaESRy6PZTd9JeK+diwG1pyngkyUf127IXOEzubTIbu52VSdpGBklszu33ws05BAGDa4oBE4mQ==
"@phosphor/properties@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@phosphor/properties/-/properties-1.1.3.tgz#63e4355be5e22a411c566fd1860207038f171598"
integrity sha512-GiglqzU77s6+tFVt6zPq9uuyu/PLQPFcqZt914ZhJ4cN/7yNI/SLyMzpYZ56IRMXvzK9TUgbRna6URE3XAwFUg==
"@phosphor/signaling@^1.3.0", "@phosphor/signaling@^1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@phosphor/signaling/-/signaling-1.3.1.tgz#1cd10b069bdb2c9adb3ba74245b30141e5afc2d7"
integrity sha512-Eq3wVCPQAhUd9+gUGaYygMr+ov7dhSGblSBXiDzpZlSIfa8OVD4P3cCvYXr/acDTNmZ/gHTcSFO8/n3rDkeXzg==
dependencies:
"@phosphor/algorithm" "^1.2.0"
ajv@^6.5.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
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==
fast-deep-equal@^3.1.1:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json5@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
dependencies:
minimist "^1.2.5"
minimist@^1.2.5, minimist@~1.2.0:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
moment@^2.24.0:
version "2.29.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
path-posix@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f"
integrity sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==
requires-port@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
url-parse@~1.4.3:
version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"

View file

@ -1,59 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
'use strict';
//@ts-check
/** @typedef {import('webpack').Configuration} WebpackConfig **/
const path = require('path');
const webpack = require('webpack');
module.exports = /** @type WebpackConfig */ {
context: path.dirname(__dirname),
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
target: 'webworker', // extensions run in a webworker context
entry: {
'extension': './src/extension.ts',
},
resolve: {
mainFields: ['module', 'main'],
extensions: ['.ts', '.js'], // support ts-files and js-files
alias: {
},
fallback: {
'assert': require.resolve('assert')
}
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}]
},
plugins: [
new webpack.ProvidePlugin({
process: 'process/browser',
}),
],
externals: {
'vscode': 'commonjs vscode', // ignored because it doesn't exist
},
performance: {
hints: false
},
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist'),
libraryTarget: 'commonjs'
},
devtool: 'nosources-source-map'
};

File diff suppressed because it is too large Load diff

View file

@ -1,95 +0,0 @@
{
"name": "jupyter",
"displayName": "%displayName%",
"description": "%description%",
"publisher": "vscode",
"version": "0.0.1",
"license": "MIT",
"engines": {
"vscode": "^1.57.0"
},
"keywords": [
"jupyter",
"notebook",
"ipynb"
],
"categories": [
"Other"
],
"activationEvents": [
"onLanguage:jupyter",
"onNotebook:jupyter-notebook"
],
"extensionKind": [
"web",
"ui",
"workspace"
],
"main": "./dist/extension.js",
"browser": "./dist/extension.js",
"capabilities": {
"virtualWorkspaces": true,
"untrustedWorkspaces": {
"supported": true
}
},
"contributes": {
"languages": [
{
"id": "jupyter",
"aliases": [
"Jupyter",
"Notebook"
],
"extensions": [
".ipynb"
]
}
],
"notebooks": [
{
"type": "jupyter-notebook",
"displayName": "Jupyter Notebook",
"selector": [
{
"filenamePattern": "*.ipynb"
}
],
"priority": "default"
}
]
},
"scripts": {
"test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js",
"pretest": "npm run compile-web",
"vscode:prepublish": "npm run package-web",
"compile-web": "webpack --config ./build/web-extension.webpack.config.js",
"watch-web": "webpack --watch --config ./build/web-extension.webpack.config.js",
"package-web": "webpack --mode production --devtool hidden-source-map --config ./build/web-extension.webpack.config.js",
"lint": "eslint src --ext ts"
},
"dependencies": {
"detect-indent": "^6.0.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@jupyterlab/coreutils": "^3.1.0",
"@types/lodash": "^4.14.104",
"@types/mocha": "^8.2.2",
"@types/node": "14.x",
"@types/vscode": "^1.57.0",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.26.0",
"@typescript-eslint/parser": "^4.26.0",
"assert": "^2.0.0",
"eslint": "^7.27.0",
"mocha": "^8.4.0",
"process": "^0.11.10",
"ts-loader": "^9.2.2",
"typescript": "^4.3.2",
"vscode-test-web": "^0.0.2",
"webpack": "^5.38.1",
"webpack-cli": "^4.7.0",
"vsce": "^1.95.0"
}
}

View file

@ -1,732 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { nbformat } from '@jupyterlab/coreutils';
import { extensions, NotebookCell, NotebookCellData, NotebookCellExecutionSummary, NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from 'vscode';
import cloneDeep = require('lodash/cloneDeep');
export 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' : 'plaintext';
// Note, what ever 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);
}
export 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/svg+xml',
'image/png',
'image/jpeg',
'application/json',
'text/plain'
];
function sortOutputItemsBasedOnDisplayOrder(outputItems: NotebookCellOutputItem[]): NotebookCellOutputItem[] {
return outputItems.sort((outputItemA, outputItemB) => {
const isMimeTypeMatch = (value: string, compareWith: string) => {
if (value.endsWith('.*')) {
value = value.substr(0, value.indexOf('.*'));
}
return compareWith.startsWith(value);
};
const indexOfMimeTypeA = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(outputItemA.mime, mime));
const indexOfMimeTypeB = orderOfMimeTypes.findIndex((mime) => isMimeTypeMatch(outputItemB.mime, mime));
return indexOfMimeTypeA - indexOfMimeTypeB;
});
}
export enum CellOutputMimeTypes {
error = 'application/vnd.code.notebook.error',
stderr = 'application/vnd.code.notebook.stderr',
stdout = 'application/vnd.code.notebook.stdout'
}
const textMimeTypes = ['text/plain', 'text/markdown', CellOutputMimeTypes.stderr, CellOutputMimeTypes.stdout];
export function concatMultilineString(str: string | string[], trim?: boolean): string {
const nonLineFeedWhiteSpaceTrim = /(^[\t\f\v\r ]+|[\t\f\v\r ]+$)/g; // Local var so don't have to reset the lastIndex.
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.
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);
}
}
export function createJupyterCellFromVSCNotebookCell(
vscCell: NotebookCell | NotebookCellData
): nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell {
let cell: nbformat.IRawCell | nbformat.IMarkdownCell | nbformat.ICodeCell;
if (vscCell.kind === NotebookCellKind.Markup) {
cell = createMarkdownCellFromNotebookCell(vscCell);
} else if (
('document' in vscCell && vscCell.document.languageId === 'raw') ||
('languageId' in vscCell && vscCell.languageId === 'raw')
) {
cell = createRawCellFromNotebookCell(vscCell);
} else {
cell = createCodeCellFromNotebookCell(vscCell);
}
return cell;
}
function createCodeCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.ICodeCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const code = 'document' in cell ? cell.document.getText() : cell.value;
const codeCell: any = {
cell_type: 'code',
execution_count: cell.executionSummary?.executionOrder ?? null,
source: splitMultilineString(code),
outputs: (cell.outputs || []).map(translateCellDisplayOutput),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
return codeCell;
}
function createRawCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.IRawCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const rawCell: any = {
cell_type: 'raw',
source: splitMultilineString('document' in cell ? cell.document.getText() : cell.value),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
if (cellMetadata?.attachments) {
rawCell.attachments = cellMetadata.attachments;
}
return rawCell;
}
export function splitMultilineString(source: nbformat.MultilineString): string[] {
// Make sure a multiline string is back the way Jupyter expects it
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 [];
}
/**
* Metadata we store in VS Code cell output items.
* This contains the original metadata from the Jupyuter Outputs.
*/
export type CellOutputMetadata = {
/**
* Cell output metadata.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
metadata?: any;
/**
* Transient data from Jupyter.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transient?: {
/**
* This is used for updating the output in other cells.
* We don't know of others properties, but this is definitely used.
*/
display_id?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} & 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;
};
export 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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: output.items.reduceRight((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 =
<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) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
unknownOutput.data = output.items.reduceRight((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;
}
export 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(new TextDecoder().decode(firstItem.data.buffer.slice(firstItem.data.byteOffset)));
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 || '')
};
}
export function getOutputStreamType(output: NotebookCellOutput): string | undefined {
if (output.items.length > 0) {
return output.items[0].mime === CellOutputMimeTypes.stderr ? 'stderr' : 'stdout';
}
}
type JupyterOutput =
| nbformat.IUnrecognizedOutput
| nbformat.IExecuteResult
| nbformat.IDisplayData
| nbformat.IStream
| nbformat.IError;
function convertStreamOutput(output: NotebookCellOutput): JupyterOutput {
const outputs = output.items
.filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout)
.map((opit) => convertOutputMimeToJupyterOutput(opit.mime, opit.data as Uint8Array) as string)
.reduceRight<string[]>((prev, curr) => (Array.isArray(curr) ? prev.concat(...curr) : prev.concat(curr)), []);
const streamType = getOutputStreamType(output) || 'stdout';
return {
output_type: 'stream',
name: streamType,
text: splitMultilineString(outputs.join(''))
};
}
function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) {
if (!value) {
return '';
}
try {
const stringValue = new TextDecoder().decode(value.buffer.slice(value.byteOffset));
if (mime === CellOutputMimeTypes.error) {
return JSON.parse(stringValue);
} else if (mime.startsWith('text/') || textMimeTypes.includes(mime)) {
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.
// https://developer.mozilla.org/en-US/docs/Glossary/Base64#solution_1_%E2%80%93_escaping_the_string_before_encoding_it
return btoa(encodeURIComponent(stringValue).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode(Number.parseInt('0x' + p1));
}));
} else if (mime.toLowerCase().includes('json')) {
return stringValue.length > 0 ? JSON.parse(stringValue) : stringValue;
} else {
return stringValue;
}
} catch (ex) {
return '';
}
}
function createMarkdownCellFromNotebookCell(cell: NotebookCell | NotebookCellData): nbformat.IMarkdownCell {
const cellMetadata = cell.metadata?.custom as CellMetadata | undefined;
const markdownCell: any = {
cell_type: 'markdown',
source: splitMultilineString('document' in cell ? cell.document.getText() : cell.value),
metadata: cellMetadata?.metadata || {} // This cannot be empty.
};
if (cellMetadata?.attachments) {
markdownCell.attachments = cellMetadata.attachments;
}
return markdownCell;
}
/**
* Metadata we store in VS Code cells.
* This contains the original metadata from the Jupyuter cells.
*/
export type CellMetadata = {
/**
* Stores attachments for cells.
*/
attachments?: nbformat.IAttachments;
/**
* Stores cell metadata.
*/
metadata?: Partial<nbformat.ICellMetadata>;
};
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)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) as nbformat.ICell; // nyc (code coverage) barfs on this so just trick it.
// Remove outputs and execution_count from non code cells
if (result.cell_type !== 'code') {
// Map to any so nyc will build.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (<any>result).outputs;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (<any>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: {}
};
export 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<string>;
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;
}
export 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'];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const custom: CellMetadata = {};
propertiesToClone.forEach((propertyToClone) => {
if (cell[propertyToClone]) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
custom[propertyToClone] = cloneDeep(cell[propertyToClone]) as any;
}
});
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 ? cloneDeep(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[] = [];
// eslint-disable-next-line
const data: Record<string, any> = output.data || {};
// eslint-disable-next-line
for (const key in data) {
items.push(convertJupyterOutputToBuffer(key, data[key]));
}
return new NotebookCellOutput(sortOutputItemsBasedOnDisplayOrder(items), metadata);
}
export 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 factoryFn = output.name === 'stderr' ? NotebookCellOutputItem.stderr : NotebookCellOutputItem.stdout;
return new NotebookCellOutput([factoryFn(value)], getOutputMetadata(output));
}
const cellOutputMappers = new Map<nbformat.OutputType, (output: nbformat.IOutput) => NotebookCellOutput>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('display_data', translateDisplayDataOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('error', translateErrorOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('execute_result', translateDisplayDataOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('stream', translateStreamOutput as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
cellOutputMappers.set('update_display_data', translateDisplayDataOutput as any);
export function cellOutputToVSCCellOutput(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', [
"<svg baseProfile=\"full\" height=\"200\" version=\"1.1\" width=\"300\" xmlns=\"http://www.w3.org/2000/svg\">\n",
" <rect fill=\"blue\" height=\"100%\" width=\"100%\"/>\n",
" <circle cx=\"150\" cy=\"100\" fill=\"green\" r=\"80\"/>\n",
" <text fill=\"white\" font-size=\"60\" text-anchor=\"middle\" x=\"150\" y=\"125\">SVG</text>\n",
"</svg>"
]),
]),
]
*
*/
const fn = cellOutputMappers.get(output.output_type as nbformat.OutputType);
let result: NotebookCellOutput;
if (fn) {
result = fn(output);
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
result = translateDisplayDataOutput(output as any);
}
return result;
}
export function createVSCCellOutputsFromOutputs(outputs?: nbformat.IOutput[]): NotebookCellOutput[] {
const cellOutputs: nbformat.IOutput[] = Array.isArray(outputs) ? (outputs as []) : [];
return cellOutputs.map(cellOutputToVSCCellOutput);
}
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 {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cellOutputs: nbformat.IOutput[] = Array.isArray(cell.outputs) ? cell.outputs : [];
const outputs = createVSCCellOutputsFromOutputs(cellOutputs);
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 cellData = new NotebookCellData(NotebookCellKind.Code, source, cellLanguage);
cellData.outputs = outputs;
cellData.metadata = { custom: getNotebookCellMetadata(cell) };
cellData.executionSummary = executionSummary;
return cellData;
}
export function createVSCNotebookCellDataFromCell(
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);
}
default: {
}
}
}
/**
* Converts a NotebookModel into VSCode friendly format.
*/
export function notebookModelToVSCNotebookData(
notebookContentWithoutCells: Exclude<Partial<nbformat.INotebookContent>, 'cells'>,
nbCells: nbformat.IBaseCell[],
preferredLanguage: string,
originalJson: Partial<nbformat.INotebookContent>
): NotebookData {
const cells = nbCells
.map((cell) => createVSCNotebookCellDataFromCell(preferredLanguage, cell))
.filter((item) => !!item)
.map((item) => item!);
if (cells.length === 0 && Object.keys(originalJson).length === 0) {
cells.push(new NotebookCellData(NotebookCellKind.Code, '', preferredLanguage));
}
const notebookData = new NotebookData(cells);
notebookData.metadata = { custom: notebookContentWithoutCells };
return notebookData;
}

View file

@ -1,92 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type { nbformat } from '@jupyterlab/coreutils';
import detectIndent = require('detect-indent');
import * as vscode from 'vscode';
import { defaultNotebookFormat } from './constants';
import { createJupyterCellFromVSCNotebookCell, getPreferredLanguage, notebookModelToVSCNotebookData, pruneCell } from './helpers';
export function registerNotebookSerializer(context: vscode.ExtensionContext) {
context.subscriptions.push(vscode.workspace.registerNotebookSerializer('jupyter-notebook', new NotebookSerializer(), {
transientOutputs: false,
transientCellMetadata: {
breakpointMargin: true,
inputCollapsed: true,
outputCollapsed: true,
custom: false
}
}));
}
export class NotebookSerializer implements vscode.NotebookSerializer {
public deserializeNotebook(content: Uint8Array, _token: vscode.CancellationToken): vscode.NotebookData {
const contents = new TextDecoder().decode(content.buffer.slice(content.byteOffset));
const json = contents ? (JSON.parse(contents) as Partial<nbformat.INotebookContent>) : {};
// Then compute indent. It's computed from the contents
const indentAmount = contents ? detectIndent(contents).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 = notebookModelToVSCNotebookData(
{ ...json, cells: [] },
json?.cells || [],
preferredCellLanguage,
json || {}
);
data.metadata = data.metadata || {};
data.metadata.indentAmount = indentAmount;
return data;
}
public serializeNotebookDocument(data: vscode.NotebookDocument): string {
return this.serialize(data);
}
public serializeNotebook(data: vscode.NotebookData, _token: vscode.CancellationToken): Uint8Array {
return new TextEncoder().encode(this.serialize(data));
}
private serialize(data: vscode.NotebookDocument | vscode.NotebookData): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const notebookContent: Partial<nbformat.INotebookContent> = (data.metadata?.custom as any) || {};
notebookContent.cells = notebookContent.cells || [];
notebookContent.nbformat = notebookContent.nbformat || 4;
notebookContent.nbformat_minor = notebookContent.nbformat_minor || 2;
notebookContent.metadata = notebookContent.metadata || { orig_nbformat: 4 };
// Override with what ever is in the metadata.
const indentAmount =
data.metadata && 'indentAmount' in data.metadata && typeof data.metadata.indentAmount === 'string'
? data.metadata.indentAmount
: ' ';
if ('notebookType' in data) {
notebookContent.cells = data
.getCells()
.map((cell) => createJupyterCellFromVSCNotebookCell(cell))
.map(pruneCell);
} else {
notebookContent.cells = data.cells.map((cell) => createJupyterCellFromVSCNotebookCell(cell)).map(pruneCell);
}
return JSON.stringify(notebookContent, undefined, indentAmount);
}
}

View file

@ -1,21 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"outDir": "dist",
"lib": [
"es6", "WebWorker"
],
"sourceMap": true,
"rootDir": "src",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
},
"exclude": [
"node_modules",
".vscode-test-web"
]
}