From 799144a74e58160e5ea0eac7c1fc436674f78b50 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 30 Aug 2022 10:44:20 +0200 Subject: [PATCH] Anchors --- .../plugos-silverbullet-syscall/editor.ts | 2 +- packages/plugs/core/anchor.ts | 50 +++++++++++++++++++ packages/plugs/core/core.plug.yaml | 15 ++++++ packages/plugs/core/navigate.ts | 11 +++- packages/plugs/core/page.ts | 2 +- packages/web/editor.tsx | 28 +++++++++-- packages/web/navigator.ts | 25 +++++----- packages/web/styles/theme.scss | 5 +- packages/web/syscalls/editor.ts | 2 +- website/CHANGELOG.md | 5 +- 10 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 packages/plugs/core/anchor.ts diff --git a/packages/plugos-silverbullet-syscall/editor.ts b/packages/plugos-silverbullet-syscall/editor.ts index 19438936..d8337496 100644 --- a/packages/plugos-silverbullet-syscall/editor.ts +++ b/packages/plugos-silverbullet-syscall/editor.ts @@ -31,7 +31,7 @@ export function save(): Promise { export function navigate( name: string, - pos?: number, + pos?: string | number, replaceState = false ): Promise { return syscall("editor.navigate", name, pos, replaceState); diff --git a/packages/plugs/core/anchor.ts b/packages/plugs/core/anchor.ts new file mode 100644 index 00000000..9de315c8 --- /dev/null +++ b/packages/plugs/core/anchor.ts @@ -0,0 +1,50 @@ +import { collectNodesOfType, traverseTree } from "@silverbulletmd/common/tree"; +import { + batchSet, + queryPrefix, +} from "@silverbulletmd/plugos-silverbullet-syscall"; +import { + getCurrentPage, + matchBefore, +} from "@silverbulletmd/plugos-silverbullet-syscall/editor"; +import type { IndexTreeEvent } from "@silverbulletmd/web/app_event"; +import { applyQuery, QueryProviderEvent } from "../query/engine"; +import { removeQueries } from "../query/util"; + +// Key space +// a:pageName:anchorName => pos + +export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) { + removeQueries(tree); + let anchors: { key: string; value: string }[] = []; + + collectNodesOfType(tree, "NamedAnchor").forEach((n) => { + let aName = n.children![0].text!; + anchors.push({ + key: `a:${pageName}:${aName}`, + value: "" + n.from, + }); + }); + console.log("Found", anchors.length, "anchors(s)"); + await batchSet(pageName, anchors); +} + +export async function anchorComplete() { + let prefix = await matchBefore("\\[\\[[^\\]@]*@[\\w\\.\\-\\/]*"); + if (!prefix) { + return null; + } + const [pageRefPrefix, anchorRef] = prefix.text.split("@"); + let pageRef = pageRefPrefix.substring(2); + if (!pageRef) { + pageRef = await getCurrentPage(); + } + let allAnchors = await queryPrefix(`a:${pageRef}:@${anchorRef}`); + return { + from: prefix.from + pageRefPrefix.length + 1, + options: allAnchors.map((a) => ({ + label: a.key.split("@")[1], + type: "anchor", + })), + }; +} diff --git a/packages/plugs/core/core.plug.yaml b/packages/plugs/core/core.plug.yaml index 8a88d440..f3b6d1ea 100644 --- a/packages/plugs/core/core.plug.yaml +++ b/packages/plugs/core/core.plug.yaml @@ -15,6 +15,11 @@ syntax: - "{" regex: "\\{\\[[^\\]]+\\]\\}" className: sb-command-link + NamedAnchor: + firstCharacters: + - "@" + regex: "@[a-zA-Z\\.\\-\\/]+[\\w\\.\\-\\/]*" + className: sb-named-anchor functions: clearPageIndex: path: "./page.ts:clearPageIndex" @@ -111,6 +116,16 @@ functions: events: - query:tag + # Anchors + indexAnchors: + path: "./anchor.ts:indexAnchors" + events: + - page:index + anchorComplete: + path: "./anchor.ts:anchorComplete" + events: + - page:complete + # Full text search searchIndex: path: ./search.ts:index diff --git a/packages/plugs/core/navigate.ts b/packages/plugs/core/navigate.ts index 88f4c954..4e7d6404 100644 --- a/packages/plugs/core/navigate.ts +++ b/packages/plugs/core/navigate.ts @@ -1,6 +1,7 @@ import type { ClickEvent } from "@silverbulletmd/web/app_event"; import { flashNotification, + getCurrentPage, getCursor, getText, navigate as navigateTo, @@ -18,11 +19,17 @@ async function actionClickOrActionEnter(mdTree: ParseTree | null) { switch (mdTree.type) { case "WikiLinkPage": let pageLink = mdTree.children![0].text!; - let pos = "0"; + let pos; if (pageLink.includes("@")) { [pageLink, pos] = pageLink.split("@"); + if (pos.match(/^\d+$/)) { + pos = +pos; + } } - await navigateTo(pageLink, +pos); + if (!pageLink) { + pageLink = await getCurrentPage(); + } + await navigateTo(pageLink, pos); break; case "URL": case "NakedURL": diff --git a/packages/plugs/core/page.ts b/packages/plugs/core/page.ts index 7278859f..55a749ef 100644 --- a/packages/plugs/core/page.ts +++ b/packages/plugs/core/page.ts @@ -206,7 +206,7 @@ export async function reindexCommand() { // Completion export async function pageComplete() { - let prefix = await matchBefore("\\[\\[[^\\]]*"); + let prefix = await matchBefore("\\[\\[[^\\]@]*"); if (!prefix) { return null; } diff --git a/packages/web/editor.tsx b/packages/web/editor.tsx index c08d3f04..f8a4f2df 100644 --- a/packages/web/editor.tsx +++ b/packages/web/editor.tsx @@ -203,8 +203,8 @@ export class Editor { async init() { this.focus(); - this.pageNavigator.subscribe(async (pageName, pos) => { - console.log("Now navigating to", pageName); + this.pageNavigator.subscribe(async (pageName, pos: number | string) => { + console.log("Now navigating to", pageName, pos); if (!this.editorView) { return; @@ -212,8 +212,30 @@ export class Editor { await this.loadPage(pageName); if (pos) { + if (typeof pos === "string") { + console.log("Navigating to anchor", pos); + + // We're going to look up the anchor through a direct page store query... + // TODO: This is a bit hacky, but it works for now + let posLookup = await this.system.syscallWithContext( + // Mock the "core" plug + { plug: { name: "core" } as any }, + "index.get", + [pageName, `a:${pageName}:@${pos}`] + ); + + if (!posLookup) { + return this.flashNotification( + `Could not find anchor @${pos}`, + "error" + ); + } else { + pos = +posLookup; + } + } this.editorView.dispatch({ selection: { anchor: pos }, + scrollIntoView: true, }); } }); @@ -562,7 +584,7 @@ export class Editor { this.editorView!.focus(); } - async navigate(name: string, pos?: number, replaceState = false) { + async navigate(name: string, pos?: number | string, replaceState = false) { if (!name) { name = this.indexPage; } diff --git a/packages/web/navigator.ts b/packages/web/navigator.ts index 265ad84f..110dc634 100644 --- a/packages/web/navigator.ts +++ b/packages/web/navigator.ts @@ -13,7 +13,7 @@ export class PathPageNavigator { constructor(readonly indexPage: string, readonly root: string = "") {} - async navigate(page: string, pos?: number, replaceState = false) { + async navigate(page: string, pos?: number | string, replaceState = false) { let encodedPage = encodePageUrl(page); if (page === this.indexPage) { encodedPage = ""; @@ -43,7 +43,7 @@ export class PathPageNavigator { } subscribe( - pageLoadCallback: (pageName: string, pos: number) => Promise + pageLoadCallback: (pageName: string, pos: number | string) => Promise ): void { const cb = (event?: PopStateEvent) => { const gotoPage = this.getCurrentPage(); @@ -53,7 +53,7 @@ export class PathPageNavigator { safeRun(async () => { await pageLoadCallback( this.getCurrentPage(), - event?.state && event.state.pos + event?.state?.pos || this.getCurrentPos() ); if (this.navigationResolve) { this.navigationResolve(); @@ -64,17 +64,18 @@ export class PathPageNavigator { cb(); } - decodeURI(): [string, number] { - let parts = decodeURI( + decodeURI(): [string, number | string] { + let [page, pos] = decodeURI( location.pathname.substring(this.root.length + 1) ).split("@"); - let page = - parts.length > 1 ? parts.slice(0, parts.length - 1).join("@") : parts[0]; - let pos = parts.length > 1 ? parts[parts.length - 1] : "0"; - if (pos.match(/^\d+$/)) { - return [page, +pos]; + if (pos) { + if (pos.match(/^\d+$/)) { + return [page, +pos]; + } else { + return [page, pos]; + } } else { - return [`${page}@${pos}`, 0]; + return [page, 0]; } } @@ -82,7 +83,7 @@ export class PathPageNavigator { return decodePageUrl(this.decodeURI()[0]) || this.indexPage; } - getCurrentPos(): number { + getCurrentPos(): number | string { // console.log("Pos", this.decodeURI()[1]); return this.decodeURI()[1]; } diff --git a/packages/web/styles/theme.scss b/packages/web/styles/theme.scss index 8d34d385..f2cf8efa 100644 --- a/packages/web/styles/theme.scss +++ b/packages/web/styles/theme.scss @@ -141,10 +141,13 @@ .sb-naked-url { color: #0330cb; - text-decoration: underline; cursor: pointer; } +.sb-named-anchor { + color: #959595; +} + .sb-command-link { background-color: #e3dfdf; cursor: pointer; diff --git a/packages/web/syscalls/editor.ts b/packages/web/syscalls/editor.ts index 57708078..92f124d9 100644 --- a/packages/web/syscalls/editor.ts +++ b/packages/web/syscalls/editor.ts @@ -46,7 +46,7 @@ export function editorSyscalls(editor: Editor): SysCallMapping { "editor.navigate": async ( ctx, name: string, - pos: number, + pos: number | string, replaceState = false ) => { await editor.navigate(name, pos, replaceState); diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index b833dd0f..f9de47e6 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -1,10 +1,11 @@ An attempt at documenting of the changes/new features introduced in each (pre) release. ## 0.0.32 -* Inline image previews are now implemented, use the standard `![alt text](https://url.com/image.jpg)` notation and a preview of the image will appear automatically. Example: +* **Inline image previews**: use the standard `![alt text](https://url.com/image.jpg)` notation and a preview of the image will appear automatically. Example: ![Inline image preview](https://user-images.githubusercontent.com/812886/186218876-6d8a4a71-af8b-4e9e-83eb-4ac89607a6b4.png) -* Dark mode! Toggle between the dark and light mode using a new button, top-right. +* **Dark mode**. Toggle between the dark and light mode using a new button, top-right. ![Dark mode screenshot](https://user-images.githubusercontent.com/6335792/187000151-ba06ce55-ad27-494b-bfe9-6b19ef62145b.png) +* **Named anchors** and references, create an anchor with the new @anchor notation (anywhere on a page), then reference it locally via [[@anchor]] or cross page via [[CHANGELOG@anchor]]. ## 0.0.31 * Update to the query language: the `render` clause now uses page reference syntax `[[page]]`. For example `render [[template/task]]` rather than `render "template/task"`. The old syntax still works, but is deprecated, completion for the old syntax has been removed.