From 2f1f266482bdaa24438c2b29bce55a6c722789af Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Tue, 8 Aug 2023 16:35:46 +0200 Subject: [PATCH] Make attribute extensible --- plugs/core/attributes.ts | 145 ++++++++++++++++++++++++++++++------ plugs/core/core.plug.yaml | 16 ++++ plugs/directive/complete.ts | 102 ++++++++----------------- server/http_server.ts | 6 +- 4 files changed, 172 insertions(+), 97 deletions(-) diff --git a/plugs/core/attributes.ts b/plugs/core/attributes.ts index 5f3efa1e..c64fe9fb 100644 --- a/plugs/core/attributes.ts +++ b/plugs/core/attributes.ts @@ -1,5 +1,6 @@ import { index } from "$sb/silverbullet-syscall/mod.ts"; import type { CompleteEvent } from "$sb/app_event.ts"; +import { events } from "$sb/plugos-syscall/mod.ts"; export type AttributeContext = "page" | "item" | "task"; @@ -7,6 +8,47 @@ type AttributeEntry = { type: string; }; +export type AttributeCompleteEvent = { + source: string; + prefix: string; +}; + +export type AttributeCompletion = { + name: string; + source: string; + type: string; + builtin?: boolean; +}; + +const builtinAttributes: Record> = { + page: { + name: "string", + lastModified: "number", + perm: "rw|ro", + contentType: "string", + size: "number", + tags: "array", + }, + task: { + name: "string", + done: "boolean", + page: "string", + deadline: "string", + pos: "number", + tags: "array", + }, + item: { + name: "string", + page: "string", + pos: "number", + tags: "array", + }, + tag: { + name: "string", + freq: "number", + }, +}; + function determineType(v: any): string { const t = typeof v; if (t === "object") { @@ -37,6 +79,52 @@ export async function indexAttributes( ); } +export async function customAttributeCompleter( + attributeCompleteEvent: AttributeCompleteEvent, +): Promise { + const sourcePrefix = attributeCompleteEvent.source === "*" + ? "" + : `${attributeCompleteEvent.source}:`; + const allAttributes = await index.queryPrefix( + `${attributeKeyPrefix}${sourcePrefix}`, + ); + return allAttributes.map((attr) => { + const [_prefix, context, name] = attr.key.split(":"); + return { + name, + source: context, + type: attr.value.type, + }; + }); +} + +export function builtinAttributeCompleter( + attributeCompleteEvent: AttributeCompleteEvent, +): AttributeCompletion[] { + let allAttributes = builtinAttributes[attributeCompleteEvent.source]; + if (attributeCompleteEvent.source === "*") { + allAttributes = {}; + for (const [source, attributes] of Object.entries(builtinAttributes)) { + for (const [name, type] of Object.entries(attributes)) { + allAttributes[name] = `${type}|${source}`; + } + } + } + if (!allAttributes) { + return []; + } + return Object.entries(allAttributes).map(([name, type]) => { + return { + name, + source: attributeCompleteEvent.source === "*" + ? type.split("|")[1] + : attributeCompleteEvent.source, + type: attributeCompleteEvent.source === "*" ? type.split("|")[0] : type, + builtin: true, + }; + }); +} + export async function attributeComplete(completeEvent: CompleteEvent) { const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec( completeEvent.linePrefix, @@ -49,41 +137,50 @@ export async function attributeComplete(completeEvent: CompleteEvent) { } else if (completeEvent.parentNodes.includes("ListItem")) { type = "item"; } - const allAttributes = await index.queryPrefix( - `${attributeKeyPrefix}${type}:`, - ); + const completions = (await events.dispatchEvent( + `attribute:complete:${type}`, + { + source: type, + prefix: inlineAttributeMatch[2], + } as AttributeCompleteEvent, + )).flat() as AttributeCompletion[]; return { from: completeEvent.pos - inlineAttributeMatch[2].length, - options: allAttributes.map((attr) => { - const [_prefix, _context, name] = attr.key.split(":"); - return { - label: name, - apply: `${name}: `, - detail: attr.value.type, - type: "attribute", - }; - }), + options: attributeCompletionsToCMCompletion( + completions.filter((completion) => !completion.builtin), + ), }; } const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix); if (attributeMatch) { if (completeEvent.parentNodes.includes("FrontMatterCode")) { - const allAttributes = await index.queryPrefix( - `${attributeKeyPrefix}page:`, - ); + const completions = (await events.dispatchEvent( + `attribute:complete:page`, + { + source: "page", + prefix: attributeMatch[1], + } as AttributeCompleteEvent, + )).flat() as AttributeCompletion[]; return { from: completeEvent.pos - attributeMatch[1].length, - options: allAttributes.map((attr) => { - const [_prefix, _context, name] = attr.key.split(":"); - return { - label: name, - apply: `${name}: `, - detail: attr.value.type, - type: "attribute", - }; - }), + options: attributeCompletionsToCMCompletion( + completions.filter((completion) => !completion.builtin), + ), }; } } return null; } + +export function attributeCompletionsToCMCompletion( + completions: AttributeCompletion[], +) { + return completions.map( + (completion) => ({ + label: completion.name, + apply: `${completion.name}: `, + detail: `${completion.type} (${completion.source})`, + type: "attribute", + }), + ); +} diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index 374fae93..b1306978 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -96,6 +96,22 @@ functions: events: - editor:complete + customAttributeCompleter: + path: ./attributes.ts:customAttributeCompleter + events: + - attribute:complete:page + - attribute:complete:task + - attribute:complete:item + - attribute:complete:* + + builtinAttributeCompleter: + path: ./attributes.ts:builtinAttributeCompleter + events: + - attribute:complete:page + - attribute:complete:task + - attribute:complete:item + - attribute:complete:* + # Commands commandComplete: path: "./command.ts:commandComplete" diff --git a/plugs/directive/complete.ts b/plugs/directive/complete.ts index 482e63d3..10a43a58 100644 --- a/plugs/directive/complete.ts +++ b/plugs/directive/complete.ts @@ -2,36 +2,10 @@ import { events } from "$sb/plugos-syscall/mod.ts"; import { CompleteEvent } from "$sb/app_event.ts"; import { buildHandebarOptions } from "./util.ts"; import type { PageMeta } from "../../web/types.ts"; -import { index } from "$sb/silverbullet-syscall/mod.ts"; - -const builtinAttributes: Record> = { - page: { - name: "string", - lastModified: "number", - perm: "rw|ro", - contentType: "string", - size: "number", - tags: "array", - }, - task: { - name: "string", - done: "boolean", - page: "string", - deadline: "string", - pos: "number", - tags: "array", - }, - item: { - name: "string", - page: "string", - pos: "number", - tags: "array", - }, - tag: { - name: "string", - freq: "number", - }, -}; +import { + AttributeCompleteEvent, + AttributeCompletion, +} from "../core/attributes.ts"; export async function queryComplete(completeEvent: CompleteEvent) { const querySourceMatch = /#query\s+([\w\-_]*)$/.exec( @@ -61,51 +35,22 @@ export async function queryComplete(completeEvent: CompleteEvent) { if (querySourceMatch && whereMatch) { const type = querySourceMatch[1]; const attributePrefix = whereMatch[3]; - // console.log("Type", type); - // console.log("Where", attributePrefix); - const allAttributes = await index.queryPrefix( - `attr:${type}:`, - ); - + const completions = (await events.dispatchEvent( + `attribute:complete:${type}`, + { + source: type, + prefix: attributePrefix, + } as AttributeCompleteEvent, + )).flat() as AttributeCompletion[]; return { from: completeEvent.pos - attributePrefix.length, - options: compileAttributeCompletions(allAttributes, type), + options: attributeCompletionsToCMCompletion(completions), }; } } return null; } -function compileAttributeCompletions( - allAttributes: { key: string; value: any }[], - type?: string, -) { - let allCompletions: any[] = allAttributes.map((attr) => { - const [_prefix, context, name] = attr.key.split(":"); - return { - label: name, - detail: `${attr.value.type} (${context})`, - type: "attribute", - }; - }); - const allContexts = type ? [type] : Object.keys(builtinAttributes); - - for (const context of allContexts) { - allCompletions = allCompletions.concat( - builtinAttributes[context] - ? Object.entries( - builtinAttributes[context], - ).map(([name, type]) => ({ - label: name, - detail: `${type} (${context}: builtin)`, - type: "attribute", - })) - : [], - ); - } - return allCompletions; -} - export async function templateVariableComplete(completeEvent: CompleteEvent) { const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix); if (!match) { @@ -123,9 +68,16 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) { })), ); - const allAttributes = await index.queryPrefix(`attr:`); + const completions = (await events.dispatchEvent( + `attribute:complete:*`, + { + source: "*", + prefix: match[1], + } as AttributeCompleteEvent, + )).flat() as AttributeCompletion[]; + allCompletions = allCompletions.concat( - compileAttributeCompletions(allAttributes), + attributeCompletionsToCMCompletion(completions), ); return { @@ -133,3 +85,15 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) { options: allCompletions, }; } + +export function attributeCompletionsToCMCompletion( + completions: AttributeCompletion[], +) { + return completions.map( + (completion) => ({ + label: completion.name, + detail: `${completion.type} (${completion.source})`, + type: "attribute", + }), + ); +} diff --git a/server/http_server.ts b/server/http_server.ts index 2d7631eb..f2cd0463 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -334,12 +334,11 @@ export class HttpServer { fsRouter .get( filePathRegex, - // corsMiddleware, async ({ params, response, request }) => { const name = params[0]; console.log("Requested file", name); if (!request.headers.has("X-Sync-Mode") && name.endsWith(".md")) { - // It can happen that during a sync, authentication expires + // It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page. console.log("Request was without X-Sync-Mode, redirecting to page"); response.redirect(`/${name.slice(0, -3)}`); return; @@ -401,7 +400,7 @@ export class HttpServer { response.body = fileData.data; } catch (e: any) { - console.error("Error GETting of file", name, e); + console.error("Error GETting file", name, e); response.status = 404; response.body = "Not found"; } @@ -409,7 +408,6 @@ export class HttpServer { ) .put( filePathRegex, - // corsMiddleware, async ({ request, response, params }) => { const name = params[0]; console.log("Saving file", name);