diff --git a/common/space_index.ts b/common/space_index.ts index 954962e..239ddac 100644 --- a/common/space_index.ts +++ b/common/space_index.ts @@ -4,7 +4,7 @@ import { System } from "../plugos/system.ts"; const indexVersionKey = ["$indexVersion"]; // Bump this one every time a full reinxex is needed -const desiredIndexVersion = 3; +const desiredIndexVersion = 4; let indexOngoing = false; diff --git a/plug-api/lib/page.test.ts b/plug-api/lib/page.test.ts index c18373e..5db7159 100644 --- a/plug-api/lib/page.test.ts +++ b/plug-api/lib/page.test.ts @@ -7,6 +7,10 @@ Deno.test("Page utility functions", () => { assertEquals(parsePageRef("[[foo]]"), { page: "foo" }); assertEquals(parsePageRef("foo@1"), { page: "foo", pos: 1 }); assertEquals(parsePageRef("foo$bar"), { page: "foo", anchor: "bar" }); + assertEquals(parsePageRef("foo#My header"), { + page: "foo", + header: "My header", + }); assertEquals(parsePageRef("foo$bar@1"), { page: "foo", anchor: "bar", @@ -21,4 +25,5 @@ Deno.test("Page utility functions", () => { assertEquals(encodePageRef({ page: "foo" }), "foo"); assertEquals(encodePageRef({ page: "foo", pos: 10 }), "foo@10"); assertEquals(encodePageRef({ page: "foo", anchor: "bar" }), "foo$bar"); + assertEquals(encodePageRef({ page: "foo", header: "bar" }), "foo#bar"); }); diff --git a/plug-api/lib/page.ts b/plug-api/lib/page.ts index 0416f0a..be3ec93 100644 --- a/plug-api/lib/page.ts +++ b/plug-api/lib/page.ts @@ -15,11 +15,13 @@ export type PageRef = { page: string; pos?: number; anchor?: string; + header?: string; }; const posRegex = /@(\d+)$/; // Should be kept in sync with the regex in index.plug.yaml const anchorRegex = /\$([a-zA-Z\.\-\/]+[\w\.\-\/]*)$/; +const headerRegex = /#([^#]*)$/; export function parsePageRef(name: string): PageRef { // Normalize the page name @@ -37,6 +39,11 @@ export function parsePageRef(name: string): PageRef { pageRef.anchor = anchorMatch[1]; pageRef.page = pageRef.page.replace(anchorRegex, ""); } + const headerMatch = pageRef.page.match(headerRegex); + if (headerMatch) { + pageRef.header = headerMatch[1]; + pageRef.page = pageRef.page.replace(headerRegex, ""); + } return pageRef; } @@ -48,5 +55,8 @@ export function encodePageRef(pageRef: PageRef): string { if (pageRef.anchor) { name += `$${pageRef.anchor}`; } + if (pageRef.header) { + name += `#${pageRef.header}`; + } return name; } diff --git a/plugos/deps.ts b/plugos/deps.ts index 539e52d..8f9751b 100644 --- a/plugos/deps.ts +++ b/plugos/deps.ts @@ -5,6 +5,6 @@ export { expandGlobSync } from "https://deno.land/std@0.165.0/fs/mod.ts"; export { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts"; export { default as cacheDir } from "https://deno.land/x/cache_dir@0.2.0/mod.ts"; export * as flags from "https://deno.land/std@0.165.0/flags/mod.ts"; -export * as esbuild from "https://deno.land/x/esbuild@v0.19.2/mod.js"; -export { denoPlugins } from "https://deno.land/x/esbuild_deno_loader@0.8.2/mod.ts"; +export * as esbuild from "https://deno.land/x/esbuild@v0.19.12/mod.js"; +export { denoPlugins } from "https://deno.land/x/esbuild_deno_loader@0.8.5/mod.ts"; export * as YAML from "https://deno.land/std@0.184.0/yaml/mod.ts"; diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index 2d8dc1b..5b8a74c 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -5,7 +5,7 @@ import { queryObjects } from "../index/plug_api.ts"; // Completion export async function pageComplete(completeEvent: CompleteEvent) { - const match = /\[\[([^\]@$:\{}]*)$/.exec(completeEvent.linePrefix); + const match = /\[\[([^\]@$#:\{}]*)$/.exec(completeEvent.linePrefix); if (!match) { return null; } diff --git a/plugs/index/attributes.ts b/plugs/index/attributes.ts index 8df715b..c243a2a 100644 --- a/plugs/index/attributes.ts +++ b/plugs/index/attributes.ts @@ -51,7 +51,7 @@ export async function objectAttributeCompleter( distinct: true, select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, { name: "readOnly", - }], + }, { name: "tagName" }], }, 5); return allAttributes.map((value) => { return { diff --git a/plugs/index/builtins.ts b/plugs/index/builtins.ts index 03b80f4..7248fd5 100644 --- a/plugs/index/builtins.ts +++ b/plugs/index/builtins.ts @@ -70,6 +70,13 @@ export const builtins: Record> = { alias: "!string", asTemplate: "!boolean", }, + header: { + ref: "!string", + name: "!string", + page: "!string", + level: "!number", + pos: "!number", + }, paragraph: { text: "!string", page: "!string", diff --git a/plugs/index/header.ts b/plugs/index/header.ts new file mode 100644 index 0000000..2adf33b --- /dev/null +++ b/plugs/index/header.ts @@ -0,0 +1,59 @@ +import { collectNodesMatching } from "$sb/lib/tree.ts"; +import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts"; +import { ObjectValue } from "$sb/types.ts"; +import { indexObjects, queryObjects } from "./api.ts"; +import { parsePageRef } from "$sb/lib/page.ts"; + +type HeaderObject = ObjectValue<{ + name: string; + page: string; + level: number; + pos: number; +}>; + +export async function indexHeaders({ name: pageName, tree }: IndexTreeEvent) { + const headers: ObjectValue[] = []; + + collectNodesMatching(tree, (t) => !!t.type?.startsWith("ATXHeading")).forEach( + (n) => { + const level = +n.type!.substring("ATXHeading".length); + const name = n.children![1].text!.trim(); + headers.push({ + ref: `${pageName}#${name}@${n.from}`, + tag: "header", + level, + name, + page: pageName, + pos: n.from!, + }); + }, + ); + console.log("Found", headers.length, "headers(s)"); + await indexObjects(pageName, headers); +} + +export async function headerComplete(completeEvent: CompleteEvent) { + const match = /\[\[([^\]$:#]*#.*)$/.exec( + completeEvent.linePrefix, + ); + if (!match) { + return null; + } + + const pageRef = parsePageRef(match[1]).page; + const allHeaders = await queryObjects("header", { + filter: ["=", ["attr", "page"], [ + "string", + pageRef || completeEvent.pageName, + ]], + }, 5); + return { + from: completeEvent.pos - match[1].length, + options: allHeaders.map((a) => ({ + label: a.page === completeEvent.pageName + ? `#${a.name}` + : a.ref.split("@")[0], + type: "header", + })), + }; +} diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index a72cd97..5010673 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -108,7 +108,17 @@ functions: path: "./anchor.ts:anchorComplete" events: - editor:complete - + + # Headers + indexHeaders: + path: header.ts:indexHeaders + events: + - page:index + headerComplete: + path: header.ts:headerComplete + events: + - editor:complete + # Data indexData: path: data.ts:indexData diff --git a/plugs/index/tags.ts b/plugs/index/tags.ts index 3f22d69..9784192 100644 --- a/plugs/index/tags.ts +++ b/plugs/index/tags.ts @@ -54,6 +54,11 @@ const taskPrefixRegex = /^\s*[\-\*]\s+\[([^\]]+)\]/; const itemPrefixRegex = /^\s*[\-\*]\s+/; export async function tagComplete(completeEvent: CompleteEvent) { + const inLinkMatch = /\[\[([^\]]*)$/.exec(completeEvent.linePrefix); + if (inLinkMatch) { + return null; + } + const match = /#[^#\s]+$/.exec(completeEvent.linePrefix); if (!match) { return null; diff --git a/web/client.ts b/web/client.ts index c7cb057..6dd8c01 100644 --- a/web/client.ts +++ b/web/client.ts @@ -320,7 +320,8 @@ export class Client { if ( pageState.scrollTop !== undefined && !(pageState.scrollTop === 0 && - (pageState.pos !== undefined || pageState.anchor !== undefined)) + (pageState.pos !== undefined || pageState.anchor !== undefined || + pageState.header !== undefined)) ) { setTimeout(() => { console.log("Kicking off scroll to", pageState.scrollTop); @@ -330,7 +331,10 @@ export class Client { } // Was a particular cursor/selection set? - if (pageState.selection?.anchor && !pageState.pos && !pageState.anchor) { // Only do this if we got a specific cursor position + if ( + pageState.selection?.anchor && !pageState.pos && !pageState.anchor && + !pageState.header + ) { // Only do this if we got a specific cursor position console.log("Changing cursor position to", pageState.selection); this.editorView.dispatch({ selection: pageState.selection, @@ -344,6 +348,7 @@ export class Client { console.log("Navigating to anchor", pageState.anchor); const pageText = this.editorView.state.sliceDoc(); + // This is somewhat of a simplistic way to find the anchor, but it works for now pos = pageText.indexOf(`$${pageState.anchor}`); if (pos === -1) { @@ -355,6 +360,22 @@ export class Client { adjustedPosition = true; } + if (pageState.header) { + console.log("Navigating to header", pageState.header); + const pageText = this.editorView.state.sliceDoc(); + + // This is somewhat of a simplistic way to find the header, but it works for now + pos = pageText.indexOf(`# ${pageState.header}\n`) + 2; + + if (pos === -1) { + return this.flashNotification( + `Could not find header "${pageState.header}"`, + "error", + ); + } + + adjustedPosition = true; + } if (pos !== undefined) { // setTimeout(() => { console.log("Doing this pos set to", pos); diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index fb15c82..64f576f 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -162,7 +162,6 @@ export class MarkdownWidget extends WidgetType { div.querySelectorAll("span.hashtag").forEach((el_) => { const el = el_ as HTMLElement; // Override default click behavior with a local navigate (faster) - console.log("Found hashtag", el.innerText); el.addEventListener("click", (e) => { console.log("Hashtag clicked", el.innerText); if (e.ctrlKey || e.metaKey) { diff --git a/web/navigator.ts b/web/navigator.ts index 87740b0..0c3f97e 100644 --- a/web/navigator.ts +++ b/web/navigator.ts @@ -146,5 +146,9 @@ export function parsePageRefFromURI(): PageRef { location.pathname.substring(1), )); + if (location.hash) { + pageRef.header = decodeURI(location.hash.substring(1)); + } + return pageRef; }