From 22a56839891dd1242258b464ea2fbc0772473661 Mon Sep 17 00:00:00 2001 From: Gordon Smith Date: Thu, 1 Sep 2022 09:56:24 +0100 Subject: [PATCH] fix(renderer): Workaround delay loading of cells Signed-off-by: Gordon Smith --- package.json | 4 +- rollup.config.js | 2 +- samples/tmp.ojs | 219 +++++++++++++----- src/compiler/cell.ts | 152 ++++++++----- src/compiler/notebook.ts | 43 +++- src/compiler/parser.ts | 260 +++++++++++----------- src/compiler/types.ts | 4 +- src/compiler/util.ts | 112 ++-------- src/compiler/writer.ts | 75 +++++++ src/notebook-renderers/observableTypes.ts | 24 -- src/notebook-renderers/ojsModule.ts | 18 -- src/notebook-renderers/ojsNotebook.ts | 46 ---- src/notebook-renderers/ojsRenderer.ts | 35 --- src/notebook-renderers/ojsVariable.ts | 103 --------- src/notebook-renderers/parser.ts | 143 ------------ src/notebook-renderers/util.ts | 94 -------- src/notebook/command.ts | 48 ---- src/notebook/controller.ts | 91 -------- src/notebook/controller/controller.ts | 77 ++++--- src/notebook/controller/serializer.ts | 22 +- src/notebook/renderers/index.ts | 39 ---- src/notebook/renderers/renderer.ts | 81 +++++++ src/notebook/serializer.ts | 60 ----- src/notebook/types.ts | 141 ------------ src/ojs/command.ts | 32 +-- src/ojs/meta.ts | 8 +- src/ojs/preview.ts | 6 +- src/test/index.ts | 6 +- src/util/fs.ts | 2 +- src/webview.ts | 4 +- tsconfig.json | 25 ++- 31 files changed, 715 insertions(+), 1261 deletions(-) create mode 100644 src/compiler/writer.ts delete mode 100644 src/notebook-renderers/observableTypes.ts delete mode 100644 src/notebook-renderers/ojsModule.ts delete mode 100644 src/notebook-renderers/ojsNotebook.ts delete mode 100644 src/notebook-renderers/ojsRenderer.ts delete mode 100644 src/notebook-renderers/ojsVariable.ts delete mode 100644 src/notebook-renderers/parser.ts delete mode 100644 src/notebook-renderers/util.ts delete mode 100644 src/notebook/command.ts delete mode 100644 src/notebook/controller.ts delete mode 100644 src/notebook/renderers/index.ts create mode 100644 src/notebook/renderers/renderer.ts delete mode 100644 src/notebook/serializer.ts delete mode 100644 src/notebook/types.ts diff --git a/package.json b/package.json index 0bd67f3..556449a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ], "main": "./dist/extension.js", "scripts": { - "clean": "rimraf out lib* dist types *.vsix", + "clean": "rimraf out lib* dist types *.vsix *.tsbuildinfo", "compile": "tsc", "compile-watch": "npm run compile -- -watch", "compile-es6": "tsc --module es6 --outDir ./lib-es6", @@ -364,4 +364,4 @@ } ] } -} +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 213981d..aa9f466 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -40,7 +40,7 @@ export default [{ }, plugins: plugins }, { - input: "./lib-es6/notebook/renderers/index", + input: "./lib-es6/notebook/renderers/renderer", output: [{ file: "dist/ojsRenderer.js", format: "es", diff --git a/samples/tmp.ojs b/samples/tmp.ojs index 74311e7..c66b3a7 100644 --- a/samples/tmp.ojs +++ b/samples/tmp.ojs @@ -1,60 +1,175 @@ -md` -# 3D Sample -`; +md`# Projection Transitions -{ - const div = DOM.element('div'); - - Plotly.newPlot(div, data, layout); - - return div; -} +This notebook interpolates smoothly between projections; this is easiest when both projections are well-defined over the given viewport (here, the world).` -md` ---- -`; +viewof projection = { + const input = projectionInput({ + value: new URLSearchParams(location.search).get("projection"), + name: "projection" + }); + const interval = setInterval(() => { + input.i.selectedIndex = (input.i.selectedIndex + 1) % projections.length; + input.dispatchEvent(new CustomEvent("input")); + }, 1500); + input.addEventListener("change", () => clearInterval(interval)); + invalidation.then(() => clearInterval(interval)); + return input; +} -rawData = await d3.csv('https://raw.githubusercontent.com/plotly/datasets/master/api_docs/mt_bruno_elevation.csv'); +viewof context = { + const context = DOM.context2d(width, height); + context.canvas.style.display = "block"; + context.canvas.style.maxWidth = "100%"; + context.canvas.value = context; + return context.canvas; +} -function unpack(rows, key) { - return rows.map(row => row[key]); +function render(projection) { + const path = d3.geoPath(projection, context); + context.clearRect(0, 0, width, height); + context.save(); + context.beginPath(), path(outline), context.clip(), context.fillStyle = "#fff", context.fillRect(0, 0, width, height); + context.beginPath(), path(graticule), context.strokeStyle = "#ccc", context.stroke(); + context.beginPath(), path(land), context.fillStyle = "#000", context.fill(); + context.restore(); + context.beginPath(), path(outline), context.strokeStyle = "#000", context.stroke(); } -function zData(rows) { - let z_data=[ ] - for(let i=0;i<24;i++) - { - z_data.push(unpack(rows,i)); - } - return z_data; -} - -data = [{ - z: zData(rawData), - type: 'surface', - contours: { - z: { - show:true, - usecolormap: true, - highlightcolor:"#42f462", - project:{z: true} - } +update = { + const r0 = mutable previousProjection; + const r1 = projection; + if (r0 === r1) return; + mutable previousProjection = r1; + const interpolate = interpolateProjection(r0, r1); + for (let j = 1, m = 45; true; ++j) { + const t = Math.min(1, ease(j / m)); + render(interpolate(t).rotate([performance.now() / 100, 0])); + yield; } -}]; - - -layout = ({ - title: 'Mt Bruno Elevation With Projected Contours', - scene: {camera: {eye: {x: 1.87, y: 0.88, z: -0.64}}}, - autosize: false, - width, - height: 800, - margin: { - l: 65, - r: 50, - b: 65, - t: 90, - } -}) +} + +mutable previousProjection = d3.geoEquirectangularRaw + +function interpolateProjection(raw0, raw1) { + const {scale: scale0, translate: translate0} = fit(raw0); + const {scale: scale1, translate: translate1} = fit(raw1); + return t => d3.geoProjection((x, y) => lerp2(raw0(x, y), raw1(x, y), t)) + .scale(lerp1(scale0, scale1, t)) + .translate(lerp2(translate0, translate1, t)) + .precision(0.1); +} + +function lerp1(x0, x1, t) { + return (1 - t) * x0 + t * x1; +} + +function lerp2([x0, y0], [x1, y1], t) { + return [(1 - t) * x0 + t * x1, (1 - t) * y0 + t * y1]; +} + +function fit(raw) { + const p = d3.geoProjection(raw).fitExtent([[0.5, 0.5], [width - 0.5, height - 0.5]], outline); + return {scale: p.scale(), translate: p.translate()}; +} + +ease = d3.easeCubicInOut + +width = 954 + +height = 600 + +outline = ({type: "Sphere"}) + +graticule = d3.geoGraticule10() + +land = topojson.feature(world, world.objects.land) + +world = FileAttachment(/* "land-110m.json" */"https://static.observableusercontent.com/files/f75ca3dc7c0b65cf225cea300e01e5e3cb5abf4ad75592936a2b6c79b797e933a208355d31d5b160f5b1db2a7de61fa402fe279d036a052211cd09462f524cad").json() + +topojson = require("topojson-client@3") + +d3 = require("d3-geo@2", "d3-geo-projection@3", "d3-ease@2") + +projections = [ + {name: "Aitoff", value: d3.geoAitoffRaw}, + {name: "American polyconic", value: d3.geoPolyconicRaw}, + {name: "August", value: d3.geoAugustRaw}, + {name: "Baker dinomic", value: d3.geoBakerRaw}, + {name: "Boggs’ eumorphic", value: d3.geoBoggsRaw}, + {name: "Bonne", value: d3.geoBonneRaw(Math.PI / 4)}, + {name: "Bottomley", value: d3.geoBottomleyRaw(0.5)}, + {name: "Bromley", value: d3.geoBromleyRaw}, + {name: "Collignon", value: d3.geoCollignonRaw}, + {name: "conic equal-area", value: d3.geoConicEqualAreaRaw(0, Math.PI / 3)}, + {name: "conic equidistant", value: d3.geoConicEquidistantRaw(0, Math.PI / 3)}, + {name: "Craster parabolic", value: d3.geoCrasterRaw}, + {name: "cylindrical equal-area", value: d3.geoCylindricalEqualAreaRaw(38.58 / 180 * Math.PI)}, + {name: "cylindrical stereographic", value: d3.geoCylindricalStereographicRaw(0)}, + {name: "Eckert I", value: d3.geoEckert1Raw}, + {name: "Eckert II", value: d3.geoEckert2Raw}, + {name: "Eckert III", value: d3.geoEckert3Raw}, + {name: "Eckert IV", value: d3.geoEckert4Raw}, + {name: "Eckert V", value: d3.geoEckert5Raw}, + {name: "Eckert VI", value: d3.geoEckert6Raw}, + {name: "Eisenlohr conformal", value: d3.geoEisenlohrRaw}, + {name: "Equal Earth", value: d3.geoEqualEarthRaw}, + {name: "Equirectangular (plate carrée)", value: d3.geoEquirectangularRaw}, + {name: "Fahey pseudocylindrical", value: d3.geoFaheyRaw}, + {name: "flat-polar parabolic", value: d3.geoMtFlatPolarParabolicRaw}, + {name: "flat-polar quartic", value: d3.geoMtFlatPolarQuarticRaw}, + {name: "flat-polar sinusoidal", value: d3.geoMtFlatPolarSinusoidalRaw}, + {name: "Foucaut’s stereographic equivalent", value: d3.geoFoucautRaw}, + {name: "Foucaut’s sinusoidal", value: d3.geoFoucautSinusoidalRaw(0.5)}, + {name: "Ginzburg V", value: d3.geoGinzburg5Raw}, + {name: "Ginzburg VI", value: d3.geoGinzburg6Raw}, + {name: "Ginzburg VIII", value: d3.geoGinzburg8Raw}, + {name: "Ginzburg IX", value: d3.geoGinzburg9Raw}, + {name: "Goode’s homolosine", value: d3.geoHomolosineRaw}, + {name: "Hammer", value: d3.geoHammerRaw(2)}, + {name: "Hill eucyclic", value: d3.geoHillRaw(1)}, + {name: "Hufnagel pseudocylindrical", value: d3.geoHufnagelRaw(1, 0, Math.PI / 4, 2)}, + {name: "Kavrayskiy VII", value: d3.geoKavrayskiy7Raw}, + {name: "Lagrange conformal", value: d3.geoLagrangeRaw(0.5)}, + {name: "Larrivée", value: d3.geoLarriveeRaw}, + {name: "Laskowski tri-optimal", value: d3.geoLaskowskiRaw}, + {name: "Loximuthal", value: d3.geoLoximuthalRaw(40 / 180 * Math.PI)}, + {name: "Miller cylindrical", value: d3.geoMillerRaw}, + {name: "Mollweide", value: d3.geoMollweideRaw}, + {name: "Natural Earth", value: d3.geoNaturalEarth1Raw}, + {name: "Natural Earth II", value: d3.geoNaturalEarth2Raw}, + {name: "Nell–Hammer", value: d3.geoNellHammerRaw}, + {name: "Nicolosi globular", value: d3.geoNicolosiRaw}, + {name: "Patterson cylindrical", value: d3.geoPattersonRaw}, + {name: "rectangular polyconic", value: d3.geoRectangularPolyconicRaw(0)}, + {name: "Robinson", value: d3.geoRobinsonRaw}, + {name: "sinusoidal", value: d3.geoSinusoidalRaw}, + {name: "sinu-Mollweide", value: d3.geoSinuMollweideRaw}, + {name: "Times", value: d3.geoTimesRaw}, + {name: "Tobler hyperelliptical", value: d3.geoHyperellipticalRaw(0, 2.5, 1.183136)}, + {name: "Van der Grinten", value: d3.geoVanDerGrintenRaw}, + {name: "Van der Grinten II", value: d3.geoVanDerGrinten2Raw}, + {name: "Van der Grinten III", value: d3.geoVanDerGrinten3Raw}, + {name: "Van der Grinten IV", value: d3.geoVanDerGrinten4Raw}, + {name: "Wagner IV", value: d3.geoWagner4Raw}, + {name: "Wagner VI", value: d3.geoWagner6Raw}, + {name: "Wagner VII", value: d3.geoWagnerRaw(65 / 180 * Math.PI, 60 / 180 * Math.PI, 0, 200)}, + {name: "Wagner VIII", value: d3.geoWagnerRaw(65 / 180 * Math.PI, 60 / 180 * Math.PI, 20, 200)}, + {name: "Werner", value: d3.geoBonneRaw(Math.PI / 2)}, + {name: "Winkel tripel", value: d3.geoWinkel3Raw} +] -Plotly = require("https://cdn.plot.ly/plotly-latest.min.js") \ No newline at end of file +function projectionInput({name = "", value} = {}) { + const form = html`
${name}`; + form.onchange = () => form.dispatchEvent(new CustomEvent("input")); // Safari + form.oninput = (event) => { + if (event && event.isTrusted) form.onchange = null; + form.value = projections[form.i.selectedIndex].value; + }; + form.oninput(); + return form; +} \ No newline at end of file diff --git a/src/compiler/cell.ts b/src/compiler/cell.ts index 3f083f4..be3c776 100644 --- a/src/compiler/cell.ts +++ b/src/compiler/cell.ts @@ -1,35 +1,48 @@ -import { Inspector } from "@observablehq/inspector"; import { observablehq as ohq } from "./types"; import { Notebook } from "./notebook"; import { parseCell } from "./parser"; import { obfuscatedImport } from "./util"; +import { Writer } from "./writer"; + +function encode(str: string) { + return str + // .split("\\").join("\\\\") + .split("`").join("\\`") + // .split("$").join("\\$") + ; +} + +class NullObserver implements ohq.Inspector { + pending() { + } + fulfilled(value: any) { + } + rejected(error: any) { + } +} +export const nullObserver = new NullObserver(); + +const nullObserverFactory: ohq.InspectorFactory = (name?: string) => nullObserver; export class Cell { protected _notebook: Notebook; - protected _variable: ohq.Variable; // Regular variable - protected _initialValue: ohq.Variable; // Cell is "mutable" - protected _variableValue: ohq.Variable; // Cell is a "viewof" or "mutable" - protected _imported: { variable: ohq.Variable, variableValue?: ohq.Variable }[]; // Cell is an import + protected _id: string | number; protected _observer: ohq.InspectorFactory; + protected _variables = new Set(); - constructor(notebook: Notebook, observer: ohq.InspectorFactory) { + constructor(notebook: Notebook, observer: ohq.InspectorFactory = nullObserverFactory) { this._notebook = notebook; this._observer = observer; } reset() { - this._imported?.forEach(v => { - v.variable.delete(); - v.variableValue?.delete(); - }); - this._initialValue?.delete(); - this._variable?.delete(); - this._variableValue?.delete(); + this._variables?.forEach(v => v.delete()); + this._variables.clear(); } dispose() { - this.reset(); + this._notebook.disposeCell(this); } async importFile(partial) { @@ -54,48 +67,83 @@ export class Cell { return obfuscatedImport(`https://api.observablehq.com/${partial[0] === "@" ? partial : `d/${partial}`}.js?v=3`); } - async interpret(cellSource: string) { + protected _cellSource: string = ""; + text(): string; + text(cellSource: string, languageId?: string): this; + text(cellSource?: string, languageId: string = "ojs"): string | this { + if (arguments.length === 0) return this._cellSource; + if (languageId === "markdown") { + languageId = "md"; + } + this._cellSource = languageId === "ojs" ? cellSource! : `${languageId}\`${encode(cellSource!)}\``; + return this; + } + + async evaluate() { this.reset(); - const parsed = parseCell(cellSource); - if (parsed.import) { - const impMod: any = [".", "/"].indexOf(parsed.import.src[0]) === 0 ? - await this.importFile(parsed.import.src) : - await this.importNotebook(parsed.import.src); - - let mod = this._notebook.createModule(impMod.default); - if (parsed.import.injections.length) { - mod = mod.derive(parsed.import.injections, this._notebook.main()); - } - - this._imported = parsed.import.specifiers.map(spec => { - const viewof = spec.view ? "viewof " : ""; - const retVal = { - variable: this._notebook.importVariable(viewof + spec.name, viewof + spec.alias, mod), - variableValue: undefined - }; - if (spec.view) { - retVal.variableValue = this._notebook.importVariable(spec.name, spec.alias, mod); + const parsed = parseCell(this._cellSource); + switch (parsed.type) { + case "import": + const impMod: any = [".", "/"].indexOf(parsed.src[0]) === 0 ? + await this.importFile(parsed.src) : + await this.importNotebook(parsed.src); + + let mod = this._notebook.createModule(impMod.default); + if (parsed.injections.length) { + mod = mod.derive(parsed.injections, this._notebook.main()); } - return retVal; - }); - this._variable = this._notebook.createVariable(this._observer()); - this._variable.define(undefined, ["md"], md => { - return md`\`\`\`JavaScript -${cellSource} + + parsed.specifiers.forEach(spec => { + const viewof = spec.view ? "viewof " : ""; + this._variables.add(this._notebook.importVariable(viewof + spec.name, viewof + spec.alias, mod)); + if (spec.view) { + this._variables.add(this._notebook.importVariable(spec.name, spec.alias, mod)); + } + }); + this._variables.add(this._notebook.createVariable(this._observer(), undefined, ["md"], md => { + return md`\`\`\`JavaScript +${this._cellSource} \`\`\``; - }); - } else { - if (parsed.initialValue) { - this._initialValue = this._notebook.createVariable(); - this._initialValue.define(parsed.initialValue.id, parsed.initialValue.inputs, parsed.initialValue.func); - } - this._variable = this._notebook.createVariable(this._observer(parsed.id)); - this._variable.define(parsed.id, parsed.inputs, parsed.func); - if (parsed.viewofValue) { - this._variableValue = this._notebook.createVariable(parsed.initialValue && this._observer(parsed.viewofValue.id)); - this._variableValue.define(parsed.viewofValue.id, parsed.viewofValue.inputs, parsed.viewofValue.func); - } + })); + break; + case "viewof": + this._variables.add(this._notebook.createVariable(this._observer(parsed.variable.id), parsed.variable.id, parsed.variable.inputs, parsed.variable.func)); + this._variables.add(this._notebook.createVariable(this._observer(parsed.variableValue.id), parsed.variableValue.id, parsed.variableValue.inputs, parsed.variableValue.func)); + break; + case "mutable": + this._variables.add(this._notebook.createVariable(undefined, parsed.initial.id, parsed.initial.inputs, parsed.initial.func)); + this._variables.add(this._notebook.createVariable(this._observer(parsed.variable.id), parsed.variable.id, parsed.variable.inputs, parsed.variable.func)); + this._variables.add(this._notebook.createVariable(this._observer(parsed.variableValue.id), parsed.variableValue.id, parsed.variableValue.inputs, parsed.variableValue.func)); + break; + case "variable": + this._variables.add(this._notebook.createVariable(this._observer(parsed.id), parsed.id, parsed.inputs, parsed.func)); + break; + } + } + + compile(writer: Writer) { + const parsed = parseCell(this._cellSource); + let id; + switch (parsed.type) { + case "import": + writer.import(parsed); + break; + case "viewof": + id = writer.function(parsed.variable); + writer.define(parsed.variable, true, false, id); + writer.define(parsed.variableValue, true, true); + break; + case "mutable": + id = writer.function(parsed.initial); + writer.define(parsed.initial, false, false, id); + writer.define(parsed.variable, true, true); + writer.define(parsed.variableValue, true, true); + break; + case "variable": + id = writer.function(parsed); + writer.define(parsed, true, false, id); + break; } } } diff --git a/src/compiler/notebook.ts b/src/compiler/notebook.ts index 95563fe..220b76c 100644 --- a/src/compiler/notebook.ts +++ b/src/compiler/notebook.ts @@ -2,11 +2,13 @@ import { Runtime, Library } from "@observablehq/runtime"; import { FileAttachments } from "@observablehq/stdlib"; import { Cell } from "./cell"; import { observablehq as ohq } from "./types"; +import { Writer } from "./writer"; export class Notebook { protected _runtime: ohq.Runtime; protected _main: ohq.Module; + protected _cells: Set = new Set(); constructor(notebook?: ohq.Notebook, plugins: object = {}) { const files = {}; @@ -30,12 +32,28 @@ export class Notebook { dispose() { this._runtime.dispose(); - delete this._runtime; - delete this._main; + this._cells.clear(); } - createCell(inspector: ohq.InspectorFactory): Cell { - return new Cell(this, inspector); + createCell(observer?: ohq.InspectorFactory): Cell { + const newCell = new Cell(this, observer); + this._cells.add(newCell); + return newCell; + } + + disposeCell(cell: Cell) { + cell.reset(); + this._cells.delete(cell); + } + + compile(writer: Writer) { + this._cells.forEach(cell => { + try { + cell.compile(writer); + } catch (e: any) { + writer.error(e?.message); + } + }); } // ObservableHQ --- @@ -47,14 +65,19 @@ export class Notebook { return this._runtime.module(define); } - createVariable(inspector?: ohq.Inspector): ohq.Variable { - return this._main.variable(inspector); + createVariable(inspector?: ohq.Inspector, name?: string, inputs?: string[], definition?: any): ohq.Variable { + const retVal = this._main.variable(inspector); + if (arguments.length > 1) { + try { + retVal.define(name, inputs, definition); + } catch (e: any) { + console.error(e?.message); + } + } + return retVal; } - importVariable(name: string, alias: string, otherModule: ohq.Module): ohq.Variable { + importVariable(name: string, alias: string | undefined, otherModule: ohq.Module): ohq.Variable { return this._main.import(name, alias, otherModule); } - - removeCell(id: string) { - } } \ No newline at end of file diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index df248b2..fb6382c 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -1,46 +1,58 @@ -import { parseCell as ohqParseCell } from "@observablehq/parser"; -import { calcRefs, createFunction } from "./util"; +import { walk, parseCell as ohqParseCell } from "@observablehq/parser"; +import { ancestor } from "acorn-walk"; +import { createFunction, Refs } from "./util"; -type IdIdentifier = { type: "Identifier", name: string }; -type IdViewExpression = { type: "ViewExpression", id: IdIdentifier }; -type IdMutableExpression = { type: "MutableExpression", id: IdIdentifier }; -type Id = null | IdIdentifier | IdViewExpression | IdMutableExpression; +function calcRefs(cellAst, cellStr): Refs { + if (cellAst.references === undefined) return { inputs: [], args: [], patches: [] }; -type BodyType = "BinaryExpression" | "BlockStatement" | "Literal" | "YieldExpression" | "CallExpression" | "MemberExpression" | "Identifier" | "ViewExpression" | "MutableExpression" | "ImportDeclaration"; -interface BodyAny { - type: BodyType; - [key: string]: any; + const dedup = {}; + cellAst.references.forEach(r => dedup[cellStr.substring(r.start, r.end)] = true); + const retVal: Refs = { + inputs: Object.keys(dedup), + args: Object.keys(dedup).map(r => r.split(" ").join("_")), + patches: [] + }; + const pushPatch = (node, newText) => retVal.patches.push({ start: node.start - cellAst.body.start, end: node.end - cellAst.body.start, newText }); + ancestor(cellAst.body, { + Identifier(node) { + const value = cellStr.substring(node.start, node.end); + if (dedup[value]) { + } + }, + MutableExpression(node) { + const value = cellStr.substring(node.start, node.end); + const newText = value.split(" ").join("_") + ".value"; + pushPatch(node, newText); + }, + ViewExpression(node) { + const value = cellStr.substring(node.start, node.end); + const newText = value.split(" ").join("_"); + pushPatch(node, newText); + }, + ThisExpression(node, ancestors: acorn.Node[]) { + const value = cellStr.substring(node.start, node.end); + if (value === "this" && !ancestors.find(n => n.type === "FunctionExpression")) { + pushPatch(node, "(this === window ? undefined : this.valueOf())"); + } + } + }, walk); + return retVal; } -type Body = null | BodyAny; - -interface ParsedCell { - type: "ParsedImport" +export interface ParsedCell { + type: "import" | "viewof" | "mutable" | "variable" | "identifier" } -interface ParsedImportCell extends ParsedCell { - type: "ParsedImport" +export interface ParsedImportCell extends ParsedCell { + type: "import" src: string; - injections: { name: string, alias: string }[]; specifiers: { view: boolean, name: string, alias?: string }[]; + injections: { name: string, alias: string }[]; } -interface ParsedVariable { - id: null | string, - inputs: string[], - func: any, -} - -interface ParseResponse extends ParsedVariable { - initialValue?: ParsedVariable - viewofValue?: ParsedVariable; - import?: ParsedImportCell; - debug: any -} - -function parseImportDeclaration(cellAst): ParsedImportCell { +function parseImportExpression(cellAst): ParsedImportCell { return { - type: "ParsedImport", + type: "import", src: cellAst.body.source.value, specifiers: cellAst.body.specifiers?.map(spec => { return { @@ -58,112 +70,102 @@ function parseImportDeclaration(cellAst): ParsedImportCell { }; } -// function parseBody(cellStr: string, cellAst) { -// switch ((cellAst.body as Body)?.type) { -// case "ImportDeclaration": -// return parseImportDeclaration(cellStr); -// case undefined: -// break; -// case "ViewExpression": -// case "Identifier": -// case "BinaryExpression": -// case "BlockStatement": -// case "CallExpression": -// case "Literal": -// case "MemberExpression": -// case "YieldExpression": -// retVal.func = retVal.func ?? createFunction(refs, bodyStr, cellAst.async, cellAst.generator, ["BlockStatement"].indexOf(cellAst.body.type) >= 0); -// break; -// default: -// console.warn(`Unexpected cell.body.type: ${cellAst.body?.type}`); -// retVal.func = retVal.func ?? createFunction(refs, bodyStr, cellAst.async, cellAst.generator, ["BlockStatement"].indexOf(cellAst.body.type) >= 0); -// } +export interface ParsedVariable extends ParsedCell { + type: "variable" + id?: string, + inputs: string[], + func: any, +} -// } +export interface ParsedViewCell extends ParsedCell { + type: "viewof", + variable: ParsedVariable; + variableValue: ParsedVariable; +} -export function parseCell(cellStr: string): ParseResponse { - const cellAst = ohqParseCell(cellStr); - const refs = calcRefs(cellAst, cellStr); +function parseViewExpression(cellStr: string, cellAst, refs: Refs, bodyStr?: string): ParsedViewCell { + const id = cellAst.id && cellStr.substring(cellAst.id.start, cellAst.id.end); + return { + type: "viewof", + variable: { + type: "variable", + id, + inputs: refs.inputs, + func: createFunction(refs, cellAst.async, cellAst.generator, cellAst.body.type === "BlockStatement", bodyStr) + }, + variableValue: { + type: "variable", + id: cellAst?.id?.id?.name, + inputs: ["Generators", id], + func: (G, _) => G.input(_) + } + }; +} + +interface ParsedMutableCell extends ParsedCell { + type: "mutable", + initial: ParsedVariable; + variable: ParsedVariable; + variableValue: ParsedVariable; +} - const retVal: ParseResponse = { - id: null, +function parseMutableExpression(cellStr: string, cellAst, refs: Refs, bodyStr?: string): ParsedMutableCell { + const id = cellAst.id && cellStr.substring(cellAst.id.start, cellAst.id.end); + const initialValueId = cellAst?.id?.id?.name; + const initialId = `initial ${initialValueId}`; + return { + type: "mutable", + initial: { + type: "variable", + id: initialId, + inputs: refs.inputs, + func: createFunction(refs, cellAst.async, cellAst.generator, cellAst.body.type === "BlockStatement", bodyStr) + }, + variable: { + type: "variable", + id, + inputs: ["Mutable", initialId], + func: (M, _) => new M(_) + }, + variableValue: { + type: "variable", + id: initialValueId, + inputs: [id], + func: _ => _.generator + } + }; +} + +interface ParsedVariableCell extends ParsedCell { + type: "variable", + id: string, + inputs: string[], + func: any, +} + +function parseVariableExpression(cellStr: string, cellAst, refs: Refs, bodyStr?: string): ParsedVariableCell { + return { + type: "variable", + id: cellAst.id && cellStr.substring(cellAst.id?.start, cellAst.id?.end), inputs: refs.inputs, - func: undefined, - debug: cellAst + func: createFunction(refs, cellAst.async, cellAst.generator, cellAst.body.type === "BlockStatement", bodyStr) }; +} - const cellId = cellAst.id as Id; - const bodyStr = cellAst.body ? cellStr.substring(cellAst.body.start, cellAst.body.end) : ""; - switch (cellId?.type) { - case "ViewExpression": - retVal.id = cellStr.substring(cellAst.id.start, cellAst.id.end); - retVal.viewofValue = { - id: cellAst?.id?.id?.name, - inputs: ["Generators", retVal.id], - func: (G, _) => G.input(_) - }; - break; - case "MutableExpression": - retVal.initialValue = { - id: `initial ${cellAst?.id?.id?.name}`, - inputs: refs.inputs, - func: createFunction(refs, bodyStr, cellAst.async, cellAst.generator, ["BlockStatement"].indexOf(cellAst.body.type) >= 0) - }; - retVal.id = cellStr.substring(cellAst.id.start, cellAst.id.end); - retVal.inputs = ["Mutable", retVal.initialValue.id]; - retVal.func = (M, _) => new M(_); - retVal.viewofValue = { - id: cellAst?.id?.id?.name, - inputs: [retVal.id], - func: _ => _.generator - }; - break; - case undefined: - break; - case "Identifier": - retVal.id = cellStr.substring(cellAst.id.start, cellAst.id.end); - break; - default: - console.warn(`Unexpected cell.id.type: ${cellAst.id?.type}`); - retVal.id = cellStr.substring(cellAst.id.start, cellAst.id.end); +export function parseCell(cellStr: string): ParsedImportCell | ParsedViewCell | ParsedMutableCell | ParsedVariableCell { + const cellAst = ohqParseCell(cellStr); + if ((cellAst.body)?.type == "ImportDeclaration") { + return parseImportExpression(cellAst); } + const refs = calcRefs(cellAst, cellStr); - switch ((cellAst.body as Body)?.type) { - case "ImportDeclaration": - retVal.import = { - type: "ParsedImport", - src: cellAst.body.source.value, - specifiers: cellAst.body.specifiers?.map(spec => { - return { - view: spec.view, - name: spec.imported.name, - alias: (spec.local?.name && spec.imported.name !== spec.local.name) ? spec.local.name : spec.imported.name - }; - }) ?? [], - injections: cellAst.body.injections?.map(inj => { - return { - name: inj.imported.name, - alias: inj.local?.name ?? inj.imported.name - }; - }) ?? [], - }; - break; - case undefined: - break; + const bodyStr = cellAst.body && cellStr.substring(cellAst.body.start, cellAst.body.end); + switch (cellAst.id?.type) { case "ViewExpression": - case "Identifier": - case "BinaryExpression": - case "BlockStatement": - case "CallExpression": - case "Literal": - case "MemberExpression": - case "YieldExpression": - retVal.func = retVal.func ?? createFunction(refs, bodyStr, cellAst.async, cellAst.generator, ["BlockStatement"].indexOf(cellAst.body.type) >= 0); - break; + return parseViewExpression(cellStr, cellAst, refs, bodyStr); + case "MutableExpression": + return parseMutableExpression(cellStr, cellAst, refs, bodyStr); default: - // console.warn(`Unexpected cell.body.type: ${cellAst.body?.type}`); - retVal.func = retVal.func ?? createFunction(refs, bodyStr, cellAst.async, cellAst.generator, ["BlockStatement"].indexOf(cellAst.body.type) >= 0); + return parseVariableExpression(cellStr, cellAst, refs, bodyStr); } - - return retVal; } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4051796..367d720 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -153,13 +153,13 @@ export namespace observablehq { export interface Variable { delete(); - define(name: string | null, inputs: string[], definition: any); + define(name?: string, inputs?: string[], definition?: any); } export interface Module { variable(inspector?: Inspector): Variable; derive(specifiers: string[] | { name: string, alias: string }[], source: any); - import(name: string, alias: string, mod: Module): Variable; + import(name: string, alias: string | undefined, mod: Module): Variable; } export interface Runtime { diff --git a/src/compiler/util.ts b/src/compiler/util.ts index 4d48c61..590d9c9 100644 --- a/src/compiler/util.ts +++ b/src/compiler/util.ts @@ -1,55 +1,3 @@ -import { ancestor, simple } from "acorn-walk"; -import { walk } from "@observablehq/parser"; - -interface Ref { - start: number, - end: number, - newText: string -} - -export interface Refs { - inputs: string[]; - args: string[]; - patches: Ref[]; -} - -export function calcRefs(cellAst, cellStr): Refs { - if (cellAst.references === undefined) return { inputs: [], args: [], patches: [] }; - - const dedup = {}; - cellAst.references.forEach(r => dedup[cellStr.substring(r.start, r.end)] = true); - const retVal: Refs = { - inputs: Object.keys(dedup), - args: Object.keys(dedup).map(r => r.split(" ").join("_")), - patches: [] - }; - const pushPatch = (node, newText) => retVal.patches.push({ start: node.start - cellAst.body.start, end: node.end - cellAst.body.start, newText }); - ancestor(cellAst.body, { - Identifier(node) { - const value = cellStr.substring(node.start, node.end); - if (dedup[value]) { - } - }, - MutableExpression(node) { - const value = cellStr.substring(node.start, node.end); - const newText = value.split(" ").join("_") + ".value"; - pushPatch(node, newText); - }, - ViewExpression(node) { - const value = cellStr.substring(node.start, node.end); - const newText = value.split(" ").join("_"); - pushPatch(node, newText); - }, - ThisExpression(node, ancestors: acorn.Node[]) { - const value = cellStr.substring(node.start, node.end); - if (value === "this" && !ancestors.find(n => n.type === "FunctionExpression")) { - pushPatch(node, "(this === window ? undefined : this.valueOf())"); - } - } - }, walk); - return retVal; -} - const FuncTypes = { functionType: Object.getPrototypeOf(function () { }).constructor, asyncFunctionType: Object.getPrototypeOf(async function () { }).constructor, @@ -64,50 +12,34 @@ function funcType(async: boolean = false, generator: boolean = false) { return FuncTypes.asyncGeneratorFunctionType; } -// Hide "import" from bundlers as they have a habit of replacing "import" with "require" -export async function obfuscatedImport(url: string) { - return new FuncTypes.asyncFunctionType("url", "return import(url)")(url); -} - -export function createFunction(refs: Refs, body: string, async = false, generator = false, blockStatement = false) { - refs.patches.sort((l, r) => r.start - l.start); - refs.patches.forEach(r => { - body = body.substring(0, r.start) + r.newText + body.substring(r.end); - }); - return new (funcType(async, generator))(...refs.args, blockStatement ? body.substring(1, body.length - 1).trim() : `return (${body});`); +interface Ref { + start: number, + end: number, + newText: string } -export function encodeOMD(str: string) { - return str - .split("`").join("\`") - .split("$").join("\$") - ; +export interface Refs { + inputs: string[]; + args: string[]; + patches: Ref[]; } -export function encodeMD(str: string) { - return str - .split("`").join("\\`") - .split("$").join("\\$") - ; -} +export function createFunction(refs: Refs, async = false, generator = false, blockStatement = false, body?: string) { + if (body === undefined) { + return undefined; + } -export function encodeBacktick(str: string) { - return str - .split("`").join("\\`") - ; + refs.patches.sort((l, r) => r.start - l.start); + refs.patches.forEach(r => { + body = body!.substring(0, r.start) + r.newText + body!.substring(r.end); + }); + return new (funcType(async, generator))(...refs.args, blockStatement ? + body.substring(1, body.length - 1).trim() : + `return (\n${body}\n);`); } -export type OJSVariableMessageType = "error" | "pending" | "fulfilled" | "rejected"; -export class OJSSyntaxError { - name = "OJSSyntaxError"; - - constructor(public start: number, public end: number, public message: string) { - } +// Hide "import" from bundlers as they have a habit of replacing "import" with "require" +export async function obfuscatedImport(url: string) { + return new FuncTypes.asyncFunctionType("url", "return import(url)")(url); } -export class OJSRuntimeError { - name = "OJSRuntimeError"; - - constructor(public severity: OJSVariableMessageType, public start: number, public end: number, public message: string) { - } -} diff --git a/src/compiler/writer.ts b/src/compiler/writer.ts new file mode 100644 index 0000000..23de7bd --- /dev/null +++ b/src/compiler/writer.ts @@ -0,0 +1,75 @@ +import { ParsedImportCell, ParsedVariable } from "./parser"; + +function escape(str: string) { + return str + .split("`").join("\\`") + ; +} + +export class Writer { + + protected _imports: string[] = []; + protected _functions: string[] = []; + protected _defines: string[] = []; + protected _defineUid = 0; + protected _functionUid = 0; + + constructor() { + } + + toString() { + return `\ +${this._imports.join("\n")} + +${this._functions.join("\n").split("\n) {").join("){")} + +export default function define(runtime, observer) { + const main = runtime.module(); + + ${this._defines.join("\n ")} + + return main; +}\n`; + } + + import(imp: ParsedImportCell) { + this._imports.push(`import define${++this._defineUid} from "${imp.src}"; `); + const injections = imp.injections.map(inj => { + return inj.name === inj.alias ? + `"${inj.name}"` : + `{name: "${inj.name}", alias: "${inj.alias}"}`; + }); + const derive = imp.injections.length ? `.derive([${injections.join(", ")}], main)` : ""; + this._defines.push(`const child${this._defineUid} = runtime.module(define${this._defineUid})${derive};`); + imp.specifiers.forEach(s => { + this._defines.push(`main.import("${s.name}"${s.alias && s.alias !== s.name ? `, "${s.alias}"` : ""}, child${this._defineUid}); `); + }); + // if (imp.specifiers.filter(s => s.view).length) { + // this._defines.push(`main.import(${imp.specifiers.filter(s => s.view).map(s => s.name + (s.alias ? `as ${s.alias}` : "")).join(", ")}, child${this._defineUid}); `); + // } + // this._defines.push(`main.import("selection", "cars", child1); `); + } + + function(variable: ParsedVariable) { + let id = variable.id ?? `${++this._functionUid}`; + const idParts = id.split(" "); + id = `_${idParts[idParts.length - 1]}`; + this._functions.push(`${variable.func?.toString()?.replace("anonymous", `${id}`)}`); + return id; + } + + define(variable: ParsedVariable, observable = true, inlineFunc = false, funcId?: string) { + funcId = funcId ?? variable.id; + const observe = observable ? `.variable(observer(${variable.id ? JSON.stringify(variable.id) : ""}))` : ""; + const id = variable.id ? `${JSON.stringify(variable.id)}, ` : ""; + const inputs = variable.inputs.length ? `[${variable.inputs.map(i => JSON.stringify(i)).join(", ")}], ` : ""; + const func = inlineFunc ? + variable.func?.toString() : + funcId; + this._defines.push(`main${observe}.define(${id}${inputs}${func});`); + } + + error(msg: string) { + } +} + diff --git a/src/notebook-renderers/observableTypes.ts b/src/notebook-renderers/observableTypes.ts deleted file mode 100644 index 256cabe..0000000 --- a/src/notebook-renderers/observableTypes.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface Inspector { - pending(); - fulfilled(value); - rejected(error); -} - -export type InspectorFactory = (name: string) => Inspector; - -export interface Variable { - delete(); - define(name: string | null, inputs: string[], definition: any); -} - -export interface Module { - variable(inspector?: InspectorFactory): Variable; - derive(specifiers: string[], source: any); - import(name: string, alias: string, mod: Module): Variable; -} - -export interface Runtime { - module(define?, inspector?: InspectorFactory): Module; - dispose(): void; -} - diff --git a/src/notebook-renderers/ojsModule.ts b/src/notebook-renderers/ojsModule.ts deleted file mode 100644 index fc93b16..0000000 --- a/src/notebook-renderers/ojsModule.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { InspectorFactory, Module } from "./observableTypes"; -import { OJSNotebook } from "./ojsNotebook"; -import { OJSVariable } from "./ojsVariable"; - -export class OJSModule { - - _notebook: OJSNotebook; - _module: Module; - - constructor(notebook: OJSNotebook, define?) { - this._notebook = notebook; - this._module = notebook._runtime.module(define); - } - - createVariable(inspector: InspectorFactory) { - return new OJSVariable(this, inspector); - } -} diff --git a/src/notebook-renderers/ojsNotebook.ts b/src/notebook-renderers/ojsNotebook.ts deleted file mode 100644 index 1345486..0000000 --- a/src/notebook-renderers/ojsNotebook.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Runtime, Library } from "@observablehq/runtime"; -import { FileAttachments } from "@observablehq/stdlib"; -import { OJSModule } from "./ojsModule"; -import { InspectorFactory } from "./observableTypes"; -import { OJSVariable } from "./ojsVariable"; -import { Notebook } from "../notebook/types"; - -export class OJSNotebook { - - _runtime: Runtime; - _mainModule: OJSModule; - - constructor(notebook?: Notebook, plugins: object = {}) { - const files = {}; - notebook?.files?.forEach(f => files[f.name] = f.url); - const library = new Library(); - library.FileAttachment = function () { - return FileAttachments(name => { - return files[name]; - }); - }; - const domDownload = library.DOM.download; - library.DOM.download = function (blob, file) { - return domDownload(blob, files[file]); - }; - this._runtime = new Runtime({ ...library, ...plugins }); - this._mainModule = this.createModule(); - } - - dispose() { - this._runtime.dispose(); - delete this._runtime; - } - - createModule(define?) { - const retVal = new OJSModule(this, define); - return retVal; - } - - createVariable(inspector: InspectorFactory): OJSVariable { - return this._mainModule.createVariable(inspector); - } - - removeCell(id: string) { - } -} \ No newline at end of file diff --git a/src/notebook-renderers/ojsRenderer.ts b/src/notebook-renderers/ojsRenderer.ts deleted file mode 100644 index f01d954..0000000 --- a/src/notebook-renderers/ojsRenderer.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Inspector } from "@observablehq/inspector"; -import type { ActivationFunction } from "vscode-notebook-renderer"; -import type { OJSOutput } from "../notebook/controller"; -import { OJSNotebook } from "./ojsNotebook"; -import { OJSVariable } from "./ojsVariable"; - -export const activate: ActivationFunction = context => { - - const notebooks: { [uri: string]: OJSNotebook } = {}; - const variables: { [id: string]: OJSVariable } = {}; - - return { - renderOutputItem(outputItem, element) { - const data: OJSOutput = outputItem.json(); - if (!notebooks[data.uri]) { - notebooks[data.uri] = new OJSNotebook(data.notebook); - } - if (!variables[outputItem.id]) { - variables[outputItem.id] = notebooks[data.uri].createVariable(new Inspector(element)); - } - variables[outputItem.id] - .define(data.ojsSource) - .catch(e => { - element.innerText = e.message; - }); - }, - - disposeOutputItem(id?: string) { - if (variables[id]) { - variables[id].dispose(); - } - } - }; -}; - diff --git a/src/notebook-renderers/ojsVariable.ts b/src/notebook-renderers/ojsVariable.ts deleted file mode 100644 index 50e8184..0000000 --- a/src/notebook-renderers/ojsVariable.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { debug } from "console"; -import { InspectorFactory, Module, Variable } from "./observableTypes"; -import { OJSModule } from "./ojsModule"; -import { OJSNotebook } from "./ojsNotebook"; -import { parse, parse2 } from "./parser"; -import { obfuscatedImport } from "./util"; - -export class OJSVariable { - _notebook: OJSNotebook; - _module: OJSModule; - _initialValue: Variable; - _variable: Variable; - _variableValue: Variable; - _imported: { variable: Variable, variableValue?: Variable }[]; - - constructor(module: OJSModule, inspector: InspectorFactory) { - this._notebook = module._notebook; - this._module = module; - this._variable = module._module.variable(inspector); - } - - dispose() { - this._imported?.forEach(v => { - v.variable.delete(); - v.variableValue?.delete(); - }); - this._variableValue?.delete(); - this._variable.delete(); - this._initialValue?.delete(); - } - - async importFile(partial) { - // TODO --- - // const path = join(this._folder, partial); - // let ojs = await this.fetchUrl(path); - // if (partial.indexOf(".omd") > 1) { - // ojs = omd2ojs(ojs).ojsArr.map(row => row.ojs).join("\n"); - // } - - // const context = this; - // return { - // default: function define(runtime, observer) { - // const newModule = runtime.module(); - // const ojsModule = new OJSModule(context._ojsRuntime, partial, newModule, ojs, dirname(path)); - // ojsModule.parse(true); - // } - // }; - } - - async importNotebook(partial) { - return obfuscatedImport(`https://api.observablehq.com/${partial[0] === "@" ? partial : `d/${partial}`}.js?v=3`); - } - - async define(ojsSource: string) { - this._initialValue?.delete(); - this._variableValue?.delete(); - this._imported?.forEach(v => { - v.variable.delete(); - v.variableValue?.delete(); - }); - - const parsed = parse(ojsSource); - if (parsed.import) { - const impMod: any = [".", "/"].indexOf(parsed.import.src[0]) === 0 ? - await this.importFile(parsed.import.src) : - await this.importNotebook(parsed.import.src); - - let mod = this._notebook._runtime.module(impMod.default); - - if (parsed.import.injections.length) { - mod = mod.derive(parsed.import.injections, this._module._module); - } - - this._imported = parsed.import.specifiers.map(spec => { - const viewof = spec.view ? "viewof " : ""; - const retVal = { - variable: this._module._module.import(viewof + spec.name, viewof + spec.alias, mod), - variableValue: undefined - }; - if (spec.view) { - retVal.variableValue = this._module._module.import(spec.name, spec.alias, mod); - } - return retVal; - }); - const md = "md`" + "```javascript\n" + ojsSource + "\n```"; - this._variable.define(undefined, ["md"], md => { - return md`\`\`\`JavaScript -${ojsSource} -\`\`\``; - }); - } else { - if (parsed.initialValue) { - this._initialValue = this._module._module.variable(); - this._initialValue.define(parsed.initialValue.id, parsed.initialValue.refs, parsed.initialValue.func); - } - this._variable.define(parsed.id, parsed.refs, parsed.func); - if (parsed.viewofValue) { - this._variableValue = this._module._module.variable(); - this._variableValue.define(parsed.viewofValue.id, parsed.viewofValue.refs, parsed.viewofValue.func); - } - } - } -} diff --git a/src/notebook-renderers/parser.ts b/src/notebook-renderers/parser.ts deleted file mode 100644 index 136233e..0000000 --- a/src/notebook-renderers/parser.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { parseCell } from "@observablehq/parser"; -import { calcRefs, createFunction, obfuscatedImport } from "./util"; - -type Parse2Response = { id: string, refs: string[], func: any }; - -export function parse2(str: string): Parse2Response { - const cell = parseCell(str); - const refs = calcRefs(cell.references, str); - const retVal: Parse2Response = { - id: cell.id ? str.substring(cell.id.start, cell.id.end) : null, - refs: Object.keys(refs), - func: undefined - }; - const body = cell.body ? str.substring(cell.body.start, cell.body.end) : ""; - if (cell.id && cell.id.type === "MutableExpression") { - retVal.id = str.substring(cell.id.id.start, cell.id.id.end); - retVal.func = createFunction(refs, body, cell.async, cell.generator, cell.body && cell.body.type === "BlockStatement"); - } else { - retVal.func = createFunction(refs, body, cell.async, cell.generator, cell.body && cell.body.type === "BlockStatement"); - } - if (cell.id && cell.id.type === "ViewExpression") { - retVal.refs = ["Generators", retVal.id]; - retVal.id = str.substring(cell.id.id.start, cell.id.id.end); - retVal.func = (G, _) => G.input(_); - } - return retVal; -} - -type IdIdentifier = { type: "Identifier", name: string }; -type IdViewExpression = { type: "ViewExpression", id: IdIdentifier }; -type IdMutableExpression = { type: "MutableExpression", id: IdIdentifier }; -type Id = null | IdIdentifier | IdViewExpression | IdMutableExpression; - -type BodyType = "BinaryExpression" | "BlockStatement" | "Literal" | "YieldExpression" | "CallExpression" | "MemberExpression" | "Identifier" | "ViewExpression" | "MutableExpression" | "ImportDeclaration"; -interface BodyAny { - type: BodyType; - [key: string]: any; -} - -type Body = null | BodyAny; - -interface ParsedVariable { - id: null | string, - refs: string[], - func: any, -} - -interface ParseResponse extends ParsedVariable { - initialValue?: ParsedVariable - viewofValue?: ParsedVariable; - import?: { - src: string; - injections: { name: string, alias?: string }[]; - specifiers: { view: boolean, name: string, alias?: string }[]; - } - debug: any -} - -export function parse(str: string): ParseResponse { - const cell = parseCell(str); - const refs = calcRefs(cell.references, str); - - const retVal: ParseResponse = { - id: null, - refs: Object.keys(refs), - func: undefined, - debug: cell - }; - - const cellId = cell.id as Id; - const bodyStr = cell.body ? str.substring(cell.body.start, cell.body.end) : ""; - switch (cellId?.type) { - case "ViewExpression": - retVal.id = str.substring(cell.id.start, cell.id.end); - retVal.viewofValue = { - id: cell?.id?.id?.name, - refs: ["Generators", retVal.id], - func: (G, _) => G.input(_) - }; - break; - case "MutableExpression": - retVal.id = str.substring(cell.id.start, cell.id.end); - retVal.initialValue = { - id: `initial ${cell?.id?.id?.name}`, - refs: Object.keys(refs), - func: createFunction(refs, bodyStr, false, false, false) - }; - retVal.refs = ["Mutable", retVal.initialValue.id]; - retVal.func = (M, _) => { return new M(_); }; - retVal.viewofValue = { - id: cell?.id?.id?.name, - refs: [retVal.id], - func: _ => _.generator - }; - break; - case undefined: - break; - case "Identifier": - retVal.id = str.substring(cell.id.start, cell.id.end); - break; - default: - console.warn(`Unexpected cell.id.type: ${cell.id?.type}`); - retVal.id = str.substring(cell.id.start, cell.id.end); - } - - switch ((cell.body as Body)?.type) { - case "ImportDeclaration": - retVal.import = { - src: cell.body.source.value, - specifiers: cell.body.specifiers?.map(spec => { - return { - view: spec.view, - name: spec.imported.name, - alias: (spec.local?.name && spec.imported.name !== spec.local.name) ? spec.local.name : spec.imported.name - }; - }) ?? [], - injections: cell.body.injections?.map(inj => { - return { - name: inj.imported.name, - alias: inj.local?.name ?? inj.imported.name - }; - }) ?? [], - }; - break; - case undefined: - break; - case "ViewExpression": - case "Identifier": - case "BinaryExpression": - case "BlockStatement": - case "CallExpression": - case "Literal": - case "MemberExpression": - case "YieldExpression": - retVal.func = retVal.func ?? createFunction(refs, bodyStr, cell.async, cell.generator, ["BlockStatement"].indexOf(cell.body.type) >= 0); - break; - default: - console.warn(`Unexpected cell.body.type: ${cell.body?.type}`); - retVal.func = retVal.func ?? createFunction(refs, bodyStr, cell.async, cell.generator, ["BlockStatement"].indexOf(cell.body.type) >= 0); - } - - return retVal; -} \ No newline at end of file diff --git a/src/notebook-renderers/util.ts b/src/notebook-renderers/util.ts deleted file mode 100644 index a49e08a..0000000 --- a/src/notebook-renderers/util.ts +++ /dev/null @@ -1,94 +0,0 @@ -const FuncTypes = { - functionType: Object.getPrototypeOf(function () { }).constructor, - asyncFunctionType: Object.getPrototypeOf(async function () { }).constructor, - generatorFunctionType: Object.getPrototypeOf(function* () { }).constructor, - asyncGeneratorFunctionType: Object.getPrototypeOf(async function* () { }).constructor -}; - -function funcType(async: boolean = false, generator: boolean = false) { - if (!async && !generator) return FuncTypes.functionType; - if (async && !generator) return FuncTypes.asyncFunctionType; - if (!async && generator) return FuncTypes.generatorFunctionType; - return FuncTypes.asyncGeneratorFunctionType; -} - -// Hide "import" from bundlers as they have a habit of replacing "import" with "require" -export async function obfuscatedImport(url: string) { - return new FuncTypes.asyncFunctionType("url", "return import(url)")(url); -} - -export type Refs = { [key: string]: { from: string, to: string } }; -export function createFunction(refs: Refs, _body: string, async = false, generator = false, blockStatement = false) { - const args = []; - const replace = []; - let body = _body; - for (const key in refs) { - args.push(refs[key].to); - replace.push(refs[key]); - } - - // Need to sort by length - otherwise it matches on prefix... - replace.sort((l, r) => r.from.length - l.from.length); - replace.forEach(r => { - if (r.from !== r.to) { - if (r.from.indexOf("mutable ") === 0) { - body = body.split(r.from).join(`${r.to}.value`); - } else { - body = body.split(r.from).join(r.to); - } - } - }); - return new (funcType(async, generator))(...args, blockStatement ? body : `{ return (${body}); }`); -} - -export function calcRefs(refs, str): Refs { - if (refs === undefined) return {}; - const dedup = {}; - refs.forEach(r => { - const body = str.substring(r.start, r.end); - const rhs = { from: body, to: body.split(" ").join("_") }; - if (r.idxxx) { - dedup[r.id.name] = rhs; - } else if (r.name) { - dedup[r.name] = rhs; - } else if (r.start !== undefined && r.end !== undefined) { - dedup[body] = rhs; - } - }); - return dedup; -} - -export function encodeOMD(str: string) { - return str - .split("`").join("\`") - .split("$").join("\$") - ; -} - -export function encodeMD(str: string) { - return str - .split("`").join("\\`") - .split("$").join("\\$") - ; -} - -export function encodeBacktick(str: string) { - return str - .split("`").join("\\`") - ; -} - -export type OJSVariableMessageType = "error" | "pending" | "fulfilled" | "rejected"; -export class OJSSyntaxError { - name = "OJSSyntaxError"; - - constructor(public start: number, public end: number, public message: string) { - } -} - -export class OJSRuntimeError { - name = "OJSRuntimeError"; - - constructor(public severity: OJSVariableMessageType, public start: number, public end: number, public message: string) { - } -} diff --git a/src/notebook/command.ts b/src/notebook/command.ts deleted file mode 100644 index b0f25c9..0000000 --- a/src/notebook/command.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as vscode from "vscode"; -import fetch from "node-fetch"; - -export let commands: Commands; -export class Commands { - _ctx: vscode.ExtensionContext; - - private constructor(ctx: vscode.ExtensionContext) { - this._ctx = ctx; - - ctx.subscriptions.push(vscode.commands.registerCommand("ojs.download", this.download, this)); - } - - static attach(ctx: vscode.ExtensionContext): Commands { - if (!commands) { - commands = new Commands(ctx); - } - return commands; - } - - async download(fileUri?: vscode.Uri) { - const impUrl = await vscode.window.showInputBox({ - prompt: "URL", placeHolder: "https://observablehq.com/@user/notebook" - }); - if (impUrl) { - const isShared = impUrl.indexOf("https://observablehq.com/d") === 0; - const nb = await fetch(impUrl.replace(`https://observablehq.com/${isShared ? "d/" : ""}`, "https://api.observablehq.com/document/"), { - headers: { - origin: "https://observablehq.com", - referer: impUrl - } - }).then(r => r.json() as any); - if (nb) { - const saveUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(`${nb.title}.ojsnb`), - filters: { - "OJS Notebook": ["ojsnb"] - } - }); - if (saveUri) { - const buffer = Buffer.from(JSON.stringify(nb, undefined, 4)); - vscode.workspace.fs.writeFile(saveUri, buffer); - } - } - } - } - -} diff --git a/src/notebook/controller.ts b/src/notebook/controller.ts deleted file mode 100644 index 08e4bd6..0000000 --- a/src/notebook/controller.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as vscode from "vscode"; -import * as path from "path"; -import { Notebook } from "./types"; - -function encodeID(id: string) { - return id.split(" ").join("_"); -} - -export interface OJSOutput { - uri: string; - ojsSource: string; - folder: string; - notebook: Notebook; -} - -export class Controller { - readonly controllerId = "ojs-kernal"; - readonly notebookType = "ojs-notebook"; - readonly label = "OJS Notebook"; - readonly supportedLanguages = ["ojs", "html", "svg", "dot"]; - - private readonly _controller: vscode.NotebookController; - private _executionOrder = 0; - - constructor() { - this._controller = vscode.notebooks.createNotebookController( - this.controllerId, - this.notebookType, - this.label - ); - - this._controller.supportedLanguages = this.supportedLanguages; - this._controller.supportsExecutionOrder = true; - this._controller.executeHandler = this.execute.bind(this); - - const ojsMessagaging = vscode.notebooks.createRendererMessaging("ojs-notebook-renderer"); - ojsMessagaging.onDidReceiveMessage(event => { - switch (event.message.command) { - case "fetchConfigs": - ojsMessagaging.postMessage({ - type: "fetchConfigsResponse", - configurations: "hello" - }, event.editor); - break; - } - }); - - vscode.workspace.onDidChangeNotebookDocument(evt => { - // evt.notebook.uri.toString(); - }); - } - - dispose() { - this._controller.dispose(); - } - - private async executeOJS(cell: vscode.NotebookCell, uri: vscode.Uri): Promise { - - const retVal: OJSOutput = { - uri: uri.toString(), - ojsSource: cell.document.languageId === "ojs" ? - cell.document.getText() : - `${cell.document.languageId}\`${cell.document.getText()}\``, - folder: path.dirname(cell.document.uri.path), - notebook: cell.notebook.metadata.notebook - }; - return vscode.NotebookCellOutputItem.json(retVal, "application/gordonsmith.ojs+json"); - } - - private async executeCell(cell: vscode.NotebookCell, notebook: vscode.NotebookDocument): Promise { - const execution = this._controller.createNotebookCellExecution(cell); - execution.executionOrder = ++this._executionOrder; - execution.start(Date.now()); - const cellOutput = new vscode.NotebookCellOutput([], {}); - await execution.replaceOutput(cellOutput); - switch (cell.document.languageId) { - case "ojs": - case "html": - cellOutput.items.push(await this.executeOJS(cell, notebook.uri)); - break; - } - await execution.replaceOutput(cellOutput); - execution.end(true, Date.now()); - } - - private async execute(cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument): Promise { - for (const cell of cells) { - await this.executeCell(cell, notebook); - } - } -} diff --git a/src/notebook/controller/controller.ts b/src/notebook/controller/controller.ts index 10e4f0e..a431ea9 100644 --- a/src/notebook/controller/controller.ts +++ b/src/notebook/controller/controller.ts @@ -1,10 +1,7 @@ import * as vscode from "vscode"; import * as path from "path"; import { observablehq as ohq } from "../../compiler/types"; - -function encodeID(id: string) { - return id.split(" ").join("_"); -} +import { parseCell } from "../../compiler/parser"; export interface OJSOutput { uri: string; @@ -22,6 +19,10 @@ export class Controller { private readonly _controller: vscode.NotebookController; private _executionOrder = 0; + private _ojsMessagaging: vscode.NotebookRendererMessaging; + + private _oldId = new Map(); + constructor() { this._controller = vscode.notebooks.createNotebookController( this.controllerId, @@ -33,20 +34,17 @@ export class Controller { this._controller.supportsExecutionOrder = true; this._controller.executeHandler = this.execute.bind(this); - const ojsMessagaging = vscode.notebooks.createRendererMessaging("ojs-notebook-renderer"); - ojsMessagaging.onDidReceiveMessage(event => { + this._ojsMessagaging = vscode.notebooks.createRendererMessaging("ojs-notebook-renderer"); + this._ojsMessagaging.onDidReceiveMessage(event => { switch (event.message.command) { - case "fetchConfigs": - ojsMessagaging.postMessage({ - type: "fetchConfigsResponse", - configurations: "hello" - }, event.editor); + case "renderOutputItem": + break; + case "disposeOutputItem": break; } }); vscode.workspace.onDidChangeNotebookDocument(evt => { - // evt.notebook.uri.toString(); }); } @@ -54,52 +52,61 @@ export class Controller { this._controller.dispose(); } - private async executeOJS(cell: vscode.NotebookCell, uri: vscode.Uri): Promise { - - // debugger; - - // function qt(a) { - // console.log(a, this); - // } - - // const qt2 = new Function("a", "debugger; console.log(a, this);"); + ojsSource(cell: vscode.NotebookCell) { + return cell.document.languageId === "ojs" ? + cell.document.getText() : + `${cell.document.languageId}\`${cell.document.getText()}\``; + } - // qt("111"); - // qt.call("222", "333"); - // qt2("444"); - // qt2.call("555", "666"); - // // qt.apply("444", "555"); + private ojsOutput(cell: vscode.NotebookCell, uri: vscode.Uri): OJSOutput { - const retVal: OJSOutput = { + return { uri: uri.toString(), - ojsSource: cell.document.languageId === "ojs" ? - cell.document.getText() : - `${cell.document.languageId}\`${cell.document.getText()}\``, + ojsSource: this.ojsSource(cell), folder: path.dirname(cell.document.uri.path), notebook: cell.notebook.metadata.notebook }; - return vscode.NotebookCellOutputItem.json(retVal, "application/gordonsmith.ojs+json"); } - private async executeCell(cell: vscode.NotebookCell, notebook: vscode.NotebookDocument): Promise { + private executeOJS(ojsOutput: OJSOutput): vscode.NotebookCellOutputItem { + return vscode.NotebookCellOutputItem.json(ojsOutput, "application/gordonsmith.ojs+json"); + } + + private async executeCell(cell: vscode.NotebookCell, notebook: vscode.NotebookDocument): Promise<[string, OJSOutput, string?]> { const execution = this._controller.createNotebookCellExecution(cell); execution.executionOrder = ++this._executionOrder; execution.start(Date.now()); + let success = true; + try { + await parseCell(this.ojsSource(cell)); + } catch (e) { + success = false; + } + const oldId = this._oldId.get(cell); const cellOutput = new vscode.NotebookCellOutput([], {}); await execution.replaceOutput(cellOutput); + this._oldId.set(cell, (cellOutput as any).id); + let ojsOutput; switch (cell.document.languageId) { case "ojs": case "html": - cellOutput.items.push(await this.executeOJS(cell, notebook.uri)); + ojsOutput = this.ojsOutput(cell, notebook.uri); + cellOutput.items.push(this.executeOJS(ojsOutput)); break; } await execution.replaceOutput(cellOutput); - execution.end(true, Date.now()); + execution.end(success, Date.now()); + return [(cellOutput as any).id, ojsOutput, oldId]; } private async execute(cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument): Promise { + const outputs: [string, OJSOutput, string?][] = []; for (const cell of cells) { - await this.executeCell(cell, notebook); + outputs.push(await this.executeCell(cell, notebook)); } + this._ojsMessagaging.postMessage({ + command: "renderOutputItem", + outputs + }); } } diff --git a/src/notebook/controller/serializer.ts b/src/notebook/controller/serializer.ts index 3c9e661..f017f9e 100644 --- a/src/notebook/controller/serializer.ts +++ b/src/notebook/controller/serializer.ts @@ -1,6 +1,8 @@ -import { NotebookSerializer, CancellationToken, NotebookData, NotebookCellData, NotebookCellKind } from "vscode"; +import { env, NotebookSerializer, CancellationToken, NotebookData, NotebookCellData, NotebookCellKind } from "vscode"; import { TextDecoder, TextEncoder } from "util"; import { observablehq as ohq } from "../../compiler/types"; +import { Notebook } from "../../compiler/notebook"; +import { Writer } from "../../compiler/writer"; export class Serializer implements NotebookSerializer { async deserializeNotebook(content: Uint8Array, _token: CancellationToken): Promise { @@ -13,10 +15,10 @@ export class Serializer implements NotebookSerializer { notebook = { files: [], nodes: [] - } as ohq.Notebook; + } as unknown as ohq.Notebook; } - const cells = notebook.nodes.map(node => { + const cells = notebook.nodes?.map(node => { const retVal = new NotebookCellData(node.mode === "md" ? NotebookCellKind.Markup : NotebookCellKind.Code, node.value, @@ -37,9 +39,10 @@ export class Serializer implements NotebookSerializer { } async serializeNotebook(data: NotebookData, _token: CancellationToken): Promise { - const src: ohq.Notebook = data.metadata?.notebook; - src.nodes = []; + const jsonNotebook: ohq.Notebook = data.metadata?.notebook; + jsonNotebook.nodes = []; + const notebook = new Notebook(jsonNotebook); let id = 0; for (const cell of data.cells) { const item = { @@ -53,9 +56,14 @@ export class Serializer implements NotebookSerializer { "js" : cell.languageId }; - src.nodes.push(item); + jsonNotebook.nodes.push(item); + notebook.createCell().text(cell.value, cell.languageId); ++id; } - return new TextEncoder().encode(JSON.stringify(src, undefined, 4)); + const writer = new Writer(); + notebook.compile(writer); + env.clipboard.writeText(writer.toString()); + + return new TextEncoder().encode(JSON.stringify(jsonNotebook, undefined, 4)); } } diff --git a/src/notebook/renderers/index.ts b/src/notebook/renderers/index.ts deleted file mode 100644 index dd9459b..0000000 --- a/src/notebook/renderers/index.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Inspector } from "@observablehq/inspector"; -import type { ActivationFunction } from "vscode-notebook-renderer"; -import type { OJSOutput } from "../controller/controller"; -import { Notebook } from "../../compiler/notebook"; -import { Cell } from "../../compiler/cell"; - -export const activate: ActivationFunction = context => { - - const notebooks: { [uri: string]: Notebook } = {}; - const variables: { [id: string]: Cell } = {}; - - return { - renderOutputItem(outputItem, element) { - const data: OJSOutput = outputItem.json(); - if (!notebooks[data.uri]) { - notebooks[data.uri] = new Notebook(data.notebook); - } - if (!variables[outputItem.id]) { - variables[outputItem.id] = notebooks[data.uri].createCell(name => { - const div = document.createElement("div"); - element.appendChild(div); - return new Inspector(div); - }); - } - variables[outputItem.id] - .interpret(data.ojsSource) - .catch(e => { - element.innerText = e.message; - }); - }, - - disposeOutputItem(id?: string) { - if (variables[id]) { - variables[id].dispose(); - } - } - }; -}; - diff --git a/src/notebook/renderers/renderer.ts b/src/notebook/renderers/renderer.ts new file mode 100644 index 0000000..cb69476 --- /dev/null +++ b/src/notebook/renderers/renderer.ts @@ -0,0 +1,81 @@ +import { Inspector } from "@observablehq/inspector"; +import type { ActivationFunction } from "vscode-notebook-renderer"; +import type { OJSOutput } from "../controller/controller"; +import { Notebook } from "../../compiler/notebook"; +import { Cell, nullObserver } from "../../compiler/cell"; +import { observablehq } from "src/compiler/types"; + +export const activate: ActivationFunction = context => { + + const notebooks: { [uri: string]: Notebook } = {}; + const cells: { [id: string]: { cell: Cell, element?: HTMLElement } } = {}; + + context.onDidReceiveMessage!(e => { + switch (e.command) { + case "renderOutputItem": + e.outputs.forEach(([id, data, oldId]) => { + if (oldId && oldId !== id) { + disposeCell(oldId); + } + render(id, data); + }); + break; + case "disposeOutputItem": + disposeCell(e.id); + } + }); + + function render(id: string, data: OJSOutput, element?: HTMLElement) { + if (!notebooks[data.uri]) { + notebooks[data.uri] = new Notebook(data.notebook); + } + if (cells[id] && !cells[id].element && element) { + disposeCell(id); + } + if (!cells[id]) { + cells[id] = { + cell: notebooks[data.uri].createCell((name): observablehq.Inspector => { + if (element) { + const div = document.createElement("div"); + element.appendChild(div); + return new Inspector(div); + } + return nullObserver; + }), + }; + } + if (cells[id].cell.text() !== data.ojsSource) { + cells[id].cell + .text(data.ojsSource) + .evaluate() + .catch(e => { + if (element) { + element.innerText = `ERROR: ${e.message}`; + } + }); + } + } + + function disposeCell(id: string) { + if (cells[id]) { + cells[id].cell.dispose(); + delete cells[id]; + } + } + + return { + renderOutputItem(outputItem, element) { + const data: OJSOutput = outputItem.json(); + render(outputItem.id, data, element); + }, + + disposeOutputItem(id?: string) { + if (id) { + disposeCell(id); + } else { + Object.keys(cells).forEach(disposeCell); + } + } + }; +}; + diff --git a/src/notebook/serializer.ts b/src/notebook/serializer.ts deleted file mode 100644 index e514a8f..0000000 --- a/src/notebook/serializer.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NotebookSerializer, CancellationToken, NotebookData, NotebookCellData, NotebookCellKind } from "vscode"; -import { TextDecoder, TextEncoder } from "util"; -import { Notebook } from "./types"; - -export class Serializer implements NotebookSerializer { - async deserializeNotebook(content: Uint8Array, _token: CancellationToken): Promise { - const contents = new TextDecoder("utf-8").decode(content); - - let notebook: Notebook; - try { - notebook = JSON.parse(contents); - } catch { - notebook = { - nodes: [] - } as Notebook; - } - - const cells = notebook.nodes.map(node => { - const retVal = new NotebookCellData(node.mode === "md" ? - NotebookCellKind.Markup : - NotebookCellKind.Code, node.value, - node.mode === "md" ? - "markdown" : - node.mode === "html" ? - "html" : - "ojs"); - retVal.metadata = retVal.metadata ?? {}; - retVal.metadata.node = node; - return retVal; - }); - - const retVal = new NotebookData(cells); - retVal.metadata = retVal.metadata ?? {}; - retVal.metadata.notebook = notebook; - return retVal; - } - - async serializeNotebook(data: NotebookData, _token: CancellationToken): Promise { - const src: Notebook = data.metadata?.notebook; - src.nodes = []; - - let id = 0; - for (const cell of data.cells) { - const item = { - ...cell.metadata?.node, - id: id, - name: "", - value: cell.value, - mode: cell.kind === NotebookCellKind.Markup ? - "md" : - cell.languageId === "ojs" ? - "js" : - cell.languageId - }; - src.nodes.push(item); - ++id; - } - return new TextEncoder().encode(JSON.stringify(src, undefined, 4)); - } -} diff --git a/src/notebook/types.ts b/src/notebook/types.ts deleted file mode 100644 index 9288f02..0000000 --- a/src/notebook/types.ts +++ /dev/null @@ -1,141 +0,0 @@ - -export interface Owner { - id: string; - github_login: string; - avatar_url: string; - login: string; - name: string; - bio: string; - home_url: string; - type: string; - tier: string; -} - -export interface Creator { - id: string; - github_login: string; - avatar_url: string; - login: string; - name: string; - bio: string; - home_url: string; - tier: string; -} - -export interface Author { - id: string; - avatar_url: string; - name: string; - login: string; - bio: string; - home_url: string; - github_login: string; - tier: string; - approved: boolean; - description: string; -} - -export interface Owner2 { - id: string; - github_login: string; - avatar_url: string; - login: string; - name: string; - bio: string; - home_url: string; - type: string; - tier: string; -} - -export interface Collection { - id: string; - type: string; - slug: string; - title: string; - description: string; - update_time: Date; - pinned: boolean; - ordered: boolean; - custom_thumbnail?: any; - default_thumbnail: string; - thumbnail: string; - listing_count: number; - parent_collection_count: number; - owner: Owner2; -} - -export interface File { - id: string; - url: string; - download_url: string; - name: string; - create_time: Date; - status: string; - size: number; - mime_type: string; - content_encoding: string; -} - -export interface User { - id: string; - github_login: string; - avatar_url: string; - login: string; - name: string; - bio: string; - home_url: string; - tier: string; -} - -export interface Comment { - id: string; - content: string; - node_id: number; - create_time: Date; - update_time?: any; - resolved: boolean; - user: User; -} - -export interface Node { - id: number; - value: string; - pinned?: boolean; - mode: string; - data?: any; - name: string; -} - -export interface Notebook { - id: string; - slug?: any; - trashed: boolean; - description: string; - likes: number; - publish_level: string; - forks: number; - fork_of?: any; - update_time: Date; - publish_time: Date; - publish_version: number; - latest_version: number; - thumbnail: string; - default_thumbnail: string; - roles: any[]; - sharing?: any; - owner: Owner; - creator: Creator; - authors: Author[]; - collections: Collection[]; - files: File[]; - comments: Comment[]; - commenting_lock?: any; - suggestion_from?: any; - suggestions_to: any[]; - version: number; - title: string; - license: string; - copyright: string; - nodes: Node[]; - resolutions: any[]; -} diff --git a/src/ojs/command.ts b/src/ojs/command.ts index 4e56f0c..d5fcb1b 100644 --- a/src/ojs/command.ts +++ b/src/ojs/command.ts @@ -14,7 +14,7 @@ export function encode(str: string) { ; } -const isObservableFile = (languageId: string) => languageId === "omd" || languageId === "ojs"; +const isObservableFile = (languageId?: string) => languageId === "omd" || languageId === "ojs"; export let commands: Commands; export class Commands { @@ -51,7 +51,7 @@ export class Commands { } async preview(fileUri?: vscode.Uri) { - let textDocument: vscode.TextDocument; + let textDocument: vscode.TextDocument | undefined; if (fileUri) { textDocument = await vscode.workspace.openTextDocument(fileUri); } else if (vscode.window.activeTextEditor) { @@ -171,19 +171,21 @@ ${encode(node.value)} if (textEditor) { InsertText(textEditor, () => text); } else { - const folder = vscode.workspace.workspaceFolders[0]?.uri.path; - const filePath = path.posix.join(folder, `Untitled-${Math.round(1000 + Math.random() * 1000)}.${languageId}`); - const newFile = vscode.Uri.parse("untitled://" + filePath); - const document = await vscode.workspace.openTextDocument(newFile); - const edit = new vscode.WorkspaceEdit(); - edit.insert(newFile, new vscode.Position(0, 0), text); - await vscode.workspace.applyEdit(edit).then(success => { - if (success) { - vscode.window.showTextDocument(document); - } else { - vscode.window.showInformationMessage("Error!"); - } - }); + const folder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]?.uri.path; + if (folder) { + const filePath = path.posix.join(folder, `Untitled-${Math.round(1000 + Math.random() * 1000)}.${languageId}`); + const newFile = vscode.Uri.parse("untitled://" + filePath); + const document = await vscode.workspace.openTextDocument(newFile); + const edit = new vscode.WorkspaceEdit(); + edit.insert(newFile, new vscode.Position(0, 0), text); + await vscode.workspace.applyEdit(edit).then(success => { + if (success) { + vscode.window.showTextDocument(document); + } else { + vscode.window.showInformationMessage("Error!"); + } + }); + } } } } diff --git a/src/ojs/meta.ts b/src/ojs/meta.ts index e270e0c..2cff988 100644 --- a/src/ojs/meta.ts +++ b/src/ojs/meta.ts @@ -75,11 +75,11 @@ export class Meta { private constructor() { } - static attach(doc: vscode.TextDocument): Meta | undefined { + static attach(doc: vscode.TextDocument): Meta { if (!meta.has(doc.uri.fsPath)) { meta.set(doc.uri.fsPath, new Meta()); } - const m = meta.get(doc.uri.fsPath); + const m = meta.get(doc.uri.fsPath)!; if (!m._doc || m._doc.version < doc.version) { return m.refresh(doc); } @@ -112,7 +112,7 @@ export class Meta { this._cells.forEach(cell => { this._cellMap[cell.uid] = cell; }); - } catch (e) { + } catch (e: any) { const pos = e.pos || 0; let raisedAt = e.raisedAt || pos; raisedAt += raisedAt === pos ? 1 : 0; @@ -145,7 +145,7 @@ export class Meta { updateRuntimeValues() { const ojsConfig = vscode.workspace.getConfiguration("ojs"); const includeValues = ojsConfig.get("showRuntimeValues"); - const errors = []; + const errors: vscode.Diagnostic[] = []; this._cells.forEach(cell => { if (includeValues || cell.value.error) { errors.push(new vscode.Diagnostic(cell.idRange || cell.range, cell.value.value, cell.value.error ? vscode.DiagnosticSeverity.Warning : vscode.DiagnosticSeverity.Information)); diff --git a/src/ojs/preview.ts b/src/ojs/preview.ts index 413d171..07732e3 100644 --- a/src/ojs/preview.ts +++ b/src/ojs/preview.ts @@ -29,7 +29,7 @@ export class Preview { // Otherwise, create a new panel. const localResourceRoots = [ vscode.Uri.file(path.join(ctx.extensionPath, "dist")), - ...vscode.workspace.workspaceFolders.map(wf => wf.uri) + ...vscode.workspace.workspaceFolders?.map(wf => wf.uri) ?? [] ]; const panel = vscode.window.createWebviewPanel( Preview.viewType, @@ -75,7 +75,7 @@ export class Preview { } _callbackID = 0; - _callbacks: { [key: number]: (msg: WebviewMessage) => void } = {}; + _callbacks: { [key: number]: (msg: ValueMessage) => void } = {}; _doc: vscode.TextDocument; async init() { // Handle messages from the webview @@ -91,7 +91,7 @@ export class Preview { // }, 1000); return new Promise((resolve, reject) => { this._panel.webview.onDidReceiveMessage((message: WebviewMessage) => { - const callback: (msg: WebviewMessage) => void = this._callbacks[message.callbackID]; + const callback: (msg: WebviewMessage) => void = message.callbackID && this._callbacks[message.callbackID]; if (callback) { callback(message); } else { diff --git a/src/test/index.ts b/src/test/index.ts index e6b1a8f..5ece0f0 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -2,9 +2,9 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ -import * as glob from "glob"; -import * as Mocha from "mocha"; -import * as path from "path"; +import glob from "glob"; +import Mocha from "mocha"; +import path from "path"; export function run(): Promise { // Create the mocha test diff --git a/src/util/fs.ts b/src/util/fs.ts index 21aab11..626f321 100644 --- a/src/util/fs.ts +++ b/src/util/fs.ts @@ -101,7 +101,7 @@ export async function eclTempFile(document: TextDocument): Promise { } }; if (document.isUntitled) { - tmpFile = await writeTempFile({ prefix: leafname(document.fileName), content: document.getText(), folder: workspace.workspaceFolders[0]?.uri?.fsPath, ext: "ecl" }); + tmpFile = await writeTempFile({ prefix: leafname(document.fileName), content: document.getText(), folder: workspace.workspaceFolders && workspace.workspaceFolders[0]?.uri?.fsPath, ext: "ecl" }); } else if (document.isDirty) { tmpFile = await writeTempFile({ prefix: leafname(document.fileName), content: document.getText(), folder: dirname(document.fileName), ext: "ecl" }); } diff --git a/src/webview.ts b/src/webview.ts index 66febf2..6ec0400 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,5 +1,5 @@ /* eslint-disable no-inner-declarations */ -import { OJSRuntime, OJSRuntimeError, OJSSyntaxError, OMDRuntime, VariableValue } from "@hpcc-js/observable-md"; +import { OJSRuntime, OJSSyntaxError, OMDRuntime, VariableValue } from "@hpcc-js/observable-md"; import { hashSum, IObserverHandle } from "@hpcc-js/util"; export interface Message { @@ -35,7 +35,7 @@ interface VSCodeAPI { declare const acquireVsCodeApi: () => VSCodeAPI; -const placeholder = document.getElementById("placeholder"); +const placeholder = document.getElementById("placeholder")!; if (window["__hpcc_test"]) { placeholder.innerText = ""; diff --git a/tsconfig.json b/tsconfig.json index 6bd1035..1128a47 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,22 +2,25 @@ "compilerOptions": { "baseUrl": ".", "rootDir": "./src", - "module": "es6", - "moduleResolution": "node", - "target": "ES2019", "outDir": "lib-es6", + "target": "ES2021", + "module": "ES6", + "declaration": true, + "declarationDir": "./types", "sourceMap": true, - "noImplicitAny": false, - "noEmitOnError": false, - "noUnusedLocals": false, - "strictNullChecks": false, + "declarationMap": true, + "moduleResolution": "Node", "importHelpers": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, "skipLibCheck": true, + "noImplicitAny": false, + "strictPropertyInitialization": false, "lib": [ - "dom", - "ES2019" + "DOM", + "ES2021" ], "paths": { "@hpcc-js/*": [