{"version":3,"file":"use-js-editor.cjs","sources":["../../../app/hooks/use-js-editor.ts"],"sourcesContent":["import { EditorState, EditorStateConfig, Extension } from \"@codemirror/state\"\nimport { EditorView, ViewUpdate, keymap, placeholder as placeholderExtension } from \"@codemirror/view\"\nimport { useLayoutEffect, useRef, useCallback, useEffect, useMemo } from \"react\"\nimport { html } from \"@codemirror/lang-html\"\nimport { javascript, javascriptLanguage, scopeCompletionSource } from \"@codemirror/lang-javascript\"\nimport { defaultKeymap, historyKeymap } from \"@codemirror/commands\"\nimport { \n  completionKeymap,\n  autocompletion\n} from \"@codemirror/autocomplete\"\nimport { \n  defaultHighlightStyle, \n  syntaxHighlighting, \n  foldKeymap\n} from \"@codemirror/language\"\nimport { lintKeymap } from \"@codemirror/lint\"\nimport { searchKeymap } from \"@codemirror/search\"\nimport { EditorView as EditorViewTheme } from \"@codemirror/view\"\nimport { useBaseExtensions } from \"./use-base-extensions\"\nimport { createFormatExtension, jsEditorKeymap, keyboardHandler } from \"../extensions\"\n\ninterface UseJsEditorProps {\n  autoFocus?: boolean\n  /**\n   * 延迟触发间隔（毫秒），与 onDebouncedChange 搭配使用。\n   * 默认 300ms。\n   */\n  debouncedDelayMs?: number,\n  lineWrapping?: boolean,\n  onChange?: (value: string, viewUpdate: ViewUpdate) => void,\n  onDebouncedChange?: (value: string, viewUpdate: ViewUpdate) => void,\n  onEndChange?: (value: string) => void\n  onFormat?: (formattedCode: string) => void\n  /**\n   * Whether to capture Tab key inside the editor.\n   * - true: Tab is handled by the editor (indent / completion)\n   * - false: Tab moves focus to the next focusable element\n   */\n  captureTabKey?: boolean\n  placeholder?: string\n  readonly?: boolean\n  value?: string\n}\n\nexport function useJsEditor(props: UseJsEditorProps) {\n  const {\n    value,\n    autoFocus,\n    onChange,\n    onDebouncedChange,\n    debouncedDelayMs,\n    onEndChange,\n    readonly,\n    lineWrapping,\n    placeholder,\n    onFormat,\n    captureTabKey = true,\n  } = props\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const editorViewRef = useRef<EditorView | null>(null)\n  const isInternalChangeRef = useRef(false)\n  // 用于延迟回调的定时器\n  const debouncedTimerRef = useRef<number | null>(null)\n  // 记录上一次触发 onEndChange 时的内容，用于避免重复触发\n  const lastEndChangeValueRef = useRef<string | null>(value ?? null)\n\n  const customTheme = useMemo(() => {\n    return EditorViewTheme.theme({\n      \"&\": {\n        height: \"100%\",\n        fontSize: \"14px\",\n        backgroundColor: \"transparent\",\n      },\n      \".cm-scroller\": {\n        fontFamily: \"'JetBrains Mono', 'Fira Code', monospace\",\n        overflow: \"auto\",\n      },\n      \".cm-content\": {\n        caretColor: \"var(--foreground-default, #000)\",\n        minHeight: \"100%\",\n        whiteSpace: \"pre\",\n      },\n      \"&.cm-focused\": {\n        outline: \"none\",\n      },\n      \"&.cm-focused .cm-cursor\": {\n        borderLeftColor: \"var(--foreground-default, #000)\",\n      },\n      \"&.cm-focused .cm-selectionBackground, ::selection\": {\n        backgroundColor: \"var(--background-selected, #d4d4d4)\",\n      },\n      \".cm-gutters\": {\n        color: \"var(--foreground-subtle, #999)\",\n        border: \"none\",\n        paddingRight: \"0.5rem\",\n      },\n      \".cm-activeLine\": {\n        backgroundColor: \"transparent\",\n      },\n      \".cm-line\": {\n        padding: \"0\",\n      },\n    })\n  }, [])\n\n  const handleEndChange = useCallback(\n    (content: string) => {\n      // 只有在内容相对于上一次触发 onEndChange 有变化时才调用外部回调\n      if (content === lastEndChangeValueRef.current) {\n        return\n      }\n\n      lastEndChangeValueRef.current = content\n      onEndChange?.(content)\n    },\n    [onEndChange],\n  )\n\n  const baseExtensions = useBaseExtensions({\n    readonly,\n    lineWrapping,\n    onChange: (newValue, update) => {\n      isInternalChangeRef.current = true\n      onChange?.(newValue, update)\n\n      if (onDebouncedChange) {\n        if (debouncedTimerRef.current !== null) {\n          window.clearTimeout(debouncedTimerRef.current)\n        }\n\n        const delay = debouncedDelayMs ?? 300\n        debouncedTimerRef.current = window.setTimeout(() => {\n          onDebouncedChange(newValue, update)\n        }, delay)\n      }\n    },\n    onEndChange: onEndChange ? handleEndChange : undefined,\n    theme: customTheme,\n    includeLineNumbers: true,\n    includeKeymap: false,\n  })\n\n  // 格式化扩展\n  // 即使没有 onFormat 回调，也需要添加格式化扩展以支持格式化功能\n  const formatExtension = useMemo(() => {\n    return createFormatExtension(onFormat ?? (() => {}))\n  }, [onFormat])\n\n  // JavaScript/HTML 特定的扩展\n  const languageExtensions = useMemo((): Extension[] => {\n    const keymaps = !readonly\n      ? (captureTabKey\n        ? jsEditorKeymap\n        // When not capturing Tab, remove Tab bindings so that focus can move naturally\n        : jsEditorKeymap.filter((binding) => binding.key !== \"Tab\" && binding.key !== \"Shift-Tab\"))\n      : [\n        ...defaultKeymap,\n        ...searchKeymap,\n        ...historyKeymap,\n        ...foldKeymap,\n        ...completionKeymap,\n        ...lintKeymap,\n      ]\n\n    const extensions: Extension[] = [\n      html({\n        autoCloseTags: true,\n      }),\n      javascript({\n        jsx: false,\n        typescript: false,\n      }),\n      autocompletion({\n        activateOnTyping: true,\n        icons: true,\n      }),\n      javascriptLanguage.data.of({\n        autocomplete: scopeCompletionSource(globalThis),\n      }),\n      \n      syntaxHighlighting(defaultHighlightStyle),\n      keyboardHandler,\n      // 添加格式化扩展（即使没有 onFormat 回调也添加，以支持格式化功能）\n      ...formatExtension,\n      keymap.of([\n        // 使用 jsEditorKeymap，包含 HTML/JavaScript 格式化功能\n        ...keymaps,\n      ]),\n    ]\n\n    // Placeholder\n    if (placeholder) {\n      extensions.push(placeholderExtension(placeholder))\n    }\n\n    return extensions\n  }, [placeholder, formatExtension, readonly, captureTabKey])\n\n  const createEditor = useCallback(() => {\n    if (!containerRef.current) return\n\n    const config: EditorStateConfig = {\n      doc: value ?? \"\",\n      extensions: [\n        ...baseExtensions,\n        ...languageExtensions,\n      ],\n    }\n\n    const state = EditorState.create(config)\n\n    const editorView = new EditorView({\n      parent: containerRef.current,\n      state,\n    })\n\n    editorViewRef.current = editorView\n  }, [value, baseExtensions, languageExtensions])\n\n  // 初始化编辑器\n  useLayoutEffect(() => {\n    createEditor()\n\n    return () => {\n      if (debouncedTimerRef.current !== null) {\n        window.clearTimeout(debouncedTimerRef.current)\n      }\n\n      if (editorViewRef.current) {\n        editorViewRef.current.destroy()\n        editorViewRef.current = null\n      }\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  // 当外部 value 改变时，更新编辑器内容\n  useEffect(() => {\n    const editor = editorViewRef.current\n    if (!editor) return\n\n    // 如果这是内部变化触发的更新，跳过\n    if (isInternalChangeRef.current) {\n      isInternalChangeRef.current = false\n      return\n    }\n\n    const currentValue = editor.state.doc.toString()\n    const newValue = value ?? \"\"\n\n    if (currentValue !== newValue) {\n      editor.dispatch({\n        changes: {\n          from: 0,\n          to: currentValue.length,\n          insert: newValue,\n        },\n      })\n    }\n\n    // 同步外部受控值到 onEndChange 的比较基线，\n    // 避免在内容实际上没有变化时误触发 onEndChange\n    lastEndChangeValueRef.current = newValue\n  }, [value])\n\n  useEffect(() => {\n    if (autoFocus && editorViewRef.current) {\n      editorViewRef.current.focus()\n    }\n  }, [autoFocus])\n\n  return {\n    containerRef,\n  }\n}\n\n"],"names":["useRef","useMemo","EditorViewTheme","useCallback","useBaseExtensions","createFormatExtension","jsEditorKeymap","defaultKeymap","searchKeymap","historyKeymap","foldKeymap","completionKeymap","lintKeymap","html","javascript","autocompletion","javascriptLanguage","scopeCompletionSource","syntaxHighlighting","defaultHighlightStyle","keyboardHandler","keymap","placeholderExtension","EditorState","EditorView","useLayoutEffect","useEffect"],"mappings":"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"}