import { ref, nextTick } from 'vue' import { EditorView, EditorState } from '@/utils/codemirrorExports' export interface TabEditorTab { id?: number key: string title: string content: string connectionId?: number } export interface TabEditorOptions { findContainer: (tabKey: string, retryCount?: number) => Promise<{ container: HTMLElement; pane?: HTMLElement } | null> checkContainerSize: (container: HTMLElement) => Promise createExtensions: (tab: TabEditorTab) => any[] getInitialContent: (tab: TabEditorTab) => string onContentChange?: (tabKey: string, content: string) => void onEditorReady?: (tabKey: string, editor: EditorView) => void } const INIT_DELAY = 200 export function useTabEditor(options: TabEditorOptions) { const { findContainer, checkContainerSize, createExtensions, getInitialContent, onContentChange, onEditorReady } = options const editorViews = ref>(new Map()) const getEditor = (tabKey: string): EditorView | null => { return editorViews.value.get(tabKey) as EditorView || null } const destroyEditor = (tabKey: string): void => { const editor = editorViews.value.get(tabKey) if (!editor) return if (onContentChange) { onContentChange(tabKey, editor.state.doc.toString()) } editor.destroy() editorViews.value.delete(tabKey) } const focusEditor = (editor: EditorView, delay = 0): void => { if (!editor) return const focus = () => { editor.requestMeasure?.() editor.dispatch({ effects: [] }) requestAnimationFrame(() => editor.focus()) } delay > 0 ? setTimeout(focus, delay) : requestAnimationFrame(focus) } const initEditor = async (tabKey: string, tab: any, isActive: boolean, forceInit = false): Promise => { if (!isActive && !forceInit) return false const existingEditor = editorViews.value.get(tabKey) if (existingEditor instanceof EditorView) { if (isActive) focusEditor(existingEditor, 100) return true } destroyEditor(tabKey) await nextTick() const containerResult = await findContainer(tabKey) if (!containerResult) return false const { container } = containerResult await checkContainerSize(container) const rect = container.getBoundingClientRect() if (rect.width === 0 || rect.height === 0) { if (isActive) { setTimeout(() => initEditor(tabKey, tab, isActive, forceInit), 100) } return false } const state = EditorState.create({ doc: getInitialContent(tab), extensions: createExtensions(tab) }) container.innerHTML = '' const editorView = new EditorView({ state, parent: container }) editorViews.value.set(tabKey, editorView) if (onEditorReady) onEditorReady(tabKey, editorView) if (isActive) focusEditor(editorView, INIT_DELAY) return true } const destroyAll = (): void => { editorViews.value.forEach((_, tabKey) => destroyEditor(tabKey)) editorViews.value.clear() } const updateEditorContent = (tabKey: string, content: string): boolean => { const editor = editorViews.value.get(tabKey) if (!editor) return false const update = () => { const state = editor.state if (!state?.doc) return false if (state.doc.toString() === content) return true try { editor.dispatch(state.update({ changes: { from: 0, to: state.doc.length, insert: content } })) return true } catch { return false } } try { return update() || update() } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '' if (errorMessage?.includes('doesn\'t start from the previous state')) { try { return update() } catch {} } return false } } return { editorViews, getEditor, destroyEditor, initEditor, focusEditor, destroyAll, updateEditorContent } }