pull/3/head
Zef Hemel 2022-04-26 19:04:36 +02:00
parent cb2d3f8652
commit 76636dd9b1
41 changed files with 500 additions and 287 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@ -2,7 +2,7 @@ import { SpacePrimitives } from "./space_primitives";
import { EventHook } from "@plugos/plugos/hooks/event";
import { PageMeta } from "../types";
import { Plug } from "@plugos/plugos/plug";
import { trashPrefix } from "./constants";
import { plugPrefix, trashPrefix } from "./constants";
export class EventedSpacePrimitives implements SpacePrimitives {
constructor(private wrapped: SpacePrimitives, private eventHook: EventHook) {}
@ -41,7 +41,7 @@ export class EventedSpacePrimitives implements SpacePrimitives {
lastModified
);
// This can happen async
if (!pageName.startsWith(trashPrefix)) {
if (!pageName.startsWith(trashPrefix) && !pageName.startsWith(plugPrefix)) {
this.eventHook
.dispatchEvent("page:saved", pageName)
.then(() => {

View File

@ -13,8 +13,6 @@ export type SpaceEvents = {
pageChanged: (meta: PageMeta) => void;
pageDeleted: (name: string) => void;
pageListUpdated: (pages: Set<PageMeta>) => void;
plugLoaded: (plugName: string, plug: Manifest) => void;
plugUnloaded: (plugName: string) => void;
};
export class Space extends EventEmitter<SpaceEvents> {
@ -25,70 +23,45 @@ export class Space extends EventEmitter<SpaceEvents> {
constructor(private space: SpacePrimitives, private trashEnabled = true) {
super();
this.on({
pageCreated: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
this.watchPage(pageMeta.name);
}
},
pageChanged: async (pageMeta) => {
if (pageMeta.name.startsWith(plugPrefix)) {
let pageData = await this.readPage(pageMeta.name);
this.emit(
"plugLoaded",
pageMeta.name.substring(plugPrefix.length),
JSON.parse(pageData.text)
);
}
},
});
}
public updatePageListAsync() {
safeRun(async () => {
let newPageList = await this.space.fetchPageList();
let deletedPages = new Set<string>(this.pageMetaCache.keys());
newPageList.pages.forEach((meta) => {
const pageName = meta.name;
const oldPageMeta = this.pageMetaCache.get(pageName);
const newPageMeta = {
name: pageName,
lastModified: meta.lastModified,
};
if (
!oldPageMeta &&
(pageName.startsWith(plugPrefix) || !this.initialPageListLoad)
) {
this.emit("pageCreated", newPageMeta);
} else if (
oldPageMeta &&
oldPageMeta.lastModified !== newPageMeta.lastModified &&
(!this.trashEnabled ||
(this.trashEnabled && !pageName.startsWith(trashPrefix)))
) {
this.emit("pageChanged", newPageMeta);
}
// Page found, not deleted
deletedPages.delete(pageName);
// Update in cache
this.pageMetaCache.set(pageName, newPageMeta);
});
for (const deletedPage of deletedPages) {
this.pageMetaCache.delete(deletedPage);
this.emit("pageDeleted", deletedPage);
public async updatePageList() {
let newPageList = await this.space.fetchPageList();
let deletedPages = new Set<string>(this.pageMetaCache.keys());
newPageList.pages.forEach((meta) => {
const pageName = meta.name;
const oldPageMeta = this.pageMetaCache.get(pageName);
const newPageMeta = {
name: pageName,
lastModified: meta.lastModified,
};
if (
!oldPageMeta &&
(pageName.startsWith(plugPrefix) || !this.initialPageListLoad)
) {
this.emit("pageCreated", newPageMeta);
} else if (
oldPageMeta &&
oldPageMeta.lastModified !== newPageMeta.lastModified &&
(!this.trashEnabled ||
(this.trashEnabled && !pageName.startsWith(trashPrefix)))
) {
this.emit("pageChanged", newPageMeta);
}
// Page found, not deleted
deletedPages.delete(pageName);
this.emit("pageListUpdated", this.listPages());
this.initialPageListLoad = false;
// Update in cache
this.pageMetaCache.set(pageName, newPageMeta);
});
for (const deletedPage of deletedPages) {
this.pageMetaCache.delete(deletedPage);
this.emit("pageDeleted", deletedPage);
}
this.emit("pageListUpdated", this.listPages());
this.initialPageListLoad = false;
}
watch() {
@ -109,7 +82,7 @@ export class Space extends EventEmitter<SpaceEvents> {
}
});
}, pageWatchInterval);
this.updatePageListAsync();
this.updatePageList().catch(console.error);
}
async deletePage(name: string, deleteDate?: number): Promise<void> {
@ -152,14 +125,18 @@ export class Space extends EventEmitter<SpaceEvents> {
return this.space.invokeFunction(plug, env, name, args);
}
listPages(): Set<PageMeta> {
return new Set(
[...this.pageMetaCache.values()].filter(
(pageMeta) =>
!pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
);
listPages(unfiltered = false): Set<PageMeta> {
if (unfiltered) {
return new Set(this.pageMetaCache.values());
} else {
return new Set(
[...this.pageMetaCache.values()].filter(
(pageMeta) =>
!pageMeta.name.startsWith(trashPrefix) &&
!pageMeta.name.startsWith(plugPrefix)
)
);
}
}
listTrash(): Set<PageMeta> {

View File

@ -62,6 +62,14 @@ export function hideLhs(): Promise<void> {
return syscall("editor.hideLhs");
}
export function showBhs(html: string, flex = 1): Promise<void> {
return syscall("editor.showBhs", html, flex);
}
export function hideBhs(): Promise<void> {
return syscall("editor.hideBhs");
}
export function insertAtPos(text: string, pos: number): Promise<void> {
return syscall("editor.insertAtPos", text, pos);
}

View File

@ -1,8 +1,8 @@
import { syscall } from "./syscall";
import { PageMeta } from "../common/types";
export async function listPages(): Promise<PageMeta[]> {
return syscall("space.listPages");
export async function listPages(unfiltered = false): Promise<PageMeta[]> {
return syscall("space.listPages", unfiltered);
}
export async function readPage(

View File

@ -7,3 +7,7 @@ export async function invokeFunction(
): Promise<any> {
return syscall("system.invokeFunction", env, name, ...args);
}
export async function reloadPlugs() {
return syscall("system.reloadPlugs");
}

View File

@ -20,6 +20,10 @@ async function bundle(
(await readFile(manifestPath)).toString()
) as Manifest<any>;
if (!manifest.name) {
throw new Error(`Missing 'name' in ${manifestPath}`);
}
for (let [name, def] of Object.entries(manifest.functions)) {
let jsFunctionName = "default",
filePath = path.join(rootPath, def.path!);

View File

@ -9,8 +9,8 @@ import { System } from "../system";
test("Run a plugos endpoint server", async () => {
let system = new System<EndpointHookT>("server");
let plug = await system.load(
"test",
{
name: "test",
functions: {
testhandler: {
http: {

View File

@ -1,6 +1,7 @@
import { Hook, Manifest } from "../types";
import { System } from "../system";
import { safeRun } from "../util";
import { EventEmitter } from "events";
// System events:
// - plug:load (plugName: string)
@ -11,6 +12,14 @@ export type EventHookT = {
export class EventHook implements Hook<EventHookT> {
private system?: System<EventHookT>;
public localListeners: Map<string, ((data: any) => any)[]> = new Map();
addLocalListener(eventName: string, callback: (data: any) => any) {
if (!this.localListeners.has(eventName)) {
this.localListeners.set(eventName, []);
}
this.localListeners.get(eventName)!.push(callback);
}
async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
if (!this.system) {
@ -32,15 +41,25 @@ export class EventHook implements Hook<EventHookT> {
}
}
}
let localListeners = this.localListeners.get(eventName);
if (localListeners) {
for (let localListener of localListeners) {
let result = await Promise.resolve(localListener(data));
if (result) {
responses.push(result);
}
}
}
return responses;
}
apply(system: System<EventHookT>): void {
this.system = system;
this.system.on({
plugLoaded: (name) => {
plugLoaded: (plug) => {
safeRun(async () => {
await this.dispatchEvent("plug:load", name);
await this.dispatchEvent("plug:load", plug.name);
});
},
});

View File

@ -11,10 +11,10 @@ export class NodeCronHook implements Hook<CronHookT> {
apply(system: System<CronHookT>): void {
let tasks: ScheduledTask[] = [];
system.on({
plugLoaded: (name, plug) => {
plugLoaded: () => {
reloadCrons();
},
plugUnloaded(name, plug) {
plugUnloaded() {
reloadCrons();
},
});

View File

@ -3,11 +3,7 @@ import watch from "node-watch";
import path from "path";
import { createSandbox } from "./environments/node_sandbox";
import { System } from "./system";
function extractPlugName(localPath: string): string {
const baseName = path.basename(localPath);
return baseName.substring(0, baseName.length - ".plug.json".length);
}
import { Manifest } from "./types";
export class DiskPlugLoader<HookT> {
private system: System<HookT>;
@ -27,13 +23,13 @@ export class DiskPlugLoader<HookT> {
.then(async () => {
try {
// let localPath = path.join(this.plugPath, filename);
const plugName = extractPlugName(localPath);
console.log("Change detected for", plugName);
console.log("Change detected for", localPath);
try {
await fs.stat(localPath);
} catch (e) {
// Likely removed
await this.system.unload(plugName);
console.log("Plug removed, TODO: Unload");
return;
}
const plugDef = await this.loadPlugFromFile(localPath);
} catch (e) {
@ -47,12 +43,11 @@ export class DiskPlugLoader<HookT> {
private async loadPlugFromFile(localPath: string) {
const plug = await fs.readFile(localPath, "utf8");
const plugName = extractPlugName(localPath);
console.log("Now loading plug", plugName);
try {
const plugDef = JSON.parse(plug);
await this.system.load(plugName, plugDef, createSandbox);
const plugDef: Manifest<HookT> = JSON.parse(plug);
console.log("Now loading plug", plugDef.name);
await this.system.load(plugDef, createSandbox);
return plugDef;
} catch (e) {
console.error("Could not parse plugin file", e);

View File

@ -23,8 +23,8 @@ test("Run a Node sandbox", async () => {
},
});
let plug = await system.load(
"test",
{
name: "test",
requiredPermissions: ["dangerous"],
functions: {
addTen: {

View File

@ -34,9 +34,7 @@ export function esbuildSyscalls(): SysCallMapping {
}
await writeFile(`${tmpDir}/${filename}`, code);
console.log("Dir", tmpDir);
let jsCode = await compile(`${tmpDir}/${filename}`, "", false, ["yaml"]);
// console.log("JS code", jsCode);
await rm(tmpDir, { recursive: true });
return jsCode;
},

View File

@ -10,8 +10,8 @@ test("Test store", async () => {
let system = new System("server");
system.registerSyscalls([], storeSyscalls("test", "test"));
let plug = await system.load(
"test",
{
name: "test",
functions: {
test1: {
code: `(() => {

View File

@ -17,8 +17,8 @@ test("Test store", async () => {
let system = new System("server");
system.registerSyscalls([], storeSyscalls(db, "test_table"));
let plug = await system.load(
"test",
{
name: "test",
functions: {
test1: {
code: `(() => {

View File

@ -7,11 +7,11 @@ export interface SysCallMapping {
[key: string]: (ctx: SyscallContext, ...args: any) => Promise<any> | any;
}
export type SystemJSON<HookT> = { [key: string]: Manifest<HookT> };
export type SystemJSON<HookT> = Manifest<HookT>[];
export type SystemEvents<HookT> = {
plugLoaded: (name: string, plug: Plug<HookT>) => void;
plugUnloaded: (name: string, plug: Plug<HookT>) => void;
plugLoaded: (plug: Plug<HookT>) => void;
plugUnloaded: (name: string) => void;
};
export type SyscallContext = {
@ -83,10 +83,10 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
}
async load(
name: string,
manifest: Manifest<HookT>,
sandboxFactory: SandboxFactory<HookT>
): Promise<Plug<HookT>> {
const name = manifest.name;
if (this.plugs.has(name)) {
await this.unload(name);
}
@ -100,29 +100,31 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
}
// Ok, let's load this thing!
const plug = new Plug(this, name, sandboxFactory);
console.log("Loading", name);
await plug.load(manifest);
this.plugs.set(name, plug);
this.emit("plugLoaded", name, plug);
this.emit("plugLoaded", plug);
return plug;
}
async unload(name: string) {
console.log("Unloading", name);
const plug = this.plugs.get(name);
if (!plug) {
throw Error(`Plug ${name} not found`);
}
await plug.stop();
this.emit("plugUnloaded", name, plug);
this.emit("plugUnloaded", name);
this.plugs.delete(name);
}
toJSON(): SystemJSON<HookT> {
let plugJSON: { [key: string]: Manifest<HookT> } = {};
let plugJSON: Manifest<HookT>[] = [];
for (let [name, plug] of this.plugs) {
if (!plug.manifest) {
continue;
}
plugJSON[name] = plug.manifest;
plugJSON.push(plug.manifest);
}
return plugJSON;
}
@ -132,9 +134,9 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
sandboxFactory: SandboxFactory<HookT>
) {
await this.unloadAll();
for (let [name, manifest] of Object.entries(json)) {
console.log("Loading plug", name);
await this.load(name, manifest, sandboxFactory);
for (let manifest of json) {
console.log("Loading plug", manifest.name);
await this.load(manifest, sandboxFactory);
}
}

View File

@ -1,6 +1,7 @@
import { System } from "./system";
export interface Manifest<HookT> {
name: string;
requiredPermissions?: string[];
functions: {
[key: string]: FunctionDef<HookT>;

View File

@ -1,3 +1,4 @@
name: core
syntax:
HashTag:
firstCharacters:

View File

@ -1,3 +1,4 @@
name: emoji
functions:
emojiCompleter:
path: "./emoji.ts:emojiCompleter"

View File

@ -1,3 +1,4 @@
name: ghost
functions:
downloadAllPostsCommand:
path: "./ghost.ts:downloadAllPostsCommand"

View File

@ -1,3 +1,4 @@
name: git
requiredPermissions:
- shell
functions:

View File

@ -1,3 +1,4 @@
name: markdown
functions:
toggle:
path: "./markdown.ts:togglePreview"

View File

@ -1,3 +1,4 @@
name: mattermost
functions:
test:
path: mattermost.ts:savedPostsQueryProvider

View File

@ -1,8 +0,0 @@
functions:
compile:
path: "./plugger.ts:compileCommand"
command:
name: "Plugger: Compile"
compileJS:
path: "./plugger.ts:compileJS"
env: server

View File

@ -1,85 +0,0 @@
import type { Manifest } from "@silverbulletmd/common/manifest";
import {
addParentPointers,
collectNodesOfType,
findNodeOfType,
} from "@silverbulletmd/common/tree";
import {
getCurrentPage,
getText,
} from "@silverbulletmd/plugos-silverbullet-syscall/editor";
import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown";
import { writePage } from "@silverbulletmd/plugos-silverbullet-syscall/space";
import { invokeFunction } from "@silverbulletmd/plugos-silverbullet-syscall/system";
import YAML from "yaml";
import { extractMeta } from "../query/data";
export async function compileCommand() {
let text = await getText();
let tree = await parseMarkdown(text);
addParentPointers(tree);
let allHeaders = collectNodesOfType(tree, "ATXHeading2");
let manifest: Manifest = {
functions: {},
};
for (let t of allHeaders) {
let parent = t.parent!;
let headerIdx = parent.children!.indexOf(t);
let headerTitle = t.children![1].text!.trim();
if (!headerTitle.startsWith("function ")) {
continue;
}
let functionName = headerTitle
.substring("function ".length)
.replace(/[^\w]/g, "_");
let meta: any;
let code: string | undefined;
let language = "js";
for (let i = headerIdx + 1; i < parent.children!.length; i++) {
let child = parent.children![i];
if (child.type === "FencedCode") {
let codeInfo = findNodeOfType(child, "CodeInfo")!.children![0].text!;
let codeText = findNodeOfType(child, "CodeText")!.children![0].text!;
if (codeInfo === "yaml") {
meta = YAML.parse(codeText);
continue;
}
if (codeInfo === "typescript" || codeInfo === "ts") {
language = "ts";
}
code = codeText;
}
if (child.type?.startsWith("ATXHeading")) {
break;
}
}
if (code) {
let compiled = await invokeFunction(
"server",
"compileJS",
`file.${language}`,
code
);
manifest.functions[functionName] = meta;
manifest.functions[functionName].code = compiled;
}
}
let pageMeta = extractMeta(tree);
if (pageMeta.name) {
await writePage(
`_plug/${pageMeta.name}`,
JSON.stringify(manifest, null, 2)
);
console.log("Wrote this plug", manifest);
}
}
export async function compileJS(
filename: string,
code: string
): Promise<string> {
return self.syscall("esbuild.compile", filename, code);
}

View File

@ -0,0 +1,25 @@
name: plugmd
functions:
updatePlugsCommand:
path: ./plugmd.ts:updatePlugsCommand
command:
name: "Plugs: Update"
key: "Ctrl-Shift-p"
mac: "Cmd-Shift-p"
updatePlugs:
path: ./plugmd.ts:updatePlugs
env: server
compile:
path: "./plugmd.ts:compileCommand"
command:
name: "Plug: Compile"
mac: "Cmd-Shift-c"
key: "Ctrl-Shift-c"
compileJS:
path: "./plugmd.ts:compileJS"
env: server
getPlugPlugMd:
path: "./plugmd.ts:getPlugPlugMd"
events:
- get-plug:plugmd

View File

@ -0,0 +1,174 @@
import { dispatch } from "@plugos/plugos-syscall/event";
import type { Manifest } from "@silverbulletmd/common/manifest";
import {
addParentPointers,
collectNodesOfType,
findNodeOfType,
} from "@silverbulletmd/common/tree";
import {
getCurrentPage,
getText,
hideBhs,
showBhs,
} from "@silverbulletmd/plugos-silverbullet-syscall/editor";
import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown";
import {
deletePage,
listPages,
readPage,
writePage,
} from "@silverbulletmd/plugos-silverbullet-syscall/space";
import {
invokeFunction,
reloadPlugs,
} from "@silverbulletmd/plugos-silverbullet-syscall/system";
import YAML from "yaml";
import { extractMeta } from "../query/data";
export async function compileCommand() {
let text = await getText();
try {
let manifest = await compileDefinition(text);
await writePage(
`_plug/${manifest.name}`,
JSON.stringify(manifest, null, 2)
);
console.log("Wrote this plug", manifest);
await hideBhs();
// Important not to await!
reloadPlugs();
} catch (e: any) {
await showBhs(e.message);
// console.error("Got this error from compiler", e.message);
}
}
async function compileDefinition(text: string): Promise<Manifest> {
let tree = await parseMarkdown(text);
let pageMeta = extractMeta(tree);
if (!pageMeta.name) {
throw new Error("No 'name' specified in page meta");
}
addParentPointers(tree);
let allHeaders = collectNodesOfType(tree, "ATXHeading2");
let manifest: Manifest = {
name: pageMeta.name,
functions: {},
};
for (let t of allHeaders) {
let parent = t.parent!;
let headerIdx = parent.children!.indexOf(t);
let headerTitle = t.children![1].text!.trim();
if (!headerTitle.startsWith("function ")) {
continue;
}
let functionName = headerTitle
.substring("function ".length)
.replace(/[^\w]/g, "_");
let meta: any;
let code: string | undefined;
let language = "js";
for (let i = headerIdx + 1; i < parent.children!.length; i++) {
let child = parent.children![i];
if (child.type === "FencedCode") {
let codeInfo = findNodeOfType(child, "CodeInfo")!.children![0].text!;
let codeText = findNodeOfType(child, "CodeText")!.children![0].text!;
if (codeInfo === "yaml") {
meta = YAML.parse(codeText);
continue;
}
if (codeInfo === "typescript" || codeInfo === "ts") {
language = "ts";
}
code = codeText;
}
if (child.type?.startsWith("ATXHeading")) {
break;
}
}
if (code) {
let compiled = await invokeFunction(
"server",
"compileJS",
`file.${language}`,
code
);
manifest.functions[functionName] = meta;
manifest.functions[functionName].code = compiled;
}
}
return manifest;
}
export async function compileJS(
filename: string,
code: string
): Promise<string> {
return self.syscall("esbuild.compile", filename, code);
}
async function listPlugs(): Promise<string[]> {
let unfilteredPages = await listPages(true);
return unfilteredPages
.filter((p) => p.name.startsWith("_plug/"))
.map((p) => p.name.substring("_plug/".length));
}
export async function listCommand() {
console.log(await listPlugs());
}
export async function updatePlugsCommand() {
await invokeFunction("server", "updatePlugs");
await reloadPlugs();
}
export async function updatePlugs() {
let { text: plugPageText } = await readPage("PLUGS");
let tree = await parseMarkdown(plugPageText);
let codeTextNode = findNodeOfType(tree, "CodeText");
if (!codeTextNode) {
console.error("Could not find yaml block in PLUGS");
return;
}
let plugYaml = codeTextNode.children![0].text;
let plugList = YAML.parse(plugYaml!);
console.log("Plug YAML", plugList);
let allPlugNames: string[] = [];
for (let plugUri of plugList) {
let [protocol, ...rest] = plugUri.split(":");
let manifests = await dispatch(`get-plug:${protocol}`, rest.join(":"));
if (manifests.length === 0) {
console.error("Could not resolve plug", plugUri);
}
// console.log("Got manifests", plugUri, protocol, manifests);
let manifest = manifests[0];
allPlugNames.push(manifest.name);
// console.log("Writing", `_plug/${manifest.name}`);
await writePage(
`_plug/${manifest.name}`,
JSON.stringify(manifest, null, 2)
);
}
// And delete extra ones
for (let existingPlug of await listPlugs()) {
if (!allPlugNames.includes(existingPlug)) {
console.log("Removing plug", existingPlug);
await deletePage(`_plug/${existingPlug}`);
}
}
// Important not to await!
reloadPlugs();
}
export async function getPlugPlugMd(pageName: string): Promise<Manifest> {
let { text } = await readPage(pageName);
return compileDefinition(text);
}

View File

@ -1,3 +1,4 @@
name: query
functions:
updateMaterializedQueriesOnPage:
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage

View File

@ -1,3 +1,4 @@
name: tasks
syntax:
DeadlineDate:
firstCharacters:

View File

@ -1,5 +1,5 @@
import express, { Express } from "express";
import { SilverBulletHooks } from "@silverbulletmd/common/manifest";
import { Manifest, SilverBulletHooks } from "@silverbulletmd/common/manifest";
import { EndpointHook } from "@plugos/plugos/hooks/endpoint";
import { readFile } from "fs/promises";
import { System } from "@plugos/plugos/system";
@ -24,36 +24,40 @@ import buildMarkdown from "@silverbulletmd/web/parser";
import { loadMarkdownExtensions } from "@silverbulletmd/web/markdown_ext";
import http, { Server } from "http";
import { esbuildSyscalls } from "@plugos/plugos/syscalls/esbuild";
import { systemSyscalls } from "./syscalls/system";
export class ExpressServer {
app: Express;
system: System<SilverBulletHooks>;
private rootPath: string;
private space: Space;
private distDir: string;
private eventHook: EventHook;
private db: Knex<any, unknown[]>;
private port: number;
private server?: Server;
builtinPlugDir: string;
preloadedModules: string[];
constructor(
port: number,
rootPath: string,
pagesPath: string,
distDir: string,
builtinPlugDir: string,
preloadedModules: string[]
) {
this.port = port;
this.app = express();
this.rootPath = rootPath;
this.builtinPlugDir = builtinPlugDir;
this.distDir = distDir;
this.system = new System<SilverBulletHooks>("server");
this.preloadedModules = preloadedModules;
// Setup system
this.eventHook = new EventHook();
this.system.addHook(this.eventHook);
this.space = new Space(
new EventedSpacePrimitives(
new DiskSpacePrimitives(rootPath),
new DiskSpacePrimitives(pagesPath),
this.eventHook
),
true
@ -61,12 +65,12 @@ export class ExpressServer {
this.db = knex({
client: "better-sqlite3",
connection: {
filename: path.join(rootPath, "data.db"),
filename: path.join(pagesPath, "data.db"),
},
useNullAsDefault: true,
});
this.system.registerSyscalls(["shell"], shellSyscalls(rootPath));
this.system.registerSyscalls(["shell"], shellSyscalls(pagesPath));
this.system.addHook(new NodeCronHook());
this.system.registerSyscalls([], pageIndexSyscalls(this.db));
@ -74,6 +78,7 @@ export class ExpressServer {
this.system.registerSyscalls([], eventSyscalls(this.eventHook));
this.system.registerSyscalls([], markdownSyscalls(buildMarkdown([])));
this.system.registerSyscalls([], esbuildSyscalls());
this.system.registerSyscalls([], systemSyscalls(this));
this.system.registerSyscalls([], jwtSyscalls());
this.system.addHook(new EndpointHook(this.app, "/_/"));
@ -81,29 +86,26 @@ export class ExpressServer {
this.rebuildMdExtensions();
}, 100);
this.space.on({
plugLoaded: (plugName, plug) => {
safeRun(async () => {
console.log("Plug load", plugName);
await this.system.load(plugName, plug, (p) =>
createSandbox(p, preloadedModules)
this.eventHook.addLocalListener(
"get-plug:builtin",
async (plugName: string): Promise<Manifest> => {
// console.log("Ok, resovling a plugin", plugName);
try {
let manifestJson = await readFile(
path.join(this.builtinPlugDir, `${plugName}.plug.json`),
"utf8"
);
});
throttledRebuildMdExtensions();
},
plugUnloaded: (plugName) => {
safeRun(async () => {
console.log("Plug unload", plugName);
await this.system.unload(plugName);
});
throttledRebuildMdExtensions();
},
});
return JSON.parse(manifestJson);
} catch (e) {
throw new Error(`No such builtin: ${plugName}`);
}
}
);
setInterval(() => {
this.space.updatePageListAsync();
this.space.updatePageList().catch(console.error);
}, 5000);
this.space.updatePageListAsync();
this.reloadPlugs().catch(console.error);
}
rebuildMdExtensions() {
@ -113,6 +115,19 @@ export class ExpressServer {
);
}
async reloadPlugs() {
await this.space.updatePageList();
await this.system.unloadAll();
console.log("Reloading plugs");
for (let pageInfo of this.space.listPlugs()) {
let { text } = await this.space.readPage(pageInfo.name);
await this.system.load(JSON.parse(text), (p) =>
createSandbox(p, this.preloadedModules)
);
}
this.rebuildMdExtensions();
}
async start() {
await ensurePageIndexTable(this.db);
console.log("Setting up router");

View File

@ -28,11 +28,16 @@ const webappDistDir = realpathSync(
`${nodeModulesDir}/node_modules/@silverbulletmd/web/dist`
);
console.log("Webapp dist dir", webappDistDir);
const plugDistDir = realpathSync(
`${nodeModulesDir}/node_modules/@silverbulletmd/plugs/dist`
);
console.log("Builtin plug dist dir", plugDistDir);
const expressServer = new ExpressServer(
port,
pagesPath,
webappDistDir,
plugDistDir,
preloadModules
);
expressServer.start().catch((e) => {

View File

@ -4,8 +4,8 @@ import { Space } from "@silverbulletmd/common/spaces/space";
export default (space: Space): SysCallMapping => {
return {
"space.listPages": async (ctx): Promise<PageMeta[]> => {
return [...space.listPages()];
"space.listPages": async (ctx, unfiltered = false): Promise<PageMeta[]> => {
return [...space.listPages(unfiltered)];
},
"space.readPage": async (
ctx,

View File

@ -0,0 +1,21 @@
import { SysCallMapping } from "@plugos/plugos/system";
import type { ExpressServer } from "../api_server";
export function systemSyscalls(expressServer: ExpressServer): SysCallMapping {
return {
"system.invokeFunction": async (
ctx,
env: string,
name: string,
...args: any[]
) => {
if (!ctx.plug) {
throw Error("No plug associated with context");
}
return ctx.plug.invoke(name, args);
},
"system.reloadPlugs": async () => {
return expressServer.reloadPlugs();
},
};
}

View File

@ -10,7 +10,7 @@ export function StatusBar({ editorView }: { editorView?: EditorView }) {
readingTime = util.readingTime(wordCount);
}
return (
<div id="bottom">
<div id="status-bar">
<div className="inner">
{wordCount} words | {readingTime} min
</div>

View File

@ -58,13 +58,10 @@ import { FilterOption } from "@silverbulletmd/common/types";
import { syntaxTree } from "@codemirror/language";
class PageState {
scrollTop: number;
selection: EditorSelection;
constructor(scrollTop: number, selection: EditorSelection) {
this.scrollTop = scrollTop;
this.selection = selection;
}
constructor(
readonly scrollTop: number,
readonly selection: EditorSelection
) {}
}
const saveInterval = 1000;
@ -123,7 +120,7 @@ export class Editor {
this.system.registerSyscalls([], editorSyscalls(this));
this.system.registerSyscalls([], spaceSyscalls(this));
this.system.registerSyscalls([], indexerSyscalls(this.space));
this.system.registerSyscalls([], systemSyscalls(this.space));
this.system.registerSyscalls([], systemSyscalls(this));
this.system.registerSyscalls(
[],
markdownSyscalls(buildMarkdown(this.mdExtensions))
@ -153,10 +150,6 @@ export class Editor {
}
});
let throttledRebuildEditorState = throttle(() => {
this.rebuildEditorState();
}, 100);
this.space.on({
pageChanged: (meta) => {
if (this.currentPage === meta.name) {
@ -171,22 +164,10 @@ export class Editor {
pages: pages,
});
},
plugLoaded: (plugName, plug) => {
safeRun(async () => {
console.log("Plug load", plugName);
await this.system.load(plugName, plug, createIFrameSandbox);
throttledRebuildEditorState();
});
},
plugUnloaded: (plugName) => {
safeRun(async () => {
console.log("Plug unload", plugName);
await this.system.unload(plugName);
throttledRebuildEditorState();
});
},
});
await this.reloadPlugs();
if (this.pageNavigator.getCurrentPage() === "") {
await this.pageNavigator.navigate("start");
}
@ -359,7 +340,7 @@ export class Editor {
mac: "Cmd-k",
run: (): boolean => {
this.viewDispatch({ type: "start-navigate" });
this.space.updatePageListAsync();
this.space.updatePageList();
return true;
},
},
@ -427,6 +408,17 @@ export class Editor {
});
}
async reloadPlugs() {
await this.space.updatePageList();
await this.system.unloadAll();
console.log("(Re)loading plugs");
for (let pageInfo of this.space.listPlugs()) {
let { text } = await this.space.readPage(pageInfo.name);
await this.system.load(JSON.parse(text), createIFrameSandbox);
}
this.rebuildEditorState();
}
rebuildEditorState() {
const editorView = this.editorView;
if (editorView && this.currentPage) {
@ -438,13 +430,16 @@ export class Editor {
markdownSyscalls(buildMarkdown(this.mdExtensions))
);
this.saveState();
editorView.setState(
this.createEditorState(this.currentPage, editorView.state.sliceDoc())
);
if (editorView.contentDOM) {
editorView.contentDOM.spellcheck = true;
}
editorView.focus();
this.restoreState(this.currentPage);
}
}
@ -489,12 +484,7 @@ export class Editor {
// Persist current page state and nicely close page
if (this.currentPage) {
let pageState = this.openPages.get(this.currentPage);
if (pageState) {
pageState.selection = this.editorView!.state.selection;
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
// console.log("Saved pageState", this.currentPage, pageState);
}
this.saveState();
this.space.unwatchPage(this.currentPage);
await this.save(true);
}
@ -513,26 +503,11 @@ export class Editor {
}
let editorState = this.createEditorState(pageName, doc.text);
let pageState = this.openPages.get(pageName);
editorView.setState(editorState);
if (editorView.contentDOM) {
editorView.contentDOM.spellcheck = true;
}
if (!pageState) {
pageState = new PageState(0, editorState.selection);
this.openPages.set(pageName, pageState!);
editorView.dispatch({
selection: { anchor: 0 },
});
} else {
// Restore state
// console.log("Restoring selection state", pageState);
editorView.dispatch({
selection: pageState.selection,
});
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
}
this.restoreState(pageName);
this.space.watchPage(pageName);
this.viewDispatch({
@ -543,6 +518,30 @@ export class Editor {
await this.eventHook.dispatchEvent("editor:pageSwitched");
}
private restoreState(pageName: string) {
let pageState = this.openPages.get(pageName);
const editorView = this.editorView!;
if (pageState) {
// Restore state
// console.log("Restoring selection state", pageState);
editorView.dispatch({
selection: pageState.selection,
});
editorView.scrollDOM.scrollTop = pageState!.scrollTop;
}
editorView.focus();
}
private saveState() {
this.openPages.set(
this.currentPage!,
new PageState(
this.editorView!.scrollDOM.scrollTop,
this.editorView!.state.selection
)
);
}
ViewComponent(): React.ReactElement {
const [viewState, dispatch] = useReducer(reducer, initialViewState);
this.viewState = viewState;
@ -625,6 +624,11 @@ export class Editor {
<Panel html={viewState.rhsHTML} flex={viewState.showRHS} />
)}
</div>
{!!viewState.showBHS && (
<div id="bhs">
<Panel html={viewState.bhsHTML} flex={1} />
</div>
)}
<StatusBar editorView={editor.editorView} />
</>
);

View File

@ -102,6 +102,18 @@ export default function reducer(
showLHS: 0,
lhsHTML: "",
};
case "show-bhs":
return {
...state,
showBHS: action.flex,
bhsHTML: action.html,
};
case "hide-bhs":
return {
...state,
showBHS: 0,
bhsHTML: "",
};
case "show-filterbox":
return {
...state,

View File

@ -110,13 +110,26 @@ body {
height: 100%;
}
#bottom {
#bhs {
height: 200px;
width: 100%;
.panel {
iframe {
border: 0;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
}
}
#status-bar {
height: 40px;
line-height: 40px;
padding: 0 10px;
text-align: right;
background-color: rgb(213, 213, 213);
border-top: rgb(193, 193, 193) 1px solid;
.inner {
}
}

View File

@ -92,6 +92,18 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
type: "hide-lhs",
});
},
"editor.showBhs": (ctx, html: string, flex: number) => {
editor.viewDispatch({
type: "show-bhs",
flex,
html,
});
},
"editor.hideBhs": (ctx) => {
editor.viewDispatch({
type: "hide-bhs",
});
},
"editor.insertAtPos": (ctx, text: string, pos: number) => {
editor.editorView!.dispatch({
changes: {

View File

@ -4,8 +4,8 @@ import { PageMeta } from "@silverbulletmd/common/types";
export function spaceSyscalls(editor: Editor): SysCallMapping {
return {
"space.listPages": async (): Promise<PageMeta[]> => {
return [...(await editor.space.listPages())];
"space.listPages": async (ctx, unfiltered = false): Promise<PageMeta[]> => {
return [...(await editor.space.listPages(unfiltered))];
},
"space.readPage": async (
ctx,

View File

@ -1,7 +1,7 @@
import { SysCallMapping } from "@plugos/plugos/system";
import { Space } from "@silverbulletmd/common/spaces/space";
import type { Editor } from "../editor";
export function systemSyscalls(space: Space): SysCallMapping {
export function systemSyscalls(editor: Editor): SysCallMapping {
return {
"system.invokeFunction": async (
ctx,
@ -17,7 +17,10 @@ export function systemSyscalls(space: Space): SysCallMapping {
return ctx.plug.invoke(name, args);
}
return space.invokeFunction(ctx.plug, env, name, args);
return editor.space.invokeFunction(ctx.plug, env, name, args);
},
"system.reloadPlugs": async () => {
return editor.reloadPlugs();
},
};
}

View File

@ -16,8 +16,10 @@ export type AppViewState = {
unsavedChanges: boolean;
showLHS: number; // 0 = hide, > 0 = flex
showRHS: number; // 0 = hide, > 0 = flex
showBHS: number;
rhsHTML: string;
lhsHTML: string;
bhsHTML: string;
allPages: Set<PageMeta>;
commands: Map<string, AppCommand>;
notifications: Notification[];
@ -36,8 +38,10 @@ export const initialViewState: AppViewState = {
unsavedChanges: false,
showLHS: 0,
showRHS: 0,
showBHS: 0,
rhsHTML: "",
lhsHTML: "",
bhsHTML: "",
allPages: new Set(),
commands: new Map(),
notifications: [],
@ -65,6 +69,8 @@ export type Action =
| { type: "hide-rhs" }
| { type: "show-lhs"; html: string; flex: number }
| { type: "hide-lhs" }
| { type: "show-bhs"; html: string; flex: number }
| { type: "hide-bhs" }
| {
type: "show-filterbox";
options: FilterOption[];