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
JavaScript
/*
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