Support slashes in file names

pull/3/head
Zef Hemel 2022-02-27 10:17:43 +01:00
parent 158e1cb2ef
commit 89f93963f5
12 changed files with 142 additions and 71 deletions

View File

@ -4,7 +4,7 @@
"invoke": "word_count_command" "invoke": "word_count_command"
}, },
"Navigate To page": { "Navigate To page": {
"invoke": "link_navigate", "invoke": "linkNavigate",
"key": "Ctrl-Enter", "key": "Ctrl-Enter",
"mac": "Cmd-Enter" "mac": "Cmd-Enter"
}, },
@ -24,9 +24,13 @@
} }
}, },
"events": { "events": {
"page:click": ["taskToggle", "clickNavigate"] "page:click": ["taskToggle", "clickNavigate"],
"editor:complete": ["pageComplete"]
}, },
"functions": { "functions": {
"pageComplete": {
"path": "./navigate.ts:pageComplete"
},
"linkNavigate": { "linkNavigate": {
"path": "./navigate.ts:linkNavigate" "path": "./navigate.ts:linkNavigate"
}, },

View File

@ -1,5 +0,0 @@
import {syscall} from "./syscall.ts";
export async function publish(event: string, data?: object) {
return await syscall("event.publish", event, data);
}

View File

@ -1,16 +1,16 @@
export function syscall(name: string, ...args: Array<any>): any { export function syscall(name: string, ...args: any[]): any {
let reqId = Math.floor(Math.random() * 1000000); let reqId = Math.floor(Math.random() * 1000000);
// console.log("Syscall", name, reqId); // console.log("Syscall", name, reqId);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
self.dispatchEvent( self.dispatchEvent(
new CustomEvent("syscall", { new CustomEvent("syscall", {
detail: { detail: {
id: reqId, id: reqId,
name: name, name: name,
args: args, args: args,
callback: resolve, callback: resolve,
}, },
}), })
); );
}); });
} }

View File

@ -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",
})),
};
}

View File

@ -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 { 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 { 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 = { type PageMeta = {
name: string; name: string;
lastModified: number; lastModified: number;
@ -21,22 +23,23 @@ fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] }));
fsRouter.get("/", async (context) => { fsRouter.get("/", async (context) => {
const localPath = pagesPath; const localPath = pagesPath;
let fileNames: PageMeta[] = []; let fileNames: PageMeta[] = [];
for await (const dirEntry of Deno.readDir(localPath)) { const markdownFiles = (await recursiveReaddir(localPath)).filter(
if (dirEntry.isFile) { (file: string) => path.extname(file) === ".md"
const stat = await Deno.stat(`${localPath}/${dirEntry.name}`); );
fileNames.push({ for (const p of markdownFiles) {
name: dirEntry.name.substring( const stat = await Deno.stat(p);
0, fileNames.push({
dirEntry.name.length - path.extname(dirEntry.name).length name: p.substring(
), localPath.length + 1,
lastModified: stat.mtime?.getTime()!, p.length - path.extname(p).length
}); ),
} lastModified: stat.mtime?.getTime()!,
});
} }
context.response.body = JSON.stringify(fileNames); context.response.body = JSON.stringify(fileNames);
}); });
fsRouter.get("/:page", async (context) => { fsRouter.get("/:page(.*)", async (context) => {
const pageName = context.params.page; const pageName = context.params.page;
const localPath = `${pagesPath}/${pageName}.md`; const localPath = `${pagesPath}/${pageName}.md`;
try { 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`; const localPath = `${pagesPath}/${context.params.page}.md`;
try { try {
const stat = await Deno.stat(localPath); 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 pageName = context.params.page;
const localPath = `${pagesPath}/${pageName}.md`; const localPath = `${pagesPath}/${pageName}.md`;
const existingPage = await exists(localPath); const existingPage = await exists(localPath);
let dirName = path.dirname(localPath);
if (!(await exists(dirName))) {
await Deno.mkdir(dirName, {
recursive: true,
});
}
let file; let file;
try { try {
file = await Deno.create(localPath); file = await Deno.create(localPath);

View File

@ -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 = { export type ClickEvent = {
pos: number; pos: number;

View File

@ -134,10 +134,17 @@ export class Editor {
} }
// TODO: Parallelize? // TODO: Parallelize?
async dispatchAppEvent(name: AppEvent, data?: any) { async dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]> {
let results: any[] = [];
for (let plugin of this.plugins) { 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 { get currentPage(): PageMeta | undefined {
@ -176,7 +183,7 @@ export class Editor {
closeBrackets(), closeBrackets(),
autocompletion({ autocompletion({
override: [ override: [
this.pageCompleter.bind(this), this.pluginCompleter.bind(this),
this.commandCompleter.bind(this), this.commandCompleter.bind(this),
], ],
}), }),
@ -223,6 +230,14 @@ export class Editor {
return true; return true;
}, },
}, },
{
key: "Ctrl-s",
mac: "Cmd-s",
run: (target): boolean => {
this.save();
return true;
},
},
{ {
key: "Ctrl-.", key: "Ctrl-.",
mac: "Cmd-.", mac: "Cmd-.",
@ -258,18 +273,20 @@ export class Editor {
}); });
} }
pageCompleter(ctx: CompletionContext): CompletionResult | null { async pluginCompleter(
let prefix = ctx.matchBefore(/\[\[[\w\s]*/); ctx: CompletionContext
if (!prefix) { ): Promise<CompletionResult | null> {
return null; 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 { return null;
from: prefix.from + 2,
options: this.viewState.allPages.map((pageMeta) => ({
label: pageMeta.name,
type: "page",
})),
};
} }
commandCompleter(ctx: CompletionContext): CompletionResult | null { commandCompleter(ctx: CompletionContext): CompletionResult | null {

View File

@ -113,14 +113,17 @@ export class Plugin {
return await this.runningFunctions.get(name)!.invoke(args); return await this.runningFunctions.get(name)!.invoke(args);
} }
async dispatchEvent(name: string, data?: any) { async dispatchEvent(name: string, data?: any): Promise<any[]> {
let functionsToSpawn = this.manifest!.events[name]; let functionsToSpawn = this.manifest!.events[name];
if (functionsToSpawn) { if (functionsToSpawn) {
await Promise.all( return await Promise.all(
functionsToSpawn.map(async (functionToSpawn: string) => { functionsToSpawn.map(
await this.invoke(functionToSpawn, [data]); async (functionToSpawn: string) =>
}) await this.invoke(functionToSpawn, [data])
)
); );
} else {
return [];
} }
} }

View File

@ -1,10 +1,8 @@
import { SyscallContext } from "../plugins/runtime";
export default { export default {
"db.put": (ctx: SyscallContext, key: string, value: any) => { "db.put": (key: string, value: any) => {
localStorage.setItem(key, value); localStorage.setItem(key, value);
}, },
"db.get": (ctx: SyscallContext, key: string) => { "db.get": (key: string) => {
return localStorage.getItem(key); return localStorage.getItem(key);
}, },
}; };

View File

@ -9,6 +9,22 @@ type SyntaxNode = {
to: number; 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) => ({ export default (editor: Editor) => ({
"editor.getText": () => { "editor.getText": () => {
return editor.editorView?.state.sliceDoc(); 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 => { "editor.getSyntaxNodeAtPos": (pos: number): SyntaxNode | undefined => {
const editorState = editor.editorView!.state; const editorState = editor.editorView!.state;
let node = syntaxTree(editorState).resolveInner(pos); let node = syntaxTree(editorState).resolveInner(pos);

View File

@ -1,22 +1,16 @@
import { Editor } from "../editor"; import { Editor } from "../editor";
import { SyscallContext } from "../plugins/runtime";
import { PageMeta } from "../types"; import { PageMeta } from "../types";
export default (editor: Editor) => ({ export default (editor: Editor) => ({
"space.listPages": (ctx: SyscallContext): PageMeta[] => { "space.listPages": (): PageMeta[] => {
return editor.viewState.allPages; return editor.viewState.allPages;
}, },
"space.readPage": async ( "space.readPage": async (
ctx: SyscallContext,
name: string name: string
): Promise<{ text: string; meta: PageMeta }> => { ): Promise<{ text: string; meta: PageMeta }> => {
return await editor.fs.readPage(name); return await editor.fs.readPage(name);
}, },
"space.writePage": async ( "space.writePage": async (name: string, text: string): Promise<PageMeta> => {
ctx: SyscallContext,
name: string,
text: string
): Promise<PageMeta> => {
return await editor.fs.writePage(name, text); return await editor.fs.writePage(name, text);
}, },
}); });

View File

@ -1,5 +1,3 @@
import { SyscallContext } from "../plugins/runtime";
// @ts-ignore // @ts-ignore
let frameTest = document.getElementById("main-frame"); let frameTest = document.getElementById("main-frame");
@ -13,7 +11,7 @@ window.addEventListener("message", async (event) => {
}); });
export default { export default {
"ui.update": function (ctx: SyscallContext, doc: any) { "ui.update": function (doc: any) {
// frameTest.contentWindow.postMessage({ // frameTest.contentWindow.postMessage({
// type: "loadContent", // type: "loadContent",
// doc: doc, // doc: doc,