109 lines
3.3 KiB
TypeScript
109 lines
3.3 KiB
TypeScript
import type { KeyBinding } from "@codemirror/view";
|
||
import { syntaxTree } from "@codemirror/language";
|
||
import { EditorSelection } from "@codemirror/state";
|
||
import type { Client } from "../client.ts";
|
||
|
||
import type { SmartQuotesConfig } from "../../plug-api/types/config.ts";
|
||
|
||
const straightQuoteContexts = [
|
||
"CommentBlock",
|
||
"CodeBlock",
|
||
"CodeText",
|
||
"FencedCode",
|
||
"InlineCode",
|
||
"FrontMatterCode",
|
||
"Attribute",
|
||
"LuaDirective",
|
||
"HTMLTag",
|
||
];
|
||
|
||
// TODO: Add support for selection (put quotes around or create blockquote block?)
|
||
function keyBindingForQuote(
|
||
originalQuote: string,
|
||
left: string,
|
||
right: string,
|
||
): KeyBinding {
|
||
return {
|
||
any: (target, event): boolean => {
|
||
// Moving this check here rather than using the regular "key" property because
|
||
// for some reason the "ä" key is not recognized as a quote key by CodeMirror.
|
||
if (event.key !== originalQuote) {
|
||
return false;
|
||
}
|
||
const cursorPos = target.state.selection.main.from;
|
||
const chBefore = target.state.sliceDoc(cursorPos - 1, cursorPos);
|
||
|
||
// Figure out the context, if in some sort of code/comment fragment don't be smart
|
||
let node = syntaxTree(target.state).resolveInner(cursorPos);
|
||
while (node) {
|
||
if (
|
||
straightQuoteContexts.find((sqc) => sqc.startsWith(node.type.name))
|
||
) {
|
||
return false;
|
||
}
|
||
if (node.parent) {
|
||
node = node.parent;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Text-based fallback: detect if we're inside an unclosed HTML tag
|
||
// (handles incomplete tags like `<marquee class=` that the parser
|
||
// doesn't recognize as HTMLTag yet)
|
||
const line = target.state.doc.lineAt(cursorPos);
|
||
const textBefore = target.state.sliceDoc(line.from, cursorPos);
|
||
const lastOpen = textBefore.lastIndexOf("<");
|
||
const lastClose = textBefore.lastIndexOf(">");
|
||
if (lastOpen > lastClose) {
|
||
return false;
|
||
}
|
||
|
||
// Ok, still here, let's use a smart quote
|
||
const changes = target.state.changeByRange((range) => {
|
||
if (!range.empty) {
|
||
return {
|
||
changes: [
|
||
{ insert: left, from: range.from },
|
||
{ insert: right, from: range.to },
|
||
],
|
||
range: EditorSelection.range(
|
||
range.anchor + left.length,
|
||
range.head + left.length,
|
||
),
|
||
};
|
||
} else {
|
||
const quote =
|
||
/\W/.exec(chBefore) && !/[!?,.\-=“]/.exec(chBefore) ? left : right;
|
||
|
||
return {
|
||
changes: {
|
||
insert: quote,
|
||
from: cursorPos,
|
||
},
|
||
range: EditorSelection.cursor(range.anchor + quote.length),
|
||
};
|
||
}
|
||
});
|
||
target.dispatch(changes);
|
||
|
||
return true;
|
||
},
|
||
};
|
||
}
|
||
|
||
export function createSmartQuoteKeyBindings(client: Client): KeyBinding[] {
|
||
const smartQuotes = client.config.get<SmartQuotesConfig>("smartQuotes", {
|
||
enabled: true,
|
||
double: { left: "“", right: "”" },
|
||
single: { left: "‘", right: "’" },
|
||
});
|
||
if (smartQuotes.enabled === false) {
|
||
return [];
|
||
}
|
||
return [
|
||
keyBindingForQuote('"', smartQuotes.double!.left!, smartQuotes.double!.right!),
|
||
keyBindingForQuote("'", smartQuotes.single!.left!, smartQuotes.single!.right!),
|
||
];
|
||
}
|