Tons of progress
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { Editor } from "./editor";
|
||||
import { AppCommand, CommandContext } from "./types";
|
||||
|
||||
export function buildContext(cmd: AppCommand, editor: Editor) {
|
||||
let ctx: CommandContext = {};
|
||||
if (!cmd.command.requiredContext) {
|
||||
return ctx;
|
||||
}
|
||||
if (cmd.command.requiredContext.text) {
|
||||
ctx.text = editor.editorView?.state.sliceDoc();
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,20 +1,29 @@
|
||||
import { AppCommand } from "../types";
|
||||
import { FilterList } from "./filter";
|
||||
import { FilterList, Option } from "./filter";
|
||||
|
||||
export function CommandPalette({
|
||||
commands,
|
||||
onTrigger,
|
||||
}: {
|
||||
commands: AppCommand[];
|
||||
onTrigger: (command: AppCommand) => void;
|
||||
commands: Map<string, AppCommand>;
|
||||
onTrigger: (command: AppCommand | undefined) => void;
|
||||
}) {
|
||||
let options: Option[] = [];
|
||||
for (let [name, def] of commands.entries()) {
|
||||
options.push({ name: name });
|
||||
}
|
||||
console.log("Commands", options);
|
||||
return (
|
||||
<FilterList
|
||||
placeholder="Enter command to run"
|
||||
options={commands}
|
||||
options={options}
|
||||
allowNew={false}
|
||||
onSelect={(opt) => {
|
||||
onTrigger(opt as AppCommand);
|
||||
if (opt) {
|
||||
onTrigger(commands.get(opt.name));
|
||||
} else {
|
||||
onTrigger(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { autocompletion, completionKeymap } from "@codemirror/autocomplete";
|
||||
import {
|
||||
autocompletion,
|
||||
CompletionContext,
|
||||
completionKeymap,
|
||||
CompletionResult,
|
||||
} from "@codemirror/autocomplete";
|
||||
import { closeBrackets, closeBracketsKeymap } from "@codemirror/closebrackets";
|
||||
import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
||||
import { history, historyKeymap } from "@codemirror/history";
|
||||
import { indentOnInput } from "@codemirror/language";
|
||||
import { indentOnInput, syntaxTree } from "@codemirror/language";
|
||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||
import { searchKeymap } from "@codemirror/search";
|
||||
import { EditorState, StateField, Transaction } from "@codemirror/state";
|
||||
import { KeyBinding } from "@codemirror/view";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
@@ -13,33 +19,33 @@ import {
|
||||
highlightSpecialChars,
|
||||
keymap,
|
||||
} from "@codemirror/view";
|
||||
import React, { useEffect, useReducer, useRef } from "react";
|
||||
import React, { useEffect, useReducer } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import coreManifest from "../../plugins/dist/core.plugin.json";
|
||||
import { buildContext } from "./buildContext";
|
||||
import * as commands from "./commands";
|
||||
import { CommandPalette } from "./components/commandpalette";
|
||||
import { NavigationBar } from "./components/navigation_bar";
|
||||
import { NoteNavigator } from "./components/notenavigator";
|
||||
import { StatusBar } from "./components/status_bar";
|
||||
import { FileSystem, HttpFileSystem } from "./fs";
|
||||
import { lineWrapper } from "./lineWrapper";
|
||||
import { markdown } from "./markdown";
|
||||
import customMarkDown from "./parser";
|
||||
import { BrowserSystem } from "./plugins/browser_system";
|
||||
import { Manifest } from "./plugins/types";
|
||||
import reducer from "./reducer";
|
||||
import customMarkdownStyle from "./style";
|
||||
import { Action, AppViewState } from "./types";
|
||||
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import * as util from "./util";
|
||||
import { NoteMeta } from "./types";
|
||||
|
||||
const initialViewState: AppViewState = {
|
||||
isSaved: false,
|
||||
showNoteNavigator: false,
|
||||
showCommandPalette: false,
|
||||
allNotes: [],
|
||||
};
|
||||
|
||||
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||
import { NavigationBar } from "./components/navigation_bar";
|
||||
import { StatusBar } from "./components/status_bar";
|
||||
import dbSyscalls from "./syscalls/db.localstorage";
|
||||
import editorSyscalls from "./syscalls/editor.browser";
|
||||
import {
|
||||
Action,
|
||||
AppCommand,
|
||||
AppViewState,
|
||||
CommandContext,
|
||||
initialViewState,
|
||||
} from "./types";
|
||||
import { safeRun } from "./util";
|
||||
|
||||
class NoteState {
|
||||
editorState: EditorState;
|
||||
@@ -51,15 +57,18 @@ class NoteState {
|
||||
}
|
||||
}
|
||||
|
||||
class Editor {
|
||||
export class Editor {
|
||||
editorView?: EditorView;
|
||||
viewState: AppViewState;
|
||||
viewDispatch: React.Dispatch<Action>;
|
||||
$hashChange?: () => void;
|
||||
openNotes: Map<string, NoteState>;
|
||||
fs: FileSystem;
|
||||
editorCommands: Map<string, AppCommand>;
|
||||
|
||||
constructor(fs: FileSystem, parent: Element) {
|
||||
this.editorCommands = new Map();
|
||||
this.openNotes = new Map();
|
||||
this.fs = fs;
|
||||
this.viewState = initialViewState;
|
||||
this.viewDispatch = () => {};
|
||||
@@ -69,9 +78,37 @@ class Editor {
|
||||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
this.addListeners();
|
||||
this.loadNoteList();
|
||||
this.openNotes = new Map();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadNoteList();
|
||||
await this.loadPlugins();
|
||||
this.$hashChange!();
|
||||
this.focus();
|
||||
}
|
||||
|
||||
async loadPlugins() {
|
||||
const system = new BrowserSystem("plugin");
|
||||
system.registerSyscalls(dbSyscalls, editorSyscalls(this));
|
||||
|
||||
await system.bootServiceWorker();
|
||||
console.log("Now loading core plugin");
|
||||
let mainCartridge = await system.load("core", coreManifest as Manifest);
|
||||
this.editorCommands = new Map<string, AppCommand>();
|
||||
const cmds = mainCartridge.manifest!.commands;
|
||||
for (let name in cmds) {
|
||||
let cmd = cmds[name];
|
||||
this.editorCommands.set(name, {
|
||||
command: cmd,
|
||||
run: async (arg: CommandContext): Promise<any> => {
|
||||
return await mainCartridge.invoke(cmd.invoke, [arg]);
|
||||
},
|
||||
});
|
||||
}
|
||||
this.viewDispatch({
|
||||
type: "update-commands",
|
||||
commands: this.editorCommands,
|
||||
});
|
||||
}
|
||||
|
||||
get currentNote(): string | undefined {
|
||||
@@ -80,6 +117,23 @@ class Editor {
|
||||
|
||||
createEditorState(text: string): EditorState {
|
||||
const editor = this;
|
||||
let commandKeyBindings: KeyBinding[] = [];
|
||||
for (let def of this.editorCommands.values()) {
|
||||
if (def.command.key) {
|
||||
commandKeyBindings.push({
|
||||
key: def.command.key,
|
||||
mac: def.command.mac,
|
||||
run: (): boolean => {
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
await def.run(buildContext(def, this));
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
return true;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return EditorState.create({
|
||||
doc: text,
|
||||
extensions: [
|
||||
@@ -110,6 +164,7 @@ class Editor {
|
||||
...historyKeymap,
|
||||
...completionKeymap,
|
||||
indentWithTab,
|
||||
...commandKeyBindings,
|
||||
{
|
||||
key: "Ctrl-b",
|
||||
mac: "Cmd-b",
|
||||
@@ -133,25 +188,6 @@ class Editor {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Ctrl-Enter",
|
||||
mac: "Cmd-Enter",
|
||||
run: (target): boolean => {
|
||||
// TODO: Factor this and click handler into one action
|
||||
let selection = target.state.selection.main;
|
||||
if (selection.empty) {
|
||||
let node = syntaxTree(target.state).resolveInner(
|
||||
selection.from
|
||||
);
|
||||
if (node && node.name === "WikiLinkPage") {
|
||||
let noteName = target.state.sliceDoc(node.from, node.to);
|
||||
this.navigate(noteName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "Ctrl-p",
|
||||
mac: "Cmd-p",
|
||||
@@ -371,10 +407,13 @@ class Editor {
|
||||
dispatch({ type: "hide-palette" });
|
||||
editor!.focus();
|
||||
if (cmd) {
|
||||
console.log("Run", cmd);
|
||||
safeRun(async () => {
|
||||
let result = await cmd.run(buildContext(cmd, editor));
|
||||
console.log("Result of command", result);
|
||||
});
|
||||
}
|
||||
}}
|
||||
commands={[{ name: "My command", run: () => {} }]}
|
||||
commands={viewState.commands}
|
||||
/>
|
||||
)}
|
||||
<NavigationBar
|
||||
@@ -400,7 +439,13 @@ let ed = new Editor(
|
||||
document.getElementById("root")!
|
||||
);
|
||||
|
||||
ed.focus();
|
||||
ed.loadPlugins().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
safeRun(async () => {
|
||||
await ed.init();
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.editor = ed;
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Noot</title>
|
||||
<link rel="stylesheet" href="styles.css" />
|
||||
<script type="module" src="app.tsx"></script>
|
||||
<script type="module" src="editor.tsx"></script>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
</head>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Manifest } from "./plugins/types";
|
||||
|
||||
import { openDB, wrap, unwrap } from "idb";
|
||||
|
||||
const rootUrl = location.origin + "/plugin";
|
||||
|
||||
// Storing manifests in IndexedDB, y'all
|
||||
let manifestCache = caches.open("manifests");
|
||||
|
||||
const db = openDB("manifests-store", undefined, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore("manifests");
|
||||
},
|
||||
});
|
||||
|
||||
async function saveManifest(name: string, manifest: Manifest) {
|
||||
await (await db).put("manifests", manifest, name);
|
||||
}
|
||||
|
||||
async function getManifest(name: string): Promise<Manifest | undefined> {
|
||||
return (await (await db).get("manifests", name)) as Manifest | undefined;
|
||||
}
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
console.log("Installing");
|
||||
// @ts-ignore
|
||||
self.skipWaiting();
|
||||
// event.waitUntil(fetchBundle());
|
||||
});
|
||||
|
||||
async function handlePut(req: Request, path: string) {
|
||||
console.log("Got manifest load for", path);
|
||||
let manifest = (await req.json()) as Manifest;
|
||||
await saveManifest(path, manifest);
|
||||
// loadedBundles.set(path, manifest);
|
||||
return new Response("ok");
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event: any) => {
|
||||
const req = event.request;
|
||||
if (req.url.startsWith(rootUrl)) {
|
||||
let path = req.url.substring(rootUrl.length + 1);
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
// console.log("Service worker is serving", path);
|
||||
if (path === `$ping`) {
|
||||
// console.log("Got ping");
|
||||
return new Response("ok");
|
||||
}
|
||||
|
||||
if (req.method === "PUT") {
|
||||
return await handlePut(req, path);
|
||||
}
|
||||
|
||||
let [cartridgeName, resourceType, functionName] = path.split("/");
|
||||
|
||||
let manifest = await getManifest(cartridgeName);
|
||||
|
||||
if (!manifest) {
|
||||
// console.log("Ain't got", cartridgeName);
|
||||
return new Response(`Cartridge not loaded: ${cartridgeName}`, {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
if (resourceType === "$manifest") {
|
||||
return new Response(JSON.stringify(manifest));
|
||||
}
|
||||
|
||||
if (resourceType === "function") {
|
||||
let func = manifest.functions[functionName];
|
||||
// console.log("Serving function", functionName, func);
|
||||
if (!func) {
|
||||
return new Response("Not found", {
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
return new Response(func.code, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-type": "application/javascript",
|
||||
},
|
||||
});
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
// console.log("Now ready to pick up fetches");
|
||||
// @ts-ignore
|
||||
event.waitUntil(self.clients.claim());
|
||||
});
|
||||
|
||||
// console.log("I'm a service worker, look at me!", location.href);
|
||||
@@ -0,0 +1,56 @@
|
||||
import { CartridgeLoader, System } from "./runtime";
|
||||
import { Manifest } from "./types";
|
||||
import { sleep } from "../util";
|
||||
|
||||
export class BrowserLoader implements CartridgeLoader {
|
||||
readonly pathPrefix: string;
|
||||
|
||||
constructor(pathPrefix: string) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
async load(name: string, manifest: Manifest): Promise<void> {
|
||||
await fetch(`${this.pathPrefix}/${name}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(manifest),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowserSystem extends System {
|
||||
constructor(pathPrefix: string) {
|
||||
super(new BrowserLoader(pathPrefix), pathPrefix);
|
||||
}
|
||||
// Service worker stuff
|
||||
async pollServiceWorkerActive() {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
try {
|
||||
console.log("Pinging...", `${this.pathPrefix}/$ping`);
|
||||
let ping = await fetch(`${this.pathPrefix}/$ping`);
|
||||
let text = await ping.text();
|
||||
if (ping.status === 200 && text === "ok") {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Not yet");
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
// Alright, something's messed up
|
||||
throw new Error("Worker not successfully activated");
|
||||
}
|
||||
|
||||
async bootServiceWorker() {
|
||||
// @ts-ignore
|
||||
let reg = navigator.serviceWorker.register(
|
||||
new URL("../plugin_sw.ts", import.meta.url),
|
||||
{
|
||||
type: "module",
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Service worker registered successfully");
|
||||
|
||||
await this.pollServiceWorkerActive();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
function safeRun(fn: () => Promise<void>) {
|
||||
fn().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
let func = null;
|
||||
let pendingRequests: {
|
||||
[key: number]: any;
|
||||
} = {};
|
||||
|
||||
self.addEventListener("syscall", (event) => {
|
||||
let customEvent = event as CustomEvent;
|
||||
let detail = customEvent.detail;
|
||||
pendingRequests[detail.id] = detail.callback;
|
||||
self.postMessage({
|
||||
type: "syscall",
|
||||
id: detail.id,
|
||||
name: detail.name,
|
||||
args: detail.args,
|
||||
});
|
||||
});
|
||||
|
||||
self.addEventListener("result", (event) => {
|
||||
let customEvent = event as CustomEvent;
|
||||
self.postMessage({
|
||||
type: "result",
|
||||
result: customEvent.detail,
|
||||
});
|
||||
});
|
||||
|
||||
self.addEventListener("app-error", (event) => {
|
||||
let customEvent = event as CustomEvent;
|
||||
postMessage({
|
||||
type: "error",
|
||||
reason: customEvent.detail,
|
||||
});
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
safeRun(async () => {
|
||||
let messageEvent = event as MessageEvent;
|
||||
let data = messageEvent.data;
|
||||
switch (data.type) {
|
||||
case "boot":
|
||||
console.log("Booting", `./${data.prefix}/function/${data.name}`);
|
||||
importScripts(`./${data.prefix}/function/${data.name}`);
|
||||
// if (data.userAgent && data.userAgent.indexOf("Firefox") !== -1) {
|
||||
// // @ts-ignore
|
||||
// } else {
|
||||
// await import(`./${data.prefix}/function/${data.name}`);
|
||||
// }
|
||||
self.postMessage({
|
||||
type: "inited",
|
||||
});
|
||||
break;
|
||||
case "invoke":
|
||||
self.dispatchEvent(
|
||||
new CustomEvent("invoke-function", {
|
||||
detail: {
|
||||
args: data.args || [],
|
||||
},
|
||||
})
|
||||
);
|
||||
break;
|
||||
case "syscall-response":
|
||||
let id = data.id;
|
||||
const lookup = pendingRequests[id];
|
||||
if (!lookup) {
|
||||
console.log(
|
||||
"Current outstanding requests",
|
||||
pendingRequests,
|
||||
"looking up",
|
||||
id
|
||||
);
|
||||
throw Error("Invalid request id");
|
||||
}
|
||||
return await lookup(data.data);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Manifest } from "./types";
|
||||
|
||||
export class SyscallContext {
|
||||
public cartridge: Cartridge;
|
||||
|
||||
constructor(cartridge: Cartridge) {
|
||||
this.cartridge = cartridge;
|
||||
}
|
||||
}
|
||||
|
||||
interface SysCallMapping {
|
||||
// TODO: Better typing
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class FunctionWorker {
|
||||
private worker: Worker;
|
||||
private inited: Promise<any>;
|
||||
private initCallback: any;
|
||||
private invokeResolve?: (result?: any) => void;
|
||||
private invokeReject?: (reason?: any) => void;
|
||||
private cartridge: Cartridge;
|
||||
|
||||
constructor(cartridge: Cartridge, pathPrefix: string, name: string) {
|
||||
this.worker = new Worker(new URL("function_worker.ts", import.meta.url));
|
||||
// console.log("Starting worker", this.worker);
|
||||
this.worker.onmessage = this.onmessage.bind(this);
|
||||
this.worker.postMessage({
|
||||
type: "boot",
|
||||
prefix: pathPrefix,
|
||||
name: name,
|
||||
// @ts-ignore
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
this.inited = new Promise((resolve) => {
|
||||
this.initCallback = resolve;
|
||||
});
|
||||
this.cartridge = cartridge;
|
||||
}
|
||||
|
||||
async onmessage(evt: MessageEvent) {
|
||||
let data = evt.data;
|
||||
if (!data) return;
|
||||
switch (data.type) {
|
||||
case "inited":
|
||||
this.initCallback();
|
||||
break;
|
||||
case "syscall":
|
||||
const ctx = new SyscallContext(this.cartridge);
|
||||
let result = await this.cartridge.system.syscall(
|
||||
ctx,
|
||||
data.name,
|
||||
data.args
|
||||
);
|
||||
|
||||
this.worker.postMessage({
|
||||
type: "syscall-response",
|
||||
id: data.id,
|
||||
data: result,
|
||||
});
|
||||
break;
|
||||
case "result":
|
||||
this.invokeResolve!(data.result);
|
||||
break;
|
||||
case "error":
|
||||
this.invokeReject!(data.reason);
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type", data);
|
||||
}
|
||||
}
|
||||
|
||||
async invoke(args: Array<any>): Promise<any> {
|
||||
await this.inited;
|
||||
this.worker.postMessage({
|
||||
type: "invoke",
|
||||
args: args,
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
this.invokeResolve = resolve;
|
||||
this.invokeReject = reject;
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.worker.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
export interface CartridgeLoader {
|
||||
load(name: string, manifest: Manifest): Promise<void>;
|
||||
}
|
||||
|
||||
export class Cartridge {
|
||||
pathPrefix: string;
|
||||
system: System;
|
||||
private runningFunctions: Map<string, FunctionWorker>;
|
||||
public manifest?: Manifest;
|
||||
private name: string;
|
||||
|
||||
constructor(system: System, pathPrefix: string, name: string) {
|
||||
this.name = name;
|
||||
this.pathPrefix = `${pathPrefix}/${name}`;
|
||||
this.system = system;
|
||||
this.runningFunctions = new Map<string, FunctionWorker>();
|
||||
}
|
||||
|
||||
async load(manifest: Manifest) {
|
||||
this.manifest = manifest;
|
||||
await this.system.cartridgeLoader.load(this.name, manifest);
|
||||
await this.dispatchEvent("load");
|
||||
}
|
||||
|
||||
async invoke(name: string, args: Array<any>): Promise<any> {
|
||||
if (!this.runningFunctions.has(name)) {
|
||||
this.runningFunctions.set(
|
||||
name,
|
||||
new FunctionWorker(this, this.pathPrefix, name)
|
||||
);
|
||||
}
|
||||
return await this.runningFunctions.get(name)!.invoke(args);
|
||||
}
|
||||
|
||||
async dispatchEvent(name: string, data?: any) {
|
||||
let functionsToSpawn = this.manifest!.events[name];
|
||||
if (functionsToSpawn) {
|
||||
await Promise.all(
|
||||
functionsToSpawn.map(async (functionToSpawn: string) => {
|
||||
await this.invoke(functionToSpawn, [data]);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
for (const [functionname, worker] of Object.entries(
|
||||
this.runningFunctions
|
||||
)) {
|
||||
console.log(`Stopping ${functionname}`);
|
||||
worker.stop();
|
||||
}
|
||||
this.runningFunctions = new Map<string, FunctionWorker>();
|
||||
}
|
||||
}
|
||||
|
||||
export class System {
|
||||
protected cartridges: Map<string, Cartridge>;
|
||||
protected pathPrefix: string;
|
||||
registeredSyscalls: SysCallMapping;
|
||||
cartridgeLoader: CartridgeLoader;
|
||||
|
||||
constructor(cartridgeLoader: CartridgeLoader, pathPrefix: string) {
|
||||
this.cartridgeLoader = cartridgeLoader;
|
||||
this.pathPrefix = pathPrefix;
|
||||
this.cartridges = new Map<string, Cartridge>();
|
||||
this.registeredSyscalls = {};
|
||||
}
|
||||
|
||||
registerSyscalls(...registrationObjects: Array<SysCallMapping>) {
|
||||
for (const registrationObject of registrationObjects) {
|
||||
for (let p in registrationObject) {
|
||||
this.registeredSyscalls[p] = registrationObject[p];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async syscall(
|
||||
ctx: SyscallContext,
|
||||
name: string,
|
||||
args: Array<any>
|
||||
): Promise<any> {
|
||||
const callback = this.registeredSyscalls[name];
|
||||
if (!name) {
|
||||
throw Error(`Unregistered syscall ${name}`);
|
||||
}
|
||||
if (!callback) {
|
||||
throw Error(`Registered but not implemented syscall ${name}`);
|
||||
}
|
||||
return Promise.resolve(callback(ctx, ...args));
|
||||
}
|
||||
|
||||
async load(name: string, manifest: Manifest): Promise<Cartridge> {
|
||||
const cartridge = new Cartridge(this, this.pathPrefix, name);
|
||||
await cartridge.load(manifest);
|
||||
this.cartridges.set(name, cartridge);
|
||||
return cartridge;
|
||||
}
|
||||
|
||||
async stop(): Promise<void[]> {
|
||||
return Promise.all(
|
||||
Array.from(this.cartridges.values()).map((cartridge) => cartridge.stop())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Starting");
|
||||
@@ -0,0 +1,27 @@
|
||||
export interface Manifest {
|
||||
events: { [key: string]: string[] };
|
||||
commands: {
|
||||
[key: string]: CommandDef;
|
||||
};
|
||||
functions: {
|
||||
[key: string]: FunctionDef;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CommandDef {
|
||||
// Function name to invoke
|
||||
invoke: string;
|
||||
|
||||
// Bind to keyboard shortcut
|
||||
key?: string;
|
||||
mac?: string;
|
||||
// Required context to be passed in as function arguments
|
||||
requiredContext?: {
|
||||
text?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FunctionDef {
|
||||
path: string;
|
||||
code?: string;
|
||||
}
|
||||
@@ -51,6 +51,11 @@ export default function reducer(
|
||||
...state,
|
||||
showCommandPalette: false,
|
||||
};
|
||||
case "update-commands":
|
||||
return {
|
||||
...state,
|
||||
commands: action.commands,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { SyscallContext } from "../plugins/runtime";
|
||||
|
||||
export default {
|
||||
"db.put": (ctx: SyscallContext, key: string, value: any) => {
|
||||
localStorage.setItem(key, value);
|
||||
},
|
||||
"db.get": (ctx: SyscallContext, key: string) => {
|
||||
return localStorage.getItem(key);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Editor } from "../editor";
|
||||
import { SyscallContext } from "../plugins/runtime";
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
|
||||
export default (editor: Editor) => ({
|
||||
"editor.getText": (ctx: SyscallContext) => {
|
||||
return editor.editorView?.state.sliceDoc();
|
||||
},
|
||||
"editor.getCursor": (ctx: SyscallContext): number => {
|
||||
return editor.editorView!.state.selection.main.from;
|
||||
},
|
||||
"editor.navigate": async (ctx: SyscallContext, name: string) => {
|
||||
await editor.navigate(name);
|
||||
},
|
||||
"editor.insertAtPos": (ctx: SyscallContext, text: string, pos: number) => {
|
||||
editor.editorView!.dispatch({
|
||||
changes: {
|
||||
insert: text,
|
||||
from: pos,
|
||||
},
|
||||
});
|
||||
},
|
||||
"editor.replaceRange": (
|
||||
ctx: SyscallContext,
|
||||
from: number,
|
||||
to: number,
|
||||
text: string
|
||||
) => {
|
||||
editor.editorView!.dispatch({
|
||||
changes: {
|
||||
insert: text,
|
||||
from: from,
|
||||
to: to,
|
||||
},
|
||||
});
|
||||
},
|
||||
"editor.moveCursor": (ctx: SyscallContext, pos: number) => {
|
||||
editor.editorView!.dispatch({
|
||||
selection: {
|
||||
anchor: pos,
|
||||
},
|
||||
});
|
||||
},
|
||||
"editor.insertAtCursor": (ctx: SyscallContext, text: string) => {
|
||||
let editorView = editor.editorView!;
|
||||
let from = editorView.state.selection.main.from;
|
||||
editorView.dispatch({
|
||||
changes: {
|
||||
insert: text,
|
||||
from: from,
|
||||
},
|
||||
selection: {
|
||||
anchor: from + text.length,
|
||||
},
|
||||
});
|
||||
},
|
||||
"editor.getSyntaxNodeUnderCursor": (
|
||||
ctx: SyscallContext
|
||||
): { name: string; text: string } | undefined => {
|
||||
const editorState = editor.editorView!.state;
|
||||
let selection = editorState.selection.main;
|
||||
if (selection.empty) {
|
||||
let node = syntaxTree(editorState).resolveInner(selection.from);
|
||||
if (node) {
|
||||
return {
|
||||
name: node.name,
|
||||
text: editorState.sliceDoc(node.from, node.to),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { SyscallContext } from "../plugins/runtime";
|
||||
|
||||
export default {
|
||||
"event.publish": async (ctx: SyscallContext, name: string, data: any) => {
|
||||
await ctx.cartridge.dispatchEvent(name, data);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { SyscallContext } from "../plugins/runtime";
|
||||
|
||||
// @ts-ignore
|
||||
let frameTest = document.getElementById("main-frame");
|
||||
|
||||
window.addEventListener("message", async (event) => {
|
||||
let messageEvent = event as MessageEvent;
|
||||
let data = messageEvent.data;
|
||||
if (data.type === "iframe_event") {
|
||||
// @ts-ignore
|
||||
window.mainCartridge.dispatchEvent(data.data.event, data.data.data);
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
"ui.update": function (ctx: SyscallContext, doc: any) {
|
||||
// frameTest.contentWindow.postMessage({
|
||||
// type: "loadContent",
|
||||
// doc: doc,
|
||||
// });
|
||||
},
|
||||
};
|
||||
+18
-2
@@ -1,10 +1,16 @@
|
||||
import { CommandDef } from "./plugins/types";
|
||||
|
||||
export type NoteMeta = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type CommandContext = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type AppCommand = {
|
||||
name: string;
|
||||
run: () => void;
|
||||
command: CommandDef;
|
||||
run: (ctx: CommandContext) => Promise<any>;
|
||||
};
|
||||
|
||||
export type AppViewState = {
|
||||
@@ -13,6 +19,15 @@ export type AppViewState = {
|
||||
showNoteNavigator: boolean;
|
||||
showCommandPalette: boolean;
|
||||
allNotes: NoteMeta[];
|
||||
commands: Map<string, AppCommand>;
|
||||
};
|
||||
|
||||
export const initialViewState: AppViewState = {
|
||||
isSaved: false,
|
||||
showNoteNavigator: false,
|
||||
showCommandPalette: false,
|
||||
allNotes: [],
|
||||
commands: new Map(),
|
||||
};
|
||||
|
||||
export type Action =
|
||||
@@ -22,5 +37,6 @@ export type Action =
|
||||
| { type: "notes-listed"; notes: NoteMeta[] }
|
||||
| { type: "start-navigate" }
|
||||
| { type: "stop-navigate" }
|
||||
| { type: "update-commands"; commands: Map<string, AppCommand> }
|
||||
| { type: "show-palette" }
|
||||
| { type: "hide-palette" };
|
||||
|
||||
@@ -7,3 +7,17 @@ export function readingTime(wordCount: number): number {
|
||||
// 225 is average word reading speed for adults
|
||||
return Math.ceil(wordCount / 225);
|
||||
}
|
||||
|
||||
export function safeRun(fn: () => Promise<void>) {
|
||||
fn().catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user