@@ -11,12 +11,13 @@ import {
1111} from "react"
1212
1313import { createLogger } from "@agenta/shared/utils"
14- import { $isCodeNode } from "@lexical/code"
14+ import { $createCodeNode , $ isCodeNode} from "@lexical/code"
1515import { $convertFromMarkdownString , TRANSFORMERS } from "@lexical/markdown"
1616import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
1717import { LexicalExtensionComposer } from "@lexical/react/LexicalExtensionComposer"
1818import { mergeRegister } from "@lexical/utils"
1919import clsx from "clsx"
20+ import { useSetAtom } from "jotai"
2021import yaml from "js-yaml"
2122import {
2223 COMMAND_PRIORITY_HIGH ,
@@ -29,7 +30,7 @@ import {
2930 defineExtension ,
3031 type InitialEditorStateType ,
3132} from "lexical"
32- import { $getRoot } from "lexical"
33+ import { $createTextNode , $ getRoot} from "lexical"
3334import { v4 as uuidv4 } from "uuid"
3435
3536import FormView from "./form/FormView"
@@ -56,11 +57,18 @@ import {$getEditorCodeAsString} from "./plugins/code/utils/editorCodeUtils"
5657import { $getLineCount } from "./plugins/code/utils/segmentUtils"
5758import { $convertToMarkdownStringCustom } from "./plugins/markdown/assets/transformers"
5859import { ON_CHANGE_COMMAND } from "./plugins/markdown/commands"
60+ import { stripBackslashEscapes } from "./plugins/markdown/utils/textCleanup"
5961import {
6062 TokenBehaviorExtension ,
6163 TokenBehaviorCoreExtension ,
6264} from "./plugins/token/extensions/tokenBehavior"
65+ import { markdownViewAtom } from "./state/assets/atoms"
6366import type { EditorProps } from "./types"
67+ import {
68+ isEditorLargeDocument ,
69+ isLargeRichTextDocument ,
70+ setEditorLargeDocumentFlag ,
71+ } from "./utils/largeDocument"
6472
6573export const ON_HYDRATE_FROM_REMOTE_CONTENT = createCommand < {
6674 hydrateWithRemoteContent : string
@@ -92,6 +100,17 @@ function $getNativeCodeAsString(): string {
92100 return root . getTextContent ( )
93101}
94102
103+ function $getRichTextAsMarkdownString ( ) : string {
104+ const root = $getRoot ( )
105+ const firstChild = root . getFirstChild ( )
106+
107+ if ( $isCodeNode ( firstChild ) && firstChild . getLanguage ( ) === "markdown" ) {
108+ return firstChild . getTextContent ( )
109+ }
110+
111+ return $convertToMarkdownStringCustom ( TRANSFORMERS , undefined , true )
112+ }
113+
95114// Re-export the useLexicalComposerContext hook for easier access
96115export { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"
97116
@@ -165,6 +184,12 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
165184 } )
166185
167186 const [ editor ] = useLexicalComposerContext ( )
187+ const effectiveValue = value !== undefined ? value : initialValue
188+ const isLargeRichTextDoc = useMemo (
189+ ( ) => ! codeOnly && isLargeRichTextDocument ( effectiveValue || "" ) ,
190+ [ codeOnly , effectiveValue ] ,
191+ )
192+ const setMarkdownView = useSetAtom ( markdownViewAtom ( id ) )
168193
169194 const handleUpdate = useCallback (
170195 ( editorState : EditorState , _editor : LexicalEditor , tags ?: ReadonlySet < string > ) => {
@@ -250,56 +275,64 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
250275 } )
251276 }
252277 } else {
253- const root = $getRoot ( )
254- const firstChild = root . getFirstChild ( )
255- let textContent : string
278+ const emitRichTextChange = ( ) => {
279+ const textContent = $getRichTextAsMarkdownString ( )
280+ const tokens : unknown [ ] = [ ] // Extract tokens if needed
281+ const result = {
282+ value : "" , // Omit this for now
283+ textContent : stripBackslashEscapes ( textContent ) ,
284+ tokens,
285+ }
256286
257- if (
258- $isCodeNode ( firstChild ) &&
259- firstChild . getLanguage ( ) === "markdown"
260- ) {
261- textContent = firstChild . getTextContent ( )
262- } else {
263- textContent = $convertToMarkdownStringCustom (
264- TRANSFORMERS ,
265- undefined ,
266- true ,
287+ setEditorLargeDocumentFlag (
288+ _editor ,
289+ isLargeRichTextDocument ( result . textContent ) ,
267290 )
291+ lastEmittedTextRef . current = result . textContent
292+ const callbackStartMs = getNow ( )
293+ onChange ( result )
294+ const callbackMs = getNow ( ) - callbackStartMs
295+ const totalMs = getNow ( ) - onChangeStartMs
296+ if (
297+ ( DEBUG_ENTER_ON_CHANGE_PROFILE && isEnterUpdate ) ||
298+ totalMs >= SLOW_ON_CHANGE_THRESHOLD_MS
299+ ) {
300+ onChangeLog ( "updateProfile" , {
301+ editorId : id ,
302+ isEnterUpdate,
303+ codeOnly : false ,
304+ contentLength : result . textContent . length ,
305+ callbackMs : Number ( callbackMs . toFixed ( 2 ) ) ,
306+ totalMs : Number ( totalMs . toFixed ( 2 ) ) ,
307+ } )
308+ }
268309 }
269310
270- const tokens : unknown [ ] = [ ] // Extract tokens if needed
311+ const shouldDebounceRichTextChange =
312+ isLargeRichTextDoc || isEditorLargeDocument ( _editor )
271313
272- const result = {
273- value : "" , // Omit this for now
274- textContent : textContent . replaceAll ( / \\ ( .) / g, "$1" ) ,
275- tokens,
314+ if ( shouldDebounceRichTextChange ) {
315+ if ( onChangeDebounceRef . current != null ) {
316+ clearTimeout ( onChangeDebounceRef . current )
317+ }
318+ onChangeDebounceRef . current = setTimeout ( ( ) => {
319+ onChangeDebounceRef . current = null
320+ editor . getEditorState ( ) . read ( ( ) => {
321+ if ( ! editor . isEditable ( ) ) return
322+ emitRichTextChange ( )
323+ } )
324+ } , LARGE_DOC_ON_CHANGE_DEBOUNCE_MS )
325+ return true
276326 }
277327
278- lastEmittedTextRef . current = result . textContent
279- const callbackStartMs = getNow ( )
280- onChange ( result )
281- const callbackMs = getNow ( ) - callbackStartMs
282- const totalMs = getNow ( ) - onChangeStartMs
283- if (
284- ( DEBUG_ENTER_ON_CHANGE_PROFILE && isEnterUpdate ) ||
285- totalMs >= SLOW_ON_CHANGE_THRESHOLD_MS
286- ) {
287- onChangeLog ( "updateProfile" , {
288- editorId : id ,
289- isEnterUpdate,
290- codeOnly : false ,
291- contentLength : result . textContent . length ,
292- callbackMs : Number ( callbackMs . toFixed ( 2 ) ) ,
293- totalMs : Number ( totalMs . toFixed ( 2 ) ) ,
294- } )
295- }
328+ emitRichTextChange ( )
296329 }
297330 } )
298331 return true
299332 } ,
300333 COMMAND_PRIORITY_HIGH ,
301334 )
302- } , [ codeOnly , editor , onChange ] )
335+ } , [ codeOnly , editor , id , isLargeRichTextDoc , onChange , useNativeCodeNodes ] )
303336
304337 const [ view , setView ] = useState < "code" | "form" > ( "code" )
305338 const [ jsonValue , setJsonValue ] = useState < Record < string , unknown > > ( { } )
@@ -433,26 +466,48 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
433466 }
434467 isInitRef . current = true
435468 if ( hydrateWithRemoteContent ) {
436- $convertFromMarkdownString (
437- hydrateWithRemoteContent ,
438- TRANSFORMERS ,
439- undefined ,
440- true ,
441- )
469+ if ( ! codeOnly && isLargeRichTextDocument ( hydrateWithRemoteContent ) ) {
470+ setEditorLargeDocumentFlag ( editor , true )
471+
472+ const root = $getRoot ( )
473+ const codeNode = $createCodeNode ( "markdown" )
474+ codeNode . append ( $createTextNode ( hydrateWithRemoteContent ) )
475+ root . clear ( ) . append ( codeNode )
476+ setMarkdownView ( true )
477+ } else {
478+ $convertFromMarkdownString (
479+ hydrateWithRemoteContent ,
480+ TRANSFORMERS ,
481+ undefined ,
482+ true ,
483+ )
484+ }
442485 }
443486 return false
444487 } ,
445488 COMMAND_PRIORITY_LOW ,
446489 ) ,
447490 )
448- } , [ editor ] )
491+ } , [ codeOnly , editor ] )
449492
450493 const lastHydratedRef = useRef < string > ( "" )
451494 const lastEmittedTextRef = useRef < string > ( "" )
452495 const onChangeDebounceRef = useRef < ReturnType < typeof setTimeout > | null > ( null )
453496
454- // Use controlled value if provided, otherwise fall back to initialValue
455- const effectiveValue = value !== undefined ? value : initialValue
497+ useEffect (
498+ ( ) => ( ) => {
499+ if ( onChangeDebounceRef . current != null ) {
500+ clearTimeout ( onChangeDebounceRef . current )
501+ onChangeDebounceRef . current = null
502+ }
503+ } ,
504+ [ ] ,
505+ )
506+
507+ useEffect ( ( ) => {
508+ setEditorLargeDocumentFlag ( editor , isLargeRichTextDoc )
509+ } , [ editor , isLargeRichTextDoc ] )
510+
456511 const shouldDisableLineNumbersForLargeDoc = useMemo ( ( ) => {
457512 if ( ! codeOnly || ! showLineNumbers ) {
458513 return false
@@ -563,9 +618,13 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
563618 style . height = dimensions . height
564619 }
565620
566- // Virtualization needs a bounded scroll container.
567- // If caller did not provide a fixed height, apply a large-doc default.
568- if ( codeOnly && shouldEnableLargeDocOptimizations && ! hasExplicitHeight ) {
621+ // Large documents benefit from a bounded scroll container so the
622+ // page does not reflow around extremely tall editor content.
623+ if (
624+ ( ( codeOnly && shouldEnableLargeDocOptimizations ) ||
625+ ( ! codeOnly && isLargeRichTextDoc ) ) &&
626+ ! hasExplicitHeight
627+ ) {
569628 style . maxHeight = "70vh"
570629 style . overflow = "auto"
571630 }
@@ -576,6 +635,7 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
576635 dimensions ?. height ,
577636 dimensions ?. width ,
578637 hasExplicitHeight ,
638+ isLargeRichTextDoc ,
579639 shouldEnableLargeDocOptimizations ,
580640 ] )
581641
@@ -617,13 +677,7 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
617677 // code fences, etc.) causing false mismatches after markdown paste/conversion.
618678 let currentContent = ""
619679 editor . getEditorState ( ) . read ( ( ) => {
620- const root = $getRoot ( )
621- const firstChild = root . getFirstChild ( )
622- if ( $isCodeNode ( firstChild ) && firstChild . getLanguage ( ) === "markdown" ) {
623- currentContent = firstChild . getTextContent ( )
624- } else {
625- currentContent = $convertToMarkdownStringCustom ( TRANSFORMERS , undefined , true )
626- }
680+ currentContent = $getRichTextAsMarkdownString ( )
627681 } )
628682 // Skip if content already matches (no change needed)
629683 if ( currentContent === next ) {
@@ -656,6 +710,7 @@ const EditorInner = forwardRef<HTMLDivElement, EditorProps>(
656710 showMarkdownToggleButton = { showMarkdownToggleButton }
657711 singleLine = { singleLine }
658712 codeOnly = { codeOnly }
713+ largeDocumentMode = { isLargeRichTextDoc }
659714 debug = { debug }
660715 language = { language }
661716 placeholder = { placeholder }
0 commit comments