LaTeX Suite Snippets

LaTeX snippets with full options, visual snippets, matrix shortcuts, brace-args jump (\frac{x}{y} -> Tab selects x/y), env switch, smart fraction, tabout, auto-enlarge, robust CE range replacement. Circle toggle.

Fra 07.10.2025. Se den seneste versjonen.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         LaTeX Suite Snippets
// @namespace    https://nekko-obsidian-latex-suite
// @version      0.7.1
// @description  LaTeX snippets with full options, visual snippets, matrix shortcuts, brace-args jump (\frac{x}{y} -> Tab selects x/y), env switch, smart fraction, tabout, auto-enlarge, robust CE range replacement. Circle toggle.
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==
(() => {
  'use strict';

  /*************************************************************************
   * 配置
   *************************************************************************/
  const NON_AUTO_TRIGGER_KEY = 'Tab';
  const FRACTION_CMD = "\\frac";
  const EXCLUDED_ENVIRONMENTS = [ ["^{", "}"], ["\\pu{", "}"] ];
  const BREAKING_CHARS = "+-=";
  const TABOUT_ENABLED = true;
  const AUTO_ENLARGE_ENABLED = true;
  const AUTO_ENLARGE_TRIGGERS = ["\\sum", "\\int", "\\frac", "\\prod"];

  const MATRIX_ENV_NAMES = [
    "matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix",
    "array", "aligned", "align", "align*", "cases"
  ];
  const ENV_CYCLES = [
    ["matrix", "pmatrix", "bmatrix", "Bmatrix", "vmatrix", "Vmatrix"],
    ["align", "align*", "aligned"],
    ["array"],
    ["cases"]
  ];

  /*************************************************************************
   * 你的 snippets(把你的数组粘贴到这里)
   *************************************************************************/
const SNIPPETS = [
  // Math mode
  {trigger: "mk", replacement: "$$0$", options: "tA"},
  {trigger: "dm", replacement: "$$\n$0\n$$", options: "tAw"},
  {trigger: "beg", replacement: "\\begin{$0}\n$1\n\\end{$0}", options: "mA"},


  // Dashes
  // {trigger: "--", replacement: "–", options: "tA"},
  // {trigger: "–-", replacement: "—", options: "tA"},
  // {trigger: "—-", replacement: "---", options: "tA"},


  // Greek letters
  {trigger: "@a", replacement: "\\alpha", options: "mA"},
  {trigger: "@A", replacement: "\\alpha", options: "mA"},
  {trigger: "@b", replacement: "\\beta", options: "mA"},
  {trigger: "@B", replacement: "\\beta", options: "mA"},
  {trigger: "@c", replacement: "\\chi", options: "mA"},
  {trigger: "@C", replacement: "\\chi", options: "mA"},
  {trigger: "@g", replacement: "\\gamma", options: "mA"},
  {trigger: "@G", replacement: "\\Gamma", options: "mA"},
  {trigger: "@d", replacement: "\\delta", options: "mA"},
  {trigger: "@D", replacement: "\\Delta", options: "mA"},
  {trigger: "@e", replacement: "\\epsilon", options: "mA"},
  {trigger: "@E", replacement: "\\epsilon", options: "mA"},
  {trigger: ":e", replacement: "\\varepsilon", options: "mA"},
  {trigger: ":E", replacement: "\\varepsilon", options: "mA"},
  {trigger: "@z", replacement: "\\zeta", options: "mA"},
  {trigger: "@Z", replacement: "\\zeta", options: "mA"},
  {trigger: "@t", replacement: "\\theta", options: "mA"},
  {trigger: "@T", replacement: "\\Theta", options: "mA"},
  {trigger: "@k", replacement: "\\kappa", options: "mA"},
  {trigger: "@K", replacement: "\\kappa", options: "mA"},
  {trigger: "@l", replacement: "\\lambda", options: "mA"},
  {trigger: "@L", replacement: "\\Lambda", options: "mA"},
  {trigger: "@m", replacement: "\\mu", options: "mA"},
  {trigger: "@M", replacement: "\\mu", options: "mA"},
  {trigger: "@r", replacement: "\\rho", options: "mA"},
  {trigger: "@R", replacement: "\\rho", options: "mA"},
  {trigger: "@s", replacement: "\\sigma", options: "mA"},
  {trigger: "@S", replacement: "\\Sigma", options: "mA"},
  {trigger: "ome", replacement: "\\omega", options: "mA"},
  {trigger: "@o", replacement: "\\omega", options: "mA"},
  {trigger: "@O", replacement: "\\Omega", options: "mA"},
  {trigger: "([^\\\\])(${GREEK}|${SYMBOL})", replacement: "[[0]]\\[[1]]", options: "rmA", description: "Add backslash before greek letters and symbols"},


  // Insert space after greek letters and symbols, etc
  {trigger: "\\\\(${GREEK}|${SYMBOL}|${SHORT_SYMBOL})([A-Za-z])", replacement: "\\[[0]] [[1]]", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) sr", replacement: "\\[[0]]^{2}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) cb", replacement: "\\[[0]]^{3}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) rd", replacement: "\\[[0]]^{$0}$1", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) hat", replacement: "\\hat{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) dot", replacement: "\\dot{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) bar", replacement: "\\bar{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) vec", replacement: "\\vec{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) tilde", replacement: "\\tilde{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}|${SYMBOL}) und", replacement: "\\underline{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK}),\\.", replacement: "\\boldsymbol{\\[[0]]}", options: "rmA"},
  {trigger: "\\\\(${GREEK})\\.,", replacement: "\\boldsymbol{\\[[0]]}", options: "rmA"},


  // Operations
  {trigger: "te", replacement: "\\text{$0}", options: "m"},
  {trigger: "text", replacement: "\\text{$0}", options: "mA"},
  {trigger: "bf", replacement: "\\mathbf{$0}", options: "mA"},
  {trigger: "sr", replacement: "^{2}", options: "mA"},
  {trigger: "cb", replacement: "^{3}", options: "mA"},
  {trigger: "rd", replacement: "^{$0}$1", options: "mA"},
  {trigger: "_", replacement: "_{$0}$1", options: "mA"},
  {trigger: "sts", replacement: "_\\text{$0}", options: "rmA"},
  {trigger: "sq", replacement: "\\sqrt{ $0 }$1", options: "mA"},
  {trigger: "//", replacement: "\\frac{$0}{$1}$2", options: "mA"},
  {trigger: "ee", replacement: "e^{ $0 }$1", options: "mA"},
  {trigger: "rm", replacement: "\\mathrm{$0}$1", options: "mA"},
  {trigger: "conj", replacement: "^{*}", options: "mA"},
  {trigger: "trace", replacement: "\\mathrm{Tr}", options: "mA"},
  {trigger: "det", replacement: "\\det", options: "mA"},
  {trigger: "re", replacement: "\\mathrm{Re}", options: "mA"},
  {trigger: "im", replacement: "\\mathrm{Im}", options: "mA"},

  {trigger: "([a-zA-Z]),\\.", replacement: "\\mathbf{[[0]]}", options: "rmA"},
  {trigger: "([a-zA-Z])\\.,", replacement: "\\mathbf{[[0]]}", options: "rmA"},
  {trigger: "([A-Za-z])(\\d)", replacement: "[[0]]_{[[1]]}", options: "rmA", description: "Auto letter subscript", priority: -1},
  {trigger: "([A-Za-z])_(\\d\\d)", replacement: "[[0]]_{[[1]]}", options: "rmA"},
  {trigger: "\\hat{([A-Za-z])}(\\d)", replacement: "hat{[[0]]}_{[[1]]}", options: "rmA"},
  {trigger: "\\\\mathbf{([A-Za-z])}(\\d)", replacement: "\\mathbf{[[0]]}_{[[1]]}", options: "rmA"},
  {trigger: "\\\\vec{([A-Za-z])}(\\d)", replacement: "\\vec{[[0]]}_{[[1]]}", options: "rmA"},
  {trigger: "([a-zA-Z])bar", replacement: "\\bar{[[0]]}", options: "rmA"},
  {trigger: "([a-zA-Z])hat", replacement: "\\hat{[[0]]}", options: "rmA"},
  {trigger: "([a-zA-Z])ddot", replacement: "\\ddot{[[0]]}", options: "rmA", priority: 3},
  {trigger: "([a-zA-Z])dot", replacement: "\\dot{[[0]]}", options: "rmA", priority: 1},
  {trigger: "([a-zA-Z])vec", replacement: "\\vec{[[0]]}", options: "rmA"},
  {trigger: "([a-zA-Z])tilde", replacement: "\\tilde{[[0]]}", options: "rmA"},
  {trigger: "([a-zA-Z])und", replacement: "\\underline{[[0]]}", options: "rmA"},
  {trigger: "bar", replacement: "\\bar{$0}$1", options: "mA"},
  {trigger: "hat", replacement: "\\hat{$0}$1", options: "mA"},
  {trigger: "dot", replacement: "\\dot{$0}$1", options: "mA"},
  {trigger: "ddot", replacement: "\\ddot{$0}$1", options: "mA", priority: 2},
  {trigger: "cdot", replacement: "\\cdot", options: "mA", priority: 2},
  {trigger: "vec", replacement: "\\vec{$0}$1", options: "mA"},
  {trigger: "tilde", replacement: "\\tilde{$0}$1", options: "mA"},
  {trigger: "und", replacement: "\\underline{$0}$1", options: "mA"},

  {trigger: "([^\\\\])(arcsin|arccos|arctan|arccot|arccsc|arcsec|sin|cos|tan|cot|csc|sec)", replacement: "[[0]]\\[[1]]", options: "rmA"},
  {trigger: "\\\\(arcsin|arccos|arctan|arccot|arccsc|arcsec|sin|cos|tan|cot|csc|sec)([A-Za-gi-z])", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after trig funcs. Skips letter "h" to allow sinh, cosh, etc.
  {trigger: "\\\\(arcsinh|arccosh|arctanh|arccoth|arcsch|arcsech|sinh|cosh|tanh|coth|csch|sech)([A-Za-z])", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after trig funcs
  {trigger: "\\\\(neq|geq|leq|gg|ll|sim)([0-9]+)", replacement: "\\[[0]] [[1]]", options: "rmA"}, // Insert space after inequality symbols


  // Visual operations
  {trigger: "U", replacement: "\\underbrace{ ${VISUAL} }_{ $0 }", options: "mA"},
  {trigger: "B", replacement: "\\underset{ $0 }{ ${VISUAL} }", options: "mA"},
  {trigger: "C", replacement: "\\cancel{ ${VISUAL} }", options: "mA"},
  {trigger: "K", replacement: "\\cancelto{ $0 }{ ${VISUAL} }", options: "mA"},
  {trigger: "S", replacement: "\\sqrt{ ${VISUAL} }", options: "mA"},



  // Symbols
  {trigger: "ooo", replacement: "\\infty", options: "mA"},
  {trigger: "sum", replacement: "\\sum", options: "mA"},
  {trigger: "prod", replacement: "\\prod", options: "mA"},
  {trigger: "lim", replacement: "\\lim_{ ${0:n} \\to ${1:\\infty} } $2", options: "mA"},
  {trigger: "([^\\\\])pm", replacement: "[[0]]\\pm", options: "rm"},
  {trigger: "([^\\\\])mp", replacement: "[[0]]\\mp", options: "rm"},
  {trigger: "+-", replacement: "\\pm", options: "mA"},
  {trigger: "-+", replacement: "\\mp", options: "mA"},
  {trigger: "...", replacement: "\\dots", options: "mA"},
  {trigger: "<->", replacement: "\\leftrightarrow ", options: "mA"},
  {trigger: "->", replacement: "\\to", options: "mA"},
  {trigger: "!>", replacement: "\\mapsto", options: "mA"},
  {trigger: "invs", replacement: "^{-1}", options: "mA"},
  {trigger: "\\\\\\", replacement: "\\setminus", options: "mA"},
  {trigger: "||", replacement: "\\mid", options: "mA"},
  {trigger: "and", replacement: "\\cap", options: "mA"},
  {trigger: "orr", replacement: "\\cup", options: "mA"},
  {trigger: "inn", replacement: "\\in", options: "mA"},
  {trigger: "\\subset eq", replacement: "\\subseteq", options: "mA"},
  {trigger: "set", replacement: "\\{ $0 \\}$1", options: "mA"},
  {trigger: "=>", replacement: "\\implies", options: "mA"},
  {trigger: "=<", replacement: "\\impliedby", options: "mA"},
  {trigger: "iff", replacement: "\\iff", options: "mA"},
  {trigger: "e\\xi sts", replacement: "\\exists", options: "mA", priority: 1},
  {trigger: "===", replacement: "\\equiv", options: "mA"},
  {trigger: "Sq", replacement: "\\square", options: "mA"},
  {trigger: "!=", replacement: "\\neq", options: "mA"},
  {trigger: ">=", replacement: "\\geq", options: "mA"},
  {trigger: "<=", replacement: "\\leq", options: "mA"},
  {trigger: ">>", replacement: "\\gg", options: "mA"},
  {trigger: "<<", replacement: "\\ll", options: "mA"},
  {trigger: "~~", replacement: "\\sim", options: "mA"},
  {trigger: "\\sim ~", replacement: "\\approx", options: "mA"},
  {trigger: "prop", replacement: "\\propto", options: "mA"},
  {trigger: "nabl", replacement: "\\nabla", options: "mA"},
  {trigger: "del", replacement: "\\nabla", options: "mA"},
  {trigger: "xx", replacement: "\\times", options: "mA"},
  {trigger: "**", replacement: "\\cdot", options: "mA"},
  {trigger: "para", replacement: "\\parallel", options: "mA"},


  {trigger: "xnn", replacement: "x_{n}", options: "mA"},
  {trigger: "xii", replacement: "x_{i}", options: "mA"},
  {trigger: "xjj", replacement: "x_{j}", options: "mA"},
  {trigger: "xp1", replacement: "x_{n+1}", options: "mA"},
  {trigger: "ynn", replacement: "y_{n}", options: "mA"},
  {trigger: "yii", replacement: "y_{i}", options: "mA"},
  {trigger: "yjj", replacement: "y_{j}", options: "mA"},


  {trigger: "mcal", replacement: "\\mathcal{$0}$1", options: "mA"},
  {trigger: "mbb", replacement: "\\mathbb{$0}$1", options: "mA"},
  {trigger: "ell", replacement: "\\ell", options: "mA"},
  {trigger: "lll", replacement: "\\ell", options: "mA"},
  {trigger: "LL", replacement: "\\mathcal{L}", options: "mA"},
  {trigger: "HH", replacement: "\\mathcal{H}", options: "mA"},
  {trigger: "CC", replacement: "\\mathbb{C}", options: "mA"},
  {trigger: "RR", replacement: "\\mathbb{R}", options: "mA"},
  {trigger: "ZZ", replacement: "\\mathbb{Z}", options: "mA"},
  {trigger: "NN", replacement: "\\mathbb{N}", options: "mA"},
  {trigger: "II", replacement: "\\mathbb{1}", options: "mA"},
  {trigger: "\\mathbb{1}I", replacement: "\\hat{\\mathbb{1}}", options: "mA"},
  {trigger: "AA", replacement: "\\mathcal{A}", options: "mA"},
  {trigger: "BB", replacement: "\\mathbf{B}", options: "mA"},
  {trigger: "EE", replacement: "\\mathbf{E}", options: "mA"},



  // Unit vectors
  {trigger: ":i", replacement: "\\mathbf{i}", options: "mA"},
  {trigger: ":j", replacement: "\\mathbf{j}", options: "mA"},
  {trigger: ":k", replacement: "\\mathbf{k}", options: "mA"},
  {trigger: ":x", replacement: "\\hat{\\mathbf{x}}", options: "mA"},
  {trigger: ":y", replacement: "\\hat{\\mathbf{y}}", options: "mA"},
  {trigger: ":z", replacement: "\\hat{\\mathbf{z}}", options: "mA"},



  // Derivatives
  {trigger: "par", replacement: "\\frac{ \\partial ${0:y} }{ \\partial ${1:x} } $2", options: "m"},
  {trigger: "pa2", replacement: "\\frac{ \\partial^{2} ${0:y} }{ \\partial ${1:x}^{2} } $2", options: "mA"},
  {trigger: "pa3", replacement: "\\frac{ \\partial^{3} ${0:y} }{ \\partial ${1:x}^{3} } $2", options: "mA"},
  {trigger: "pa([A-Za-z])([A-Za-z])", replacement: "\\frac{ \\partial [[0]] }{ \\partial [[1]] } ", options: "rm"},
  {trigger: "pa([A-Za-z])([A-Za-z])([A-Za-z])", replacement: "\\frac{ \\partial^{2} [[0]] }{ \\partial [[1]] \\partial [[2]] } ", options: "rm"},
  {trigger: "pa([A-Za-z])([A-Za-z])2", replacement: "\\frac{ \\partial^{2} [[0]] }{ \\partial [[1]]^{2} } ", options: "rmA"},
  {trigger: "de([A-Za-z])([A-Za-z])", replacement: "\\frac{ d[[0]] }{ d[[1]] } ", options: "rm"},
  {trigger: "de([A-Za-z])([A-Za-z])2", replacement: "\\frac{ d^{2}[[0]] }{ d[[1]]^{2} } ", options: "rmA"},
  {trigger: "ddt", replacement: "\\frac{d}{dt} ", options: "mA"},



  // Integrals
  {trigger: "oinf", replacement: "\\int_{0}^{\\infty} $0 \\, d${1:x} $2", options: "mA"},
  {trigger: "infi", replacement: "\\int_{-\\infty}^{\\infty} $0 \\, d${1:x} $2", options: "mA"},
  {trigger: "dint", replacement: "\\int_{${0:0}}^{${1:\\infty}} $2 \\, d${3:x} $4", options: "mA"},
  {trigger: "oint", replacement: "\\oint", options: "mA"},
  {trigger: "iiint", replacement: "\\iiint", options: "mA"},
  {trigger: "iint", replacement: "\\iint", options: "mA"},
  {trigger: "int", replacement: "\\int $0 \\, d${1:x} $2", options: "mA"},



  // Physics
  {trigger: "kbt", replacement: "k_{B}T", options: "mA"},


  // Quantum mechanics
  {trigger: "hba", replacement: "\\hbar", options: "mA"},
  {trigger: "dag", replacement: "^{\\dagger}", options: "mA"},
  {trigger: "o+", replacement: "\\oplus ", options: "mA"},
  {trigger: "ox", replacement: "\\otimes ", options: "mA"},
  {trigger: "ot\\mathrm{Im}es", replacement: "\\otimes ", options: "mA"}, // Handle conflict with "im" snippet
  {trigger: "bra", replacement: "\\bra{$0} $1", options: "mA"},
  {trigger: "ket", replacement: "\\ket{$0} $1", options: "mA"},
  {trigger: "brk", replacement: "\\braket{ $0 | $1 } $2", options: "mA"},
  {trigger: "\\\\bra{([^|]+)\\|", replacement: "\\braket{ [[0]] | $0 ", options: "rmA", description: "Convert bra into braket"},
  {trigger: "\\\\bra{(.+)}([^ ]+)>", replacement: "\\braket{ [[0]] | $0 ", options: "rmA", description: "Convert bra into braket (alternate)"},
  {trigger: "outp", replacement: "\\ket{${0:\\psi}} \\bra{${0:\\psi}} $1", options: "mA"},



  // Chemistry
  {trigger: "pu", replacement: "\\pu{ $0 }", options: "mA"},
  {trigger: "msun", replacement: "M_{\\odot}", options: "mA"},
  {trigger: "solm", replacement: "M_{\\odot}", options: "mA"},
  {trigger: "ce", replacement: "\\ce{ $0 }", options: "mA"},
  {trigger: "iso", replacement: "{}^{${0:4}}_{${1:2}}${2:He}", options: "mA"},
  {trigger: "hel4", replacement: "{}^{4}_{2}He ", options: "mA"},
  {trigger: "hel3", replacement: "{}^{3}_{2}He ", options: "mA"},



  // Environments
  {trigger: "pmat", replacement: "\\begin{pmatrix}\n$0\n\\end{pmatrix}", options: "mA"},
  {trigger: "bmat", replacement: "\\begin{bmatrix}\n$0\n\\end{bmatrix}", options: "mA"},
  {trigger: "Bmat", replacement: "\\begin{Bmatrix}\n$0\n\\end{Bmatrix}", options: "mA"},
  {trigger: "vmat", replacement: "\\begin{vmatrix}\n$0\n\\end{vmatrix}", options: "mA"},
  {trigger: "Vmat", replacement: "\\begin{Vmatrix}\n$0\n\\end{Vmatrix}", options: "mA"},
  {trigger: "case", replacement: "\\begin{cases}\n$0\n\\end{cases}", options: "mA"},
  {trigger: "align", replacement: "\\begin{align}\n$0\n\\end{align}", options: "mA"},
  {trigger: "array", replacement: "\\begin{array}\n$0\n\\end{array}", options: "mA"},
  {trigger: "matrix", replacement: "\\begin{matrix}\n$0\n\\end{matrix}", options: "mA"},



  // Brackets
  {trigger: "avg", replacement: "\\langle $0 \\rangle $1", options: "mA"},
  {trigger: "norm", replacement: "\\lvert $0 \\rvert $1", options: "mA", priority: 1},
  {trigger: "mod", replacement: "|$0|$1", options: "mA"},
  {trigger: "(", replacement: "(${VISUAL})", options: "mA"},
  {trigger: "[", replacement: "[${VISUAL}]", options: "mA"},
  {trigger: "{", replacement: "{${VISUAL}}", options: "mA"},
  {trigger: "(", replacement: "($0)$1", options: "mA"},
  {trigger: "{", replacement: "{$0}$1", options: "mA"},
  {trigger: "[", replacement: "[$0]$1", options: "mA"},
  {trigger: "lr(", replacement: "\\left( $0 \\right) $1", options: "mA"},
  {trigger: "lr|", replacement: "\\left| $0 \\right| $1", options: "mA"},
  {trigger: "lr{", replacement: "\\left\\{ $0 \\right\\} $1", options: "mA"},
  {trigger: "lr[", replacement: "\\left[ $0 \\right] $1", options: "mA"},
  {trigger: "lra", replacement: "\\left< $0 \\right> $1", options: "mA"},



  // Misc
  {trigger: "tayl", replacement: "${0:f}(${1:x} + ${2:h}) = ${0:f}(${1:x}) + ${0:f}'(${1:x})${2:h} + ${0:f}''(${1:x}) \\frac{${2:h}^{2}}{2!} + \\dots$3", options: "mA"},
];

  /*************************************************************************
   * 状态
   *************************************************************************/
  const MAX_LOOKBEHIND = 256;
  const CARET_TOKEN = '\uFFF9';
  const FIELD_START = '\uFFF0';
  const FIELD_END   = '\uFFF1';
  const STORE_KEY_ENABLED = 'latex_suite_enabled';
  const STORE_KEY_BTN_POS = 'latex_suite_button_pos';

  // 默认 OFF
  let enabled = !!GM_getValue(STORE_KEY_ENABLED, false);
  let _latexSuiteBusy = false;
  let _isComposing = false;
  let _allowOnceAfterComposition = false;

  /*************************************************************************
   * 圆形小球按钮(右上角,拖动,ON=不透明,OFF=0.5)
   *************************************************************************/
  GM_addStyle(`
    .latex-suite-toggle {
      position: fixed; top: 12px; right: 12px; z-index: 2147483647;
      width: 44px; height: 44px; border-radius: 50%;
      box-shadow: 0 2px 10px rgba(0,0,0,0.2);
      cursor: move; user-select: none;
      display: flex; align-items: center; justify-content: center;
      color: #fff; font-size: 12px; font-family: system-ui,-apple-system,"Segoe UI",Roboto,Arial,"Noto Sans","Helvetica Neue","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
      background: #22c55e;
      opacity: ${enabled ? '1' : '0.5'};
    }
    .latex-suite-off { background: #ef4444; }
  `);
  const btn = document.createElement('div');
  btn.className = `latex-suite-toggle ${enabled ? '' : 'latex-suite-off'}`;
  btn.textContent = 'La';
  document.documentElement.appendChild(btn);

  (function restoreBtnPos() {
    const pos = GM_getValue(STORE_KEY_BTN_POS);
    if (!pos) return;
    const { top, left } = pos;
    btn.style.top = top + 'px';
    btn.style.left = left + 'px';
    btn.style.right = 'unset';
  })();
  (function initDragAndToggle() {
    let dragging = false, moved = false, startX = 0, startY = 0, startTop = 0, startLeft = 0;
    btn.addEventListener('mousedown', (e) => {
      dragging = true; moved = false; btn.style.cursor = 'grabbing';
      startX = e.clientX; startY = e.clientY;
      const r = btn.getBoundingClientRect(); startTop = r.top; startLeft = r.left;
      e.preventDefault();
    });
    window.addEventListener('mousemove', (e) => {
      if (!dragging) return;
      const dx = e.clientX - startX, dy = e.clientY - startY;
      if (Math.abs(dx) > 2 || Math.abs(dy) > 2) moved = true;
      btn.style.top = Math.max(0, startTop + dy) + 'px';
      btn.style.left = Math.max(0, startLeft + dx) + 'px';
      btn.style.right = 'unset';
    }, true);
    window.addEventListener('mouseup', () => {
      if (!dragging) return; dragging = false; btn.style.cursor = 'move';
      if (!moved) {
        enabled = !enabled;
        GM_setValue(STORE_KEY_ENABLED, enabled);
        btn.classList.toggle('latex-suite-off', !enabled);
        btn.style.opacity = enabled ? '1' : '0.5';
      } else {
        const r = btn.getBoundingClientRect();
        GM_setValue(STORE_KEY_BTN_POS, { top: r.top, left: r.left });
      }
    }, true);
  })();

  /*************************************************************************
   * 编辑区/CE 工具
   *************************************************************************/
  function getActiveEditable() {
    const el = document.activeElement;
    if (!el) return null;
    const isTextarea = el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && el.type === 'text');
    const isCE = el.isContentEditable;
    if (isTextarea || isCE) return el;
    return null;
  }
  function getSelectionInCE(root) {
    const sel = window.getSelection();
    if (!sel || sel.rangeCount === 0) return null;
    const range = sel.getRangeAt(0);
    if (!root.contains(range.startContainer)) return null;
    return range;
  }
  function getTextAndCaret(el) {
    if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
      return { type: 'textarea', text: el.value, start: el.selectionStart, end: el.selectionEnd };
    } else {
      const range = getSelectionInCE(el); if (!range) return null;
      const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
      let text = '', startOff = -1, endOff = -1, cur = 0;
      while (walker.nextNode()) {
        const node = walker.currentNode;
        if (node === range.startContainer) startOff = cur + range.startOffset;
        if (node === range.endContainer)   endOff = cur + range.endOffset;
        text += node.nodeValue; cur += node.nodeValue.length;
      }
      return { type: 'contenteditable', text, start: Math.min(startOff, endOff), end: Math.max(startOff, endOff), root: el };
    }
  }
  function locateNodeByOffset(root, targetOffset) {
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
    let offset = 0;
    while (walker.nextNode()) {
      const node = walker.currentNode;
      const next = offset + node.nodeValue.length;
      if (targetOffset <= next) return { node, local: targetOffset - offset };
      offset = next;
    }
    return { node: root, local: root.childNodes.length };
  }
  function setCaretOnly(ed, sel, newPos) {
    if (sel.type === 'textarea') {
      ed.setSelectionRange(newPos, newPos);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    } else {
      const range = document.createRange();
      const { node, local } = locateNodeByOffset(ed, newPos);
      if (node.nodeType === Node.TEXT_NODE) range.setStart(node, local);
      else range.setStart(ed, ed.childNodes.length);
      range.collapse(true);
      const s = window.getSelection(); s.removeAllRanges(); s.addRange(range);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    }
  }
  function setSelectionInEditor(ed, sel, start, end) {
    if (sel.type === 'textarea') {
      ed.setSelectionRange(start, end);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    } else {
      const a = locateNodeByOffset(ed, start);
      const b = locateNodeByOffset(ed, end);
      const range = document.createRange();
      if (a.node.nodeType === Node.TEXT_NODE) range.setStart(a.node, a.local);
      else range.setStart(ed, ed.childNodes.length);
      if (b.node.nodeType === Node.TEXT_NODE) range.setEnd(b.node, b.local);
      else range.setEnd(ed, ed.childNodes.length);
      const s = window.getSelection(); s.removeAllRanges(); s.addRange(range);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    }
  }
  function replaceRangeInEditor(ed, sel, start, end, replacement, newCaret) {
    if (sel.type === 'textarea') {
      ed.setSelectionRange(start, end);
      ed.setRangeText(replacement, start, end, 'end');
      ed.setSelectionRange(newCaret, newCaret);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    } else {
      const range = document.createRange();
      const a = locateNodeByOffset(ed, start);
      const b = locateNodeByOffset(ed, end);
      if (a.node.nodeType === Node.TEXT_NODE) range.setStart(a.node, a.local);
      else range.setStart(ed, ed.childNodes.length);
      if (b.node.nodeType === Node.TEXT_NODE) range.setEnd(b.node, b.local);
      else range.setEnd(ed, ed.childNodes.length);
      range.deleteContents();
      const textNode = document.createTextNode(replacement);
      range.insertNode(textNode);
      const caretRange = document.createRange();
      const caretLocal = Math.max(0, Math.min(replacement.length, newCaret - start));
      caretRange.setStart(textNode, caretLocal);
      caretRange.collapse(true);
      const s = window.getSelection(); s.removeAllRanges(); s.addRange(caretRange);
      ed.dispatchEvent(new InputEvent('input', { bubbles: true }));
    }
  }

  /*************************************************************************
   * 组合输入保护
   *************************************************************************/
  window.addEventListener('compositionstart', () => { _isComposing = true; }, true);
  window.addEventListener('compositionend',   () => {
    _isComposing = false; _allowOnceAfterComposition = true;
    setTimeout(() => { _allowOnceAfterComposition = false; }, 0);
  }, true);
  function isCharacterInsertion(e) {
    if (!(e instanceof InputEvent)) return false;
    if (_isComposing) return false;
    const t = e.inputType || '';
    if (t === 'insertText' || t === 'insertCompositionText') return true;
    if (_allowOnceAfterComposition) return true;
    return false;
  }

  /*************************************************************************
   * 模式与数学判断
   *************************************************************************/
  function isInsideCodeFence(text, pos) {
    const fence = /```/g; let count = 0, m;
    while ((m = fence.exec(text)) && m.index < pos) count++;
    return (count % 2) === 1;
  }
  function isEscaped(text, idx) {
    let backslashes = 0; for (let i = idx - 1; i >= 0 && text[i] === '\\'; i--) backslashes++;
    return (backslashes % 2) === 1;
  }
  function nearestUnescapedPairPositions(text, pos, token) {
    const re = token === '$$' ? /\$\$/g : /\$/g;
    const idxs = []; let m;
    while ((m = re.exec(text))) { if (!isEscaped(text, m.index)) idxs.push(m.index); }
    let left = -1, right = -1;
    for (let i = 0; i < idxs.length; i++) {
      const a = idxs[i], b = idxs[i + 1];
      if (a < pos && b !== undefined && b > pos) { left = a; right = b; break; }
    }
    return { left, right };
  }
  function inBlockMath(text, pos) {
    const { left, right } = nearestUnescapedPairPositions(text, pos, '$$');
    return left !== -1 && right !== -1;
  }
  function inInlineMath(text, pos) {
    if (inBlockMath(text, pos)) return false;
    const { left, right } = nearestUnescapedPairPositions(text, pos, '$');
    return left !== -1 && right !== -1;
  }
  function inMath(text, pos) { return inInlineMath(text, pos) || inBlockMath(text, pos); }

  /*************************************************************************
   * 词边界 / 模式过滤
   *************************************************************************/
  function passesModeOptions(options, fullText, caretPos) {
    if (!options) return true;
    const inCode = isInsideCodeFence(fullText, caretPos);
    const inBlk  = inBlockMath(fullText, caretPos);
    const inInl  = inInlineMath(fullText, caretPos);
    const inM    = inBlk || inInl;
    if (options.includes('c') && !inCode) return false;
    if (options.includes('t') && inM) return false;
    if (options.includes('m') && !inM) return false;
    if (options.includes('M') && !inBlk) return false;
    if (options.includes('n') && !inInl) return false;
    return true;
  }
  function passesWordBoundary(options, fullText, from, to) {
    if (!options || !options.includes('w')) return true;
    const isDelim = (ch) => { if (!ch) return true; return /\s|[.,;:!?()\[\]{}<>\-+*/=|\\]/.test(ch); };
    const prev = fullText[from - 1];
    const next = fullText[to];
    return isDelim(prev) && isDelim(next);
  }

  /*************************************************************************
   * 触发匹配
   *************************************************************************/
  function matchSnippet(beforeText, fullText, caretPos, snippet) {
    const { trigger, options = '' } = snippet;
    const hay = beforeText.slice(-MAX_LOOKBEHIND);
    if (!passesModeOptions(options, fullText, caretPos)) return null;
    function prevCharIsBackslash(globalBefore, fromIndex) {
      const prevIdx = fromIndex - 1; if (prevIdx < 0) return false;
      return globalBefore[prevIdx] === '\\';
    }
    if (options.includes('r')) {
      let re; try { re = new RegExp(trigger + '$', options.includes('m') ? 'm' : ''); } catch { return null; }
      const m = hay.match(re); if (!m) return null;
      const from = beforeText.length - m[0].length;
      const globalFrom = caretPos - m[0].length, globalTo = caretPos;
      if (!passesWordBoundary(options, fullText, globalFrom, globalTo)) return null;
      if (prevCharIsBackslash(fullText, globalFrom)) return null;
      return { from: globalFrom, to: globalTo, match: m };
    } else {
      if (!hay.endsWith(trigger)) return null;
      const globalFrom = caretPos - trigger.length, globalTo = caretPos;
      if (!passesWordBoundary(options, fullText, globalFrom, globalTo)) return null;
      if (prevCharIsBackslash(fullText, globalFrom)) return null;
      return { from: globalFrom, to: globalTo, match: null };
    }
  }

  /*************************************************************************
   * expand(就地)
   *************************************************************************/
  function parseSnippetTemplate(tpl, visualText = '') {
    tpl = tpl.replace(/\$\{?VISUAL\}?/g, visualText);
    const fields = [];
    tpl = tpl.replace(/\$\{(\d+):([^}]*)\}/g, (m, nStr, def) => FIELD_START + parseInt(nStr, 10) + ':' + def + FIELD_END);
    tpl = tpl.replace(/\$(\d+)/g, (m, nStr) => FIELD_START + parseInt(nStr, 10) + ':' + '' + FIELD_END);
    tpl = tpl.replace(/\$0/g, CARET_TOKEN);
    let out = '', i = 0;
    while (i < tpl.length) {
      if (tpl[i] === FIELD_START) {
        let j = i + 1, buf = '';
        while (j < tpl.length && tpl[j] !== FIELD_END) buf += tpl[j++];
        const m = buf.match(/^(\d+):(.*)$/s);
        const n = parseInt(m[1], 10), def = m[2];
        const start = out.length; out += def; const end = out.length;
        fields.push({ n, start, end }); i = j + 1;
      } else { out += tpl[i++]; }
    }
    const caretIndex = out.indexOf(CARET_TOKEN);
    const text = out.replace(CARET_TOKEN, '');
    fields.sort((a, b) => a.n - b.n);
    return { text, fields, caret: (caretIndex === -1 ? null : caretIndex) };
  }
  function expandWithSnippet(ed, sel, snip, m, visualText = '') {
    let replTpl = snip.replacement;
    if (m && m.match && m.match.length && /\[\[\d+\]\]/.test(replTpl)) {
      replTpl = replTpl.replace(/\[\[(\d+)\]\]/g, (mm, k) => m.match[parseInt(k, 10)] ?? '');
    }
    const { text: replaced, fields, caret } = parseSnippetTemplate(replTpl, visualText);
    const start = m ? m.from : sel.start;
    const end   = m ? m.to   : sel.end;
    let newCaret = start + replaced.length;
    if (caret !== null) newCaret = start + caret;
    else if (fields.length > 0) newCaret = start + fields[0].start;
    _latexSuiteBusy = true;
    replaceRangeInEditor(ed, sel, start, end, replaced, newCaret);
    setTimeout(() => { _latexSuiteBusy = false; }, 0);
    return true;
  }

  /*************************************************************************
   * 自动片段
   *************************************************************************/
  const autoSnipsByLastChar = new Map(), autoRegexSnips = [];
  (function prepareAutoIndex() {
    for (const s of SNIPPETS) {
      const hasA = (s.options || '').includes('A');
      const auto = (s.auto !== undefined) ? !!s.auto : !!hasA;
      if (!auto) continue;
      if ((s.options || '').includes('r')) autoRegexSnips.push(s);
      else if (typeof s.trigger === 'string' && s.trigger.length > 0) {
        const last = s.trigger.slice(-1);
        if (!autoSnipsByLastChar.has(last)) autoSnipsByLastChar.set(last, []);
        autoSnipsByLastChar.get(last).push(s);
      }
    }
  })();
  function tryAutoExpand(e) {
    if (!enabled) return;
    if (_latexSuiteBusy) return;
    if (!isCharacterInsertion(e)) return;
    const ed = getActiveEditable(); if (!ed) return;
    const sel = getTextAndCaret(ed); if (!sel) return;
    if (sel.start !== sel.end) return;
    const full = sel.text;
    const before = full.slice(0, sel.start);
    const lastChar = (e && typeof e.data === 'string' && e.data.length === 1) ? e.data : null;
    if (lastChar && autoSnipsByLastChar.has(lastChar)) {
      for (const snip of autoSnipsByLastChar.get(lastChar)) {
        const m = matchSnippet(before, full, sel.start, snip); if (!m) continue;
        expandWithSnippet(ed, sel, snip, m); return;
      }
    }
    for (const snip of autoRegexSnips) {
      const m = matchSnippet(before, full, sel.start, snip); if (!m) continue;
      expandWithSnippet(ed, sel, snip, m); return;
    }
  }
  window.addEventListener('input', tryAutoExpand, true);

  /*************************************************************************
   * Visual snippets:选区 + 单字符触发
   *************************************************************************/
  window.addEventListener('keydown', (e) => {
    if (!enabled) return;
    const ed = getActiveEditable(); if (!ed) return;
    const sel = getTextAndCaret(ed); if (!sel) return;
    if (sel.start === sel.end) return;
    if (e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
      const visualText = sel.text.slice(sel.start, sel.end);
      for (const snip of SNIPPETS) {
        const opts = snip.options || '';
        if (!opts.includes('v')) continue;
        if (opts.includes('r')) continue;
        if (snip.trigger !== e.key) continue;
        if (!passesModeOptions(opts, sel.text, sel.start)) continue;
        e.preventDefault();
        expandWithSnippet(ed, sel, snip, null, visualText);
        return;
      }
    }
  }, true);

  /*************************************************************************
   * 环境定位/切换 与 花括号参数跳转
   *************************************************************************/
  function findEnvAtPos(fullText, pos) {
    const beginRe = /\\begin\{([a-zA-Z*]+)\}/g;
    let beginMatch, begins = [];
    while ((beginMatch = beginRe.exec(fullText))) {
      const name = beginMatch[1];
      begins.push({
        name,
        beginNameStart: beginMatch.index + "\\begin{".length,
        beginNameEnd:   beginMatch.index + "\\begin{".length + name.length,
        beginTokenStart: beginMatch.index,
        beginTokenEnd:   beginMatch.index + beginMatch[0].length
      });
    }
    let cand = null;
    for (const b of begins) if (b.beginTokenStart <= pos && (!cand || b.beginTokenStart > cand.beginTokenStart)) cand = b;
    if (!cand) return null;
    const name = cand.name;
    const endRe = new RegExp(String.raw`\\(begin|end)\{(${name.replace('*','\\*')})\}`, 'g');
    endRe.lastIndex = cand.beginTokenEnd;
    let depth = 1, m, endTokenStart=-1, endNameStart=-1, endNameEnd=-1, endTokenEnd=-1;
    while ((m = endRe.exec(fullText))) {
      const kind = m[1];
      if (kind === 'begin') depth++; else depth--;
      if (depth === 0) {
        endTokenStart = m.index;
        endNameStart  = m.index + "\\end{".length;
        endNameEnd    = endNameStart + name.length;
        endTokenEnd   = m.index + m[0].length;
        break;
      }
    }
    if (depth !== 0) return null;
    if (!(cand.beginTokenStart <= pos && pos <= endTokenEnd)) return null;
    return { name,
      beginNameStart: cand.beginNameStart, beginNameEnd: cand.beginNameEnd,
      endNameStart, endNameEnd,
      beginTokenStart: cand.beginTokenStart, beginTokenEnd: cand.beginTokenEnd,
      endTokenStart, endTokenEnd
    };
  }
  function nextEnvName(current, dir) {
    for (const cycle of ENV_CYCLES) {
      const idx = cycle.indexOf(current);
      if (idx !== -1) {
        if (cycle.length <= 1) return current;
        const n = (idx + (dir > 0 ? 1 : -1) + cycle.length) % cycle.length;
        return cycle[n];
      }
    }
    return current;
  }

  // 找到以命令为中心的连续 {…} 参数,并返回各参数内部区间
  function findBraceArgsNear(fullText, pos) {
    let i = pos - 1;
    while (i >= 0 && /\s/.test(fullText[i])) i--;
    while (i >= 0 && /[a-zA-Z*]/.test(fullText[i])) i--;
    if (i < 0 || fullText[i] !== '\\') return null;
    const cmdStart = i;
    let j = i + 1;
    while (j < fullText.length && /[a-zA-Z*]/.test(fullText[j])) j++;
    const cmdEnd = j;
    const args = [];
    let k = j;
    while (k < fullText.length) {
      while (k < fullText.length && /\s/.test(fullText[k])) k++;
      if (fullText[k] !== '{') break;
      let depth = 0, a = k, b = k;
      while (b < fullText.length) {
        if (fullText[b] === '{') depth++;
        else if (fullText[b] === '}') { depth--; if (depth === 0) break; }
        b++;
      }
      if (depth !== 0) break;
      args.push({ innerStart: a + 1, innerEnd: b });
      k = b + 1;
    }
    if (args.length === 0) return null;
    return { cmdStart, cmdEnd, args };
  }

  // 关键:在参数内部按 Tab,直接跳到“下一个”;已选中某参数时继续跳到下一/上一
  function pickArgIndex(args, selStart, selEnd, dir) {
    const step = (dir > 0 ? 1 : -1);
    const L = args.length;
    for (let idx = 0; idx < L; idx++) {
      const a = args[idx];
      if (selStart === a.innerStart && selEnd === a.innerEnd) {
        return (idx + step + L) % L;
      }
    }
    for (let idx = 0; idx < L; idx++) {
      const a = args[idx];
      if (selStart >= a.innerStart && selStart <= a.innerEnd) {
        return (idx + step + L) % L;
      }
    }
    return dir > 0 ? 0 : L - 1;
  }

  /*************************************************************************
   * 键盘:环境切换 / 花括号参数跳转 / matrix 快捷 / Tabout / 非自动片段
   *************************************************************************/
  function onKeyDown(e) {
    const ed = getActiveEditable(); if (!ed) return;
    const sel = getTextAndCaret(ed); if (!sel) return;
    if (!enabled) return;

    // 1) 花括号参数跳转:Tab / Shift+Tab(设置“选区”,让后续输入覆盖选中文本)
    if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
      const full = sel.text;
      const brace = findBraceArgsNear(full, sel.start);
      if (brace && inMath(full, sel.start)) {
        e.preventDefault();
        const dir = e.shiftKey ? -1 : +1;
        const idx = pickArgIndex(brace.args, sel.start, sel.end, dir);
        const target = brace.args[idx];
        setSelectionInEditor(ed, sel, target.innerStart, target.innerEnd);
        return;
      }
    }

    // 2) 环境切换:Tab / Shift+Tab(光标在 begin..end 范围内)
    if ((e.key === 'Tab') && !e.ctrlKey && !e.metaKey && !e.altKey) {
      const full = sel.text;
      if (sel.start === sel.end) {
        const env = findEnvAtPos(full, sel.start);
        if (env) {
          const dir = e.shiftKey ? -1 : +1;
          const newName = nextEnvName(env.name, dir);
          if (newName !== env.name) {
            e.preventDefault();
            _latexSuiteBusy = true;
            replaceRangeInEditor(ed, sel, env.endNameStart,   env.endNameEnd,   newName, sel.start);
            replaceRangeInEditor(ed, getTextAndCaret(ed), env.beginNameStart, env.beginNameEnd, newName, sel.start);
            setTimeout(() => { _latexSuiteBusy = false; }, 0);
            return;
          }
        }
      }
    }

    // 3) Matrix 快捷
    if (e.key === 'Tab' || e.key === 'Enter') {
      const full = sel.text;
      if (inMatrixLikeEnv(full, sel.start)) {
        if (e.key === 'Tab' && !e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey) {
          e.preventDefault();
          _latexSuiteBusy = true;
          replaceRangeInEditor(ed, sel, sel.start, sel.end, '&', sel.start + 1);
          setTimeout(() => { _latexSuiteBusy = false; }, 0);
          return;
        }
        if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey && !e.altKey) {
          e.preventDefault();
          if (!e.shiftKey) {
            const rep = '\\\\\n';
            _latexSuiteBusy = true;
            replaceRangeInEditor(ed, sel, sel.start, sel.end, rep, sel.start + rep.length);
            setTimeout(() => { _latexSuiteBusy = false; }, 0);
            return;
          } else {
            const after = full.slice(sel.end);
            const nIdx = after.indexOf('\n');
            const jumpPos = (nIdx === -1) ? full.length : (sel.end + nIdx);
            setCaretOnly(ed, sel, jumpPos);
            e.preventDefault();
            return;
          }
        }
      }
    }

    // 4) Tabout
    if (TABOUT_ENABLED && e.key === NON_AUTO_TRIGGER_KEY && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
      const after = sel.text.slice(sel.end);
      if (after.startsWith('$$')) { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 2); return; }
      if (after.startsWith('$'))  { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 1); return; }
      const first = after[0];
      if (')]}>|'.includes(first)) { e.preventDefault(); setCaretOnly(ed, sel, sel.start + 1); return; }
    }

    // 5) 非自动片段(Tab 触发)
    if (e.key === NON_AUTO_TRIGGER_KEY && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
      if (_latexSuiteBusy) return;
      if (sel.start !== sel.end) return;
      const before = sel.text.slice(0, sel.start);
      for (const snip of SNIPPETS) {
        const opts = snip.options || '';
        const hasA = opts.includes('A');
        const auto = (snip.auto !== undefined) ? !!snip.auto : !!hasA;
        if (auto) continue;
        const m = matchSnippet(before, sel.text, sel.start, snip); if (!m) continue;
        e.preventDefault();
        expandWithSnippet(ed, sel, snip, m);
        return;
      }
    }
  }
  window.addEventListener('keydown', onKeyDown, true);

  function inMatrixLikeEnv(text, pos) {
    const left = text.lastIndexOf('\\begin{', pos);
    if (left === -1) return false;
    const right = text.indexOf('}', left + 7);
    if (right === -1 || right > pos) return false;
    const env = text.slice(left + 7, right);
    return MATRIX_ENV_NAMES.includes(env);
  }

  /*************************************************************************
   * Auto-fraction(仅字符输入、数学环境,“/”)
   *************************************************************************/
  window.addEventListener('input', function onSlashAutoFrac(e) {
    if (!enabled) return; if (_latexSuiteBusy) return; if (!isCharacterInsertion(e)) return;
    const ed = getActiveEditable(); if (!ed) return;
    const sel = getTextAndCaret(ed); if (!sel) return;
    if (sel.start !== sel.end) return;
    const full = sel.text;
    const before = full.slice(0, sel.start);
    const after  = full.slice(sel.end);
    if (!before.endsWith('/')) return;
    if (!inMath(full, sel.start)) return;
    if (inExcludedEnvironment(before, after, EXCLUDED_ENVIRONMENTS)) return;
    const leftBound = findNumeratorStart(before.slice(0, -1), BREAKING_CHARS);
    const rightBound = findDenominatorEnd(after, BREAKING_CHARS);
    const numerator   = before.slice(leftBound, -1);
    const denominator = after.slice(0, rightBound);
    if (!numerator.trim() || !denominator.trim()) return;
    const globalStart = leftBound;
    const globalEnd   = sel.start + rightBound;
    const replacement = `${FRACTION_CMD}{${numerator}}{${denominator}}`;
    const newCaret = globalStart + FRACTION_CMD.length + 2;
    _latexSuiteBusy = true;
    replaceRangeInEditor(ed, sel, globalStart, globalEnd, replacement, newCaret);
    setTimeout(() => { _latexSuiteBusy = false; }, 0);
  }, true);
  function isBreakingChar(ch, breakingStr) { return breakingStr.includes(ch); }
  function findNumeratorStart(leftText, breakingStr) {
    let i = leftText.length - 1, depth = 0;
    while (i >= 0) {
      const ch = leftText[i];
      if (ch === ')' || ch === ']' || ch === '}') depth++;
      else if (ch === '(' || ch === '[' || ch === '{') { if (depth === 0) break; depth--; }
      if (depth === 0 && (isBreakingChar(ch, breakingStr) || /\s/.test(ch))) break;
      i--;
    }
    return i + 1;
  }
  function findDenominatorEnd(rightText, breakingStr) {
    let i = 0, depth = 0;
    while (i < rightText.length) {
      const ch = rightText[i];
      if (ch === '(' || ch === '[' || ch === '{') depth++;
      else if (ch === ')' || ch === ']' || ch === '}') { if (depth === 0) break; depth--; }
      if (depth === 0 && (isBreakingChar(ch, breakingStr) || /\s/.test(ch))) break;
      i++;
    }
    return i;
  }
  function inExcludedEnvironment(before, _after, envs) {
    for (const [startTok, _endTok] of envs) {
      const idx = before.lastIndexOf(startTok); if (idx === -1) continue;
      const segment = before.slice(idx + startTok.length);
      let balance = 0;
      for (const ch of segment) { if (ch === '{') balance++; else if (ch === '}') balance--; }
      if (balance > 0) return true;
    }
    return false;
  }

  /*************************************************************************
   * Auto-enlarge:右括号时放大(数学环境)
   *************************************************************************/
  window.addEventListener('input', function onAutoEnlarge(e) {
    if (!enabled) return; if (!AUTO_ENLARGE_ENABLED) return;
    if (_latexSuiteBusy) return; if (!isCharacterInsertion(e)) return;
    const ed = getActiveEditable(); if (!ed) return;
    const sel = getTextAndCaret(ed); if (!sel) return;
    if (sel.start !== sel.end) return;
    const full = sel.text;
    const before = full.slice(0, sel.start);
    const after  = full.slice(sel.end);
    const lastCh = before.slice(-1);
    if (!')]}'.includes(lastCh)) return;
    if (!inMath(full, sel.start)) return;
    const matchOpen = (ch) => ch === ')' ? '(' : (ch === ']' ? '[' : '{');
    const openCh = matchOpen(lastCh);
    const openPos = findMatchingOpen(before.slice(0, -1), openCh, lastCh);
    if (openPos === -1) return;
    const inner = before.slice(openPos + 1, -1);
    if (!AUTO_ENLARGE_TRIGGERS.some(t => inner.includes(t))) return;
    const alreadyLeft  = before.slice(Math.max(0, openPos - 6), openPos + 1).includes('\\left');
    const alreadyRight = after.startsWith('\\right' + lastCh);
    if (alreadyLeft || alreadyRight) return;
    const openChar  = before[openPos];
    const closeChar = lastCh;
    const enlarged = `\\left${openChar}${inner}\\right${closeChar}`;
    const globalStart = openPos;
    const globalEnd   = sel.start;
    const newCaret    = globalStart + enlarged.length;
    _latexSuiteBusy = true;
    replaceRangeInEditor(ed, sel, globalStart, globalEnd, enlarged, newCaret);
    setTimeout(() => { _latexSuiteBusy = false; }, 0);
  }, true);
  function findMatchingOpen(text, openCh, closeCh) {
    let depth = 1;
    for (let i = text.length - 1; i >= 0; i--) {
      const ch = text[i];
      if (ch === closeCh) depth++;
      else if (ch === openCh) { depth--; if (depth === 0) return i; }
    }
    return -1;
  }

})();