Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

De-/Serialization to allow full bidirectional notebook editing #73

Merged
merged 4 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
9 changes: 5 additions & 4 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 All @@ -13,7 +12,7 @@ import { normalizeLanguage, verifyCheckedInFile } from './utils'
declare var globalThis: any

// const CODE_REGEX = /```(\w+)?\n[^`]*```/g
const DEFAULT_LANG_ID = 'text'
export const DEFAULT_LANG_ID = 'text'

export class Serializer implements NotebookSerializer {
private readonly ready: Promise<Error | void>
Expand All @@ -31,7 +30,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 @@ -187,3 +186,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