Bunch of stuff

pull/3/head
Zef Hemel 2022-02-28 14:35:51 +01:00
parent 89f93963f5
commit ad37b9ed10
18 changed files with 581 additions and 114 deletions

View File

@ -21,13 +21,41 @@
"invoke": "toggle_h2",
"mac": "Cmd-2",
"key": "Ctrl-2"
},
"Page: Delete": {
"invoke": "deletePage"
},
"Page: Rename": {
"invoke": "renamePage"
},
"Pages: Reindex": {
"invoke": "reindexPages"
},
"Pages: Back Links": {
"invoke": "showBackLinks"
}
},
"events": {
"page:click": ["taskToggle", "clickNavigate"],
"editor:complete": ["pageComplete"]
"editor:complete": ["pageComplete"],
"page:index": ["indexLinks"]
},
"functions": {
"indexLinks": {
"path": "./page.ts:indexLinks"
},
"deletePage": {
"path": "./page.ts:deletePage"
},
"showBackLinks": {
"path": "./page.ts:showBackLinks"
},
"renamePage": {
"path": "./page.ts:renamePage"
},
"reindexPages": {
"path": "./page.ts:reindex"
},
"pageComplete": {
"path": "./navigate.ts:pageComplete"
},

104
plugins/core/page.ts Normal file
View File

@ -0,0 +1,104 @@
import { IndexEvent } from "../../webapp/src/app_event.ts";
import { pageLinkRegex } from "../../webapp/src/constant.ts";
import { syscall } from "./lib/syscall.ts";
const wikilinkRegex = new RegExp(pageLinkRegex, "g");
export async function indexLinks({ name, text }: IndexEvent) {
console.log("Now indexing", name);
let backLinks: { key: string; value: string }[] = [];
for (let match of text.matchAll(wikilinkRegex)) {
let toPage = match[1];
let pos = match.index!;
backLinks.push({
key: `pl:${toPage}:${pos}`,
value: name,
});
}
console.log("Found", backLinks.length, "wiki link(s)");
await syscall("indexer.batchSet", name, backLinks);
}
export async function deletePage() {
let pageMeta = await syscall("editor.getCurrentPage");
console.log("Navigating to start page");
await syscall("editor.navigate", "start");
console.log("Deleting page from space");
await syscall("space.deletePage", pageMeta.name);
console.log("Reloading page list");
await syscall("space.reloadPageList");
}
export async function renamePage() {
const pageMeta = await syscall("editor.getCurrentPage");
const oldName = pageMeta.name;
const newName = await syscall("editor.prompt", `Rename ${oldName} to:`);
if (!newName) {
return;
}
console.log("New name", newName);
let pagesToUpdate = await getBackLinks(oldName);
console.log("All pages containing backlinks", pagesToUpdate);
let text = await syscall("editor.getText");
console.log("Writing new page to space");
await syscall("space.writePage", newName, text);
console.log("Deleting page from space");
await syscall("space.deletePage", oldName);
console.log("Reloading page list");
await syscall("space.reloadPageList");
console.log("Navigating to new page");
await syscall("editor.navigate", newName);
let pageToUpdateSet = new Set<string>();
for (let pageToUpdate of pagesToUpdate) {
pageToUpdateSet.add(pageToUpdate.page);
}
for (let pageToUpdate of pageToUpdateSet) {
console.log("Now going to update links in", pageToUpdate);
let { text } = await syscall("space.readPage", pageToUpdate);
if (!text) {
// Page likely does not exist, but at least we can skip it
continue;
}
let newText = text.replaceAll(`[[${oldName}]]`, `[[${newName}]]`);
if (text !== newText) {
console.log("Changes made, saving...");
await syscall("space.writePage", pageToUpdate, newText);
}
}
}
type BackLink = {
page: string;
pos: number;
};
async function getBackLinks(pageName: string): Promise<BackLink[]> {
let allBackLinks = await syscall(
"indexer.scanPrefixGlobal",
`pl:${pageName}:`
);
let pagesToUpdate: BackLink[] = [];
for (let { key, value } of allBackLinks) {
let keyParts = key.split(":");
pagesToUpdate.push({
page: value,
pos: +keyParts[keyParts.length - 1],
});
}
return pagesToUpdate;
}
export async function showBackLinks() {
const pageMeta = await syscall("editor.getCurrentPage");
let backLinks = await getBackLinks(pageMeta.name);
console.log("Backlinks", backLinks);
}
export async function reindex() {
await syscall("space.reindex");
}

View File

@ -18,7 +18,7 @@ const pagesPath = "../pages";
const fsRouter = new Router();
fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST"] }));
fsRouter.use(oakCors({ methods: ["OPTIONS", "GET", "PUT", "POST", "DELETE"] }));
fsRouter.get("/", async (context) => {
const localPath = pagesPath;
@ -96,6 +96,22 @@ fsRouter.put("/:page(.*)", async (context) => {
context.response.body = "OK";
});
fsRouter.delete("/:page(.*)", async (context) => {
const pageName = context.params.page;
const localPath = `${pagesPath}/${pageName}.md`;
try {
await Deno.remove(localPath);
} catch (e) {
console.error("Error deleting file", localPath, e);
context.response.status = 500;
context.response.body = e.message;
return;
}
console.log("Deleted", localPath);
context.response.body = "OK";
});
const app = new Application();
app.use(
new Router()
@ -109,7 +125,8 @@ app.use(async (context, next) => {
index: "index.html",
});
} catch {
next();
await context.send({ root: "../webapp/dist", path: "index.html" });
// next();
}
});

View File

@ -30,6 +30,7 @@
"@codemirror/state": "^0.19.7",
"@codemirror/view": "^0.19.42",
"@parcel/service-worker": "^2.3.2",
"dexie": "^3.2.1",
"idb": "^7.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"

View File

@ -2,6 +2,7 @@ export type AppEvent =
| "app:ready"
| "page:save"
| "page:click"
| "page:index"
| "editor:complete";
export type ClickEvent = {
@ -10,3 +11,12 @@ export type ClickEvent = {
ctrlKey: boolean;
altKey: boolean;
};
export type IndexEvent = {
name: string;
text: string;
};
export interface AppEventDispatcher {
dispatchAppEvent(name: AppEvent, data?: any): Promise<any[]>;
}

View File

@ -1,21 +1,35 @@
import { PageMeta } from "../types";
import { FilterList } from "./filter";
import { FilterList, Option } from "./filter";
export function PageNavigator({
allPages: allPages,
allPages,
onNavigate,
currentPage,
}: {
allPages: PageMeta[];
onNavigate: (page: string | undefined) => void;
currentPage?: PageMeta;
}) {
let options: Option[] = [];
for (let pageMeta of allPages) {
if (currentPage && currentPage.name == pageMeta.name) {
continue;
}
// Order by last modified date in descending order
let orderId = -pageMeta.lastModified.getTime();
// Unless it was opened and is still in memory
if (pageMeta.lastOpened) {
orderId = -pageMeta.lastOpened.getTime();
}
options.push({
...pageMeta,
orderId: orderId,
});
}
return (
<FilterList
placeholder=""
options={allPages.map((meta) => ({
...meta,
// Order by last modified date in descending order
orderId: -meta.lastModified.getTime(),
}))}
options={options}
allowNew={true}
newHint="Create page"
onSelect={(opt) => {

1
webapp/src/constant.ts Normal file
View File

@ -0,0 +1 @@
export const pageLinkRegex = /\[\[([\w\s\/\:,\.\-]+)\]\]/;

View File

@ -39,6 +39,7 @@ import customMarkdownStyle from "./style";
import dbSyscalls from "./syscalls/db.localstorage";
import { Plugin } from "./plugins/runtime";
import editorSyscalls from "./syscalls/editor.browser";
import indexerSyscalls from "./syscalls/indexer.native";
import spaceSyscalls from "./syscalls/space.native";
import {
Action,
@ -47,8 +48,15 @@ import {
initialViewState,
PageMeta,
} from "./types";
import { AppEvent, ClickEvent } from "./app_event";
import {
AppEvent,
AppEventDispatcher,
ClickEvent,
IndexEvent,
} from "./app_event";
import { safeRun } from "./util";
import { Indexer } from "./indexer";
import { IPageNavigator, PathPageNavigator } from "./navigator";
class PageState {
editorState: EditorState;
@ -64,21 +72,23 @@ class PageState {
const watchInterval = 5000;
export class Editor {
export class Editor implements AppEventDispatcher {
editorView?: EditorView;
viewState: AppViewState;
viewDispatch: React.Dispatch<Action>;
$hashChange?: () => void;
openPages: Map<string, PageState>;
fs: Space;
space: Space;
editorCommands: Map<string, AppCommand>;
plugins: Plugin[];
indexer: Indexer;
navigationResolve?: (val: undefined) => void;
pageNavigator: IPageNavigator;
constructor(fs: Space, parent: Element) {
constructor(space: Space, parent: Element) {
this.editorCommands = new Map();
this.openPages = new Map();
this.plugins = [];
this.fs = fs;
this.space = space;
this.viewState = initialViewState;
this.viewDispatch = () => {};
this.render(parent);
@ -86,16 +96,30 @@ export class Editor {
state: this.createEditorState(""),
parent: document.getElementById("editor")!,
});
this.addListeners();
// this.watch();
this.pageNavigator = new PathPageNavigator();
this.indexer = new Indexer("page-index", space);
this.watch();
}
async init() {
await this.loadPageList();
await this.loadPlugins();
this.$hashChange!();
this.focus();
await this.dispatchAppEvent("app:ready");
this.pageNavigator.subscribe(async (pageName) => {
await this.save();
console.log("Now navigating to", pageName);
if (!this.editorView) {
return;
}
await this.loadPage(pageName);
});
if (this.pageNavigator.getCurrentPage() === "") {
this.pageNavigator.navigate("start");
}
}
async loadPlugins() {
@ -103,7 +127,8 @@ export class Editor {
system.registerSyscalls(
dbSyscalls,
editorSyscalls(this),
spaceSyscalls(this)
spaceSyscalls(this),
indexerSyscalls(this.indexer)
);
await system.bootServiceWorker();
@ -332,41 +357,20 @@ export class Editor {
return null;
}
click(event: MouseEvent, view: EditorView) {
// if (event.metaKey || event.ctrlKey) {
// let coords = view.posAtCoords(event)!;
// let node = syntaxTree(view.state).resolveInner(coords);
// if (node && node.name === "WikiLinkPage") {
// let pageName = view.state.sliceDoc(node.from, node.to);
// this.navigate(pageName);
// }
// if (node && node.name === "TaskMarker") {
// let checkBoxText = view.state.sliceDoc(node.from, node.to);
// if (checkBoxText === "[x]" || checkBoxText === "[X]") {
// view.dispatch({
// changes: { from: node.from, to: node.to, insert: "[ ]" },
// });
// } else {
// view.dispatch({
// changes: { from: node.from, to: node.to, insert: "[x]" },
// });
// }
// }
// return false;
// }
}
async save() {
const editorState = this.editorView!.state;
if (!this.currentPage) {
return;
}
if (this.viewState.isSaved) {
console.log("Page not modified, skipping saving");
return;
}
// Write to file system
let pageMeta = await this.fs.writePage(
this.currentPage.name,
editorState.sliceDoc()
);
let text = editorState.sliceDoc();
let pageMeta = await this.space.writePage(this.currentPage.name, text);
// Update in open page cache
this.openPages.set(
@ -381,10 +385,18 @@ export class Editor {
if (pageMeta.created) {
await this.loadPageList();
}
// Reindex page
await this.indexPage(text, pageMeta);
}
private async indexPage(text: string, pageMeta: PageMeta) {
console.log("Indexing page", pageMeta.name);
this.indexer.indexPage(this, pageMeta, text, true);
}
async loadPageList() {
let pagesMeta = await this.fs.listPages();
let pagesMeta = await this.space.listPages();
this.viewDispatch({
type: "pages-listed",
pages: pagesMeta,
@ -394,63 +406,52 @@ export class Editor {
watch() {
setInterval(() => {
safeRun(async () => {
if (!this.currentPage) {
return;
}
const currentPageName = this.currentPage.name;
let newPageMeta = await this.fs.getPageMeta(currentPageName);
if (
this.currentPage.lastModified.getTime() <
newPageMeta.lastModified.getTime()
) {
console.log("File changed on disk, reloading");
let pageData = await this.fs.readPage(currentPageName);
this.openPages.set(
newPageMeta.name,
new PageState(this.createEditorState(pageData.text), 0, newPageMeta)
);
await this.loadPage(currentPageName);
if (this.currentPage && this.viewState.isSaved) {
await this.checkForNewVersion(this.currentPage);
}
});
}, watchInterval);
}
async checkForNewVersion(cachedMeta: PageMeta) {
const currentPageName = cachedMeta.name;
let newPageMeta = await this.space.getPageMeta(currentPageName);
if (
cachedMeta.lastModified.getTime() !== newPageMeta.lastModified.getTime()
) {
console.log("File changed on disk, reloading");
let pageData = await this.space.readPage(currentPageName);
this.openPages.set(
newPageMeta.name,
new PageState(this.createEditorState(pageData.text), 0, newPageMeta)
);
await this.loadPage(currentPageName);
}
}
focus() {
this.editorView!.focus();
}
async navigate(name: string) {
location.hash = encodeURIComponent(name);
}
hashChange() {
Promise.resolve()
.then(async () => {
await this.save();
const pageName = decodeURIComponent(location.hash.substring(1));
console.log("Now navigating to", pageName);
if (!this.editorView) {
return;
}
await this.loadPage(pageName);
})
.catch((e) => {
console.error(e);
});
await this.pageNavigator.navigate(name);
}
async loadPage(pageName: string) {
let pageState = this.openPages.get(pageName);
if (!pageState) {
let pageData = await this.fs.readPage(pageName);
let pageData = await this.space.readPage(pageName);
pageState = new PageState(
this.createEditorState(pageData.text),
0,
pageData.meta
);
this.openPages.set(pageName, pageState!);
} else {
// Loaded page from in-mory cache, let's async see if this page hasn't been updated
this.checkForNewVersion(pageState.meta).catch((e) => {
console.error("Failed to check for new version");
});
}
this.editorView!.setState(pageState!.editorState);
this.editorView!.scrollDOM.scrollTop = pageState!.scrollTop;
@ -459,16 +460,15 @@ export class Editor {
type: "page-loaded",
meta: pageState.meta,
});
}
addListeners() {
this.$hashChange = this.hashChange.bind(this);
window.addEventListener("hashchange", this.$hashChange);
}
dispose() {
if (this.$hashChange) {
window.removeEventListener("hashchange", this.$hashChange);
let indexerPageMeta = await this.indexer.getPageIndexPageMeta(pageName);
if (
(indexerPageMeta &&
pageState.meta.lastModified.getTime() !==
indexerPageMeta.lastModified.getTime()) ||
!indexerPageMeta
) {
await this.indexPage(pageState.editorState.sliceDoc(), pageState.meta);
}
}
@ -477,12 +477,6 @@ export class Editor {
this.viewState = viewState;
this.viewDispatch = dispatch;
useEffect(() => {
if (!location.hash) {
this.navigate("start");
}
}, []);
// Auto save
useEffect(() => {
const id = setTimeout(() => {
@ -508,18 +502,14 @@ export class Editor {
{viewState.showPageNavigator && (
<PageNavigator
allPages={viewState.allPages}
currentPage={this.currentPage}
onNavigate={(page) => {
dispatch({ type: "stop-navigate" });
editor!.focus();
editor.focus();
if (page) {
editor
?.save()
.then(() => {
editor!.navigate(page);
})
.catch((e) => {
alert("Could not save page, not switching");
});
safeRun(async () => {
editor.navigate(page);
});
}
}}
/>

155
webapp/src/indexer.ts Normal file
View File

@ -0,0 +1,155 @@
import { Dexie, Table } from "dexie";
import { AppEventDispatcher, IndexEvent } from "./app_event";
import { Space } from "./space";
import { PageMeta } from "./types";
function constructKey(pageName: string, key: string): string {
return `${pageName}:${key}`;
}
function cleanKey(pageName: string, fromKey: string): string {
return fromKey.substring(pageName.length + 1);
}
export type KV = {
key: string;
value: any;
};
export class Indexer {
db: Dexie;
pageIndex: Table;
space: Space;
constructor(name: string, space: Space) {
this.db = new Dexie(name);
this.space = space;
this.db.version(1).stores({
pageIndex: "ck, page, key",
});
this.pageIndex = this.db.table("pageIndex");
}
async clearPageIndexForPage(pageName: string) {
await this.pageIndex.where({ page: pageName }).delete();
}
async clearPageIndex() {
await this.pageIndex.clear();
}
async setPageIndexPageMeta(pageName: string, meta: PageMeta) {
await this.set(pageName, "$meta", {
lastModified: meta.lastModified.getTime(),
});
}
async getPageIndexPageMeta(pageName: string): Promise<PageMeta | null> {
let meta = await this.get(pageName, "$meta");
if (meta) {
return {
name: pageName,
lastModified: new Date(meta.lastModified),
};
} else {
return null;
}
}
async indexPage(
appEventDispatcher: AppEventDispatcher,
pageMeta: PageMeta,
text: string,
withFlush: boolean
) {
if (withFlush) {
await this.clearPageIndexForPage(pageMeta.name);
}
let indexEvent: IndexEvent = {
name: pageMeta.name,
text,
};
await appEventDispatcher.dispatchAppEvent("page:index", indexEvent);
await this.setPageIndexPageMeta(pageMeta.name, pageMeta);
}
async reindexSpace(space: Space, appEventDispatcher: AppEventDispatcher) {
await this.clearPageIndex();
let allPages = await space.listPages();
// TODO: Parallelize?
for (let page of allPages) {
let pageData = await space.readPage(page.name);
await this.indexPage(
appEventDispatcher,
pageData.meta,
pageData.text,
false
);
}
}
async set(pageName: string, key: string, value: any) {
await this.pageIndex.put({
ck: constructKey(pageName, key),
page: pageName,
key: key,
value: value,
});
}
async batchSet(pageName: string, kvs: KV[]) {
await this.pageIndex.bulkPut(
kvs.map(({ key, value }) => ({
ck: constructKey(pageName, key),
key: key,
page: pageName,
value: value,
}))
);
}
async get(pageName: string, key: string): Promise<any | null> {
let result = await this.pageIndex.get({
ck: constructKey(pageName, key),
});
return result ? result.value : null;
}
async scanPrefixForPage(
pageName: string,
keyPrefix: string
): Promise<{ key: string; value: any }[]> {
let results = await this.pageIndex
.where("ck")
.startsWith(constructKey(pageName, keyPrefix))
.toArray();
return results.map((result) => ({
key: cleanKey(pageName, result.key),
value: result.value,
}));
}
async scanPrefixGlobal(
keyPrefix: string
): Promise<{ key: string; value: any }[]> {
let results = await this.pageIndex
.where("key")
.startsWith(keyPrefix)
.toArray();
return results.map((result) => ({
key: result.key,
value: result.value,
}));
}
async deletePrefixForPage(pageName: string, keyPrefix: string) {
await this.pageIndex
.where("ck")
.startsWith(constructKey(pageName, keyPrefix))
.delete();
}
async delete(pageName: string, key: string) {
await this.pageIndex.delete(constructKey(pageName, key));
}
}

71
webapp/src/navigator.ts Normal file
View File

@ -0,0 +1,71 @@
import { safeRun } from "./util";
export interface IPageNavigator {
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void;
navigate(page: string): void;
getCurrentPage(): string;
}
function encodePageUrl(name: string): string {
return name.replaceAll(" ", "_");
}
function decodePageUrl(url: string): string {
return url.replaceAll("_", " ");
}
export class PathPageNavigator implements IPageNavigator {
navigationResolve?: (value: undefined) => void;
async navigate(page: string) {
console.log("Pushing state", page);
window.history.pushState({ page: page }, page, `/${encodePageUrl(page)}`);
window.dispatchEvent(new PopStateEvent("popstate"));
await new Promise<undefined>((resolve) => {
this.navigationResolve = resolve;
});
this.navigationResolve = undefined;
}
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
const cb = () => {
console.log("State popped", this.getCurrentPage());
safeRun(async () => {
await pageLoadCallback(this.getCurrentPage());
if (this.navigationResolve) {
this.navigationResolve(undefined);
}
});
};
window.addEventListener("popstate", cb);
cb();
}
getCurrentPage(): string {
return decodePageUrl(location.pathname.substring(1));
}
}
export class HashPageNavigator implements IPageNavigator {
navigationResolve?: (value: undefined) => void;
async navigate(page: string) {
location.hash = encodePageUrl(page);
await new Promise<undefined>((resolve) => {
this.navigationResolve = resolve;
});
this.navigationResolve = undefined;
}
subscribe(pageLoadCallback: (pageName: string) => Promise<void>): void {
const cb = () => {
safeRun(async () => {
await pageLoadCallback(this.getCurrentPage());
if (this.navigationResolve) {
this.navigationResolve(undefined);
}
});
};
window.addEventListener("hashchange", cb);
cb();
}
getCurrentPage(): string {
return decodePageUrl(location.hash.substring(1));
}
}

View File

@ -2,6 +2,11 @@ import { styleTags } from "@codemirror/highlight";
import { MarkdownConfig, TaskList } from "@lezer/markdown";
import { commonmark, mkLang } from "./markdown/markdown";
import * as ct from "./customtags";
import { pageLinkRegex } from "./constant";
const pageLinkRegexPrefix = new RegExp(
"^" + pageLinkRegex.toString().slice(1, -1)
);
const WikiLink: MarkdownConfig = {
defineNodes: ["WikiLink", "WikiLinkPage"],
@ -12,13 +17,13 @@ const WikiLink: MarkdownConfig = {
let match: RegExpMatchArray | null;
if (
next != 91 /* '[' */ ||
!(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))
!(match = pageLinkRegexPrefix.exec(cx.slice(pos, cx.end)))
) {
return -1;
}
return cx.addElement(
cx.elt("WikiLink", pos, pos + match[0].length + 1, [
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 1),
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 2),
])
);
},

View File

@ -9,6 +9,11 @@ export default function reducer(
case "page-loaded":
return {
...state,
allPages: state.allPages.map((pageMeta) =>
pageMeta.name === action.meta.name
? { ...pageMeta, lastOpened: new Date() }
: pageMeta
),
currentPage: action.meta,
isSaved: true,
};

View File

@ -4,6 +4,7 @@ export interface Space {
listPages(): Promise<PageMeta[]>;
readPage(name: string): Promise<{ text: string; meta: PageMeta }>;
writePage(name: string, text: string): Promise<PageMeta>;
deletePage(name: string): Promise<void>;
getPageMeta(name: string): Promise<PageMeta>;
}
@ -12,6 +13,7 @@ export class HttpRemoteSpace implements Space {
constructor(url: string) {
this.url = url;
}
async listPages(): Promise<PageMeta[]> {
let req = await fetch(this.url, {
method: "GET",
@ -22,6 +24,7 @@ export class HttpRemoteSpace implements Space {
lastModified: new Date(meta.lastModified),
}));
}
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
let req = await fetch(`${this.url}/${name}`, {
method: "GET",
@ -34,6 +37,7 @@ export class HttpRemoteSpace implements Space {
},
};
}
async writePage(name: string, text: string): Promise<PageMeta> {
let req = await fetch(`${this.url}/${name}`, {
method: "PUT",
@ -47,6 +51,15 @@ export class HttpRemoteSpace implements Space {
};
}
async deletePage(name: string): Promise<void> {
let req = await fetch(`${this.url}/${name}`, {
method: "DELETE",
});
if (req.status !== 200) {
throw Error(`Failed to delete page: ${req.statusText}`);
}
}
async getPageMeta(name: string): Promise<PageMeta> {
let req = await fetch(`${this.url}/${name}`, {
method: "OPTIONS",

View File

@ -1,6 +1,7 @@
import { Editor } from "../editor";
import { syntaxTree } from "@codemirror/language";
import { Transaction } from "@codemirror/state";
import { PageMeta } from "../types";
type SyntaxNode = {
name: string;
@ -26,6 +27,9 @@ function ensureAnchor(expr: any, start: boolean) {
}
export default (editor: Editor) => ({
"editor.getCurrentPage": (): PageMeta => {
return editor.currentPage!;
},
"editor.getText": () => {
return editor.editorView?.state.sliceDoc();
},
@ -120,4 +124,7 @@ export default (editor: Editor) => ({
"editor.dispatch": (change: Transaction) => {
editor.editorView!.dispatch(change);
},
"editor.prompt": (message: string): string | null => {
return prompt(message);
},
});

View File

@ -0,0 +1,22 @@
import { Indexer, KV } from "../indexer";
export default (indexer: Indexer) => ({
"indexer.scanPrefixForPage": async (pageName: string, keyPrefix: string) => {
return await indexer.scanPrefixForPage(pageName, keyPrefix);
},
"indexer.scanPrefixGlobal": async (keyPrefix: string) => {
return await indexer.scanPrefixGlobal(keyPrefix);
},
"indexer.get": async (pageName: string, key: string): Promise<any> => {
return await indexer.get(pageName, key);
},
"indexer.set": async (pageName: string, key: string, value: any) => {
await indexer.set(pageName, key, value);
},
"indexer.batchSet": async (pageName: string, kvs: KV[]) => {
await indexer.batchSet(pageName, kvs);
},
"indexer.delete": async (pageName: string, key: string) => {
await indexer.delete(pageName, key);
},
});

View File

@ -5,12 +5,30 @@ export default (editor: Editor) => ({
"space.listPages": (): PageMeta[] => {
return editor.viewState.allPages;
},
"space.reloadPageList": async () => {
await editor.loadPageList();
},
"space.reindex": async () => {
await editor.indexer.reindexSpace(editor.space, editor);
},
"space.readPage": async (
name: string
): Promise<{ text: string; meta: PageMeta }> => {
return await editor.fs.readPage(name);
return await editor.space.readPage(name);
},
"space.writePage": async (name: string, text: string): Promise<PageMeta> => {
return await editor.fs.writePage(name, text);
return await editor.space.writePage(name, text);
},
"space.deletePage": async (name: string) => {
console.log("Clearing page index", name);
await editor.indexer.clearPageIndexForPage(name);
// If we're deleting the current page, navigate to the start page
if (editor.currentPage?.name === name) {
await editor.navigate("start");
}
// Remove page from open pages in editor
editor.openPages.delete(name);
console.log("Deleting page");
await editor.space.deletePage(name);
},
});

View File

@ -4,6 +4,7 @@ export type PageMeta = {
name: string;
lastModified: Date;
created?: boolean;
lastOpened?: Date;
};
export type AppCommand = {

View File

@ -1186,6 +1186,11 @@ detect-libc@^1.0.3:
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
dexie@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.1.tgz#ef21456d725e700c1ab7ac4307896e4fdabaf753"
integrity sha512-Y8oz3t2XC9hvjkP35B5I8rUkKKwM36GGRjWQCMjzIYScg7W+GHKDXobSYswkisW7CxL1/tKQtggMDsiWqDUc1g==
dom-serializer@^1.0.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"