@@ -31,19 +31,28 @@ export function wrapLine({ line, maxLength, remainder = '', indent = '', bullet
3131 newLine : string ;
3232 newRemainder : string ;
3333} {
34- const parts = Array . from ( line . substring ( indent . length ) . match ( / ( [ \t ] + | \S + ) / g) || [ ] ) ;
34+ // When we have a bullet, skip the bullet prefix to get the actual text parts
35+ const prefixLength = bullet ? bullet . prefix . length : indent . length ;
36+ const parts = Array . from ( line . substring ( prefixLength ) . match ( / ( [ \t ] + | \S + ) / g) || [ ] ) ;
3537 let acc = remainder . length > 0 ? indent + remainder + ' ' : bullet ? bullet . prefix : indent ;
36- for ( const word of parts ) {
37- if ( acc . length + word . length > maxLength ) {
38- const newLine = acc ? acc : word ;
39- const concatLine = remainder
40- ? indent + remainder + ' ' + line . substring ( indent . length )
41- : line ;
42- const newRemainder = concatLine . slice ( newLine . length ) . trim ( ) ;
4338
39+ for ( let i = 0 ; i < parts . length ; i ++ ) {
40+ const word = parts [ i ] ;
41+ if ( acc . length + word . length > maxLength ) {
42+ // If acc is empty/just prefix, use the current word as newLine
43+ // and start remainder from next word
44+ if ( ! acc || acc === indent || acc === ( bullet ?. prefix ?? '' ) ) {
45+ const remainingParts = parts . slice ( i + 1 ) ;
46+ return {
47+ newLine : word ,
48+ newRemainder : remainingParts . join ( '' ) . trim ( )
49+ } ;
50+ }
51+ // Otherwise, acc becomes newLine and remainder starts from current word
52+ const remainingParts = parts . slice ( i ) ;
4453 return {
45- newLine,
46- newRemainder
54+ newLine : acc ,
55+ newRemainder : remainingParts . join ( '' ) . trim ( )
4756 } ;
4857 }
4958 acc += word ;
@@ -85,7 +94,7 @@ export function parseBullet(text: string): Bullet | undefined {
8594 return { prefix, indent, number } ;
8695}
8796
88- export function wrapIfNecssary ( { node, maxLength } : { node : TextNode ; maxLength : number } ) {
97+ export function wrapIfNecessary ( { node, maxLength } : { node : TextNode ; maxLength : number } ) {
8998 const line = node . getTextContent ( ) ;
9099 if ( line . length <= maxLength ) {
91100 return ;
@@ -102,14 +111,14 @@ export function wrapIfNecssary({ node, maxLength }: { node: TextNode; maxLength:
102111 const paragraph = node . getParent ( ) ;
103112
104113 if ( ! $isParagraphNode ( paragraph ) ) {
105- console . warn ( '[wrapIfNecssary ] Node parent is not a paragraph:' , paragraph ?. getType ( ) ) ;
114+ console . warn ( '[wrapIfNecessary ] Node parent is not a paragraph:' , paragraph ?. getType ( ) ) ;
106115 return ;
107116 }
108117
109118 const selection = getSelection ( ) ;
110119 const selectionOffset = isRangeSelection ( selection ) ? selection . focus . offset : 0 ;
111120
112- // Wrap the current line
121+ // Wrap only the current line - don't collect other paragraphs
113122 const { newLine, newRemainder } = wrapLine ( {
114123 line,
115124 maxLength,
@@ -120,48 +129,23 @@ export function wrapIfNecssary({ node, maxLength }: { node: TextNode; maxLength:
120129 // Update current text node
121130 node . setTextContent ( newLine ) ;
122131
123- // If there's a remainder, we need to create new paragraphs or reuse related ones
132+ // If there's a remainder, create new paragraphs for it
124133 if ( newRemainder ) {
125134 let remainder = newRemainder ;
126135 let lastParagraph = paragraph ;
127136
128- // Get related paragraphs (paragraphs with same indentation following this one)
129- const relatedParagraphs = getRelatedParagraphs ( paragraph , indent ) ;
130-
131- // Process the remainder with related paragraphs
132- for ( const relatedPara of relatedParagraphs ) {
133- if ( ! remainder ) break ;
134-
135- const relatedText = relatedPara . getTextContent ( ) ;
136-
137- // Combine remainder with related paragraph text
138- const combinedText = remainder + ' ' + relatedText ;
139- const { newLine : wrappedLine , newRemainder : newRem } = wrapLine ( {
140- line : combinedText ,
141- maxLength,
142- indent
143- } ) ;
144-
145- // Update the related paragraph
146- const textNode = relatedPara . getFirstChild ( ) ;
147- if ( isTextNode ( textNode ) ) {
148- textNode . setTextContent ( wrappedLine ) ;
149- }
150-
151- remainder = newRem ;
152- lastParagraph = relatedPara ;
153- }
154-
155- // Create new paragraphs for any remaining text
137+ // Create new paragraphs for the wrapped text
156138 while ( remainder && remainder . length > 0 ) {
139+ // Prepend indent to the remainder before wrapping it
140+ const indentedLine = indent + remainder ;
157141 const { newLine : finalLine , newRemainder : finalRem } = wrapLine ( {
158- line : remainder ,
142+ line : indentedLine ,
159143 maxLength,
160144 indent
161145 } ) ;
162146
163147 const newParagraph = new ParagraphNode ( ) ;
164- const newTextNode = new TextNode ( indent + finalLine ) ;
148+ const newTextNode = new TextNode ( finalLine ) ;
165149 newParagraph . append ( newTextNode ) ;
166150 lastParagraph . insertAfter ( newParagraph ) ;
167151
@@ -170,85 +154,59 @@ export function wrapIfNecssary({ node, maxLength }: { node: TextNode; maxLength:
170154 }
171155
172156 // Try to maintain cursor position
173- if ( selectionOffset > newLine . length ) {
174- // Cursor was after the wrap point
175- // Try to find which paragraph it should be in now
176- let targetPara = paragraph ;
177- let accumulatedLength = newLine . length ;
178-
179- // Walk through paragraphs to find where cursor should land
180- let nextPara = paragraph . getNextSibling ( ) ;
181- while ( nextPara && $isParagraphNode ( nextPara ) ) {
182- const nextText = nextPara . getFirstChild ( ) ;
183- if ( ! isTextNode ( nextText ) ) break ;
184-
185- const paraLength = nextText . getTextContentSize ( ) ;
186-
187- // Check if cursor should be in this paragraph
188- if ( selectionOffset <= accumulatedLength + paraLength ) {
189- const offset = Math . min ( selectionOffset - accumulatedLength , paraLength ) ;
190- nextText . select ( offset , offset ) ;
191- return ;
192- }
193-
194- accumulatedLength += paraLength ;
195- targetPara = nextPara ;
196- nextPara = nextPara . getNextSibling ( ) ;
197-
198- // Stop at non-related paragraphs
199- const text = targetPara . getTextContent ( ) ;
200- const paraIndent = parseIndent ( text ) ;
201- if ( paraIndent !== indent || parseBullet ( text ) ) {
157+ // Calculate which paragraph the cursor should end up in
158+ let remainingOffset = selectionOffset ;
159+
160+ // If cursor was in the first line
161+ if ( remainingOffset <= newLine . length ) {
162+ // Keep cursor in the current paragraph at the same position
163+ node . select ( remainingOffset , remainingOffset ) ;
164+ } else {
165+ // Cursor should be in one of the wrapped paragraphs
166+ remainingOffset -= newLine . length + 1 ; // Account for the line and space
167+
168+ // Walk through the created paragraphs to find where cursor belongs
169+ let currentPara : ParagraphNode | null = paragraph . getNextSibling ( ) as ParagraphNode | null ;
170+ let tempRemainder = newRemainder ;
171+
172+ // Calculate all the wrapped lines to find cursor position
173+ while ( tempRemainder && tempRemainder . length > 0 ) {
174+ const indentedLine = indent + tempRemainder ;
175+ const { newLine : tempLine , newRemainder : tempRem } = wrapLine ( {
176+ line : indentedLine ,
177+ maxLength,
178+ indent
179+ } ) ;
180+
181+ // tempLine now includes the indent, so just check against its length
182+ if ( remainingOffset <= tempLine . length ) {
183+ // Cursor belongs in this line
202184 break ;
203185 }
186+ remainingOffset -= tempLine . length + 1 ; // +1 for space between lines
187+ tempRemainder = tempRem ;
188+ currentPara = currentPara ?. getNextSibling ( ) as ParagraphNode | null ;
204189 }
205190
206- // If we didn't find a place, put cursor at end of last paragraph
207- if ( targetPara ) {
208- const lastText = targetPara . getFirstChild ( ) ;
209- if ( isTextNode ( lastText ) ) {
210- lastText . selectEnd ( ) ;
191+ // Set cursor in the appropriate paragraph
192+ if ( currentPara && $isParagraphNode ( currentPara ) ) {
193+ const textNode = currentPara . getFirstChild ( ) ;
194+ if ( isTextNode ( textNode ) ) {
195+ textNode . select ( Math . max ( 0 , remainingOffset ) , Math . max ( 0 , remainingOffset ) ) ;
196+ }
197+ } else {
198+ // Fallback: put cursor at end of last created paragraph
199+ if ( lastParagraph ) {
200+ const textNode = lastParagraph . getFirstChild ( ) ;
201+ if ( isTextNode ( textNode ) ) {
202+ textNode . selectEnd ( ) ;
203+ }
211204 }
212205 }
213206 }
214207 }
215208}
216209
217- /**
218- * Returns paragraphs that follow the given paragraph that are considered part of the same
219- * logical paragraph. This enables us to re-wrap a paragraph when edited in the middle.
220- *
221- * In the multi-paragraph structure, "related paragraphs" are those with the same
222- * indentation and no bullet points, representing continuation of the same text block.
223- */
224- function getRelatedParagraphs ( paragraph : ParagraphNode , indent : string ) : ParagraphNode [ ] {
225- const collectedParagraphs : ParagraphNode [ ] = [ ] ;
226- let next = paragraph . getNextSibling ( ) ;
227-
228- while ( next && $isParagraphNode ( next ) ) {
229- const text = next . getTextContent ( ) ;
230-
231- // Empty paragraphs break the chain
232- if ( text . trimStart ( ) === '' ) {
233- break ;
234- }
235-
236- // We don't consider altered indentations or new bullet points to be
237- // part of the same logical paragraph.
238- const bullet = parseBullet ( text ) ;
239- const lineIndent = parseIndent ( text ) ;
240-
241- if ( indent !== lineIndent || bullet ) {
242- break ;
243- }
244-
245- collectedParagraphs . push ( next ) ;
246- next = next . getNextSibling ( ) ;
247- }
248-
249- return collectedParagraphs ;
250- }
251-
252210export function wrapAll ( editor : LexicalEditor , maxLength : number ) {
253211 editor . update (
254212 ( ) => {
@@ -259,7 +217,7 @@ export function wrapAll(editor: LexicalEditor, maxLength: number) {
259217 if ( $isParagraphNode ( child ) ) {
260218 const textNode = child . getFirstChild ( ) ;
261219 if ( isTextNode ( textNode ) ) {
262- wrapIfNecssary ( { node : textNode , maxLength } ) ;
220+ wrapIfNecessary ( { node : textNode , maxLength } ) ;
263221 }
264222 }
265223 }
0 commit comments