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>

413 lines (355 loc) 11.1 kB
import { Language, Options, Position, ScanResult, Token, TokenInfo, TokenMatcher } from '@ryusei/code'; import { AbstractArrayLike } from '../../classes/AbstractArrayLike/AbstractArrayLike'; import { LINE_BREAK } from '../../constants/characters'; import { Editor } from '../../core/Editor/Editor'; import { EventBus } from '../../event/EventBus'; import { forOwn, isUndefined, max, min } from '../../utils'; import { matchesToken } from '../../utils/token'; import { Line } from './Line'; /** * The max number of lines to be inserted at once. * Inserting many lines harms the performance. * * @since 0.1.0 */ const MAX_INSERTION_LENGTH = 100; /** * Delay for the asynchronous tokenization. * * @since 0.1.0 */ const ASYNC_SYNC_DELAY = 10; /** * The class for managing data of all lines. * * @since 0.1.0 */ export class Lines extends AbstractArrayLike<Line> { /** * Holds the setTimeout ids. */ private readonly timers: Record<string, ReturnType<typeof setTimeout>> = {}; /** * Holds the language object. */ private readonly language: Language; /** * Holds options. */ private readonly options: Options; /** * Holds the EventBus object. */ private readonly event: EventBus<Editor>; /** * The Lines constructor. * * @param event - An EventBus instance. * @param language - A Language object. * @param options - Options. */ constructor( event: EventBus<Editor>, language: Language, options: Options ) { super(); this.language = language; this.event = event; this.options = options; } /** * Inserts a new empty Line instance or instances at the specified row. * * @param row - A row index. * @param count - A number of lines to insert. */ insert( row: number, count?: number ): void { count = count || 1; while ( count-- > 0 ) { this.splice( row, 0, new Line( this.options ) ); } } /** * Deletes a Line instance or instances from the specified row. * * @param row - A row index. * @param count - A number of lines to delete. */ delete( row: number, count: number ): void { this.splice( row, count ); } /** * Syncs Line instances with the provided code. * * @param row - A row index where sync starts. * @param code - Code to sync. * @param limit - Optional. Limits the number of lines to sync. * @param before - Optional. A pseudo line prepended to the code. * @param time - Optional. A timestamp when the sync starts. * * @return `true` if the last line is changed, or otherwise `false`. */ sync( row: number, code: string, limit?: number, before = '', time?: number ): boolean { if ( before ) { code = before + LINE_BREAK + code; if ( limit ) { limit++; } } const lines = this.language.lexer.run( code, limit ); if ( before ) { lines.shift(); } let changed; for ( let i = 0; i < lines.length; i++ ) { const rowIndex = row + i; const tokens = lines[ i ]; if ( ! this[ rowIndex ] ) { this.insert( rowIndex ); } if ( this[ rowIndex ] ) { if ( i === lines.length - 1 ) { changed = ! this.isSame( this[ rowIndex ].tokens, tokens ); } this[ rowIndex ].set( tokens, time ); } } return changed; } /** * Starts an asynchronous sync process. * * @param id - A worker ID. * @param row - A row index where sync starts. * @param code - Code to sync. * @param limit - Optional. Limits the number of lines to sync. * @param before - Optional. A pseudo line prepended to the code. * @param callback - Optional. A function called after syncing. */ asyncSync( id: string, row: number, code: string, limit?: number, before = '', callback?: () => void ): void { const { timers } = this; if ( timers[ id ] ) { clearTimeout( timers[ id ] ); } const time = Date.now(); timers[ id ] = setTimeout( () => { this.sync( row, code, limit, before, time ); if ( callback ) { callback(); } }, ASYNC_SYNC_DELAY ); } /** * Finds the minimum indent string between the `startRow` and the `endRow`. * * @param startRow - A start row index to search from. * @param endRow - An end row index to search to. * * @return A minimum indent string. */ findMinIndent( startRow: number, endRow: number ): string { let minIndent = undefined; for ( let i = startRow; i <= endRow; i++ ) { const indent = this[ i ].getIndent(); minIndent = isUndefined( minIndent ) || minIndent.length > indent.length ? indent : minIndent; } return minIndent || ''; } /** * Returns a token info at the specified position. * * @param position - A position to search at. * * @return A TokenInfo if available, or otherwise `undefined`. */ getInfoAt( position: Position ): TokenInfo | undefined { const line = this[ position[ 0 ] ]; return line ? line.getInfoAt( position[ 1 ] ) : undefined; } /** * Searches backwards for a token that matches the matcher. * If the `counterpart` matcher is provided, * this method attempts to match the target and counterpart, such as `{` and `}`. * * @param position - A position to start searching. * @param matcher - A matcher. * @param counterpart - Optional. A matcher of the counter part. * @param depth - Optional. Determines the initial depth. * @param limit - Optional. Limits the number of lines to scan. * * @return A TokenInfo object if found, or `undefined` if not. */ scanUp( position: Position, matcher: TokenMatcher, counterpart?: TokenMatcher, depth = 0, limit?: number ): ScanResult | undefined { const info = this.getInfoAt( position ); if ( info ) { const [ row ] = position; const min = limit ? max( 0, row - limit ) : 0; for ( let i = row; i >= min; i-- ) { const tokens = this[ i ].tokens; for ( let j = i === row ? info.index : tokens.length - 1; j >= 0; j-- ) { if ( matchesToken( tokens[ j ], matcher ) ) { if ( ! depth ) { return { row: i, info: this[ i ].getInfo( j ) }; } depth++; } if ( counterpart && matchesToken( tokens[ j ], counterpart ) ) { depth--; } } } } } /** * Searches forwards for a token that matches the matcher. * If the `counterpart` matcher is provided, * this method attempts to match the target and counterpart, such as `{` and `}`. * * @param position - A position to start searching. * @param matcher - A matcher. * @param counterpart - Optional. A matcher of the counter part. * @param depth - Optional. Determines the initial depth. * @param limit - Optional. Limits the number of lines to scan. * * @return A TokenInfo object if found, or `undefined` if not. */ scanDown( position: Position, matcher: TokenMatcher, counterpart?: TokenMatcher, depth = 0, limit?: number ): ScanResult | undefined { const info = this.getInfoAt( position ); if ( info ) { const [ row ] = position; const { length } = this; const max = limit ? min( length, row + limit ) : length; for ( let i = row; i < max; i++ ) { const tokens = this[ i ].tokens; for ( let j = i === row ? info.index : 0; j < tokens.length; j++ ) { if ( matchesToken( tokens[ j ], matcher ) ) { if ( ! depth ) { return { row: i, info: this[ i ].getInfo( j ) }; } depth++; } if ( counterpart && matchesToken( tokens[ j ], counterpart ) ) { depth--; } } } } } /** * Searches for a start position where the split token actually starts. * If the token at the position is not split, this returns `undefined`. * * @param position - A position. * * @return A position where the split token starts if available. */ findBlockStart( position: Position ): Position | undefined { const info = this.getInfoAt( position ); if ( info ) { if ( info.split ) { const startRow = position[ 0 ] - info.distance; const line = this[ startRow ]; const lastInfo = line.getInfo( line.tokens.length - 1 ); return [ startRow, lastInfo.from ]; } return [ position[ 0 ], info.from ]; } } /** * Searches for an end position where the split token actually ends. * If the token at the position is not split, this returns `undefined`. * * @param position - A position. * * @return A position where the split token ends if available. */ findBlockEnd( position: Position ): Position | undefined { const info = this.getInfoAt( position ); if ( info ) { if ( info.split && ! info.tail ) { for ( let i = position[ 0 ] + 1; i < this.length; i++ ) { const info = this[ i ].getInfo( 0 ); if ( info && info.tail ) { return [ i, info.to ]; } } } return [ position[ 0 ], info.to ]; } } /** * Syncs the number of lines. * * @param row - A row index. * @param value - A new size. * * @return Increased or decreased number of lines. */ syncSize( row: number, value: number ): number { const diff = value - this.length; if ( diff > 0 ) { if ( diff < MAX_INSERTION_LENGTH ) { this.insert( row, diff ); } else { this.setLength( value ); } } else if ( diff < 0 ) { this.delete( row, -diff ); } return diff; } /** * Destroys the instance. * * @internal */ destroy(): void { forOwn( this.timers, clearTimeout ); } /** * Sets the length of this Lines. * All overflown items will be removed from the end, or all missing items are added to the end as empty lines. * * @param value - A new length. */ private setLength( value: number ): void { const { length } = this; if ( length > value ) { this.splice( value, length - value ); } else if ( length < value ) { while ( value-- > length ) { this.push( new Line( this.options ) ); } } } /** * Checks if the passed 2 arrays with tokens are same or not. * * @param tokens1 - An array with tokens. * @param tokens2 - Another array with tokens. * * @return `true` if they are considered as same, or otherwise `false`. */ private isSame( tokens1: readonly Token[], tokens2: readonly Token[] ): boolean { return tokens1.length === tokens2.length && tokens1.every( ( token1, index ) => { const token2 = tokens2[ index ]; return token1[ 0 ] === token2[ 0 ] && token1[ 1 ] === token2[ 1 ] && token1[ 2 ].depth === token2[ 2 ].depth; } ); } }