Attribute indexing and code completion
This commit is contained in:
parent
52a28c78a7
commit
701a567c95
@ -46,6 +46,7 @@ export type CompleteEvent = {
|
|||||||
pageName: string;
|
pageName: string;
|
||||||
linePrefix: string;
|
linePrefix: string;
|
||||||
pos: number;
|
pos: number;
|
||||||
|
parentNodes: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WidgetContent = {
|
export type WidgetContent = {
|
||||||
|
@ -60,7 +60,7 @@ export async function extractFrontmatter(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} 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:
|
events:
|
||||||
- editor:complete
|
- editor:complete
|
||||||
|
|
||||||
|
attributeComplete:
|
||||||
|
path: "./attributes.ts:attributeComplete"
|
||||||
|
events:
|
||||||
|
- editor:complete
|
||||||
|
|
||||||
# Commands
|
# Commands
|
||||||
commandComplete:
|
commandComplete:
|
||||||
path: "./command.ts:commandComplete"
|
path: "./command.ts:commandComplete"
|
||||||
@ -317,7 +322,7 @@ functions:
|
|||||||
extractToPageCommand:
|
extractToPageCommand:
|
||||||
path: ./refactor.ts:extractToPageCommand
|
path: ./refactor.ts:extractToPageCommand
|
||||||
command:
|
command:
|
||||||
name: "Extract text to new page"
|
name: "Page: Extract"
|
||||||
renamePageCommand:
|
renamePageCommand:
|
||||||
path: "./refactor.ts:renamePageCommand"
|
path: "./refactor.ts:renamePageCommand"
|
||||||
command:
|
command:
|
||||||
@ -328,7 +333,7 @@ functions:
|
|||||||
renamePrefixCommand:
|
renamePrefixCommand:
|
||||||
path: "./refactor.ts:renamePrefixCommand"
|
path: "./refactor.ts:renamePrefixCommand"
|
||||||
command:
|
command:
|
||||||
name: "Refactor: Batch Rename Page Prefix"
|
name: "Page: Batch Rename Prefix"
|
||||||
|
|
||||||
# Plug manager
|
# Plug manager
|
||||||
updatePlugsCommand:
|
updatePlugsCommand:
|
||||||
|
@ -5,6 +5,7 @@ import { collectNodesOfType, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
|||||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||||
import { extractAttributes } from "$sb/lib/attribute.ts";
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
import { rewritePageRefs } from "$sb/lib/resolve.ts";
|
import { rewritePageRefs } from "$sb/lib/resolve.ts";
|
||||||
|
import { indexAttributes } from "./attributes.ts";
|
||||||
|
|
||||||
export type Item = {
|
export type Item = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -23,6 +24,8 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
|||||||
|
|
||||||
const coll = collectNodesOfType(tree, "ListItem");
|
const coll = collectNodesOfType(tree, "ListItem");
|
||||||
|
|
||||||
|
const allAttributes: Record<string, any> = {};
|
||||||
|
|
||||||
for (const n of coll) {
|
for (const n of coll) {
|
||||||
if (!n.children) {
|
if (!n.children) {
|
||||||
continue;
|
continue;
|
||||||
@ -46,8 +49,10 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
|||||||
}
|
}
|
||||||
// Extract attributes and remove from tree
|
// Extract attributes and remove from tree
|
||||||
const extractedAttributes = await extractAttributes(child, true);
|
const extractedAttributes = await extractAttributes(child, true);
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(extractedAttributes)) {
|
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||||
item[key] = value;
|
item[key] = value;
|
||||||
|
allAttributes[key] = value;
|
||||||
}
|
}
|
||||||
textNodes.push(child);
|
textNodes.push(child);
|
||||||
}
|
}
|
||||||
@ -71,6 +76,7 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
|||||||
}
|
}
|
||||||
// console.log("Found", items, "item(s)");
|
// console.log("Found", items, "item(s)");
|
||||||
await index.batchSet(name, items);
|
await index.batchSet(name, items);
|
||||||
|
await indexAttributes(name, allAttributes, "item");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function queryProvider({
|
export async function queryProvider({
|
||||||
|
@ -5,6 +5,7 @@ import { extractAttributes } from "$sb/lib/attribute.ts";
|
|||||||
import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||||
import { applyQuery } from "$sb/lib/query.ts";
|
import { applyQuery } from "$sb/lib/query.ts";
|
||||||
import { resolvePath } from "$sb/lib/resolve.ts";
|
import { resolvePath } from "$sb/lib/resolve.ts";
|
||||||
|
import { indexAttributes } from "./attributes.ts";
|
||||||
|
|
||||||
// Key space:
|
// Key space:
|
||||||
// l:toPage:pos => {name: pageName, inDirective: true, asTemplate: true}
|
// 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 index.set(name, "meta:", pageMeta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await indexAttributes(name, pageMeta, "page");
|
||||||
|
|
||||||
let directiveDepth = 0;
|
let directiveDepth = 0;
|
||||||
traverseTree(tree, (n): boolean => {
|
traverseTree(tree, (n): boolean => {
|
||||||
if (n.type === "DirectiveStart") {
|
if (n.type === "DirectiveStart") {
|
||||||
|
@ -2,23 +2,97 @@ import { events } from "$sb/plugos-syscall/mod.ts";
|
|||||||
import { CompleteEvent } from "$sb/app_event.ts";
|
import { CompleteEvent } from "$sb/app_event.ts";
|
||||||
import { buildHandebarOptions } from "./util.ts";
|
import { buildHandebarOptions } from "./util.ts";
|
||||||
import type { PageMeta } from "../../web/types.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) {
|
export async function queryComplete(completeEvent: CompleteEvent) {
|
||||||
const match = /#query ([\w\-_]*)$/.exec(completeEvent.linePrefix);
|
const querySourceMatch = /#query\s+([\w\-_]*)$/.exec(
|
||||||
if (!match) {
|
completeEvent.linePrefix,
|
||||||
return null;
|
);
|
||||||
}
|
if (querySourceMatch) {
|
||||||
|
|
||||||
const allEvents = await events.listEvents();
|
const allEvents = await events.listEvents();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
from: completeEvent.pos - match[1].length,
|
from: completeEvent.pos - querySourceMatch[1].length,
|
||||||
options: allEvents
|
options: allEvents
|
||||||
.filter((eventName) => eventName.startsWith("query:"))
|
.filter((eventName) => eventName.startsWith("query:"))
|
||||||
.map((source) => ({
|
.map((source) => ({
|
||||||
label: source.substring("query:".length),
|
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) {
|
export function handlebarHelperComplete(completeEvent: CompleteEvent) {
|
||||||
|
@ -25,6 +25,7 @@ import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
|||||||
import { niceDate } from "$sb/lib/dates.ts";
|
import { niceDate } from "$sb/lib/dates.ts";
|
||||||
import { extractAttributes } from "$sb/lib/attribute.ts";
|
import { extractAttributes } from "$sb/lib/attribute.ts";
|
||||||
import { rewritePageRefs } from "$sb/lib/resolve.ts";
|
import { rewritePageRefs } from "$sb/lib/resolve.ts";
|
||||||
|
import { indexAttributes } from "../core/attributes.ts";
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -45,6 +46,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||||||
const tasks: { key: string; value: Task }[] = [];
|
const tasks: { key: string; value: Task }[] = [];
|
||||||
removeQueries(tree);
|
removeQueries(tree);
|
||||||
addParentPointers(tree);
|
addParentPointers(tree);
|
||||||
|
const allAttributes: Record<string, any> = {};
|
||||||
await traverseTreeAsync(tree, async (n) => {
|
await traverseTreeAsync(tree, async (n) => {
|
||||||
if (n.type !== "Task") {
|
if (n.type !== "Task") {
|
||||||
return false;
|
return false;
|
||||||
@ -78,6 +80,7 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
|||||||
const extractedAttributes = await extractAttributes(n, true);
|
const extractedAttributes = await extractAttributes(n, true);
|
||||||
for (const [key, value] of Object.entries(extractedAttributes)) {
|
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||||
task[key] = value;
|
task[key] = value;
|
||||||
|
allAttributes[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.name = n.children!.slice(1).map(renderToText).join("").trim();
|
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)");
|
// console.log("Found", tasks, "task(s)");
|
||||||
await index.batchSet(name, tasks);
|
await index.batchSet(name, tasks);
|
||||||
|
await indexAttributes(name, allAttributes, "task");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function taskToggle(event: ClickEvent) {
|
export function taskToggle(event: ClickEvent) {
|
||||||
|
@ -551,10 +551,21 @@ export class Client {
|
|||||||
const line = editorState.doc.lineAt(selection.from);
|
const line = editorState.doc.lineAt(selection.from);
|
||||||
const linePrefix = line.text.slice(0, selection.from - line.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, {
|
const results = await this.dispatchAppEvent(eventName, {
|
||||||
pageName: this.currentPage!,
|
pageName: this.currentPage!,
|
||||||
linePrefix,
|
linePrefix,
|
||||||
pos: selection.from,
|
pos: selection.from,
|
||||||
|
parentNodes,
|
||||||
} as CompleteEvent);
|
} as CompleteEvent);
|
||||||
let actualResult = null;
|
let actualResult = null;
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
|
Loading…
Reference in New Issue
Block a user