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 `"); 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("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!), ]; }