1
0
silverbullet/plugs/index/attributes.ts

158 lines
4.4 KiB
TypeScript
Raw Normal View History

2023-08-01 19:35:19 +00:00
import type { CompleteEvent } from "$sb/app_event.ts";
2023-08-28 15:12:15 +00:00
import { events } from "$sb/syscalls.ts";
2023-12-21 17:37:50 +00:00
import { queryObjects } from "./api.ts";
import { ObjectValue, QueryExpression } from "$sb/types.ts";
import { determineTags } from "../../plug-api/lib/cheap_yaml.ts";
2023-08-01 19:35:19 +00:00
export type AttributeObject = ObjectValue<{
name: string;
attributeType: string;
tagName: string;
page: string;
2023-12-21 17:37:50 +00:00
readOnly: boolean;
}>;
2023-08-01 19:35:19 +00:00
2023-08-08 14:35:46 +00:00
export type AttributeCompleteEvent = {
source: string;
prefix: string;
};
export type AttributeCompletion = {
name: string;
source: string;
attributeType: string;
2023-12-21 17:37:50 +00:00
readOnly: boolean;
2023-08-08 14:35:46 +00:00
};
export function determineType(v: any): string {
2023-08-01 19:35:19 +00:00
const t = typeof v;
if (t === "object") {
if (Array.isArray(v)) {
return "array";
}
}
return t;
}
2023-12-21 17:37:50 +00:00
/**
* Triggered by the `attribute:complete:*` event (that is: gimme all attribute completions)
* @param attributeCompleteEvent
* @returns
*/
export async function objectAttributeCompleter(
2023-08-08 14:35:46 +00:00
attributeCompleteEvent: AttributeCompleteEvent,
): Promise<AttributeCompletion[]> {
2023-12-21 17:37:50 +00:00
const prefixFilter: QueryExpression = ["call", "startsWith", [[
"attr",
"name",
], ["string", attributeCompleteEvent.prefix]]];
const attributeFilter: QueryExpression | undefined =
attributeCompleteEvent.source === ""
2023-12-21 17:37:50 +00:00
? prefixFilter
: ["and", prefixFilter, ["=", ["attr", "tagName"], [
2023-12-21 17:37:50 +00:00
"string",
attributeCompleteEvent.source,
]]];
const allAttributes = await queryObjects<AttributeObject>("attribute", {
filter: attributeFilter,
2023-12-21 17:37:50 +00:00
distinct: true,
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
name: "readOnly",
}],
2023-08-08 14:35:46 +00:00
});
return allAttributes.map((value) => {
2023-08-08 14:35:46 +00:00
return {
name: value.name,
source: value.tagName,
attributeType: value.attributeType,
2023-12-21 17:37:50 +00:00
readOnly: value.readOnly,
} as AttributeCompletion;
2023-08-08 14:35:46 +00:00
});
}
2023-12-21 17:37:50 +00:00
/**
* Offer completions for _setting_ attributes on objects (either in frontmatter or inline)
* Triggered by `editor:complete` events from the editor
*/
2023-08-01 19:35:19 +00:00
export async function attributeComplete(completeEvent: CompleteEvent) {
if (/([\-\*]\s+\[)([^\]]+)$/.test(completeEvent.linePrefix)) {
// Don't match task states, which look similar
return null;
}
2023-12-21 17:37:50 +00:00
// Inline attribute completion (e.g. [myAttr: 10])
2023-08-04 19:35:58 +00:00
const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec(
2023-08-01 19:35:19 +00:00
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";
}
2023-08-08 14:35:46 +00:00
const completions = (await events.dispatchEvent(
`attribute:complete:${type}`,
{
source: type,
prefix: inlineAttributeMatch[2],
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
2023-08-01 19:35:19 +00:00
return {
from: completeEvent.pos - inlineAttributeMatch[2].length,
2023-08-08 14:35:46 +00:00
options: attributeCompletionsToCMCompletion(
2023-12-21 17:37:50 +00:00
// Filter out read-only attributes
completions.filter((completion) => !completion.readOnly),
2023-08-08 14:35:46 +00:00
),
2023-08-01 19:35:19 +00:00
};
}
2023-12-21 17:37:50 +00:00
// Frontmatter attribute completion
2023-08-01 19:35:19 +00:00
const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix);
if (attributeMatch) {
2023-12-21 17:37:50 +00:00
const frontmatterParent = completeEvent.parentNodes.find((node) =>
node.startsWith("FrontMatter:")
);
if (frontmatterParent) {
const tags = [
"page",
2023-12-21 17:37:50 +00:00
...determineTags(frontmatterParent.slice("FrontMatter:".length)),
];
const completions = (await Promise.all(tags.map((tag) =>
events.dispatchEvent(
`attribute:complete:${tag}`,
{
source: tag,
prefix: attributeMatch[1],
} as AttributeCompleteEvent,
)
))).flat(2) as AttributeCompletion[];
// console.log("Completions", completions);
2023-08-01 19:35:19 +00:00
return {
from: completeEvent.pos - attributeMatch[1].length,
2023-08-08 14:35:46 +00:00
options: attributeCompletionsToCMCompletion(
completions.filter((completion) =>
2023-12-21 17:37:50 +00:00
!completion.readOnly
),
2023-08-08 14:35:46 +00:00
),
2023-08-01 19:35:19 +00:00
};
}
}
return null;
}
2023-08-08 14:35:46 +00:00
export function attributeCompletionsToCMCompletion(
completions: AttributeCompletion[],
) {
return completions.map(
(completion) => ({
label: completion.name,
apply: `${completion.name}: `,
detail: `${completion.attributeType} (${completion.source})`,
2023-08-08 14:35:46 +00:00
type: "attribute",
}),
);
}