Skip to content

Commit

Permalink
De-/Serialization to allow full bidirectional notebook editing (#73)
Browse files Browse the repository at this point in the history
First revision of WASM serializer interface
  • Loading branch information
sourishkrout committed Dec 13, 2022
1 parent f0ffa7e commit 56e86bb
Show file tree
Hide file tree
Showing 6 changed files with 198 additions and 14 deletions.
14 changes: 7 additions & 7 deletions src/extension/extension.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@

import { workspace, notebooks, commands, ExtensionContext } from 'vscode'

import { Serializer } from './notebook'
import { Kernel } from './kernel'
import { ShowTerminalProvider, BackgroundTaskProvider, StopBackgroundTaskProvider} from './provider/background'
import { CopyProvider } from './provider/copy'
import { resetEnv } from './utils'
import { CliProvider } from './provider/cli'
import {
openTerminal,
runCLICommand,
copyCellToClipboard,
openAsRunmeNotebook,
import {
openTerminal,
runCLICommand,
copyCellToClipboard,
openAsRunmeNotebook,
openSplitViewAsMarkdownText ,
stopBackgroundTask
} from './commands'
import { Serializer } from './serializer'


export class RunmeExtension {
async initialise (context: ExtensionContext) {
async initialize(context: ExtensionContext) {
const kernel = new Kernel(context)
context.subscriptions.push(
kernel,
Expand Down
4 changes: 2 additions & 2 deletions src/extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ const ext = new RunmeExtension()
export async function activate (context: ExtensionContext) {
console.log('[Runme] Activating Extension')
try {
await ext.initialise(context)
await ext.initialize(context)
console.log('[Runme] Extension successfully activated')
} catch (err: any) {
console.log(`[Runme] Failed to initialise the extension ${err.message}`)
console.log(`[Runme] Failed to initialize the extension ${err.message}`)
}
}

Expand Down
7 changes: 4 additions & 3 deletions src/extension/notebook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
NotebookSerializer, ExtensionContext, Uri, workspace, NotebookData, NotebookCellData, NotebookCellKind,
window
} from 'vscode'
window} from 'vscode'

import type { WasmLib } from '../types'

Expand Down Expand Up @@ -32,7 +31,7 @@ export class Serializer implements NotebookSerializer {
return WebAssembly.instantiate(wasmFile, go.importObject).then(
(result) => { go.run(result.instance) },
(err: Error) => {
console.error(`[Runme] failed initialising WASM file: ${err.message}`)
console.error(`[Runme] failed initializing WASM file: ${err.message}`)
return err
}
)
Expand Down Expand Up @@ -197,3 +196,5 @@ export class Serializer implements NotebookSerializer {
])
}
}


156 changes: 156 additions & 0 deletions src/extension/serializer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
NotebookSerializer,
ExtensionContext,
Uri,
workspace,
NotebookData,
NotebookCellData,
NotebookCellKind,
CancellationToken,
} from 'vscode'

import { WasmLib } from '../types'

import executor from './executors'
import Languages from './languages'
import { PLATFORM_OS } from './constants'
import { Serializer as OldSerializer, DEFAULT_LANG_ID } from './notebook'

declare var globalThis: any

export class Serializer implements NotebookSerializer {
private readonly wasmReady: Promise<void>
private readonly languages: Languages

constructor(private context: ExtensionContext) {
this.languages = Languages.fromContext(this.context)
this.wasmReady = this.#initWasm()
}

async #initWasm() {
const go = new globalThis.Go()
const wasmUri = Uri.joinPath(
this.context.extensionUri,
'wasm',
'runme.wasm'
)
const wasmFile = await workspace.fs.readFile(wasmUri)
return WebAssembly.instantiate(wasmFile, go.importObject).then(
(result) => {
go.run(result.instance)
},
(err: Error) => {
console.error(`[Runme] failed initializing WASM file: ${err.message}`)
throw err
}
)
}

public async serializeNotebook(
data: NotebookData,
token: CancellationToken
): Promise<Uint8Array> {
console.log(new Date(), 'serializeNotebook')
try {
await this.wasmReady
const { Runme } = globalThis as WasmLib.New.Serializer

const notebook = JSON.stringify(data)
const markdown = await Runme.serialize(notebook)

const encoder = new TextEncoder()
const encoded = encoder.encode(markdown)

return encoded
} catch (err: any) {
console.error(err)
throw err
}
}

public async deserializeNotebook(
content: Uint8Array,
token: CancellationToken
): Promise<NotebookData> {
console.log(new Date(), 'deserializeNotebook')
let notebook: WasmLib.New.Notebook
try {
await this.wasmReady
const { Runme } = globalThis as WasmLib.New.Serializer

const markdown = content.toString()

notebook = await Runme.deserialize(markdown)

if (!notebook || (notebook.cells ?? []).length === 0) {
return this.#printCell('⚠️ __Error__: no cells found!')
}
} catch (err: any) {
return this.#printCell(
'⚠️ __Error__: document could not be loaded' +
(err ? `\n<small>${err.message}</small>` : '') +
'.<p>Please report bug at https://github.com/stateful/vscode-runme/issues' +
' or let us know on Discord (https://discord.gg/BQm8zRCBUY)</p>'
)
}

try {
const cells = notebook.cells ?? []
notebook.cells = await Promise.all(
cells.map((elem) => {
if (
elem.kind === NotebookCellKind.Code &&
elem.value &&
!elem.languageId
) {
const norm = OldSerializer.normalize(elem.value)
return this.languages.guess(norm, PLATFORM_OS).then((guessed) => {
elem.languageId = guessed
return elem
})
}
return Promise.resolve(elem)
})
)
} catch (err: any) {
console.error(`Error guessing snippet languages: ${err}`)
}

const cells = Serializer.revive(notebook)
return new NotebookData(cells)
}

protected static revive(notebook: WasmLib.New.Notebook) {
return notebook.cells.reduce((accu, elem, index) => {
let cell: NotebookCellData
const isSupported = Object.keys(executor).includes(elem.languageId ?? '')

if (elem.kind === NotebookCellKind.Code && isSupported) {
cell = new NotebookCellData(
NotebookCellKind.Code,
elem.value,
elem.languageId || DEFAULT_LANG_ID
)
} else {
cell = new NotebookCellData(
NotebookCellKind.Markup,
elem.value,
'markdown'
)
}

// todo(sebastian): incomplete
cell.metadata = { ...elem.metadata, index }
accu.push(cell)

return accu
}, <NotebookCellData[]>[])
}

#printCell(content: string, languageId = 'markdown') {
return new NotebookData([
new NotebookCellData(NotebookCellKind.Markup, content, languageId),
])
}
}
27 changes: 27 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
import { NotebookCellKind } from 'vscode'

import { OutputType, ClientMessages } from './constants'

export namespace WasmLib {
export namespace New {
export type Notebook = {
cells: Cell[]
}

export type Cell = {
metadata?: Attribute
languageId?: string
value: string
kind: NotebookCellKind.Markup
} | {
metadata?: Attribute
languageId?: string
value: string
kind: NotebookCellKind.Code
}

export interface Serializer {
Runme: {
deserialize: (content: string) => Promise<Notebook>
serialize: (content: string) => Promise<string>
}
}
}

export interface Runme {
Runme: {
initialize: (source: string) => void
Expand Down
4 changes: 2 additions & 2 deletions tests/extension/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { RunmeExtension } from '../../src/extension/extension'
vi.mock('vscode')


test('initialises all providers', async () => {
test('initializes all providers', async () => {
const context: any = { subscriptions: [], extensionUri: { fsPath: '/foo/bar' } }
const ext = new RunmeExtension()
await ext.initialise(context)
await ext.initialize(context)
expect(notebooks.registerNotebookCellStatusBarItemProvider).toBeCalledTimes(5)
expect(workspace.registerNotebookSerializer).toBeCalledTimes(1)
expect(commands.registerCommand).toBeCalledTimes(7)
Expand Down

0 comments on commit 56e86bb

Please sign in to comment.