Templates 2.0 (#636)
Templates 2.0 and a whole bunch of other refactoring
This commit is contained in:
+41
-14
@@ -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}`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export function MiniEditor(
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [editorDiv]);
|
||||
}, [editorDiv, placeholderText]);
|
||||
|
||||
useEffect(() => {
|
||||
callbacksRef.current = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user