22const repl = require ( 'repl' ) ;
33const highlight = require ( './highlight' ) ;
44const memoizeStringTransformerMethod = require ( './memoize-string-transformer' ) ;
5+ const findAllMatchingBrackets = require ( './find-all-matching-brackets' ) ;
56const ansiRegex = require ( 'ansi-regex' ) ;
67const stripAnsi = require ( 'strip-ansi' ) ;
78
@@ -10,6 +11,10 @@ const ansiRegexMatchAll = ansiRegex();
1011// Regex that matches ANSI escape sequences only at the beginning of a string.
1112const ansiRegexMatchBeginningOnly = new RegExp ( `^(${ ansiRegexMatchAll . source } )` ) ;
1213
14+ // Every open/close pair that should be matched against its counterpart for
15+ // highlighting.
16+ const allBrackets = '()[]{}\'"`$' ;
17+
1318// Compute the length of the longest common prefix of 'before' and 'after',
1419// taking ANSI escape sequences into account. For example:
1520// 'abcd', 'abab' -> 2
@@ -52,7 +57,10 @@ class PrettyREPLServer extends repl.REPLServer {
5257 super ( options ) ;
5358 options . output = options . output || process . stdout ;
5459 this . colorize = ( options && options . colorize ) || highlight ( options . output ) ;
60+ this . colorizeMatchingBracket = ( options && options . colorizeMatchingBracket ) ||
61+ highlight ( options . output ) . colorizeMatchingBracket ;
5562 this . lineBeforeInsert = undefined ;
63+ this . highlightBracketPosition = - 1 ;
5664
5765 // For some reason, tests fail if we don't initialize line to be the empty string.
5866 // Specifically, `REPLServer.Interface.getCursorPos()` finds itself in a state where `line`
@@ -61,6 +69,30 @@ class PrettyREPLServer extends repl.REPLServer {
6169 this . __prettyModuleLoaded = __filename ;
6270 }
6371
72+ // If the cursor is moved onto or off a bracket, refresh the whole line so that
73+ // we can mark the matching bracket.
74+ _moveCursor ( dx ) {
75+ const cursorWasOnBracket = allBrackets . includes ( this . line [ this . cursor ] ) ;
76+ super . _moveCursor ( dx ) ;
77+ const cursorIsOnBracket = allBrackets . includes ( this . line [ this . cursor ] ) ;
78+ if ( cursorWasOnBracket || cursorIsOnBracket ) {
79+ this . _refreshLine ( ) ;
80+ }
81+ }
82+
83+ // When refreshinng the whole line, find matching brackets and keep the position
84+ // of the matching one in mind (if there is any).
85+ _refreshLine ( ) {
86+ try {
87+ if ( this . colorizeMatchingBracket && allBrackets . includes ( this . line [ this . cursor ] ) ) {
88+ this . highlightBracketPosition = this . _findMatchingBracket ( this . line , this . cursor ) ;
89+ }
90+ return super . _refreshLine ( ) ;
91+ } finally {
92+ this . highlightBracketPosition = - 1 ;
93+ }
94+ }
95+
6496 _writeToOutput ( stringToWrite ) {
6597 // Skip false-y values, and if we print only whitespace or have not yet
6698 // been fully initialized, just write to output directly.
@@ -141,7 +173,22 @@ class PrettyREPLServer extends repl.REPLServer {
141173 // In those cases, we split the string into prompt and non-prompt parts,
142174 // and colorize the full non-prompt part.
143175 stringToWrite = stringToWrite . substring ( this . _prompt . length ) ;
144- this . output . write ( this . _prompt + this . _doColorize ( stringToWrite ) ) ;
176+ if ( this . highlightBracketPosition !== - 1 ) {
177+ // If there is a matching bracket, we mark it in the string before
178+ // highlighting using BOM characters (because it seems safe to assume
179+ // that they are ignored by highlighting) so that we can remember where
180+ // the bracket was.
181+ stringToWrite =
182+ stringToWrite . substring ( 0 , this . highlightBracketPosition ) +
183+ '\ufeff' + stringToWrite [ this . highlightBracketPosition ] + '\ufeff' +
184+ stringToWrite . substring ( this . highlightBracketPosition + 1 ) ;
185+ stringToWrite = this . _doColorize ( stringToWrite ) ;
186+ // Then remove the BOM characters again and colorize the bracket in between.
187+ stringToWrite = stringToWrite . replace ( / \ufeff ( .+ ) \ufeff / , ( _ , bracket ) => this . colorizeMatchingBracket ( bracket ) ) ;
188+ } else {
189+ stringToWrite = this . _doColorize ( stringToWrite ) ;
190+ }
191+ this . output . write ( this . _prompt + stringToWrite ) ;
145192 }
146193
147194 _insertString ( c ) {
@@ -157,47 +204,45 @@ class PrettyREPLServer extends repl.REPLServer {
157204 return this . colorize ( str ) ;
158205 } ) ;
159206
160- _stripCompleteJSStructures ( str ) {
207+ _stripCompleteJSStructures = memoizeStringTransformerMethod ( 1000 , function ( str ) {
161208 // Remove substructures of the JS input string `str` in order to simplify it,
162- // by repeatedly removing matching pairs of quotes and parentheses/brackets.
163- let before ;
164- do {
165- before = str ;
166- str = this . _stripCompleteJSStructuresStep ( before ) ;
167- } while ( before !== str ) ;
168- return str ;
169- }
209+ // by removing matching pairs of quotes and parentheses/brackets.
170210
171- _stripCompleteJSStructuresStep = memoizeStringTransformerMethod ( 10000 , function ( str ) {
172- // This regular expression matches:
173- // - matching pairs of (), without any of ()[]{}`'" in between
174- // - matching pairs of [], without any of ()[]{}`'" in between
175- // - matching pairs of {}, without any of ()[]{}`'" in between
176- // - matching pairs of '', with only non-'\, \\, and \' preceded by an even number of \ in between
177- // - matching pairs of "", with only non-"\, \\, and \" preceded by an even number of \ in between
178- // - matching pairs of ``, with only non-`{}\, \\ and \` preceded by an even number of \ in between
179- const re = / \( [ ^ \( \) \[ \] \{ \} ` ' " ] * \) | \[ [ ^ \( \) \[ \] \{ \} ` ' " ] * \] | \{ [ ^ \( \) \[ \] \{ \} ` ' " ] * \} | ' ( [ ^ ' \\ ] | (?< = [ ^ \\ ] ( \\ \\ ) * ) \\ ' | \\ \\ ) * ' | " ( [ ^ " \\ ] | (?< = [ ^ \\ ] ( \\ \\ ) * ) \\ " | \\ \\ ) * " | ` ( [ ^ \{ \} ` \\ ] | (?< = [ ^ \\ ] ( \\ \\ ) * ) \\ ` | \\ \\ ) * ` / g;
180- // Match the regexp against the input. If there are no matches, we can just return.
181- const matches = [ ...str . matchAll ( re ) ] ;
182- if ( matches . length === 0 ) {
183- return str ;
184- }
185- // Remove all but the last, non-nested pair of (), because () can affect
186- // whether the previous word is seen as a keyword.
211+ // Specifically, remove all but the last, non-nested pair of (), because ()
212+ // can affect whether the previous word is seen as a keyword.
187213 // E.g.: When input is `function() {`, do not replace the ().
188214 // When input is `{ foo(); }`, do replace the `()`, then afterwards the `{ ... }`.
189- let startsReplaceIndex = matches . length - 1 ;
190- const lastMatch = matches [ matches . length - 1 ] ;
191- if ( lastMatch [ 0 ] . startsWith ( '(' ) && ! str . substr ( lastMatch . index + lastMatch [ 0 ] . length ) . match ( / [ \) \] \} ` ' " ] / ) ) {
192- startsReplaceIndex -- ;
215+ const brackets = this . _findAllMatchingBracketsIgnoreMismatches ( str ) ;
216+ if ( brackets . length > 0 ) {
217+ const last = brackets [ brackets . length - 1 ] ;
218+ if ( last . kind === '(' && ( last . parent == null || last . parent . end === - 1 ) )
219+ brackets . pop ( ) ;
193220 }
194- for ( let i = startsReplaceIndex ; i >= 0 ; i -- ) {
195- // Replace str1<match>str2 with str1str2. Go backwards so that the match
196- // indices into the string remain valid.
197- str = str . substr ( 0 , matches [ i ] . index ) + str . substr ( matches [ i ] . index + matches [ i ] [ 0 ] . length ) ;
221+ // Remove brackets in reverse order, so that their indices remain valid.
222+ for ( let i = brackets . length - 1 ; i >= 0 ; i -- ) {
223+ str = str . substr ( 0 , brackets [ i ] . start ) + str . substr ( brackets [ i ] . end + 1 ) ;
198224 }
199225 return str ;
200226 } ) ;
227+
228+ _findAllMatchingBracketsIgnoreMismatches = memoizeStringTransformerMethod ( 1000 , function ( str ) {
229+ return findAllMatchingBrackets ( str , true ) ;
230+ } ) ;
231+ _findAllMatchingBracketsIncludeMismatches = memoizeStringTransformerMethod ( 1000 , function ( str ) {
232+ return findAllMatchingBrackets ( str , false ) ;
233+ } ) ;
234+
235+ // Find the matching bracket opposite of the one at `position`.
236+ _findMatchingBracket ( line , position ) {
237+ const brackets = this . _findAllMatchingBracketsIncludeMismatches ( line ) ;
238+ for ( const bracket of brackets ) {
239+ if ( bracket . start === position )
240+ return bracket . end ;
241+ if ( bracket . end === position )
242+ return bracket . start ;
243+ }
244+ return - 1 ;
245+ }
201246}
202247
203248module . exports = {
0 commit comments