From 89f93963f5e3800f642bace5952fa5f9ecf5d739 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Sun, 27 Feb 2022 10:17:43 +0100 Subject: [PATCH] Support slashes in file names --- plugins/core/core.plugin.json | 8 +++-- plugins/core/lib/event.ts | 5 --- plugins/core/lib/syscall.ts | 30 ++++++++--------- plugins/core/navigate.ts | 15 +++++++++ server/server.ts | 37 +++++++++++++-------- webapp/src/app_event.ts | 6 +++- webapp/src/editor.tsx | 45 ++++++++++++++++++-------- webapp/src/plugins/runtime.ts | 13 +++++--- webapp/src/syscalls/db.localstorage.ts | 6 ++-- webapp/src/syscalls/editor.browser.ts | 34 +++++++++++++++++++ webapp/src/syscalls/space.native.ts | 10 ++---- webapp/src/syscalls/ui.browser.ts | 4 +-- 12 files changed, 142 insertions(+), 71 deletions(-) delete mode 100644 plugins/core/lib/event.ts diff --git a/plugins/core/core.plugin.json b/plugins/core/core.plugin.json index a7c8f368..7fd9a0ba 100644 --- a/plugins/core/core.plugin.json +++ b/plugins/core/core.plugin.json @@ -4,7 +4,7 @@ "invoke": "word_count_command" }, "Navigate To page": { - "invoke": "link_navigate", + "invoke": "linkNavigate", "key": "Ctrl-Enter", "mac": "Cmd-Enter" }, @@ -24,9 +24,13 @@ } }, "events": { - "page:click": ["taskToggle", "clickNavigate"] + "page:click": ["taskToggle", "clickNavigate"], + "editor:complete": ["pageComplete"] }, "functions": { + "pageComplete": { + "path": "./navigate.ts:pageComplete" + }, "linkNavigate": { "path": "./navigate.ts:linkNavigate" }, diff --git a/plugins/core/lib/event.ts b/plugins/core/lib/event.ts deleted file mode 100644 index 9dae53e5..00000000 --- a/plugins/core/lib/event.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {syscall} from "./syscall.ts"; - -export async function publish(event: string, data?: object) { - return await syscall("event.publish", event, data); -} diff --git a/plugins/core/lib/syscall.ts b/plugins/core/lib/syscall.ts index 37f826dc..08808860 100644 --- a/plugins/core/lib/syscall.ts +++ b/plugins/core/lib/syscall.ts @@ -1,16 +1,16 @@ -export function syscall(name: string, ...args: Array): any { - let reqId = Math.floor(Math.random() * 1000000); - // console.log("Syscall", name, reqId); - return new Promise((resolve, reject) => { - self.dispatchEvent( - new CustomEvent("syscall", { - detail: { - id: reqId, - name: name, - args: args, - callback: resolve, - }, - }), - ); - }); +export function syscall(name: string, ...args: any[]): any { + let reqId = Math.floor(Math.random() * 1000000); + // console.log("Syscall", name, reqId); + return new Promise((resolve, reject) => { + self.dispatchEvent( + new CustomEvent("syscall", { + detail: { + id: reqId, + name: name, + args: args, + callback: resolve, + }, + }) + ); + }); } diff --git a/plugins/core/navigate.ts b/plugins/core/navigate.ts index 5ef68d21..756a11ca 100644 --- a/plugins/core/navigate.ts +++ b/plugins/core/navigate.ts @@ -19,3 +19,18 @@ export async function clickNavigate(event: ClickEvent) { } } } + +export async function pageComplete() { + let prefix = await syscall("editor.matchBefore", "\\[\\[[\\w\\s]*"); + if (!prefix) { + return null; + } + let allPages = await syscall("space.listPages"); + return { + from: prefix.from + 2, + options: allPages.map((pageMeta: any) => ({ + label: pageMeta.name, + type: "page", + })), + }; +} diff --git a/server/server.ts b/server/server.ts index c6018123..061633cf 100644 --- a/server/server.ts +++ b/server/server.ts @@ -6,6 +6,8 @@ import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts"; import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts"; import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts"; +import { recursiveReaddir } from "https://deno.land/x/recursive_readdir@v2.0.0/mod.ts"; + type PageMeta = { name: string; lastModified: number; @@ -21,22 +23,23 @@ fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] })); fsRouter.get("/", async (context) => { const localPath = pagesPath; let fileNames: PageMeta[] = []; - for await (const dirEntry of Deno.readDir(localPath)) { - if (dirEntry.isFile) { - const stat = await Deno.stat(`${localPath}/${dirEntry.name}`); - fileNames.push({ - name: dirEntry.name.substring( - 0, - dirEntry.name.length - path.extname(dirEntry.name).length - ), - lastModified: stat.mtime?.getTime()!, - }); - } + const markdownFiles = (await recursiveReaddir(localPath)).filter( + (file: string) => path.extname(file) === ".md" + ); + for (const p of markdownFiles) { + const stat = await Deno.stat(p); + fileNames.push({ + name: p.substring( + localPath.length + 1, + p.length - path.extname(p).length + ), + lastModified: stat.mtime?.getTime()!, + }); } context.response.body = JSON.stringify(fileNames); }); -fsRouter.get("/:page", async (context) => { +fsRouter.get("/:page(.*)", async (context) => { const pageName = context.params.page; const localPath = `${pagesPath}/${pageName}.md`; try { @@ -50,7 +53,7 @@ fsRouter.get("/:page", async (context) => { } }); -fsRouter.options("/:page", async (context) => { +fsRouter.options("/:page(.*)", async (context) => { const localPath = `${pagesPath}/${context.params.page}.md`; try { const stat = await Deno.stat(localPath); @@ -63,10 +66,16 @@ fsRouter.options("/:page", async (context) => { } }); -fsRouter.put("/:page", async (context) => { +fsRouter.put("/:page(.*)", async (context) => { const pageName = context.params.page; const localPath = `${pagesPath}/${pageName}.md`; const existingPage = await exists(localPath); + let dirName = path.dirname(localPath); + if (!(await exists(dirName))) { + await Deno.mkdir(dirName, { + recursive: true, + }); + } let file; try { file = await Deno.create(localPath); diff --git a/webapp/src/app_event.ts b/webapp/src/app_event.ts index 3d5f33e8..39e7a84d 100644 --- a/webapp/src/app_event.ts +++ b/webapp/src/app_event.ts @@ -1,4 +1,8 @@ -export type AppEvent = "app:ready" | "page:save" | "page:load" | "page:click"; +export type AppEvent = + | "app:ready" + | "page:save" + | "page:click" + | "editor:complete"; export type ClickEvent = { pos: number; diff --git a/webapp/src/editor.tsx b/webapp/src/editor.tsx index fe9ef377..47596b95 100644 --- a/webapp/src/editor.tsx +++ b/webapp/src/editor.tsx @@ -134,10 +134,17 @@ export class Editor { } // TODO: Parallelize? - async dispatchAppEvent(name: AppEvent, data?: any) { + async dispatchAppEvent(name: AppEvent, data?: any): Promise { + let results: any[] = []; for (let plugin of this.plugins) { - await plugin.dispatchEvent(name, data); + let pluginResults = await plugin.dispatchEvent(name, data); + if (pluginResults) { + for (let result of pluginResults) { + results.push(result); + } + } } + return results; } get currentPage(): PageMeta | undefined { @@ -176,7 +183,7 @@ export class Editor { closeBrackets(), autocompletion({ override: [ - this.pageCompleter.bind(this), + this.pluginCompleter.bind(this), this.commandCompleter.bind(this), ], }), @@ -223,6 +230,14 @@ export class Editor { return true; }, }, + { + key: "Ctrl-s", + mac: "Cmd-s", + run: (target): boolean => { + this.save(); + return true; + }, + }, { key: "Ctrl-.", mac: "Cmd-.", @@ -258,18 +273,20 @@ export class Editor { }); } - pageCompleter(ctx: CompletionContext): CompletionResult | null { - let prefix = ctx.matchBefore(/\[\[[\w\s]*/); - if (!prefix) { - return null; + async pluginCompleter( + ctx: CompletionContext + ): Promise { + let allCompletionResults = await this.dispatchAppEvent("editor:complete"); + // console.log("All results", allCompletionResults); + if (allCompletionResults.length === 1) { + return allCompletionResults[0]; + } else if (allCompletionResults.length > 1) { + console.error( + "Got completion results from multiple sources, cannot deal with that", + allCompletionResults + ); } - return { - from: prefix.from + 2, - options: this.viewState.allPages.map((pageMeta) => ({ - label: pageMeta.name, - type: "page", - })), - }; + return null; } commandCompleter(ctx: CompletionContext): CompletionResult | null { diff --git a/webapp/src/plugins/runtime.ts b/webapp/src/plugins/runtime.ts index 29d27d35..c694ed6e 100644 --- a/webapp/src/plugins/runtime.ts +++ b/webapp/src/plugins/runtime.ts @@ -113,14 +113,17 @@ export class Plugin { return await this.runningFunctions.get(name)!.invoke(args); } - async dispatchEvent(name: string, data?: any) { + async dispatchEvent(name: string, data?: any): Promise { let functionsToSpawn = this.manifest!.events[name]; if (functionsToSpawn) { - await Promise.all( - functionsToSpawn.map(async (functionToSpawn: string) => { - await this.invoke(functionToSpawn, [data]); - }) + return await Promise.all( + functionsToSpawn.map( + async (functionToSpawn: string) => + await this.invoke(functionToSpawn, [data]) + ) ); + } else { + return []; } } diff --git a/webapp/src/syscalls/db.localstorage.ts b/webapp/src/syscalls/db.localstorage.ts index 7fa9bb41..b666f958 100644 --- a/webapp/src/syscalls/db.localstorage.ts +++ b/webapp/src/syscalls/db.localstorage.ts @@ -1,10 +1,8 @@ -import { SyscallContext } from "../plugins/runtime"; - export default { - "db.put": (ctx: SyscallContext, key: string, value: any) => { + "db.put": (key: string, value: any) => { localStorage.setItem(key, value); }, - "db.get": (ctx: SyscallContext, key: string) => { + "db.get": (key: string) => { return localStorage.getItem(key); }, }; diff --git a/webapp/src/syscalls/editor.browser.ts b/webapp/src/syscalls/editor.browser.ts index c582908f..5d81d479 100644 --- a/webapp/src/syscalls/editor.browser.ts +++ b/webapp/src/syscalls/editor.browser.ts @@ -9,6 +9,22 @@ type SyntaxNode = { to: number; }; +function ensureAnchor(expr: any, start: boolean) { + var _a; + let { source } = expr; + let addStart = start && source[0] != "^", + addEnd = source[source.length - 1] != "$"; + if (!addStart && !addEnd) return expr; + return new RegExp( + `${addStart ? "^" : ""}(?:${source})${addEnd ? "$" : ""}`, + (_a = expr.flags) !== null && _a !== void 0 + ? _a + : expr.ignoreCase + ? "i" + : "" + ); +} + export default (editor: Editor) => ({ "editor.getText": () => { return editor.editorView?.state.sliceDoc(); @@ -71,6 +87,24 @@ export default (editor: Editor) => ({ } } }, + "editor.matchBefore": ( + regexp: string + ): { from: number; to: number; text: string } | null => { + const editorState = editor.editorView!.state; + let selection = editorState.selection.main; + let from = selection.from; + if (selection.empty) { + let line = editorState.doc.lineAt(from); + let start = Math.max(line.from, from - 250); + let str = line.text.slice(start - line.from, from - line.from); + let found = str.search(ensureAnchor(new RegExp(regexp), false)); + // console.log("Line", line, start, str, new RegExp(regexp), found); + return found < 0 + ? null + : { from: start + found, to: from, text: str.slice(found) }; + } + return null; + }, "editor.getSyntaxNodeAtPos": (pos: number): SyntaxNode | undefined => { const editorState = editor.editorView!.state; let node = syntaxTree(editorState).resolveInner(pos); diff --git a/webapp/src/syscalls/space.native.ts b/webapp/src/syscalls/space.native.ts index ae191529..983e16ed 100644 --- a/webapp/src/syscalls/space.native.ts +++ b/webapp/src/syscalls/space.native.ts @@ -1,22 +1,16 @@ import { Editor } from "../editor"; -import { SyscallContext } from "../plugins/runtime"; import { PageMeta } from "../types"; export default (editor: Editor) => ({ - "space.listPages": (ctx: SyscallContext): PageMeta[] => { + "space.listPages": (): PageMeta[] => { return editor.viewState.allPages; }, "space.readPage": async ( - ctx: SyscallContext, name: string ): Promise<{ text: string; meta: PageMeta }> => { return await editor.fs.readPage(name); }, - "space.writePage": async ( - ctx: SyscallContext, - name: string, - text: string - ): Promise => { + "space.writePage": async (name: string, text: string): Promise => { return await editor.fs.writePage(name, text); }, }); diff --git a/webapp/src/syscalls/ui.browser.ts b/webapp/src/syscalls/ui.browser.ts index 5448ee8d..40f6a4e9 100644 --- a/webapp/src/syscalls/ui.browser.ts +++ b/webapp/src/syscalls/ui.browser.ts @@ -1,5 +1,3 @@ -import { SyscallContext } from "../plugins/runtime"; - // @ts-ignore let frameTest = document.getElementById("main-frame"); @@ -13,7 +11,7 @@ window.addEventListener("message", async (event) => { }); export default { - "ui.update": function (ctx: SyscallContext, doc: any) { + "ui.update": function (doc: any) { // frameTest.contentWindow.postMessage({ // type: "loadContent", // doc: doc,