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>

317 lines (270 loc) 9.2 kB
import { RowRange } from '@ryusei/code'; import { Component } from '../../classes/Component/Component'; import { EVENT_SYNCED } from '../../constants/events'; import { between, isUndefined, max, min } from '../../utils'; import { ASYNC_SYNC_LINES, ASYNC_SYNC_LINES_BACKWARDS, SYNC_LINES_BACKWARDS } from './constants'; /** * The type for an object with the number to start syncing the code and a prefix. * * @since 0.1.0 */ type SyncStartInfo = { startRow: number, before: string }; /** * The class for syncing changes to Lines and View components. * * @since 0.1.0 */ export class Sync extends Component { /** * Holds the minimum row for asynchronous syncing. */ private minStart = Infinity; /** * Holds the maximum row for asynchronous syncing. */ private maxEnd = 0; /** * Indicates whether the asynchronous syncing is on going or not. */ private syncing: boolean; /** * Syncs changes between the start and end rows to other components. * * @example * Consider the following HTML as an example: * * ```html * <pre> * function message() { * console.log( 'Hi!' ); * } * </pre> * ``` * * Let's attempt to modify the line 2 (the row index is `1`): * * ```ts * const ryuseiCode = new RyuseiCode(); * ryuseiCode.apply( 'pre' ); * * const { Code, Sync } = ryuseiCode.Editor.Components; * * // Only the Code component knows the change * Code.replaceLines( 1, 1, ` console.warn( 'error' );\n` ); * * // Syncs the change to other components * Sync.sync( 1, 1 ); * ``` * * @param startRow - A start row index. * @param endRow - An end row index. * @param jumpTo - Optional. Jumps to the specified row before starting synchronization. */ sync( startRow: number, endRow: number, jumpTo?: number ): void { const { Chunk, View } = this; const diff = this.lines.syncSize( startRow, this.Code.size ); View.autoHeight(); View.autoWidth(); if ( ! isUndefined( jumpTo ) ) { View.jump( jumpTo ); } if ( Chunk.includes( startRow ) ) { this.run( startRow, Chunk.end - startRow + 1 ); } else { const { start, end } = Chunk; this.run( start, end - start + 1, false ); this.syncLines( startRow, endRow ); } Chunk.syncDiff( startRow, diff ); Chunk.sync(); } /** * Starts the sync sequence. * * @param row - A row index. * @param limit - Limits the number of synchronously syncing. * @param strict - Optional. Determines whether the synchronization must be strict or not. */ private run( row: number, limit: number, strict = true ): void { const result = this.find( row, SYNC_LINES_BACKWARDS ); let { startRow } = result; if ( ! strict && row - startRow > SYNC_LINES_BACKWARDS ) { startRow = row - SYNC_LINES_BACKWARDS; } limit = row - startRow + limit; const changed = this.lines.sync( startRow, this.Code.after( startRow ), limit, result.before ); if ( changed || this.syncing ) { const { size } = this.Code; startRow = startRow + limit; if ( startRow < size ) { this.syncLines( startRow, size - 1 ); } } } /** * Asynchronously syncs lines between the provided range. * If the range is wider than the current running process, cancels it and starts a new process. * * @param startRow - A start row index. * @param endRow - An end row index. */ private syncLines( startRow: number, endRow: number ): void { this.minStart = min( startRow, this.minStart ); this.maxEnd = max( endRow, this.maxEnd ); this.syncing = true; const ranges = this.splitRows( this.minStart, this.maxEnd ); this.syncRanges( ranges, () => { this.minStart = Infinity; this.maxEnd = 0; this.syncing = false; this.Chunk.sync(); } ); } /** * Syncs provided ranges step by step. * * @param ranges - An array with row ranges. * @param callback - Optional. A callback fired after the sync is completed. */ private syncRanges( ranges: RowRange[], callback?: () => void ): void { const range = ranges.shift(); const { startRow, before } = this.find( range[ 0 ], ASYNC_SYNC_LINES_BACKWARDS ); const limit = range[ 1 ] - startRow + 1; this.lines.asyncSync( 'syncRanges', startRow, this.Code.after( startRow ), limit, before, () => { if ( ranges.length ) { this.syncRanges( ranges, callback ); this.emit( EVENT_SYNCED, this, false ); } else { if ( callback ) { callback(); } this.emit( EVENT_SYNCED, this, true ); } } ); } /** * Splits the provided row range into small fragments. * * @param startRow - A start row index. * @param endRow - An end row index. * * @return An array with row ranges. */ private splitRows( startRow: number, endRow: number ): RowRange[] { const ranges: RowRange[] = []; while ( startRow <= endRow ) { ranges.push( [ startRow, min( startRow + ASYNC_SYNC_LINES - 1, endRow ) ] ); startRow += ASYNC_SYNC_LINES; } return ranges; } /** * Returns an info object to start syncing. * * @param row - A row index. * @param limit - Limits the number of lines. * * @return An object with a start row index and code to prepend. */ private find( row: number, limit: number ): SyncStartInfo { if ( this.isEmbedded( row ) ) { return this.findStartInLanguageBlock( row, limit ); } const startRow = this.findRoot( row ); if ( row - startRow > limit ) { if ( this.isEmbedded( row - limit ) ) { return this.findStartInLanguageBlock( row - limit, limit / 2 ); } return this.compress( startRow, row, '', limit ); } return { startRow, before: '' }; } /** * If the distance from the `row` to `startRow` is greater than the `limit`, * attempt to shorten the distance by generating pseudo code. * * @param startRow - A start row index. * @param row - An original row index. * @param before - A pseudo line to prepend. * @param limit - A limit number of lines. * * @return An object with a start row index and code to prepend. */ private compress( startRow: number, row: number, before: string, limit: number ): SyncStartInfo { if ( row - startRow > limit ) { const start = this.lines.findBlockStart( [ row - 1, 0 ] ); if ( start ) { const { multiline } = this.getLanguage( start ); const info = this.lines.getInfoAt( start ); if ( info && multiline ) { for ( let i = 0; i < multiline.length; i++ ) { const item = multiline[ i ]; if ( info.category === item[ 2 ] && ( ! item[ 3 ] || info.state === item[ 3 ] ) ) { startRow = start[ 0 ] + 1; before += item[ 0 ]; break; } } } } } return { startRow, before }; } /** * Finds the likely appropriate index where tokenization should start. * * @param row - A row index. * @param depth - Optional. Minimum depth of a line that can be a candidate. * * @return A better index for starting tokenization. */ private findRoot( row: number, depth = 0 ): number { const { lines } = this; if ( between( row, 0, lines.length, true ) ) { for ( let i = row - 1; i >= 0; i-- ) { const line = lines[ i ]; if ( line.depth <= depth && line.tokens.length && ! line.isEmpty() ) { if ( line.split ) { i -= line.first[ 2 ].distance + 1; } else { return i; } } } } return 0; } /** * Finds a sync start info in an embedded language block. * * @param row - A row index. * @param limit - A limit number of lines. * * @return An object with a start row index and code to prepend. */ private findStartInLanguageBlock( row: number, limit: number ): SyncStartInfo { const { lines } = this; const lang = lines[ row ].language; const config = this.language.use[ lang ]; const startRow = this.findRoot( row, config.depth ); const startLang = lines[ startRow ].language; if ( startLang === lang ) { return this.compress( startRow, row, config.code, limit ); } return { startRow, before: '' }; } /** * Checks if the line at the specified row is inside an embedded block or not. * * @param row - A row index. * * @return `true` if the row is inside an embedded block, or otherwise `false`. */ private isEmbedded( row: number ): boolean { const line = this.lines[ row ]; if ( line ) { const { language } = line; return language && this.language.language.id !== language; } } }