/**
 * This helper automatically handles matching pairs.
 * Specifically, it does the following:
 *
 * 1. If the key is a closer, and the character in front of the cursor is the
 *    same, simply move the cursor forward.
 * 2. If the key is an opener, insert the opener at the beginning of the
 *    selection, and the closer at the end of the selection, and move the
 *    selection forward.
 * 3. If the backspace is hit, and the characters before and after the cursor
 *    are a pair, remove both characters and move the cursor backward.
 */ const pairs = [ '()' , '[]' , '{}' , `''` , '""' ] ; const openers = pairs . map ( ( pair ) => pair [ 0 ] ) ; const closers = pairs . map ( ( pair ) => pair [ 1 ] ) ; interface MatchPairsOptions { value : string ; selectionStart : number ; selectionEnd : number ; key : string ; metaKey : boolean ; updateQuery : ( query : string , selectionStart : number , selectionEnd : number ) => void ; preventDefault : ( ) => void ; } export function matchPairs ( { value , selectionStart , selectionEnd , key , metaKey , updateQuery , preventDefault , } ) { if ( shouldMoveCursorForward ( key , value , selectionStart , selectionEnd ) ) { preventDefault ( ) ; updateQuery ( value , selectionStart + 1 , selectionEnd + 1 ) ; } else if ( shouldInsertMatchingCloser ( key , value , selectionStart , selectionEnd ) ) { preventDefault ( ) ; const newValue = value . substr ( 0 , selectionStart ) + key + value . substring ( selectionStart , selectionEnd ) + closers [ openers . indexOf ( key ) ] + value . substr ( selectionEnd ) ; updateQuery ( newValue , selectionStart + 1 , selectionEnd + 1 ) ; } else if ( shouldRemovePair ( key , metaKey , value , selectionStart , selectionEnd ) ) { preventDefault ( ) ; const newValue = value . substr ( 0 , selectionEnd - 1 ) + value . substr ( selectionEnd + 1 ) ; updateQuery ( newValue , selectionStart - 1 , selectionEnd - 1 ) ; } } function shouldMoveCursorForward ( key , value , selectionStart , selectionEnd ) { if ( ! closers . includes ( key ) ) { return false ; } if ( selectionStart !== selectionEnd ) { return false ; } return value . charAt ( selectionEnd ) === key ; } function shouldInsertMatchingCloser ( key , value , selectionStart , selectionEnd ) { if ( ! openers . includes ( key ) ) { return false ; } if ( selectionStart !== selectionEnd ) { return true ; } const precedingCharacter = value . charAt ( selectionStart - 1 ) ; const followingCharacter = value . charAt ( selectionStart + 1 ) ; if ( precedingCharacter === '\\' ) { return false ; } return ! ( [ '"' , `'` ] . includes ( key ) && ( isAlphanumeric ( precedingCharacter ) || isAlphanumeric ( followingCharacter ) ) ) ; } function shouldRemovePair ( key , metaKey , value , selectionStart , selectionEnd ) { if ( key !== 'Backspace' || metaKey ) { return false ; } if ( selectionStart !== selectionEnd ) { return false ; } return pairs . includes ( value . substr ( selectionEnd - 1 , 2 ) ) ; } function isAlphanumeric ( value = '' ) { return value . match ( /[a-zA-Z0-9_]/ ) ; }