:lightning: native-like performance
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { EditorView, Decoration, type DecorationSet, hoverTooltip, keymap, ViewPlugin, WidgetType, type ViewUpdate } from '@codemirror/view'
|
import { EditorView, Decoration, type DecorationSet, hoverTooltip, keymap, ViewPlugin, WidgetType, type ViewUpdate, dropCursor, drawSelection } from '@codemirror/view'
|
||||||
import { EditorState, EditorSelection, StateField, StateEffect, Annotation, RangeSetBuilder, Compartment, Transaction } from '@codemirror/state'
|
import { EditorState, EditorSelection, StateField, StateEffect, Annotation, RangeSetBuilder, Compartment, Transaction } from '@codemirror/state'
|
||||||
import { markdown } from '@codemirror/lang-markdown'
|
import { markdown } from '@codemirror/lang-markdown'
|
||||||
import { syntaxHighlighting, HighlightStyle, syntaxTree, indentUnit } from '@codemirror/language'
|
import { syntaxHighlighting, HighlightStyle, syntaxTree, indentUnit } from '@codemirror/language'
|
||||||
@@ -660,13 +660,31 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return
|
if (!containerRef.current) return
|
||||||
|
|
||||||
|
// Debounced setContent — avoids a Zustand→React re-render on every keystroke.
|
||||||
|
// The actual file save already has its own 1500ms debounce downstream.
|
||||||
|
let contentTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function scheduleSetContent(text: string): void {
|
||||||
|
if (contentTimer) clearTimeout(contentTimer)
|
||||||
|
contentTimer = setTimeout(() => { contentTimer = null; setContent(text) }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounced word count — avoids expensive string ops on every cursor move.
|
||||||
|
let wordTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
function scheduleWordStats(getStats: () => { atCursor: number; total: number }): void {
|
||||||
|
if (wordTimer) clearTimeout(wordTimer)
|
||||||
|
wordTimer = setTimeout(() => { wordTimer = null; setWordStats(getStats()) }, 150)
|
||||||
|
}
|
||||||
|
|
||||||
const view = new EditorView({
|
const view = new EditorView({
|
||||||
state: EditorState.create({
|
state: EditorState.create({
|
||||||
doc: '',
|
doc: '',
|
||||||
extensions: [
|
extensions: [
|
||||||
indentUnit.of(" "),
|
indentUnit.of(" "),
|
||||||
history(),
|
history(),
|
||||||
|
drawSelection(),
|
||||||
|
dropCursor(),
|
||||||
search({ top: true }),
|
search({ top: true }),
|
||||||
|
EditorView.contentAttributes.of({ spellcheck: 'true' }),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
key: 'Tab',
|
key: 'Tab',
|
||||||
@@ -716,13 +734,21 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
setContent(update.state.doc.toString())
|
const text = update.state.doc.toString()
|
||||||
wordTotalRef.current = countWords(update.state.doc.toString())
|
scheduleSetContent(text)
|
||||||
}
|
const total = countWords(text)
|
||||||
if (update.selectionSet || update.docChanged) {
|
wordTotalRef.current = total
|
||||||
const head = update.state.selection.main.head
|
const head = update.state.selection.main.head
|
||||||
const atCursor = countWords(update.state.doc.sliceString(0, head))
|
scheduleWordStats(() => ({
|
||||||
setWordStats({ atCursor, total: wordTotalRef.current })
|
atCursor: countWords(update.state.doc.sliceString(0, head)),
|
||||||
|
total: wordTotalRef.current
|
||||||
|
}))
|
||||||
|
} else if (update.selectionSet) {
|
||||||
|
const head = update.state.selection.main.head
|
||||||
|
scheduleWordStats(() => ({
|
||||||
|
atCursor: countWords(update.state.doc.sliceString(0, head)),
|
||||||
|
total: wordTotalRef.current
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
if (update.selectionSet) {
|
if (update.selectionSet) {
|
||||||
const { from, to } = update.state.selection.main
|
const { from, to } = update.state.selection.main
|
||||||
@@ -815,6 +841,8 @@ export function MarkdownEditor(): JSX.Element {
|
|||||||
return () => {
|
return () => {
|
||||||
view.scrollDOM.removeEventListener('scroll', onScroll)
|
view.scrollDOM.removeEventListener('scroll', onScroll)
|
||||||
if (scrollTimer) clearTimeout(scrollTimer)
|
if (scrollTimer) clearTimeout(scrollTimer)
|
||||||
|
if (contentTimer) clearTimeout(contentTimer)
|
||||||
|
if (wordTimer) clearTimeout(wordTimer)
|
||||||
view.destroy()
|
view.destroy()
|
||||||
viewRef.current = null
|
viewRef.current = null
|
||||||
currentEditorView = null
|
currentEditorView = null
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user