UNPKG

@ryusei/code

Version:

<div align="center"> <a href="https://code.ryuseijs.com"> <img alt="RyuseiCode" src="https://code.ryuseijs.com/images/svg/logo.svg" width="70"> </a>

161 lines (138 loc) 4.68 kB
import { BracketMatchingOptions, Elements, EventBusEvent, Position, Range, ScanResult, TokenInfo } from '@ryusei/code'; import { CATEGORY_BRACKET } from '@ryusei/light'; import { Component } from '../../classes/Component/Component'; import { Selection } from '../../components'; import { EVENT_BLUR, EVENT_READONLY, EVENT_SELECTED } from '../../constants/events'; import { CHANGED, EXTEND, SELECTING, START } from '../../constants/selection-states'; import { Editor } from '../../core/Editor/Editor'; import { debounce, escapeRegExp, rafThrottle } from '../../utils'; import { Throttle } from '../../utils/function/throttle/throttle'; import { DEFAULT_OPTIONS } from './defaults'; /** * The group ID for markers. * * @since 0.1.0 */ export const MARKER_ID = 'brackets'; /** * The debounce duration for the clear method. * * @since 0.1.0 */ export const CLEAR_DEBOUNCE_DURATION = 50; /** * The component for highlighting matched brackets. * * @since 0.1.0 */ export class BracketMatching extends Component { /** * The debounced clear function. */ private clear: Throttle<() => void>; /** * The collection of brackets. */ private brackets: BracketMatchingOptions[ 'brackets' ]; /** * Limits the number of lines to match brackets. */ private maxScanLines: number; /** * Initializes the component. * * @param elements - A collection of essential elements. */ mount( elements: Elements ): void { const options = this.getOptions( 'bracketMatching', DEFAULT_OPTIONS ); this.brackets = options.brackets; this.maxScanLines = options.maxScanLines; super.mount( elements ); this.clear = debounce( () => { this.Range.clear( MARKER_ID ) }, CLEAR_DEBOUNCE_DURATION ); this.update = rafThrottle( this.update.bind( this ) ); this.on( EVENT_SELECTED, this.onSelected, this ); this.on( EVENT_BLUR, this.clear ); this.on( EVENT_READONLY, ( e, readOnly ) => { if ( readOnly ) { this.clear(); } } ); } /** * Called when the selection state is changed. * * @param e - An EventBusEvent object. * @param Selection - A Selection instance. */ private onSelected( e: EventBusEvent<Editor>, Selection: Selection ): void { if ( Selection.is( START, SELECTING, EXTEND ) ) { this.clear(); } else if ( Selection.is( CHANGED ) ) { if ( ! this.Editor.readOnly && Selection.isCollapsed() ) { this.update(); } } } /** * Checks the current location and renders markers. */ private update(): void { const { focus } = this.Selection; const before: Position = focus[ 1 ] > 0 ? [ focus[ 0 ], focus[ 1 ] - 1 ] : null; this.clear.invoke(); [ before, focus ].some( position => { if ( position && this.Scope.inCategory( CATEGORY_BRACKET, position ) ) { this.draw( position[ 0 ], this.lines.getInfoAt( position ) ); return true; } } ); } /** * Draws the provided bracket token and its counterpart. * * @param row - A row index. * @param info - A TokenInfo object. */ private draw( row: number, info: TokenInfo ): void { const match = this.find( false, row, info ) || this.find( true, row, info ); if ( match ) { const { Range } = this; Range.clear( MARKER_ID ); Range.register( MARKER_ID, [ this.infoToRange( row, info ), this.infoToRange( match.row, match.info ) ] ); } } /** * Finds the counterpart of the provided token. * * @param findClosing - Determines whether to find closing part or not. * @param row - A row index. * @param info - A TokenInfo object. * * @return A counter token of the passed info if found, or otherwise `undefined`. */ private find( findClosing: boolean, row: number, info: TokenInfo ): ScanResult | undefined { const { brackets } = this; const index = brackets[ Number( ! findClosing ) ].indexOf( info.code ); if ( index > -1 ) { const counterpart = brackets[ Number( findClosing ) ][ index ]; return this.lines[ `scan${ findClosing ? 'Down' : 'Up' }` ]( [ row, info.from ], [ CATEGORY_BRACKET, new RegExp( escapeRegExp( counterpart ) ) ], [ CATEGORY_BRACKET, new RegExp( escapeRegExp( info.code ) ) ], 1, this.maxScanLines ); } } /** * Converts the provided TokeInfo object to the range. * * @param row - A row index. * @param info - A TokenInfo object to convert. * * @return A Range object. */ private infoToRange( row: number, info: TokenInfo ): Range { return { start: [ row, info.from ], end: [ row, info.to ] }; } }