Templates 2.0 (#636)

Templates 2.0 and a whole bunch of other refactoring
This commit is contained in:
Zef Hemel
2024-01-20 19:16:07 +01:00
committed by GitHub
parent 0a6a0016a2
commit f30b1d3418
171 changed files with 2038 additions and 1628 deletions
+41 -14
View File
@@ -1,5 +1,6 @@
// Third party web dependencies
import {
Compartment,
CompletionContext,
CompletionResult,
EditorView,
@@ -52,6 +53,8 @@ import {
markFullSpaceIndexComplete,
} from "../common/space_index.ts";
import { LimitedMap } from "$sb/lib/limited_map.ts";
import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts";
import { buildQueryFunctions } from "../common/query_functions.ts";
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
const autoSaveInterval = 1000;
@@ -71,6 +74,8 @@ declare global {
export class Client {
system!: ClientSystem;
editorView!: EditorView;
keyHandlerCompartment?: Compartment;
private pageNavigator!: PathPageNavigator;
private dbPrefix: string;
@@ -136,7 +141,10 @@ export class Client {
`${this.dbPrefix}_state`,
);
await stateKvPrimitives.init();
this.stateDataStore = new DataStore(stateKvPrimitives);
this.stateDataStore = new DataStore(
stateKvPrimitives,
buildQueryFunctions(this.allKnownPages),
);
// Setup message queue
this.mq = new DataStoreMQ(this.stateDataStore);
@@ -190,8 +198,7 @@ export class Client {
await this.system.init();
// Load settings
this.settings = await ensureSettingsAndIndex(localSpacePrimitives);
await this.loadSettings();
await this.loadCaches();
// Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page
@@ -240,6 +247,10 @@ export class Client {
this.updatePageListCache().catch(console.error);
}
async loadSettings() {
this.settings = await ensureSettingsAndIndex(this.space.spacePrimitives);
}
private async initSync() {
this.syncService.start();
@@ -303,7 +314,7 @@ export class Client {
private initNavigator() {
this.pageNavigator = new PathPageNavigator(
cleanPageRef(this.settings.indexPage),
cleanPageRef(renderHandlebarsTemplate(this.settings.indexPage, {}, {})),
);
this.pageNavigator.subscribe(
@@ -478,7 +489,12 @@ export class Client {
new EventedSpacePrimitives(
// Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet
new FallbackSpacePrimitives(
new DataStoreSpacePrimitives(new DataStore(spaceKvPrimitives)),
new DataStoreSpacePrimitives(
new DataStore(
spaceKvPrimitives,
buildQueryFunctions(this.allKnownPages),
),
),
this.plugSpaceRemotePrimitives,
),
this.eventHook,
@@ -487,7 +503,7 @@ export class Client {
// Run when a list of files has been retrieved
async () => {
if (!this.settings) {
this.settings = await ensureSettingsAndIndex(localSpacePrimitives!);
await this.loadSettings();
}
if (typeof this.settings?.spaceIgnore === "string") {
@@ -547,11 +563,12 @@ export class Client {
"file:listed",
(allFiles: FileMeta[]) => {
// Update list of known pages
this.allKnownPages = new Set(
allFiles.filter((f) => f.name.endsWith(".md")).map((f) =>
f.name.slice(0, -3)
),
);
this.allKnownPages.clear();
allFiles.forEach((f) => {
if (f.name.endsWith(".md")) {
this.allKnownPages.add(f.name.slice(0, -3));
}
});
},
);
@@ -638,9 +655,9 @@ export class Client {
);
}
startPageNavigate() {
startPageNavigate(mode: "page" | "template") {
// Then show the page navigator
this.ui.viewDispatch({ type: "start-navigate" });
this.ui.viewDispatch({ type: "start-navigate", mode });
this.updatePageListCache().catch(console.error);
}
@@ -854,7 +871,9 @@ export class Client {
newWindow = false,
) {
if (!name) {
name = cleanPageRef(this.settings.indexPage);
name = cleanPageRef(
renderHandlebarsTemplate(this.settings.indexPage, {}, {}),
);
}
try {
@@ -903,6 +922,7 @@ export class Client {
if (e.message.includes("Not found")) {
// Not found, new page
console.log("Page doesn't exist, creating new page:", pageName);
// Initialize page
doc = {
text: "",
meta: {
@@ -914,6 +934,13 @@ export class Client {
perm: "rw",
} as PageMeta,
};
this.system.system.invokeFunction("template.newPage", [pageName]).then(
() => {
this.focus();
},
).catch(
console.error,
);
} else {
this.flashNotification(
`Could not load page ${pageName}: ${e.message}`,
+7
View File
@@ -42,6 +42,7 @@ import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts";
import { deepObjectMerge } from "$sb/lib/json.ts";
import { Query } from "$sb/types.ts";
import { PanelWidgetHook } from "./hooks/panel_widget.ts";
import { createKeyBindings } from "./editor_state.ts";
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
@@ -103,6 +104,12 @@ export class ClientSystem {
type: "update-commands",
commands: commandMap,
});
// Replace the key mapping compartment (keybindings)
this.client.editorView.dispatch({
effects: this.client.keyHandlerCompartment?.reconfigure(
createKeyBindings(this.client),
),
});
},
});
this.system.addHook(this.commandHook);
+5 -2
View File
@@ -131,9 +131,12 @@ export function attachmentExtension(editor: Client) {
if (currentNode) {
const fencedParentNode = findParentMatching(
currentNode,
(t) => t.type === "FencedCode",
(t) => ["FrontMatter", "FencedCode"].includes(t.type!),
);
if (fencedParentNode || currentNode.type === "FencedCode") {
if (
fencedParentNode ||
["FrontMatter", "FencedCode"].includes(currentNode.type!)
) {
console.log("Inside of fenced code block, not pasting rich text");
return false;
}
+7 -2
View File
@@ -165,8 +165,13 @@ export class MarkdownWidget extends WidgetType {
el.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
console.info("Command link clicked in widget, running", command);
this.client.runCommandByName(command).catch(console.error);
console.info(
"Command link clicked in widget, running",
parsedOnclick,
);
this.client.runCommandByName(command, parsedOnclick[2]).catch(
console.error,
);
});
}
});
+11 -3
View File
@@ -30,13 +30,21 @@ export class LinkWidget extends WidgetType {
anchor.textContent = this.options.text;
// Mouse handling
anchor.addEventListener("mousedown", (e) => {
anchor.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
});
anchor.addEventListener("mouseup", (e) => {
if (e.button !== 0) {
return;
}
e.preventDefault();
e.stopPropagation();
this.options.callback(e);
try {
this.options.callback(e);
} catch (e) {
console.error("Error handling wiki link click", e);
}
});
// Touch handling
@@ -111,7 +119,7 @@ export class ButtonWidget extends WidgetType {
const anchor = document.createElement("button");
anchor.className = this.cssClass;
anchor.textContent = this.text;
anchor.addEventListener("click", (e) => {
anchor.addEventListener("mouseup", (e) => {
e.preventDefault();
e.stopPropagation();
this.callback(e);
+1 -1
View File
@@ -40,7 +40,7 @@ export function CommandPalette({
});
if (commandOverride) {
shortcut = commandOverride;
console.log(`Shortcut override for ${name}:`, shortcut);
// console.log(`Shortcut override for ${name}:`, shortcut);
}
}
options.push({
+4 -1
View File
@@ -47,7 +47,10 @@ export function FilterList({
}) {
const [text, setText] = useState("");
const [matchingOptions, setMatchingOptions] = useState(
fuzzySearchAndSort(options, ""),
fuzzySearchAndSort(
preFilter ? preFilter(options, "") : options,
"",
),
);
const [selectedOption, setSelectionOption] = useState(0);
+1 -1
View File
@@ -84,7 +84,7 @@ export function MiniEditor(
}
};
}
}, [editorDiv]);
}, [editorDiv, placeholderText]);
useEffect(() => {
callbacksRef.current = {
+27 -14
View File
@@ -11,12 +11,14 @@ export function PageNavigator({
onNavigate,
completer,
vimMode,
mode,
darkMode,
currentPage,
}: {
allPages: PageMeta[];
vimMode: boolean;
darkMode: boolean;
mode: "page" | "template";
onNavigate: (page: string | undefined) => void;
completer: (context: CompletionContext) => Promise<CompletionResult | null>;
currentPage?: string;
@@ -72,7 +74,7 @@ export function PageNavigator({
}
return (
<FilterList
placeholder="Page"
placeholder={mode === "page" ? "Page" : "Template"}
label="Open"
options={options}
vimMode={vimMode}
@@ -83,24 +85,35 @@ export function PageNavigator({
return phrase;
}}
preFilter={(options, phrase) => {
const allTags = phrase.match(tagRegex);
if (allTags) {
// Search phrase contains hash tags, let's pre-filter the results based on this
const filterTags = allTags.map((t) => t.slice(1));
if (mode === "page") {
const allTags = phrase.match(tagRegex);
if (allTags) {
// Search phrase contains hash tags, let's pre-filter the results based on this
const filterTags = allTags.map((t) => t.slice(1));
options = options.filter((pageMeta) => {
if (!pageMeta.tags) {
return false;
}
return filterTags.every((tag) =>
pageMeta.tags.find((itemTag: string) => itemTag.startsWith(tag))
);
});
}
options = options.filter((pageMeta) => {
if (!pageMeta.tags) {
return false;
}
return filterTags.every((tag) =>
pageMeta.tags.find((itemTag: string) => itemTag.startsWith(tag))
);
return !pageMeta.tags?.includes("template");
});
return options;
} else {
// Filter on pages tagged with "template"
options = options.filter((pageMeta) => {
return pageMeta.tags?.includes("template");
});
return options;
}
return options;
}}
allowNew={true}
helpText="Press <code>Enter</code> to open the selected page, or <code>Shift-Enter</code> to create a new page with this exact name."
newHint="Create page"
helpText={`Press <code>Enter</code> to open the selected ${mode}, or <code>Shift-Enter</code> to create a new ${mode} with this exact name.`}
newHint={`Create ${mode}`}
completePrefix={completePrefix}
onSelect={(opt) => {
onNavigate(opt?.name);
+1 -1
View File
@@ -66,7 +66,7 @@ export function TopBar({
// Then calculate a new width
currentPageElement.style.width = `${
Math.min(editorWidth - 170, innerDiv.clientWidth - 170)
Math.min(editorWidth - 200, innerDiv.clientWidth - 200)
}px`;
}
}
+1
View File
@@ -14,6 +14,7 @@ export {
Home as HomeIcon,
RefreshCw as RefreshCwIcon,
Terminal as TerminalIcon,
Type as TemplateIcon,
} from "https://esm.sh/preact-feather@4.2.1?external=preact";
// Vim mode
+109 -114
View File
@@ -45,6 +45,7 @@ import { TextChange } from "$sb/lib/change.ts";
import { postScriptPrefacePlugin } from "./cm_plugins/top_bottom_panels.ts";
import { languageFor } from "../common/languages.ts";
import { plugLinter } from "./cm_plugins/lint.ts";
import { Compartment, Extension } from "@codemirror/state";
export function createEditorState(
client: Client,
@@ -52,85 +53,16 @@ export function createEditorState(
text: string,
readOnly: boolean,
): EditorState {
const commandKeyBindings: KeyBinding[] = [];
// Track which keyboard shortcuts for which commands we've overridden, so we can skip them later
const overriddenCommands = new Set<string>();
// Keyboard shortcuts from SETTINGS take precedense
if (client.settings?.shortcuts) {
for (const shortcut of client.settings.shortcuts) {
// Figure out if we're using the command link syntax here, if so: parse it out
const commandMatch = commandLinkRegex.exec(shortcut.command);
let cleanCommandName = shortcut.command;
let args: any[] = [];
if (commandMatch) {
cleanCommandName = commandMatch[1];
args = commandMatch[5] ? JSON.parse(`[${commandMatch[5]}]`) : [];
}
if (args.length === 0) {
// If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set:
overriddenCommands.add(cleanCommandName);
}
commandKeyBindings.push({
key: shortcut.key,
mac: shortcut.mac,
run: (): boolean => {
client.runCommandByName(cleanCommandName, args).catch((e: any) => {
console.error(e);
client.flashNotification(
`Error running command: ${e.message}`,
"error",
);
}).then(() => {
// Always be focusing the editor after running a command
client.focus();
});
return true;
},
});
}
}
// Then add bindings for plug commands
for (const def of client.system.commandHook.editorCommands.values()) {
if (def.command.key) {
// If we've already overridden this command, skip it
if (overriddenCommands.has(def.command.key)) {
continue;
}
commandKeyBindings.push({
key: def.command.key,
mac: def.command.mac,
run: (): boolean => {
if (def.command.contexts) {
const context = client.getContext();
if (!context || !def.command.contexts.includes(context)) {
return false;
}
}
Promise.resolve([])
.then(def.run)
.catch((e: any) => {
console.error(e);
client.flashNotification(
`Error running command: ${e.message}`,
"error",
);
})
.then(() => {
// Always be focusing the editor after running a command
client.focus();
});
return true;
},
});
}
}
let touchCount = 0;
const markdownLanguage = buildMarkdown(client.system.mdExtensions);
// Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded
client.keyHandlerCompartment = new Compartment();
const keyBindings = client.keyHandlerCompartment.of(
createKeyBindings(client),
);
return EditorState.create({
doc: text,
extensions: [
@@ -209,48 +141,13 @@ export function createEditorState(
{ selector: "BulletList", class: "sb-line-ul" },
{ selector: "OrderedList", class: "sb-line-ol" },
{ selector: "TableHeader", class: "sb-line-tbl-header" },
{ selector: "FrontMatter", class: "sb-frontmatter" },
]),
keymap.of([
...commandKeyBindings,
...smartQuoteKeymap,
...closeBracketsKeymap,
...standardKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
indentWithTab,
{
key: "Ctrl-k",
mac: "Cmd-k",
run: (): boolean => {
client.startPageNavigate();
return true;
},
},
{
key: "Ctrl-/",
mac: "Cmd-/",
run: (): boolean => {
client.ui.viewDispatch({
type: "show-palette",
context: client.getContext(),
});
return true;
},
},
{
key: "Ctrl-.",
mac: "Cmd-.",
run: (): boolean => {
client.ui.viewDispatch({
type: "show-palette",
context: client.getContext(),
});
return true;
},
selector: "FrontMatter",
class: "sb-frontmatter",
disableSpellCheck: true,
},
]),
keyBindings,
EditorView.domEventHandlers({
// This may result in duplicated touch events on mobile devices
touchmove: () => {
@@ -366,3 +263,101 @@ export function createEditorState(
],
});
}
export function createKeyBindings(client: Client): Extension {
const commandKeyBindings: KeyBinding[] = [];
// Track which keyboard shortcuts for which commands we've overridden, so we can skip them later
const overriddenCommands = new Set<string>();
// Keyboard shortcuts from SETTINGS take precedense
if (client.settings?.shortcuts) {
for (const shortcut of client.settings.shortcuts) {
// Figure out if we're using the command link syntax here, if so: parse it out
const commandMatch = commandLinkRegex.exec(shortcut.command);
let cleanCommandName = shortcut.command;
let args: any[] = [];
if (commandMatch) {
cleanCommandName = commandMatch[1];
args = commandMatch[5] ? JSON.parse(`[${commandMatch[5]}]`) : [];
}
if (args.length === 0) {
// If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set:
overriddenCommands.add(cleanCommandName);
}
commandKeyBindings.push({
key: shortcut.key,
mac: shortcut.mac,
run: (): boolean => {
client.runCommandByName(cleanCommandName, args).catch((e: any) => {
console.error(e);
client.flashNotification(
`Error running command: ${e.message}`,
"error",
);
}).then(() => {
// Always be focusing the editor after running a command
client.focus();
});
return true;
},
});
}
}
// Then add bindings for plug commands
for (const def of client.system.commandHook.editorCommands.values()) {
if (def.command.key) {
// If we've already overridden this command, skip it
if (overriddenCommands.has(def.command.key)) {
continue;
}
commandKeyBindings.push({
key: def.command.key,
mac: def.command.mac,
run: (): boolean => {
if (def.command.contexts) {
const context = client.getContext();
if (!context || !def.command.contexts.includes(context)) {
return false;
}
}
Promise.resolve([])
.then(def.run)
.catch((e: any) => {
console.error(e);
client.flashNotification(
`Error running command: ${e.message}`,
"error",
);
})
.then(() => {
// Always be focusing the editor after running a command
client.focus();
});
return true;
},
});
}
}
return keymap.of([
...commandKeyBindings,
...smartQuoteKeymap,
...closeBracketsKeymap,
...standardKeymap,
...searchKeymap,
...historyKeymap,
...completionKeymap,
indentWithTab,
{
key: "Ctrl-.",
mac: "Cmd-.",
run: (): boolean => {
client.ui.viewDispatch({
type: "show-palette",
context: client.getContext(),
});
return true;
},
},
]);
}
+18 -4
View File
@@ -12,6 +12,7 @@ import {
preactRender,
RefreshCwIcon,
runScopeHandlers,
TemplateIcon,
TerminalIcon,
useEffect,
useReducer,
@@ -20,6 +21,7 @@ import type { Client } from "./client.ts";
import { Panel } from "./components/panel.tsx";
import { h } from "./deps.ts";
import { sleep } from "$sb/lib/async.ts";
import { template } from "https://esm.sh/v132/handlebars@4.7.7/runtime.d.ts";
export class MainUI {
viewState: AppViewState = initialViewState;
@@ -44,7 +46,7 @@ export class MainUI {
if (ev.touches.length === 2) {
ev.stopPropagation();
ev.preventDefault();
client.startPageNavigate();
client.startPageNavigate("page");
}
// Launch the command palette using a three-finger tap
if (ev.touches.length === 3) {
@@ -99,6 +101,7 @@ export class MainUI {
<PageNavigator
allPages={viewState.allPages}
currentPage={client.currentPage}
mode={viewState.pageNavigatorMode}
completer={client.miniEditorComplete.bind(client)}
vimMode={viewState.uiOptions.vimMode}
darkMode={viewState.uiOptions.darkMode}
@@ -201,8 +204,8 @@ export class MainUI {
return;
}
console.log("Now renaming page to...", newName);
await client.system.system.loadedPlugs.get("index")!.invoke(
"renamePageCommand",
await client.system.system.invokeFunction(
"index.renamePageCommand",
[{ page: newName }],
);
client.focus();
@@ -244,6 +247,8 @@ export class MainUI {
description: `Go to the index page (Alt-h)`,
callback: () => {
client.navigate("", 0);
// And let's make sure all panels are closed
dispatch({ type: "hide-filterbox" });
},
href: "",
},
@@ -251,7 +256,16 @@ export class MainUI {
icon: BookIcon,
description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`,
callback: () => {
client.startPageNavigate();
client.startPageNavigate("page");
},
},
{
icon: TemplateIcon,
description: `Open template (${
isMacLike() ? "Cmd-Shift-t" : "Ctrl-Shift-t"
})`,
callback: () => {
client.startPageNavigate("template");
},
},
{
+87 -4
View File
@@ -1,6 +1,13 @@
import { Hook, Manifest } from "../../plugos/types.ts";
import { System } from "../../plugos/system.ts";
import { EventEmitter } from "../../plugos/event.ts";
import { ObjectValue } from "$sb/types.ts";
import {
FrontmatterConfig,
SnippetConfig,
} from "../../plugs/template/types.ts";
import { throttle } from "$sb/lib/async.ts";
import { NewPageConfig } from "../../plugs/template/types.ts";
export type CommandDef = {
name: string;
@@ -31,10 +38,15 @@ export type CommandHookEvents = {
export class CommandHook extends EventEmitter<CommandHookEvents>
implements Hook<CommandHookT> {
editorCommands = new Map<string, AppCommand>();
system!: System<CommandHookT>;
buildAllCommands(system: System<CommandHookT>) {
throttledBuildAllCommands = throttle(() => {
this.buildAllCommands().catch(console.error);
}, 1000);
async buildAllCommands() {
this.editorCommands.clear();
for (const plug of system.loadedPlugs.values()) {
for (const plug of this.system.loadedPlugs.values()) {
for (
const [name, functionDef] of Object.entries(
plug.manifest!.functions,
@@ -52,18 +64,89 @@ export class CommandHook extends EventEmitter<CommandHookEvents>
});
}
}
await this.loadPageTemplateCommands();
this.emit("commandsUpdated", this.editorCommands);
}
async loadPageTemplateCommands() {
// This relies on two plugs being loaded: index and template
const indexPlug = this.system.loadedPlugs.get("index");
const templatePlug = this.system.loadedPlugs.get("template");
if (!indexPlug || !templatePlug) {
// Index and template plugs not yet loaded, let's wait
return;
}
// Query all page templates that have a command configured
const templateCommands: ObjectValue<FrontmatterConfig>[] = await indexPlug
.invoke(
"queryObjects",
["template", {
// where hooks.newPage.command or hooks.snippet.command
filter: ["or", [
"attr",
["attr", ["attr", "hooks"], "newPage"],
"command",
], [
"attr",
["attr", ["attr", "hooks"], "snippet"],
"command",
]],
}],
);
// console.log("Template commands", templateCommands);
for (const page of templateCommands) {
try {
if (page.hooks!.newPage) {
const newPageConfig = NewPageConfig.parse(page.hooks!.newPage);
const cmdDef = {
name: newPageConfig.command!,
key: newPageConfig.key,
mac: newPageConfig.mac,
};
this.editorCommands.set(newPageConfig.command!, {
command: cmdDef,
run: () => {
return templatePlug.invoke("newPageCommand", [cmdDef, page.ref]);
},
});
}
if (page.hooks!.snippet) {
const snippetConfig = SnippetConfig.parse(page.hooks!.snippet);
const cmdDef = {
name: snippetConfig.command!,
key: snippetConfig.key,
mac: snippetConfig.mac,
};
this.editorCommands.set(snippetConfig.command!, {
command: cmdDef,
run: () => {
return templatePlug.invoke("insertSnippetTemplate", [
{ templatePage: page.ref },
]);
},
});
}
} catch (e: any) {
console.error("Error loading command from", page.ref, e);
}
}
// console.log("Page template commands", pageTemplateCommands);
}
apply(system: System<CommandHookT>): void {
this.system = system;
system.on({
plugLoaded: () => {
this.buildAllCommands(system);
this.throttledBuildAllCommands();
},
});
// On next tick
setTimeout(() => {
this.buildAllCommands(system);
this.throttledBuildAllCommands();
});
}
+3 -11
View File
@@ -115,18 +115,10 @@ export class SlashCommandHook implements Hook<SlashCommandHookT> {
});
// Replace with whatever the completion is
safeRun(async () => {
const [plugName, functionName] = slashCompletion.invoke.split(
".",
await this.editor.system.system.invokeFunction(
slashCompletion.invoke,
[slashCompletion],
);
const plug = this.editor.system.system.loadedPlugs.get(plugName);
if (!plug) {
this.editor.flashNotification(
`Plug ${plugName} not found`,
"error",
);
return;
}
await plug.invoke(functionName, [slashCompletion]);
this.editor.focus();
});
},
+3
View File
@@ -66,6 +66,7 @@ export default function reducer(
return {
...state,
showPageNavigator: true,
pageNavigatorMode: action.mode,
showCommandPalette: false,
showFilterBox: false,
};
@@ -141,6 +142,8 @@ export default function reducer(
case "hide-filterbox":
return {
...state,
showCommandPalette: false,
showPageNavigator: false,
showFilterBox: false,
filterBoxOnSelect: () => {},
filterBoxPlaceHolder: "",
+44 -32
View File
@@ -14,22 +14,22 @@ import { SysCallMapping } from "../../plugos/system.ts";
import type { FilterOption } from "../types.ts";
import { UploadFile } from "../../plug-api/types.ts";
export function editorSyscalls(editor: Client): SysCallMapping {
export function editorSyscalls(client: Client): SysCallMapping {
const syscalls: SysCallMapping = {
"editor.getCurrentPage": (): string => {
return editor.currentPage!;
return client.currentPage!;
},
"editor.getText": () => {
return editor.editorView.state.sliceDoc();
return client.editorView.state.sliceDoc();
},
"editor.getCursor": (): number => {
return editor.editorView.state.selection.main.from;
return client.editorView.state.selection.main.from;
},
"editor.getSelection": (): { from: number; to: number } => {
return editor.editorView.state.selection.main;
return client.editorView.state.selection.main;
},
"editor.save": () => {
return editor.save(true);
return client.save(true);
},
"editor.navigate": async (
_ctx,
@@ -38,14 +38,18 @@ export function editorSyscalls(editor: Client): SysCallMapping {
replaceState = false,
newWindow = false,
) => {
await editor.navigate(name, pos, replaceState, newWindow);
await client.navigate(name, pos, replaceState, newWindow);
},
"editor.reloadPage": async () => {
await editor.reloadPage();
await client.reloadPage();
},
"editor.reloadUI": () => {
location.reload();
},
"editor.reloadSettingsAndCommands": async () => {
await client.loadSettings();
await client.system.commandHook.buildAllCommands();
},
"editor.openUrl": (_ctx, url: string, existingWindow = false) => {
if (!existingWindow) {
const win = window.open(url, "_blank");
@@ -113,7 +117,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
message: string,
type: "error" | "info" = "info",
) => {
editor.flashNotification(message, type);
client.flashNotification(message, type);
},
"editor.filterBox": (
_ctx,
@@ -122,7 +126,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
helpText = "",
placeHolder = "",
): Promise<FilterOption | undefined> => {
return editor.filterBox(label, options, helpText, placeHolder);
return client.filterBox(label, options, helpText, placeHolder);
},
"editor.showPanel": (
_ctx,
@@ -131,28 +135,28 @@ export function editorSyscalls(editor: Client): SysCallMapping {
html: string,
script: string,
) => {
editor.ui.viewDispatch({
client.ui.viewDispatch({
type: "show-panel",
id: id as any,
config: { html, script, mode },
});
setTimeout(() => {
// Dummy dispatch to rerender the editor and toggle the panel
editor.editorView.dispatch({});
client.editorView.dispatch({});
});
},
"editor.hidePanel": (_ctx, id: string) => {
editor.ui.viewDispatch({
client.ui.viewDispatch({
type: "hide-panel",
id: id as any,
});
setTimeout(() => {
// Dummy dispatch to rerender the editor and toggle the panel
editor.editorView.dispatch({});
client.editorView.dispatch({});
});
},
"editor.insertAtPos": (_ctx, text: string, pos: number) => {
editor.editorView.dispatch({
client.editorView.dispatch({
changes: {
insert: text,
from: pos,
@@ -160,7 +164,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
});
},
"editor.replaceRange": (_ctx, from: number, to: number, text: string) => {
editor.editorView.dispatch({
client.editorView.dispatch({
changes: {
insert: text,
from: from,
@@ -169,13 +173,13 @@ export function editorSyscalls(editor: Client): SysCallMapping {
});
},
"editor.moveCursor": (_ctx, pos: number, center = false) => {
editor.editorView.dispatch({
client.editorView.dispatch({
selection: {
anchor: pos,
},
});
if (center) {
editor.editorView.dispatch({
client.editorView.dispatch({
effects: [
EditorView.scrollIntoView(
pos,
@@ -186,10 +190,10 @@ export function editorSyscalls(editor: Client): SysCallMapping {
],
});
}
editor.editorView.focus();
client.editorView.focus();
},
"editor.setSelection": (_ctx, from: number, to: number) => {
editor.editorView.dispatch({
client.editorView.dispatch({
selection: {
anchor: from,
head: to,
@@ -198,7 +202,7 @@ export function editorSyscalls(editor: Client): SysCallMapping {
},
"editor.insertAtCursor": (_ctx, text: string) => {
const editorView = editor.editorView;
const editorView = client.editorView;
const from = editorView.state.selection.main.from;
editorView.dispatch({
changes: {
@@ -211,47 +215,55 @@ export function editorSyscalls(editor: Client): SysCallMapping {
});
},
"editor.dispatch": (_ctx, change: Transaction) => {
editor.editorView.dispatch(change);
client.editorView.dispatch(change);
},
"editor.prompt": (
_ctx,
message: string,
defaultValue = "",
): Promise<string | undefined> => {
return editor.prompt(message, defaultValue);
return client.prompt(message, defaultValue);
},
"editor.confirm": (_ctx, message: string): Promise<boolean> => {
return editor.confirm(message);
return client.confirm(message);
},
"editor.getUiOption": (_ctx, key: string): any => {
return (editor.ui.viewState.uiOptions as any)[key];
return (client.ui.viewState.uiOptions as any)[key];
},
"editor.setUiOption": (_ctx, key: string, value: any) => {
editor.ui.viewDispatch({
client.ui.viewDispatch({
type: "set-ui-option",
key,
value,
});
},
"editor.vimEx": (_ctx, exCommand: string) => {
const cm = vimGetCm(editor.editorView)!;
const cm = vimGetCm(client.editorView)!;
return Vim.handleEx(cm, exCommand);
},
"editor.openPageNavigator": (_ctx, mode: "page" | "template" = "page") => {
client.startPageNavigate(mode);
},
"editor.openCommandPalette": () => {
client.ui.viewDispatch({
type: "show-palette",
});
},
// Folding
"editor.fold": () => {
foldCode(editor.editorView);
foldCode(client.editorView);
},
"editor.unfold": () => {
unfoldCode(editor.editorView);
unfoldCode(client.editorView);
},
"editor.toggleFold": () => {
toggleFold(editor.editorView);
toggleFold(client.editorView);
},
"editor.foldAll": () => {
foldAll(editor.editorView);
foldAll(client.editorView);
},
"editor.unfoldAll": () => {
unfoldAll(editor.editorView);
unfoldAll(client.editorView);
},
};
+1 -4
View File
@@ -18,11 +18,8 @@ export async function proxySyscall(
name: string,
args: any[],
): Promise<any> {
if (!ctx.plug) {
throw new Error(`Cannot proxy ${name} syscall without plug context`);
}
const resp = await httpSpacePrimitives.authenticatedFetch(
`${httpSpacePrimitives.url}/.rpc/${ctx.plug}/${name}`,
`${httpSpacePrimitives.url}/.rpc/${ctx.plug || "_"}/${name}`,
{
method: "POST",
body: JSON.stringify(args),
+5 -1
View File
@@ -63,6 +63,9 @@ export type AppViewState = {
forcedROMode: boolean;
};
// Page navigator mode
pageNavigatorMode: "page" | "template";
// Filter box
showFilterBox: boolean;
filterBoxLabel: string;
@@ -87,6 +90,7 @@ export const initialViewState: AppViewState = {
isLoading: false,
showPageNavigator: false,
showCommandPalette: false,
pageNavigatorMode: "page",
unsavedChanges: false,
syncFailures: 0,
uiOptions: {
@@ -122,7 +126,7 @@ export type Action =
| { type: "page-saved" }
| { type: "sync-change"; syncSuccess: boolean }
| { type: "update-page-list"; allPages: PageMeta[] }
| { type: "start-navigate" }
| { type: "start-navigate"; mode: "page" | "template" }
| { type: "stop-navigate" }
| {
type: "update-commands";