LeetCode SQL Autocomplete

一个为力扣 SQL 题目编辑器自动注入表名、列名的插件

2026/06/04のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         LeetCode SQL Autocomplete
// @namespace    https://github.com/linyisu/leetcode-sql-autocomplete
// @version      1.1.2
// @author       linyisu
// @match        https://leetcode.cn/problems/*
// @match        https://leetcode.com/problems/*
// @license      MIT
// @description  一个为力扣 SQL 题目编辑器自动注入表名、列名的插件
// @grant        none
// ==/UserScript==

(function () {
  'use strict';

  const KEYWORDS = [
    // query
    'SELECT', 'FROM', 'WHERE', 'AS', 'DISTINCT', 'LIMIT', 'OFFSET', 'ALL', 'ANY',
    // join
    'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'CROSS JOIN', 'NATURAL JOIN', 'ON', 'USING',
    // logic / comparison
    'AND', 'OR', 'NOT', 'IN', 'NOT IN', 'EXISTS', 'NOT EXISTS',
    'BETWEEN', 'LIKE', 'REGEXP', 'IS NULL', 'IS NOT NULL', 'NULL',
    // grouping / ordering
    'GROUP BY', 'HAVING', 'ORDER BY', 'ASC', 'DESC',
    // set operations
    'UNION', 'UNION ALL', 'INTERSECT', 'EXCEPT',
    // CTE
    'WITH', 'RECURSIVE',
    // conditional
    'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
    'IF', 'IFNULL', 'NULLIF', 'COALESCE', 'GREATEST', 'LEAST', 'ISNULL',
    // aggregate
    'COUNT', 'SUM', 'AVG', 'MAX', 'MIN',
    'GROUP_CONCAT', 'SEPARATOR',
    // math
    'ROUND', 'FLOOR', 'CEIL', 'CEILING', 'ABS', 'MOD', 'POW', 'POWER', 'SQRT', 'TRUNCATE', 'SIGN',
    // string
    'CONCAT', 'CONCAT_WS', 'LENGTH', 'CHAR_LENGTH', 'SUBSTRING', 'SUBSTR',
    'LEFT', 'RIGHT', 'TRIM', 'LTRIM', 'RTRIM', 'UPPER', 'LOWER',
    'REPLACE', 'LOCATE', 'INSTR', 'LPAD', 'RPAD', 'REPEAT', 'REVERSE', 'FORMAT',
    // type conversion
    'CAST', 'CONVERT',
    // date / time
    'NOW', 'CURDATE', 'CURTIME', 'CURRENT_DATE', 'CURRENT_TIMESTAMP',
    'DATE', 'DATE_ADD', 'DATE_SUB', 'DATEDIFF', 'DATE_FORMAT', 'STR_TO_DATE',
    'TIMESTAMPDIFF', 'EXTRACT',
    'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'WEEK', 'QUARTER', 'LAST_DAY',
    // window functions
    'RANK', 'DENSE_RANK', 'ROW_NUMBER', 'NTILE',
    'LAG', 'LEAD', 'FIRST_VALUE', 'LAST_VALUE', 'NTH_VALUE',
    'PERCENT_RANK', 'CUME_DIST',
    'OVER', 'PARTITION BY', 'ROWS BETWEEN', 'RANGE BETWEEN',
    'UNBOUNDED PRECEDING', 'CURRENT ROW', 'UNBOUNDED FOLLOWING',
  ];

  const SQL_LANGUAGES = ['mysql', 'sql', 'pgsql', 'postgresql', 'mssql', 'oraclesql', 'plaintext'];
  const DESCRIPTION_SELECTORS = [
    '[data-track-load="description_content"]',
    '[data-track-load*="description"]',
    '[data-testid*="description"]',
    '.question-content__JfgR',
    '.question-content',
    '[class*="description"]',
  ];
  const DESCRIPTION_SELECTOR = DESCRIPTION_SELECTORS.join(',');
  const IGNORED_TABLE_NAMES = /^(input|output|inputs|outputs|example|explanation|note|follow|hint|return|this|that|the|result|results|sample|data)$/i;
  const MAX_DOCUMENT_WORD_SUGGESTIONS = 50;

  let cachedDescriptionElement = null;

  function hasSchemaText(text) {
    return /\bcolumn\s+name\b/i.test(text) ||
      /列名/.test(text) ||
      /(?:^|\n)\s*(?:table|表)\s*[::]/i.test(text) ||
      /(?:^|\n)\s*[`'"]?[a-zA-Z_]\w*[`'"]?\s*(?:table|表)\s*[::]/i.test(text);
  }

  function findDescriptionElement() {
    for (const selector of DESCRIPTION_SELECTORS) {
      for (const el of document.querySelectorAll(selector)) {
        const text = el.innerText || el.textContent || '';
        if (hasSchemaText(text) || el.querySelector('pre, table')) return el;
      }
    }
    return null;
  }

  function getDescriptionElement() {
    if (cachedDescriptionElement?.isConnected) return cachedDescriptionElement;
    cachedDescriptionElement = findDescriptionElement();
    return cachedDescriptionElement;
  }

  function getDescriptionText() {
    const descEl = getDescriptionElement();
    const domText = descEl ? (descEl.innerText || descEl.textContent || '') : '';
    const metaText = document.querySelector('meta[name="description"]')?.content || '';

    if (hasSchemaText(domText)) return domText;
    if (hasSchemaText(metaText)) return metaText;
    return domText || metaText;
  }

  function normalizeIdentifier(value) {
    const match = String(value || '').match(/[a-zA-Z_]\w*/);
    return match ? match[0] : '';
  }

  function parseTableName(line) {
    const normalized = line
      .replace(/^(?:input|inputs|输入)\s*[::]\s*/i, '')
      .replace(/^示例\s*\d*\s*[::]?\s*/i, '')
      .trim();

    const patterns = [
      /(?:^|[\s\-–—])(?:table|表)\s*[::]\s*[`'"]?([a-zA-Z_]\w*)[`'"]?/i,
      /(?:^|[\s::])[`'"]?([a-zA-Z_]\w*)[`'"]?\s*(?:table|表)\s*[::]/i,
      /(?:^|[\s\-–—])(?:table\s+name|表名)\s*[::]\s*[`'"]?([a-zA-Z_]\w*)[`'"]?/i,
    ];

    for (const pattern of patterns) {
      const match = normalized.match(pattern);
      if (match?.[1] && !IGNORED_TABLE_NAMES.test(match[1])) return match[1];
    }
    return '';
  }

  function splitPipeRow(line) {
    const trimmed = line.trim();
    if (!trimmed.includes('|')) return [];

    const parts = trimmed.split('|').map(part => part.trim());
    if (parts[0] === '') parts.shift();
    if (parts[parts.length - 1] === '') parts.pop();
    return parts;
  }

  function isTableDividerLine(line) {
    const trimmed = line.trim();
    return /^\+[-+\s]+\+$/.test(trimmed) ||
      /^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?$/.test(trimmed);
  }

  function isSchemaHeaderLine(line) {
    const cells = splitPipeRow(line).map(cell => cell.toLowerCase());
    if (cells.length >= 2) {
      return /^(column\s+name|列名)$/.test(cells[0]) && /^(type|类型)$/.test(cells[1]);
    }
    return (/\bcolumn\s+name\b/i.test(line) && /\btype\b/i.test(line)) ||
      (/列名/.test(line) && /类型/.test(line));
  }

  function extractColumnsFromSchemaTable(lines, startIndex, seenCols, columns) {
    let i = startIndex;

    for (; i < lines.length; i++) {
      const trimmed = lines[i].trim();
      if (!trimmed) break;
      if (isTableDividerLine(trimmed) || isSchemaHeaderLine(trimmed)) continue;

      const cells = splitPipeRow(trimmed);
      if (cells.length < 2) break;

      const colName = normalizeIdentifier(cells[0]);
      const colType = cells[1].replace(/\s+/g, ' ').trim();
      if (colName && !seenCols.has(colName)) {
        seenCols.add(colName);
        columns.push({ name: colName, type: colType });
      }
    }

    return i;
  }

  function extractSchema() {
    const tableNames = [];
    const columns = [];
    const seenTables = new Set();
    const seenCols = new Set();

    const text = getDescriptionText();
    if (!text) return { tableNames, columns };

    const lines = text.replace(/\r\n?/g, '\n').replace(/\u00a0/g, ' ').split('\n');
    let inOutput = false;

    for (let i = 0; i < lines.length; i++) {
      const trimmed = lines[i].trim();
      if (!trimmed) continue;

      if (/^(?:input|inputs)\b/i.test(trimmed) || /^输入/.test(trimmed)) inOutput = false;
      if (/^(?:output|outputs)\b/i.test(trimmed) || /^输出/.test(trimmed)) inOutput = true;

      if (!inOutput) {
        const tableName = parseTableName(trimmed);
        if (tableName && tableName.length > 1 && !seenTables.has(tableName)) {
          seenTables.add(tableName);
          tableNames.push(tableName);
        }
      }

      if (isSchemaHeaderLine(trimmed)) {
        i = extractColumnsFromSchemaTable(lines, i + 1, seenCols, columns) - 1;
      }
    }

    return { tableNames, columns };
  }

  let disposable = null;
  let editorOptionsDisposable = null;
  let lastSchemaKey = null;

  function buildSchemaKey(tableNames, columns) {
    if (tableNames.length === 0 && columns.length === 0) return '';
    return [
      tableNames.join('\0'),
      columns.map(({ name, type }) => `${name}:${type}`).join('\0'),
    ].join('|');
  }

  function applyEditorOptions(editor) {
    editor?.updateOptions?.({
      quickSuggestions: { other: true, comments: false, strings: false },
    });
  }

  function installEditorOptions(monaco) {
    monaco.editor?.getEditors?.()?.forEach(applyEditorOptions);

    if (!editorOptionsDisposable && monaco.editor?.onDidCreateEditor) {
      editorOptionsDisposable = monaco.editor.onDidCreateEditor(applyEditorOptions);
    }
  }

  function labelText(item) {
    return typeof item.label === 'string' ? item.label : item.label.label;
  }

  function collectDocumentWords(value, currentWord, knownLabels) {
    const words = value.match(/\b[a-zA-Z_]\w*\b/g) || [];
    const currentKey = currentWord.toLowerCase();
    const seen = new Set();
    const result = [];

    for (const word of words) {
      const key = word.toLowerCase();
      if (word.length <= 1 || key === currentKey || seen.has(key) || knownLabels.has(key)) continue;

      seen.add(key);
      result.push(word);
      if (result.length >= MAX_DOCUMENT_WORD_SUGGESTIONS) break;
    }

    return result;
  }

  function registerProvider(monaco, tableNames, columns) {
    if (disposable) {
      try { disposable.dispose(); } catch (_) { }
      disposable = null;
    }

    if (tableNames.length === 0 && columns.length === 0) return;

    const { CompletionItemKind } = monaco.languages;

    const items = [
      ...tableNames.map(name => ({
        label: { label: name, description: 'table' },
        kind: CompletionItemKind.Class,
        insertText: name,
        sortText: '0_' + name,
      })),
      ...columns.map(({ name, type }) => ({
        label: { label: name, description: type },
        kind: CompletionItemKind.Field,
        insertText: name,
        sortText: '1_' + name,
      })),
    ];

    const knownLabels = new Set([
      ...items.map(item => labelText(item).toLowerCase()),
      ...KEYWORDS.map(kw => kw.toLowerCase()),
    ]);

    const providers = SQL_LANGUAGES.map(lang =>
      monaco.languages.registerCompletionItemProvider(lang, {
        provideCompletionItems(model, position) {
          const textUntilPos = model.getValueInRange({
            startLineNumber: position.lineNumber,
            startColumn: 1,
            endLineNumber: position.lineNumber,
            endColumn: position.column,
          });
          const wordMatch = textUntilPos.match(/\w+$/);
          const currentWord = wordMatch ? wordMatch[0] : '';
          const range = {
            startLineNumber: position.lineNumber,
            endLineNumber: position.lineNumber,
            startColumn: wordMatch ? position.column - currentWord.length : position.column,
            endColumn: position.column,
          };

          const isLower = currentWord.length > 0 && /[a-z]/.test(currentWord) && currentWord === currentWord.toLowerCase();
          const kwItems = KEYWORDS.map(kw => ({
            label: isLower ? kw.toLowerCase() : kw,
            kind: CompletionItemKind.Keyword,
            insertText: isLower ? kw.toLowerCase() : kw,
            sortText: '5_' + kw,
            range,
          }));

          const docWords = collectDocumentWords(model.getValue(), currentWord, knownLabels);

          return {
            suggestions: [
              ...items.map(item => ({ ...item, range })),
              ...kwItems,
              ...docWords.map(w => ({
                label: w,
                kind: CompletionItemKind.Text,
                insertText: w,
                sortText: '9_' + w,
                range,
              })),
            ],
          };
        },
      })
    );

    disposable = { dispose() { providers.forEach(p => p.dispose()); } };
    installEditorOptions(monaco);
  }

  function waitForMonaco(timeout = 30_000) {
    return new Promise((resolve, reject) => {
      let intervalId = null;
      let timeoutId = null;
      const done = (monaco) => {
        clearInterval(intervalId);
        clearTimeout(timeoutId);
        resolve(monaco);
      };

      if (window.monaco?.languages?.registerCompletionItemProvider) return resolve(window.monaco);
      intervalId = setInterval(() => {
        if (window.monaco?.languages?.registerCompletionItemProvider) {
          done(window.monaco);
        }
      }, 500);
      timeoutId = setTimeout(() => {
        clearInterval(intervalId);
        reject(new Error('Monaco timeout'));
      }, timeout);
    });
  }

  function syncSchema(monaco) {
    const { tableNames, columns } = extractSchema();
    const key = buildSchemaKey(tableNames, columns);
    if (key !== lastSchemaKey) {
      lastSchemaKey = key;
      registerProvider(monaco, tableNames, columns);
    }
  }

  function elementMatchesDescriptionSelector(el) {
    return DESCRIPTION_SELECTORS.some(selector => el.matches?.(selector));
  }

  function nodeMayContainDescription(node) {
    if (node.nodeType !== 1) return false;
    return elementMatchesDescriptionSelector(node) || Boolean(node.querySelector?.(DESCRIPTION_SELECTOR));
  }

  function mutationsMayChangeDescription(mutations) {
    const descEl = getDescriptionElement();
    if (!descEl) {
      return mutations.some(mutation =>
        nodeMayContainDescription(mutation.target) ||
        [...mutation.addedNodes].some(nodeMayContainDescription)
      );
    }

    return mutations.some(mutation =>
      mutation.target === descEl ||
      descEl.contains(mutation.target) ||
      [...mutation.addedNodes].some(node => node === descEl || descEl.contains(node))
    );
  }

  async function main() {
    let monaco;
    try {
      monaco = await waitForMonaco();
    } catch (_) {
      console.warn('[SQ] Monaco 未就绪,退出');
      return;
    }

    installEditorOptions(monaco);
    syncSchema(monaco);

    let syncTimer = null;
    const scheduleSync = (delay = 800) => {
      clearTimeout(syncTimer);
      syncTimer = setTimeout(() => syncSchema(monaco), delay);
    };

    let lastUrl = location.href;
    new MutationObserver((mutations) => {
      if (location.href !== lastUrl) {
        lastUrl = location.href;
        lastSchemaKey = null;
        cachedDescriptionElement = null;
        scheduleSync(1500);
        return;
      }

      if (mutationsMayChangeDescription(mutations)) scheduleSync();
    }).observe(document.body, { childList: true, subtree: true });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', () => main());
  } else {
    main();
  }
})();