1
0

Attribute indexing and code completion

This commit is contained in:
Zef Hemel 2023-08-01 21:35:19 +02:00
parent 52a28c78a7
commit 701a567c95
10 changed files with 210 additions and 17 deletions

View File

@ -46,6 +46,7 @@ export type CompleteEvent = {
pageName: string;
linePrefix: string;
pos: number;
parentNodes: string[];
};
export type WidgetContent = {

View File

@ -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
View 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;
}

View File

@ -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:

View File

@ -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({

View File

@ -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") {

View File

@ -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) {

View File

@ -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) {

View File

@ -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) {