Initial space mounting support

pull/3/head
Zef Hemel 2022-07-06 12:18:33 +02:00
parent f1a7cc1fb7
commit 05edbfe305
5 changed files with 233 additions and 5 deletions

View File

@ -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

View File

@ -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<string> {
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<PageMeta> {
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<void> {
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<PageMeta> {
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<PageMeta[]> {
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;
}

View File

@ -75,9 +75,13 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
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}`

View File

@ -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;
}

View File

@ -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) {