@@ -14,7 +14,13 @@ export function textareaInsertText(textarea, value) {
1414 triggerEditorContentChanged ( textarea ) ;
1515}
1616
17- function handleIndentSelection ( textarea , e ) {
17+ type TextareaValueSelection = {
18+ value : string ;
19+ selStart : number ;
20+ selEnd : number ;
21+ }
22+
23+ function handleIndentSelection ( textarea : HTMLTextAreaElement , e ) {
1824 const selStart = textarea . selectionStart ;
1925 const selEnd = textarea . selectionEnd ;
2026 if ( selEnd === selStart ) return ; // do not process when no selection
@@ -56,53 +62,125 @@ function handleIndentSelection(textarea, e) {
5662 triggerEditorContentChanged ( textarea ) ;
5763}
5864
59- function handleNewline ( textarea : HTMLTextAreaElement , e : Event ) {
60- const selStart = textarea . selectionStart ;
61- const selEnd = textarea . selectionEnd ;
62- if ( selEnd !== selStart ) return ; // do not process when there is a selection
65+ type MarkdownHandleIndentionResult = {
66+ handled : boolean ;
67+ valueSelection ?: TextareaValueSelection ;
68+ }
69+
70+ type TextLinesBuffer = {
71+ lines : string [ ] ;
72+ lengthBeforePosLine : number ;
73+ posLineIndex : number ;
74+ inlinePos : number
75+ }
76+
77+ export function textareaSplitLines ( value : string , pos : number ) : TextLinesBuffer {
78+ const lines = value . split ( '\n' ) ;
79+ let lengthBeforePosLine = 0 , inlinePos = 0 , posLineIndex = 0 ;
80+ for ( ; posLineIndex < lines . length ; posLineIndex ++ ) {
81+ const lineLength = lines [ posLineIndex ] . length + 1 ;
82+ if ( lengthBeforePosLine + lineLength > pos ) {
83+ inlinePos = pos - lengthBeforePosLine ;
84+ break ;
85+ }
86+ lengthBeforePosLine += lineLength ;
87+ }
88+ return { lines, lengthBeforePosLine, posLineIndex, inlinePos} ;
89+ }
90+
91+ function markdownReformatListNumbers ( linesBuf : TextLinesBuffer , indention : string ) {
92+ const reDeeperIndention = new RegExp ( `^${ indention } \\s+` ) ;
93+ const reSameLevel = new RegExp ( `^${ indention } ([0-9]+)\\.` ) ;
94+ let firstLineIdx : number ;
95+ for ( firstLineIdx = linesBuf . posLineIndex - 1 ; firstLineIdx >= 0 ; firstLineIdx -- ) {
96+ const line = linesBuf . lines [ firstLineIdx ] ;
97+ if ( ! reDeeperIndention . test ( line ) && ! reSameLevel . test ( line ) ) break ;
98+ }
99+ firstLineIdx ++ ;
100+ let num = 1 ;
101+ for ( let i = firstLineIdx ; i < linesBuf . lines . length ; i ++ ) {
102+ const oldLine = linesBuf . lines [ i ] ;
103+ const sameLevel = reSameLevel . test ( oldLine ) ;
104+ if ( ! sameLevel && ! reDeeperIndention . test ( oldLine ) ) break ;
105+ if ( sameLevel ) {
106+ const newLine = `${ indention } ${ num } .${ oldLine . replace ( reSameLevel , '' ) } ` ;
107+ linesBuf . lines [ i ] = newLine ;
108+ num ++ ;
109+ if ( linesBuf . posLineIndex === i ) {
110+ // need to correct the cursor inline position if the line length changes
111+ linesBuf . inlinePos += newLine . length - oldLine . length ;
112+ linesBuf . inlinePos = Math . max ( 0 , linesBuf . inlinePos ) ;
113+ linesBuf . inlinePos = Math . min ( newLine . length , linesBuf . inlinePos ) ;
114+ }
115+ }
116+ }
117+ recalculateLengthBeforeLine ( linesBuf ) ;
118+ }
119+
120+ function recalculateLengthBeforeLine ( linesBuf : TextLinesBuffer ) {
121+ linesBuf . lengthBeforePosLine = 0 ;
122+ for ( let i = 0 ; i < linesBuf . posLineIndex ; i ++ ) {
123+ linesBuf . lengthBeforePosLine += linesBuf . lines [ i ] . length + 1 ;
124+ }
125+ }
63126
64- const value = textarea . value ;
127+ export function markdownHandleIndention ( tvs : TextareaValueSelection ) : MarkdownHandleIndentionResult {
128+ const unhandled : MarkdownHandleIndentionResult = { handled : false } ;
129+ if ( tvs . selEnd !== tvs . selStart ) return unhandled ; // do not process when there is a selection
65130
66- // find the current line
67- // * if selStart is 0, lastIndexOf(..., -1) is the same as lastIndexOf(..., 0)
68- // * if lastIndexOf reruns -1, lineStart is 0 and it is still correct.
69- const lineStart = value . lastIndexOf ( '\n' , selStart - 1 ) + 1 ;
70- let lineEnd = value . indexOf ( '\n' , selStart ) ;
71- lineEnd = lineEnd < 0 ? value . length : lineEnd ;
72- let line = value . slice ( lineStart , lineEnd ) ;
73- if ( ! line ) return ; // if the line is empty, do nothing, let the browser handle it
131+ const linesBuf = textareaSplitLines ( tvs . value , tvs . selStart ) ;
132+ const line = linesBuf . lines [ linesBuf . posLineIndex ] ?? '' ;
133+ if ( ! line ) return unhandled ; // if the line is empty, do nothing, let the browser handle it
74134
75135 // parse the indention
76- const indention = / ^ \s * / . exec ( line ) [ 0 ] ;
77- line = line . slice ( indention . length ) ;
136+ let lineContent = line ;
137+ const indention = / ^ \s * / . exec ( lineContent ) [ 0 ] ;
138+ lineContent = lineContent . slice ( indention . length ) ;
139+ if ( linesBuf . inlinePos <= indention . length ) return unhandled ; // if cursor is at the indention, do nothing, let the browser handle it
78140
79141 // parse the prefixes: "1. ", "- ", "* ", there could also be " [ ] " or " [x] " for task lists
80142 // there must be a space after the prefix because none of "1.foo" / "-foo" is a list item
81- const prefixMatch = / ^ ( [ 0 - 9 ] + \. | [ - * ] ) ( \s \[ ( [ x ] ) \] ) ? \s / . exec ( line ) ;
143+ const prefixMatch = / ^ ( [ 0 - 9 ] + \. | [ - * ] ) ( \s \[ ( [ x ] ) \] ) ? \s / . exec ( lineContent ) ;
82144 let prefix = '' ;
83145 if ( prefixMatch ) {
84146 prefix = prefixMatch [ 0 ] ;
85- if ( lineStart + prefix . length > selStart ) prefix = '' ; // do not add new line if cursor is at prefix
147+ if ( prefix . length > linesBuf . inlinePos ) prefix = '' ; // do not add new line if cursor is at prefix
86148 }
87149
88- line = line . slice ( prefix . length ) ;
89- if ( ! indention && ! prefix ) return ; // if no indention and no prefix, do nothing, let the browser handle it
150+ lineContent = lineContent . slice ( prefix . length ) ;
151+ if ( ! indention && ! prefix ) return unhandled ; // if no indention and no prefix, do nothing, let the browser handle it
90152
91- e . preventDefault ( ) ;
92- if ( ! line ) {
153+ if ( ! lineContent ) {
93154 // clear current line if we only have i.e. '1. ' and the user presses enter again to finish creating a list
94- textarea . value = value . slice ( 0 , lineStart ) + value . slice ( lineEnd ) ;
95- textarea . setSelectionRange ( selStart - prefix . length , selStart - prefix . length ) ;
155+ linesBuf . lines [ linesBuf . posLineIndex ] = '' ;
156+ linesBuf . inlinePos = 0 ;
96157 } else {
97- // start a new line with the same indention and prefix
158+ // start a new line with the same indention
98159 let newPrefix = prefix ;
99- // a simple approach, otherwise it needs to parse the lines after the current line
100160 if ( / ^ \d + \. / . test ( prefix ) ) newPrefix = `1. ${ newPrefix . slice ( newPrefix . indexOf ( '.' ) + 2 ) } ` ;
101161 newPrefix = newPrefix . replace ( '[x]' , '[ ]' ) ;
102- const newLine = `\n${ indention } ${ newPrefix } ` ;
103- textarea . value = value . slice ( 0 , selStart ) + newLine + value . slice ( selEnd ) ;
104- textarea . setSelectionRange ( selStart + newLine . length , selStart + newLine . length ) ;
162+
163+ const inlinePos = linesBuf . inlinePos ;
164+ linesBuf . lines [ linesBuf . posLineIndex ] = line . substring ( 0 , inlinePos ) ;
165+ const newLineLeft = `${ indention } ${ newPrefix } ` ;
166+ const newLine = `${ newLineLeft } ${ line . substring ( inlinePos ) } ` ;
167+ linesBuf . lines . splice ( linesBuf . posLineIndex + 1 , 0 , newLine ) ;
168+ linesBuf . posLineIndex ++ ;
169+ linesBuf . inlinePos = newLineLeft . length ;
170+ recalculateLengthBeforeLine ( linesBuf ) ;
105171 }
172+
173+ markdownReformatListNumbers ( linesBuf , indention ) ;
174+ const newPos = linesBuf . lengthBeforePosLine + linesBuf . inlinePos ;
175+ return { handled : true , valueSelection : { value : linesBuf . lines . join ( '\n' ) , selStart : newPos , selEnd : newPos } } ;
176+ }
177+
178+ function handleNewline ( textarea : HTMLTextAreaElement , e : Event ) {
179+ const ret = markdownHandleIndention ( { value : textarea . value , selStart : textarea . selectionStart , selEnd : textarea . selectionEnd } ) ;
180+ if ( ! ret . handled ) return ;
181+ e . preventDefault ( ) ;
182+ textarea . value = ret . valueSelection . value ;
183+ textarea . setSelectionRange ( ret . valueSelection . selStart , ret . valueSelection . selEnd ) ;
106184 triggerEditorContentChanged ( textarea ) ;
107185}
108186
0 commit comments