UNPKG

terminal-kit

Version:

256 colors, keys and mouse, input field, progress bars, screen buffer (including 32-bit composition and image loading), text buffer, and many more... Whether you just need colors and styles, build a simple interactive command line tool or a complexe termi

1,872 lines (1,339 loc) 60.5 kB
/* Terminal Kit Copyright (c) 2009 - 2022 Cédric Ronvel The MIT License (MIT) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ "use strict" ; const misc = require( './misc.js' ) ; const fs = require( 'fs' ) ; const string = require( 'string-kit' ) ; // A buffer suitable for text editor function TextBuffer( options = {} ) { this.ScreenBuffer = options.ScreenBuffer || ( options.dst && options.dst.constructor ) || termkit.ScreenBuffer ; // a screenBuffer this.dst = options.dst ; this.palette = options.palette || ( this.dst && this.dst.palette ) ; // virtually infinity by default this.width = options.width || Infinity ; // not used except by the blitter this.height = options.height || Infinity ; // not used except by the blitter this.dstClipRect = options.dstClipRect ? new termkit.Rect( options.dstClipRect ) : null ; this.x = options.x || 0 ; this.y = options.y || 0 ; this.firstLineRightShift = options.firstLineRightShift || 0 ; this.cx = 0 ; this.cy = 0 ; this.ch = false ; // cursor hidden this.voidTextBuffer = null ; // Another TextBuffer used as fallback for empty cells, usefull for placeholder/hint/etc this.defaultAttr = this.ScreenBuffer.prototype.DEFAULT_ATTR ; this.voidAttr = this.ScreenBuffer.prototype.DEFAULT_ATTR ; this.preserveMarkupFormat = this.ScreenBuffer.prototype.preserveMarkupFormat ; this.markupToAttrObject = this.ScreenBuffer.prototype.markupToAttrObject ; this.hidden = false ; this.tabWidth = + options.tabWidth || 4 ; this.forceInBound = !! options.forceInBound ; // If set to a number, force line-splitting when exceeding that width this.lineWrapWidth = options.lineWrapWidth || null ; // If true, force word-aware line-splitting this.wordWrap = !! options.wordWrap ; // DEPRECATED but kept for backward compatibility. if ( options.wordWrapWidth ) { this.lineWrapWidth = options.wordWrapWidth ; this.wordWrap = true ; } this.selectionRegion = null ; this.buffer = [ [] ] ; this.stateMachine = options.stateMachine || null ; if ( options.hidden ) { this.setHidden( options.hidden ) ; } } module.exports = TextBuffer ; // Backward compatibility TextBuffer.create = ( ... args ) => new TextBuffer( ... args ) ; TextBuffer.prototype.parseMarkup = string.markupMethod.bind( misc.markupOptions ) ; // Special: if positive or 0, it's the width of the char, if -1 it's an anti-filler, if -2 it's a filler function Cell( char = ' ' , special = 1 , attr = null , misc_ = null ) { this.char = char ; this.width = special >= 0 ? special : -special - 1 ; this.filler = special < 0 ; // note: antiFiller ARE filler this.attr = attr ; this.misc = misc_ ; } TextBuffer.Cell = Cell ; const termkit = require( './termkit.js' ) ; TextBuffer.prototype.getText = function() { return this.buffer.map( line => string.unicode.fromCells( line ) ).join( '' ) ; } ; // TODOC TextBuffer.prototype.getLineText = function( y = this.cy ) { if ( y >= this.buffer.length ) { return null ; } if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } return string.unicode.fromCells( this.buffer[ y ] ) ; } ; // TODOC // Get the indentation part of the line, return null if the line is empty (no char or no non-space char) TextBuffer.prototype.getLineIndent = function( y = this.cy ) { if ( ! this.buffer[ y ] ) { return null ; } var x , xmin , xmax , cell , indent = '' ; for ( x = 0 , xmax = this.buffer[ y ].length - 1 ; x <= xmax ; x ++ ) { cell = this.buffer[ y ][ x ] ; if ( ! cell.filler ) { if ( cell.char === '\t' || cell.char === ' ' ) { indent += cell.char ; } else if ( cell.char === '\n' ) { return null ; } else { return indent ; } } } return null ; } ; // TODOC // Count characters in this line, excluding fillers TextBuffer.prototype.getLineCharCount = function( y = this.cy ) { if ( y >= this.buffer.length ) { return null ; } if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } return this.getCellsCharCount( this.buffer[ y ] ) ; } ; // internal TextBuffer.prototype.getCellsCharCount = function( cells ) { var count = 0 ; for ( let cell of cells ) { if ( ! cell.filler ) { count ++ ; } } return count ; } ; // TODOC // Remove spaces and tabs at the end of the line TextBuffer.prototype.removeTrailingSpaces = function( y = this.cy , x = null , dry = false ) { if ( y >= this.buffer.length ) { return '' ; } if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } var line = this.buffer[ y ] ; x = x ?? line.length - 1 ; if ( x < 0 || x >= line.length ) { return '' ; } var deletedStr = '' , hasNL = line[ x ].char === '\n' ; if ( hasNL ) { x -- ; } for ( ; x >= 0 ; x -- ) { if ( line[ x ].filler ) { continue ; } let char = line[ x ].char ; if ( char === ' ' || char === '\t' ) { deletedStr = char + deletedStr ; } else { break ; } } if ( deletedStr && ! dry ) { line.splice( x + 1 , deletedStr.length ) ; } return deletedStr ; } ; // TODOC // Get the text, but separate before the cursor and after the cursor TextBuffer.prototype.getCursorSplittedText = function() { var y , line , before = '' , after = '' ; for ( y = 0 ; y < this.buffer.length ; y ++ ) { line = this.buffer[ y ] ; if ( y < this.cy ) { before += string.unicode.fromCells( line ) ; } else if ( y > this.cy ) { after += string.unicode.fromCells( line ) ; } else { before += string.unicode.fromCells( line.slice( 0 , this.cx ) ) ; after += string.unicode.fromCells( line.slice( this.cx ) ) ; } } return [ before , after ] ; } ; // .setText( text , [[hasMarkup] , baseAttr ] ) TextBuffer.prototype.setText = function( text , hasMarkup , baseAttr ) { // Argument management if ( typeof hasMarkup !== 'boolean' && typeof hasMarkup !== 'string' ) { baseAttr = hasMarkup ; hasMarkup = false ; } var legacyColor = false , parser = null ; switch ( hasMarkup ) { case 'ansi' : parser = string.ansi.parse ; break ; case 'legacyAnsi' : parser = string.ansi.parse ; legacyColor = true ; break ; case true : parser = this.parseMarkup ; break ; } if ( baseAttr === undefined ) { baseAttr = this.defaultAttr ; } if ( typeof baseAttr === 'object' ) { baseAttr = this.object2attr( baseAttr ) ; } // It must be reset now, because word-wrapping will be faster (always splice at the end of the array) this.buffer.length = 0 ; text.split( /(?<=\n)/g ).forEach( line => { var index = this.buffer.length ; this.buffer[ index ] = this.lineToCells( line , parser , baseAttr , 0 , legacyColor ) ; // /!\ Warning /!\ string.unicode.toCells() strips '\n', so we need to restore it at the end of the line if ( line[ line.length - 1 ] === '\n' ) { this.buffer[ index ].push( new Cell( '\n' , 1 , baseAttr ) ) ; } // word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine( index ) ; } } ) ; this.selectionRegion = null ; } ; // Internal, transform a line of text, with or without markup to cells... TextBuffer.prototype.lineToCells = function( line , parser , baseAttr , offset = 0 , legacyColor = false ) { if ( ! parser ) { return string.unicode.toCells( Cell , line , this.tabWidth , offset , baseAttr ) ; } var attr , attrObject , cells = [] ; const defaultAttrObject = this.ScreenBuffer.attr2object( this.ScreenBuffer.DEFAULT_ATTR ) ; const baseAttrObject = this.ScreenBuffer.attr2object( baseAttr ) ; parser( line ).forEach( part => { attrObject = Object.assign( {} , part.specialReset ? defaultAttrObject : baseAttrObject , part ) ; delete attrObject.text ; // Remove incompatible flags if ( attrObject.defaultColor && attrObject.color ) { delete attrObject.defaultColor ; } if ( attrObject.bgDefaultColor && attrObject.bgColor ) { delete attrObject.bgDefaultColor ; } attr = this.object2attr( attrObject , undefined , legacyColor ) ; if ( part.text ) { cells.push( ... string.unicode.toCells( Cell , part.text , this.tabWidth , offset + cells.length , attr ) ) ; } } ) ; return cells ; } ; TextBuffer.prototype.setHidden = function( value ) { this.hidden = typeof value === 'string' && value.length ? value[ 0 ] : value ? termkit.spChars.password : false ; } ; TextBuffer.prototype.getHidden = function() { return this.hidden ; } ; TextBuffer.prototype.setVoidTextBuffer = function( textBuffer = null ) { this.voidTextBuffer = textBuffer ; } ; TextBuffer.prototype.getVoidTextBuffer = function() { return this.voidTextBuffer ; } ; TextBuffer.prototype.getContentSize = function() { return { width: Math.max( 1 , ... this.buffer.map( line => line.length ) ) , height: this.buffer.length } ; } ; // TODOC TextBuffer.prototype.coordinateToOffset = function( px , py ) { var x , y , line , offset = 0 ; for ( y = 0 ; y < py ; y ++ ) { line = this.buffer[ y ] ; if ( ! line ) { continue ; } for ( x = 0 ; x < line.length ; x ++ ) { if ( ! line[ x ].filler ) { offset ++ ; } } } line = this.buffer[ py ] ; if ( line ) { for ( x = 0 ; x < px && x < line.length ; x ++ ) { if ( ! line[ x ].filler ) { offset ++ ; } } } return offset ; } ; // Cursor offset in the text-content (excluding fillers) TextBuffer.prototype.getCursorOffset = function() { return this.coordinateToOffset( this.cx , this.cy ) ; } ; // TODOC TextBuffer.prototype.offsetToCoordinate = function( offset ) { var line , x = 0 , y = 0 ; if ( offset < 0 ) { return ; } while ( y < this.buffer.length ) { x = 0 ; line = this.buffer[ y ] ; //console.error( " iter cy" , offset , y , x , "---" , line.length ) ; if ( ! line ) { continue ; } while ( x < line.length ) { //console.error( " iter cx" , offset , y , x ) ; if ( ! line[ x ].filler ) { if ( offset <= 0 ) { if ( x === line.length && line[ line.length - 1 ].char === '\n' ) { //console.error( " Exit with \\n" ) ; x = 0 ; y ++ ; } //console.error( "Exit" , y , x ) ; return { x , y } ; } offset -- ; } x ++ ; } y ++ ; } //console.error( "End of input" , offset , y , x ) ; } ; // Set the cursor position (cx,cy) depending on the offset in the text-content (excluding fillers) TextBuffer.prototype.setCursorOffset = function( offset ) { var coord = this.offsetToCoordinate() ; if ( ! coord ) { return ; } this.cx = coord.x ; this.cy = coord.y ; } ; // TODOC TextBuffer.prototype.setTabWidth = function( tabWidth ) { this.tabWidth = + tabWidth || 4 ; this.reTab() ; } ; // TODOC TextBuffer.prototype.reTab = function() { for ( let y = 0 ; y < this.buffer.length ; y ++ ) { this.reTabLine( 0 , y ) ; } } ; // Recompute tabs TextBuffer.prototype.reTabLine = function( startAt = 0 , y = this.cy ) { var length , cell , index , fillSize , input , output , linePosition = startAt ; if ( this.buffer[ y ] === undefined ) { this.buffer[ y ] = [] ; } input = this.buffer[ y ] ; output = input.slice( 0 , startAt ) ; length = input.length ; for ( index = startAt ; index < length ; index ++ ) { cell = input[ index ] ; if ( cell.char === '\t' ) { fillSize = this.tabWidth - ( linePosition % this.tabWidth ) - 1 ; output.push( cell ) ; linePosition += 1 + fillSize ; while ( fillSize -- ) { output.push( new Cell( ' ' , -2 , cell.attr , cell.misc ) ) ; } // Skip input filler while ( index + 1 < length && input[ index + 1 ].filler ) { index ++ ; } } else { output.push( cell ) ; linePosition ++ ; } } this.buffer[ y ] = output ; } ; // Forbidden split for word-wrap, only if there is only one space before const FORBIDDEN_SPLIT = new Set( [ // French typo double graph punctuation, '!' , '?' , ':' , ';' , '«' , '»' , // Other common punctuation that are often misused, should not be splitted anyway ',' , '.' , '…' ] ) ; // Wrap/word-wrap the current line, stop on the next explicit '\n' or at the end of the buffer. // Return the next line to scan. // /!\ Should probably .reTabLine() TextBuffer.prototype.wrapLine = function( startY = this.cy , width = this.lineWrapWidth , wordWrap = this.wordWrap ) { var x , y , rightShift , endY , line , lineWidth , previousLine , lastChar , found , cursorInlineOffset , checkCursor = this.cy === startY ; if ( startY >= this.buffer.length ) { return startY ; } // First check early exit conditions line = this.buffer[ startY ] ; previousLine = this.buffer[ startY - 1 ] ; rightShift = startY ? 0 : this.firstLineRightShift ; lineWidth = width - rightShift ; //console.error( "startY:" , startY ) ; if ( ! width || ( line.length && line.length <= lineWidth && line[ line.length - 1 ].char === '\n' && ( ! previousLine || ! previousLine.length || previousLine[ previousLine.length - 1 ].char === '\n' ) ) ) { //console.error( "exit" , previousLine); // There is nothing to do: we only have one line and it is not even longer than the lineWidth return startY + 1 ; } // Avoid creating arrays if early exit triggers var unifiedLine = [] , replacementLines = [] ; // First, search BACKWARD for the previous \n or start of buffer, to adjust startY value for ( y = startY - 1 ; y >= 0 ; y -- ) { line = this.buffer[ y ] ; if ( line.length && line[ line.length - 1 ].char === '\n' ) { startY = y + 1 ; break ; } else if ( ! y ) { startY = 0 ; break ; } } //console.error( "startY aft:" , startY ) ; // Then, search for the next \n and concat everything in a single line for ( y = startY ; y < this.buffer.length ; y ++ ) { //console.error( " iter" , y , this.buffer.length) ; line = this.buffer[ y ] ; unifiedLine.push( ... line ) ; if ( line.length && line[ line.length - 1 ].char === '\n' ) { //console.error( "has \\n" ) ; // If we found the next \n, we don't go any further, but we still increment y because of endY y ++ ; break ; } } // Save the last line index endY = y ; rightShift = startY ? 0 : this.firstLineRightShift ; //console.error( "endY:" , endY ) ; if ( checkCursor ) { // Compute the cursor "inline" position cursorInlineOffset = 0 ; for ( y = startY ; y < this.cy ; y ++ ) { // +1 because the cursor is allowed to be ahead by one cell cursorInlineOffset += this.buffer[ y ].length ; } cursorInlineOffset += this.cx ; } while ( unifiedLine.length ) { lineWidth = width - rightShift ; rightShift = 0 ; // Next time rightShift will be 0 if ( unifiedLine.length <= lineWidth ) { // No more than the allowed lineWidth: add it and finish replacementLines.push( unifiedLine ) ; // If the length is EXACTLY the line-width and it's the last lines, create a new empty line if ( unifiedLine.length === lineWidth ) { replacementLines.push( [] ) ; } break ; } if ( ! wordWrap ) { replacementLines.push( unifiedLine.splice( 0 , lineWidth ) ) ; continue ; } found = false ; x = lineWidth ; if ( unifiedLine[ x ].char === ' ' ) { // Search forward for the first non-space while ( x < unifiedLine.length && unifiedLine[ x ].char === ' ' ) { x ++ ; } if ( x >= unifiedLine.length ) { // No non-space found: feed every remaining cells replacementLines.push( unifiedLine ) ; break ; } if ( x === lineWidth + 1 && FORBIDDEN_SPLIT.has( unifiedLine[ x ].char ) && unifiedLine[ lineWidth - 1 ].char !== ' ' ) { // Dang! We can't split here! We will search backward starting from lineWidth - 1 x = lineWidth - 1 ; } else { // Else, cut at that non-space found = true ; } } if ( ! found ) { // Search backward for the first space lastChar = null ; while ( x >= 0 && ( unifiedLine[ x ].char !== ' ' || ( FORBIDDEN_SPLIT.has( lastChar ) && x > 0 && unifiedLine[ x - 1 ].char !== ' ' ) ) ) { lastChar = unifiedLine[ x ].char ; x -- ; } if ( x < 0 ) { x = lineWidth ; } // No space found, cut at the lineWidth else { x ++ ; } // Cut just after the space } replacementLines.push( unifiedLine.splice( 0 , x ) ) ; } this.buffer.splice( startY , endY - startY , ... replacementLines ) ; // New endY to be returned, and used for cursor computing endY = startY + replacementLines.length ; if ( checkCursor ) { //console.error( "cursorInlineOffset:" , cursorInlineOffset , "endY:" , endY ) ; for ( y = startY ; ; y ++ ) { //console.error( " iter" , y , "-- cursorInlineOffset:" , cursorInlineOffset , "this.buffer[ y ].length:" , this.buffer[ y ] && this.buffer[ y ].length ) ; if ( y >= endY ) { //console.error( " exit #1" ) ; if ( y > 0 ) { y -- ; } this.cy = y ; this.cx = this.buffer[ y ] ? this.buffer[ y ].length : 0 ; break ; } if ( ! this.buffer[ y ] ) { //console.error( " exit #2" ) ; this.cy = y ; this.cx = 0 ; break ; } if ( cursorInlineOffset < this.buffer[ y ].length ) { //console.error( " exit #3" ) ; this.cy = y ; this.cx = cursorInlineOffset ; break ; } cursorInlineOffset -= this.buffer[ y ].length ; } //* // If we are after a true line breaker, go to the next line if ( this.cx && this.buffer[ this.cy ] && this.buffer[ this.cy ][ this.cx - 1 ] && this.buffer[ this.cy ][ this.cx - 1 ].char === '\n' ) { this.cy ++ ; this.cx = 0 ; if ( ! this.buffer[ this.cy ] ) { this.buffer[ this.cy ] = [] ; } } //*/ } return endY ; } ; TextBuffer.prototype.wrapAllLines = function( width = this.lineWrapWidth , wordWrap = this.wordWrap ) { var y = 0 ; while ( y < this.buffer.length ) { y = this.wrapLine( y , width , wordWrap ) ; } } ; // Probably DEPRECATED TextBuffer.prototype.wordWrapLine = function( startY = this.cy , width = this.lineWrapWidth ) { return this.wrapLine( startY , width , true ) ; } ; // Probably DEPRECATED TextBuffer.prototype.wordWrapAllLines = function( width = this.lineWrapWidth ) { var y = 0 ; while ( y < this.buffer.length ) { y = this.wrapLine( y , width , true ) ; } } ; TextBuffer.prototype.setDefaultAttr = function( attr ) { if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; } else if ( typeof attr !== 'number' ) { return ; } this.defaultAttr = attr ; } ; TextBuffer.prototype.setEmptyCellAttr = // DEPRECATED TextBuffer.prototype.setVoidAttr = function( attr ) { if ( attr === null ) { this.voidAttr = null ; } // null: don't draw else if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; } else if ( typeof attr !== 'number' ) { return ; } this.voidAttr = attr ; } ; TextBuffer.prototype.setAttrAt = function( attr , x , y ) { if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; } else if ( typeof attr !== 'number' ) { return ; } this.setAttrCodeAt( attr , x , y ) ; } ; // Faster than setAttrAt(), do no check attr, assume an attr code (number) TextBuffer.prototype.setAttrCodeAt = function( attr , x , y ) { if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } if ( ! this.buffer[ y ][ x ] ) { this.buffer[ y ][ x ] = new Cell( ' ' , 1 , attr ) ; } else { this.buffer[ y ][ x ].attr = attr ; } } ; const WHOLE_BUFFER_REGION = { xmin: 0 , xmax: Infinity , ymin: 0 , ymax: Infinity } ; // Set a whole region TextBuffer.prototype.setAttrRegion = function( attr , region ) { if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; } else if ( typeof attr !== 'number' ) { return ; } this.setAttrCodeRegion( attr , region ) ; } ; // Faster than setAttrRegion(), do no check attr, assume an attr code (number) TextBuffer.prototype.setAttrCodeRegion = function( attr , region = WHOLE_BUFFER_REGION ) { var x , y , xmin , xmax , ymax ; ymax = Math.min( region.ymax , this.buffer.length - 1 ) ; for ( y = region.ymin ; y <= ymax ; y ++ ) { if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } xmin = y === region.ymin ? region.xmin : 0 ; xmax = y === region.ymax ? Math.min( region.xmax , this.buffer[ y ].length - 1 ) : this.buffer[ y ].length - 1 ; for ( x = xmin ; x <= xmax ; x ++ ) { this.buffer[ y ][ x ].attr = attr ; } } } ; TextBuffer.prototype.isInSelection = function( x = this.cx , y = this.cy ) { if ( ! this.selectionRegion ) { return false ; } return this.isInRegion( this.selectionRegion , x , y ) ; } ; TextBuffer.prototype.isInRegion = function( region , x = this.cx , y = this.cy ) { return ( y >= region.ymin && y <= region.ymax && ( y !== region.ymin || x >= region.xmin ) && ( y !== region.ymax || x <= region.xmax ) ) ; } ; TextBuffer.prototype.setSelectionRegion = function( region ) { if ( ! this.selectionRegion ) { this.selectionRegion = {} ; } if ( region.xmin !== undefined && region.ymin !== undefined ) { if ( region.xmin < 0 ) { region.xmin = 0 ; } if ( region.ymin < 0 ) { region.ymin = 0 ; } this.selectionRegion.xmin = region.xmin ; this.selectionRegion.ymin = region.ymin ; this.selectionRegion.cellMin = this.buffer[ region.ymin ]?.[ region.xmin ] ?? null ; } if ( region.xmax !== undefined && region.ymax !== undefined ) { this.selectionRegion.xmax = region.xmax ; this.selectionRegion.ymax = region.ymax ; this.selectionRegion.cellMax = this.buffer[ region.ymax ]?.[ region.xmax ] ?? null ; } } ; // TODOC TextBuffer.prototype.startOfSelection = function() { if ( ! this.selectionRegion ) { this.selectionRegion = {} ; } this.selectionRegion.xmin = this.cx ; this.selectionRegion.ymin = this.cy ; this.selectionRegion.cellMin = this.buffer[ this.cy ]?.[ this.cx ] ?? null ; } ; // TODOC TextBuffer.prototype.endOfSelection = function() { var coord = this.oneStepBackward() ; if ( ! this.selectionRegion ) { this.selectionRegion = {} ; } if ( ! coord ) { // Start of the file this.selectionRegion = null ; return ; } this.selectionRegion.xmax = coord.x ; this.selectionRegion.ymax = coord.y ; this.selectionRegion.cellMax = this.buffer[ coord.y ]?.[ coord.x ] ?? null ; } ; // TODOC // Reset the region by scanning for the starting and ending cell // If cursorCell is set, set cursor position to this cell TextBuffer.prototype.updateSelectionFromCells = function( cursorCell = null ) { if ( ! this.selectionRegion ) { return ; } if ( ! this.selectionRegion.cellMin || ! this.selectionRegion.cellMax ) { this.selectionRegion = null ; return ; } var xmin , xmax , ymin , ymax ; for ( let y = 0 ; y < this.buffer.length ; y ++ ) { let currentLine = this.buffer[ y ] ; if ( ! currentLine ) { continue ; } for ( let x = 0 ; x < currentLine.length ; x ++ ) { if ( currentLine[ x ] === this.selectionRegion.cellMin ) { xmin = x ; ymin = y ; } if ( currentLine[ x ] === this.selectionRegion.cellMax ) { xmax = x ; ymax = y ; } if ( cursorCell && currentLine[ x ] === cursorCell ) { this.cx = x ; this.cy = y ; } } } if ( ymin === undefined || ymax === undefined ) { this.selectionRegion = null ; return ; } this.selectionRegion.xmin = xmin ; this.selectionRegion.xmax = xmax ; this.selectionRegion.ymin = ymin ; this.selectionRegion.ymax = ymax ; } ; // TODOC // Return a Cell instance that is at the cursor location, or null if none TextBuffer.prototype.getCursorCell = function() { return this.buffer[ this.cy ]?.[ this.cx ] ?? null ; } ; // TODOC // Return true if found, else return false TextBuffer.prototype.updateCursorFromCell = function( cursorCell ) { if ( ! cursorCell ) { return false ; } for ( let y = 0 ; y < this.buffer.length ; y ++ ) { let currentLine = this.buffer[ y ] ; if ( ! currentLine ) { continue ; } for ( let x = 0 ; x < currentLine.length ; x ++ ) { if ( cursorCell && currentLine[ x ] === cursorCell ) { this.cx = x ; this.cy = y ; return true ; } } } return false ; } ; TextBuffer.prototype.resetSelectionRegion = function() { if ( ! this.selectionRegion ) { return ; } this.selectionRegion = null ; } ; TextBuffer.prototype.getSelectionText = function() { return this.getRegionText( this.selectionRegion ) ; } ; // TODOC TextBuffer.prototype.getRegionText = function( region , structured = false ) { var x , y , xmin , xmax , ymax , cell , count = 0 , str = '' ; if ( ! region || region.xmin === undefined || region.ymin === undefined || region.xmax === undefined || region.ymax === undefined ) { return ; } ymax = Math.min( region.ymax , this.buffer.length - 1 ) ; for ( y = region.ymin ; y <= ymax ; y ++ ) { if ( ! this.buffer[ y ] ) { this.buffer[ y ] = [] ; } xmin = y === region.ymin ? region.xmin : 0 ; xmax = y === region.ymax ? Math.min( region.xmax , this.buffer[ y ].length - 1 ) : this.buffer[ y ].length - 1 ; for ( x = xmin ; x <= xmax ; x ++ ) { cell = this.buffer[ y ][ x ] ; if ( ! cell.filler ) { str += cell.char ; count ++ ; } } } if ( structured ) { return { string: str , count } ; } return str ; } ; // TODOC TextBuffer.prototype.deleteSelection = function( getDeleted = false ) { if ( ! this.selectionRegion ) { return ; } var region = this.selectionRegion ; this.selectionRegion = null ; // unselect now return this.deleteRegion( region , getDeleted ) ; } ; // TODOC // Delete current line TextBuffer.prototype.deleteRegion = function( region , getDeleted = false ) { var x , y , xmin , xmax , ymax , currentLine , tabIndex , deleted , cursorCell ; if ( ! region || region.xmin === undefined || region.ymin === undefined || region.xmax === undefined || region.ymax === undefined ) { return ; } cursorCell = this.buffer[ this.cy ]?.[ this.cx ] ?? null ; if ( getDeleted ) { deleted = this.getRegionText( region , true ) ; } ymax = Math.min( region.ymax , this.buffer.length - 1 ) ; y = region.ymin ; currentLine = this.buffer[ y ] ; if ( y === ymax ) { if ( ! this.buffer[ y ] ) { return deleted ; } xmin = region.xmin ; xmax = Math.min( region.xmax , currentLine.length - 1 ) ; currentLine.splice( xmin , xmax - xmin + 1 ) ; } else { let lastLine = this.buffer[ ymax ] ; // First, remove next lines this.buffer.splice( y + 1 , ymax - y ) ; xmin = region.xmin ; currentLine.splice( xmin , currentLine.length - xmin ) ; if ( lastLine && lastLine.length ) { xmax = Math.min( region.xmax , lastLine.length - 1 ) ; lastLine.splice( 0 , xmax + 1 ) ; if ( lastLine.length ) { currentLine.splice( currentLine.length , 0 , ... lastLine ) ; } } } if ( y < this.buffer.length - 1 && ( ! currentLine.length || currentLine[ currentLine.length - 1 ].char !== '\n' ) ) { this.joinLine( true , y ) ; } tabIndex = this.indexOfCharInLine( currentLine , '\t' , region.xmin ) ; if ( tabIndex !== -1 ) { this.reTabLine( tabIndex , y ) ; } if ( this.selectionRegion ) { this.updateSelectionFromCells( cursorCell ) ; } else if ( cursorCell ) { this.updateCursorFromCell( cursorCell ) ; } return deleted ; } ; // Misc data are lazily created TextBuffer.prototype.getMisc = function() { if ( ! this.buffer[ this.cy ] || ! this.buffer[ this.cy ][ this.cx ] ) { return ; } if ( ! this.buffer[ this.cy ][ this.cx ].misc ) { this.buffer[ this.cy ][ this.cx ].misc = {} ; } return this.buffer[ this.cy ][ this.cx ].misc ; } ; TextBuffer.prototype.getMiscAt = function( x , y ) { if ( ! this.buffer[ y ] || ! this.buffer[ y ][ x ] ) { return ; } if ( ! this.buffer[ y ][ x ].misc ) { this.buffer[ y ][ x ].misc = {} ; } return this.buffer[ y ][ x ].misc ; } ; TextBuffer.prototype.iterate = function( options , callback ) { var x , y , yMax , cell , lastNonFillerCell , offset = 0 , length ; if ( typeof options === 'function' ) { callback = options ; options = {} ; } else if ( ! options || typeof options !== 'object' ) { options = {} ; } if ( ! this.buffer.length ) { return ; } for ( y = 0 , yMax = this.buffer.length ; y < yMax ; y ++ ) { if ( this.buffer[ y ] ) { length = this.buffer[ y ].length ; lastNonFillerCell = null ; for ( x = 0 ; x < length ; x ++ ) { cell = this.buffer[ y ][ x ] ; if ( cell.filler ) { if ( options.fillerCopyAttr && lastNonFillerCell ) { cell.attr = lastNonFillerCell.attr ; } } else { callback( { offset: offset , x: x , y: y , text: cell.char , attr: cell.attr , misc: cell.misc } ) ; offset ++ ; lastNonFillerCell = cell ; } } } } // Call the callback one last time at the end of the buffer, with an empty string. // Useful for 'Ne' (Neon) state machine. if ( options.finalCall ) { callback( { offset: offset + 1 , x: null , y: y , text: '' , attr: null , misc: null } ) ; } } ; // Move to the left to the leading cell of a full-width char TextBuffer.prototype.moveToLeadingFullWidth = function() { var currentLine = this.buffer[ this.cy ] ; while ( this.cx && currentLine?.[ this.cx ]?.filler && currentLine?.[ this.cx ]?.width === 0 ) { this.cx -- ; } } ; TextBuffer.prototype.moveTo = function( x , y ) { this.cx = x >= 0 ? x : 0 ; this.cy = y >= 0 ? y : 0 ; if ( this.forceInBound ) { this.moveInBound( true ) ; } this.moveToLeadingFullWidth() ; } ; TextBuffer.prototype.move = function( x , y ) { this.moveTo( this.cx + x , this.cy + y ) ; } ; TextBuffer.prototype.moveToColumn = function( x ) { this.moveTo( x , this.cy ) ; } ; TextBuffer.prototype.moveToLine = TextBuffer.prototype.moveToRow = function( y ) { this.moveTo( this.cx , y ) ; } ; TextBuffer.prototype.moveUp = function() { this.cy = this.cy > 0 ? this.cy - 1 : 0 ; if ( this.forceInBound ) { this.moveInBound( true ) ; } this.moveToLeadingFullWidth() ; } ; TextBuffer.prototype.moveDown = function() { this.cy ++ ; if ( this.forceInBound ) { this.moveInBound( true ) ; } this.moveToLeadingFullWidth() ; } ; TextBuffer.prototype.moveLeft = function() { this.cx = this.cx > 0 ? this.cx - 1 : 0 ; if ( this.forceInBound ) { this.moveInBound( true ) ; } this.moveToLeadingFullWidth() ; } ; TextBuffer.prototype.moveRight = function() { this.cx ++ ; var currentLine = this.buffer[ this.cy ] ; while ( currentLine?.[ this.cx ]?.filler && currentLine?.[ this.cx ]?.width === 0 ) { this.cx ++ ; } if ( this.forceInBound ) { this.moveInBound( true ) ; } } ; TextBuffer.prototype.moveForward = function( testFn , justSkipFiller ) { var oldCx = this.cx , currentLine = this.buffer[ this.cy ] ; //if ( justSkipFiller && ( ! currentLine || ! currentLine[ this.cx ] || ! currentLine[ this.cx ].filler || currentLine[ this.cx ].char !== '\n' ) ) { return ; } if ( justSkipFiller && ( ! currentLine || ! currentLine[ this.cx ] || ! currentLine[ this.cx ].filler ) ) { return ; } for ( ;; ) { if ( ! currentLine || this.cx + 1 > currentLine.length || ( this.cx < currentLine.length && currentLine[ this.cx ].char === '\n' ) ) { if ( this.cy + 1 < this.buffer.length || ! this.forceInBound ) { this.cy ++ ; this.cx = 0 ; } else { this.cx = oldCx ; } break ; } this.cx ++ ; if ( ! currentLine[ this.cx ] || ( ! currentLine[ this.cx ].filler && ( ! testFn || testFn( currentLine[ this.cx ].char , this.cx , this.cy ) ) ) ) { break ; } } if ( this.forceInBound ) { this.moveInBound() ; } } ; TextBuffer.prototype.moveBackward = function( testFn , justSkipFiller ) { var lineLength , currentLine = this.buffer[ this.cy ] ; //if ( justSkipFiller && ( ! currentLine || ! currentLine[ this.cx ] || ! currentLine[ this.cx ].filler || currentLine[ this.cx ].char !== '\n' ) ) { return ; } if ( justSkipFiller && ( ! currentLine || ! currentLine[ this.cx ] || ! currentLine[ this.cx ].filler ) ) { return ; } for ( ;; ) { lineLength = currentLine ? currentLine.length : 0 ; if ( this.cx > lineLength ) { this.cx = lineLength ; } else { this.cx -- ; } if ( this.cx < 0 ) { this.cy -- ; if ( this.cy < 0 ) { this.cy = 0 ; this.cx = 0 ; break ; } this.moveToEndOfLine() ; break ; } if ( ! currentLine || ! currentLine[ this.cx ] || ( ! currentLine[ this.cx ].filler && ( ! testFn || testFn( currentLine[ this.cx ].char ) ) ) ) { break ; } } if ( this.forceInBound ) { this.moveInBound() ; } } ; // Rough word boundary test const WORD_BOUNDARY = new Set( [ ' ' , '\t' , '.' , ',' , ';' , ':' , '!' , '?' , '/' , '\\' , '(' , ')' , '[' , ']' , '{' , '}' , '<' , '>' , '=' , "'" , '"' ] ) ; TextBuffer.prototype.wordBoundary_ = function( method , checkInitial ) { var initialChar , nonBoundarySeen = false ; if ( checkInitial && this.buffer[ this.cy ] && this.buffer[ this.cy ][ this.cx ] ) { initialChar = this.buffer[ this.cy ][ this.cx ].char ; if ( ! WORD_BOUNDARY.has( initialChar ) ) { nonBoundarySeen = true ; } } this[ method ]( char => { if ( WORD_BOUNDARY.has( char ) ) { if ( nonBoundarySeen ) { return true ; } return false ; } nonBoundarySeen = true ; return false ; } ) ; } ; TextBuffer.prototype.moveToEndOfWord = function() { return this.wordBoundary_( 'moveForward' , true ) ; } ; TextBuffer.prototype.moveToStartOfWord = function() { var char , oldCx = this.cx , oldCy = this.cy ; this.wordBoundary_( 'moveBackward' ) ; if ( this.cx < oldCx && this.cy === oldCy && this.buffer[ this.cy ] && this.buffer[ this.cy ][ this.cx ] ) { char = this.buffer[ this.cy ][ this.cx ].char ; if ( WORD_BOUNDARY.has( char ) ) { this.moveForward() ; } } else if ( this.cy < oldCy && oldCx !== 0 && this.buffer[ oldCy ] && this.buffer[ oldCy ][ 0 ] ) { char = this.buffer[ oldCy ][ 0 ].char ; if ( ! WORD_BOUNDARY.has( char ) ) { this.cx = 0 ; this.cy = oldCy ; } } } ; TextBuffer.prototype.moveToStartOfLine = function() { this.cx = 0 ; } ; TextBuffer.prototype.moveToEndOfLine = function() { var currentLine = this.buffer[ this.cy ] ; if ( ! currentLine ) { this.cx = 0 ; } else if ( currentLine.length && currentLine[ currentLine.length - 1 ].char === '\n' ) { this.cx = currentLine.length - 1 ; } else { this.cx = currentLine.length ; } } ; // Move to the start of the buffer: 0,0 TextBuffer.prototype.moveToStartOfBuffer = function() { this.cx = this.cy = 0 ; } ; // Move to the end of the buffer: end of line of the last line TextBuffer.prototype.moveToEndOfBuffer = function() { this.cy = this.buffer.length ? this.buffer.length - 1 : 0 ; this.moveToEndOfLine() ; } ; TextBuffer.prototype.moveInBound = function( ignoreCx ) { var currentLine = this.buffer[ this.cy ] ; if ( this.cy > this.buffer.length ) { this.cy = this.buffer.length ; } if ( ignoreCx ) { return ; } if ( ! currentLine ) { this.cx = 0 ; } else if ( currentLine.length && currentLine[ currentLine.length - 1 ].char === '\n' ) { if ( this.cx > currentLine.length - 1 ) { this.cx = currentLine.length - 1 ; } } else if ( this.cx > currentLine.length ) { this.cx = currentLine.length ; } } ; // .insert( text , [[hasMarkup] , attr ] ) TextBuffer.prototype.insert = function( text , hasMarkup , attr ) { var lines , index , length , count = 0 ; if ( ! text ) { return count ; } if ( typeof hasMarkup !== 'boolean' && typeof hasMarkup !== 'string' ) { attr = hasMarkup ; hasMarkup = false ; } var legacyColor = false , parser = null ; switch ( hasMarkup ) { case 'ansi' : parser = string.ansi.parse ; break ; case 'legacyAnsi' : parser = string.ansi.parse ; legacyColor = true ; break ; case true : parser = this.parseMarkup ; break ; } lines = text.split( '\n' ) ; length = lines.length ; if ( attr && typeof attr === 'object' ) { attr = this.object2attr( attr ) ; } else if ( typeof attr !== 'number' ) { attr = this.defaultAttr ; } if ( this.forceInBound ) { this.moveInBound() ; } count += this.inlineInsert( lines[ 0 ] , parser , attr ) ; for ( index = 1 ; index < length ; index ++ ) { this.newLine( true ) ; count ++ ; count += this.inlineInsert( lines[ index ] , parser , attr , legacyColor ) ; } if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return count ; } ; TextBuffer.prototype.prepend = function( text , hasMarkup , attr ) { this.moveToStartOfBuffer() ; this.insert( text , hasMarkup , attr ) ; } ; TextBuffer.prototype.append = function( text , hasMarkup , attr ) { this.moveToEndOfBuffer() ; this.insert( text , hasMarkup , attr ) ; } ; // Internal API: // Insert inline chars (no control chars) TextBuffer.prototype.inlineInsert = function( text , parser , attr , legacyColor = false ) { var currentLine , currentLineLength , hasNL , nlCell , tabIndex , fillSize , cells , cellsCharCount , count = 0 ; this.moveForward( undefined , true ) ; // just skip filler char // Should come after moving forward (rely on this.cx) //cells = string.unicode.toCells( Cell , text , this.tabWidth , this.cx , attr ) ; cells = this.lineToCells( text , parser , attr , this.cx , legacyColor ) ; cellsCharCount = this.getCellsCharCount( cells ) ; // Is this a new line? if ( this.cy >= this.buffer.length ) { // Create all missing lines, if any while ( this.buffer.length < this.cy ) { this.buffer.push( [ new Cell( '\n' , 1 , this.defaultAttr ) ] ) ; count ++ ; } // Add a '\n' to the last line, if it is missing if ( this.cy && ( ! this.buffer[ this.cy - 1 ].length || this.buffer[ this.cy - 1 ][ this.buffer[ this.cy - 1 ].length - 1 ].char !== '\n' ) ) { this.buffer[ this.cy - 1 ].push( new Cell( '\n' , 1 , this.defaultAttr ) ) ; count ++ ; } this.buffer[ this.cy ] = [] ; } currentLine = this.buffer[ this.cy ] ; currentLineLength = currentLine.length ; hasNL = currentLineLength && currentLine[ currentLineLength - 1 ].char === '\n' ; // Apply if ( this.cx === currentLineLength ) { if ( hasNL ) { currentLine.splice( currentLineLength - 1 , 0 , new Cell( ' ' , 1 , this.defaultAttr ) , ... cells ) ; count += 1 + cellsCharCount ; } else { currentLine.push( ... cells ) ; count += cellsCharCount ; } } else if ( this.cx < currentLineLength ) { currentLine.splice( this.cx , 0 , ... cells ) ; count += cellsCharCount ; } // this.cx > currentLineLength else if ( hasNL ) { fillSize = this.cx - currentLineLength + 1 ; nlCell = currentLine.pop() ; while ( fillSize -- ) { currentLine.push( new Cell( ' ' , 1 , this.defaultAttr ) ) ; count ++ ; } currentLine.push( ... cells , nlCell ) ; count += cellsCharCount ; } else { fillSize = this.cx - currentLineLength ; while ( fillSize -- ) { currentLine.push( new Cell( ' ' , 1 , this.defaultAttr ) ) ; count ++ ; } currentLine.push( ... cells ) ; count += cellsCharCount ; } // Patch tab if needed tabIndex = this.indexOfCharInLine( currentLine , '\t' , this.cx ) ; this.cx += cells.length ; // (AFTER cx++) word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine() ; } if ( tabIndex !== -1 ) { this.reTabLine( tabIndex ) ; } return count ; } ; // Internal utility function TextBuffer.prototype.indexOfCharInLine = function( line , char , index = 0 ) { var iMax = line.length ; for ( ; index < iMax ; index ++ ) { if ( line[ index ].char === char ) { return index ; } } // Like .indexOf() does... return -1 ; } ; // Delete chars TextBuffer.prototype.delete = function( count , getDeleted = false ) { var currentLine , inlineCount , fillerCount , hasNL , removedCells , deleted = getDeleted ? { string: '' , count: 0 } : undefined ; if ( count === undefined ) { count = 1 ; } if ( this.forceInBound ) { this.moveInBound() ; } if ( this.buffer[ this.cy ] && this.buffer[ this.cy ][ this.cx ] && this.buffer[ this.cy ][ this.cx ].filler ) { this.moveBackward( undefined , true ) ; // just skip filler char count -- ; } while ( count > 0 ) { currentLine = this.buffer[ this.cy ] ; // If we are already at the end of the buffer... if ( this.cy >= this.buffer.length || ( this.cy === this.buffer.length - 1 && this.cx >= currentLine.length ) ) { if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return deleted ; } if ( currentLine ) { // If the cursor is too far away, move it at the end of the line if ( this.cx > currentLine.length ) { this.cx = currentLine.length ; } if ( currentLine[ this.cx ] && currentLine[ this.cx ].char !== '\n' ) { // Compute inline delete hasNL = currentLine[ currentLine.length - 1 ]?.char === '\n' ; fillerCount = this.countInlineForwardFiller( count ) ; inlineCount = Math.min( count + fillerCount , currentLine.length - hasNL - this.cx ) ; // Apply inline delete if ( inlineCount > 0 ) { removedCells = currentLine.splice( this.cx , inlineCount ) ; if ( getDeleted ) { removedCells = removedCells.filter( cell => ! cell.filler ) ; deleted.string += removedCells.map( cell => cell.char ).join( '' ) ; deleted.count += removedCells.length ; } } count -= inlineCount - fillerCount ; } } if ( count > 0 ) { if ( this.joinLine( true ) ) { count -- ; if ( getDeleted ) { deleted.string += '\n' ; deleted.count ++ ; } } } } // word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine() ; } // Patch tab if needed //tabIndex = currentLine.indexOf( '\t' , this.cx ) ; //if ( tabIndex !== -1 ) { this.reTabLine( tabIndex ) ; } this.reTabLine() ; // Do it every time, before finding a better way to do it if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return deleted ; } ; // Delete backward chars TextBuffer.prototype.backDelete = function( count , getDeleted = false ) { //console.error( ">>> backDelete:" , count ) ; var currentLine , inlineCount , fillerCount , tabIndex , removedCells , deleted = getDeleted ? { string: '' , count: 0 } : undefined ; if ( count === undefined ) { count = 1 ; } if ( this.forceInBound ) { this.moveInBound() ; } if ( this.buffer[ this.cy ] && this.cx && this.buffer[ this.cy ][ this.cx - 1 ] && this.buffer[ this.cy ][ this.cx - 1 ].filler ) { this.moveBackward( undefined , true ) ; // just skip filler char //count -- ; // do not downcount: the cursor is always on a \x00 before deleting a \t } while ( count > 0 ) { currentLine = this.buffer[ this.cy ] ; // If we are already at the begining of the buffer... if ( this.cy === 0 && this.cx === 0 ) { if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return deleted ; } if ( currentLine ) { // If the cursor is to far away, move it at the end of the line, it will cost one 'count' if ( this.cx > currentLine.length ) { if ( currentLine.length && currentLine[ currentLine.length - 1 ].char === '\n' ) { this.cx = currentLine.length - 1 ; } else { this.cx = currentLine.length ; } count -- ; } else if ( this.cx && this.cx === currentLine.length && currentLine[ currentLine.length - 1 ].char === '\n' ) { this.cx = currentLine.length - 1 ; } // Compute inline delete fillerCount = this.countInlineBackwardFiller( count ) ; inlineCount = Math.min( count + fillerCount , this.cx ) ; //console.error( "inlineCount:" , inlineCount , fillerCount , this.cx , this.cx - inlineCount ) ; // Apply inline delete if ( inlineCount > 0 ) { removedCells = currentLine.splice( this.cx - inlineCount , inlineCount ) ; if ( getDeleted ) { removedCells = removedCells.filter( cell => ! cell.filler ) ; deleted.string = removedCells.map( cell => cell.char ).join( '' ) + deleted.string ; deleted.count += removedCells.length ; } this.cx -= inlineCount ; } count -= inlineCount - fillerCount ; } if ( count > 0 ) { this.cy -- ; this.cx = currentLine ? currentLine.length : 0 ; if ( this.joinLine( true ) ) { count -- ; if ( getDeleted ) { deleted.string = '\n' + deleted.string ; deleted.count ++ ; } } } } // word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine() ; } // Patch tab if needed //tabIndex = currentLine.indexOf( '\t' , this.cx ) ; //if ( tabIndex !== -1 ) { this.reTabLine( tabIndex ) ; } this.reTabLine( tabIndex ) ; // Do it every time, before finding a better way to do it if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return deleted ; } ; // Fix a backward counter, get an additional count for each null char encountered TextBuffer.prototype.countInlineBackwardFiller = function( count ) { var x , cell , filler = 0 ; for ( x = this.cx - 1 ; x >= 0 && count ; x -- ) { cell = this.buffer[ this.cy ][ x ] ; if ( cell && cell.filler ) { filler ++ ; } else { count -- ; } } return filler ; } ; // Fix a forward counter, get an additional count for each null char encountered TextBuffer.prototype.countInlineForwardFiller = function( count ) { var x , cell , xMax = this.buffer[ this.cy ].length , filler = 0 ; for ( x = this.cx ; x < xMax && count ; x ++ ) { cell = this.buffer[ this.cy ][ x + 1 ] ; if ( cell && cell.filler ) { filler ++ ; } else { count -- ; } } return filler ; } ; TextBuffer.prototype.newLine = function( internalCall ) { var currentLine , currentLineLength , nextLine = [] , tabIndex ; if ( ! internalCall && this.forceInBound ) { this.moveInBound() ; } if ( this.buffer[ this.cy ] === undefined ) { this.buffer[ this.cy ] = [] ; } currentLine = this.buffer[ this.cy ] ; currentLineLength = currentLine.length ; // Apply if ( this.cx < currentLineLength ) { nextLine = currentLine.slice( this.cx ) ; currentLine.length = this.cx ; } currentLine.push( new Cell( '\n' , 1 , this.defaultAttr ) ) ; this.buffer.splice( this.cy + 1 , 0 , nextLine ) ; this.cx = 0 ; this.cy ++ ; // Patch tab if needed if ( ! internalCall ) { // word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine() ; } tabIndex = this.indexOfCharInLine( currentLine , '\t' , this.cx ) ; if ( tabIndex !== -1 ) { this.reTabLine( tabIndex ) ; } if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } } } ; // If y is specified, we are not joining on current cursor TextBuffer.prototype.joinLine = function( internalCall , y ) { var tabIndex , currentLine , x , updateCursor = false , hasDeleted = false ; if ( y === undefined ) { y = this.cy ; updateCursor = true ; } if ( ! internalCall && this.forceInBound ) { this.moveInBound() ; } if ( this.buffer[ y ] === undefined ) { this.buffer[ y ] = [] ; } if ( this.buffer[ y + 1 ] === undefined ) { this.buffer[ y + 1 ] = [] ; } currentLine = this.buffer[ y ] ; if ( currentLine.length && currentLine[ currentLine.length - 1 ].char === '\n' ) { // Remove the last '\n' if any currentLine.length -- ; hasDeleted = true ; } x = currentLine.length ; if ( updateCursor ) { this.cx = x ; } currentLine.splice( currentLine.length , 0 , ... this.buffer[ y + 1 ] ) ; this.buffer.splice( y + 1 , 1 ) ; // Patch tab if needed if ( ! internalCall ) { // word-wrap the current line, which is always the last line of the array (=faster) if ( this.lineWrapWidth ) { this.wrapLine() ; } tabIndex = this.indexOfCharInLine( currentLine , '\t' , x ) ; if ( tabIndex !== -1 ) { this.reTabLine( tabIndex , y ) ; } if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } } return hasDeleted ; } ; // TODOC // Delete current line TextBuffer.prototype.deleteLine = function( getDeleted = false ) { var currentLine , inlineCount , fillerCount , hasNL , removedCells , deleted ; if ( this.forceInBound ) { this.moveInBound() ; } if ( this.cy >= this.buffer.length ) { return ; } if ( getDeleted ) { deleted = { count: this.getLineCharCount() , string: this.getLineText() } ; } this.buffer.splice( this.cy , 1 ) ; if ( this.selectionRegion ) { this.updateSelectionFromCells() ; } return deleted ; } ; // TODOC // Return a region where the searchString is found TextBuffer.prototype.findNext = function( searchString , startPosition , reverse ) { var index , startAt , endAt , text = this.getText() , // /!\ another function MUST BE used once unicode composition will be supported // It is meant to produce the exact same cell size size = string.unicode.toArray( searchString ).length ; reverse = !! reverse ; if ( reverse ) { startPosition = startPosition ? startPosition - size : text.length - size ; } else { startPosition = startPosition ?? 0 ; } index = reverse ? text.lastIndexOf( searchString , startPosition ) : text.indexOf( searchString , startPosition ) ; if ( index === -1 ) { return ; } startAt = this.offsetToCoordinate( index ) ; endAt = this.offsetToCoordinate( index + size - 1 ) ; return { xmin: startAt.x , ymin: startAt.y , xmax: endAt.x , ymax: endAt.y } ; } ; // TODOC TextBuffer.prototype.f