|
|
'use client'; |
|
|
|
|
|
import { EditorView } from '@codemirror/view'; |
|
|
import { EditorState, Transaction } from '@codemirror/state'; |
|
|
import { python } from '@codemirror/lang-python'; |
|
|
import { oneDark } from '@codemirror/theme-one-dark'; |
|
|
import { basicSetup } from 'codemirror'; |
|
|
import React, { memo, useEffect, useRef } from 'react'; |
|
|
import { Suggestion } from '@/lib/db/schema'; |
|
|
|
|
|
type EditorProps = { |
|
|
content: string; |
|
|
onSaveContent: (updatedContent: string, debounce: boolean) => void; |
|
|
status: 'streaming' | 'idle'; |
|
|
isCurrentVersion: boolean; |
|
|
currentVersionIndex: number; |
|
|
suggestions: Array<Suggestion>; |
|
|
}; |
|
|
|
|
|
function PureCodeEditor({ content, onSaveContent, status }: EditorProps) { |
|
|
const containerRef = useRef<HTMLDivElement>(null); |
|
|
const editorRef = useRef<EditorView | null>(null); |
|
|
|
|
|
useEffect(() => { |
|
|
if (containerRef.current && !editorRef.current) { |
|
|
const startState = EditorState.create({ |
|
|
doc: content, |
|
|
extensions: [basicSetup, python(), oneDark], |
|
|
}); |
|
|
|
|
|
editorRef.current = new EditorView({ |
|
|
state: startState, |
|
|
parent: containerRef.current, |
|
|
}); |
|
|
} |
|
|
|
|
|
return () => { |
|
|
if (editorRef.current) { |
|
|
editorRef.current.destroy(); |
|
|
editorRef.current = null; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
}, []); |
|
|
|
|
|
useEffect(() => { |
|
|
if (editorRef.current) { |
|
|
const updateListener = EditorView.updateListener.of((update) => { |
|
|
if (update.docChanged) { |
|
|
const transaction = update.transactions.find( |
|
|
(tr) => !tr.annotation(Transaction.remote), |
|
|
); |
|
|
|
|
|
if (transaction) { |
|
|
const newContent = update.state.doc.toString(); |
|
|
onSaveContent(newContent, true); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
const currentSelection = editorRef.current.state.selection; |
|
|
|
|
|
const newState = EditorState.create({ |
|
|
doc: editorRef.current.state.doc, |
|
|
extensions: [basicSetup, python(), oneDark, updateListener], |
|
|
selection: currentSelection, |
|
|
}); |
|
|
|
|
|
editorRef.current.setState(newState); |
|
|
} |
|
|
}, [onSaveContent]); |
|
|
|
|
|
useEffect(() => { |
|
|
if (editorRef.current && content) { |
|
|
const currentContent = editorRef.current.state.doc.toString(); |
|
|
|
|
|
if (status === 'streaming' || currentContent !== content) { |
|
|
const transaction = editorRef.current.state.update({ |
|
|
changes: { |
|
|
from: 0, |
|
|
to: currentContent.length, |
|
|
insert: content, |
|
|
}, |
|
|
annotations: [Transaction.remote.of(true)], |
|
|
}); |
|
|
|
|
|
editorRef.current.dispatch(transaction); |
|
|
} |
|
|
} |
|
|
}, [content, status]); |
|
|
|
|
|
return ( |
|
|
<div |
|
|
className="relative not-prose w-full pb-[calc(80dvh)] text-sm" |
|
|
ref={containerRef} |
|
|
/> |
|
|
); |
|
|
} |
|
|
|
|
|
function areEqual(prevProps: EditorProps, nextProps: EditorProps) { |
|
|
if (prevProps.suggestions !== nextProps.suggestions) return false; |
|
|
if (prevProps.currentVersionIndex !== nextProps.currentVersionIndex) |
|
|
return false; |
|
|
if (prevProps.isCurrentVersion !== nextProps.isCurrentVersion) return false; |
|
|
if (prevProps.status === 'streaming' && nextProps.status === 'streaming') |
|
|
return false; |
|
|
if (prevProps.content !== nextProps.content) return false; |
|
|
|
|
|
return true; |
|
|
} |
|
|
|
|
|
export const CodeEditor = memo(PureCodeEditor, areEqual); |
|
|
|