silverbullet/plugs/template/snippet.ts

213 lines
6.1 KiB
TypeScript
Raw Normal View History

2024-07-30 23:33:33 +08:00
import type {
CompleteEvent,
SlashCompletionOption,
SlashCompletions,
2024-02-29 22:23:05 +08:00
} from "../../plug-api/types.ts";
import {
editor,
markdown,
space,
system,
YAML,
} from "@silverbulletmd/silverbullet/syscalls";
import type { AttributeCompletion } from "../index/attributes.ts";
import { queryObjects } from "../index/plug_api.ts";
2024-07-30 23:33:33 +08:00
import type { TemplateObject } from "./types.ts";
import { loadPageObject } from "./page.ts";
import { renderTemplate } from "./api.ts";
2024-03-02 19:53:31 +08:00
import {
extractFrontmatter,
prepareFrontmatterDispatch,
} from "@silverbulletmd/silverbullet/lib/frontmatter";
import type { SnippetConfig } from "./types.ts";
import { deepObjectMerge } from "@silverbulletmd/silverbullet/lib/json";
export async function snippetSlashComplete(
completeEvent: CompleteEvent,
): Promise<SlashCompletions> {
const allTemplates = await queryObjects<TemplateObject>("template", {
// where hooks.snippet.slashCommand exists
filter: ["attr", ["attr", ["attr", "hooks"], "snippet"], "slashCommand"],
}, 5);
return {
options: allTemplates.map((template) => {
const snippetTemplate = template.hooks!.snippet!;
return {
label: snippetTemplate.slashCommand,
detail: template.description,
order: snippetTemplate.order || 0,
templatePage: template.ref,
pageName: completeEvent.pageName,
invoke: "template.insertSnippetTemplate",
};
}),
};
}
export async function insertSnippetTemplate(
slashCompletion: SlashCompletionOption,
) {
const pageObject = await loadPageObject(
2024-03-02 19:42:42 +08:00
slashCompletion.pageName || (await editor.getCurrentPage()),
);
const config = await system.getSpaceConfig();
const templateText = await space.readPage(slashCompletion.templatePage);
let { renderedFrontmatter, text: replacementText, frontmatter } =
await renderTemplate(
templateText,
pageObject,
{ page: pageObject, config },
);
const snippetTemplate: SnippetConfig = frontmatter.hooks.snippet;
let cursorPos = await editor.getCursor();
if (renderedFrontmatter) {
2024-03-02 19:53:31 +08:00
let parsedFrontmatter: Record<string, any> = {};
try {
parsedFrontmatter = await YAML.parse(renderedFrontmatter);
} catch (e: any) {
console.error(
`Invalid rendered for ${slashCompletion.templatePage}:`,
e.message,
"for frontmatter",
renderedFrontmatter,
);
await editor.flashNotification(
`Invalid frontmatter for ${slashCompletion.templatePage}, won't insert snippet`,
"error",
);
return;
}
const pageText = await editor.getText();
const tree = await markdown.parseMarkdown(pageText);
2024-03-02 19:53:31 +08:00
const currentFrontmatter = await extractFrontmatter(
tree,
parsedFrontmatter,
);
if (!currentFrontmatter.tags?.length) {
delete currentFrontmatter.tags;
}
const newFrontmatter = deepObjectMerge(
currentFrontmatter,
parsedFrontmatter,
);
const dispatch = await prepareFrontmatterDispatch(
tree,
2024-03-02 19:53:31 +08:00
newFrontmatter,
);
if (cursorPos === 0) {
dispatch.selection = { anchor: renderedFrontmatter.length + 9 };
}
await editor.dispatch(dispatch);
// update cursor position
cursorPos = await editor.getCursor();
}
if (snippetTemplate.insertAt) {
switch (snippetTemplate.insertAt) {
case "page-start":
await editor.moveCursor(0);
break;
case "page-end":
await editor.moveCursor((await editor.getText()).length);
break;
case "line-start": {
const pageText = await editor.getText();
let startOfLine = cursorPos;
while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
startOfLine--;
}
await editor.moveCursor(startOfLine);
break;
}
case "line-end": {
const pageText = await editor.getText();
let endOfLine = cursorPos;
while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
endOfLine++;
}
await editor.moveCursor(endOfLine);
break;
}
default:
// Deliberate no-op
}
}
cursorPos = await editor.getCursor();
2024-01-22 23:25:55 +08:00
if (snippetTemplate.match || snippetTemplate.matchRegex) {
const pageText = await editor.getText();
2024-02-24 16:08:10 +08:00
// Regex matching mode
2024-01-22 23:25:55 +08:00
const matchRegex = new RegExp(
(snippetTemplate.match || snippetTemplate.matchRegex)!,
);
let startOfLine = cursorPos;
while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
startOfLine--;
}
2024-02-24 16:08:10 +08:00
let endOfLine = cursorPos;
while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
endOfLine++;
}
let currentLine = pageText.slice(startOfLine, endOfLine);
const caretParts = replacementText.split("|^|");
const emptyLine = !currentLine;
2024-02-24 16:08:10 +08:00
currentLine = currentLine.replace(matchRegex, caretParts[0]);
let newSelection = emptyLine
? {
anchor: startOfLine + currentLine.length,
}
: undefined;
if (caretParts.length === 2) {
// The semantics of a caret in a replacement are:
// 1. It's a caret, so we need to move the cursor there
// 2. It's a placeholder, so we need to remove it
// 3. Any text after the caret should be inserted after the caret
const caretPos = currentLine.length;
// Now add the text after the caret
currentLine += caretParts[1];
newSelection = {
anchor: startOfLine + caretPos,
};
}
await editor.dispatch({
changes: {
from: startOfLine,
2024-02-24 16:08:10 +08:00
to: endOfLine,
insert: currentLine,
},
2024-02-24 16:08:10 +08:00
selection: newSelection,
});
} else {
const carretPos = replacementText.indexOf("|^|");
replacementText = replacementText.replace("|^|", "");
await editor.insertAtCursor(replacementText);
if (carretPos !== -1) {
await editor.moveCursor(cursorPos + carretPos);
}
}
}
export function attributeCompletionsToCMCompletion(
completions: AttributeCompletion[],
) {
return completions.map(
(completion) => ({
label: completion.name,
detail: `${completion.attributeType} (${completion.source})`,
type: "attribute",
}),
);
}