diff --git a/plug-api/app_event.ts b/plug-api/app_event.ts index 2ac03cf..197107b 100644 --- a/plug-api/app_event.ts +++ b/plug-api/app_event.ts @@ -46,6 +46,7 @@ export type CompleteEvent = { pageName: string; linePrefix: string; pos: number; + parentNodes: string[]; }; export type WidgetContent = { diff --git a/plug-api/lib/frontmatter.ts b/plug-api/lib/frontmatter.ts index 5863894..f569510 100644 --- a/plug-api/lib/frontmatter.ts +++ b/plug-api/lib/frontmatter.ts @@ -60,7 +60,7 @@ export async function extractFrontmatter( return null; } } catch (e: any) { - console.error("Could not parse frontmatter", e); + console.warn("Could not parse frontmatter", e.message); } } diff --git a/plugs/core/attributes.ts b/plugs/core/attributes.ts new file mode 100644 index 0000000..2a5d5a5 --- /dev/null +++ b/plugs/core/attributes.ts @@ -0,0 +1,89 @@ +import { index } from "$sb/silverbullet-syscall/mod.ts"; +import type { CompleteEvent } from "$sb/app_event.ts"; + +export type AttributeContext = "page" | "item" | "task"; + +type AttributeEntry = { + type: string; +}; + +function determineType(v: any): string { + const t = typeof v; + if (t === "object") { + if (Array.isArray(v)) { + return "array"; + } + } + return t; +} + +const attributeKeyPrefix = "attr:"; + +export async function indexAttributes( + pageName: string, + attributes: Record, + context: AttributeContext, +) { + await index.batchSet( + pageName, + Object.entries(attributes).map(([k, v]) => { + return { + key: `${attributeKeyPrefix}${context}:${k}`, + value: { + type: determineType(v), + } as AttributeEntry, + }; + }), + ); +} + +export async function attributeComplete(completeEvent: CompleteEvent) { + const inlineAttributeMatch = /([^\[]|^)\[(\w+)$/.exec( + completeEvent.linePrefix, + ); + if (inlineAttributeMatch) { + // console.log("Parents", completeEvent.parentNodes); + let type = "page"; + if (completeEvent.parentNodes.includes("Task")) { + type = "task"; + } else if (completeEvent.parentNodes.includes("ListItem")) { + type = "item"; + } + const allAttributes = await index.queryPrefix( + `${attributeKeyPrefix}${type}:`, + ); + 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", + }; + }), + }; + } + const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix); + if (attributeMatch) { + if (completeEvent.parentNodes.includes("FrontMatterCode")) { + const allAttributes = await index.queryPrefix( + `${attributeKeyPrefix}page:`, + ); + 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", + }; + }), + }; + } + } + return null; +} diff --git a/plugs/core/core.plug.yaml b/plugs/core/core.plug.yaml index a1e3fb7..f85ae16 100644 --- a/plugs/core/core.plug.yaml +++ b/plugs/core/core.plug.yaml @@ -89,6 +89,11 @@ functions: events: - editor:complete + attributeComplete: + path: "./attributes.ts:attributeComplete" + events: + - editor:complete + # Commands commandComplete: path: "./command.ts:commandComplete" @@ -317,7 +322,7 @@ functions: extractToPageCommand: path: ./refactor.ts:extractToPageCommand command: - name: "Extract text to new page" + name: "Page: Extract" renamePageCommand: path: "./refactor.ts:renamePageCommand" command: @@ -328,7 +333,7 @@ functions: renamePrefixCommand: path: "./refactor.ts:renamePrefixCommand" command: - name: "Refactor: Batch Rename Page Prefix" + name: "Page: Batch Rename Prefix" # Plug manager updatePlugsCommand: diff --git a/plugs/core/item.ts b/plugs/core/item.ts index 1d130a4..295f686 100644 --- a/plugs/core/item.ts +++ b/plugs/core/item.ts @@ -5,6 +5,7 @@ import { collectNodesOfType, ParseTree, renderToText } from "$sb/lib/tree.ts"; import { applyQuery, removeQueries } from "$sb/lib/query.ts"; import { extractAttributes } from "$sb/lib/attribute.ts"; import { rewritePageRefs } from "$sb/lib/resolve.ts"; +import { indexAttributes } from "./attributes.ts"; export type Item = { name: string; @@ -23,6 +24,8 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { const coll = collectNodesOfType(tree, "ListItem"); + const allAttributes: Record = {}; + for (const n of coll) { if (!n.children) { continue; @@ -46,8 +49,10 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { } // Extract attributes and remove from tree const extractedAttributes = await extractAttributes(child, true); + for (const [key, value] of Object.entries(extractedAttributes)) { item[key] = value; + allAttributes[key] = value; } textNodes.push(child); } @@ -71,6 +76,7 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { } // console.log("Found", items, "item(s)"); await index.batchSet(name, items); + await indexAttributes(name, allAttributes, "item"); } export async function queryProvider({ diff --git a/plugs/core/page_links.ts b/plugs/core/page_links.ts index d665a35..bc3b488 100644 --- a/plugs/core/page_links.ts +++ b/plugs/core/page_links.ts @@ -5,6 +5,7 @@ import { extractAttributes } from "$sb/lib/attribute.ts"; import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts"; import { applyQuery } from "$sb/lib/query.ts"; import { resolvePath } from "$sb/lib/resolve.ts"; +import { indexAttributes } from "./attributes.ts"; // Key space: // l:toPage:pos => {name: pageName, inDirective: true, asTemplate: true} @@ -41,6 +42,8 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) { await index.set(name, "meta:", pageMeta); } + await indexAttributes(name, pageMeta, "page"); + let directiveDepth = 0; traverseTree(tree, (n): boolean => { if (n.type === "DirectiveStart") { diff --git a/plugs/directive/complete.ts b/plugs/directive/complete.ts index 833d0c9..f4e1632 100644 --- a/plugs/directive/complete.ts +++ b/plugs/directive/complete.ts @@ -2,23 +2,97 @@ 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", + }, +}; export async function queryComplete(completeEvent: CompleteEvent) { - const match = /#query ([\w\-_]*)$/.exec(completeEvent.linePrefix); - if (!match) { - return null; + const querySourceMatch = /#query\s+([\w\-_]*)$/.exec( + completeEvent.linePrefix, + ); + if (querySourceMatch) { + const allEvents = await events.listEvents(); + + return { + from: completeEvent.pos - querySourceMatch[1].length, + options: allEvents + .filter((eventName) => eventName.startsWith("query:")) + .map((source) => ({ + label: source.substring("query:".length), + })), + }; } - const allEvents = await events.listEvents(); - - return { - from: completeEvent.pos - match[1].length, - options: allEvents - .filter((eventName) => eventName.startsWith("query:")) - .map((source) => ({ - label: source.substring("query:".length), - })), - }; + if (completeEvent.parentNodes.includes("DirectiveStart")) { + const querySourceMatch = /#query\s+([\w\-_]+)/.exec( + completeEvent.linePrefix, + ); + const whereMatch = + /(where|order\s+by|and|select(\s+[\w\s,]+)?)\s+([\w\-_]*)$/.exec( + completeEvent.linePrefix, + ); + 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 customAttributesCompletions = allAttributes.map((attr) => { + const [_prefix, _context, name] = attr.key.split(":"); + return { + label: name, + detail: attr.value.type, + type: "attribute", + }; + }); + const builtinAttributesCompletions = builtinAttributes[type] + ? Object.entries( + builtinAttributes[type], + ).map(([name, type]) => ({ + label: name, + detail: type, + type: "attribute", + })) + : []; + return { + from: completeEvent.pos - attributePrefix.length, + options: [ + ...customAttributesCompletions, + ...builtinAttributesCompletions, + ], + }; + } + } + return null; } export function handlebarHelperComplete(completeEvent: CompleteEvent) { diff --git a/plugs/federation/federation.plug.yaml b/plugs/federation/federation.plug.yaml index 480266c..962db57 100644 --- a/plugs/federation/federation.plug.yaml +++ b/plugs/federation/federation.plug.yaml @@ -26,4 +26,4 @@ functions: path: ./federation.ts:getFileMeta pageNamespace: pattern: "!.+" - operation: getFileMeta \ No newline at end of file + operation: getFileMeta diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts index fbe9eaf..c00c596 100644 --- a/plugs/tasks/task.ts +++ b/plugs/tasks/task.ts @@ -25,6 +25,7 @@ import { applyQuery, removeQueries } from "$sb/lib/query.ts"; import { niceDate } from "$sb/lib/dates.ts"; import { extractAttributes } from "$sb/lib/attribute.ts"; import { rewritePageRefs } from "$sb/lib/resolve.ts"; +import { indexAttributes } from "../core/attributes.ts"; export type Task = { name: string; @@ -45,6 +46,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) { const tasks: { key: string; value: Task }[] = []; removeQueries(tree); addParentPointers(tree); + const allAttributes: Record = {}; await traverseTreeAsync(tree, async (n) => { if (n.type !== "Task") { return false; @@ -78,6 +80,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) { const extractedAttributes = await extractAttributes(n, true); for (const [key, value] of Object.entries(extractedAttributes)) { task[key] = value; + allAttributes[key] = value; } task.name = n.children!.slice(1).map(renderToText).join("").trim(); @@ -96,6 +99,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) { // console.log("Found", tasks, "task(s)"); await index.batchSet(name, tasks); + await indexAttributes(name, allAttributes, "task"); } export function taskToggle(event: ClickEvent) { diff --git a/web/client.ts b/web/client.ts index 4e3364a..4c5231e 100644 --- a/web/client.ts +++ b/web/client.ts @@ -551,10 +551,21 @@ export class Client { const line = editorState.doc.lineAt(selection.from); const linePrefix = line.text.slice(0, selection.from - line.from); + const parentNodes: string[] = []; + const currentNode = syntaxTree(editorState).resolveInner(selection.from); + if (currentNode) { + let node = currentNode; + while (node.parent) { + parentNodes.push(node.parent.name); + node = node.parent; + } + } + const results = await this.dispatchAppEvent(eventName, { pageName: this.currentPage!, linePrefix, pos: selection.from, + parentNodes, } as CompleteEvent); let actualResult = null; for (const result of results) {