213 lines
6.1 KiB
TypeScript
213 lines
6.1 KiB
TypeScript
import type {
|
|
CompleteEvent,
|
|
SlashCompletionOption,
|
|
SlashCompletions,
|
|
} 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";
|
|
import type { TemplateObject } from "./types.ts";
|
|
import { loadPageObject } from "./page.ts";
|
|
import { renderTemplate } from "./api.ts";
|
|
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(
|
|
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) {
|
|
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);
|
|
const currentFrontmatter = await extractFrontmatter(
|
|
tree,
|
|
parsedFrontmatter,
|
|
);
|
|
if (!currentFrontmatter.tags?.length) {
|
|
delete currentFrontmatter.tags;
|
|
}
|
|
const newFrontmatter = deepObjectMerge(
|
|
currentFrontmatter,
|
|
parsedFrontmatter,
|
|
);
|
|
|
|
const dispatch = await prepareFrontmatterDispatch(
|
|
tree,
|
|
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();
|
|
|
|
if (snippetTemplate.match || snippetTemplate.matchRegex) {
|
|
const pageText = await editor.getText();
|
|
|
|
// Regex matching mode
|
|
const matchRegex = new RegExp(
|
|
(snippetTemplate.match || snippetTemplate.matchRegex)!,
|
|
);
|
|
|
|
let startOfLine = cursorPos;
|
|
while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
|
|
startOfLine--;
|
|
}
|
|
let endOfLine = cursorPos;
|
|
while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
|
|
endOfLine++;
|
|
}
|
|
let currentLine = pageText.slice(startOfLine, endOfLine);
|
|
const caretParts = replacementText.split("|^|");
|
|
const emptyLine = !currentLine;
|
|
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,
|
|
to: endOfLine,
|
|
insert: currentLine,
|
|
},
|
|
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",
|
|
}),
|
|
);
|
|
}
|