Plugin stuff
parent
16fa05d4cc
commit
bf32d6d0bd
|
@ -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": {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
functions:
|
||||||
|
mdTest:
|
||||||
|
path: "./markdown.ts:renderMarkdown"
|
||||||
|
env: client
|
||||||
|
command:
|
||||||
|
name: "Markdown: Render"
|
|
@ -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>`);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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[]> => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1 +1 @@
|
||||||
export const pageLinkRegex = /\[\[([\w\s\/\:,\.\-]+)\]\]/;
|
export const pageLinkRegex = /\[\[([\w\s\/\:,\.@\-]+)\]\]/;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" };
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue