File watching and various tweaks

pull/3/head
Zef Hemel 2022-02-25 15:34:00 +01:00
parent 0527430626
commit 2986c2c231
21 changed files with 264 additions and 119 deletions

View File

@ -13,7 +13,8 @@
"requiredContext": {} "requiredContext": {}
}, },
"Insert Current Date": { "Insert Current Date": {
"invoke": "insert_nice_date" "invoke": "insert_nice_date",
"slashCommand": "/insert-today"
}, },
"Toggle : Heading 1": { "Toggle : Heading 1": {
"invoke": "toggle_h1", "invoke": "toggle_h1",

View File

@ -6,24 +6,31 @@ import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts";
import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts"; import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts";
import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts"; import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts";
type NuggetMeta = {
name: string;
lastModified: number;
};
const fsPrefix = "/fs"; const fsPrefix = "/fs";
const nuggetsPath = "../nuggets"; const nuggetsPath = "../nuggets";
const fsRouter = new Router(); const fsRouter = new Router();
fsRouter.use(oakCors()); fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] }));
fsRouter.get("/", async (context) => { fsRouter.get("/", async (context) => {
const localPath = nuggetsPath; const localPath = nuggetsPath;
let fileNames: string[] = []; let fileNames: NuggetMeta[] = [];
for await (const dirEntry of Deno.readDir(localPath)) { for await (const dirEntry of Deno.readDir(localPath)) {
if (dirEntry.isFile) { if (dirEntry.isFile) {
fileNames.push( const stat = await Deno.stat(`${localPath}/${dirEntry.name}`);
dirEntry.name.substring( fileNames.push({
name: dirEntry.name.substring(
0, 0,
dirEntry.name.length - path.extname(dirEntry.name).length dirEntry.name.length - path.extname(dirEntry.name).length
) ),
); lastModified: stat.mtime?.getTime()!,
});
} }
} }
context.response.body = JSON.stringify(fileNames); context.response.body = JSON.stringify(fileNames);
@ -33,7 +40,9 @@ fsRouter.get("/:nugget", async (context) => {
const nuggetName = context.params.nugget; const nuggetName = context.params.nugget;
const localPath = `${nuggetsPath}/${nuggetName}.md`; const localPath = `${nuggetsPath}/${nuggetName}.md`;
try { try {
const stat = await Deno.stat(localPath);
const text = await Deno.readTextFile(localPath); const text = await Deno.readTextFile(localPath);
context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime());
context.response.body = text; context.response.body = text;
} catch (e) { } catch (e) {
context.response.status = 404; context.response.status = 404;
@ -46,7 +55,9 @@ fsRouter.options("/:nugget", async (context) => {
try { try {
const stat = await Deno.stat(localPath); const stat = await Deno.stat(localPath);
context.response.headers.set("Content-length", `${stat.size}`); context.response.headers.set("Content-length", `${stat.size}`);
context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime());
} catch (e) { } catch (e) {
// For CORS
context.response.status = 200; context.response.status = 200;
context.response.body = ""; context.response.body = "";
} }
@ -69,8 +80,10 @@ fsRouter.put("/:nugget", async (context) => {
const text = await readAll(result.value); const text = await readAll(result.value);
file.write(text); file.write(text);
file.close(); file.close();
const stat = await Deno.stat(localPath);
console.log("Wrote to", localPath); console.log("Wrote to", localPath);
context.response.status = existingNugget ? 200 : 201; context.response.status = existingNugget ? 200 : 201;
context.response.headers.set("Last-Modified", "" + stat.mtime?.getTime());
context.response.body = "OK"; context.response.body = "OK";
}); });

15
webapp/src/boot.ts Normal file
View File

@ -0,0 +1,15 @@
import { Editor } from "./editor";
import { HttpFileSystem } from "./fs";
import { safeRun } from "./util";
let editor = new Editor(
new HttpFileSystem(`http://${location.hostname}:2222/fs`),
document.getElementById("root")!
);
safeRun(async () => {
await editor.init();
});
// @ts-ignore
window.editor = editor;

View File

@ -1,4 +1,5 @@
import { AppCommand } from "../types"; import { AppCommand } from "../types";
import { isMacLike } from "../util";
import { FilterList, Option } from "./filter"; import { FilterList, Option } from "./filter";
export function CommandPalette({ export function CommandPalette({
@ -9,8 +10,12 @@ export function CommandPalette({
onTrigger: (command: AppCommand | undefined) => void; onTrigger: (command: AppCommand | undefined) => void;
}) { }) {
let options: Option[] = []; let options: Option[] = [];
const isMac = isMacLike();
for (let [name, def] of commands.entries()) { for (let [name, def] of commands.entries()) {
options.push({ name: name }); options.push({
name: name,
hint: isMac && def.command.mac ? def.command.mac : def.command.key,
});
} }
console.log("Commands", options); console.log("Commands", options);
return ( return (

View File

@ -2,26 +2,28 @@ import React, { useEffect, useRef, useState } from "react";
export interface Option { export interface Option {
name: string; name: string;
orderId?: number;
hint?: string; hint?: string;
} }
function magicSorter(a: Option, b: Option): number { function magicSorter(a: Option, b: Option): number {
if (a.name.toLowerCase() < b.name.toLowerCase()) { if (a.orderId && b.orderId) {
return -1; return a.orderId < b.orderId ? -1 : 1;
} else {
return 1;
} }
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
} }
export function FilterList({ export function FilterList({
placeholder, placeholder,
options, options,
onSelect, onSelect,
onKeyPress,
allowNew = false, allowNew = false,
newHint, newHint,
}: { }: {
placeholder: string; placeholder: string;
options: Option[]; options: Option[];
onKeyPress?: (key: string, currentText: string) => void;
onSelect: (option: Option | undefined) => void; onSelect: (option: Option | undefined) => void;
allowNew?: boolean; allowNew?: boolean;
newHint?: string; newHint?: string;
@ -75,7 +77,6 @@ export function FilterList({
document.addEventListener("click", closer); document.addEventListener("click", closer);
return () => { return () => {
console.log("Unsubscribing");
document.removeEventListener("click", closer); document.removeEventListener("click", closer);
}; };
}, []); }, []);
@ -90,6 +91,9 @@ export function FilterList({
onChange={filter} onChange={filter}
onKeyDown={(e: React.KeyboardEvent) => { onKeyDown={(e: React.KeyboardEvent) => {
console.log("Key up", e.key); console.log("Key up", e.key);
if (onKeyPress) {
onKeyPress(e.key, text);
}
switch (e.key) { switch (e.key) {
case "ArrowUp": case "ArrowUp":
setSelectionOption(Math.max(0, selectedOption - 1)); setSelectionOption(Math.max(0, selectedOption - 1));

View File

@ -1,14 +1,16 @@
import { NuggetMeta } from "../types";
export function NavigationBar({ export function NavigationBar({
currentNugget, currentNugget,
onClick, onClick,
}: { }: {
currentNugget?: string; currentNugget?: NuggetMeta;
onClick: () => void; onClick: () => void;
}) { }) {
return ( return (
<div id="top"> <div id="top">
<div className="current-nugget" onClick={onClick}> <div className="current-nugget" onClick={onClick}>
» {currentNugget} » {currentNugget?.name}
</div> </div>
</div> </div>
); );

View File

@ -11,7 +11,11 @@ export function NuggetNavigator({
return ( return (
<FilterList <FilterList
placeholder="" placeholder=""
options={allNuggets} options={allNuggets.map((meta) => ({
...meta,
// Order by last modified date in descending order
orderId: -meta.lastModified.getTime(),
}))}
allowNew={true} allowNew={true}
newHint="Create nugget" newHint="Create nugget"
onSelect={(opt) => { onSelect={(opt) => {

View File

@ -1,5 +1,6 @@
import { import {
autocompletion, autocompletion,
Completion,
CompletionContext, CompletionContext,
completionKeymap, completionKeymap,
CompletionResult, CompletionResult,
@ -11,12 +12,12 @@ import { indentOnInput, syntaxTree } from "@codemirror/language";
import { bracketMatching } from "@codemirror/matchbrackets"; import { bracketMatching } from "@codemirror/matchbrackets";
import { searchKeymap } from "@codemirror/search"; import { searchKeymap } from "@codemirror/search";
import { EditorState, StateField, Transaction } from "@codemirror/state"; import { EditorState, StateField, Transaction } from "@codemirror/state";
import { KeyBinding } from "@codemirror/view";
import { import {
drawSelection, drawSelection,
dropCursor, dropCursor,
EditorView, EditorView,
highlightSpecialChars, highlightSpecialChars,
KeyBinding,
keymap, keymap,
} from "@codemirror/view"; } from "@codemirror/view";
import React, { useEffect, useReducer } from "react"; import React, { useEffect, useReducer } from "react";
@ -28,12 +29,12 @@ import { CommandPalette } from "./components/command_palette";
import { NavigationBar } from "./components/navigation_bar"; import { NavigationBar } from "./components/navigation_bar";
import { NuggetNavigator } from "./components/nugget_navigator"; import { NuggetNavigator } from "./components/nugget_navigator";
import { StatusBar } from "./components/status_bar"; import { StatusBar } from "./components/status_bar";
import { FileSystem, HttpFileSystem } from "./fs"; import { FileSystem } from "./fs";
import { lineWrapper } from "./lineWrapper"; import { lineWrapper } from "./lineWrapper";
import { markdown } from "./markdown"; import { markdown } from "./markdown";
import customMarkDown from "./parser"; import customMarkDown from "./parser";
import { BrowserSystem } from "./plugins/browser_system"; import { BrowserSystem } from "./plugins/browser_system";
import { Manifest } from "./plugins/types"; import { Manifest, slashCommandRegexp } from "./plugins/types";
import reducer from "./reducer"; import reducer from "./reducer";
import customMarkdownStyle from "./style"; import customMarkdownStyle from "./style";
import dbSyscalls from "./syscalls/db.localstorage"; import dbSyscalls from "./syscalls/db.localstorage";
@ -44,19 +45,24 @@ import {
AppViewState, AppViewState,
CommandContext, CommandContext,
initialViewState, initialViewState,
NuggetMeta,
} from "./types"; } from "./types";
import { safeRun } from "./util"; import { safeRun } from "./util";
class NuggetState { class NuggetState {
editorState: EditorState; editorState: EditorState;
scrollTop: number; scrollTop: number;
meta: NuggetMeta;
constructor(editorState: EditorState, scrollTop: number) { constructor(editorState: EditorState, scrollTop: number, meta: NuggetMeta) {
this.editorState = editorState; this.editorState = editorState;
this.scrollTop = scrollTop; this.scrollTop = scrollTop;
this.meta = meta;
} }
} }
const watchInterval = 5000;
export class Editor { export class Editor {
editorView?: EditorView; editorView?: EditorView;
viewState: AppViewState; viewState: AppViewState;
@ -78,6 +84,7 @@ export class Editor {
parent: document.getElementById("editor")!, parent: document.getElementById("editor")!,
}); });
this.addListeners(); this.addListeners();
this.watch();
} }
async init() { async init() {
@ -93,15 +100,15 @@ export class Editor {
await system.bootServiceWorker(); await system.bootServiceWorker();
console.log("Now loading core plugin"); console.log("Now loading core plugin");
let mainCartridge = await system.load("core", coreManifest as Manifest); let mainPlugin = await system.load("core", coreManifest as Manifest);
this.editorCommands = new Map<string, AppCommand>(); this.editorCommands = new Map<string, AppCommand>();
const cmds = mainCartridge.manifest!.commands; const cmds = mainPlugin.manifest!.commands;
for (let name in cmds) { for (let name in cmds) {
let cmd = cmds[name]; let cmd = cmds[name];
this.editorCommands.set(name, { this.editorCommands.set(name, {
command: cmd, command: cmd,
run: async (arg: CommandContext): Promise<any> => { run: async (arg: CommandContext): Promise<any> => {
return await mainCartridge.invoke(cmd.invoke, [arg]); return await mainPlugin.invoke(cmd.invoke, [arg]);
}, },
}); });
} }
@ -111,7 +118,7 @@ export class Editor {
}); });
} }
get currentNugget(): string | undefined { get currentNugget(): NuggetMeta | undefined {
return this.viewState.currentNugget; return this.viewState.currentNugget;
} }
@ -146,7 +153,10 @@ export class Editor {
bracketMatching(), bracketMatching(),
closeBrackets(), closeBrackets(),
autocompletion({ autocompletion({
override: [this.nuggetCompleter.bind(this)], override: [
this.nuggetCompleter.bind(this),
this.commandCompleter.bind(this),
],
}), }),
EditorView.lineWrapping, EditorView.lineWrapping,
lineWrapper([ lineWrapper([
@ -176,15 +186,10 @@ export class Editor {
run: commands.insertMarker("_"), run: commands.insertMarker("_"),
}, },
{ {
key: "Ctrl-s", key: "Ctrl-e",
mac: "Cmd-s", mac: "Cmd-e",
run: (target: EditorView): boolean => { run: (): boolean => {
Promise.resolve() window.open(location.href, "_blank")!.focus();
.then(async () => {
console.log("Saving");
await this.save();
})
.catch((e) => console.error(e));
return true; return true;
}, },
}, },
@ -200,7 +205,9 @@ export class Editor {
key: "Ctrl-.", key: "Ctrl-.",
mac: "Cmd-.", mac: "Cmd-.",
run: (target): boolean => { run: (target): boolean => {
this.viewDispatch({ type: "show-palette" }); this.viewDispatch({
type: "show-palette",
});
return true; return true;
}, },
}, },
@ -220,12 +227,10 @@ export class Editor {
} }
nuggetCompleter(ctx: CompletionContext): CompletionResult | null { nuggetCompleter(ctx: CompletionContext): CompletionResult | null {
let prefix = ctx.matchBefore(/\[\[\w*/); let prefix = ctx.matchBefore(/\[\[[\w\s]*/);
if (!prefix) { if (!prefix) {
return null; return null;
} }
// TODO: Lots of optimization potential here
// TODO: put something in the cm-completionIcon-nugget style
return { return {
from: prefix.from + 2, from: prefix.from + 2,
options: this.viewState.allNuggets.map((nuggetMeta) => ({ options: this.viewState.allNuggets.map((nuggetMeta) => ({
@ -235,6 +240,39 @@ export class Editor {
}; };
} }
commandCompleter(ctx: CompletionContext): CompletionResult | null {
let prefix = ctx.matchBefore(slashCommandRegexp);
if (!prefix) {
return null;
}
let options: Completion[] = [];
for (let [name, def] of this.viewState.commands) {
if (!def.command.slashCommand) {
continue;
}
options.push({
label: def.command.slashCommand,
detail: name,
apply: () => {
this.editorView?.dispatch({
changes: {
from: prefix!.from,
to: ctx.pos,
insert: "",
},
});
safeRun(async () => {
def.run(buildContext(def, this));
});
},
});
}
return {
from: prefix.from + 1,
options: options,
};
}
update(value: null, transaction: Transaction): null { update(value: null, transaction: Transaction): null {
if (transaction.docChanged) { if (transaction.docChanged) {
this.viewDispatch({ this.viewDispatch({
@ -276,22 +314,26 @@ export class Editor {
return; return;
} }
// Write to file system // Write to file system
const created = await this.fs.writeNugget( let nuggetMeta = await this.fs.writeNugget(
this.currentNugget, this.currentNugget.name,
editorState.sliceDoc() editorState.sliceDoc()
); );
// Update in open nugget cache // Update in open nugget cache
this.openNuggets.set( this.openNuggets.set(
this.currentNugget, this.currentNugget.name,
new NuggetState(editorState, this.editorView!.scrollDOM.scrollTop) new NuggetState(
editorState,
this.editorView!.scrollDOM.scrollTop,
nuggetMeta
)
); );
// Dispatch update to view // Dispatch update to view
this.viewDispatch({ type: "nugget-saved" }); this.viewDispatch({ type: "nugget-saved", meta: nuggetMeta });
// If a new nugget was created, let's refresh the nugget list // If a new nugget was created, let's refresh the nugget list
if (created) { if (nuggetMeta.created) {
await this.loadNuggetList(); await this.loadNuggetList();
} }
} }
@ -304,6 +346,34 @@ export class Editor {
}); });
} }
watch() {
setInterval(() => {
safeRun(async () => {
if (!this.currentNugget) {
return;
}
const currentNuggetName = this.currentNugget.name;
let newNuggetMeta = await this.fs.getMeta(currentNuggetName);
if (
this.currentNugget.lastModified.getTime() <
newNuggetMeta.lastModified.getTime()
) {
console.log("File changed on disk, reloading");
let nuggetData = await this.fs.readNugget(currentNuggetName);
this.openNuggets.set(
newNuggetMeta.name,
new NuggetState(
this.createEditorState(nuggetData.text),
0,
newNuggetMeta
)
);
await this.loadNugget(currentNuggetName);
}
});
}, watchInterval);
}
focus() { focus() {
this.editorView!.focus(); this.editorView!.focus();
} }
@ -323,25 +393,33 @@ export class Editor {
return; return;
} }
let nuggetState = this.openNuggets.get(nuggetName); await this.loadNugget(nuggetName);
if (!nuggetState) {
let text = await this.fs.readNugget(nuggetName);
nuggetState = new NuggetState(this.createEditorState(text), 0);
}
this.openNuggets.set(nuggetName, nuggetState!);
this.editorView!.setState(nuggetState!.editorState);
this.editorView.scrollDOM.scrollTop = nuggetState!.scrollTop;
this.viewDispatch({
type: "nugget-loaded",
name: nuggetName,
});
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
}); });
} }
async loadNugget(nuggetName: string) {
let nuggetState = this.openNuggets.get(nuggetName);
if (!nuggetState) {
let nuggetData = await this.fs.readNugget(nuggetName);
nuggetState = new NuggetState(
this.createEditorState(nuggetData.text),
0,
nuggetData.meta
);
this.openNuggets.set(nuggetName, nuggetState!);
}
this.editorView!.setState(nuggetState!.editorState);
this.editorView!.scrollDOM.scrollTop = nuggetState!.scrollTop;
this.viewDispatch({
type: "nugget-loaded",
meta: nuggetState.meta,
});
}
addListeners() { addListeners() {
this.$hashChange = this.hashChange.bind(this); this.$hashChange = this.hashChange.bind(this);
window.addEventListener("hashchange", this.$hashChange); window.addEventListener("hashchange", this.$hashChange);
@ -380,7 +458,7 @@ export class Editor {
useEffect(() => { useEffect(() => {
if (viewState.currentNugget) { if (viewState.currentNugget) {
document.title = viewState.currentNugget; document.title = viewState.currentNugget.name;
} }
}, [viewState.currentNugget]); }, [viewState.currentNugget]);
@ -437,19 +515,3 @@ export class Editor {
ReactDOM.render(<ViewComponent />, container); ReactDOM.render(<ViewComponent />, container);
} }
} }
let ed = new Editor(
new HttpFileSystem("http://localhost:2222/fs"),
document.getElementById("root")!
);
ed.loadPlugins().catch((e) => {
console.error(e);
});
safeRun(async () => {
await ed.init();
});
// @ts-ignore
window.editor = ed;

View File

@ -2,9 +2,9 @@ import { NuggetMeta } from "./types";
export interface FileSystem { export interface FileSystem {
listNuggets(): Promise<NuggetMeta[]>; listNuggets(): Promise<NuggetMeta[]>;
readNugget(name: string): Promise<string>; readNugget(name: string): Promise<{ text: string; meta: NuggetMeta }>;
// @return whether a new nugget was created for this writeNugget(name: string, text: string): Promise<NuggetMeta>;
writeNugget(name: string, text: string): Promise<boolean>; getMeta(name: string): Promise<NuggetMeta>;
} }
export class HttpFileSystem implements FileSystem { export class HttpFileSystem implements FileSystem {
@ -17,20 +17,43 @@ export class HttpFileSystem implements FileSystem {
method: "GET", method: "GET",
}); });
return (await req.json()).map((name: string) => ({ name })); return (await req.json()).map((meta: any) => ({
name: meta.name,
lastModified: new Date(meta.lastModified),
}));
} }
async readNugget(name: string): Promise<string> { async readNugget(name: string): Promise<{ text: string; meta: NuggetMeta }> {
let req = await fetch(`${this.url}/${name}`, { let req = await fetch(`${this.url}/${name}`, {
method: "GET", method: "GET",
}); });
return await req.text(); return {
text: await req.text(),
meta: {
lastModified: new Date(+req.headers.get("Last-Modified")!),
name: name,
},
};
} }
async writeNugget(name: string, text: string): Promise<boolean> { async writeNugget(name: string, text: string): Promise<NuggetMeta> {
let req = await fetch(`${this.url}/${name}`, { let req = await fetch(`${this.url}/${name}`, {
method: "PUT", method: "PUT",
body: text, body: text,
}); });
// 201 (Created) means a new nugget was created // 201 (Created) means a new nugget was created
return req.status === 201; return {
lastModified: new Date(+req.headers.get("Last-Modified")!),
name: name,
created: req.status === 201,
};
}
async getMeta(name: string): Promise<NuggetMeta> {
let req = await fetch(`${this.url}/${name}`, {
method: "OPTIONS",
});
return {
name: name,
lastModified: new Date(+req.headers.get("Last-Modified")!),
};
} }
} }

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Nugget</title> <title>Nugget</title>
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
<script type="module" src="editor.tsx"></script> <script type="module" src="boot.ts"></script>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
</head> </head>

View File

@ -66,13 +66,13 @@ self.addEventListener("fetch", (event: any) => {
return await handlePut(req, path); return await handlePut(req, path);
} }
let [cartridgeName, resourceType, functionName] = path.split("/"); let [pluginName, resourceType, functionName] = path.split("/");
let manifest = await getManifest(cartridgeName); let manifest = await getManifest(pluginName);
if (!manifest) { if (!manifest) {
// console.log("Ain't got", cartridgeName); // console.log("Ain't got", pluginName);
return new Response(`Cartridge not loaded: ${cartridgeName}`, { return new Response(`Plugin not loaded: ${pluginName}`, {
status: 404, status: 404,
}); });
} }

View File

@ -1,8 +1,8 @@
import { CartridgeLoader, System } from "./runtime"; import { PluginLoader, System } from "./runtime";
import { Manifest } from "./types"; import { Manifest } from "./types";
import { sleep } from "../util"; import { sleep } from "../util";
export class BrowserLoader implements CartridgeLoader { export class BrowserLoader implements PluginLoader {
readonly pathPrefix: string; readonly pathPrefix: string;
constructor(pathPrefix: string) { constructor(pathPrefix: string) {

View File

@ -1,10 +1,10 @@
import { Manifest } from "./types"; import { Manifest } from "./types";
export class SyscallContext { export class SyscallContext {
public cartridge: Cartridge; public plugin: Plugin;
constructor(cartridge: Cartridge) { constructor(Plugin: Plugin) {
this.cartridge = cartridge; this.plugin = Plugin;
} }
} }
@ -19,9 +19,9 @@ export class FunctionWorker {
private initCallback: any; private initCallback: any;
private invokeResolve?: (result?: any) => void; private invokeResolve?: (result?: any) => void;
private invokeReject?: (reason?: any) => void; private invokeReject?: (reason?: any) => void;
private cartridge: Cartridge; private plugin: Plugin;
constructor(cartridge: Cartridge, pathPrefix: string, name: string) { constructor(plugin: Plugin, pathPrefix: string, name: string) {
// this.worker = new Worker(new URL("function_worker.ts", import.meta.url), { // this.worker = new Worker(new URL("function_worker.ts", import.meta.url), {
// type: "classic", // type: "classic",
// }); // });
@ -40,7 +40,7 @@ export class FunctionWorker {
this.inited = new Promise((resolve) => { this.inited = new Promise((resolve) => {
this.initCallback = resolve; this.initCallback = resolve;
}); });
this.cartridge = cartridge; this.plugin = plugin;
} }
async onmessage(evt: MessageEvent) { async onmessage(evt: MessageEvent) {
@ -51,8 +51,8 @@ export class FunctionWorker {
this.initCallback(); this.initCallback();
break; break;
case "syscall": case "syscall":
const ctx = new SyscallContext(this.cartridge); const ctx = new SyscallContext(this.plugin);
let result = await this.cartridge.system.syscall( let result = await this.plugin.system.syscall(
ctx, ctx,
data.name, data.name,
data.args data.args
@ -92,11 +92,11 @@ export class FunctionWorker {
} }
} }
export interface CartridgeLoader { export interface PluginLoader {
load(name: string, manifest: Manifest): Promise<void>; load(name: string, manifest: Manifest): Promise<void>;
} }
export class Cartridge { export class Plugin {
pathPrefix: string; pathPrefix: string;
system: System; system: System;
private runningFunctions: Map<string, FunctionWorker>; private runningFunctions: Map<string, FunctionWorker>;
@ -112,7 +112,7 @@ export class Cartridge {
async load(manifest: Manifest) { async load(manifest: Manifest) {
this.manifest = manifest; this.manifest = manifest;
await this.system.cartridgeLoader.load(this.name, manifest); await this.system.pluginLoader.load(this.name, manifest);
await this.dispatchEvent("load"); await this.dispatchEvent("load");
} }
@ -149,15 +149,15 @@ export class Cartridge {
} }
export class System { export class System {
protected cartridges: Map<string, Cartridge>; protected plugins: Map<string, Plugin>;
protected pathPrefix: string; protected pathPrefix: string;
registeredSyscalls: SysCallMapping; registeredSyscalls: SysCallMapping;
cartridgeLoader: CartridgeLoader; pluginLoader: PluginLoader;
constructor(cartridgeLoader: CartridgeLoader, pathPrefix: string) { constructor(PluginLoader: PluginLoader, pathPrefix: string) {
this.cartridgeLoader = cartridgeLoader; this.pluginLoader = PluginLoader;
this.pathPrefix = pathPrefix; this.pathPrefix = pathPrefix;
this.cartridges = new Map<string, Cartridge>(); this.plugins = new Map<string, Plugin>();
this.registeredSyscalls = {}; this.registeredSyscalls = {};
} }
@ -184,16 +184,16 @@ export class System {
return Promise.resolve(callback(ctx, ...args)); return Promise.resolve(callback(ctx, ...args));
} }
async load(name: string, manifest: Manifest): Promise<Cartridge> { async load(name: string, manifest: Manifest): Promise<Plugin> {
const cartridge = new Cartridge(this, this.pathPrefix, name); const plugin = new Plugin(this, this.pathPrefix, name);
await cartridge.load(manifest); await plugin.load(manifest);
this.cartridges.set(name, cartridge); this.plugins.set(name, plugin);
return cartridge; return plugin;
} }
async stop(): Promise<void[]> { async stop(): Promise<void[]> {
return Promise.all( return Promise.all(
Array.from(this.cartridges.values()).map((cartridge) => cartridge.stop()) Array.from(this.plugins.values()).map((plugin) => plugin.stop())
); );
} }
} }

View File

@ -8,6 +8,8 @@ export interface Manifest {
}; };
} }
export const slashCommandRegexp = /\/[\w\-]*/;
export interface CommandDef { export interface CommandDef {
// Function name to invoke // Function name to invoke
invoke: string; invoke: string;
@ -15,6 +17,11 @@ export interface CommandDef {
// Bind to keyboard shortcut // Bind to keyboard shortcut
key?: string; key?: string;
mac?: string; mac?: string;
// If to show in slash invoked menu and if so, with what label
// should match slashCommandRegexp
slashCommand?: string;
// Required context to be passed in as function arguments // Required context to be passed in as function arguments
requiredContext?: { requiredContext?: {
text?: boolean; text?: boolean;

View File

@ -9,12 +9,13 @@ export default function reducer(
case "nugget-loaded": case "nugget-loaded":
return { return {
...state, ...state,
currentNugget: action.name, currentNugget: action.meta,
isSaved: true, isSaved: true,
}; };
case "nugget-saved": case "nugget-saved":
return { return {
...state, ...state,
currentNugget: action.meta,
isSaved: true, isSaved: true,
}; };
case "nugget-updated": case "nugget-updated":

View File

@ -177,8 +177,8 @@ body {
border: #333 1px solid; border: #333 1px solid;
z-index: 1000; z-index: 1000;
position: absolute; position: absolute;
left: 8px; left: 25px;
top: 8px; top: 10px;
right: 10px; right: 10px;
} }

View File

@ -2,6 +2,6 @@ import { SyscallContext } from "../plugins/runtime";
export default { export default {
"event.publish": async (ctx: SyscallContext, name: string, data: any) => { "event.publish": async (ctx: SyscallContext, name: string, data: any) => {
await ctx.cartridge.dispatchEvent(name, data); await ctx.plugin.dispatchEvent(name, data);
}, },
}; };

View File

@ -8,7 +8,7 @@ window.addEventListener("message", async (event) => {
let data = messageEvent.data; let data = messageEvent.data;
if (data.type === "iframe_event") { if (data.type === "iframe_event") {
// @ts-ignore // @ts-ignore
window.mainCartridge.dispatchEvent(data.data.event, data.data.data); window.mainPlugin.dispatchEvent(data.data.event, data.data.data);
} }
}); });

View File

@ -2,6 +2,8 @@ import { CommandDef } from "./plugins/types";
export type NuggetMeta = { export type NuggetMeta = {
name: string; name: string;
lastModified: Date;
created?: boolean;
}; };
export type CommandContext = { export type CommandContext = {
@ -14,7 +16,7 @@ export type AppCommand = {
}; };
export type AppViewState = { export type AppViewState = {
currentNugget?: string; currentNugget?: NuggetMeta;
isSaved: boolean; isSaved: boolean;
showNuggetNavigator: boolean; showNuggetNavigator: boolean;
showCommandPalette: boolean; showCommandPalette: boolean;
@ -31,8 +33,8 @@ export const initialViewState: AppViewState = {
}; };
export type Action = export type Action =
| { type: "nugget-loaded"; name: string } | { type: "nugget-loaded"; meta: NuggetMeta }
| { type: "nugget-saved" } | { type: "nugget-saved"; meta: NuggetMeta }
| { type: "nugget-updated" } | { type: "nugget-updated" }
| { type: "nuggets-listed"; nuggets: NuggetMeta[] } | { type: "nuggets-listed"; nuggets: NuggetMeta[] }
| { type: "start-navigate" } | { type: "start-navigate" }

View File

@ -21,3 +21,7 @@ export function sleep(ms: number): Promise<void> {
}, ms); }, ms);
}); });
} }
export function isMacLike() {
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform);
}

2
webapp/src/watcher.ts Normal file
View File

@ -0,0 +1,2 @@
import { Editor } from "./editor";
import { safeRun } from "./util";