diff --git a/packages/plugs/core/core.plug.yaml b/packages/plugs/core/core.plug.yaml index 30bc2d2..172bd1b 100644 --- a/packages/plugs/core/core.plug.yaml +++ b/packages/plugs/core/core.plug.yaml @@ -158,8 +158,14 @@ functions: quickNoteCommand: path: ./template.ts:quickNoteCommand command: - name: "Template: Quick Note" + name: "Quick Note" key: "Alt-Shift-n" + priority: 1 + dailyNoteCommand: + path: ./template.ts:dailyNoteCommand + command: + name: "Open Daily Note" + key: "Alt-Shift-d" instantiateTemplateCommand: path: ./template.ts:instantiateTemplateCommand @@ -259,3 +265,30 @@ functions: name: "UI: Hide BHS" key: "Ctrl-Alt-b" mac: "Cmd-Alt-b" + + # Mounting + readPageMounted: + path: ./mount.ts:readPageMounted + pageNamespace: + pattern: "^🚪 .+" + operation: readPage + writePageMounted: + path: ./mount.ts:writePageMounted + pageNamespace: + pattern: "^🚪 .+" + operation: writePage + deletePageMounted: + path: ./mount.ts:deletePageMounted + pageNamespace: + pattern: "^🚪 .+" + operation: deletePage + getPageMetaMounted: + path: ./mount.ts:getPageMetaMounted + pageNamespace: + pattern: "^🚪 .+" + operation: getPageMeta + listPagesMounted: + path: ./mount.ts:listPagesMounted + pageNamespace: + pattern: "^🚪 .+" + operation: listPages diff --git a/packages/plugs/core/mount.ts b/packages/plugs/core/mount.ts new file mode 100644 index 0000000..0569ac0 --- /dev/null +++ b/packages/plugs/core/mount.ts @@ -0,0 +1,190 @@ +import { PageMeta } from "@silverbulletmd/common/types"; +import { + deleteFile, + getFileMeta, + listFiles, + readFile, + writeFile, +} from "@plugos/plugos-syscall/fs"; +import { parseMarkdown } from "@silverbulletmd/plugos-silverbullet-syscall/markdown"; +import { + findNodeOfType, + renderToText, + replaceNodesMatching, +} from "@silverbulletmd/common/tree"; +import { readPage } from "@silverbulletmd/plugos-silverbullet-syscall/space"; +import YAML from "yaml"; + +const globalMountPrefix = "🚪 "; + +type MountPoint = { + prefix: string; + path: string; + perm: "rw" | "ro"; +}; + +let mountPointCache: MountPoint[] = []; + +async function updateMountPoints() { + let mountPointsText = ""; + try { + let { text } = await readPage("MOUNTS"); + mountPointsText = text; + } catch { + // No MOUNTS file, so that's all folks! + mountPointCache = []; + return; + } + + let tree = await parseMarkdown(mountPointsText); + + let codeTextNode = findNodeOfType(tree, "CodeText"); + if (!codeTextNode) { + console.error("Could not find yaml block in MOUNTS"); + return; + } + let mountsYaml = codeTextNode.children![0].text; + let mountList = YAML.parse(mountsYaml!); + if (!Array.isArray(mountList)) { + console.error("Invalid MOUNTS file, should have array of mount points"); + return; + } + for (let mountPoint of mountList) { + if (!mountPoint.prefix) { + console.error("Invalid mount point, no prefix specified", mountPoint); + return; + } + if (!mountPoint.path) { + console.error("Invalid mount point, no path specified", mountPoint); + return; + } + if (!mountPoint.perm) { + mountPoint.perm = "rw"; + } + } + + mountPointCache = mountList; +} + +async function translateLinksWithPrefix( + text: string, + prefix: string +): Promise { + prefix = `${globalMountPrefix}${prefix}`; + let tree = await parseMarkdown(text); + replaceNodesMatching(tree, (tree) => { + if (tree.type === "WikiLinkPage") { + // Add the prefix in the link text + tree.children![0].text = prefix + tree.children![0].text; + } + return undefined; + }); + text = renderToText(tree); + return text; +} + +async function translateLinksWithoutPrefix(text: string, prefix: string) { + prefix = `${globalMountPrefix}${prefix}`; + let tree = await parseMarkdown(text); + replaceNodesMatching(tree, (tree) => { + if (tree.type === "WikiLinkPage") { + // Remove the prefix in the link text + let text = tree.children![0].text!; + if (text.startsWith(prefix)) { + tree.children![0].text = text.substring(prefix.length); + } + } + return undefined; + }); + return renderToText(tree); +} + +function lookupMountPoint(fullPath: string): { + resolvedPath: string; + mountPoint: MountPoint; +} { + fullPath = fullPath.substring(globalMountPrefix.length); + for (let mp of mountPointCache) { + if (fullPath.startsWith(mp.prefix)) { + return { + resolvedPath: `${mp.path}/${fullPath.substring(mp.prefix.length)}`, + mountPoint: mp, + }; + } + } + throw new Error("No mount point found for " + fullPath); +} + +export async function readPageMounted( + name: string +): Promise<{ text: string; meta: PageMeta }> { + await updateMountPoints(); + let { resolvedPath, mountPoint } = lookupMountPoint(name); + let { text, meta } = await readFile(`${resolvedPath}.md`); + return { + text: await translateLinksWithPrefix(text, mountPoint.prefix), + meta: { + name: name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }, + }; +} + +export async function writePageMounted( + name: string, + text: string +): Promise { + await updateMountPoints(); + let { resolvedPath, mountPoint } = lookupMountPoint(name); + text = await translateLinksWithoutPrefix(text, mountPoint.prefix); + let meta = await writeFile(`${resolvedPath}.md`, text); + return { + name: name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }; +} + +export async function deletePageMounted(name: string): Promise { + await updateMountPoints(); + let { resolvedPath, mountPoint } = lookupMountPoint(name); + if (mountPoint.perm === "rw") { + await deleteFile(`${resolvedPath}.md`); + } else { + throw new Error("Deleting read-only page"); + } +} + +export async function getPageMetaMounted(name: string): Promise { + await updateMountPoints(); + let { resolvedPath, mountPoint } = lookupMountPoint(name); + let meta = await getFileMeta(`${resolvedPath}.md`); + return { + name, + lastModified: meta.lastModified, + perm: mountPoint.perm, + }; +} + +export async function listPagesMounted(): Promise { + await updateMountPoints(); + let allPages: PageMeta[] = []; + for (let mp of mountPointCache) { + let files = await listFiles(mp.path, true); + for (let file of files) { + if (!file.name.endsWith(".md")) { + continue; + } + allPages.push({ + name: `${globalMountPrefix}${mp.prefix}${file.name.substring( + 0, + file.name.length - 3 + )}`, + lastModified: file.lastModified, + perm: mp.perm, + }); + } + } + return allPages; +} diff --git a/packages/server/hooks/page_namespace.ts b/packages/server/hooks/page_namespace.ts index 7788c9f..4e99e0a 100644 --- a/packages/server/hooks/page_namespace.ts +++ b/packages/server/hooks/page_namespace.ts @@ -75,9 +75,13 @@ export class PageNamespaceHook implements Hook { errors.push(`Function ${funcName} has a namespace but no operation`); } if ( - !["readPage", "writePage", "getPageMeta", "listPages"].includes( - funcDef.pageNamespace.operation - ) + ![ + "readPage", + "writePage", + "getPageMeta", + "listPages", + "deletePage", + ].includes(funcDef.pageNamespace.operation) ) { errors.push( `Function ${funcName} has an invalid operation ${funcDef.pageNamespace.operation}` diff --git a/packages/web/editor.tsx b/packages/web/editor.tsx index ad2f6c4..9053e24 100644 --- a/packages/web/editor.tsx +++ b/packages/web/editor.tsx @@ -459,6 +459,7 @@ export class Editor { await this.system.unloadAll(); console.log("(Re)loading plugs"); for (let pageInfo of this.space.listPlugs()) { + console.log("Loading plug", pageInfo.name); let { text } = await this.space.readPage(pageInfo.name); await this.system.load(JSON.parse(text), createIFrameSandbox); } @@ -587,6 +588,7 @@ export class Editor { // console.log("Restoring selection state", pageState); editorView.dispatch({ selection: pageState.selection, + scrollIntoView: true, }); editorView.scrollDOM.scrollTop = pageState!.scrollTop; } diff --git a/packages/web/line_wrapper.ts b/packages/web/line_wrapper.ts index 176843d..54f35c7 100644 --- a/packages/web/line_wrapper.ts +++ b/packages/web/line_wrapper.ts @@ -24,7 +24,6 @@ function wrapLines(view: EditorView, wrapElements: WrapElement[]) { from, to, enter: ({ type, from, to }) => { - const bodyText = doc.sliceString(from, to); for (let wrapElement of wrapElements) { if (type.name == wrapElement.selector) { if (wrapElement.nesting) {