Attribute indexing and code completion
This commit is contained in:
parent
52a28c78a7
commit
701a567c95
@ -46,6 +46,7 @@ export type CompleteEvent = {
|
||||
pageName: string;
|
||||
linePrefix: string;
|
||||
pos: number;
|
||||
parentNodes: string[];
|
||||
};
|
||||
|
||||
export type WidgetContent = {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
89
plugs/core/attributes.ts
Normal file
89
plugs/core/attributes.ts
Normal file
@ -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<string, any>,
|
||||
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;
|
||||
}
|
@ -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:
|
||||
|
@ -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<string, any> = {};
|
||||
|
||||
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({
|
||||
|
@ -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") {
|
||||
|
@ -2,17 +2,46 @@ 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<string, Record<string, string>> = {
|
||||
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 - match[1].length,
|
||||
from: completeEvent.pos - querySourceMatch[1].length,
|
||||
options: allEvents
|
||||
.filter((eventName) => eventName.startsWith("query:"))
|
||||
.map((source) => ({
|
||||
@ -21,6 +50,51 @@ export async function queryComplete(completeEvent: CompleteEvent) {
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
|
@ -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<string, any> = {};
|
||||
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) {
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user