From d43dbcacec55ab2540d384e33ce6bca1a7ca6c25 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 28 Dec 2023 16:14:30 +0100 Subject: [PATCH] Fixed a lot of bugs with new widget rendering --- plug-api/types.ts | 7 ++ plugs/index/index.plug.yaml | 3 + plugs/index/linked_mentions.ts | 20 +++-- plugs/index/toc.ts | 33 ++++--- plugs/query/query.plug.yaml | 6 +- plugs/query/query.ts | 18 +++- plugs/query/widget.ts | 8 +- server/server_system.ts | 2 +- web/client.ts | 67 ++++++++++++--- web/cm_plugins/iframe_widget.ts | 2 +- web/cm_plugins/inline_image.ts | 6 +- web/cm_plugins/markdown_widget.ts | 109 +++++++++++++++--------- web/cm_plugins/top_bottom_panels.ts | 2 +- web/components/widget_sandbox_iframe.ts | 2 +- web/space.ts | 46 ---------- web/styles/editor.scss | 21 ++--- web/syscalls/client_code_widget.ts | 2 + 17 files changed, 212 insertions(+), 142 deletions(-) diff --git a/plug-api/types.ts b/plug-api/types.ts index 496c9f5..119e571 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -133,6 +133,13 @@ export type CodeWidgetContent = { html?: string; markdown?: string; script?: string; + buttons?: CodeWidgetButton[]; +}; + +export type CodeWidgetButton = { + description: string; + svg: string; + invokeFunction: string; }; export type LintDiagnostic = { diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index fad311f..672f74f 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -178,6 +178,9 @@ functions: env: client panelWidget: top + refreshTOC: + path: toc.ts:refreshTOC + lintYAML: path: lint.ts:lintYAML events: diff --git a/plugs/index/linked_mentions.ts b/plugs/index/linked_mentions.ts index 9398ffc..767f371 100644 --- a/plugs/index/linked_mentions.ts +++ b/plugs/index/linked_mentions.ts @@ -1,4 +1,4 @@ -import { clientStore, editor, system } from "$sb/silverbullet-syscall/mod.ts"; +import { clientStore, codeWidget, editor, system } from "$sb/syscalls.ts"; import { CodeWidgetContent } from "$sb/types.ts"; import { queryObjects } from "./api.ts"; import { LinkObject } from "./page_links.ts"; @@ -9,14 +9,16 @@ export async function toggleMentions() { let hideMentions = await clientStore.get(hideMentionsKey); hideMentions = !hideMentions; await clientStore.set(hideMentionsKey, hideMentions); - if (!hideMentions) { - await renderMentions(); - } else { - await editor.dispatch({}); - } + await codeWidget.refreshAll(); } export async function renderMentions(): Promise { + console.log("Hide mentions", await clientStore.get(hideMentionsKey)); + if (await clientStore.get(hideMentionsKey)) { + return null; + } + + console.log("Stil here"); const page = await editor.getCurrentPage(); const linksResult = await queryObjects("link", { // Query all links that point to this page, excluding those that are inside directives and self pointers. @@ -41,6 +43,12 @@ export async function renderMentions(): Promise { } return { markdown: renderedMd, + buttons: [{ + description: "Hide", + svg: + ``, + invokeFunction: "index.toggleMentions", + }], }; } } diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index 9209d8a..6d7cc5a 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -1,10 +1,10 @@ import { clientStore, + codeWidget, editor, markdown, - system, } from "$sb/silverbullet-syscall/mod.ts"; -import { renderToText, traverseTree, traverseTreeAsync } from "$sb/lib/tree.ts"; +import { renderToText, traverseTree } from "$sb/lib/tree.ts"; import { CodeWidgetContent } from "$sb/types.ts"; const hideTOCKey = "hideTOC"; @@ -16,23 +16,18 @@ type Header = { level: number; }; -let cachedTOC: string | undefined; - export async function toggleTOC() { - cachedTOC = undefined; let hideTOC = await clientStore.get(hideTOCKey); hideTOC = !hideTOC; await clientStore.set(hideTOCKey, hideTOC); - await renderTOC(); // This will hide it if needed + await codeWidget.refreshAll(); } -async function markdownToHtml(text: string): Promise { - return system.invokeFunction("markdown.markdownToHtml", text); +export async function refreshTOC() { + await codeWidget.refreshAll(); } -export async function renderTOC( - reload = false, -): Promise { +export async function renderTOC(): Promise { if (await clientStore.get(hideTOCKey)) { return null; } @@ -40,7 +35,7 @@ export async function renderTOC( const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); const headers: Header[] = []; - await traverseTreeAsync(tree, async (n) => { + traverseTree(tree, (n) => { if (n.type?.startsWith("ATXHeading")) { headers.push({ name: n.children!.slice(1).map(renderToText).join("").trim(), @@ -66,5 +61,19 @@ export async function renderTOC( // console.log("Markdown", renderedMd); return { markdown: renderedMd, + buttons: [ + { + description: "Reload", + svg: + ``, + invokeFunction: "index.refreshTOC", + }, + { + description: "Hide", + svg: + ``, + invokeFunction: "index.toggleTOC", + }, + ], }; } diff --git a/plugs/query/query.plug.yaml b/plugs/query/query.plug.yaml index 1c8225f..abab814 100644 --- a/plugs/query/query.plug.yaml +++ b/plugs/query/query.plug.yaml @@ -26,11 +26,15 @@ functions: - editor:complete refreshAllWidgets: - path: widget.ts:refreshAll + path: widget.ts:refreshAllWidgets command: name: "Live Queries and Templates: Refresh All" key: "Alt-q" + # Query widget buttons + editButton: + path: widget.ts:editButton + # Slash commands insertQuery: redirect: template.insertTemplateText diff --git a/plugs/query/query.ts b/plugs/query/query.ts index e4e9447..cef4e0b 100644 --- a/plugs/query/query.ts +++ b/plugs/query/query.ts @@ -9,12 +9,12 @@ import { astToKvQuery } from "$sb/lib/parse-query.ts"; import { jsonToMDTable, renderQueryTemplate } from "../directive/util.ts"; import { loadPageObject, replaceTemplateVars } from "../template/template.ts"; import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts"; -import { LintDiagnostic } from "$sb/types.ts"; +import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts"; export async function widget( bodyText: string, pageName: string, -): Promise { +): Promise { const pageObject = await loadPageObject(pageName); try { @@ -72,6 +72,20 @@ export async function widget( return { markdown: resultMarkdown, + buttons: [ + { + description: "Edit", + svg: + ``, + invokeFunction: "query.editButton", + }, + { + description: "Reload", + svg: + ``, + invokeFunction: "query.refreshAllWidgets", + }, + ], }; } catch (e: any) { return { markdown: `**Error:** ${e.message}` }; diff --git a/plugs/query/widget.ts b/plugs/query/widget.ts index 2c9565d..2b3fe48 100644 --- a/plugs/query/widget.ts +++ b/plugs/query/widget.ts @@ -1,5 +1,9 @@ -import { codeWidget } from "$sb/syscalls.ts"; +import { codeWidget, editor } from "$sb/syscalls.ts"; -export function refreshAll() { +export function refreshAllWidgets() { codeWidget.refreshAll(); } + +export async function editButton(pos: number) { + await editor.moveCursor(pos); +} diff --git a/server/server_system.ts b/server/server_system.ts index 1a0a2c8..9b0fcb9 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -98,7 +98,7 @@ export class ServerSystem { ), eventHook, ); - const space = new Space(this.spacePrimitives, this.ds, eventHook); + const space = new Space(this.spacePrimitives, eventHook); // Add syscalls this.system.registerSyscalls( diff --git a/web/client.ts b/web/client.ts index fd9f01b..a5ad4cc 100644 --- a/web/client.ts +++ b/web/client.ts @@ -38,7 +38,7 @@ import { OpenPages } from "./open_pages.ts"; import { MainUI } from "./editor_ui.tsx"; import { cleanPageRef } from "$sb/lib/resolve.ts"; import { SpacePrimitives } from "../common/spaces/space_primitives.ts"; -import { FileMeta, PageMeta } from "$sb/types.ts"; +import { CodeWidgetButton, FileMeta, PageMeta } from "$sb/types.ts"; import { DataStore } from "../plugos/lib/datastore.ts"; import { IndexedDBKvPrimitives } from "../plugos/lib/indexeddb_kv_primitives.ts"; import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts"; @@ -188,13 +188,7 @@ export class Client { // Load settings this.settings = await ensureSettingsAndIndex(localSpacePrimitives); - // Load widget cache - this.widgetCache = new LimitedMap( - 100, - await this.stateDataStore.get(["cache", "widgets"]) || - {}, - ); - + await this.loadCaches(); // Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page try { await this.httpSpacePrimitives.ping(); @@ -484,7 +478,6 @@ export class Client { this.space = new Space( localSpacePrimitives, - this.stateDataStore, this.eventHook, ); @@ -1013,7 +1006,56 @@ export class Client { return; } - private widgetCache = new LimitedMap(100); + // Widget and image height caching + private widgetCache = new LimitedMap(100); // bodyText -> WidgetCacheItem + private imageHeightCache = new LimitedMap(100); // url -> height + private widgetHeightCache = new LimitedMap(100); // bodytext -> height + + async loadCaches() { + const [imageHeightCache, widgetHeightCache, widgetCache] = await this + .stateDataStore.batchGet([["cache", "imageHeight"], [ + "cache", + "widgetHeight", + ], ["cache", "widgets"]]); + this.imageHeightCache = new LimitedMap(100, imageHeightCache || {}); + this.widgetHeightCache = new LimitedMap(100, widgetHeightCache || {}); + this.widgetCache = new LimitedMap(100, widgetCache || {}); + } + + debouncedImageCacheFlush = throttle(() => { + this.stateDataStore.set(["cache", "imageHeight"], this.imageHeightCache) + .catch( + console.error, + ); + console.log("Flushed image height cache to store"); + }, 5000); + + setCachedImageHeight(url: string, height: number) { + this.imageHeightCache.set(url, height); + this.debouncedImageCacheFlush(); + } + getCachedImageHeight(url: string): number { + return this.imageHeightCache.get(url) ?? -1; + } + + debouncedWidgetHeightCacheFlush = throttle(() => { + this.stateDataStore.set( + ["cache", "widgetHeight"], + this.widgetHeightCache.toJSON(), + ) + .catch( + console.error, + ); + // console.log("Flushed widget height cache to store"); + }, 5000); + + setCachedWidgetHeight(bodyText: string, height: number) { + this.widgetHeightCache.set(bodyText, height); + this.debouncedWidgetHeightCacheFlush(); + } + getCachedWidgetHeight(bodyText: string): number { + return this.widgetHeightCache.get(bodyText) ?? -1; + } debouncedWidgetCacheFlush = throttle(() => { this.stateDataStore.set(["cache", "widgets"], this.widgetCache.toJSON()) @@ -1023,8 +1065,8 @@ export class Client { console.log("Flushed widget cache to store"); }, 5000); - setWidgetCache(key: string, height: number, html: string) { - this.widgetCache.set(key, { height, html }); + setWidgetCache(key: string, cacheItem: WidgetCacheItem) { + this.widgetCache.set(key, cacheItem); this.debouncedWidgetCacheFlush(); } @@ -1036,4 +1078,5 @@ export class Client { type WidgetCacheItem = { height: number; html: string; + buttons?: CodeWidgetButton[]; }; diff --git a/web/cm_plugins/iframe_widget.ts b/web/cm_plugins/iframe_widget.ts index 86636c5..c407427 100644 --- a/web/cm_plugins/iframe_widget.ts +++ b/web/cm_plugins/iframe_widget.ts @@ -57,7 +57,7 @@ export class IFrameWidget extends WidgetType { } get estimatedHeight(): number { - const cachedHeight = this.client.space.getCachedWidgetHeight(this.bodyText); + const cachedHeight = this.client.getCachedWidgetHeight(this.bodyText); // console.log("Calling estimated height", cachedHeight); return cachedHeight > 0 ? cachedHeight : 150; } diff --git a/web/cm_plugins/inline_image.ts b/web/cm_plugins/inline_image.ts index 566bcda..78ca12f 100644 --- a/web/cm_plugins/inline_image.ts +++ b/web/cm_plugins/inline_image.ts @@ -25,7 +25,7 @@ class InlineImageWidget extends WidgetType { } get estimatedHeight(): number { - const cachedHeight = this.client.space.getCachedImageHeight(this.url); + const cachedHeight = this.client.getCachedImageHeight(this.url); // console.log("Estimated height requested", this.url, cachedHeight); return cachedHeight; } @@ -35,11 +35,11 @@ class InlineImageWidget extends WidgetType { let url = this.url; url = resolvePath(this.client.currentPage!, url, true); // console.log("Creating DOM", this.url); - const cachedImageHeight = this.client.space.getCachedImageHeight(url); + const cachedImageHeight = this.client.getCachedImageHeight(url); img.onload = () => { // console.log("Loaded", this.url, "with height", img.height); if (img.height !== cachedImageHeight) { - this.client.space.setCachedImageHeight(url, img.height); + this.client.setCachedImageHeight(url, img.height); } }; img.src = url; diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index 98c18b1..2932182 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -1,14 +1,17 @@ import { WidgetType } from "../deps.ts"; import type { Client } from "../client.ts"; -import type { CodeWidgetCallback } from "$sb/types.ts"; +import type { CodeWidgetButton, CodeWidgetCallback } from "$sb/types.ts"; import { renderMarkdownToHtml } from "../../plugs/markdown/markdown_render.ts"; import { resolveAttachmentPath } from "$sb/lib/resolve.ts"; import { parse } from "../../common/markdown_parser/parse_tree.ts"; import buildMarkdown from "../../common/markdown_parser/parser.ts"; import { renderToText } from "$sb/lib/tree.ts"; +const activeWidgets = new Set(); + export class MarkdownWidget extends WidgetType { renderedMarkdown?: string; + public dom?: HTMLElement; constructor( readonly from: number | undefined, @@ -27,19 +30,20 @@ export class MarkdownWidget extends WidgetType { if (cacheItem) { div.innerHTML = this.wrapHtml( cacheItem.html, - this.from !== undefined, - this.from !== undefined, + cacheItem.buttons, ); - this.attachListeners(div); + this.attachListeners(div, cacheItem.buttons); } // Async kick-off of content renderer this.renderContent(div, cacheItem?.html).catch(console.error); + this.dom = div; + return div; } - private async renderContent( + async renderContent( div: HTMLElement, cachedHtml: string | undefined, ) { @@ -47,6 +51,7 @@ export class MarkdownWidget extends WidgetType { this.bodyText, this.client.currentPage!, ); + activeWidgets.add(this); if (!widgetContent) { div.innerHTML = ""; // div.style.display = "none"; @@ -89,42 +94,30 @@ export class MarkdownWidget extends WidgetType { // HTML still same as in cache, no need to re-render return; } - div.innerHTML = this.wrapHtml( - html, - this.from !== undefined, - this.from !== undefined, - ); - this.attachListeners(div); + div.innerHTML = this.wrapHtml(html, widgetContent.buttons); + this.attachListeners(div, widgetContent.buttons); // Let's give it a tick, then measure and cache setTimeout(() => { this.client.setWidgetCache( this.bodyText, - div.clientHeight, - html, + { height: div.clientHeight, html, buttons: widgetContent.buttons }, ); }); } - private wrapHtml(html: string, editButton = true, sourceButton = true) { - return ` -
- ${ - sourceButton - ? `` - : "" + private wrapHtml(html: string, buttons?: CodeWidgetButton[]) { + if (!buttons) { + return html; } - - ${ - editButton - ? `` - : "" - } -
- ${html}`; + return `
${ + buttons.map((button, idx) => + ` ` + ).join("") + }
${html}`; } - private attachListeners(div: HTMLElement) { + private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) { div.querySelectorAll("a[data-ref]").forEach((el_) => { const el = el_ as HTMLElement; // Override default click behavior with a local navigate (faster) @@ -160,20 +153,30 @@ export class MarkdownWidget extends WidgetType { ); }); - if (this.from !== undefined) { - div.querySelector(".edit-button")!.addEventListener("click", () => { - this.client.editorView.dispatch({ - selection: { anchor: this.from! }, - }); - this.client.focus(); - }); - div.querySelector(".source-button")!.addEventListener("click", () => { - div.innerText = this.renderedMarkdown!; - }); + if (!buttons) { + buttons = []; } - div.querySelector(".reload-button")!.addEventListener("click", () => { - this.renderContent(div, undefined).catch(console.error); - }); + + for (let i = 0; i < buttons.length; i++) { + const button = buttons[i]; + div.querySelector(`button[data-button="${i}"]`)!.addEventListener( + "click", + () => { + this.client.system.localSyscall("system.invokeFunction", [ + button.invokeFunction, + this.from, + ]).then((newContent: string | undefined) => { + if (newContent) { + div.innerText = newContent; + } + this.client.focus(); + }).catch(console.error); + }, + ); + } + // div.querySelectorAll("ul > li").forEach((el) => { + // el.classList.add("sb-line-li-1", "sb-line-ul"); + // }); } get estimatedHeight(): number { @@ -189,3 +192,25 @@ export class MarkdownWidget extends WidgetType { ); } } + +export function reloadAllMarkdownWidgets() { + for (const widget of activeWidgets) { + // Garbage collect as we go + if (!widget.dom || !widget.dom.parentNode) { + activeWidgets.delete(widget); + continue; + } + widget.renderContent(widget.dom!, undefined).catch(console.error); + } +} + +function garbageCollectWidgets() { + for (const widget of activeWidgets) { + if (!widget.dom || !widget.dom.parentNode) { + // console.log("Garbage collecting widget", widget.bodyText); + activeWidgets.delete(widget); + } + } +} + +setInterval(garbageCollectWidgets, 5000); diff --git a/web/cm_plugins/top_bottom_panels.ts b/web/cm_plugins/top_bottom_panels.ts index ab18808..24d2c00 100644 --- a/web/cm_plugins/top_bottom_panels.ts +++ b/web/cm_plugins/top_bottom_panels.ts @@ -1,4 +1,4 @@ -import { Decoration, EditorState, WidgetType } from "../deps.ts"; +import { Decoration, EditorState } from "../deps.ts"; import type { Client } from "../client.ts"; import { decoratorStateField } from "./util.ts"; import { MarkdownWidget } from "./markdown_widget.ts"; diff --git a/web/components/widget_sandbox_iframe.ts b/web/components/widget_sandbox_iframe.ts index 3099107..e018239 100644 --- a/web/components/widget_sandbox_iframe.ts +++ b/web/components/widget_sandbox_iframe.ts @@ -149,7 +149,7 @@ export function mountIFrame( case "setHeight": iframe.height = data.height + "px"; if (widgetHeightCacheKey) { - client.space.setCachedWidgetHeight( + client.setCachedWidgetHeight( widgetHeightCacheKey, data.height, ); diff --git a/web/space.ts b/web/space.ts index b2e7515..604959c 100644 --- a/web/space.ts +++ b/web/space.ts @@ -11,40 +11,6 @@ import { LimitedMap } from "../common/limited_map.ts"; const pageWatchInterval = 5000; export class Space { - imageHeightCache = new LimitedMap(100); // url -> height - widgetHeightCache = new LimitedMap(100); // bodytext -> height - - debouncedImageCacheFlush = throttle(() => { - this.ds.set(["cache", "imageHeight"], this.imageHeightCache).catch( - console.error, - ); - console.log("Flushed image height cache to store"); - }, 5000); - - setCachedImageHeight(url: string, height: number) { - this.imageHeightCache.set(url, height); - this.debouncedImageCacheFlush(); - } - getCachedImageHeight(url: string): number { - return this.imageHeightCache.get(url) ?? -1; - } - - debouncedWidgetCacheFlush = throttle(() => { - this.ds.set(["cache", "widgetHeight"], this.widgetHeightCache.toJSON()) - .catch( - console.error, - ); - // console.log("Flushed widget height cache to store"); - }, 5000); - - setCachedWidgetHeight(bodyText: string, height: number) { - this.widgetHeightCache.set(bodyText, height); - this.debouncedWidgetCacheFlush(); - } - getCachedWidgetHeight(bodyText: string): number { - return this.widgetHeightCache.get(bodyText) ?? -1; - } - // We do watch files in the background to detect changes // This set of pages should only ever contain 1 page watchedPages = new Set(); @@ -55,20 +21,8 @@ export class Space { constructor( readonly spacePrimitives: SpacePrimitives, - private ds: DataStore, eventHook: EventHook, ) { - // super(); - this.ds.batchGet([["cache", "imageHeight"], ["cache", "widgetHeight"]]) - .then(([imageCache, widgetCache]) => { - if (imageCache) { - this.imageHeightCache = new LimitedMap(100, imageCache); - } - if (widgetCache) { - // console.log("Loaded widget cache from store", widgetCache); - this.widgetHeightCache = new LimitedMap(100, widgetCache); - } - }); eventHook.addLocalListener("page:deleted", (pageName: string) => { if (this.watchedPages.has(pageName)) { // Stop watching deleted pages already diff --git a/web/styles/editor.scss b/web/styles/editor.scss index d9392e0..1b5a2cc 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -451,13 +451,13 @@ .sb-markdown-bottom-widget h1 { border-top-right-radius: 5px; border-top-left-radius: 5px; - margin: 0; + margin: 0 0 5px 0; padding: 10px !important; background-color: var(--editor-directive-background-color); font-size: 1.2em; } - .sb-markdown-top-widget { + .sb-markdown-top-widget:has(*) { margin-bottom: 10px; } @@ -471,7 +471,7 @@ overflow-y: scroll; border: 1px solid var(--editor-directive-background-color); border-radius: 5px; - white-space: nowrap; + white-space: wrap; position: relative; ul, @@ -482,7 +482,7 @@ ul { list-style: none; - padding-left: 1ch; + // padding-left: 1ch; } ul li::before { @@ -493,7 +493,7 @@ /* Needed to add space between the bullet and the text */ width: 1em; /* Also needed for space (tweak if needed) */ - // margin-left: -1em; + margin-left: -1em; } h1, @@ -528,27 +528,24 @@ .button-bar { position: absolute; - right: 6px; - top: 6px; + right: 10px; + top: 10px; display: none; background: var(--editor-directive-background-color); padding-inline: 3px; padding-bottom: 1px; border-radius: 5px; + button { border: none; background: none; cursor: pointer; color: var(--root-color); + margin-right: -8px; } - } - .edit-button, - .reload-button { - margin-left: -10px; } - } .sb-fenced-code-iframe { diff --git a/web/syscalls/client_code_widget.ts b/web/syscalls/client_code_widget.ts index dccc327..e3e3461 100644 --- a/web/syscalls/client_code_widget.ts +++ b/web/syscalls/client_code_widget.ts @@ -1,10 +1,12 @@ import { SysCallMapping } from "../../plugos/system.ts"; +import { reloadAllMarkdownWidgets } from "../cm_plugins/markdown_widget.ts"; import { broadcastReload } from "../components/widget_sandbox_iframe.ts"; export function clientCodeWidgetSyscalls(): SysCallMapping { return { "codeWidget.refreshAll": () => { broadcastReload(); + reloadAllMarkdownWidgets(); }, }; }