Skip to content

Commit 65c70a7

Browse files
yanglbmeclaude
andcommitted
fix: remove cursor-driven scroll sync, keep only bidirectional scroll sync
Remove the cursor-driven preview auto-scroll logic (syncPreviewToEditorCursor) to avoid interference with useScrollSync ratio-based scrolling, which caused visual jumping when focusing the editor. Retain the click-preview-element-to- navigate-to-editor feature. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent cc0b321 commit 65c70a7

3 files changed

Lines changed: 20 additions & 139 deletions

File tree

apps/web/src/components/editor/EditorPanel.vue

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ import { checkImage, toBase64 } from '@/utils'
1717
import { fileUpload } from '@/utils/file'
1818
import { store } from '@/utils/storage'
1919
20-
const props = defineProps<{
21-
skipCursorDrivenPreviewSync: boolean
22-
onCursorActivity: () => void
23-
}>()
24-
2520
const editorStore = useEditorStore()
2621
const postStore = usePostStore()
2722
const renderStore = useRenderStore()
@@ -456,10 +451,6 @@ function createFormTextArea(dom: HTMLDivElement) {
456451
currentPost.content = value
457452
}, 300)
458453
}
459-
460-
if (update.selectionSet || update.docChanged) {
461-
props.onCursorActivity()
462-
}
463454
}),
464455
EditorView.domEventHandlers({
465456
paste: createPasteHandler(),
Lines changed: 17 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import type { MaybeRefOrGetter } from 'vue'
22
import { EditorView } from '@codemirror/view'
33

4+
/**
5+
* 点击预览区元素时,定位回编辑器对应位置。
6+
*/
47
export function useCursorSync(
58
codeMirrorViewRef: MaybeRefOrGetter<EditorView | null>,
6-
previewContainerRef: MaybeRefOrGetter<HTMLElement | null>,
79
) {
8-
const cursorSyncTimer = ref<ReturnType<typeof setTimeout>>()
9-
const skipCursorDrivenPreviewSync = ref(false)
10-
1110
const getEditorView = () => toValue(codeMirrorViewRef)
12-
const getPreviewContainer = () => toValue(previewContainerRef)
1311

1412
function normalizeText(text: string) {
1513
return text
@@ -18,62 +16,23 @@ export function useCursorSync(
1816
}
1917

2018
function parseMarkdownHeadingLine(line: string): { level: number, title: string } | null {
21-
if (!line.startsWith(`#`)) {
19+
if (!line.startsWith(`#`))
2220
return null
23-
}
2421

2522
let level = 0
26-
while (level < line.length && line[level] === `#` && level < 6) {
23+
while (level < line.length && line[level] === `#` && level < 6)
2724
level++
28-
}
2925

30-
if (level === 0 || line[level] !== ` `) {
26+
if (level === 0 || line[level] !== ` `)
3127
return null
32-
}
3328

3429
const title = normalizeText(line.slice(level + 1).replace(/#+\s*$/, ``))
35-
if (!title) {
30+
if (!title)
3631
return null
37-
}
3832

3933
return { level, title }
4034
}
4135

42-
function scrollPreviewToElement(el: HTMLElement, behavior: ScrollBehavior = `auto`) {
43-
const container = getPreviewContainer()
44-
if (!container)
45-
return
46-
47-
const cRect = container.getBoundingClientRect()
48-
const eRect = el.getBoundingClientRect()
49-
const inView = eRect.top >= cRect.top + 32 && eRect.bottom <= cRect.bottom - 32
50-
51-
if (!inView) {
52-
el.scrollIntoView({ behavior, block: `center` })
53-
}
54-
}
55-
56-
function findHeadingElementInPreview(title: string, level?: number) {
57-
const headings = document.querySelectorAll<HTMLElement>(`#output [data-heading]`)
58-
const normalizedTitle = normalizeText(title)
59-
60-
for (const heading of headings) {
61-
if (level && Number(heading.tagName.slice(1)) !== level)
62-
continue
63-
if (normalizeText(heading.textContent || ``) === normalizedTitle) {
64-
return heading
65-
}
66-
}
67-
68-
for (const heading of headings) {
69-
if (level && Number(heading.tagName.slice(1)) !== level)
70-
continue
71-
if (normalizeText(heading.textContent || ``).includes(normalizedTitle)) {
72-
return heading
73-
}
74-
}
75-
}
76-
7736
function findHeadingPosInEditor(title: string, level?: number) {
7837
const view = getEditorView()
7938
if (!view)
@@ -119,73 +78,13 @@ export function useCursorSync(
11978

12079
for (const candidate of candidates) {
12180
const pos = docText.indexOf(candidate)
122-
if (pos !== -1) {
81+
if (pos !== -1)
12382
return pos
124-
}
12583
}
12684

12785
return null
12886
}
12987

130-
function focusEditorAtPos(pos: number) {
131-
const view = getEditorView()
132-
if (!view)
133-
return
134-
135-
skipCursorDrivenPreviewSync.value = true
136-
view.dispatch({
137-
selection: { anchor: pos },
138-
effects: EditorView.scrollIntoView(pos, { y: `center` }),
139-
})
140-
view.focus()
141-
142-
setTimeout(() => {
143-
skipCursorDrivenPreviewSync.value = false
144-
}, 180)
145-
}
146-
147-
function syncPreviewToEditorCursor() {
148-
if (skipCursorDrivenPreviewSync.value)
149-
return
150-
151-
const view = getEditorView()
152-
if (!view)
153-
return
154-
155-
const cursorPos = view.state.selection.main.head
156-
const doc = view.state.doc
157-
const cursorLineNo = doc.lineAt(cursorPos).number
158-
159-
// 优先按"最近标题"进行语义定位,避免图片/代码块造成的高度失真。
160-
for (let lineNo = cursorLineNo; lineNo >= 1; lineNo--) {
161-
const text = doc.line(lineNo).text
162-
const parsed = parseMarkdownHeadingLine(text)
163-
if (!parsed)
164-
continue
165-
166-
const headingEl = findHeadingElementInPreview(parsed.title, parsed.level)
167-
if (headingEl) {
168-
scrollPreviewToElement(headingEl)
169-
return
170-
}
171-
}
172-
173-
// 无可用语义锚点时,退化为轻量比例定位。
174-
const container = getPreviewContainer()
175-
if (!container)
176-
return
177-
const maxScrollTop = container.scrollHeight - container.offsetHeight
178-
const ratio = doc.length > 0 ? cursorPos / doc.length : 0
179-
container.scrollTo({ top: Math.max(0, maxScrollTop * ratio), behavior: `auto` })
180-
}
181-
182-
function scheduleSyncPreviewToEditorCursor() {
183-
clearTimeout(cursorSyncTimer.value)
184-
cursorSyncTimer.value = setTimeout(() => {
185-
syncPreviewToEditorCursor()
186-
}, 100)
187-
}
188-
18988
function syncEditorToPreviewElement(el: HTMLElement) {
19089
const tag = el.tagName.toLowerCase()
19190
let pos: number | null = null
@@ -209,7 +108,15 @@ export function useCursorSync(
209108
}
210109

211110
if (pos != null) {
212-
focusEditorAtPos(pos)
111+
const view = getEditorView()
112+
if (!view)
113+
return
114+
115+
view.dispatch({
116+
selection: { anchor: pos },
117+
effects: EditorView.scrollIntoView(pos, { y: `center` }),
118+
})
119+
view.focus()
213120
}
214121
}
215122

@@ -225,14 +132,7 @@ export function useCursorSync(
225132
syncEditorToPreviewElement(block)
226133
}
227134

228-
function cleanup() {
229-
clearTimeout(cursorSyncTimer.value)
230-
}
231-
232135
return {
233-
skipCursorDrivenPreviewSync,
234-
scheduleSyncPreviewToEditorCursor,
235136
handlePreviewContentClick,
236-
cleanup,
237137
}
238138
}

apps/web/src/views/CodemirrorEditor.vue

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,8 @@ const previewPanelCompRef = ref<InstanceType<typeof PreviewPanel> | null>(null)
3030
const getEditorView = () => editorPanelCompRef.value?.codeMirrorView ?? null
3131
const getPreviewContainer = () => previewPanelCompRef.value?.previewRef ?? null
3232
33-
// --- 游标同步 ---
34-
const {
35-
skipCursorDrivenPreviewSync,
36-
scheduleSyncPreviewToEditorCursor,
37-
handlePreviewContentClick,
38-
cleanup: cleanupCursorSync,
39-
} = useCursorSync(getEditorView, getPreviewContainer)
33+
// --- 点击预览内容跳转到编辑器对应位置 ---
34+
const { handlePreviewContentClick } = useCursorSync(getEditorView)
4035
4136
// --- 滚动同步 ---
4237
useScrollSync(getEditorView, getPreviewContainer, enableScrollSync)
@@ -141,7 +136,6 @@ const progressValue = computed(() => editorPanelCompRef.value?.progressValue ??
141136
142137
// --- 清理 ---
143138
onUnmounted(() => {
144-
cleanupCursorSync()
145139
})
146140
</script>
147141

@@ -188,11 +182,7 @@ onUnmounted(() => {
188182
collapsible
189183
:collapsed-size="0"
190184
>
191-
<EditorPanel
192-
ref="editorPanelCompRef"
193-
:skip-cursor-driven-preview-sync="skipCursorDrivenPreviewSync"
194-
:on-cursor-activity="scheduleSyncPreviewToEditorCursor"
195-
/>
185+
<EditorPanel ref="editorPanelCompRef" />
196186
</ResizablePanel>
197187
<ResizableHandle v-show="viewMode === 'split'" />
198188

0 commit comments

Comments
 (0)