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

View File

@ -13,8 +13,6 @@ export type SpaceEvents = {
pageChanged: (meta: PageMeta) => void; pageChanged: (meta: PageMeta) => void;
pageDeleted: (name: string) => void; pageDeleted: (name: string) => void;
pageListUpdated: (pages: Set<PageMeta>) => void; pageListUpdated: (pages: Set<PageMeta>) => void;
plugLoaded: (plugName: string, plug: Manifest) => void;
plugUnloaded: (plugName: string) => void;
}; };
export class Space extends EventEmitter<SpaceEvents> { export class Space extends EventEmitter<SpaceEvents> {
@ -25,33 +23,9 @@ export class Space extends EventEmitter<SpaceEvents> {
constructor(private space: SpacePrimitives, private trashEnabled = true) { constructor(private space: SpacePrimitives, private trashEnabled = true) {
super(); 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() { public async updatePageList() {
safeRun(async () => {
let newPageList = await this.space.fetchPageList(); let newPageList = await this.space.fetchPageList();
let deletedPages = new Set<string>(this.pageMetaCache.keys()); let deletedPages = new Set<string>(this.pageMetaCache.keys());
newPageList.pages.forEach((meta) => { newPageList.pages.forEach((meta) => {
@ -88,7 +62,6 @@ export class Space extends EventEmitter<SpaceEvents> {
this.emit("pageListUpdated", this.listPages()); this.emit("pageListUpdated", this.listPages());
this.initialPageListLoad = false; this.initialPageListLoad = false;
});
} }
watch() { watch() {
@ -109,7 +82,7 @@ export class Space extends EventEmitter<SpaceEvents> {
} }
}); });
}, pageWatchInterval); }, pageWatchInterval);
this.updatePageListAsync(); this.updatePageList().catch(console.error);
} }
async deletePage(name: string, deleteDate?: number): Promise<void> { async deletePage(name: string, deleteDate?: number): Promise<void> {
@ -152,7 +125,10 @@ export class Space extends EventEmitter<SpaceEvents> {
return this.space.invokeFunction(plug, env, name, args); return this.space.invokeFunction(plug, env, name, args);
} }
listPages(): Set<PageMeta> { listPages(unfiltered = false): Set<PageMeta> {
if (unfiltered) {
return new Set(this.pageMetaCache.values());
} else {
return new Set( return new Set(
[...this.pageMetaCache.values()].filter( [...this.pageMetaCache.values()].filter(
(pageMeta) => (pageMeta) =>
@ -161,6 +137,7 @@ export class Space extends EventEmitter<SpaceEvents> {
) )
); );
} }
}
listTrash(): Set<PageMeta> { listTrash(): Set<PageMeta> {
return new Set( return new Set(

View File

@ -62,6 +62,14 @@ export function hideLhs(): Promise<void> {
return syscall("editor.hideLhs"); 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> { export function insertAtPos(text: string, pos: number): Promise<void> {
return syscall("editor.insertAtPos", text, pos); return syscall("editor.insertAtPos", text, pos);
} }

View File

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

View File

@ -7,3 +7,7 @@ export async function invokeFunction(
): Promise<any> { ): Promise<any> {
return syscall("system.invokeFunction", env, name, ...args); 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() (await readFile(manifestPath)).toString()
) as Manifest<any>; ) as Manifest<any>;
if (!manifest.name) {
throw new Error(`Missing 'name' in ${manifestPath}`);
}
for (let [name, def] of Object.entries(manifest.functions)) { for (let [name, def] of Object.entries(manifest.functions)) {
let jsFunctionName = "default", let jsFunctionName = "default",
filePath = path.join(rootPath, def.path!); filePath = path.join(rootPath, def.path!);

View File

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

View File

@ -1,6 +1,7 @@
import { Hook, Manifest } from "../types"; import { Hook, Manifest } from "../types";
import { System } from "../system"; import { System } from "../system";
import { safeRun } from "../util"; import { safeRun } from "../util";
import { EventEmitter } from "events";
// System events: // System events:
// - plug:load (plugName: string) // - plug:load (plugName: string)
@ -11,6 +12,14 @@ export type EventHookT = {
export class EventHook implements Hook<EventHookT> { export class EventHook implements Hook<EventHookT> {
private system?: System<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[]> { async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
if (!this.system) { 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; return responses;
} }
apply(system: System<EventHookT>): void { apply(system: System<EventHookT>): void {
this.system = system; this.system = system;
this.system.on({ this.system.on({
plugLoaded: (name) => { plugLoaded: (plug) => {
safeRun(async () => { 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 { apply(system: System<CronHookT>): void {
let tasks: ScheduledTask[] = []; let tasks: ScheduledTask[] = [];
system.on({ system.on({
plugLoaded: (name, plug) => { plugLoaded: () => {
reloadCrons(); reloadCrons();
}, },
plugUnloaded(name, plug) { plugUnloaded() {
reloadCrons(); reloadCrons();
}, },
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
name: mattermost
functions: functions:
test: test:
path: mattermost.ts:savedPostsQueryProvider 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: functions:
updateMaterializedQueriesOnPage: updateMaterializedQueriesOnPage:
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage path: ./materialized_queries.ts:updateMaterializedQueriesOnPage

View File

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

View File

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

View File

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

View File

@ -4,8 +4,8 @@ import { Space } from "@silverbulletmd/common/spaces/space";
export default (space: Space): SysCallMapping => { export default (space: Space): SysCallMapping => {
return { return {
"space.listPages": async (ctx): Promise<PageMeta[]> => { "space.listPages": async (ctx, unfiltered = false): Promise<PageMeta[]> => {
return [...space.listPages()]; return [...space.listPages(unfiltered)];
}, },
"space.readPage": async ( "space.readPage": async (
ctx, 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); readingTime = util.readingTime(wordCount);
} }
return ( return (
<div id="bottom"> <div id="status-bar">
<div className="inner"> <div className="inner">
{wordCount} words | {readingTime} min {wordCount} words | {readingTime} min
</div> </div>

View File

@ -58,13 +58,10 @@ import { FilterOption } from "@silverbulletmd/common/types";
import { syntaxTree } from "@codemirror/language"; import { syntaxTree } from "@codemirror/language";
class PageState { class PageState {
scrollTop: number; constructor(
selection: EditorSelection; readonly scrollTop: number,
readonly selection: EditorSelection
constructor(scrollTop: number, selection: EditorSelection) { ) {}
this.scrollTop = scrollTop;
this.selection = selection;
}
} }
const saveInterval = 1000; const saveInterval = 1000;
@ -123,7 +120,7 @@ export class Editor {
this.system.registerSyscalls([], editorSyscalls(this)); this.system.registerSyscalls([], editorSyscalls(this));
this.system.registerSyscalls([], spaceSyscalls(this)); this.system.registerSyscalls([], spaceSyscalls(this));
this.system.registerSyscalls([], indexerSyscalls(this.space)); this.system.registerSyscalls([], indexerSyscalls(this.space));
this.system.registerSyscalls([], systemSyscalls(this.space)); this.system.registerSyscalls([], systemSyscalls(this));
this.system.registerSyscalls( this.system.registerSyscalls(
[], [],
markdownSyscalls(buildMarkdown(this.mdExtensions)) markdownSyscalls(buildMarkdown(this.mdExtensions))
@ -153,10 +150,6 @@ export class Editor {
} }
}); });
let throttledRebuildEditorState = throttle(() => {
this.rebuildEditorState();
}, 100);
this.space.on({ this.space.on({
pageChanged: (meta) => { pageChanged: (meta) => {
if (this.currentPage === meta.name) { if (this.currentPage === meta.name) {
@ -171,22 +164,10 @@ export class Editor {
pages: pages, 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() === "") { if (this.pageNavigator.getCurrentPage() === "") {
await this.pageNavigator.navigate("start"); await this.pageNavigator.navigate("start");
} }
@ -359,7 +340,7 @@ export class Editor {
mac: "Cmd-k", mac: "Cmd-k",
run: (): boolean => { run: (): boolean => {
this.viewDispatch({ type: "start-navigate" }); this.viewDispatch({ type: "start-navigate" });
this.space.updatePageListAsync(); this.space.updatePageList();
return true; 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() { rebuildEditorState() {
const editorView = this.editorView; const editorView = this.editorView;
if (editorView && this.currentPage) { if (editorView && this.currentPage) {
@ -438,13 +430,16 @@ export class Editor {
markdownSyscalls(buildMarkdown(this.mdExtensions)) markdownSyscalls(buildMarkdown(this.mdExtensions))
); );
this.saveState();
editorView.setState( editorView.setState(
this.createEditorState(this.currentPage, editorView.state.sliceDoc()) this.createEditorState(this.currentPage, editorView.state.sliceDoc())
); );
if (editorView.contentDOM) { if (editorView.contentDOM) {
editorView.contentDOM.spellcheck = true; 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 // Persist current page state and nicely close page
if (this.currentPage) { if (this.currentPage) {
let pageState = this.openPages.get(this.currentPage); this.saveState();
if (pageState) {
pageState.selection = this.editorView!.state.selection;
pageState.scrollTop = this.editorView!.scrollDOM.scrollTop;
// console.log("Saved pageState", this.currentPage, pageState);
}
this.space.unwatchPage(this.currentPage); this.space.unwatchPage(this.currentPage);
await this.save(true); await this.save(true);
} }
@ -513,26 +503,11 @@ export class Editor {
} }
let editorState = this.createEditorState(pageName, doc.text); let editorState = this.createEditorState(pageName, doc.text);
let pageState = this.openPages.get(pageName);
editorView.setState(editorState); editorView.setState(editorState);
if (editorView.contentDOM) { if (editorView.contentDOM) {
editorView.contentDOM.spellcheck = true; editorView.contentDOM.spellcheck = true;
} }
if (!pageState) { this.restoreState(pageName);
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.space.watchPage(pageName); this.space.watchPage(pageName);
this.viewDispatch({ this.viewDispatch({
@ -543,6 +518,30 @@ export class Editor {
await this.eventHook.dispatchEvent("editor:pageSwitched"); 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 { ViewComponent(): React.ReactElement {
const [viewState, dispatch] = useReducer(reducer, initialViewState); const [viewState, dispatch] = useReducer(reducer, initialViewState);
this.viewState = viewState; this.viewState = viewState;
@ -625,6 +624,11 @@ export class Editor {
<Panel html={viewState.rhsHTML} flex={viewState.showRHS} /> <Panel html={viewState.rhsHTML} flex={viewState.showRHS} />
)} )}
</div> </div>
{!!viewState.showBHS && (
<div id="bhs">
<Panel html={viewState.bhsHTML} flex={1} />
</div>
)}
<StatusBar editorView={editor.editorView} /> <StatusBar editorView={editor.editorView} />
</> </>
); );

View File

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

View File

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

View File

@ -92,6 +92,18 @@ export function editorSyscalls(editor: Editor): SysCallMapping {
type: "hide-lhs", 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.insertAtPos": (ctx, text: string, pos: number) => {
editor.editorView!.dispatch({ editor.editorView!.dispatch({
changes: { changes: {

View File

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

View File

@ -1,7 +1,7 @@
import { SysCallMapping } from "@plugos/plugos/system"; 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 { return {
"system.invokeFunction": async ( "system.invokeFunction": async (
ctx, ctx,
@ -17,7 +17,10 @@ export function systemSyscalls(space: Space): SysCallMapping {
return ctx.plug.invoke(name, args); 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; unsavedChanges: boolean;
showLHS: number; // 0 = hide, > 0 = flex showLHS: number; // 0 = hide, > 0 = flex
showRHS: number; // 0 = hide, > 0 = flex showRHS: number; // 0 = hide, > 0 = flex
showBHS: number;
rhsHTML: string; rhsHTML: string;
lhsHTML: string; lhsHTML: string;
bhsHTML: string;
allPages: Set<PageMeta>; allPages: Set<PageMeta>;
commands: Map<string, AppCommand>; commands: Map<string, AppCommand>;
notifications: Notification[]; notifications: Notification[];
@ -36,8 +38,10 @@ export const initialViewState: AppViewState = {
unsavedChanges: false, unsavedChanges: false,
showLHS: 0, showLHS: 0,
showRHS: 0, showRHS: 0,
showBHS: 0,
rhsHTML: "", rhsHTML: "",
lhsHTML: "", lhsHTML: "",
bhsHTML: "",
allPages: new Set(), allPages: new Set(),
commands: new Map(), commands: new Map(),
notifications: [], notifications: [],
@ -65,6 +69,8 @@ export type Action =
| { type: "hide-rhs" } | { type: "hide-rhs" }
| { type: "show-lhs"; html: string; flex: number } | { type: "show-lhs"; html: string; flex: number }
| { type: "hide-lhs" } | { type: "hide-lhs" }
| { type: "show-bhs"; html: string; flex: number }
| { type: "hide-bhs" }
| { | {
type: "show-filterbox"; type: "show-filterbox";
options: FilterOption[]; options: FilterOption[];