Path: blob/main/extensions/copilot/src/util/common/test/shims/notebookDocument.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { StringSHA1 } from '../../../vs/base/common/hash';7import { Schemas } from '../../../vs/base/common/network';8import { URI as Uri } from '../../../vs/base/common/uri';9import { NotebookCellKind, NotebookCellOutput, NotebookCellOutputItem, NotebookData } from '../../../vs/workbench/api/common/extHostTypes/notebooks';10import { createTextDocumentData, IExtHostDocumentData } from './textDocument';1112interface ISimulationWorkspace {13addDocument(doc: IExtHostDocumentData): void;14addNotebookDocument(notebook: ExtHostNotebookDocumentData): void;15}1617export interface NotebookCellExecutionSummary {18}1920declare type OutputType = 'execute_result' | 'display_data' | 'stream' | 'error' | 'update_display_data';2122function concatMultilineString(str: string | string[], trim?: boolean): string {23const nonLineFeedWhiteSpaceTrim = /(^[\t\f\v\r ]+|[\t\f\v\r ]+$)/g;24if (Array.isArray(str)) {25let result = '';26for (let i = 0; i < str.length; i += 1) {27const s = str[i];28if (i < str.length - 1 && !s.endsWith('\n')) {29result = result.concat(`${s}\n`);30} else {31result = result.concat(s);32}33}3435// Just trim whitespace. Leave \n in place36return trim ? result.replace(nonLineFeedWhiteSpaceTrim, '') : result;37}38return trim ? str.toString().replace(nonLineFeedWhiteSpaceTrim, '') : str.toString();39}4041enum CellOutputMimeTypes {42error = 'application/vnd.code.notebook.error',43stderr = 'application/vnd.code.notebook.stderr',44stdout = 'application/vnd.code.notebook.stdout'45}4647const textMimeTypes = ['text/plain', 'text/markdown', 'text/latex', CellOutputMimeTypes.stderr, CellOutputMimeTypes.stdout];484950function convertJupyterOutputToBuffer(mime: string, value: unknown): NotebookCellOutputItem {51if (!value) {52return NotebookCellOutputItem.text('', mime);53}54try {55if (56(mime.startsWith('text/') || textMimeTypes.includes(mime)) &&57(Array.isArray(value) || typeof value === 'string')58) {59const stringValue = Array.isArray(value) ? concatMultilineString(value) : value;60return NotebookCellOutputItem.text(stringValue, mime);61} else if (mime.startsWith('image/') && typeof value === 'string' && mime !== 'image/svg+xml') {62// Images in Jupyter are stored in base64 encoded format.63// VS Code expects bytes when rendering images.64if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {65return new NotebookCellOutputItem(Buffer.from(value, 'base64'), mime);66} else {67const data = Uint8Array.from(atob(value), c => c.charCodeAt(0));68return new NotebookCellOutputItem(data, mime);69}70} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {71return NotebookCellOutputItem.text(JSON.stringify(value), mime);72} else if (mime === 'application/json') {73return NotebookCellOutputItem.json(value, mime);74} else {75// For everything else, treat the data as strings (or multi-line strings).76value = Array.isArray(value) ? concatMultilineString(value) : value;77return NotebookCellOutputItem.text(value as string, mime);78}79} catch (ex) {80return NotebookCellOutputItem.error(ex);81}82}8384export function translateDisplayDataOutput(85output: any86): NotebookCellOutput {87const items: NotebookCellOutputItem[] = [];88if (output.data) {89for (const key in output.data) {90items.push(convertJupyterOutputToBuffer(key, output.data[key]));91}92}9394return new NotebookCellOutput(items, {});95}9697export function translateErrorOutput(output?: any): NotebookCellOutput {98output = output || { output_type: 'error', ename: '', evalue: '', traceback: [] };99return new NotebookCellOutput(100[101NotebookCellOutputItem.error({102name: output?.ename || '',103message: output?.evalue || '',104stack: (output?.traceback || []).join('\n')105})106],107{ originalError: output }108);109}110111export function translateStreamOutput(output: any): NotebookCellOutput {112const value = concatMultilineString(output.text);113const item = output.name === 'stderr' ? NotebookCellOutputItem.stderr(value) : NotebookCellOutputItem.stdout(value);114return new NotebookCellOutput([item], {});115}116117const cellOutputMappers = new Map<OutputType, (output: any) => NotebookCellOutput>();118cellOutputMappers.set('display_data', translateDisplayDataOutput);119cellOutputMappers.set('execute_result', translateDisplayDataOutput);120cellOutputMappers.set('update_display_data', translateDisplayDataOutput);121cellOutputMappers.set('error', translateErrorOutput);122cellOutputMappers.set('stream', translateStreamOutput);123124125export function jupyterCellOutputToCellOutput(output: any): NotebookCellOutput {126const fn = cellOutputMappers.get(output.output_type);127let result: NotebookCellOutput;128if (fn) {129result = fn(output);130} else {131result = translateDisplayDataOutput(output as any);132}133return result;134}135136const textDecoder = new TextDecoder();137138export interface CellOutputMetadata {139metadata?: any;140transient?: {141display_id?: string;142} & any;143144outputType: OutputType | string;145executionCount?: number | null;146__isJson?: boolean;147}148149function splitMultilineString(source: string | string[]): string[] {150if (Array.isArray(source)) {151return source as string[];152}153const str = source.toString();154if (str.length > 0) {155// Each line should be a separate entry, but end with a \n if not last entry156const arr = str.split('\n');157return arr158.map((s, i) => {159if (i < arr.length - 1) {160return `${s}\n`;161}162return s;163})164.filter(s => s.length > 0); // Skip last one if empty (it's the only one that could be length 0)165}166return [];167}168169function translateCellErrorOutput(output: vscode.NotebookCellOutput) {170// it should have at least one output item171const firstItem = output.items[0];172// Bug in VS Code.173if (!firstItem.data) {174return {175output_type: 'error',176ename: '',177evalue: '',178traceback: []179};180}181const originalError = output.metadata?.originalError;182const value: Error = JSON.parse(textDecoder.decode(firstItem.data));183return {184output_type: 'error',185ename: value.name,186evalue: value.message,187// VS Code needs an `Error` object which requires a `stack` property as a string.188// Its possible the format could change when converting from `traceback` to `string` and back again to `string`189// When .NET stores errors in output (with their .NET kernel),190// stack is empty, hence store the message instead of stack (so that somethign gets displayed in ipynb).191traceback: originalError?.traceback || splitMultilineString(value.stack || value.message || '')192};193}194195function convertStreamOutput(output: vscode.NotebookCellOutput) {196const outputs: string[] = [];197output.items198.filter((opit) => opit.mime === CellOutputMimeTypes.stderr || opit.mime === CellOutputMimeTypes.stdout)199.map((opit) => textDecoder.decode(opit.data))200.forEach(value => {201// Ensure each line is a separate entry in an array (ending with \n).202const lines = value.split('\n');203// If the last item in `outputs` is not empty and the first item in `lines` is not empty, then concate them.204// As they are part of the same line.205if (outputs.length && lines.length && lines[0].length > 0) {206outputs[outputs.length - 1] = `${outputs[outputs.length - 1]}${lines.shift()!}`;207}208for (const line of lines) {209outputs.push(line);210}211});212213for (let index = 0; index < (outputs.length - 1); index++) {214outputs[index] = `${outputs[index]}\n`;215}216217// Skip last one if empty (it's the only one that could be length 0)218if (outputs.length && outputs[outputs.length - 1].length === 0) {219outputs.pop();220}221222const streamType = getOutputStreamType(output) || 'stdout';223224return {225output_type: 'stream',226name: streamType,227text: outputs228};229}230231function getOutputStreamType(output: vscode.NotebookCellOutput): string | undefined {232if (output.items.length > 0) {233return output.items[0].mime === CellOutputMimeTypes.stderr ? 'stderr' : 'stdout';234}235236return;237}238239function convertOutputMimeToJupyterOutput(mime: string, value: Uint8Array) {240if (!value) {241return '';242}243try {244if (mime === CellOutputMimeTypes.error) {245const stringValue = textDecoder.decode(value);246return JSON.parse(stringValue);247} else if (mime.startsWith('text/') || textMimeTypes.includes(mime)) {248const stringValue = textDecoder.decode(value);249return splitMultilineString(stringValue);250} else if (mime.startsWith('image/') && mime !== 'image/svg+xml') {251// Images in Jupyter are stored in base64 encoded format.252// VS Code expects bytes when rendering images.253if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {254return Buffer.from(value).toString('base64');255} else {256return btoa(value.reduce((s: string, b: number) => s + String.fromCharCode(b), ''));257}258} else if (mime.toLowerCase().includes('json')) {259const stringValue = textDecoder.decode(value);260return stringValue.length > 0 ? JSON.parse(stringValue) : stringValue;261} else if (mime === 'image/svg+xml') {262return splitMultilineString(textDecoder.decode(value));263} else {264return textDecoder.decode(value);265}266} catch (ex) {267return '';268}269}270271function translateCellDisplayOutput(output: vscode.NotebookCellOutput): any {272const customMetadata = output.metadata as CellOutputMetadata | undefined;273let result;274// Possible some other extension added some output (do best effort to translate & save in ipynb).275// In which case metadata might not contain `outputType`.276const outputType = customMetadata?.outputType as OutputType;277switch (outputType) {278case 'error': {279result = translateCellErrorOutput(output);280break;281}282case 'stream': {283result = convertStreamOutput(output);284break;285}286case 'display_data': {287result = {288output_type: 'display_data',289data: output.items.reduce((prev: any, curr) => {290prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);291return prev;292}, {}),293metadata: customMetadata?.metadata || {} // This can never be undefined.294};295break;296}297case 'execute_result': {298result = {299output_type: 'execute_result',300data: output.items.reduce((prev: any, curr) => {301prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);302return prev;303}, {}),304metadata: customMetadata?.metadata || {}, // This can never be undefined.305execution_count:306typeof customMetadata?.executionCount === 'number' ? customMetadata?.executionCount : null // This can never be undefined, only a number or `null`.307};308break;309}310case 'update_display_data': {311result = {312output_type: 'update_display_data',313data: output.items.reduce((prev: any, curr) => {314prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);315return prev;316}, {}),317metadata: customMetadata?.metadata || {} // This can never be undefined.318};319break;320}321default: {322const isError =323output.items.length === 1 && output.items.every((item) => item.mime === CellOutputMimeTypes.error);324const isStream = output.items.every(325(item) => item.mime === CellOutputMimeTypes.stderr || item.mime === CellOutputMimeTypes.stdout326);327328if (isError) {329return translateCellErrorOutput(output);330}331332// In the case of .NET & other kernels, we need to ensure we save ipynb correctly.333// Hence if we have stream output, save the output as Jupyter `stream` else `display_data`334// Unless we already know its an unknown output type.335const outputType: OutputType =336<OutputType>customMetadata?.outputType || (isStream ? 'stream' : 'display_data');337let unknownOutput: any;338if (outputType === 'stream') {339// If saving as `stream` ensure the mandatory properties are set.340unknownOutput = convertStreamOutput(output);341} else if (outputType === 'display_data') {342// If saving as `display_data` ensure the mandatory properties are set.343const displayData = {344data: {},345metadata: {},346output_type: 'display_data'347};348unknownOutput = displayData;349} else {350unknownOutput = {351output_type: outputType352};353}354if (customMetadata?.metadata) {355unknownOutput.metadata = customMetadata.metadata;356}357if (output.items.length > 0) {358unknownOutput.data = output.items.reduce((prev: any, curr) => {359prev[curr.mime] = convertOutputMimeToJupyterOutput(curr.mime, curr.data as Uint8Array);360return prev;361}, {});362}363result = unknownOutput;364break;365}366}367368// Account for transient data as well369// `transient.display_id` is used to update cell output in other cells, at least thats one use case we know of.370if (result && customMetadata && customMetadata.transient) {371result.transient = customMetadata.transient;372}373return result;374}375376377export class ExtHostCell {378index: number;379notebook: ExtHostNotebookDocumentData;380kind: NotebookCellKind;381documentData: IExtHostDocumentData;382metadata: { readonly [key: string]: any };383private _outputs: vscode.NotebookCellOutput[];384executionSummary: NotebookCellExecutionSummary | undefined;385386get document() {387return this.documentData.document;388}389390private _apiCell: vscode.NotebookCell | undefined;391392constructor(393index: number,394kind: NotebookCellKind,395notebook: ExtHostNotebookDocumentData,396documentData: IExtHostDocumentData,397metadata: { readonly [key: string]: any },398outputs: vscode.NotebookCellOutput[],399executionSummary: NotebookCellExecutionSummary | undefined,400) {401this.documentData = documentData;402this.index = index;403this.kind = kind;404this.metadata = metadata;405this._outputs = outputs;406this.executionSummary = executionSummary;407this.notebook = notebook;408}409410get apiCell(): vscode.NotebookCell {411if (!this._apiCell) {412const that = this;413const apiCell: vscode.NotebookCell = {414get index() { return that.notebook.getCellIndex(that); },415notebook: that.notebook.document,416kind: that.kind,417document: that.document,418get outputs() { return that._outputs.slice(0); },419get metadata() { return that.metadata; },420get executionSummary() { return that.executionSummary; }421};422this._apiCell = Object.freeze(apiCell);423}424return this._apiCell;425}426427appendOutput(outputs: vscode.NotebookCellOutput[]) {428this._outputs.push(...outputs);429}430}431432433function generateCellFragment(index: number): string {434const hash = new StringSHA1();435hash.update(`index${index}`);436return hash.digest().substring(0, 8);437}438439export class ExtHostNotebookDocumentData {440public static createJupyterNotebook(uri: Uri, contents: string, simulationWorkspace?: ISimulationWorkspace): ExtHostNotebookDocumentData {441const notebook = JSON.parse(contents);442const codeLanguageId = notebook.metadata?.language_info?.language ?? notebook.metadata?.language_info?.name ?? 'python';443const notebookDocument = new ExtHostNotebookDocumentData(uri, 'jupyter-notebook', notebook.metadata, []);444const cells: ExtHostCell[] = [];445446for (const [index, cell] of notebook.cells.entries()) {447const content = cell.source.join('');448449if (cell.cell_type === 'code') {450const doc = createTextDocumentData(uri.with({ scheme: Schemas.vscodeNotebookCell, fragment: generateCellFragment(index) }), content, codeLanguageId);451if (simulationWorkspace) {452simulationWorkspace.addDocument(doc);453}454const cellOutputs = Array.isArray(cell.outputs) ? cell.outputs : [];455const outputs = cellOutputs.map(jupyterCellOutputToCellOutput);456457cells.push(new ExtHostCell(index, NotebookCellKind.Code, notebookDocument, doc, cell.metadata, outputs, undefined));458} else {459const doc = createTextDocumentData(uri.with({ scheme: Schemas.vscodeNotebookCell, fragment: generateCellFragment(index) }), content, 'markdown');460if (simulationWorkspace) {461simulationWorkspace.addDocument(doc);462}463cells.push(new ExtHostCell(index, NotebookCellKind.Markup, notebookDocument, doc, cell.metadata, [], undefined));464}465}466notebookDocument.cells = cells;467468if (simulationWorkspace) {469simulationWorkspace.addNotebookDocument(notebookDocument);470}471472return notebookDocument;473}474475public static createGithubIssuesNotebook(uri: Uri, contents: string, simulationWorkspace?: ISimulationWorkspace): ExtHostNotebookDocumentData {476const notebook = JSON.parse(contents);477const notebookDocument = new ExtHostNotebookDocumentData(uri, 'github-issues', {}, []);478const cells: ExtHostCell[] = [];479480for (const [index, cell] of notebook.entries()) {481const doc = createTextDocumentData(uri.with({ scheme: Schemas.vscodeNotebookCell, fragment: generateCellFragment(index) }), cell.value, cell.language);482if (simulationWorkspace) {483simulationWorkspace.addDocument(doc);484}485cells.push(new ExtHostCell(index, cell.kind, notebookDocument, doc, {}, [], undefined));486}487notebookDocument.cells = cells;488489if (simulationWorkspace) {490simulationWorkspace.addNotebookDocument(notebookDocument);491}492return notebookDocument;493}494495public static fromNotebookData(uri: Uri, data: NotebookData, notebookType: string, simulationWorkspace?: ISimulationWorkspace): ExtHostNotebookDocumentData {496const notebookDocument = new ExtHostNotebookDocumentData(uri, notebookType, data.metadata || {}, []);497const cells: ExtHostCell[] = [];498499for (const [index, cell] of data.cells.entries()) {500const doc = createTextDocumentData(uri.with({ scheme: Schemas.vscodeNotebookCell, fragment: generateCellFragment(index) }), cell.value, cell.languageId);501if (cell.outputs?.length) {502throw new Error('Not implemented');503}504if (simulationWorkspace) {505simulationWorkspace.addDocument(doc);506}507cells.push(new ExtHostCell(index, cell.kind, notebookDocument, doc, cell.metadata || {}, [], undefined));508}509notebookDocument.cells = cells;510if (simulationWorkspace) {511simulationWorkspace.addNotebookDocument(notebookDocument);512}513514return notebookDocument;515}516517public static applyEdits(notebookDocument: ExtHostNotebookDocumentData, edits: vscode.NotebookEdit[], simulationWorkspace?: ISimulationWorkspace) {518for (const edit of edits) {519if (edit.newNotebookMetadata) {520throw new Error('Not Supported');521}522if (edit.newCellMetadata) {523throw new Error('Not Supported');524}525if (edit.newCells) {526ExtHostNotebookDocumentData.replaceCells(notebookDocument, edit.range, edit.newCells, simulationWorkspace);527} else {528notebookDocument._cells.splice(edit.range.start, edit.range.end - edit.range.start);529}530}531}532533private static replaceCells(notebookDocument: ExtHostNotebookDocumentData, range: vscode.NotebookRange, cells: vscode.NotebookCellData[], simulationWorkspace?: ISimulationWorkspace) {534const uri = notebookDocument.uri;535const docs = cells.map((cell, index) => {536const doc = createTextDocumentData(uri.with({ scheme: Schemas.vscodeNotebookCell, fragment: generateCellFragment(notebookDocument.cells.length + index + 1) }), cell.value, cell.languageId);537if (simulationWorkspace) {538simulationWorkspace.addDocument(doc);539}540if (cell.outputs?.length) {541// throw new Error('Not implemented');542}543return doc;544});545const extCells = docs.map((doc, index) => new ExtHostCell(index, cells[index].kind, notebookDocument, doc, cells[index].metadata || {}, [], undefined));546if (notebookDocument.cells.length) {547notebookDocument.cells.splice(range.start, range.end > range.start ? range.end - range.start : 0, ...extCells);548} else {549notebookDocument.cells.push(...extCells);550}551}552553private _cells: ExtHostCell[] = [];554set cells(cells: ExtHostCell[]) {555this._cells = cells;556}557get cells() {558return this._cells;559}560uri: Uri;561562private readonly _notebookType: string;563564private _notebook: vscode.NotebookDocument | undefined;565private _metadata: Record<string, any>;566private _versionId: number = 0;567private _isDirty: boolean = false;568private _disposed: boolean = false;569570constructor(571uri: Uri,572notebookType: string,573metadata: { [key: string]: any },574cells: ExtHostCell[],575) {576this.uri = uri;577this._notebookType = notebookType;578this._metadata = metadata;579this._cells = cells;580}581582get document(): vscode.NotebookDocument {583if (!this._notebook) {584const that = this;585const apiObject: vscode.NotebookDocument = {586get uri() { return that.uri; },587get version() { return that._versionId; },588get notebookType() { return that._notebookType; },589get isDirty() { return that._isDirty; },590get isUntitled() { return that.uri.scheme === 'untitled'; },591get isClosed() { return that._disposed; },592get metadata() { return that._metadata; },593get cellCount() { return that._cells.length; },594cellAt(index) {595return that._cells[index].apiCell;596},597getCells(range) {598const cells = range ? that._getCells(range) : that._cells;599return cells.map(cell => cell.apiCell);600},601save() {602return Promise.resolve(true);603}604};605this._notebook = Object.freeze(apiObject);606}607return this._notebook;608}609610get cellCount(): number {611return this._cells.length;612}613cellAt(index: number): ExtHostCell {614return this._cells[index];615}616617private _getCells(range: vscode.NotebookRange): ExtHostCell[] {618const result: ExtHostCell[] = [];619for (let i = range.start; i < range.end; i++) {620result.push(this._cells[i]);621}622return result;623}624625getCellIndex(cell: ExtHostCell): number {626return this._cells.indexOf(cell);627}628629getText(): string {630return JSON.stringify({631cells: this._cells.map(cell => ({632cell_type: cell.kind === 2 ? 'code' : 'markdown',633source: [cell.document.getText()],634metadata: cell.metadata,635outputs: (cell.apiCell.outputs || []).map(translateCellDisplayOutput),636})),637metadata: this._metadata,638}, undefined, 4);639}640641appendCellOutput(cellIndex: number, outputs: vscode.NotebookCellOutput[]): void {642this._cells[cellIndex].appendOutput(outputs);643}644}645// export const _documents = new ResourceMap<ExtHostNotebookDocumentData>();646647// export function addNotebookDocument(notebook: ExtHostNotebookDocumentData) {648// _documents.set(notebook.uri, notebook);649// }650651// export function getNotebookDocuments(): vscode.NotebookDocument[] {652// return Array.from(_documents.values()).map(data => data.document);653// }654655656