Plugin stuff

pull/3/head
Zef Hemel 2022-03-28 15:25:05 +02:00
parent 16fa05d4cc
commit bf32d6d0bd
36 changed files with 523 additions and 219 deletions

View File

@ -61,11 +61,13 @@
"body-parser": "^1.19.2", "body-parser": "^1.19.2",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"events": "^3.3.0",
"express": "^4.17.3", "express": "^4.17.3",
"jest": "^27.5.1", "jest": "^27.5.1",
"knex": "^1.0.4", "knex": "^1.0.4",
"node-cron": "^3.0.0", "node-cron": "^3.0.0",
"node-fetch": "2", "node-fetch": "2",
"node-watch": "^0.7.3",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -74,7 +76,6 @@
"supertest": "^6.2.2", "supertest": "^6.2.2",
"vm2": "^3.9.9", "vm2": "^3.9.9",
"yaml": "^1.10.2", "yaml": "^1.10.2",
"events": "^3.3.0",
"yargs": "^17.3.1" "yargs": "^17.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -28,20 +28,19 @@ async function compile(filePath: string, functionName: string, debug: boolean) {
bundle: true, bundle: true,
format: "iife", format: "iife",
globalName: "mod", globalName: "mod",
platform: "neutral", platform: "browser",
sourcemap: false, //sourceMap ? "inline" : false, sourcemap: false, //sourceMap ? "inline" : false,
minify: !debug, minify: !debug,
outfile: outFile, outfile: outFile,
}); });
let jsCode = (await readFile(outFile)).toString(); let jsCode = (await readFile(outFile)).toString();
jsCode = jsCode.replace(/^var mod ?= ?/, "");
await unlink(outFile); await unlink(outFile);
if (inFile !== filePath) { if (inFile !== filePath) {
await unlink(inFile); await unlink(inFile);
} }
// Strip final ';' return `(() => { ${jsCode}
return jsCode.substring(0, jsCode.length - 2); return mod;})()`;
} }
async function bundle(manifestPath: string, sourceMaps: boolean) { async function bundle(manifestPath: string, sourceMaps: boolean) {

View File

@ -72,7 +72,6 @@ parentPort.on("message", (data: any) => {
result: result && JSON.parse(JSON.stringify(result)), result: result && JSON.parse(JSON.stringify(result)),
}); });
} catch (e: any) { } catch (e: any) {
// console.log("ERROR", e);
parentPort.postMessage({ parentPort.postMessage({
type: "result", type: "result",
id: data.id, id: data.id,
@ -94,6 +93,7 @@ parentPort.on("message", (data: any) => {
} }
pendingRequests.delete(syscallId); pendingRequests.delete(syscallId);
if (data.error) { if (data.error) {
console.log("Got rejection", data.error);
lookup.reject(new Error(data.error)); lookup.reject(new Error(data.error));
} else { } else {
lookup.resolve(data.result); lookup.resolve(data.result);

View File

@ -52,14 +52,15 @@
"knex": "^1.0.4", "knex": "^1.0.4",
"node-cron": "^3.0.0", "node-cron": "^3.0.0",
"node-fetch": "2", "node-fetch": "2",
"node-watch": "^0.7.3",
"supertest": "^6.2.2", "supertest": "^6.2.2",
"vm2": "^3.9.9", "vm2": "^3.9.9",
"yaml": "^1.10.2", "yaml": "^1.10.2",
"yargs": "^17.3.1" "yargs": "^17.3.1"
}, },
"devDependencies": { "devDependencies": {
"@parcel/packager-raw-url": "2.3.2",
"@parcel/optimizer-data-url": "2.3.2", "@parcel/optimizer-data-url": "2.3.2",
"@parcel/packager-raw-url": "2.3.2",
"@parcel/service-worker": "2.3.2", "@parcel/service-worker": "2.3.2",
"@parcel/transformer-inline-string": "2.3.2", "@parcel/transformer-inline-string": "2.3.2",
"@parcel/transformer-sass": "2.3.2", "@parcel/transformer-sass": "2.3.2",

View File

@ -1,4 +1,5 @@
import fs, { watch } from "fs/promises"; import fs from "fs/promises";
import watch from "node-watch";
import path from "path"; import path from "path";
import { createSandbox } from "./environment/node_sandbox"; import { createSandbox } from "./environment/node_sandbox";
import { safeRun } from "../server/util"; import { safeRun } from "../server/util";
@ -19,14 +20,15 @@ export class DiskPlugLoader<HookT> {
} }
watcher() { watcher() {
safeRun(async () => { watch(this.plugPath, (eventType, localPath) => {
for await (const { filename, eventType } of watch(this.plugPath)) { if (!localPath.endsWith(".plug.json")) {
if (!filename.endsWith(".plug.json")) { return;
return; }
} safeRun(async () => {
try { try {
let localPath = path.join(this.plugPath, filename); // let localPath = path.join(this.plugPath, filename);
const plugName = extractPlugName(localPath); const plugName = extractPlugName(localPath);
console.log("Change detected for", plugName);
try { try {
await fs.stat(localPath); await fs.stat(localPath);
} catch (e) { } catch (e) {
@ -34,10 +36,11 @@ export class DiskPlugLoader<HookT> {
await this.system.unload(plugName); await this.system.unload(plugName);
} }
const plugDef = await this.loadPlugFromFile(localPath); const plugDef = await this.loadPlugFromFile(localPath);
} catch { } catch (e) {
console.log("Ignoring something FYI", e);
// ignore, error handled by loadPlug // ignore, error handled by loadPlug
} }
} });
}); });
} }

View File

@ -71,6 +71,7 @@ export class Sandbox {
result: result, result: result,
} as WorkerMessage); } as WorkerMessage);
} catch (e: any) { } catch (e: any) {
// console.error("Syscall fail", e);
this.worker.postMessage({ this.worker.postMessage({
type: "syscall-response", type: "syscall-response",
id: data.id, id: data.id,

View File

@ -28,8 +28,8 @@ export function storeWriteSyscalls(
tableName: string tableName: string
): SysCallMapping { ): SysCallMapping {
const apiObj: SysCallMapping = { const apiObj: SysCallMapping = {
delete: async (ctx, page: string, key: string) => { delete: async (ctx, key: string) => {
await db<Item>(tableName).where({ page, key }).del(); await db<Item>(tableName).where({ key }).del();
}, },
deletePrefix: async (ctx, prefix: string) => { deletePrefix: async (ctx, prefix: string) => {
return db<Item>(tableName).andWhereLike("key", `${prefix}%`).del(); return db<Item>(tableName).andWhereLike("key", `${prefix}%`).del();
@ -48,9 +48,15 @@ export function storeWriteSyscalls(
}); });
} }
}, },
// TODO: Optimize
batchSet: async (ctx, kvs: KV[]) => { batchSet: async (ctx, kvs: KV[]) => {
for (let { key, value } of kvs) { for (let { key, value } of kvs) {
await apiObj["store.set"](ctx, key, value); await apiObj.set(ctx, key, value);
}
},
batchDelete: async (ctx, keys: string[]) => {
for (let key of keys) {
await apiObj.delete(ctx, key);
} }
}, },
}; };

View File

@ -4005,6 +4005,11 @@ node-releases@^2.0.2:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
node-watch@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.3.tgz#6d4db88e39c8d09d3ea61d6568d80e5975abc7ab"
integrity sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==
normalize-path@^3.0.0, normalize-path@~3.0.0: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"

View File

@ -1,4 +1,10 @@
functions: functions:
clearPageIndex:
path: "./page.ts:clearPageIndex"
env: server
events:
- page:saved
- page:deleted
indexLinks: indexLinks:
path: "./page.ts:indexLinks" path: "./page.ts:indexLinks"
events: events:
@ -7,6 +13,13 @@ functions:
path: "./page.ts:deletePage" path: "./page.ts:deletePage"
command: command:
name: "Page: Delete" name: "Page: Delete"
reindexSpaceCommand:
path: "./page.ts:reindexCommand"
command:
name: "Space: Reindex"
reindexSpace:
path: "./page.ts:reindexSpace"
env: server
showBackLinks: showBackLinks:
path: "./page.ts:showBackLinks" path: "./page.ts:showBackLinks"
command: command:
@ -29,10 +42,6 @@ functions:
path: "./navigate.ts:clickNavigate" path: "./navigate.ts:clickNavigate"
events: events:
- page:click - page:click
taskToggle:
path: "./task.ts:taskToggle"
events:
- page:click
insertToday: insertToday:
path: "./dates.ts:insertToday" path: "./dates.ts:insertToday"
command: command:
@ -43,4 +52,7 @@ functions:
events: events:
- plug:load - plug:load
env: server env: server
# renderMD:
# path: "./markdown.ts:renderMD"
# command:
# name: Render Markdown

23
plugs/core/markdown.ts Normal file
View File

@ -0,0 +1,23 @@
import { syscall } from "../lib/syscall";
import mdParser from "../../webapp/parser";
export async function renderMD() {
let text = await syscall("editor.getText");
let tree = mdParser.parser.parse(text);
let slicesToRemove: [number, number][] = [];
tree.iterate({
enter(type, from, to): false | void {
switch (type.name) {
case "Comment":
slicesToRemove.push([from, to]);
return false;
}
},
});
console.log("output peices", JSON.stringify(tree));
slicesToRemove.reverse().forEach(([from, to]) => {
text = text.slice(0, from) + text.slice(to);
});
console.log("Clean md", text);
}

View File

@ -8,7 +8,12 @@ async function navigate(syntaxNode: any) {
console.log("Attempting to navigate based on syntax node", syntaxNode); console.log("Attempting to navigate based on syntax node", syntaxNode);
switch (syntaxNode.name) { switch (syntaxNode.name) {
case "WikiLinkPage": case "WikiLinkPage":
await syscall("editor.navigate", syntaxNode.text); let pageLink = syntaxNode.text;
let pos = 0;
if (pageLink.includes("@")) {
[pageLink, pos] = syntaxNode.text.split("@");
}
await syscall("editor.navigate", pageLink, +pos);
break; break;
case "URL": case "URL":
await syscall("editor.openUrl", syntaxNode.text); await syscall("editor.openUrl", syntaxNode.text);

View File

@ -7,9 +7,12 @@ const wikilinkRegex = new RegExp(pageLinkRegex, "g");
export async function indexLinks({ name, text }: IndexEvent) { export async function indexLinks({ name, text }: IndexEvent) {
let backLinks: { key: string; value: string }[] = []; let backLinks: { key: string; value: string }[] = [];
// [[Style Links]] // [[Style Links]]
console.log("Now indexing", name);
for (let match of text.matchAll(wikilinkRegex)) { for (let match of text.matchAll(wikilinkRegex)) {
let toPage = match[1]; let toPage = match[1];
if (toPage.includes("@")) {
toPage = toPage.split("@")[0];
}
let pos = match.index!; let pos = match.index!;
backLinks.push({ backLinks.push({
key: `pl:${toPage}:${pos}`, key: `pl:${toPage}:${pos}`,
@ -17,7 +20,6 @@ export async function indexLinks({ name, text }: IndexEvent) {
}); });
} }
console.log("Found", backLinks.length, "wiki link(s)"); console.log("Found", backLinks.length, "wiki link(s)");
// throw Error("Boom");
await syscall("indexer.batchSet", name, backLinks); await syscall("indexer.batchSet", name, backLinks);
} }
@ -102,6 +104,29 @@ export async function showBackLinks() {
console.log("Backlinks", backLinks); console.log("Backlinks", backLinks);
} }
export async function reindex() { export async function reindexCommand() {
await syscall("space.reindex"); await syscall("editor.flashNotification", "Reindexing...");
await syscall("system.invokeFunctionOnServer", "reindexSpace");
await syscall("editor.flashNotification", "Reindexing done");
}
// Server functions
export async function reindexSpace() {
console.log("Clearing page index...");
await syscall("indexer.clearPageIndex");
console.log("Listing all pages");
let pages = await syscall("space.listPages");
for (let { name } of pages) {
console.log("Indexing", name);
const pageObj = await syscall("space.readPage", name);
await syscall("event.dispatch", "page:index", {
name,
text: pageObj.text,
});
}
}
export async function clearPageIndex(page: string) {
console.log("Clearing page index for page", page);
await syscall("indexer.clearPageIndexForPage", page);
} }

View File

@ -1,31 +0,0 @@
import type { ClickEvent } from "../../webapp/app_event";
import { syscall } from "../lib/syscall";
export async function taskToggle(event: ClickEvent) {
let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos);
if (syntaxNode && syntaxNode.name === "TaskMarker") {
if (syntaxNode.text === "[x]" || syntaxNode.text === "[X]") {
await syscall("editor.dispatch", {
changes: {
from: syntaxNode.from,
to: syntaxNode.to,
insert: "[ ]",
},
selection: {
anchor: event.pos,
},
});
} else {
await syscall("editor.dispatch", {
changes: {
from: syntaxNode.from,
to: syntaxNode.to,
insert: "[x]",
},
selection: {
anchor: event.pos,
},
});
}
}
}

View File

@ -14,8 +14,6 @@ functions:
commit: commit:
path: "./git.ts:commit" path: "./git.ts:commit"
env: server env: server
cron:
- "*/15 * * * *"
sync: sync:
path: "./git.ts:sync" path: "./git.ts:sync"
env: server env: server

View File

@ -0,0 +1,6 @@
functions:
mdTest:
path: "./markdown.ts:renderMarkdown"
env: client
command:
name: "Markdown: Render"

View File

@ -0,0 +1,16 @@
import MarkdownIt from "markdown-it";
import { syscall } from "../lib/syscall";
var taskLists = require("markdown-it-task-lists");
const md = new MarkdownIt({
linkify: true,
html: false,
typographer: true,
}).use(taskLists);
export async function renderMarkdown() {
let text = await syscall("editor.getText");
let html = md.render(text);
await syscall("editor.showRhs", `<html><body>${html}</body></html>`);
}

View File

@ -0,0 +1,12 @@
{
"name": "markdown",
"dependencies": {
"commonmark": "^0.30.0",
"markdown-it": "^12.3.2",
"markdown-it-task-lists": "^2.1.1"
},
"devDependencies": {
"@types/commonmark": "^0.27.5",
"@types/markdown-it": "^12.2.3"
}
}

103
plugs/tasks/task.ts Normal file
View File

@ -0,0 +1,103 @@
import type { ClickEvent } from "../../webapp/app_event";
import { IndexEvent } from "../../webapp/app_event";
import { syscall } from "../lib/syscall";
const allTasksPageName = "ALL TASKS";
const taskRe = /[\-\*]\s*\[([ Xx])\]\s*(.*)/g;
const extractPageLink = /[\-\*]\s*\[[ Xx]\]\s\[\[([^\]]+)@(\d+)\]\]\s*(.*)/;
type Task = { task: string; complete: boolean; pos?: number };
export async function indexTasks({ name, text }: IndexEvent) {
if (name === allTasksPageName) {
return;
}
console.log("Indexing tasks");
let tasks: { key: string; value: Task }[] = [];
for (let match of text.matchAll(taskRe)) {
let complete = match[1] !== " ";
let task = match[2];
let pos = match.index!;
tasks.push({
key: `task:${pos}`,
value: {
task,
complete,
},
});
}
console.log("Found", tasks.length, "task(s)");
await syscall("indexer.batchSet", name, tasks);
}
export async function updateTaskPage() {
let allTasks = await syscall("indexer.scanPrefixGlobal", "task:");
let pageTasks = new Map<string, Task[]>();
for (let {
key,
page,
value: { task, complete, pos },
} of allTasks) {
if (complete) {
continue;
}
let [, pos] = key.split(":");
let tasks = pageTasks.get(page) || [];
tasks.push({ task, complete, pos });
pageTasks.set(page, tasks);
}
let mdPieces = [];
for (let pageName of [...pageTasks.keys()].sort()) {
mdPieces.push(`\n## ${pageName}\n`);
for (let task of pageTasks.get(pageName)!) {
mdPieces.push(
`* [${task.complete ? "x" : " "}] [[${pageName}@${task.pos}]] ${
task.task
}`
);
}
}
let taskMd = mdPieces.join("\n");
await syscall("space.writePage", allTasksPageName, taskMd);
}
export async function taskToggle(event: ClickEvent) {
let syntaxNode = await syscall("editor.getSyntaxNodeAtPos", event.pos);
if (syntaxNode && syntaxNode.name === "TaskMarker") {
let changeTo = "[x]";
if (syntaxNode.text === "[x]" || syntaxNode.text === "[X]") {
changeTo = "[ ]";
}
await syscall("editor.dispatch", {
changes: {
from: syntaxNode.from,
to: syntaxNode.to,
insert: changeTo,
},
selection: {
anchor: event.pos,
},
});
if (event.page === allTasksPageName) {
// Propagate back to the page in question
let line = (await syscall("editor.getLineUnderCursor")) as string;
let match = line.match(extractPageLink);
if (match) {
let [, page, posS] = match;
let pos = +posS;
let pageData = await syscall("space.readPage", page);
let text = pageData.text;
// Apply the toggle
text =
text.substring(0, pos) +
text.substring(pos).replace(/^([\-\*]\s*)\[[ xX]\]/, "$1" + changeTo);
await syscall("space.writePage", page, text);
}
}
}
}

View File

@ -0,0 +1,14 @@
functions:
indexTasks:
path: "./task.ts:indexTasks"
events:
- page:index
updateTaskPage:
path: "./task.ts:updateTaskPage"
command:
name: "Tasks: Update Page"
taskToggle:
path: "./task.ts:taskToggle"
events:
- page:click

View File

@ -4,7 +4,7 @@ import * as path from "path";
import { IndexApi } from "./index_api"; import { IndexApi } from "./index_api";
import { PageApi } from "./page_api"; import { PageApi } from "./page_api";
import { SilverBulletHooks } from "../common/manifest"; import { SilverBulletHooks } from "../common/manifest";
import pageIndexSyscalls from "./syscalls/page_index"; import { pageIndexSyscalls } from "./syscalls/page_index";
import { safeRun } from "./util"; import { safeRun } from "./util";
import { System } from "../plugos/system"; import { System } from "../plugos/system";

View File

@ -1,7 +1,7 @@
import { ApiProvider, ClientConnection } from "./api_server"; import { ApiProvider, ClientConnection } from "./api_server";
import knex, { Knex } from "knex"; import knex, { Knex } from "knex";
import path from "path"; import path from "path";
import pageIndexSyscalls from "./syscalls/page_index"; import { ensurePageIndexTable, pageIndexSyscalls } from "./syscalls/page_index";
type IndexItem = { type IndexItem = {
page: string; page: string;
@ -10,7 +10,7 @@ type IndexItem = {
}; };
export class IndexApi implements ApiProvider { export class IndexApi implements ApiProvider {
db: Knex; db: Knex<any, unknown>;
constructor(rootPath: string) { constructor(rootPath: string) {
this.db = knex({ this.db = knex({
@ -23,15 +23,7 @@ export class IndexApi implements ApiProvider {
} }
async init() { async init() {
if (!(await this.db.schema.hasTable("page_index"))) { await ensurePageIndexTable(this.db);
await this.db.schema.createTable("page_index", (table) => {
table.string("page");
table.string("key");
table.text("value");
table.primary(["page", "key"]);
});
console.log("Created table page_index");
}
} }
api() { api() {
@ -42,6 +34,7 @@ export class IndexApi implements ApiProvider {
clientConn: ClientConnection, clientConn: ClientConnection,
page: string page: string
) => { ) => {
console.log("Now going to clear index for", page);
return syscalls.clearPageIndexForPage(nullContext, page); return syscalls.clearPageIndexForPage(nullContext, page);
}, },
set: async ( set: async (

View File

@ -12,6 +12,8 @@ import { Cursor, cursorEffect } from "../webapp/cursorEffect";
import { SilverBulletHooks } from "../common/manifest"; import { SilverBulletHooks } from "../common/manifest";
import { System } from "../plugos/system"; import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event"; import { EventFeature } from "../plugos/feature/event";
import spaceSyscalls from "./syscalls/space";
import { eventSyscalls } from "../plugos/syscall/event";
export class PageApi implements ApiProvider { export class PageApi implements ApiProvider {
openPages: Map<string, Page>; openPages: Map<string, Page>;
@ -34,6 +36,8 @@ export class PageApi implements ApiProvider {
this.system = system; this.system = system;
this.eventFeature = new EventFeature(); this.eventFeature = new EventFeature();
system.addFeature(this.eventFeature); system.addFeature(this.eventFeature);
system.registerSyscalls("space", [], spaceSyscalls(this));
system.registerSyscalls("event", [], eventSyscalls(this.eventFeature));
} }
async init(): Promise<void> { async init(): Promise<void> {
@ -225,7 +229,10 @@ export class PageApi implements ApiProvider {
" to disk and indexing." " to disk and indexing."
); );
await this.flushPageToDisk(pageName, page); await this.flushPageToDisk(pageName, page);
await this.eventFeature.dispatchEvent(
"page:saved",
pageName
);
await this.eventFeature.dispatchEvent("page:index", { await this.eventFeature.dispatchEvent("page:index", {
name: pageName, name: pageName,
text: page.text.sliceString(0), text: page.text.sliceString(0),
@ -293,21 +300,32 @@ export class PageApi implements ApiProvider {
pageName: string, pageName: string,
text: string text: string
) => { ) => {
// Write to disk
let pageMeta = await this.pageStore.writePage(pageName, text);
// Notify clients that have the page open
let page = this.openPages.get(pageName); let page = this.openPages.get(pageName);
if (page) { if (page) {
for (let client of page.clientStates) { for (let client of page.clientStates) {
client.socket.emit("reloadPage", pageName); client.socket.emit("pageChanged", pageMeta);
} }
this.openPages.delete(pageName); this.openPages.delete(pageName);
} }
return this.pageStore.writePage(pageName, text); // Trigger system events
await this.eventFeature.dispatchEvent("page:saved", pageName);
await this.eventFeature.dispatchEvent("page:index", {
name: pageName,
text: text,
});
return pageMeta;
}, },
deletePage: async (clientConn: ClientConnection, pageName: string) => { deletePage: async (clientConn: ClientConnection, pageName: string) => {
this.openPages.delete(pageName); this.openPages.delete(pageName);
clientConn.openPages.delete(pageName); clientConn.openPages.delete(pageName);
// Cascading of this to all connected clients will be handled by file watcher // Cascading of this to all connected clients will be handled by file watcher
return this.pageStore.deletePage(pageName); await this.pageStore.deletePage(pageName);
await this.eventFeature.dispatchEvent("page:deleted", pageName);
}, },
listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => { listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => {

View File

@ -1,6 +1,12 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SysCallMapping } from "../../plugos/system"; import { SysCallMapping } from "../../plugos/system";
import {
ensureTable,
storeReadSyscalls,
storeWriteSyscalls,
} from "../../plugos/syscall/store.knex_node";
type IndexItem = { type IndexItem = {
page: string; page: string;
key: string; key: string;
@ -12,72 +18,99 @@ export type KV = {
value: any; value: any;
}; };
export default function (db: Knex): SysCallMapping { /*
Keyspace design:
for page lookups:
p~page~key
for global lookups:
k~key~page
*/
function pageKey(page: string, key: string) {
return `p~${page}~${key}`;
}
function unpackPageKey(dbKey: string): { page: string; key: string } {
const [, page, key] = dbKey.split("~");
return { page, key };
}
function globalKey(page: string, key: string) {
return `k~${key}~${page}`;
}
function unpackGlobalKey(dbKey: string): { page: string; key: string } {
const [, key, page] = dbKey.split("~");
return { page, key };
}
export async function ensurePageIndexTable(db: Knex<any, unknown>) {
await ensureTable(db, "page_index");
}
export function pageIndexSyscalls(db: Knex<any, unknown>): SysCallMapping {
const readCalls = storeReadSyscalls(db, "page_index");
const writeCalls = storeWriteSyscalls(db, "page_index");
const apiObj: SysCallMapping = { const apiObj: SysCallMapping = {
clearPageIndexForPage: async (ctx, page: string) => {
await db<IndexItem>("page_index").where({ page }).del();
},
set: async (ctx, page: string, key: string, value: any) => { set: async (ctx, page: string, key: string, value: any) => {
let changed = await db<IndexItem>("page_index") await writeCalls.set(ctx, pageKey(page, key), value);
.where({ page, key }) await writeCalls.set(ctx, globalKey(page, key), value);
.update("value", JSON.stringify(value));
if (changed === 0) {
await db<IndexItem>("page_index").insert({
page,
key,
value: JSON.stringify(value),
});
}
}, },
batchSet: async (ctx, page: string, kvs: KV[]) => { batchSet: async (ctx, page: string, kvs: KV[]) => {
for (let { key, value } of kvs) { for (let { key, value } of kvs) {
await apiObj.set(ctx, page, key, value); await apiObj.set(ctx, page, key, value);
} }
}, },
get: async (ctx, page: string, key: string) => {
let result = await db<IndexItem>("page_index")
.where({ page, key })
.select("value");
if (result.length) {
return JSON.parse(result[0].value);
} else {
return null;
}
},
delete: async (ctx, page: string, key: string) => { delete: async (ctx, page: string, key: string) => {
await db<IndexItem>("page_index").where({ page, key }).del(); await writeCalls.delete(ctx, pageKey(page, key));
await writeCalls.delete(ctx, globalKey(page, key));
},
get: async (ctx, page: string, key: string) => {
return readCalls.get(ctx, pageKey(page, key));
}, },
scanPrefixForPage: async (ctx, page: string, prefix: string) => { scanPrefixForPage: async (ctx, page: string, prefix: string) => {
return ( return (await readCalls.queryPrefix(ctx, pageKey(page, prefix))).map(
await db<IndexItem>("page_index") ({ key, value }: { key: string; value: any }) => {
.where({ page }) const { key: pageKey } = unpackPageKey(key);
.andWhereLike("key", `${prefix}%`) return {
.select("page", "key", "value") page,
).map(({ page, key, value }) => ({ key: pageKey,
page, value,
key, };
value: JSON.parse(value), }
})); );
}, },
scanPrefixGlobal: async (ctx, prefix: string) => { scanPrefixGlobal: async (ctx, prefix: string) => {
return ( return (await readCalls.queryPrefix(ctx, `k~${prefix}`)).map(
await db<IndexItem>("page_index") ({ key, value }: { key: string; value: any }) => {
.andWhereLike("key", `${prefix}%`) const { page, key: pageKey } = unpackGlobalKey(key);
.select("page", "key", "value") return {
).map(({ page, key, value }) => ({ page,
page, key: pageKey,
key, value,
value: JSON.parse(value), };
})); }
);
},
clearPageIndexForPage: async (ctx, page: string) => {
await apiObj.deletePrefixForPage(ctx, page, "");
}, },
deletePrefixForPage: async (ctx, page: string, prefix: string) => { deletePrefixForPage: async (ctx, page: string, prefix: string) => {
return db<IndexItem>("page_index") // Collect all global keys for this page to delete
.where({ page }) let keysToDelete = (
.andWhereLike("key", `${prefix}%`) await readCalls.queryPrefix(ctx, pageKey(page, prefix))
.del(); ).map(({ key }: { key: string; value: string }) =>
globalKey(page, unpackPageKey(key).key)
);
// Delete all page keys
await writeCalls.deletePrefix(ctx, pageKey(page, prefix));
await writeCalls.batchDelete(ctx, keysToDelete);
}, },
clearPageIndex: async () => { clearPageIndex: async (ctx) => {
return db<IndexItem>("page_index").del(); await writeCalls.deleteAll(ctx);
}, },
}; };
return apiObj; return apiObj;

27
server/syscalls/space.ts Normal file
View File

@ -0,0 +1,27 @@
import { PageMeta } from "../types";
import { SysCallMapping } from "../../plugos/system";
import { PageApi } from "../page_api";
import { ClientConnection } from "../api_server";
export default (pageApi: PageApi): SysCallMapping => {
const api = pageApi.api();
// @ts-ignore
const dummyConn = new ClientConnection(null);
return {
listPages: (ctx): Promise<PageMeta[]> => {
return api.listPages(dummyConn);
},
readPage: async (
ctx,
name: string
): Promise<{ text: string; meta: PageMeta }> => {
return api.readPage(dummyConn, name);
},
writePage: async (ctx, name: string, text: string): Promise<PageMeta> => {
return api.writePage(dummyConn, name, text);
},
deletePage: async (ctx, name: string) => {
return api.deletePage(dummyConn, name);
},
};
};

View File

@ -1,11 +1,7 @@
export type AppEvent = export type AppEvent = "page:click" | "editor:complete";
| "app:ready"
| "page:save"
| "page:click"
| "page:index"
| "editor:complete";
export type ClickEvent = { export type ClickEvent = {
page: string;
pos: number; pos: number;
metaKey: boolean; metaKey: boolean;
ctrlKey: boolean; ctrlKey: boolean;

View File

@ -0,0 +1,12 @@
import { useRef } from "react";
export function Panel({ html }: { html: string }) {
const iFrameRef = useRef<HTMLIFrameElement>(null);
// @ts-ignore
window.iframeRef = iFrameRef;
return (
<div className="panel">
<iframe srcDoc={html} ref={iFrameRef} />
</div>
);
}

View File

@ -1 +1 @@
export const pageLinkRegex = /\[\[([\w\s\/\:,\.\-]+)\]\]/; export const pageLinkRegex = /\[\[([\w\s\/\:,\.@\-]+)\]\]/;

View File

@ -31,7 +31,7 @@ import { TopBar } from "./components/top_bar";
import { Cursor } from "./cursorEffect"; import { Cursor } from "./cursorEffect";
import { lineWrapper } from "./line_wrapper"; import { lineWrapper } from "./line_wrapper";
import { markdown } from "./markdown"; import { markdown } from "./markdown";
import { IPageNavigator, PathPageNavigator } from "./navigator"; import { PathPageNavigator } from "./navigator";
import customMarkDown from "./parser"; import customMarkDown from "./parser";
import reducer from "./reducer"; import reducer from "./reducer";
import { smartQuoteKeymap } from "./smart_quotes"; import { smartQuoteKeymap } from "./smart_quotes";
@ -52,6 +52,7 @@ import { safeRun } from "./util";
import { System } from "../plugos/system"; import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event"; import { EventFeature } from "../plugos/feature/event";
import { systemSyscalls } from "./syscalls/system"; import { systemSyscalls } from "./syscalls/system";
import { Panel } from "./components/panel";
class PageState { class PageState {
scrollTop: number; scrollTop: number;
@ -72,7 +73,7 @@ export class Editor implements AppEventDispatcher {
viewDispatch: React.Dispatch<Action>; viewDispatch: React.Dispatch<Action>;
space: Space; space: Space;
navigationResolve?: (val: undefined) => void; navigationResolve?: (val: undefined) => void;
pageNavigator: IPageNavigator; pageNavigator: PathPageNavigator;
private eventFeature: EventFeature; private eventFeature: EventFeature;
constructor(space: Space, parent: Element) { constructor(space: Space, parent: Element) {
@ -102,7 +103,7 @@ export class Editor implements AppEventDispatcher {
async init() { async init() {
this.focus(); this.focus();
this.pageNavigator.subscribe(async (pageName) => { this.pageNavigator.subscribe(async (pageName, pos) => {
console.log("Now navigating to", pageName); console.log("Now navigating to", pageName);
if (!this.editorView) { if (!this.editorView) {
@ -110,6 +111,11 @@ export class Editor implements AppEventDispatcher {
} }
await this.loadPage(pageName); await this.loadPage(pageName);
if (pos) {
this.editorView.dispatch({
selection: { anchor: pos },
});
}
}); });
this.space.on({ this.space.on({
@ -175,8 +181,8 @@ export class Editor implements AppEventDispatcher {
for (let cmd of cmds) { for (let cmd of cmds) {
this.editorCommands.set(cmd.name, { this.editorCommands.set(cmd.name, {
command: cmd, command: cmd,
run: async (arg): Promise<any> => { run: () => {
return await plug.invoke(name, [arg]); return plug.invoke(name, []);
}, },
}); });
} }
@ -223,10 +229,11 @@ export class Editor implements AppEventDispatcher {
mac: def.command.mac, mac: def.command.mac,
run: (): boolean => { run: (): boolean => {
Promise.resolve() Promise.resolve()
.then(async () => { .then(def.run)
await def.run(null); .catch((e: any) => {
}) console.error(e);
.catch((e) => console.error(e)); this.flashNotification(`Error running command: ${e.message}`);
});
return true; return true;
}, },
}); });
@ -317,6 +324,7 @@ export class Editor implements AppEventDispatcher {
click: (event: MouseEvent, view: EditorView) => { click: (event: MouseEvent, view: EditorView) => {
safeRun(async () => { safeRun(async () => {
let clickEvent: ClickEvent = { let clickEvent: ClickEvent = {
page: pageName,
ctrlKey: event.ctrlKey, ctrlKey: event.ctrlKey,
metaKey: event.metaKey, metaKey: event.metaKey,
altKey: event.altKey, altKey: event.altKey,
@ -375,7 +383,7 @@ export class Editor implements AppEventDispatcher {
}, },
}); });
safeRun(async () => { safeRun(async () => {
await def.run(null); await def.run();
}); });
}, },
}); });
@ -390,8 +398,8 @@ export class Editor implements AppEventDispatcher {
this.editorView!.focus(); this.editorView!.focus();
} }
async navigate(name: string) { async navigate(name: string, pos?: number) {
await this.pageNavigator.navigate(name); await this.pageNavigator.navigate(name, pos);
} }
async loadPage(pageName: string) { async loadPage(pageName: string) {
@ -451,7 +459,7 @@ export class Editor implements AppEventDispatcher {
}, [viewState.currentPage]); }, [viewState.currentPage]);
return ( return (
<> <div className={viewState.showRHS ? "rhs-open" : ""}>
{viewState.showPageNavigator && ( {viewState.showPageNavigator && (
<PageNavigator <PageNavigator
allPages={viewState.allPages} allPages={viewState.allPages}
@ -473,15 +481,15 @@ export class Editor implements AppEventDispatcher {
dispatch({ type: "hide-palette" }); dispatch({ type: "hide-palette" });
editor!.focus(); editor!.focus();
if (cmd) { if (cmd) {
safeRun(async () => { cmd.run().catch((e) => {
let result = await cmd.run(null); console.error("Error running command", e);
console.log("Result of command", result);
}); });
} }
}} }}
commands={viewState.commands} commands={viewState.commands}
/> />
)} )}
{viewState.showRHS && <Panel html={viewState.rhsHTML} />}
<TopBar <TopBar
pageName={viewState.currentPage} pageName={viewState.currentPage}
notifications={viewState.notifications} notifications={viewState.notifications}
@ -490,7 +498,7 @@ export class Editor implements AppEventDispatcher {
}} }}
/> />
<div id="editor"></div> <div id="editor"></div>
</> </div>
); );
} }

View File

@ -1,13 +1,5 @@
import { safeRun } from "./util"; import { safeRun } from "./util";
export interface IPageNavigator {
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void;
navigate(page: string): Promise<void>;
getCurrentPage(): string;
}
function encodePageUrl(name: string): string { function encodePageUrl(name: string): string {
return name.replaceAll(" ", "_"); return name.replaceAll(" ", "_");
} }
@ -16,26 +8,34 @@ function decodePageUrl(url: string): string {
return url.replaceAll("_", " "); return url.replaceAll("_", " ");
} }
export class PathPageNavigator implements IPageNavigator { export class PathPageNavigator {
navigationResolve?: (value: undefined) => void; navigationResolve?: () => void;
async navigate(page: string) {
window.history.pushState({ page: page }, page, `/${encodePageUrl(page)}`); async navigate(page: string, pos?: number) {
window.history.pushState(
{ page, pos },
page,
`/${encodePageUrl(page)}${pos ? "@" + pos : ""}`
);
window.dispatchEvent(new PopStateEvent("popstate")); window.dispatchEvent(new PopStateEvent("popstate"));
await new Promise<undefined>((resolve) => { await new Promise<void>((resolve) => {
this.navigationResolve = resolve; this.navigationResolve = resolve;
}); });
this.navigationResolve = undefined; this.navigationResolve = undefined;
} }
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
subscribe(
pageLoadCallback: (pageName: string, pos: number) => Promise<void>
): void {
const cb = () => { const cb = () => {
const gotoPage = this.getCurrentPage(); const gotoPage = this.getCurrentPage();
if (!gotoPage) { if (!gotoPage) {
return; return;
} }
safeRun(async () => { safeRun(async () => {
await pageLoadCallback(this.getCurrentPage()); await pageLoadCallback(this.getCurrentPage(), this.getCurrentPos());
if (this.navigationResolve) { if (this.navigationResolve) {
this.navigationResolve(undefined); this.navigationResolve();
} }
}); });
}; };
@ -44,32 +44,12 @@ export class PathPageNavigator implements IPageNavigator {
} }
getCurrentPage(): string { getCurrentPage(): string {
return decodePageUrl(location.pathname.substring(1)); let [page] = location.pathname.substring(1).split("@");
return decodePageUrl(page);
} }
}
export class HashPageNavigator implements IPageNavigator { getCurrentPos(): number {
navigationResolve?: (value: undefined) => void; let [, pos] = location.pathname.substring(1).split("@");
async navigate(page: string) { return +pos || 0;
location.hash = encodePageUrl(page);
await new Promise<undefined>((resolve) => {
this.navigationResolve = resolve;
});
this.navigationResolve = undefined;
}
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
const cb = () => {
safeRun(async () => {
await pageLoadCallback(this.getCurrentPage());
if (this.navigationResolve) {
this.navigationResolve(undefined);
}
});
};
window.addEventListener("hashchange", cb);
cb();
}
getCurrentPage(): string {
return decodePageUrl(location.hash.substring(1));
} }
} }

View File

@ -58,6 +58,18 @@ export default function reducer(
...state, ...state,
notifications: state.notifications.filter((n) => n.id !== action.id), notifications: state.notifications.filter((n) => n.id !== action.id),
}; };
case "show-rhs":
return {
...state,
showRHS: true,
rhsHTML: action.html,
};
case "hide-rhs":
return {
...state,
showRHS: false,
rhsHTML: "",
};
} }
return state; return state;
} }

View File

@ -85,7 +85,7 @@ export class Space extends EventEmitter<SpaceEvents> {
this.reqId++; this.reqId++;
this.socket!.once(`${eventName}Resp${this.reqId}`, (err, result) => { this.socket!.once(`${eventName}Resp${this.reqId}`, (err, result) => {
if (err) { if (err) {
reject(err); reject(new Error(err));
} else { } else {
resolve(result); resolve(result);
} }

View File

@ -17,6 +17,24 @@ body {
padding: 0; padding: 0;
} }
.panel {
position: absolute;
top: 55px;
bottom: 0;
right: 0;
width: 400px;
z-index: 20;
background: #efefef;
iframe {
border: 0;
width: 100%;
height: 100%;
padding: 10px;
scroll: auto;
}
}
#top { #top {
height: 55px; height: 55px;
position: fixed; position: fixed;
@ -40,6 +58,7 @@ body {
padding: 3px; padding: 3px;
font-size: 14px; font-size: 14px;
} }
.current-page { .current-page {
font-family: var(--ui-font); font-family: var(--ui-font);
font-weight: bold; font-weight: bold;
@ -52,26 +71,6 @@ body {
} }
} }
// #bottom {
// position: fixed;
// bottom: 0;
// left: 0;
// right: 0;
// height: 20px;
// background-color: rgb(232, 232, 232);
// color: rgb(79, 78, 78);
// border-top: rgb(186, 186, 186) 1px solid;
// margin: 0;
// padding: 5px 10px;
// font-family: var(--ui-font);
// font-size: 0.9em;
// text-align: right;
// }
// body.keyboard #bottom {
// bottom: 250px;
// }
#editor { #editor {
position: absolute; position: absolute;
top: 55px; top: 55px;
@ -81,6 +80,10 @@ body {
overflow-y: hidden; overflow-y: hidden;
} }
div.rhs-open #editor {
right: 350px;
}
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
.cm-editor .cm-content { .cm-editor .cm-content {
margin: 0 10px !important; margin: 0 10px !important;

View File

@ -36,8 +36,8 @@ export default (editor: Editor): SysCallMapping => ({
getCursor: (): number => { getCursor: (): number => {
return editor.editorView!.state.selection.main.from; return editor.editorView!.state.selection.main.from;
}, },
navigate: async (ctx, name: string) => { navigate: async (ctx, name: string, pos: number) => {
await editor.navigate(name); await editor.navigate(name, pos);
}, },
openUrl: async (ctx, url: string) => { openUrl: async (ctx, url: string) => {
window.open(url, "_blank")!.focus(); window.open(url, "_blank")!.focus();
@ -45,6 +45,12 @@ export default (editor: Editor): SysCallMapping => ({
flashNotification: (ctx, message: string) => { flashNotification: (ctx, message: string) => {
editor.flashNotification(message); editor.flashNotification(message);
}, },
showRhs: (ctx, html: string) => {
editor.viewDispatch({
type: "show-rhs",
html: html,
});
},
insertAtPos: (ctx, text: string, pos: number) => { insertAtPos: (ctx, text: string, pos: number) => {
editor.editorView!.dispatch({ editor.editorView!.dispatch({
changes: { changes: {
@ -97,6 +103,12 @@ export default (editor: Editor): SysCallMapping => ({
} }
} }
}, },
getLineUnderCursor: (): string => {
const editorState = editor.editorView!.state;
let selection = editorState.selection.main;
let line = editorState.doc.lineAt(selection.from);
return editorState.sliceDoc(line.from, line.to);
},
matchBefore: ( matchBefore: (
ctx, ctx,
regexp: string regexp: string

View File

@ -7,7 +7,7 @@ export function systemSyscalls(space: Space): SysCallMapping {
if (!ctx.plug) { if (!ctx.plug) {
throw Error("No plug associated with context"); throw Error("No plug associated with context");
} }
return await space.wsCall("invokeFunction", ctx.plug.name, name, ...args); return space.wsCall("invokeFunction", ctx.plug.name, name, ...args);
}, },
}; };
} }

View File

@ -9,7 +9,7 @@ export type PageMeta = {
export type AppCommand = { export type AppCommand = {
command: CommandDef; command: CommandDef;
run: (arg: any) => Promise<any>; run: () => Promise<void>;
}; };
export const slashCommandRegexp = /\/[\w\-]*/; export const slashCommandRegexp = /\/[\w\-]*/;
@ -24,6 +24,8 @@ export type AppViewState = {
currentPage?: string; currentPage?: string;
showPageNavigator: boolean; showPageNavigator: boolean;
showCommandPalette: boolean; showCommandPalette: boolean;
showRHS: boolean;
rhsHTML: string;
allPages: Set<PageMeta>; allPages: Set<PageMeta>;
commands: Map<string, AppCommand>; commands: Map<string, AppCommand>;
notifications: Notification[]; notifications: Notification[];
@ -32,6 +34,8 @@ export type AppViewState = {
export const initialViewState: AppViewState = { export const initialViewState: AppViewState = {
showPageNavigator: false, showPageNavigator: false,
showCommandPalette: false, showCommandPalette: false,
showRHS: false,
rhsHTML: "<h1>Loading...</h1>",
allPages: new Set(), allPages: new Set(),
commands: new Map(), commands: new Map(),
notifications: [], notifications: [],
@ -46,4 +50,6 @@ export type Action =
| { type: "show-palette" } | { type: "show-palette" }
| { type: "hide-palette" } | { type: "hide-palette" }
| { type: "show-notification"; notification: Notification } | { type: "show-notification"; notification: Notification }
| { type: "dismiss-notification"; id: number }; | { type: "dismiss-notification"; id: number }
| { type: "show-rhs"; html: string }
| { type: "hide-rhs" };

View File

@ -4487,6 +4487,11 @@ node-releases@^2.0.2:
resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz" resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg== integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
node-watch@^0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.3.tgz#6d4db88e39c8d09d3ea61d6568d80e5975abc7ab"
integrity sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==
nodemon@^2.0.15: nodemon@^2.0.15:
version "2.0.15" version "2.0.15"
resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz" resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz"