File watching and various tweaks
parent
0527430626
commit
2986c2c231
|
@ -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",
|
||||||
|
|
|
@ -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";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -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")!),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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" }
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
import { Editor } from "./editor";
|
||||||
|
import { safeRun } from "./util";
|
Loading…
Reference in New Issue