fix: prevent SourceEditor recreation on every content change (#624)

The mountEditor callback had content in its dependency array and was
used as a React ref callback. Every content change (keystroke) gave
mountEditor a new identity, causing React to destroy and recreate
the entire CodeMirror editor — losing cursor position, undo history,
and focus.

Remove content from the dependency array and use a separate useEffect
to push external content updates to the existing editor via
dispatch(). The editor is now only recreated when filePath, language,
or readOnly change.

Co-authored-by: Test User <test@example.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Qiaochu Hu
2026-05-13 08:25:42 +02:00
committed by GitHub
co-authored by Test User Claude Opus 4.7
parent 6fe970651d
commit 236eabe248
@@ -1,4 +1,4 @@
import { useRef, useCallback, memo } from "react";
import { useRef, useCallback, useEffect, memo } from "react";
import {
EditorView,
keymap,
@@ -69,6 +69,9 @@ export const SourceEditor = memo(function SourceEditor({
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const contentRef = useRef(content);
contentRef.current = content;
const mountEditor = useCallback(
(node: HTMLDivElement | null) => {
if (editorRef.current) {
@@ -87,7 +90,7 @@ export const SourceEditor = memo(function SourceEditor({
});
const state = EditorState.create({
doc: content,
doc: contentRef.current,
extensions: [
lineNumbers(),
highlightActiveLine(),
@@ -112,8 +115,22 @@ export const SourceEditor = memo(function SourceEditor({
editorRef.current = new EditorView({ state, parent: node });
},
[content, filePath, language, readOnly],
[filePath, language, readOnly],
);
// Sync external content changes into the editor without recreating it.
// Only applies when the new content differs from the current document
// (e.g. file switch or server refresh), not on every keystroke.
useEffect(() => {
const view = editorRef.current;
if (!view) return;
const current = view.state.doc.toString();
if (current !== content) {
view.dispatch({
changes: { from: 0, to: current.length, insert: content },
});
}
}, [content]);
return <div ref={mountEditor} className="h-full w-full overflow-hidden" />;
});