Make attribute extensible

pull/503/head
Zef Hemel 2023-08-08 16:35:46 +02:00
parent b7b666ee1d
commit 2f1f266482
4 changed files with 172 additions and 97 deletions

View File

@ -1,5 +1,6 @@
import { index } from "$sb/silverbullet-syscall/mod.ts";
import type { CompleteEvent } from "$sb/app_event.ts";
import { events } from "$sb/plugos-syscall/mod.ts";
export type AttributeContext = "page" | "item" | "task";
@ -7,6 +8,47 @@ type AttributeEntry = {
type: string;
};
export type AttributeCompleteEvent = {
source: string;
prefix: string;
};
export type AttributeCompletion = {
name: string;
source: string;
type: string;
builtin?: boolean;
};
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",
},
};
function determineType(v: any): string {
const t = typeof v;
if (t === "object") {
@ -37,6 +79,52 @@ export async function indexAttributes(
);
}
export async function customAttributeCompleter(
attributeCompleteEvent: AttributeCompleteEvent,
): Promise<AttributeCompletion[]> {
const sourcePrefix = attributeCompleteEvent.source === "*"
? ""
: `${attributeCompleteEvent.source}:`;
const allAttributes = await index.queryPrefix(
`${attributeKeyPrefix}${sourcePrefix}`,
);
return allAttributes.map((attr) => {
const [_prefix, context, name] = attr.key.split(":");
return {
name,
source: context,
type: attr.value.type,
};
});
}
export function builtinAttributeCompleter(
attributeCompleteEvent: AttributeCompleteEvent,
): AttributeCompletion[] {
let allAttributes = builtinAttributes[attributeCompleteEvent.source];
if (attributeCompleteEvent.source === "*") {
allAttributes = {};
for (const [source, attributes] of Object.entries(builtinAttributes)) {
for (const [name, type] of Object.entries(attributes)) {
allAttributes[name] = `${type}|${source}`;
}
}
}
if (!allAttributes) {
return [];
}
return Object.entries(allAttributes).map(([name, type]) => {
return {
name,
source: attributeCompleteEvent.source === "*"
? type.split("|")[1]
: attributeCompleteEvent.source,
type: attributeCompleteEvent.source === "*" ? type.split("|")[0] : type,
builtin: true,
};
});
}
export async function attributeComplete(completeEvent: CompleteEvent) {
const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec(
completeEvent.linePrefix,
@ -49,41 +137,50 @@ export async function attributeComplete(completeEvent: CompleteEvent) {
} else if (completeEvent.parentNodes.includes("ListItem")) {
type = "item";
}
const allAttributes = await index.queryPrefix(
`${attributeKeyPrefix}${type}:`,
);
const completions = (await events.dispatchEvent(
`attribute:complete:${type}`,
{
source: type,
prefix: inlineAttributeMatch[2],
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
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",
};
}),
options: attributeCompletionsToCMCompletion(
completions.filter((completion) => !completion.builtin),
),
};
}
const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix);
if (attributeMatch) {
if (completeEvent.parentNodes.includes("FrontMatterCode")) {
const allAttributes = await index.queryPrefix(
`${attributeKeyPrefix}page:`,
);
const completions = (await events.dispatchEvent(
`attribute:complete:page`,
{
source: "page",
prefix: attributeMatch[1],
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
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",
};
}),
options: attributeCompletionsToCMCompletion(
completions.filter((completion) => !completion.builtin),
),
};
}
}
return null;
}
export function attributeCompletionsToCMCompletion(
completions: AttributeCompletion[],
) {
return completions.map(
(completion) => ({
label: completion.name,
apply: `${completion.name}: `,
detail: `${completion.type} (${completion.source})`,
type: "attribute",
}),
);
}

View File

@ -96,6 +96,22 @@ functions:
events:
- editor:complete
customAttributeCompleter:
path: ./attributes.ts:customAttributeCompleter
events:
- attribute:complete:page
- attribute:complete:task
- attribute:complete:item
- attribute:complete:*
builtinAttributeCompleter:
path: ./attributes.ts:builtinAttributeCompleter
events:
- attribute:complete:page
- attribute:complete:task
- attribute:complete:item
- attribute:complete:*
# Commands
commandComplete:
path: "./command.ts:commandComplete"

View File

@ -2,36 +2,10 @@ 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",
},
};
import {
AttributeCompleteEvent,
AttributeCompletion,
} from "../core/attributes.ts";
export async function queryComplete(completeEvent: CompleteEvent) {
const querySourceMatch = /#query\s+([\w\-_]*)$/.exec(
@ -61,51 +35,22 @@ export async function queryComplete(completeEvent: CompleteEvent) {
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 completions = (await events.dispatchEvent(
`attribute:complete:${type}`,
{
source: type,
prefix: attributePrefix,
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
return {
from: completeEvent.pos - attributePrefix.length,
options: compileAttributeCompletions(allAttributes, type),
options: attributeCompletionsToCMCompletion(completions),
};
}
}
return null;
}
function compileAttributeCompletions(
allAttributes: { key: string; value: any }[],
type?: string,
) {
let allCompletions: any[] = allAttributes.map((attr) => {
const [_prefix, context, name] = attr.key.split(":");
return {
label: name,
detail: `${attr.value.type} (${context})`,
type: "attribute",
};
});
const allContexts = type ? [type] : Object.keys(builtinAttributes);
for (const context of allContexts) {
allCompletions = allCompletions.concat(
builtinAttributes[context]
? Object.entries(
builtinAttributes[context],
).map(([name, type]) => ({
label: name,
detail: `${type} (${context}: builtin)`,
type: "attribute",
}))
: [],
);
}
return allCompletions;
}
export async function templateVariableComplete(completeEvent: CompleteEvent) {
const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix);
if (!match) {
@ -123,9 +68,16 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) {
})),
);
const allAttributes = await index.queryPrefix(`attr:`);
const completions = (await events.dispatchEvent(
`attribute:complete:*`,
{
source: "*",
prefix: match[1],
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
allCompletions = allCompletions.concat(
compileAttributeCompletions(allAttributes),
attributeCompletionsToCMCompletion(completions),
);
return {
@ -133,3 +85,15 @@ export async function templateVariableComplete(completeEvent: CompleteEvent) {
options: allCompletions,
};
}
export function attributeCompletionsToCMCompletion(
completions: AttributeCompletion[],
) {
return completions.map(
(completion) => ({
label: completion.name,
detail: `${completion.type} (${completion.source})`,
type: "attribute",
}),
);
}

View File

@ -334,12 +334,11 @@ export class HttpServer {
fsRouter
.get(
filePathRegex,
// corsMiddleware,
async ({ params, response, request }) => {
const name = params[0];
console.log("Requested file", name);
if (!request.headers.has("X-Sync-Mode") && name.endsWith(".md")) {
// It can happen that during a sync, authentication expires
// It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page.
console.log("Request was without X-Sync-Mode, redirecting to page");
response.redirect(`/${name.slice(0, -3)}`);
return;
@ -401,7 +400,7 @@ export class HttpServer {
response.body = fileData.data;
} catch (e: any) {
console.error("Error GETting of file", name, e);
console.error("Error GETting file", name, e);
response.status = 404;
response.body = "Not found";
}
@ -409,7 +408,6 @@ export class HttpServer {
)
.put(
filePathRegex,
// corsMiddleware,
async ({ request, response, params }) => {
const name = params[0];
console.log("Saving file", name);