1
0

Tons of refactoring, moving commands and slash commands into hooks

This commit is contained in:
Zef Hemel 2022-03-29 11:21:32 +02:00
parent bf32d6d0bd
commit b89aee97d7
42 changed files with 330 additions and 224 deletions

View File

@ -1,24 +1,14 @@
import * as plugos from "../plugos/types";
import { EndpointHook } from "../plugos/feature/endpoint";
import { CronHook } from "../plugos/feature/node_cron";
import { EventHook } from "../plugos/feature/event";
import { EndpointHookT } from "../plugos/hooks/endpoint";
import { CronHookT } from "../plugos/hooks/node_cron";
import { EventHookT } from "../plugos/hooks/event";
import { CommandHookT } from "../webapp/hooks/command";
import { SlashCommandHookT } from "../webapp/hooks/slash_command";
export type CommandDef = {
name: string;
// Bind to keyboard shortcut
key?: string;
mac?: string;
// If to show in slash invoked menu and if so, with what label
// should match slashCommandRegexp
slashCommand?: string;
};
export type SilverBulletHooks = {
command?: CommandDef | CommandDef[];
} & EndpointHook &
CronHook &
EventHook;
export type SilverBulletHooks = CommandHookT &
SlashCommandHookT &
EndpointHookT &
CronHookT &
EventHookT;
export type Manifest = plugos.Manifest<SilverBulletHooks>;

View File

@ -14,7 +14,7 @@
"clean": "rm -rf dist",
"plugs": "cd plugs && ../plugos/dist/plugos/plugos-bundle.js -w --dist dist */*.plug.yaml",
"server": "nodemon -w dist/server dist/server/server.js pages",
"test": "jest dist/test"
"test": "jest dist/test plugos/dist/test"
},
"files": [
"dist"

View File

@ -2,29 +2,29 @@
import express from "express";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { DiskPlugLoader } from "../plug_loader";
import { CronHook, NodeCronFeature } from "../feature/node_cron";
import shellSyscalls from "../syscall/shell.node";
import { System } from "../system";
import { EndpointFeature, EndpointHook } from "../feature/endpoint";
import { safeRun } from "../util";
import {hideBin} from "yargs/helpers";
import {DiskPlugLoader} from "../plug_loader";
import {CronHookT, NodeCronHook} from "../hooks/node_cron";
import shellSyscalls from "../syscalls/shell.node";
import {System} from "../system";
import {EndpointHook, EndpointHookT} from "../hooks/endpoint";
import {safeRun} from "../util";
import knex from "knex";
import {
ensureTable,
storeReadSyscalls,
storeWriteSyscalls,
} from "../syscall/store.knex_node";
import { fetchSyscalls } from "../syscall/fetch.node";
import { EventFeature, EventHook } from "../feature/event";
import { eventSyscalls } from "../syscall/event";
} from "../syscalls/store.knex_node";
import {fetchSyscalls} from "../syscalls/fetch.node";
import {EventHook, EventHookT} from "../hooks/event";
import {eventSyscalls} from "../syscalls/event";
let args = yargs(hideBin(process.argv))
.option("port", {
type: "number",
default: 1337,
})
.parse();
.option("port", {
type: "number",
default: 1337,
})
.parse();
if (!args._.length) {
console.error("Usage: plugos-server <path-to-plugs>");
@ -35,7 +35,7 @@ const plugPath = args._[0] as string;
const app = express();
type ServerHook = EndpointHook & CronHook & EventHook;
type ServerHook = EndpointHookT & CronHookT & EventHookT;
const system = new System<ServerHook>("server");
safeRun(async () => {
@ -52,11 +52,11 @@ safeRun(async () => {
let plugLoader = new DiskPlugLoader(system, plugPath);
await plugLoader.loadPlugs();
plugLoader.watcher();
system.addFeature(new NodeCronFeature());
let eventFeature = new EventFeature();
system.addFeature(eventFeature);
system.registerSyscalls("event", [], eventSyscalls(eventFeature));
system.addFeature(new EndpointFeature(app, ""));
system.addHook(new NodeCronHook());
let eventHook = new EventHook();
system.addHook(eventHook);
system.registerSyscalls("event", [], eventSyscalls(eventHook));
system.addHook(new EndpointHook(app, ""));
system.registerSyscalls("shell", [], shellSyscalls("."));
system.registerSyscalls("fetch", [], fetchSyscalls());
system.registerSyscalls(

View File

@ -50,7 +50,6 @@ parentPort.on("message", (data: any) => {
safeRun(async () => {
switch (data.type) {
case "load":
console.log("Booting", data.name);
loadedFunctions.set(data.name, new VMScript(wrapScript(data.code)));
parentPort.postMessage({
type: "inited",

View File

@ -48,7 +48,6 @@ self.addEventListener("message", (event: { data: WorkerMessage }) => {
let data = messageEvent.data;
switch (data.type) {
case "load":
console.log("Booting", data.name);
loadedFunctions.set(data.name!, new Function(wrapScript(data.code!)));
postMessage(
{

View File

@ -1,13 +1,13 @@
import { createSandbox } from "../environment/node_sandbox";
import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals";
import { Manifest } from "../types";
import express from "express";
import request from "supertest";
import { EndpointFeature, EndpointHook } from "./endpoint";
import { EndpointHook, EndpointHookT } from "./endpoint";
import { System } from "../system";
test("Run a plugos endpoint server", async () => {
let system = new System<EndpointHook>("server");
let system = new System<EndpointHookT>("server");
let plug = await system.load(
"test",
{
@ -26,14 +26,14 @@ test("Run a plugos endpoint server", async () => {
})()`,
},
},
} as Manifest<EndpointHook>,
} as Manifest<EndpointHookT>,
createSandbox
);
const app = express();
const port = 3123;
system.addFeature(new EndpointFeature(app, "/_"));
system.addHook(new EndpointHook(app, "/_"));
let server = app.listen(port, () => {
console.log(`Listening on port ${port}`);

View File

@ -1,4 +1,4 @@
import { Feature, Manifest } from "../types";
import { Hook, Manifest } from "../types";
import { Express, NextFunction, Request, Response } from "express";
import { System } from "../system";
@ -16,7 +16,7 @@ export type EndpointResponse = {
body: any;
};
export type EndpointHook = {
export type EndpointHookT = {
http?: EndPointDef | EndPointDef[];
};
@ -25,7 +25,7 @@ export type EndPointDef = {
path: string;
};
export class EndpointFeature implements Feature<EndpointHook> {
export class EndpointHook implements Hook<EndpointHookT> {
private app: Express;
private prefix: string;
@ -34,7 +34,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
this.prefix = prefix;
}
apply(system: System<EndpointHook>): void {
apply(system: System<EndpointHookT>): void {
this.app.use((req: Request, res: Response, next: NextFunction) => {
if (!req.path.startsWith(this.prefix)) {
return next();
@ -106,7 +106,7 @@ export class EndpointFeature implements Feature<EndpointHook> {
});
}
validateManifest(manifest: Manifest<EndpointHook>): string[] {
validateManifest(manifest: Manifest<EndpointHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.http) {

View File

@ -1,19 +1,19 @@
import { Feature, Manifest } from "../types";
import { Hook, Manifest } from "../types";
import { System } from "../system";
// System events:
// - plug:load (plugName: string)
export type EventHook = {
export type EventHookT = {
events?: string[];
};
export class EventFeature implements Feature<EventHook> {
private system?: System<EventHook>;
export class EventHook implements Hook<EventHookT> {
private system?: System<EventHookT>;
async dispatchEvent(eventName: string, data?: any): Promise<any[]> {
if (!this.system) {
throw new Error("EventFeature is not initialized");
throw new Error("Event hook is not initialized");
}
let promises: Promise<any>[] = [];
for (const plug of this.system.loadedPlugs.values()) {
@ -31,7 +31,7 @@ export class EventFeature implements Feature<EventHook> {
return Promise.all(promises);
}
apply(system: System<EventHook>): void {
apply(system: System<EventHookT>): void {
this.system = system;
this.system.on({
plugLoaded: (name) => {
@ -40,7 +40,7 @@ export class EventFeature implements Feature<EventHook> {
});
}
validateManifest(manifest: Manifest<EventHook>): string[] {
validateManifest(manifest: Manifest<EventHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (functionDef.events && !Array.isArray(functionDef.events)) {

View File

@ -1,14 +1,14 @@
import { Feature, Manifest } from "../types";
import { Hook, Manifest } from "../types";
import cron, { ScheduledTask } from "node-cron";
import { safeRun } from "../util";
import { System } from "../system";
export type CronHook = {
export type CronHookT = {
cron?: string | string[];
};
export class NodeCronFeature implements Feature<CronHook> {
apply(system: System<CronHook>): void {
export class NodeCronHook implements Hook<CronHookT> {
apply(system: System<CronHookT>): void {
let tasks: ScheduledTask[] = [];
system.on({
plugLoaded: (name, plug) => {
@ -56,7 +56,7 @@ export class NodeCronFeature implements Feature<CronHook> {
}
}
validateManifest(manifest: Manifest<CronHook>): string[] {
validateManifest(manifest: Manifest<CronHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.cron) {

View File

@ -10,7 +10,7 @@
"watch": "rm -rf .parcel-cache && parcel watch",
"build": "parcel build",
"clean": "rm -rf dist",
"test": "jest dist"
"test": "jest dist/test"
},
"files": [
"dist"
@ -28,9 +28,9 @@
"test": {
"source": [
"runtime.test.ts",
"feature/endpoint.test.ts",
"syscall/store.knex_node.test.ts",
"syscall/store.dexie_browser.test.ts"
"hooks/endpoint.test.ts",
"syscalls/store.knex_node.test.ts",
"syscalls/store.dexie_browser.test.ts"
],
"outputFormat": "commonjs",
"isLibrary": true,

View File

@ -1,7 +1,7 @@
import fs from "fs/promises";
import watch from "node-watch";
import path from "path";
import { createSandbox } from "./environment/node_sandbox";
import { createSandbox } from "./environments/node_sandbox";
import { safeRun } from "../server/util";
import { System } from "./system";

View File

@ -1,4 +1,4 @@
import { createSandbox } from "./environment/node_sandbox";
import { createSandbox } from "./environments/node_sandbox";
import { expect, test } from "@jest/globals";
import { System } from "./system";

View File

@ -2,7 +2,7 @@ import {
ControllerMessage,
WorkerLike,
WorkerMessage,
} from "./environment/worker";
} from "./environments/worker";
import { Plug } from "./plug";
export type SandboxFactory<HookT> = (plug: Plug<HookT>) => Sandbox;

View File

@ -1,10 +0,0 @@
import { SysCallMapping } from "../system";
import { EventFeature } from "../feature/event";
export function eventSyscalls(eventFeature: EventFeature): SysCallMapping {
return {
async dispatch(ctx, eventName: string, data: any) {
return eventFeature.dispatchEvent(eventName, data);
},
};
}

10
plugos/syscalls/event.ts Normal file
View File

@ -0,0 +1,10 @@
import { SysCallMapping } from "../system";
import { EventHook } from "../hooks/event";
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
return {
async dispatch(ctx, eventName: string, data: any) {
return eventHook.dispatchEvent(eventName, data);
},
};
}

View File

@ -1,4 +1,4 @@
import { createSandbox } from "../environment/node_sandbox";
import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals";
import { System } from "../system";
import { storeSyscalls } from "./store.dexie_browser";

View File

@ -1,4 +1,4 @@
import { createSandbox } from "../environment/node_sandbox";
import { createSandbox } from "../environments/node_sandbox";
import { expect, test } from "@jest/globals";
import { System } from "../system";
import {

View File

@ -1,4 +1,4 @@
import { Feature, Manifest, RuntimeEnvironment } from "./types";
import { Hook, Manifest, RuntimeEnvironment } from "./types";
import { EventEmitter } from "../common/event";
import { SandboxFactory } from "./sandbox";
import { Plug } from "./plug";
@ -31,7 +31,7 @@ type Syscall = {
export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
protected plugs = new Map<string, Plug<HookT>>();
protected registeredSyscalls = new Map<string, Syscall>();
protected enabledFeatures = new Set<Feature<HookT>>();
protected enabledHooks = new Set<Hook<HookT>>();
readonly runtimeEnv: RuntimeEnvironment;
@ -40,8 +40,8 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
this.runtimeEnv = env;
}
addFeature(feature: Feature<HookT>) {
this.enabledFeatures.add(feature);
addHook(feature: Hook<HookT>) {
this.enabledHooks.add(feature);
feature.apply(this);
}
@ -91,7 +91,7 @@ export class System<HookT> extends EventEmitter<SystemEvents<HookT>> {
}
// Validate
let errors: string[] = [];
for (const feature of this.enabledFeatures) {
for (const feature of this.enabledHooks) {
errors = [...errors, ...feature.validateManifest(manifest)];
}
if (errors.length > 0) {

View File

@ -1,5 +1,5 @@
{
"include": ["bin/*", "environment/*", "feature/**", "syscall/*", "*"],
"include": ["bin/*", "environments/*", "hooks/**", "syscalls/*", "*"],
"compilerOptions": {
"target": "esnext",
"strict": true,

View File

@ -15,7 +15,7 @@ export type FunctionDef<HookT> = {
export type RuntimeEnvironment = "client" | "server";
export interface Feature<HookT> {
export interface Hook<HookT> {
validateManifest(manifest: Manifest<HookT>): string[];
apply(system: System<HookT>): void;

View File

@ -44,15 +44,10 @@ functions:
- page:click
insertToday:
path: "./dates.ts:insertToday"
command:
name: Insert Current Date
slashCommand: today
slashCommand:
name: today
welcome:
path: "./server.ts:welcome"
events:
- plug:load
env: server
# renderMD:
# path: "./markdown.ts:renderMD"
# command:
# name: Render Markdown

View File

@ -1,7 +1,4 @@
import {
EndpointRequest,
EndpointResponse,
} from "../../plugos/feature/endpoint";
import { EndpointRequest, EndpointResponse } from "../../plugos/hooks/endpoint";
export function endpointTest(req: EndpointRequest): EndpointResponse {
console.log("I'm running on the server!", req);

View File

@ -1,6 +1,6 @@
import { Express } from "express";
import { SilverBulletHooks } from "../common/manifest";
import { EndpointFeature } from "../plugos/feature/endpoint";
import { EndpointHook } from "../plugos/hooks/endpoint";
import { readFile } from "fs/promises";
import { System } from "../plugos/system";
@ -19,7 +19,7 @@ export class ExpressServer {
this.rootPath = rootPath;
this.system = system;
system.addFeature(new EndpointFeature(app, "/_"));
system.addHook(new EndpointHook(app, "/_"));
// Fallback, serve index.html
let cachedIndex: string | undefined = undefined;

View File

@ -11,9 +11,9 @@ import { stat } from "fs/promises";
import { Cursor, cursorEffect } from "../webapp/cursorEffect";
import { SilverBulletHooks } from "../common/manifest";
import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event";
import { EventHook } from "../plugos/hooks/event";
import spaceSyscalls from "./syscalls/space";
import { eventSyscalls } from "../plugos/syscall/event";
import { eventSyscalls } from "../plugos/syscalls/event";
export class PageApi implements ApiProvider {
openPages: Map<string, Page>;
@ -21,7 +21,7 @@ export class PageApi implements ApiProvider {
rootPath: string;
connectedSockets: Set<Socket>;
private system: System<SilverBulletHooks>;
private eventFeature: EventFeature;
private eventHook: EventHook;
constructor(
rootPath: string,
@ -34,10 +34,10 @@ export class PageApi implements ApiProvider {
this.openPages = openPages;
this.connectedSockets = connectedSockets;
this.system = system;
this.eventFeature = new EventFeature();
system.addFeature(this.eventFeature);
this.eventHook = new EventHook();
system.addHook(this.eventHook);
system.registerSyscalls("space", [], spaceSyscalls(this));
system.registerSyscalls("event", [], eventSyscalls(this.eventFeature));
system.registerSyscalls("event", [], eventSyscalls(this.eventHook));
}
async init(): Promise<void> {
@ -229,11 +229,8 @@ export class PageApi implements ApiProvider {
" to disk and indexing."
);
await this.flushPageToDisk(pageName, page);
await this.eventFeature.dispatchEvent(
"page:saved",
pageName
);
await this.eventFeature.dispatchEvent("page:index", {
await this.eventHook.dispatchEvent("page:saved", pageName);
await this.eventHook.dispatchEvent("page:index", {
name: pageName,
text: page.text.sliceString(0),
});
@ -312,8 +309,8 @@ export class PageApi implements ApiProvider {
this.openPages.delete(pageName);
}
// Trigger system events
await this.eventFeature.dispatchEvent("page:saved", pageName);
await this.eventFeature.dispatchEvent("page:index", {
await this.eventHook.dispatchEvent("page:saved", pageName);
await this.eventHook.dispatchEvent("page:index", {
name: pageName,
text: text,
});
@ -325,7 +322,7 @@ export class PageApi implements ApiProvider {
clientConn.openPages.delete(pageName);
// Cascading of this to all connected clients will be handled by file watcher
await this.pageStore.deletePage(pageName);
await this.eventFeature.dispatchEvent("page:deleted", pageName);
await this.eventHook.dispatchEvent("page:deleted", pageName);
},
listPages: async (clientConn: ClientConnection): Promise<PageMeta[]> => {

View File

@ -9,8 +9,8 @@ import {hideBin} from "yargs/helpers";
import {SilverBulletHooks} from "../common/manifest";
import {ExpressServer} from "./express_server";
import {DiskPlugLoader} from "../plugos/plug_loader";
import {NodeCronFeature} from "../plugos/feature/node_cron";
import shellSyscalls from "../plugos/syscall/shell.node";
import {NodeCronHook} from "../plugos/hooks/node_cron";
import shellSyscalls from "../plugos/syscalls/shell.node";
import {System} from "../plugos/system";
let args = yargs(hideBin(process.argv))
@ -21,7 +21,7 @@ let args = yargs(hideBin(process.argv))
.parse();
if (!args._.length) {
console.error("Usage: silverbullet <path-to-pages>");
console.error("Usage: silverbullet <path-to-pages>");
process.exit(1);
}
@ -58,11 +58,11 @@ expressServer
);
await plugLoader.loadPlugs();
plugLoader.watcher();
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
system.addFeature(new NodeCronFeature());
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
system.registerSyscalls("shell", ["shell"], shellSyscalls(pagesPath));
system.addHook(new NodeCronHook());
server.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
})
.catch((e) => {
console.error(e);

View File

@ -5,7 +5,7 @@ import {
ensureTable,
storeReadSyscalls,
storeWriteSyscalls,
} from "../../plugos/syscall/store.knex_node";
} from "../../plugos/syscalls/store.knex_node";
type IndexItem = {
page: string;

View File

@ -1,7 +1,7 @@
import { AppCommand } from "../types";
import { isMacLike } from "../util";
import { FilterList, Option } from "./filter";
import { faPersonRunning } from "@fortawesome/free-solid-svg-icons";
import { AppCommand } from "../hooks/command";
export function CommandPalette({
commands,
@ -18,7 +18,6 @@ export function CommandPalette({
hint: isMac && def.command.mac ? def.command.mac : def.command.key,
});
}
console.log("Commands", options);
return (
<FilterList
label="Run"

View File

@ -1,7 +1,5 @@
import {
autocompletion,
Completion,
CompletionContext,
completionKeymap,
CompletionResult,
} from "@codemirror/autocomplete";
@ -21,7 +19,7 @@ import {
} from "@codemirror/view";
import React, { useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
import { createSandbox as createIFrameSandbox } from "../plugos/environment/iframe_sandbox";
import { createSandbox as createIFrameSandbox } from "../plugos/environments/iframe_sandbox";
import { AppEvent, AppEventDispatcher, ClickEvent } from "./app_event";
import { CollabDocument, collabExtension } from "./collab";
import * as commands from "./commands";
@ -40,19 +38,15 @@ import customMarkdownStyle from "./style";
import editorSyscalls from "./syscalls/editor";
import indexerSyscalls from "./syscalls/indexer";
import spaceSyscalls from "./syscalls/space";
import {
Action,
AppCommand,
AppViewState,
initialViewState,
slashCommandRegexp,
} from "./types";
import { Action, AppViewState, initialViewState } from "./types";
import { SilverBulletHooks } from "../common/manifest";
import { safeRun } from "./util";
import { System } from "../plugos/system";
import { EventFeature } from "../plugos/feature/event";
import { EventHook } from "../plugos/hooks/event";
import { systemSyscalls } from "./syscalls/system";
import { Panel } from "./components/panel";
import { CommandHook } from "./hooks/command";
import { SlashCommandHook } from "./hooks/slash_command";
class PageState {
scrollTop: number;
@ -67,22 +61,39 @@ class PageState {
export class Editor implements AppEventDispatcher {
private system = new System<SilverBulletHooks>("client");
openPages = new Map<string, PageState>();
editorCommands = new Map<string, AppCommand>();
commandHook: CommandHook;
editorView?: EditorView;
viewState: AppViewState;
viewDispatch: React.Dispatch<Action>;
space: Space;
navigationResolve?: (val: undefined) => void;
pageNavigator: PathPageNavigator;
private eventFeature: EventFeature;
eventHook: EventHook;
private slashCommandHook: SlashCommandHook;
constructor(space: Space, parent: Element) {
this.space = space;
this.viewState = initialViewState;
this.viewDispatch = () => {};
this.eventFeature = new EventFeature();
this.system.addFeature(this.eventFeature);
// Event hook
this.eventHook = new EventHook();
this.system.addHook(this.eventHook);
// Command hook
this.commandHook = new CommandHook();
this.commandHook.on({
commandsUpdated: (commandMap) => {
this.viewDispatch({
type: "update-commands",
commands: commandMap,
});
},
});
this.system.addHook(this.commandHook);
// Slash command hook
this.slashCommandHook = new SlashCommandHook(this);
this.system.addHook(this.slashCommandHook);
this.render(parent);
this.editorView = new EditorView({
@ -142,14 +153,12 @@ export class Editor implements AppEventDispatcher {
loadSystem: (systemJSON) => {
safeRun(async () => {
await this.system.replaceAllFromJSON(systemJSON, createIFrameSandbox);
this.buildAllCommands();
});
},
plugLoaded: (plugName, plug) => {
safeRun(async () => {
console.log("Plug load", plugName);
await this.system.load(plugName, plug, createIFrameSandbox);
this.buildAllCommands();
});
},
plugUnloaded: (plugName) => {
@ -161,39 +170,10 @@ export class Editor implements AppEventDispatcher {
});
if (this.pageNavigator.getCurrentPage() === "") {
this.pageNavigator.navigate("start");
await this.pageNavigator.navigate("start");
}
}
private buildAllCommands() {
console.log("Loaded plugs, now updating editor commands");
this.editorCommands.clear();
for (let plug of this.system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.command) {
continue;
}
const cmds = Array.isArray(functionDef.command)
? functionDef.command
: [functionDef.command];
for (let cmd of cmds) {
this.editorCommands.set(cmd.name, {
command: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
}
this.viewDispatch({
type: "update-commands",
commands: this.editorCommands,
});
}
flashNotification(message: string) {
let id = Math.floor(Math.random() * 1000000);
this.viewDispatch({
@ -213,7 +193,7 @@ export class Editor implements AppEventDispatcher {
}
async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
return this.eventFeature.dispatchEvent(name, data);
return this.eventHook.dispatchEvent(name, data);
}
get currentPage(): string | undefined {
@ -222,7 +202,7 @@ export class Editor implements AppEventDispatcher {
createEditorState(pageName: string, doc: CollabDocument): EditorState {
let commandKeyBindings: KeyBinding[] = [];
for (let def of this.editorCommands.values()) {
for (let def of this.commandHook.editorCommands.values()) {
if (def.command.key) {
commandKeyBindings.push({
key: def.command.key,
@ -257,7 +237,9 @@ export class Editor implements AppEventDispatcher {
autocompletion({
override: [
this.plugCompleter.bind(this),
this.commandCompleter.bind(this),
this.slashCommandHook.slashCommandCompleter.bind(
this.slashCommandHook
),
],
}),
EditorView.lineWrapping,
@ -361,39 +343,6 @@ export class Editor implements AppEventDispatcher {
return null;
}
commandCompleter(ctx: CompletionContext): CompletionResult | null {
let prefix = ctx.matchBefore(slashCommandRegexp);
if (!prefix) {
return null;
}
let options: Completion[] = [];
for (let [name, def] of this.viewState.commands) {
if (!def.command.slashCommand) {
continue;
}
options.push({
label: def.command.slashCommand,
detail: name,
apply: () => {
this.editorView?.dispatch({
changes: {
from: prefix!.from,
to: ctx.pos,
insert: "",
},
});
safeRun(async () => {
await def.run();
});
},
});
}
return {
from: prefix.from + 1,
options: options,
};
}
focus() {
this.editorView!.focus();
}
@ -469,7 +418,7 @@ export class Editor implements AppEventDispatcher {
editor.focus();
if (page) {
safeRun(async () => {
editor.navigate(page);
await editor.navigate(page);
});
}
}}
@ -497,7 +446,7 @@ export class Editor implements AppEventDispatcher {
dispatch({ type: "start-navigate" });
}}
/>
<div id="editor"></div>
<div id="editor" />
</div>
);
}

75
webapp/hooks/command.ts Normal file
View File

@ -0,0 +1,75 @@
import { Hook, Manifest } from "../../plugos/types";
import { System } from "../../plugos/system";
import { EventEmitter } from "../../common/event";
export type CommandDef = {
name: string;
// Bind to keyboard shortcut
key?: string;
mac?: string;
};
export type AppCommand = {
command: CommandDef;
run: () => Promise<void>;
};
export type CommandHookT = {
command?: CommandDef;
};
export type CommandHookEvents = {
commandsUpdated(commandMap: Map<string, AppCommand>): void;
};
export class CommandHook
extends EventEmitter<CommandHookEvents>
implements Hook<CommandHookT>
{
editorCommands = new Map<string, AppCommand>();
buildAllCommands(system: System<CommandHookT>) {
this.editorCommands.clear();
for (let plug of system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.command) {
continue;
}
const cmd = functionDef.command;
this.editorCommands.set(cmd.name, {
command: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
this.emit("commandsUpdated", this.editorCommands);
}
apply(system: System<CommandHookT>): void {
this.buildAllCommands(system);
system.on({
plugLoaded: () => {
this.buildAllCommands(system);
},
});
}
validateManifest(manifest: Manifest<CommandHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.command) {
continue;
}
const cmd = functionDef.command;
if (!cmd.name) {
errors.push(`Function ${name} has a command but no name`);
}
}
return [];
}
}

View File

@ -0,0 +1,111 @@
import { Hook, Manifest } from "../../plugos/types";
import { System } from "../../plugos/system";
import {
Completion,
CompletionContext,
CompletionResult,
} from "@codemirror/autocomplete";
import { slashCommandRegexp } from "../types";
import { safeRun } from "../util";
import { Editor } from "../editor";
export type SlashCommandDef = {
name: string;
};
export type AppSlashCommand = {
slashCommand: SlashCommandDef;
run: () => Promise<void>;
};
export type SlashCommandHookT = {
slashCommand?: SlashCommandDef;
};
export class SlashCommandHook implements Hook<SlashCommandHookT> {
slashCommands = new Map<string, AppSlashCommand>();
private editor: Editor;
constructor(editor: Editor) {
this.editor = editor;
}
buildAllCommands(system: System<SlashCommandHookT>) {
this.slashCommands.clear();
for (let plug of system.loadedPlugs.values()) {
for (const [name, functionDef] of Object.entries(
plug.manifest!.functions
)) {
if (!functionDef.slashCommand) {
continue;
}
const cmd = functionDef.slashCommand;
this.slashCommands.set(cmd.name, {
slashCommand: cmd,
run: () => {
return plug.invoke(name, []);
},
});
}
}
}
// Completer for CodeMirror
public slashCommandCompleter(
ctx: CompletionContext
): CompletionResult | null {
let prefix = ctx.matchBefore(slashCommandRegexp);
if (!prefix) {
return null;
}
let options: Completion[] = [];
for (let [name, def] of this.slashCommands.entries()) {
options.push({
label: def.slashCommand.name,
detail: name,
apply: () => {
// Delete slash command part
this.editor.editorView?.dispatch({
changes: {
from: prefix!.from,
to: ctx.pos,
insert: "",
},
});
// Replace with whatever the completion is
safeRun(async () => {
await def.run();
});
},
});
}
return {
// + 1 because of the '/'
from: prefix.from + 1,
options: options,
};
}
apply(system: System<SlashCommandHookT>): void {
this.buildAllCommands(system);
system.on({
plugLoaded: () => {
this.buildAllCommands(system);
},
});
}
validateManifest(manifest: Manifest<SlashCommandHookT>): string[] {
let errors = [];
for (const [name, functionDef] of Object.entries(manifest.functions)) {
if (!functionDef.slashCommand) {
continue;
}
const cmd = functionDef.slashCommand;
if (!cmd.name) {
errors.push(`Function ${name} has a command but no name`);
}
}
return [];
}
}

View File

@ -1,6 +1,6 @@
import { Space } from "../space";
import { SysCallMapping } from "../../plugos/system";
import { transportSyscalls } from "../../plugos/syscall/transport";
import { transportSyscalls } from "../../plugos/syscalls/transport";
export default function indexerSyscalls(space: Space): SysCallMapping {
return transportSyscalls(

View File

@ -1,4 +1,4 @@
import { CommandDef } from "../common/manifest";
import { AppCommand } from "./hooks/command";
export type PageMeta = {
name: string;
@ -7,11 +7,6 @@ export type PageMeta = {
lastOpened?: number;
};
export type AppCommand = {
command: CommandDef;
run: () => Promise<void>;
};
export const slashCommandRegexp = /\/[\w\-]*/;
export type Notification = {