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
+1 -2
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"
+35 -86
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
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 [];
}
}
+111
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 [];
}
}
+1 -1
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(
+1 -6
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 = {