diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index cf878cbc9a..39516f72f5 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -220,6 +220,9 @@ export class MockBuffer implements IBuffer { public addMarker(y: number): IMarker { throw new Error('Method not implemented.'); } + public splitLine(row: number, col: number): void { + throw new Error('Method not implemented.'); + } public isCursorInViewport!: boolean; public lines!: ICircularList; public ydisp!: number; @@ -240,6 +243,9 @@ export class MockBuffer implements IBuffer { public getWrappedRangeForLine(y: number): { first: number, last: number } { return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments as any); } + public reflowRegion(startRow: number, endRow: number, maxRows: number): boolean { + throw new Error('Method not implemented.'); + } public nextStop(x?: number): number { throw new Error('Method not implemented.'); } @@ -264,6 +270,9 @@ export class MockBuffer implements IBuffer { public clearAllMarkers(): void { throw new Error('Method not implemented.'); } + public setWrapped(row: number, value: boolean): void { + throw new Error('Method not implemented.'); + } } export class MockRenderer implements IRenderer { diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 3b4309437f..3d5a05c3f1 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -28,6 +28,7 @@ export class Terminal extends Disposable implements ITerminalApi { private _parser: IParser | undefined; private _buffer: BufferNamespaceApi | undefined; private _publicOptions: Required; + public logOutput: boolean = false; constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) { super(); @@ -224,6 +225,14 @@ export class Terminal extends Disposable implements ITerminalApi { this._core.clear(); } public write(data: string | Uint8Array, callback?: () => void): void { + if (this.logOutput && data instanceof Uint8Array) { + const thisAny = this as any; + if (! thisAny._decoder) { + thisAny._decoder = new TextDecoder(); // label = "utf-8"); + } + const str = thisAny._decoder.decode(data, { stream:true }); + console.log('write: '+JSON.stringify(str)); + } this._core.write(data, callback); } public writeln(data: string | Uint8Array, callback?: () => void): void { diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 14c4ade147..68ebe4e121 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -405,7 +405,7 @@ describe('DomRendererRowFactory', () => { }); it('should handle BCE correctly', () => { - const nullCell = lineData.loadCell(0, new CellData()); + const nullCell = CellData.fromChar(' '); nullCell.bg = Attributes.CM_P16 | 1; lineData.setCell(2, nullCell); nullCell.bg = Attributes.CM_P16 | 2; @@ -418,7 +418,7 @@ describe('DomRendererRowFactory', () => { }); it('should handle BCE for multiple cells', () => { - const nullCell = lineData.loadCell(0, new CellData()); + const nullCell = CellData.fromChar(' '); nullCell.bg = Attributes.CM_P16 | 1; lineData.setCell(0, nullCell); let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); @@ -451,7 +451,7 @@ describe('DomRendererRowFactory', () => { lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)])); lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)])); lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)])); - lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)])); + lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)])); const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1); assert.equal(extractHtml(spans), 'ac語𝄞' @@ -502,7 +502,7 @@ describe('DomRendererRowFactory', () => { } function createEmptyLineData(cols: number): IBufferLine { - const lineData = new BufferLine(cols); + const lineData = BufferLine.make(cols); for (let i = 0; i < cols; i++) { lineData.setCell(i, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE])); } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index d71edeb96f..3b6dfa06ae 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -10,7 +10,6 @@ import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { channels, color } from 'common/Color'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; import { WidthCache } from 'browser/renderer/dom/WidthCache'; @@ -44,7 +43,7 @@ export class DomRendererRowFactory { constructor( private readonly _document: Document, - @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, + @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, // FIXME remove @IOptionsService private readonly _optionsService: IOptionsService, @ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService, @ICoreService private readonly _coreService: ICoreService, @@ -71,9 +70,9 @@ export class DomRendererRowFactory { linkStart: number, linkEnd: number ): HTMLSpanElement[] { + const cell = this._workCell; const elements: HTMLSpanElement[] = []; - const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); const colors = this._themeService.colors; let lineLength = lineData.getNoBgTrimmedLength(); @@ -97,7 +96,7 @@ export class DomRendererRowFactory { for (let x = 0; x < lineLength; x++) { lineData.loadCell(x, this._workCell); - let width = this._workCell.getWidth(); + const width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 if (width === 0) { @@ -105,13 +104,13 @@ export class DomRendererRowFactory { } // If true, indicates that the current character(s) to draw were joined. - let isJoined = false; - let lastCharX = x; + const isJoined = false; + const lastCharX = x; // Process any joined character ranges as needed. Because of how the // ranges are produced, we know that they are valid for the characters // and attributes of our input. - let cell = this._workCell; + /* if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { isJoined = true; const range = joinedRanges.shift()!; @@ -130,6 +129,7 @@ export class DomRendererRowFactory { // Recalculate width width = cell.getWidth(); } + */ const isInSelection = this._isCellInSelection(x, row); const isCursorCell = isCursorRow && x === cursorX; diff --git a/src/browser/services/CharacterJoinerService.test.ts b/src/browser/services/CharacterJoinerService.test.ts index 6b5326d939..044b13dedf 100644 --- a/src/browser/services/CharacterJoinerService.test.ts +++ b/src/browser/services/CharacterJoinerService.test.ts @@ -22,7 +22,7 @@ describe('CharacterJoinerService', () => { lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]])); lines.set(3, lineData([['no joined ranges']])); - lines.set(4, new BufferLine(0)); + lines.set(4, BufferLine.make(0)); lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]])); const line6 = lineData([['wi']]); line6.resize(line6.length + 1, CellData.fromCharData([0, '¥', 2, '¥'.charCodeAt(0)])); @@ -267,7 +267,7 @@ describe('CharacterJoinerService', () => { type IPartialLineData = ([string] | [string, number]); function lineData(data: IPartialLineData[]): IBufferLine { - const tline = new BufferLine(0); + const tline = BufferLine.make(0); for (let i = 0; i < data.length; ++i) { const line = data[i][0]; const attr = (data[i][1] || 0) as number; diff --git a/src/browser/services/CharacterJoinerService.ts b/src/browser/services/CharacterJoinerService.ts index ca4f1984e3..53cc3d48d6 100644 --- a/src/browser/services/CharacterJoinerService.ts +++ b/src/browser/services/CharacterJoinerService.ts @@ -11,6 +11,7 @@ import { CellData } from 'common/buffer/CellData'; import { IBufferService } from 'common/services/Services'; import { ICharacterJoinerService } from 'browser/services/Services'; +// FIXME should probably just use plain CellData export class JoinedCellData extends AttributeData implements ICellData { private _width: number; // .content carries no meaning for joined CellData, simply nullify it diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index 67158def09..e6ea6ed008 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -55,7 +55,7 @@ describe('SelectionService', () => { }); function stringToRow(text: string): IBufferLine { - const result = new BufferLine(text.length); + const result = BufferLine.make(text.length); for (let i = 0; i < text.length; i++) { result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); } @@ -63,7 +63,7 @@ describe('SelectionService', () => { } function stringArrayToRow(chars: string[]): IBufferLine { - const line = new BufferLine(chars.length); + const line = BufferLine.make(chars.length); chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)]))); return line; } @@ -118,7 +118,7 @@ describe('SelectionService', () => { [0, 'o', 1, 'o'.charCodeAt(0)], [0, 'o', 1, 'o'.charCodeAt(0)] ]; - const line = new BufferLine(data.length); + const line = BufferLine.make(data.length); for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i])); buffer.lines.set(0, line); // Ensure wide characters take up 2 columns @@ -193,7 +193,7 @@ describe('SelectionService', () => { it('should expand upwards or downards for wrapped lines', () => { buffer.lines.set(0, stringToRow(' foo')); buffer.lines.set(1, stringToRow('bar ')); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); selectionService.selectWordAt([1, 1]); assert.equal(selectionService.selectionText, 'foobar'); selectionService.model.clearSelection(); @@ -207,10 +207,10 @@ describe('SelectionService', () => { buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbb')); buffer.lines.set(3, stringToRow('cccccccccccccccccccc')); buffer.lines.set(4, stringToRow('bar ')); - buffer.lines.get(1)!.isWrapped = true; - buffer.lines.get(2)!.isWrapped = true; - buffer.lines.get(3)!.isWrapped = true; - buffer.lines.get(4)!.isWrapped = true; + buffer.setWrapped(1, true); + buffer.setWrapped(2, true); + buffer.setWrapped(3, true); + buffer.setWrapped(4, true); selectionService.selectWordAt([18, 0]); assert.equal(selectionService.selectionText, expectedText); selectionService.model.clearSelection(); @@ -349,8 +349,8 @@ describe('SelectionService', () => { it('should select the entire wrapped line', () => { buffer.lines.set(0, stringToRow('foo')); const line2 = stringToRow('bar'); - line2.isWrapped = true; buffer.lines.set(1, line2); + buffer.setWrapped(1, true); selectionService.selectLineAt(0); assert.equal(selectionService.selectionText, 'foobar', 'The selected text is correct'); assert.deepEqual(selectionService.model.selectionStart, [0, 0]); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 9da8ab5d49..0a78c21f11 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -864,7 +864,7 @@ export class SelectionService extends Disposable implements ISelectionService { } // Expand the string in both directions until a space is hit - while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) { + while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell) as CellData)) { bufferLine.loadCell(startCol - 1, this._workCell); const length = this._workCell.getChars().length; if (this._workCell.getWidth() === 0) { @@ -880,7 +880,7 @@ export class SelectionService extends Disposable implements ISelectionService { startIndex--; startCol--; } - while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) { + while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell) as CellData)) { bufferLine.loadCell(endCol + 1, this._workCell); const length = this._workCell.getChars().length; if (this._workCell.getWidth() === 2) { diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index baae735c94..90ce3f47f0 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -452,18 +452,16 @@ describe('InputHandler', () => { ); // fill display with a's - for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); + const a_repeat_cols = Array(bufferService.cols + 1).join('a'); + for (let i = 0; i < bufferService.rows; ++i) await inputHandler.parseP(a_repeat_cols); // params [0] - right and below erase bufferService.buffer.y = 5; bufferService.buffer.x = 40; inputHandler.eraseInDisplay(Params.fromArray([0])); assert.deepEqual(termContent(bufferService, false), [ - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), + a_repeat_cols, a_repeat_cols, a_repeat_cols, + a_repeat_cols, a_repeat_cols, Array(40 + 1).join('a') + Array(bufferService.cols - 40 + 1).join(' '), Array(bufferService.cols + 1).join(' ') ]); @@ -1885,17 +1883,19 @@ describe('InputHandler', () => { assert.equal(cell.isUnderlineColorDefault(), false); // eAttrs in buffer pos 0 and 1 should be the same object - assert.equal( - (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[0], - (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[1] - ); + const line0 = bufferService.buffer!.lines.get(0)!; + line0.loadCell(0, cell); + const ext0 = cell.extended; + line0.loadCell(1, cell); + const ext1 = cell.extended; + assert.equal(ext0, ext1); // should not have written eAttr for pos 2 in the buffer - assert.equal((bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[2], undefined); + line0.loadCell(2, cell); + assert.isFalse(cell.hasExtendedAttrs() !== 0); // eAttrs in buffer pos 1 and pos 3 must be different objs - assert.notEqual( - (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[1], - (bufferService.buffer!.lines.get(0)! as any)._extendedAttrs[3] - ); + line0.loadCell(3, cell); + const ext3 = cell.extended; + assert.notEqual(ext1, ext3); }); }); describe('DECSTR', () => { diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b94d785544..b6a5541f32 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,13 +4,13 @@ * @license MIT */ -import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from 'common/Types'; +import { IInputHandler, IBufferLine, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, SpecialColorIndex } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { Disposable } from 'vs/base/common/lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { usingNewBufferLine, BufferLine, OldBufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types'; import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; @@ -124,6 +124,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _dirtyRowTracker: IDirtyRowTracker; protected _windowTitleStack: string[] = []; protected _iconNameStack: string[] = []; + public get precedingJoinState(): number { return this._parser.precedingJoinState; } + public set precedingJoinState(value: number) { this._parser.precedingJoinState = value; } private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); public getAttrData(): IAttributeData { return this._curAttrData; } @@ -175,7 +177,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, private readonly _coreMouseService: ICoreMouseService, - private readonly _unicodeService: IUnicodeService, + public readonly unicodeService: IUnicodeService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() ) { super(); @@ -428,6 +430,7 @@ export class InputHandler extends Disposable implements IInputHandler { let cursorStartY = this._activeBuffer.y; let start = 0; const wasPaused = this._parseStack.paused; + usingNewBufferLine() && this._activeBuffer.reflowRegion(this._activeBuffer.ybase, this._activeBuffer.lines.length, -1); if (wasPaused) { // assumption: _parseBuffer never mutates between async calls @@ -507,6 +510,68 @@ export class InputHandler extends Disposable implements IInputHandler { } public print(data: Uint32Array, start: number, end: number): void { + const curAttr = this._curAttrData; + const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + if (bufferRow instanceof NewBufferLine) { + this._printNew(data, start, end, bufferRow, curAttr); + } else { + this._printOld(data, start, end, bufferRow, curAttr); + } + } + + private _printNew(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { + const wraparoundMode = this._coreService.decPrivateModes.wraparound; + const cols = this._bufferService.cols; + this._dirtyRowTracker.markDirty(this._activeBuffer.y); + // if (charset) replace character; FIXME ok to do it in-place? + let col = (bufferRow as NewBufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, this._coreService); + while (col > cols) { + // autowrap - DECAWM + // automatically wraps to the beginning of the next line + if (wraparoundMode) { + const oldRow = bufferRow as NewBufferLine; + // this._activeBuffer.x = oldWidth; + const buffer = this._activeBuffer + if (buffer.y === this._activeBuffer.scrollBottom) { + this._bufferService.scroll(this._eraseAttrData(), true); + buffer.splitLine(buffer.y, col); + } else { + buffer.y++; + if (this._activeBuffer.y >= this._bufferService.rows) { + buffer.y = this._bufferService.rows - 1; + // FIXME overwrite last line - not impemented + col = cols; + } else { + buffer.splitLine(buffer.y, col); + } + } + bufferRow = this._activeBuffer.lines.get(buffer.ybase + buffer.y)!; + // usually same as cols, but may be less in case of wide characters. + const prevCols = (bufferRow as NewBufferLine).logicalStartColumn() - oldRow.logicalStartColumn(); + col = col - prevCols; + // row changed, get it again + /* + if (oldWidth > 0 && bufferRow instanceof BufferLine) { + // Combining character widens 1 column to 2. + // Move old character to next line. + bufferRow.copyCellsFrom(oldRow as BufferLine, + oldCol, 0, oldWidth, false); + } + // clear left over cells to the right + while (oldCol < cols) { + oldRow.setCellFromCodepoint(oldCol++, 0, 1, curAttr); + } + */ + // col = ...; + } else { + // FIXME delete excess + break; + } + } + this._activeBuffer.x = col; + } + + private _printOld(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { let code: number; let chWidth: number; const charset = this._charsetService.charset; @@ -514,8 +579,6 @@ export class InputHandler extends Disposable implements IInputHandler { const cols = this._bufferService.cols; const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._coreService.modes.insertMode; - const curAttr = this._curAttrData; - let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; this._dirtyRowTracker.markDirty(this._activeBuffer.y); @@ -538,7 +601,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } - const currentInfo = this._unicodeService.charProperties(code, precedingJoinState); + const currentInfo = this.unicodeService.charProperties(code, precedingJoinState); chWidth = UnicodeService.extractWidth(currentInfo); const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0; @@ -571,7 +634,7 @@ export class InputHandler extends Disposable implements IInputHandler { } // The line already exists (eg. the initial viewport), mark it as a // wrapped line - this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true; + this._activeBuffer.setWrapped(this._activeBuffer.ybase + this._activeBuffer.y, true); } // row changed, get it again bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; @@ -604,7 +667,7 @@ export class InputHandler extends Disposable implements IInputHandler { // if empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - bufferRow.addCodepointToCell(this._activeBuffer.x - offset, + (bufferRow as OldBufferLine).addCodepointToCell(this._activeBuffer.x - offset, code, chWidth); for (let delta = chWidth - oldWidth; --delta >= 0; ) { bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr); @@ -725,7 +788,7 @@ export class InputHandler extends Disposable implements IInputHandler { // reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics // can mess with this so windowsMode should be disabled, which is recommended on Windows build // 21376 and above. - this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false; + this._activeBuffer.setWrapped(this._activeBuffer.ybase + this._activeBuffer.y, false); } // If the end of the line is hit, prevent this action from wrapping around to the next line. if (this._activeBuffer.x >= this._bufferService.cols) { @@ -789,7 +852,7 @@ export class InputHandler extends Disposable implements IInputHandler { && this._activeBuffer.y > this._activeBuffer.scrollTop && this._activeBuffer.y <= this._activeBuffer.scrollBottom && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) { - this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false; + this._activeBuffer.setWrapped(this._activeBuffer.ybase + this._activeBuffer.y, false); this._activeBuffer.y--; this._activeBuffer.x = this._bufferService.cols - 1; // find last taken cell - last cell can have 3 different states: @@ -1142,15 +1205,19 @@ export class InputHandler extends Disposable implements IInputHandler { * @param respectProtect Whether to respect the protection attribute (DECSCA). */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; - line.replaceCells( - start, - end, - this._activeBuffer.getNullCell(this._eraseAttrData()), - respectProtect - ); - if (clearWrap) { - line.isWrapped = false; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; + const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); + if (clearWrap && end === Infinity) { + this._activeBuffer.setWrapped(row + 1, false); + } + if (! respectProtect && line instanceof NewBufferLine) { + line.eraseCells(start, end, fill); + } else { + line.replaceCells(start, end, fill, respectProtect); + } + if (clearWrap && start === 0) { + this._activeBuffer.setWrapped(row, false); } } @@ -1159,12 +1226,23 @@ export class InputHandler extends Disposable implements IInputHandler { * the terminal and the isWrapped property is set to false. * @param y row index */ - private _resetBufferLine(y: number, respectProtect: boolean = false): void { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); + private _resetBufferLine(row: number, respectProtect: boolean = false): void { + const buffer = this._activeBuffer; + const line = buffer.lines.get(row); if (line) { - line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect); - this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y); - line.isWrapped = false; + const eraseAttrs = this._eraseAttrData(); + const wasNewBufferLine = line instanceof NewBufferLine; + if (wasNewBufferLine && ! respectProtect) { + line.eraseCells(0, this._bufferService.cols, eraseAttrs); + } else { + line.fill(this._activeBuffer.getNullCell(eraseAttrs), respectProtect); + } + buffer.clearMarkers(row); + buffer.setWrapped(row, false); + if (wasNewBufferLine !== usingNewBufferLine()) { + const fill = this._activeBuffer.getNullCell(eraseAttrs); + buffer.lines.set(row, BufferLine.make(line.length, fill)); + } } } @@ -1194,16 +1272,22 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean { this._restrictCursor(this._bufferService.cols); - let j; + // When erasing wrapped lines, we do less copying if we go bottom up. + let j; let x; let y; + const buffer = this._activeBuffer; switch (params.params[0]) { case 0: - j = this._activeBuffer.y; + y = buffer.y; + x = buffer.x; + j = this._bufferService.rows; this._dirtyRowTracker.markDirty(j); - this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); - for (; j < this._bufferService.rows; j++) { - this._resetBufferLine(j, respectProtect); + this._dirtyRowTracker.markDirty(y); + while (--j > y || (j === y && x === 0)) { + this._resetBufferLine(buffer.ybase + j, respectProtect); + } + if (x > 0) { + this._eraseInBufferLine(y, x, Infinity, false, respectProtect); } - this._dirtyRowTracker.markDirty(j); break; case 1: j = this._activeBuffer.y; @@ -1212,10 +1296,10 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect); if (this._activeBuffer.x + 1 >= this._bufferService.cols) { // Deleted entire previous line. This next line can no longer be wrapped. - this._activeBuffer.lines.get(j + 1)!.isWrapped = false; + this._activeBuffer.setWrapped(j + 1, false); } while (j--) { - this._resetBufferLine(j, respectProtect); + this._resetBufferLine(buffer.ybase + j, respectProtect); } this._dirtyRowTracker.markDirty(0); break; @@ -1223,7 +1307,7 @@ export class InputHandler extends Disposable implements IInputHandler { j = this._bufferService.rows; this._dirtyRowTracker.markDirty(j - 1); while (j--) { - this._resetBufferLine(j, respectProtect); + this._resetBufferLine(buffer.ybase + j, respectProtect); } this._dirtyRowTracker.markDirty(0); break; @@ -1268,13 +1352,13 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(this._bufferService.cols); switch (params.params[0]) { case 0: - this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); + this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, Infinity, this._activeBuffer.x === 0, respectProtect); break; case 1: this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false, respectProtect); break; case 2: - this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true, respectProtect); + this._eraseInBufferLine(this._activeBuffer.y, 0, Infinity, true, respectProtect); break; } this._dirtyRowTracker.markDirty(this._activeBuffer.y); @@ -1459,9 +1543,10 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1492,9 +1577,10 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1515,9 +1601,10 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1538,9 +1625,10 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1877,7 +1965,6 @@ export class InputHandler extends Disposable implements IInputHandler { */ if (this._optionsService.rawOptions.windowOptions.setWinLines) { this._bufferService.resize(132, this._bufferService.rows); - this._onRequestReset.fire(); } break; case 6: @@ -2115,7 +2202,6 @@ export class InputHandler extends Disposable implements IInputHandler { */ if (this._optionsService.rawOptions.windowOptions.setWinLines) { this._bufferService.resize(80, this._bufferService.rows); - this._onRequestReset.fire(); } break; case 6: @@ -2632,8 +2718,9 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 6: // cursor position - const y = this._activeBuffer.y + 1; - const x = this._activeBuffer.x + 1; + const buffer = this._activeBuffer; + const y = buffer.y + 1; + const x = Math.min(buffer.x + 1, this._bufferService.cols); this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); break; } @@ -2811,6 +2898,11 @@ export class InputHandler extends Disposable implements IInputHandler { } const second = (params.length > 1) ? params.params[1] : 0; switch (params.params[0]) { + case 8: // resize + const newRows = params.params[1] || this._bufferService.rows; + const newCols = params.params[2] || this._bufferService.cols; + this._bufferService.resize(newCols, newRows); + break; case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t if (second !== 2) { this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS); @@ -3346,7 +3438,7 @@ export class InputHandler extends Disposable implements IInputHandler { const line = this._activeBuffer.lines.get(row); if (line) { line.fill(cell); - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } } this._dirtyRowTracker.markAllDirty(); diff --git a/src/common/Types.ts b/src/common/Types.ts index 289aa1f692..4bc9ecf730 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -4,7 +4,7 @@ */ import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; -import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; // eslint-disable-line no-unused-vars +import { Attributes, StyleFlags, UnderlineStyle } from 'common/buffer/Constants'; // eslint-disable-line no-unused-vars import { IBufferSet } from 'common/buffer/Types'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; @@ -101,6 +101,7 @@ export interface ICharset { [key: string]: string | undefined; } +// Deprecated export type CharData = [number, string, number, number]; export interface IColor { @@ -136,12 +137,12 @@ export interface IOscLinkData { export interface IAttributeData { /** * "fg" is a 32-bit unsigned integer that stores the foreground color of the cell in the 24 least - * significant bits and additional flags in the remaining 8 bits. + * significant bits and additional flags in the remaining 8 bits. @deprecated */ fg: number; /** * "bg" is a 32-bit unsigned integer that stores the background color of the cell in the 24 least - * significant bits and additional flags in the remaining 8 bits. + * significant bits and additional flags in the remaining 8 bits. @deprecated */ bg: number; /** @@ -164,6 +165,10 @@ export interface IAttributeData { isProtected(): number; isOverline(): number; + getFg(): number; // 26 bits including CM_MASK + getBg(): number; // 26 bits including CM_MASK + getStyleFlags(): StyleFlags; + /** * The color mode of the foreground color which determines how to decode {@link getFgColor}, * possible values include {@link Attributes.CM_DEFAULT}, {@link Attributes.CM_P16}, @@ -210,7 +215,7 @@ export interface IAttributeData { /** Cell data */ export interface ICellData extends IAttributeData { content: number; - combinedData: string; + combinedData: string; // FIXME only if using OldBufferLine isCombined(): number; getWidth(): number; getChars(): string; @@ -224,20 +229,24 @@ export interface ICellData extends IAttributeData { */ export interface IBufferLine { length: number; - isWrapped: boolean; + /** If the previous line wrapped (overflows) into the current line. */ + readonly isWrapped: boolean; + _isWrapped: boolean; // should only be used OldBufferLine get(index: number): CharData; set(index: number, value: CharData): void; loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; - addCodepointToCell(index: number, codePoint: number, width: number): void; + addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED insertCells(pos: number, n: number, ch: ICellData): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; resize(cols: number, fill: ICellData): boolean; cleanupMemory(): number; fill(fillCellData: ICellData, respectProtect?: boolean): void; + // @deprecated - only if !usingNewBufferLine() copyFrom(line: IBufferLine): void; + // @deprecated - only if !usingNewBufferLine() clone(): IBufferLine; getTrimmedLength(): number; getNoBgTrimmedLength(): number; @@ -447,6 +456,8 @@ export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestor */ export interface IInputHandler { onTitleChange: Event; + readonly unicodeService: IUnicodeService; + precedingJoinState: number; parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; diff --git a/src/common/WindowsMode.ts b/src/common/WindowsMode.ts index 7cff094b2c..7234180e28 100644 --- a/src/common/WindowsMode.ts +++ b/src/common/WindowsMode.ts @@ -20,8 +20,9 @@ export function updateWindowsModeWrappedState(bufferService: IBufferService): vo const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1); const lastChar = line?.get(bufferService.cols - 1); - const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y); + const nextRow = bufferService.buffer.ybase + bufferService.buffer.y; + const nextLine = bufferService.buffer.lines.get(nextRow); if (nextLine && lastChar) { - nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); + bufferService.buffer.setWrapped(nextRow, lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); } } diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 6221fb81d2..d40e19a29d 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -4,7 +4,7 @@ */ import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types'; -import { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from 'common/buffer/Constants'; +import { Attributes, FgFlags, BgFlags, UnderlineStyle, StyleFlags, ExtFlags } from 'common/buffer/Constants'; export class AttributeData implements IAttributeData { public static toColorRGB(value: number): IColorRGB { @@ -48,10 +48,19 @@ export class AttributeData implements IAttributeData { public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; } public isProtected(): number { return this.bg & BgFlags.PROTECTED; } public isOverline(): number { return this.bg & BgFlags.OVERLINE; } + public getStyleFlags(): StyleFlags { return ((this.fg & 0xFC000000) >>> 24) | ((this.bg & 0xFC000000) >> 16); } + public setStyleFlags(flags: StyleFlags): void { + this.fg = (this.fg & 0x03ffffff) | ((flags << 24) & 0xFC000000); + this.bg = (this.bg & 0x03ffffff) | ((flags << 16) & 0xFC000000); + } // color modes public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; } public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; } + public getFg(): number { return this.fg & Attributes.CM_COLOR_MASK; } + public getBg(): number { return this.bg & Attributes.CM_COLOR_MASK; } + public setFg(fg: number): void { this.fg = (fg & 0x3ffffff) | (this.fg & 0xfc000000); } + public setBg(bg: number): void { this.bg = (bg & 0x3ffffff) | (this.bg & 0xfc000000); } public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; } public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; } diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 39cffb4900..7d1455a657 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -68,40 +68,40 @@ describe('Buffer', () => { describe('wrapped', () => { it('should return a range for the first row', () => { buffer.fillViewportRows(); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 }); }); it('should return a range for a middle row wrapping upwards', () => { buffer.fillViewportRows(); - buffer.lines.get(12)!.isWrapped = true; + buffer.setWrapped(12, true); assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 }); }); it('should return a range for a middle row wrapping downwards', () => { buffer.fillViewportRows(); - buffer.lines.get(13)!.isWrapped = true; + buffer.setWrapped(13, true); assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 }); }); it('should return a range for a middle row wrapping both ways', () => { buffer.fillViewportRows(); - buffer.lines.get(11)!.isWrapped = true; - buffer.lines.get(12)!.isWrapped = true; - buffer.lines.get(13)!.isWrapped = true; - buffer.lines.get(14)!.isWrapped = true; + buffer.setWrapped(11, true); + buffer.setWrapped(12, true); + buffer.setWrapped(13, true); + buffer.setWrapped(14, true); assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 }); }); it('should return a range for the last row', () => { buffer.fillViewportRows(); - buffer.lines.get(23)!.isWrapped = true; + buffer.setWrapped(23, true); assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 }); }); it('should return a range for a row that wraps upward to first row', () => { buffer.fillViewportRows(); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 }); }); it('should return a range for a row that wraps downward to last row', () => { buffer.fillViewportRows(); - buffer.lines.get(buffer.lines.length - 1)!.isWrapped = true; + buffer.setWrapped(buffer.lines.length - 1, true); assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); }); }); @@ -526,7 +526,7 @@ describe('Buffer', () => { buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); // Buffer: // "ab " (wrapped) // "cd" @@ -557,7 +557,7 @@ describe('Buffer', () => { buffer.lines.get(0)!.set(i, [0, '', 0, 0]); buffer.lines.get(1)!.set(i, [0, '', 0, 0]); } - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); // Buffer: // 汉语汉语汉语 (wrapped) // 汉语汉语汉语 @@ -584,7 +584,7 @@ describe('Buffer', () => { buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); // Buffer: // "ab " (wrapped) // "cd" @@ -618,7 +618,7 @@ describe('Buffer', () => { buffer.lines.get(0)!.set(i, [0, '', 0, 0]); buffer.lines.get(1)!.set(i, [0, '', 0, 0]); } - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); // Buffer: // 汉语汉语汉语 (wrapped) // 汉语汉语汉语 @@ -673,17 +673,17 @@ describe('Buffer', () => { buffer.lines.get(0)!.set(1, [0, 'b', 1, 'b'.charCodeAt(0)]); buffer.lines.get(1)!.set(0, [0, 'c', 1, 'c'.charCodeAt(0)]); buffer.lines.get(1)!.set(1, [0, 'd', 1, 'd'.charCodeAt(0)]); - buffer.lines.get(1)!.isWrapped = true; + buffer.setWrapped(1, true); buffer.lines.get(2)!.set(0, [0, 'e', 1, 'e'.charCodeAt(0)]); buffer.lines.get(2)!.set(1, [0, 'f', 1, 'f'.charCodeAt(0)]); buffer.lines.get(3)!.set(0, [0, 'g', 1, 'g'.charCodeAt(0)]); buffer.lines.get(3)!.set(1, [0, 'h', 1, 'h'.charCodeAt(0)]); - buffer.lines.get(3)!.isWrapped = true; + buffer.setWrapped(3, true); buffer.lines.get(4)!.set(0, [0, 'i', 1, 'i'.charCodeAt(0)]); buffer.lines.get(4)!.set(1, [0, 'j', 1, 'j'.charCodeAt(0)]); buffer.lines.get(5)!.set(0, [0, 'k', 1, 'k'.charCodeAt(0)]); buffer.lines.get(5)!.set(1, [0, 'l', 1, 'l'.charCodeAt(0)]); - buffer.lines.get(5)!.isWrapped = true; + buffer.setWrapped(5, true); }); describe('viewport not yet filled', () => { it('should move the cursor up and add empty lines', () => { @@ -1104,7 +1104,7 @@ describe('Buffer', () => { describe ('translateBufferLineToString', () => { it('should handle selecting a section of ascii text', () => { - const line = new BufferLine(4); + const line = BufferLine.make(4); line.setCell(0, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([ 0, 'b', 1, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([ 0, 'c', 1, 'c'.charCodeAt(0)])); @@ -1116,7 +1116,7 @@ describe('Buffer', () => { }); it('should handle a cut-off double width character by including it', () => { - const line = new BufferLine(3); + const line = BufferLine.make(3); line.setCell(0, CellData.fromCharData([ 0, '語', 2, 35486 ])); line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); @@ -1127,7 +1127,7 @@ describe('Buffer', () => { }); it('should handle a zero width character in the middle of the string by not including it', () => { - const line = new BufferLine(3); + const line = BufferLine.make(3); line.setCell(0, CellData.fromCharData([ 0, '語', 2, '語'.charCodeAt(0) ])); line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); line.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); @@ -1144,7 +1144,7 @@ describe('Buffer', () => { }); it('should handle single width emojis', () => { - const line = new BufferLine(2); + const line = BufferLine.make(2); line.setCell(0, CellData.fromCharData([ 0, '😁', 1, '😁'.charCodeAt(0) ])); line.setCell(1, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); buffer.lines.set(0, line); @@ -1157,7 +1157,7 @@ describe('Buffer', () => { }); it('should handle double width emojis', () => { - const line = new BufferLine(2); + const line = BufferLine.make(2); line.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); line.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); buffer.lines.set(0, line); @@ -1168,7 +1168,7 @@ describe('Buffer', () => { const str2 = buffer.translateBufferLineToString(0, true, 0, 2); assert.equal(str2, '😁'); - const line2 = new BufferLine(3); + const line2 = BufferLine.make(3); line2.setCell(0, CellData.fromCharData([ 0, '😁', 2, '😁'.charCodeAt(0) ])); line2.setCell(1, CellData.fromCharData([ 0, '', 0, 0])); line2.setCell(2, CellData.fromCharData([ 0, 'a', 1, 'a'.charCodeAt(0)])); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index d5e057318a..7f8ae69e97 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -7,7 +7,7 @@ import { CircularList, IInsertEvent } from 'common/CircularList'; import { IdleTaskQueue } from 'common/TaskQueue'; import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types'; import { ExtendedAttrs } from 'common/buffer/AttributeData'; -import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, usingNewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow'; import { CellData } from 'common/buffer/CellData'; import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, WHITESPACE_CELL_WIDTH } from 'common/buffer/Constants'; @@ -38,6 +38,11 @@ export class Buffer implements IBuffer { public savedX: number = 0; public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); public savedCharset: ICharset | undefined = DEFAULT_CHARSET; + /** Reflow may be needed for line indexes less than lastReflowNeeded. + * I.e. if i >= lastReflowNeeded then lines.get(i).reflowNeeded is false. + * Lines later in the buffer are more likly to be visible and hence + * have been updated. */ + public lastReflowNeeded: number = 0; public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); @@ -85,7 +90,7 @@ export class Buffer implements IBuffer { } public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { - return new BufferLine(this._bufferService.cols, this.getNullCell(attr), isWrapped); + return BufferLine.make(this._bufferService.cols, this.getNullCell(attr), isWrapped); } public get hasScrollback(): boolean { @@ -113,6 +118,56 @@ export class Buffer implements IBuffer { return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength; } + public splitLine(row: number, col: number): void { + const bufferService = this._bufferService; + const curRow = this.lines.get(this.ybase + row - 1) as NewBufferLine; + const nextRow = this.lines.get(this.ybase + row) as NewBufferLine; + curRow.moveToLineColumn(curRow.logicalStartColumn() + bufferService.cols); + // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); + let newRow; + if (nextRow.isWrapped) { + newRow = nextRow as WrappedBufferLine; + } else { + newRow = new WrappedBufferLine(curRow); + // append nextRow contents to end of curRow.logicalLine() + this.lines.set(this.ybase + row, newRow); + } + curRow.setStartFromCache(newRow); + } + + public setWrapped(absrow: number, value: boolean): void { + const line = this.lines.get(absrow); + if (! line || line.isWrapped === value) + {return;} + if (! usingNewBufferLine()) { + line!._isWrapped = value; + } else if (value) { + const prevRow = this.lines.get(absrow - 1) as NewBufferLine; + const curRow = line as LogicalBufferLine; + const newRow = curRow.setWrapped(prevRow); + this.lines.set(absrow, newRow); + } else { + const prevRow = this.lines.get(absrow - 1) as NewBufferLine; + const curRow = line as WrappedBufferLine; + const oldStartColumn = curRow.logicalStartColumn(); + prevRow.nextRowSameLine = undefined; + const oldLine = prevRow.logicalLine(); + const startIndex = oldLine._splitIfNeeded(oldStartColumn); + const cell = new CellData(); + curRow.loadCell(oldStartColumn, cell); + const newRow = new LogicalBufferLine(line.length, cell, curRow, startIndex); + newRow.nextRowSameLine = curRow.nextRowSameLine; + const oldStart = curRow.startIndex; + for (let nextRow = newRow.nextRowSameLine; nextRow; nextRow = nextRow.nextRowSameLine) { + nextRow.startColumn -= oldStartColumn; + nextRow.startIndex -= oldStart; + nextRow._logicalLine = newRow; + } + oldLine._dataLength = startIndex; + this.lines.set(absrow, newRow); + } + } + /** * Fills the buffer's viewport with blank lines. */ @@ -161,13 +216,22 @@ export class Buffer implements IBuffer { this.lines.maxLength = newMaxLength; } - // if (this._cols > newCols) { - // console.log('increase!'); - // } + if (this._cols !== newCols && usingNewBufferLine()) { + const nlines = this.lines.length; + for (let i = 0; i < nlines; i++) { + const line = this.lines.get(i); + line && (line.length = newCols); + if (line instanceof LogicalBufferLine + && (line.nextRowSameLine || line.logicalWidth > newCols)) { + line.reflowNeeded = true; + this.lastReflowNeeded = Math.max(i, this.lastReflowNeeded); + } + } + } // The following adjustments should only happen if the buffer has been // initialized/filled. - if (this.lines.length > 0) { + if (! usingNewBufferLine() && this.lines.length > 0) { // Deal with columns increasing (reducing needs to happen after reflow) if (this._cols < newCols) { for (let i = 0; i < this.lines.length; i++) { @@ -182,9 +246,9 @@ export class Buffer implements IBuffer { for (let y = this._rows; y < newRows; y++) { if (this.lines.length < newRows + this.ybase) { if (this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined) { - // Just add the new missing rows on Windows as conpty reprints the screen with it's + // Just add the new missing rows on Windows as conpty reprints the screen with its // view of the world. Once a line enters scrollback for conpty it remains there - this.lines.push(new BufferLine(newCols, nullCell)); + this.lines.push(BufferLine.make(newCols, nullCell)); } else { if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { // There is room above the buffer and there are no empty elements below the line, @@ -198,7 +262,7 @@ export class Buffer implements IBuffer { } else { // Add a blank line if there is no buffer left at the top to scroll to, or if there // are blank lines after the cursor - this.lines.push(new BufferLine(newCols, nullCell)); + this.lines.push(BufferLine.make(newCols, nullCell)); } } } @@ -233,7 +297,7 @@ export class Buffer implements IBuffer { } // Make sure that the cursor stays on screen - this.x = Math.min(this.x, newCols - 1); + this.x = Math.min(this.x, newCols); this.y = Math.min(this.y, newRows - 1); if (addToY) { this.y += addToY; @@ -245,32 +309,41 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._isReflowEnabled) { - this._reflow(newCols, newRows); - - // Trim the end of the line off if cols shrunk - if (this._cols > newCols) { - for (let i = 0; i < this.lines.length; i++) { - // +boolean for fast 0 or 1 conversion - dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell); + if (usingNewBufferLine()) { + const lazyReflow = true; + const reflowNow = this._isReflowEnabled && this._cols !== newCols && ! lazyReflow; + this._cols = newCols; + this._rows = newRows; + this.reflowRegion(reflowNow ? 0 : this.ydisp, this.lines.length, + reflowNow? -1 : newRows); + this._fixupPosition(); + } else { // !usingNewBufferLine() + if (this._isReflowEnabled) { + this._reflow(newCols, newRows); + + // Trim the end of the line off if cols shrunk + if (! usingNewBufferLine() && this._cols > newCols) { + for (let i = 0; i < this.lines.length; i++) { + // +boolean for fast 0 or 1 conversion + dirtyMemoryLines += +this.lines.get(i)!.resize(newCols, nullCell); + } } } - } - - this._cols = newCols; - this._rows = newRows; - - this._memoryCleanupQueue.clear(); - // schedule memory cleanup only, if more than 10% of the lines are affected - if (dirtyMemoryLines > 0.1 * this.lines.length) { - this._memoryCleanupPosition = 0; - this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup()); + this._cols = newCols; + this._rows = newRows; + + this._memoryCleanupQueue.clear(); + // schedule memory cleanup only, if more than 10% of the lines are affected + if (dirtyMemoryLines > 0.1 * this.lines.length) { + this._memoryCleanupPosition = 0; + this._memoryCleanupQueue.enqueue(() => this._batchedMemoryCleanup()); + } } } + // DEPRECATED - only if !usingNewBufferLine() private _memoryCleanupQueue = new IdleTaskQueue(); private _memoryCleanupPosition = 0; - private _batchedMemoryCleanup(): boolean { let normalRun = true; if (this._memoryCleanupPosition >= this.lines.length) { @@ -301,6 +374,168 @@ export class Buffer implements IBuffer { return this._hasScrollback && !this._optionsService.rawOptions.windowsMode; } + // Only if USE_NewBufferLine + public reflowRegion(startRow: number, endRow: number, maxRows: number): void { + if (startRow >= this.lastReflowNeeded) { + return; + } + if (endRow >= this.lastReflowNeeded) { + this.lastReflowNeeded = startRow; + } + const newCols = this._cols; + while (startRow > 0 && this.lines.get(startRow)?.isWrapped) { + startRow--; + if (maxRows >= 0) { maxRows++; } + } + // POSSIBLE OPTIMIZATION: Don't need to allocate newRows if no lines + // require more rows than before. So better to allocate newRows lazily. + const newRows: NewBufferLine[] = []; + const yDispOld = this.ydisp; + const yBaseOld = this.ybase; + const yAbsOld = yBaseOld + this.y; + let yAbs = yAbsOld; + const ySavedOld = this.savedY; + let ySaved = ySavedOld; + let deltaSoFar = 0; + for (let row = startRow; row < endRow;) { + if (maxRows >= 0 && newRows.length > maxRows) { + endRow = row; + break; + } + const line = this.lines.get(row) as NewBufferLine; + newRows.push(line); + if (line instanceof LogicalBufferLine && line.reflowNeeded) { + let curRow: NewBufferLine = line; + + let logicalX, logicalSavedX; + let oldWrapCount = 0; // number of following wrapped lines + let nextRow = curRow; + for (; ; oldWrapCount++) { + if (yAbsOld === row + oldWrapCount) { + logicalX = nextRow.logicalStartColumn() + this.x; + } + if (ySavedOld === row + oldWrapCount) { + logicalSavedX = nextRow.logicalStartColumn() + this.savedX; + } + if (! nextRow.nextRowSameLine || row + oldWrapCount + 1 >= endRow) { + break; + } + nextRow = nextRow.nextRowSameLine; + } + const lineRow = row; + row++; + const newWrapStart = newRows.length; + line.reflowNeeded = false; + let startCol = 0; + const dataLength = line.dataLength(); + + // Loop over new WrappedBufferLines for current LogicalBufferLine, + // based on newCols width. Re-use old WrappedBufferLine if available. + for (;;) { + line.moveToLineColumn(startCol + newCols); + const idata = line._cachedDataIndex(); + if (idata >= dataLength) { + curRow.nextRowSameLine = undefined; + break; + } + startCol = line._cachedColumn(); + const newRow1 = row < endRow && this.lines.get(row); + const newRow = newRow1 instanceof WrappedBufferLine + ? (row++, newRow1) + : new WrappedBufferLine(curRow); + line.setStartFromCache(newRow); + newRows.push(newRow); + curRow = newRow; + } + // Skip old WrappedBufferLines that we no longer need. + while (row < endRow + && this.lines.get(row) instanceof WrappedBufferLine) { + row++; + } + const newWrapCount = newRows.length - newWrapStart; + if (yBaseOld >= lineRow && yBaseOld <= lineRow + oldWrapCount) { + this.ybase = lineRow + deltaSoFar + + Math.min(yBaseOld - lineRow, newWrapCount); + } + if (yDispOld >= lineRow && yDispOld <= lineRow + oldWrapCount) { + this.ydisp = lineRow + deltaSoFar + + Math.min(yDispOld - lineRow, newWrapCount); + } + if (logicalX !== undefined) { // update cursor x and y + let i = newWrapStart; + while (i < newRows.length && newRows[i].logicalStartColumn() <= logicalX) { i++; } + yAbs = startRow + i - 1 + deltaSoFar; + this.x = logicalX - newRows[i-1].logicalStartColumn(); + } + if (logicalSavedX !== undefined) { // update cursor x and y + let i = newWrapStart; + while (i < newRows.length && newRows[i].logicalStartColumn() <= logicalSavedX) { i++; } + ySaved = startRow + i - 1 + deltaSoFar; + this.savedX = logicalSavedX - newRows[i-1].logicalStartColumn(); + } + deltaSoFar += newWrapCount - oldWrapCount; + } else { + if (row + deltaSoFar === yBaseOld) { this.ybase = yBaseOld + deltaSoFar; } + if (row + deltaSoFar === yDispOld) { this.ydisp = yDispOld + deltaSoFar; } + if (row === yAbsOld) { + yAbs += deltaSoFar; + } + if (row === ySavedOld) { + ySaved += deltaSoFar; + } + row++; + } + } + if (deltaSoFar !== 0) { + if (yAbsOld >= endRow) { yAbs += deltaSoFar; } + if (ySavedOld >= endRow) { ySaved += deltaSoFar; } + if (yBaseOld >= endRow) { this.ybase = yBaseOld + deltaSoFar; } + if (yDispOld >= endRow) { this.ydisp = yDispOld + deltaSoFar; } + } + this.y = yAbs - this.ybase; + this.savedY = ySaved; + // FIXME. This calls onDeleteEmitter and onInsertEmitter events, + // which we want handled at finer granularity. + const oldLinesCount = this.lines.length; + this.lines.splice(startRow, endRow - startRow, ...newRows); + const trimmed = oldLinesCount + newRows.length - (endRow - startRow) + - this.lines.length; + if (trimmed > 0) { + this.ybase -= trimmed; + this.ydisp -= trimmed; + } + this._fixupPosition(); + } + + private _fixupPosition(): void { + const cols = this._cols; + const rows = this._rows; + + let ilast = this.lines.length - 1; + while (ilast >= rows && this.ybase + this.y = rows) { + const adjust = this.y - rows + 1; + this.ydisp += adjust; + this.ybase += adjust; + this.y -= adjust; + } + while (this.lines.length < rows) { + this.lines.push(new LogicalBufferLine(cols)); + } + if (this.lines.length - this.ybase < rows) { + const adjust = rows - this.lines.length + this.ybase; + this.ybase -= adjust; + this.y += adjust; + } + this.ydisp = Math.max(0, Math.min(this.ydisp, this.lines.length - rows)); + } + + // DEPRECATED - only if !usingNewBufferLine() private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; @@ -314,6 +549,7 @@ export class Buffer implements IBuffer { } } + // DEPRECATED - only if !usingNewBufferLine() private _reflowLarger(newCols: number, newRows: number): void { const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA)); if (toRemove.length > 0) { @@ -323,6 +559,7 @@ export class Buffer implements IBuffer { } } + // DEPRECATED - only if !usingNewBufferLine() private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void { const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Adjust viewport based on number of items removed @@ -334,7 +571,7 @@ export class Buffer implements IBuffer { } if (this.lines.length < newRows) { // Add an extra row at the bottom of the viewport - this.lines.push(new BufferLine(newCols, nullCell)); + this.lines.push(BufferLine.make(newCols, nullCell)); } } else { if (this.ydisp === this.ybase) { @@ -346,6 +583,7 @@ export class Buffer implements IBuffer { this.savedY = Math.max(this.savedY - countRemoved, 0); } + // DEPRECATED - only if !usingNewBufferLine() private _reflowSmaller(newCols: number, newRows: number): void { const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); // Gather all BufferLines that need to be inserted into the Buffer here so that they can be @@ -655,4 +893,40 @@ export class Buffer implements IBuffer { this.markers.splice(this.markers.indexOf(marker), 1); } } + + // for DEBUGGING + public noteError(msg: string): void { + console.log('ERROR: ' + msg); + } + + // for DEBUGGING + public checkLines(report = this.noteError): void { + const nlines = this.lines.length; + let prevRow: IBufferLine | undefined; + let logicalLine; + for (let i = 0; i < nlines; i++) { + const curRow = this.lines.get(i); + if (curRow instanceof LogicalBufferLine) { + if (curRow.isWrapped) { report('wrapped should not be set'); } + logicalLine = curRow; + } else if (curRow instanceof WrappedBufferLine) { + if (curRow.logicalLine() !== logicalLine) { + report('wrapped line points to wrong logical line') + } + if (! curRow.isWrapped) { report('wrapped should be set'); } + if (prevRow instanceof NewBufferLine) { + if (prevRow.nextRowSameLine !== curRow) { + report('bad previous nextRowSameLine'); + } + if (prevRow.logicalStartColumn() > curRow.logicalStartColumn()) + { report('bad logicalStartColumn'); } + } else { + report('bad previous line before Wrapped'); + } + } else if (! curRow) { + report('undefined line in lines list'); + } + prevRow = curRow; + } + } } diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 9d08c2bef6..549bc540ed 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -5,23 +5,16 @@ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants'; import { BufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; -import { CharData, IBufferLine } from '../Types'; +import { CharData, IBufferLine, IExtendedAttrs } from '../Types'; import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; - -class TestBufferLine extends BufferLine { - public get combined(): {[index: number]: string} { - return this._combined; - } - - public toArray(): CharData[] { - const result = []; - for (let i = 0; i < this.length; ++i) { - result.push(this.loadCell(i, new CellData()).getAsCharData()); - } - return result; +function lineToArray(line: IBufferLine): CharData[] { + const result = []; + for (let i = 0; i < line.length; ++i) { + result.push(line.loadCell(i, new CellData()).getAsCharData()); } + return result; } describe('AttributeData', () => { @@ -162,43 +155,43 @@ describe('CellData', () => { describe('BufferLine', function(): void { it('ctor', function(): void { - let line: IBufferLine = new TestBufferLine(0); + let line: IBufferLine = BufferLine.make(0); assert.equal(line.length, 0); assert.equal(line.isWrapped, false); - line = new TestBufferLine(10); + line = BufferLine.make(10); assert.equal(line.length, 10); assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); assert.equal(line.isWrapped, false); - line = new TestBufferLine(10, undefined, true); + line = BufferLine.make(10, undefined, true); assert.equal(line.length, 10); assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); assert.equal(line.isWrapped, true); - line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); + line = BufferLine.make(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); assert.equal(line.length, 10); assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]); assert.equal(line.isWrapped, true); }); it('insertCells', function(): void { - const line = new TestBufferLine(3); + const line = BufferLine.make(3); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), [ + assert.deepEqual(lineToArray(line), [ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)] ]); }); it('deleteCells', function(): void { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), [ + assert.deepEqual(lineToArray(line), [ [1, 'a', 0, 'a'.charCodeAt(0)], [4, 'd', 0, 'd'.charCodeAt(0)], [5, 'e', 0, 'e'.charCodeAt(0)], @@ -207,14 +200,14 @@ describe('BufferLine', function(): void { ]); }); it('replaceCells', function(): void { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), [ + assert.deepEqual(lineToArray(line), [ [1, 'a', 0, 'a'.charCodeAt(0)], [2, 'b', 0, 'b'.charCodeAt(0)], [6, 'f', 0, 'f'.charCodeAt(0)], @@ -223,14 +216,14 @@ describe('BufferLine', function(): void { ]); }); it('fill', function(): void { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), [ + assert.deepEqual(lineToArray(line), [ [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], [123, 'z', 0, 'z'.charCodeAt(0)], @@ -239,27 +232,27 @@ describe('BufferLine', function(): void { ]); }); it('clone', function(): void { - const line = new TestBufferLine(5, undefined, true); + const line = BufferLine.make(5, undefined, true); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); const line2 = line.clone(); - assert.deepEqual(TestBufferLine.prototype.toArray.apply(line2), line.toArray()); + assert.deepEqual(lineToArray(line2), lineToArray(line)); assert.equal(line2.length, line.length); assert.equal(line2.isWrapped, line.isWrapped); }); it('copyFrom', function(): void { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)])); - const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); + const line2 = BufferLine.make(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); line2.copyFrom(line); - assert.deepEqual(line2.toArray(), line.toArray()); + assert.deepEqual(lineToArray(line2), lineToArray(line)); assert.equal(line2.length, line.length); assert.equal(line2.isWrapped, line.isWrapped); }); @@ -267,73 +260,73 @@ describe('BufferLine', function(): void { // CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print // --> set code to the last charCodeAt value of the string // Note: needs to be fixed once the string pointer is in place - const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), [[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); - const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); + const line = BufferLine.make(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), [[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]); + const line2 = BufferLine.make(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true); line2.copyFrom(line); - assert.deepEqual(line2.toArray(), line.toArray()); + assert.deepEqual(lineToArray(line2), lineToArray(line)); const line3 = line.clone(); - assert.deepEqual(TestBufferLine.prototype.toArray.apply(line3), line.toArray()); + assert.deepEqual(lineToArray(line3), lineToArray(line)); }); describe('resize', function(): void { it('enlarge(false)', function(): void { - const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + const line = BufferLine.make(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { - const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + const line = BufferLine.make(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + const line = BufferLine.make(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + const line = BufferLine.make(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); - assert.deepEqual(line.toArray(), (Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), (Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); }); it('should remove combining data on replaced cells after shrinking then enlarging', () => { - const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); + const line = BufferLine.make(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]); assert.equal(line.translateToString(), 'aa😁aaaaaa😁'); - assert.equal(Object.keys(line.combined).length, 2); + // assert.equal(Object.keys(line.combined).length, 2); line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aa😁aa'); line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aa😁aaaaaaa'); - assert.equal(Object.keys(line.combined).length, 1); + // assert.equal(Object.keys(line.combined).length, 1); }); }); describe('getTrimLength', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); assert.equal(line.getTrimmedLength(), 0); }); it('ASCII', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.getTrimmedLength(), 3); }); it('surrogate', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); assert.equal(line.getTrimmedLength(), 3); }); it('combining', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); assert.equal(line.getTrimmedLength(), 3); }); it('fullwidth', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([0, '', 0, 0])); @@ -342,7 +335,7 @@ describe('BufferLine', function(): void { }); describe('translateToString with and w\'o trimming', function(): void { it('empty line', function(): void { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const columns: number[] = []; assert.equal(line.translateToString(false, undefined, undefined, columns), ' '); assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -351,7 +344,7 @@ describe('BufferLine', function(): void { }); it('ASCII', function(): void { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); @@ -372,7 +365,7 @@ describe('BufferLine', function(): void { }); it('surrogate', function(): void { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)])); @@ -392,7 +385,7 @@ describe('BufferLine', function(): void { }); it('combining', function(): void { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); @@ -412,7 +405,7 @@ describe('BufferLine', function(): void { }); it('fullwidth', function(): void { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)])); line.setCell(3, CellData.fromCharData([0, '', 0, 0])); @@ -441,7 +434,7 @@ describe('BufferLine', function(): void { }); it('space at end', function(): void { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); @@ -457,7 +450,7 @@ describe('BufferLine', function(): void { // sanity check - broken line with invalid out of bound null width cells // this can atm happen with deleting/inserting chars in inputhandler by "breaking" // fullwidth pairs --> needs to be fixed after settling BufferLine impl - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); assert.equal(line.translateToString(false, undefined, undefined, columns), ' '); assert.deepEqual(columns, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); assert.equal(line.translateToString(true, undefined, undefined, columns), ''); @@ -465,7 +458,7 @@ describe('BufferLine', function(): void { }); it('should work with endCol=0', () => { const columns: number[] = []; - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(true, 0, 0, columns), ''); assert.deepEqual(columns, [0]); @@ -473,7 +466,7 @@ describe('BufferLine', function(): void { }); describe('addCharToCell', () => { it('should set width to 1 for empty cell', () => { - const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0); const cell = line.loadCell(0, new CellData()); // chars contains single combining char @@ -483,7 +476,7 @@ describe('BufferLine', function(): void { assert.equal(cell.isCombined(), 0); }); it('should add char to combining string in cell', () => { - const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); line.setCell(0, cell); @@ -496,7 +489,7 @@ describe('BufferLine', function(): void { assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); }); it('should create combining string on taken cell', () => { - const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); + const line = BufferLine.make(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false); const cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); line.setCell(0, cell); @@ -517,7 +510,7 @@ describe('BufferLine', function(): void { } } it('insert - wide char at pos', () => { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), '¥¥¥¥ a'); @@ -527,7 +520,7 @@ describe('BufferLine', function(): void { assert.equal(line.translateToString(), ' a ¥¥¥a'); }); it('insert - wide char at end', () => { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aaa¥¥¥ '); @@ -537,7 +530,7 @@ describe('BufferLine', function(): void { assert.equal(line.translateToString(), 'aaa aa ¥ '); }); it('delete', () => { - const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + const line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' ¥¥¥¥a'); @@ -547,61 +540,66 @@ describe('BufferLine', function(): void { assert.equal(line.translateToString(), ' ¥¥aaaaa'); }); it('replace - start at 0', () => { - let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + let line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'a ¥¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aa¥¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aaa ¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aaaaaaaa¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aaaaaaaaa '); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), 'aaaaaaaaaa'); }); it('replace - start at 1', () => { - let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + let line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' a¥¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' aa ¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' aaa¥¥¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' aaaaaaa¥'); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' aaaaaaaa '); - line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); + line = BufferLine.make(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false); populate(line); line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)])); assert.equal(line.translateToString(), ' aaaaaaaaa'); }); }); describe('extended attributes', () => { + function extendedAttributes(line: BufferLine, index: number): IExtendedAttrs | undefined { + const cell = new CellData(); + line.loadCell(index, cell); + return cell.hasExtendedAttrs() !== 0 ? cell.extended : undefined; + } it('setCells', function(): void { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); // no eAttrs line.setCell(0, cell); @@ -624,16 +622,16 @@ describe('BufferLine', function(): void { cell.bg &= ~BgFlags.HAS_EXTENDED; line.setCell(4, cell); - assert.deepEqual(line.toArray(), [ + assert.deepEqual(lineToArray(line), [ [1, 'a', 0, 'a'.charCodeAt(0)], [1, 'a', 0, 'a'.charCodeAt(0)], [1, 'A', 0, 'A'.charCodeAt(0)], [1, 'A', 0, 'A'.charCodeAt(0)], [1, 'A', 0, 'A'.charCodeAt(0)] ]); - assert.equal((line as any)._extendedAttrs[0], undefined); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 0), undefined); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY); assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED); assert.equal((line as any)._extendedAttrs[4], undefined); // should be ref to the same object @@ -642,7 +640,7 @@ describe('BufferLine', function(): void { assert.notEqual((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[3]); }); it('loadCell', () => { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); // no eAttrs line.setCell(0, cell); @@ -665,55 +663,49 @@ describe('BufferLine', function(): void { cell.bg &= ~BgFlags.HAS_EXTENDED; line.setCell(4, cell); - const cell0 = new CellData(); - line.loadCell(0, cell0); - const cell1 = new CellData(); - line.loadCell(1, cell1); - const cell2 = new CellData(); - line.loadCell(2, cell2); - const cell3 = new CellData(); - line.loadCell(3, cell3); - const cell4 = new CellData(); - line.loadCell(4, cell4); - - assert.equal(cell0.extended.underlineStyle, UnderlineStyle.NONE); - assert.equal(cell1.extended.underlineStyle, UnderlineStyle.CURLY); - assert.equal(cell2.extended.underlineStyle, UnderlineStyle.CURLY); - assert.equal(cell3.extended.underlineStyle, UnderlineStyle.DOTTED); - assert.equal(cell4.extended.underlineStyle, UnderlineStyle.NONE); - assert.equal(cell1.extended, cell2.extended); - assert.notEqual(cell2.extended, cell3.extended); + const ext0 = extendedAttributes(line, 0); + const ext1 = extendedAttributes(line, 1); + const ext2 = extendedAttributes(line, 2); + const ext3 = extendedAttributes(line, 3); + const ext4 = extendedAttributes(line, 4); + assert.equal(ext0?.underlineStyle, undefined); // UnderlineStyle.NONE + assert.equal(ext1?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(ext2?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(ext3?.underlineStyle, UnderlineStyle.DOTTED); + assert.equal(ext4?.underlineStyle, undefined); // UnderlineStyle.NONE + assert.equal(ext1, ext2); + assert.notEqual(ext2, ext3); }); it('fill', () => { - const line = new TestBufferLine(3); + const line = BufferLine.make(3); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); cell.extended.underlineStyle = UnderlineStyle.CURLY; cell.bg |= BgFlags.HAS_EXTENDED; line.fill(cell); - assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY); }); it('insertCells', () => { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); cell.extended.underlineStyle = UnderlineStyle.CURLY; cell.bg |= BgFlags.HAS_EXTENDED; line.insertCells(1, 3, cell); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[4], undefined); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 4), undefined); cell.extended = cell.extended.clone(); cell.extended.underlineStyle = UnderlineStyle.DOTTED; line.insertCells(2, 2, cell); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOTTED); - assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOTTED); - assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOTTED); + assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED); + assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY); }); it('deleteCells', () => { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); fillCell.extended.underlineStyle = UnderlineStyle.CURLY; fillCell.bg |= BgFlags.HAS_EXTENDED; @@ -721,14 +713,14 @@ describe('BufferLine', function(): void { fillCell.extended = fillCell.extended.clone(); fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE; line.deleteCells(1, 3, fillCell); - assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE); - assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.DOUBLE); - assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.DOUBLE); + assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE); + assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOUBLE); + assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.DOUBLE); }); it('replaceCells', () => { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const fillCell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); fillCell.extended.underlineStyle = UnderlineStyle.CURLY; fillCell.bg |= BgFlags.HAS_EXTENDED; @@ -736,14 +728,14 @@ describe('BufferLine', function(): void { fillCell.extended = fillCell.extended.clone(); fillCell.extended.underlineStyle = UnderlineStyle.DOUBLE; line.replaceCells(1, 3, fillCell); - assert.equal((line as any)._extendedAttrs[0].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[1].underlineStyle, UnderlineStyle.DOUBLE); - assert.equal((line as any)._extendedAttrs[2].underlineStyle, UnderlineStyle.DOUBLE); - assert.equal((line as any)._extendedAttrs[3].underlineStyle, UnderlineStyle.CURLY); - assert.equal((line as any)._extendedAttrs[4].underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 0)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 1)?.underlineStyle, UnderlineStyle.DOUBLE); + assert.equal(extendedAttributes(line, 2)?.underlineStyle, UnderlineStyle.DOUBLE); + assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY); + assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY); }); it('clone', () => { - const line = new TestBufferLine(5); + const line = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); // no eAttrs line.setCell(0, cell); @@ -774,7 +766,7 @@ describe('BufferLine', function(): void { assert.equal((nLine as any)._extendedAttrs[4], (line as any)._extendedAttrs[4]); }); it('copyFrom', () => { - const initial = new TestBufferLine(5); + const initial = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); // no eAttrs initial.setCell(0, cell); @@ -797,7 +789,7 @@ describe('BufferLine', function(): void { cell.bg &= ~BgFlags.HAS_EXTENDED; initial.setCell(4, cell); - const line = new TestBufferLine(5); + const line = BufferLine.make(5); line.fill(CellData.fromCharData([1, 'b', 0, 'b'.charCodeAt(0)])); line.copyFrom(initial); assert.equal((line as any)._extendedAttrs[0], (initial as any)._extendedAttrs[0]); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index ee3481a24e..a6d2a09445 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,11 +3,216 @@ * @license MIT */ -import { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; +import { CharData, IInputHandler, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; -import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; -import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, StyleFlags, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR , UnderlineStyle } from 'common/buffer/Constants'; +import { stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; +import { UnicodeService } from 'common/services/UnicodeService'; +import { ICoreService } from 'common/services/Services'; + +export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); + +/** Column count within current visible row. + * The left-most coulmn is column 0. + */ +type RowColumn = number; + +/** Column count within current logical line. + * If the display is 80 columns wide, then LineColumn of the left-most + * character of the first wrapped line would normally be 80. + * (It might be 79 if the character at column 79 is double-width.) + */ +type LineColumn = number; + +// Work variables to avoid garbage collection +let $startIndex = 0; + +/** Factor when to cleanup underlying array buffer after shrinking. */ +const CLEANUP_THRESHOLD = 2; + +export abstract class AbstractBufferLine implements IBufferLine { + /** Number of logical columns */ + public length: number = 0; + _isWrapped: boolean = false; + public get isWrapped(): boolean { return this._isWrapped; } + public abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; + public abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED + public abstract resize(cols: number, fillCellData: ICellData): boolean; + public abstract fill(fillCellData: ICellData, respectProtect?: boolean): void; + public abstract copyFrom(line: BufferLine): void; + public abstract clone(): IBufferLine; + public abstract translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; + public abstract getTrimmedLength(): number; + public abstract getNoBgTrimmedLength(): number; + public abstract cleanupMemory(): number; + + public abstract loadCell(index: number, cell: ICellData): ICellData; + + public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { + // full branching on respectProtect==true, hopefully getting fast JIT for standard case + if (respectProtect) { + if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) { + this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); + } + if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) { + this.setCellFromCodepoint(end, 0, 1, fillCellData); + } + while (start < end && start < this.length) { + if (!this.isProtected(start)) { + this.setCell(start, fillCellData); + } + start++; + } + return; + } + + // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char + if (start && this.getWidth(start - 1) === 2) { + this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); + } + // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char + if (end < this.length && this.getWidth(end - 1) === 2) { + this.setCellFromCodepoint(end, 0, 1, fillCellData); + } + + while (start < end && start < this.length) { + this.setCell(start++, fillCellData); + } + } + + /** + * Get cell data CharData. + * @deprecated + */ + get(index: number): CharData { + const cell = new CellData(); + this.loadCell(index, cell); + return cell.getAsCharData(); + } + + /** + * Set cell data from CharData. + * @deprecated + */ + public set(index: number, value: CharData): void { + this.setCell(index, CellData.fromCharData(value)); + } + + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this.loadCell(index, new CellData()).content >>> Content.WIDTH_SHIFT; + } + + /** Test whether content has width. */ + public hasWidth(index: number): number { + return this.loadCell(index, new CellData()).content & Content.WIDTH_MASK; + } + + /** Get FG cell component. */ + public getFg(index: number): number { + return this.loadCell(index, new CellData()).fg; + } + + /** Get BG cell component. @deprecated */ + public getBg(index: number): number { + return this.loadCell(index, new CellData()).bg; + } + + /** + * Test whether contains any chars. @deprecated + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + */ + public hasContent(index: number): number { + return this.loadCell(index, new CellData()).content & Content.HAS_CONTENT_MASK; + } + + abstract setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; + + public abstract setCell(index: number, cell: ICellData): void; + + /** + * Get codepoint of the cell. @deprecated + * To be in line with `code` in CharData this either returns + * a single UTF32 codepoint or the last codepoint of a combined string. + */ + public getCodePoint(index: number): number { + return this.loadCell(index, new CellData()).getCode(); + } + + /** Test whether the cell contains a combined string. */ + public isCombined(index: number): number { + return this.loadCell(index, new CellData()).isCombined(); + } + + abstract deleteCells(pos: number, n: number, fillCellData: ICellData): void; + + /** Returns the string content of the cell. @deprecated */ + public getString(index: number): string { + const cell = new CellData(); + this.loadCell(index, cell); + return cell.getChars(); + } + + /** Get state of protected flag. @deprecated */ + public isProtected(index: number): number { + return this.loadCell(index, new CellData()).bg & BgFlags.PROTECTED; + } + +} + +const enum DataKind { // 4 bits + FG = 1, // lower 26 bits is RGB foreground color and CM_MASK + BG = 2, // lower 26 bits is RGB background color and CM_MASK + STYLE_FLAGS = 3, // lower 28 bits is StyleFlags + + SKIP_COLUMNS = 7, // empty ("null") columns (28 bit count) + // The following have a 21-bit codepoint value in the low-order bits + CHAR_W1 = 8, // single-non-compound, 1 column wide + CHAR_W2 = 9, // single-non-compound, 2 columns wide + // CLUSTER_START_xx have a 7=bit for number of CONTINUED entries + CLUSTER_START_W1 = 10, // start of non-trivial cluster, 1 column wide + CLUSTER_START_W2 = 11, // start of non-trivial cluster, 2 columns wide + CLUSTER_CONTINUED = 12 // continuation of cluster +} + +const NULL_DATA_WORD = DataKind.SKIP_COLUMNS << 28; + +var USE_NewBufferLine = true; +export function usingNewBufferLine(): boolean { return USE_NewBufferLine; } +export function selectNewBufferLine(value: boolean): void { USE_NewBufferLine = value; } +export abstract class BufferLine extends AbstractBufferLine implements IBufferLine { + + static make(cols: number, fillCellData?: ICellData, isWrapped: boolean = false): BufferLine { + if (USE_NewBufferLine) { + // if (isWrapped) new WrappedBufferLine(...); + return new LogicalBufferLine(cols, fillCellData); + } + return new OldBufferLine(cols, fillCellData, isWrapped); + + } + + // @deprecated - only if !usingNewBufferLine() + public abstract copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void; + + // FOLLOWING ONLY USED BY NewBufferLine + /** From a Uint23 in _data, extract the DataKind bits. */ + public static wKind(word: number): DataKind { return word >>> 28; } + public static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_W1 && kind <= DataKind.CLUSTER_CONTINUED; } + public static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_CONTINUED; } + /** From a Uint23 in _data, extract length of string within _text. + * Only for SKIP_COLUMNS. */ + public static wSkipCount(word: number): number { return word & 0xfffff; } + public static wSet1(kind: DataKind, value: number): number { + return (kind << 28) | (value & 0x0fffffff); + } +} + +// FOLLOWING ONLY APPLIES for OldBufferLine /** * buffer memory layout: @@ -35,36 +240,17 @@ const enum Cell { BG = 2 // currently unused } -export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); +// This class will be removed at some point -// Work variables to avoid garbage collection -let $startIndex = 0; - -/** Factor when to cleanup underlying array buffer after shrinking. */ -const CLEANUP_THRESHOLD = 2; - -/** - * Typed array based bufferline implementation. - * - * There are 2 ways to insert data into the cell buffer: - * - `setCellFromCodepoint` + `addCodepointToCell` - * Use these for data that is already UTF32. - * Used during normal input in `InputHandler` for faster buffer access. - * - `setCell` - * This method takes a CellData object and stores the data in the buffer. - * Use `CellData.fromCharData` to create the CellData object (e.g. from JS string). - * - * To retrieve data from the buffer use either one of the primitive methods - * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop - * memory allocs / GC pressure can be greatly reduced by reusing the CellData object. - */ -export class BufferLine implements IBufferLine { +export class OldBufferLine extends BufferLine implements IBufferLine { protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; public length: number; - constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { + constructor(cols: number, fillCellData?: ICellData, isWrapped: boolean = false) { + super(); + this._isWrapped = isWrapped; this._data = new Uint32Array(cols * CELL_SIZE); const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); for (let i = 0; i < cols; ++i) { @@ -155,7 +341,6 @@ export class BufferLine implements IBufferLine { public isCombined(index: number): number { return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK; } - /** Returns the string content of the cell. */ public getString(index: number): string { const content = this._data[index * CELL_SIZE + Cell.CONTENT]; @@ -252,7 +437,6 @@ export class BufferLine implements IBufferLine { } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } - public insertCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; @@ -304,39 +488,7 @@ export class BufferLine implements IBufferLine { this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); } if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { - this.setCellFromCodepoint(pos, 0, 1, fillCellData); - } - } - - public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { - // full branching on respectProtect==true, hopefully getting fast JIT for standard case - if (respectProtect) { - if (start && this.getWidth(start - 1) === 2 && !this.isProtected(start - 1)) { - this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); - } - if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) { - this.setCellFromCodepoint(end, 0, 1, fillCellData); - } - while (start < end && start < this.length) { - if (!this.isProtected(start)) { - this.setCell(start, fillCellData); - } - start++; - } - return; - } - - // handle fullwidth at start: reset cell one to the left if start is second cell of a wide char - if (start && this.getWidth(start - 1) === 2) { - this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); - } - // handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char - if (end < this.length && this.getWidth(end - 1) === 2) { - this.setCellFromCodepoint(end, 0, 1, fillCellData); - } - - while (start < end && start < this.length) { - this.setCell(start++, fillCellData); + this.setCellFromCodepoint(pos, 0, 1,fillCellData); } } @@ -406,7 +558,7 @@ export class BufferLine implements IBufferLine { } /** fill a line with fillCharData */ - public fill(fillCellData: ICellData, respectProtect: boolean = false): void { + public fill(fillCellData: ICellData, respectProtect?: boolean): void { // full branching on respectProtect==true, hopefully getting fast JIT for standard case if (respectProtect) { for (let i = 0; i < this.length; ++i) { @@ -424,7 +576,8 @@ export class BufferLine implements IBufferLine { } /** alter to a full copy of line */ - public copyFrom(line: BufferLine): void { + public copyFrom(xline: BufferLine): void { + const line = xline as OldBufferLine; if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { @@ -440,12 +593,12 @@ export class BufferLine implements IBufferLine { for (const el in line._extendedAttrs) { this._extendedAttrs[el] = line._extendedAttrs[el]; } - this.isWrapped = line.isWrapped; + this._isWrapped = line.isWrapped; } /** create a new clone */ public clone(): IBufferLine { - const newLine = new BufferLine(0); + const newLine = new OldBufferLine(0); newLine._data = new Uint32Array(this._data); newLine.length = this.length; for (const el in this._combined) { @@ -454,8 +607,8 @@ export class BufferLine implements IBufferLine { for (const el in this._extendedAttrs) { newLine._extendedAttrs[el] = this._extendedAttrs[el]; } - newLine.isWrapped = this.isWrapped; - return newLine; + newLine._isWrapped = this.isWrapped; + return newLine as IBufferLine; } public getTrimmedLength(): number { @@ -476,7 +629,8 @@ export class BufferLine implements IBufferLine { return 0; } - public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { + public copyCellsFrom(xsrc: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { + const src = xsrc as OldBufferLine; const srcData = src._data; if (applyInReverse) { for (let cell = length - 1; cell >= 0; cell--) { @@ -508,22 +662,7 @@ export class BufferLine implements IBufferLine { } } - /** - * Translates the buffer line to a string. - * - * @param trimRight Whether to trim any empty cells on the right. - * @param startCol The column to start the string (0-based inclusive). - * @param endCol The column to end the string (0-based exclusive). - * @param outColumns if specified, this array will be filled with column numbers such that - * `returnedString[i]` is displayed at `outColumns[i]` column. `outColumns[returnedString.length]` - * is where the character following `returnedString` will be displayed. - * - * When a single cell is translated to multiple UTF-16 code units (e.g. surrogate pair) in the - * returned string, the corresponding entries in `outColumns` will have the same column number. - */ - public translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string { - startCol = startCol ?? 0; - endCol = endCol ?? this.length; + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); } @@ -549,3 +688,1259 @@ export class BufferLine implements IBufferLine { return result; } } + +// This class will be merged with its parent when OldBufferLine is removed, + +export abstract class NewBufferLine extends BufferLine implements IBufferLine { + + nextRowSameLine: WrappedBufferLine | undefined; + + /** The "current" index into the _data array. + * The index must be either dataLength() or wKindIsTextOrSkip must be true. + * (The index never points to a CLUSTER_CONTINUED item.) + */ + _cachedDataIndex(): number { return this.logicalLine()._cache1 >>> 16; } + /** The logical column number corresponding to _cachedDataIndex(). */ + _cachedColumn(): LineColumn { return this.logicalLine()._cache1 & 0xFFFF; } + protected abstract _cachedColumnInRow(): RowColumn; + // private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED + abstract _cachedBg(): number; + abstract _cachedFg(): number; + // An index (in data()) of a STYLE_FLAGS entry; -1 if none. + protected _cachedStyleFlagsIndex(): number { return this.logicalLine()._cache4; } + protected _cacheReset(): void { const line = this.logicalLine(); line._cache1 = 0; line._cache2 = 0; line._cache3 = 0; line._cache4 = -1; } + protected _cacheSetFgBg(fg: number, bg: number): void { const line = this.logicalLine(); line._cache2 = bg; line._cache3 = fg; } + protected _cacheSetStyleFlagsIndex(index: number): void { this.logicalLine()._cache4 = index; } + protected _cacheSetColumnDataIndex(column: LineColumn, dataIndex: number): void { this.logicalLine()._cache1 = (dataIndex << 16) | (column & 0xFFFF); } + + public setStartFromCache(wrapRow: WrappedBufferLine): void { + wrapRow.startIndex = this._cachedDataIndex(); + wrapRow.startColumn = this._cachedColumn(); + wrapRow.startBg = this._cachedBg(); + wrapRow.startFg = this._cachedFg(); + wrapRow.startStyle = this._cachedStyleFlagsIndex(); + } + + // Length of data() array. + abstract dataLength(): number; + // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. + protected _extendedAttrs: IExtendedAttrs[] = []; + + public abstract logicalLine(): LogicalBufferLine; + public abstract logicalStartColumn(): LineColumn; + protected abstract data(): Uint32Array; + abstract resizeData(size: number): void; + abstract addEmptyDataElements(position: number, count: number): void; + protected shouldCleanupMemory(): boolean { + return this.dataLength() * CLEANUP_THRESHOLD < this.data().length; + } + + + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this.moveToColumn(index) >>> Content.WIDTH_SHIFT; + } + + /** Test whether content has width. */ + public hasWidth(index: number): number { + return this.moveToColumn(index) & Content.WIDTH_MASK; + } + + /** Get FG cell component. */ + public getFg(index: number): number { + this.moveToColumn(index); + const styleIndex = this._cachedStyleFlagsIndex(); + const styleWord = styleIndex < 0 ? 0 : this.data()[styleIndex]; + return this._cachedFg() | ((styleWord << 24) & Attributes.STYLE_BITS_MASK); + } + + /** Get BG cell component. @deprecated */ + public getBg(index: number): number { + this.moveToColumn(index); + const styleIndex = this._cachedStyleFlagsIndex(); + const styleWord = styleIndex < 0 ? 0 : this.data()[styleIndex]; + return this._cachedBg() | ((styleWord << 16) & Attributes.STYLE_BITS_MASK); + } + + /** + * Test whether contains any chars. @deprecated + * Basically an empty has no content, but other cells might differ in FG/BG + * from real empty cells. + */ + public hasContent(index: number): number { + return this.moveToColumn(index) & Content.HAS_CONTENT_MASK; + } + + /** Test whether the cell contains a combined string. */ + public isCombined(index: number): number { + return this.moveToColumn(index) & Content.IS_COMBINED_MASK; + } + + public showRowData(): string { + return this.showData(this.logicalStartColumn(), this.nextRowSameLine ? this.nextRowSameLine?.logicalStartColumn() : Infinity); + } + /* Human-readable display of data() array, for debugging */ + public showData(startColumn = 0, endColumn = Infinity): string { + let s = ''; + let curColumn = 0; + for (let i = 0; i < this.dataLength() && curColumn < endColumn; i++) { + const word = this.data()[i]; + const kind = BufferLine.wKind(word); + let code: string | number = kind; + const wnum = word & 0xfffffff; + let nextColumn = curColumn; + switch (kind) { + case DataKind.FG: code = 'FG'; break; + case DataKind.BG: code = 'BG'; break; + case DataKind.STYLE_FLAGS: code = 'STYLE'; break; + case DataKind.SKIP_COLUMNS: code = 'SKIP'; nextColumn += wnum; break; + case DataKind.CLUSTER_START_W1: code = 'CL1'; nextColumn += 1; break; + case DataKind.CLUSTER_START_W2: code = 'CL2'; nextColumn += 2; break; + case DataKind.CLUSTER_CONTINUED: code = 'CL_CONT'; break; + case DataKind.CHAR_W1: code = 'C1'; nextColumn += 1; break; + case DataKind.CHAR_W2: code = 'C2'; nextColumn += 2; break; + } + + if (startColumn < nextColumn) { + if (s) { + s += ', '; + } + let value; + if (kind === DataKind.CHAR_W1 || kind === DataKind.CHAR_W2) { + let count = 1; + const w = nextColumn - curColumn; + while (curColumn + count * w < endColumn && i + count < this.dataLength() && BufferLine.wKind(this.data()[i + count]) === kind) { + count++; + } + let str; + if (count === 1) { + str = stringFromCodePoint(word & 0x1fffff); + } else { + str = utf32ToString(this.data(), i, i + count); + code = code + '*' + count; + i += count - 1; + } + value = JSON.stringify(str); + nextColumn = curColumn + count * w; + } else if (kind === DataKind.CLUSTER_START_W1 + || kind === DataKind.CLUSTER_START_W2 + || kind === DataKind.CLUSTER_CONTINUED) { + value = '#' + (word & 0x1fffff).toString(16); + } else if (kind === DataKind.BG || kind === DataKind.FG) { + value = (wnum >> 24) + '#' + (wnum & 0xffffff).toString(16); + } else if (kind === DataKind.STYLE_FLAGS) { + value = '#' + (wnum & 0xfffffff).toString(16); + if (wnum & StyleFlags.HAS_EXTENDED) { + const extended = this._extendedAttrs[i]; + if (! extended) { value += " (missing ext)"; } + else { + switch (extended.underlineStyle) { + case UnderlineStyle.SINGLE: value += " us:SINGLE"; break; + case UnderlineStyle.DOUBLE: value += " us:DOUBLE"; break; + case UnderlineStyle.CURLY: value += " us:CURLY"; break; + case UnderlineStyle.DOTTED: value += " us:DOTTED"; break; + case UnderlineStyle.DASHED: value += " us:DASHED"; break; + } + } + } + } else if (kind === DataKind.SKIP_COLUMNS) { + value = nextColumn <= endColumn ? wnum + : `${endColumn - curColumn} of ${wnum}`; + } else { + value = wnum.toString(); + } + s += code + ': ' + value; + if (curColumn < startColumn) { + s += ` offset ${startColumn - curColumn}`; + } + } + curColumn = nextColumn; + } + return `[${s}]`; + } + + /** Check invariants. Useful for debugging. */ + _check(): void { + function error(str: string): void { + console.log('ERROR: '+str); + } + const data = this.data(); + if (this.dataLength() < 0 || this.dataLength() > data.length) + {error('bad _dataLength');} + if (this.dataLength() === 2 && BufferLine.wKind(data[0]) === DataKind.SKIP_COLUMNS && BufferLine.wKind(data[1]) === DataKind.BG) { + error('SKIP followed by BG'); + } + if (this.dataLength() === 1 && data[0] === BufferLine.wSet1(DataKind.BG, 0)) { + error('default BG only'); + } + for (let idata = 0; idata < this.dataLength(); idata++) { + const word = this.data()[idata]; + const kind = BufferLine.wKind(word); + switch (kind) { + case DataKind.FG: + case DataKind.BG: + break; + case DataKind.STYLE_FLAGS: + if ((word & StyleFlags.HAS_EXTENDED) != 0 + && ! this._extendedAttrs[idata]) { + error("missed ExtendedAttributes") + } + break; + case DataKind.SKIP_COLUMNS: + break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + case DataKind.CLUSTER_CONTINUED: + break; + default: + error('invalid _dataKind'); + } + } + + } + + /** + * Get cell data CharData. + * @deprecated + */ + public get(index: number): CharData { + return this.loadCell(index, new CellData()).getAsCharData(); + } + + public clusterEnd(idata: number): number { + // FIXME do we need to handle more than 7 bits of CLUSTED_CONTINUED? + return idata + 1 + ((this.data()[idata] >> 21) & 0x3F); + } + + public insertCells(pos: number, n: number, fillCellData: ICellData): void { + // FIXME handle if start or end in middle of wide character. + const width = this.length; + if (pos >= width) { + return; + } + if (pos + n < width) { + const endpos = width - n; + this.moveToColumn(endpos); + const idata = this._cachedDataIndex(); + const colOffset = this._cachedColumn(); + this.logicalLine().deleteCellsOnly(idata, this.logicalStartColumn() + endpos - colOffset, n); + } else { + n = width - pos; + } + this.preInsert(this.logicalStartColumn() + pos, fillCellData); + const idata = this._cachedDataIndex(); + this.addEmptyDataElements(idata, 1); + // Ideally should optimize for adjacent SKIP_COLUMNS (as in eraseCells). + // However, typically is followed by replacing the new empty cells. + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); + } + + /** Move to column 'index', which is a RowColumn. + * Return encoded 'content'. + */ + public moveToColumn(index: RowColumn, stopEarly: boolean = false): number { + const endColumn = this.nextRowSameLine ? this.nextRowSameLine.logicalStartColumn() : Infinity; + return this.moveToLineColumn(index + this.logicalStartColumn(), endColumn, stopEarly); + } + + /** Move to column 'index', which is a LineColumn. + * Return encoded 'content' (code value with width and possible IS_COMBINED_MARK) of following character, if any. + * If at SKIP_COLUMNS or after end then the code value is 0 and the width is 1. + * If in the middle of a multi-column character, the code value is 0 and the width is 0. + */ + public moveToLineColumn(index: LineColumn, endColumn = Infinity, stopEarly: boolean = false): number { + let curColumn = this._cachedColumn(); + if (index < curColumn) { + // FIXME can sometimes do better + this._cacheReset(); + curColumn = this._cachedColumn(); + } + let idata = this._cachedDataIndex(); + let fg = this._cachedFg(); + let bg = this._cachedBg(); + let styleFlagsIndex = this._cachedStyleFlagsIndex(); + let todo = index - curColumn; + let word; + let kind; + let content = 0; + while (stopEarly ? todo > 0 : todo >= 0) { + if (idata >= this.dataLength()) { + word = NULL_DATA_WORD; + kind = DataKind.SKIP_COLUMNS; + content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; + break; + } + let nextColumn = curColumn; + word = this.data()[idata]; + kind = BufferLine.wKind(word); + let w; + switch (kind) { + case DataKind.FG: + fg = word & 0x3FFFFFF; + idata++; + break; + case DataKind.BG: + bg = word & 0x3FFFFFF; + idata++; + break; + case DataKind.STYLE_FLAGS: + styleFlagsIndex = idata; + idata++; + break; + case DataKind.SKIP_COLUMNS: + w = BufferLine.wSkipCount(word); + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { + todo -= w; + idata++; + curColumn += w; + } else { + content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; + todo = -1; + } + break; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + w = kind + 1 - DataKind.CLUSTER_START_W1; + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { + const clEnd = this.clusterEnd(idata); + todo -= w; + curColumn = nextColumn; + idata = clEnd; + } else { + content = nextColumn > endColumn + ? (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE + : index !== curColumn ? 0 + : (w << Content.WIDTH_SHIFT) | Content.IS_COMBINED_MASK; + todo = -1; + } + break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + w = kind + 1 - DataKind.CHAR_W1; // 1, or 2 if wide characters + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { + todo -= w; + idata++; + curColumn = nextColumn; + } else { + todo = -1; + content = nextColumn > endColumn + ? (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE + : index !== curColumn ? 0 + : (w << Content.WIDTH_SHIFT) | (word & 0x1fffff); + } + break; + } + } + this._cacheSetColumnDataIndex(curColumn, idata); + this._cacheSetFgBg(fg, bg); + this._cacheSetStyleFlagsIndex(styleFlagsIndex); + return content; + } + + /** + * Load data at `index` into `cell`. This is used to access cells in a way that's more friendly + * to GC as it significantly reduced the amount of new objects/references needed. + */ + public loadCell(index: number, cell: ICellData): ICellData { + const cursor = cell as CellData; + const content = this.moveToColumn(index); + cursor.content = content; + cursor.setFg(this._cachedFg()); + cursor.setBg(this._cachedBg()); + const styleFlagsIndex = this._cachedStyleFlagsIndex(); + const word = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; + cursor.setStyleFlags(word); + if ((word & StyleFlags.HAS_EXTENDED) !== 0) { + cursor.extended = this._extendedAttrs[styleFlagsIndex]!; + } + if (content & Content.IS_COMBINED_MASK) { + // FIXME do this lazily, in CellData.getChars + const idata = this._cachedDataIndex(); + const str = utf32ToString(this.data(), idata, this.clusterEnd(idata)); + cursor.combinedData = str; + } + return cell; + } + + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + this.moveToColumn(pos); + const idata = this._cachedDataIndex(); + const curColumn = this._cachedColumn(); + this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); + } + + public _splitIfNeeded(index: LineColumn): number { + const content = this.logicalLine().moveToLineColumn(index, Infinity, true); + let curColumn = this._cachedColumn(); + let idata = this._cachedDataIndex(); + + // CASES: + // 1. idata === dataLength() - easy. + // 2. data()[idata] is SKIP_COLUMNS + // -- split if curColumnn > 0 && curColumn < wlen + // 3. kind is wKindIsText: + // a. curColumn===index + // b. index === curColumn + width + // c. otherwise - in middle of wide char + + if (curColumn < index) { + if ((content >> Content.WIDTH_SHIFT) === 0 + && index === curColumn + 1) { + // In the middle of a wide character. Well-behaved applications are + // unlikely to do this, so it's not worth optimizing. + const clEnd = this.clusterEnd(idata); + this.addEmptyDataElements(idata, 2 - (clEnd - idata)); + let wrappedBecauseWide = false; // FIXME + let prev: NewBufferLine = this.logicalLine(); + let prevStart = 0; + for (;;) { + let next = prev.nextRowSameLine; + if (! next) { break; } + let nextStart = next.logicalStartColumn(); + if (nextStart === curColumn && nextStart === prevStart + this.length - 1) { + wrappedBecauseWide = true; + index++; + } + if (wrappedBecauseWide) { + next.startColumn++; + } + prev = next; + } + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wrappedBecauseWide ? 2 : 1); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); + curColumn = index; + } else if (idata === this.dataLength()) { + this.addEmptyDataElements(idata, 1); + this.data()[idata] = + BufferLine.wSet1(DataKind.SKIP_COLUMNS, index - curColumn); + curColumn = index; + idata++; + } else if (BufferLine.wKind(this.data()[idata]) === DataKind.SKIP_COLUMNS) { + const oldSkip = BufferLine.wSkipCount(this.data()[idata]); + this.addEmptyDataElements(idata, 1); + const needed = index - curColumn; + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, needed); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, oldSkip - needed); + curColumn = index; + } else { + console.log(`can't insert at column ${index}`); + } + this._cacheSetColumnDataIndex(curColumn, idata); + } + return idata; + } + protected preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { + let idata = this._splitIfNeeded(index); + // set attributes + const newFg = attrs.getFg(); + const newBg = attrs.getBg(); + const newStyle = attrs.getStyleFlags(); + let oldFg = this._cachedFg(); + let oldBg = this._cachedBg(); + const styleFlagsIndex = this._cachedStyleFlagsIndex(); + const oldStyle = styleFlagsIndex < 0 ? 0 : (this.data()[styleFlagsIndex] & 0xfffffff); + let data = this.data(); + const idata0 = idata; + let dataLength = this.dataLength(); + for (; idata < dataLength; idata++) { + const word = data[idata]; + let done = true; + switch (BufferLine.wKind(word)) { + case DataKind.BG: + if ((word & 0x3ffffff) === newBg) { + oldBg = newBg; + done = false; + } + break; + case DataKind.FG: + if ((word & 0x3ffffff) === newFg) { + oldFg = newFg; + done = false; + } + break; + } + if (done) { + break; + } + } + let needFg = newFg !== oldFg; + let needBg = newBg !== oldBg; + let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[styleFlagsIndex]; + let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; + let needStyle = newStyle !== oldStyle || oldExt !== newExt; + const add1 = extendToEnd ? 1 : 2; + let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); + if (add ) { + add =(needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); + this.addEmptyDataElements(idata, add - (idata0 - idata)); + data = this.data(); + if (needFg) { + data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); + } + if (needBg) { + data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); + } + if (needStyle) { + if (newStyle & StyleFlags.HAS_EXTENDED) + {this._extendedAttrs[idata] = attrs.extended;} + this._cacheSetStyleFlagsIndex(idata); + data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); + } + this._cacheSetColumnDataIndex(index, idata); + let xdata = idata; // FIXME + if (! extendToEnd) { + if (needFg) { + data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); + } + if (needStyle) { + if ((oldStyle & StyleFlags.HAS_EXTENDED) !== 0 && oldExt) + {this._extendedAttrs[xdata] = oldExt;} + data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); + } + if (needBg) { + data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + } + } + this._cacheSetFgBg(newFg, newBg); + } else if (idata > idata0) { + this._cacheSetColumnDataIndex(index, idata); + } + return add > 0; + } + + /** Insert characters from 'data' (from 'start' to 'end'). + * @return The ending column. This may be more than the available width, + * in which case the caller is responsible for wrapping. + */ + public insertText(index: RowColumn, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, coreService: ICoreService): RowColumn { + const insertMode = coreService.modes.insertMode; + const wraparoundMode = coreService.decPrivateModes.wraparound; + let lstart = this.logicalStartColumn(); + let lindex = index + lstart; + const add = this.preInsert(lindex, attrs); + lstart = this.logicalStartColumn(); + lindex = index + lstart; + let curColumn = this._cachedColumn(); + const lline = this.logicalLine(); + const startColumn: LineColumn = curColumn; + let idata = this._cachedDataIndex(); + let precedingJoinState = inputHandler.precedingJoinState; + let inext; + if (add || idata === this.dataLength() || lindex === curColumn) + {inext = idata;} + else { + const kind = BufferLine.wKind(this.data()[idata]); + if (BufferLine.wKindIsText(kind)) + {inext = this.clusterEnd(idata);} + else + {inext = idata;} + } + // FIXME optimize of overwriting simple text in-place + this.addEmptyDataElements(inext, end - start); + + let cellColumn = curColumn; + let chWidth = 0; + for (let i = start; i < end; i++) { + // inext is the insertion point for the current codepoint + // idata is the start of the most recent character or cluster, + // assuming all codepoints from idata until inext are the same cluster. + // If there is no preceding character/cluster that can be added to, + // then idata === inext. + const code = data[i]; + const currentInfo = inputHandler.unicodeService.charProperties(code, precedingJoinState); + chWidth = UnicodeService.extractWidth(currentInfo); + const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); + const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0; + precedingJoinState = currentInfo; + let kind; + if (shouldJoin) { + kind = chWidth === 2 ? DataKind.CLUSTER_START_W2 : DataKind.CLUSTER_START_W1; + const oldCount = (this.data()[idata] >> 21) & 0x3F; + const startChar = this.data()[idata] & 0x1FFFFF; + // FIXME check for count overflow; + this.data()[idata] = BufferLine.wSet1(kind, + startChar + ((oldCount + 1) << 21)); + kind = DataKind.CLUSTER_CONTINUED; + curColumn += chWidth - oldWidth; + } else { + kind = chWidth === 2 ? DataKind.CHAR_W2 : DataKind.CHAR_W1; + idata = inext; + cellColumn = curColumn; + curColumn += chWidth; + } + this.data()[inext++] = BufferLine.wSet1(kind, code); + } + const lastChar = idata; + inputHandler.precedingJoinState = precedingJoinState; + if (! insertMode && idata < this.dataLength()) { + this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); + } + if (curColumn > lline.logicalWidth) + {lline.logicalWidth = curColumn;} + curColumn -= lstart; + if (curColumn > this.length && ! wraparoundMode) { + this.moveToColumn(this.length - chWidth); + idata = this._cachedDataIndex(); + this.addEmptyDataElements(idata, idata - lastChar); + } else { + this._cacheSetColumnDataIndex(cellColumn, idata); + } + return curColumn; + } + + public eraseCells(start: RowColumn, end: RowColumn, attrs: IAttributeData): void { + const startColumn = this.logicalStartColumn(); + if (end === Infinity && this.nextRowSameLine) { end = this.length; } + const count = end - start; + start += startColumn; + this.moveToLineColumn(start); + end += startColumn; + let idata = this._cachedDataIndex(); + const colOffset = start - this._cachedColumn(); + const lline = this.logicalLine(); + lline.deleteCellsOnly(idata, colOffset, count); + this.preInsert(start, attrs, end === Infinity); + idata = this._cachedDataIndex(); + const data = this.data(); + if (idata > 0 && BufferLine.wKind(data[idata-1]) === DataKind.SKIP_COLUMNS) { + const skipped = BufferLine.wSkipCount(data[idata - 1]); + if (idata === this.dataLength()) { + end = start - skipped; + idata--; + lline._dataLength = idata; + } else { + data[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, skipped + count); + } + } else { + if (idata === this.dataLength()) { + return; + } + this.addEmptyDataElements(idata, 1); + data[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, count); + } + this._cacheSetColumnDataIndex(end, idata); + } + + /** + * Set data at `index` to `cell`. FIXME doesn't handle combined chars. + */ + public setCell(index: number, cell: ICellData): void { + const width = cell.getWidth(); + if (cell.content & Content.IS_COMBINED_MASK) { + const str = cell.combinedData; + const nstr = str.length; + const arr = new Uint32Array(nstr); + let istr = 0; + let iarr = 0; + while (istr < nstr) { + const cp = str.codePointAt(istr) || 0; + arr[iarr++] += cp; + istr += cp >= 0x10000 ? 2 : 1; + } + if (iarr <= 1) { + this.setCellFromCodepoint(index, iarr > 0 ? arr[0] : NULL_CELL_CODE, width, cell); + } else { + const lindex = index + this.logicalStartColumn(); + const add = this.preInsert(lindex, cell); // FIXME + let curColumn = this._cachedColumn(); + let idata = this._cachedDataIndex(); + let inext = idata; + let cellColumn = curColumn; + const kind = width === 2 ? DataKind.CLUSTER_START_W2 : DataKind.CLUSTER_START_W1; + idata = inext; + cellColumn = curColumn; + curColumn += width; + this.addEmptyDataElements(inext, iarr); + this.data()[inext++] = BufferLine.wSet1(kind, arr[0] + ((iarr - 1) << 21)); + for (let i = 1; i < iarr; i++) { + this.data()[inext++] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, arr[i]); + } + this._cacheSetColumnDataIndex(cellColumn, idata); + if (idata < this.dataLength()) { + this.logicalLine().deleteCellsOnly(inext, 0, width); + } + } + } else { + this.setCellFromCodepoint(index, cell.getCode(), width, cell); + } + } + + public setCellFromCodepoint(index: RowColumn, codePoint: number, width: number, attrs: IAttributeData): void { + if (codePoint === NULL_CELL_CODE) { + if (width === 0) { + // i.e. combining character + // FIXME - usually a no-op + } else { + this.eraseCells(index, index + 1, attrs); + } + return; + } + const lindex = index + this.logicalStartColumn(); + const add = this.preInsert(lindex, attrs); // FIXME + let curColumn = this._cachedColumn(); + let idata = this._cachedDataIndex(); + let inext; + if (add || idata === this.dataLength() || lindex === curColumn) + {inext = idata;} + else { + const kind = BufferLine.wKind(this.data()[idata]); + if (BufferLine.wKindIsText(kind)) + {inext = this.clusterEnd(idata);} + else + {inext = idata;} + } + let cellColumn = curColumn; + const kind = width === 2 ? DataKind.CHAR_W2 : DataKind.CHAR_W1; + idata = inext; + cellColumn = curColumn; + curColumn += width; + // FIXME optimize of overwriting simple text in-place + this.addEmptyDataElements(inext, 1); + this.data()[inext++] = BufferLine.wSet1(kind, codePoint); + this._cacheSetColumnDataIndex(cellColumn, idata); + if (idata < this.dataLength()) { + this.logicalLine().deleteCellsOnly(inext, 0, width); + } + } + + public replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { + if (! respectProtect && fillCellData.getCode() === 0) { + // FIXME optimize + } + super.replaceCells(start, end, fillCellData, respectProtect); + } + + // DEPRECATED + public addCodepointToCell(index: number, codePoint: number, width: number): void { + const content = this.moveToColumn(index); + const idata = this._cachedDataIndex(); + const clEnd = this.clusterEnd(idata); + this.addEmptyDataElements(clEnd, 1); + const nContinued = clEnd - idata; + const data = this.data(); + const startWord = data[idata]; + const startChar = startWord & 0x1FFFFF; + width = width || (BufferLine.wKind(startWord) === DataKind.CLUSTER_START_W2 ? 2 : 1); + const kind = width === 2 ? DataKind.CLUSTER_START_W2 : DataKind.CLUSTER_START_W1; + data[idata] = BufferLine.wSet1(kind, + startChar + (nContinued << 21)); + data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); + } + + /** + * Resize BufferLine to `cols` filling excess cells with `fillCellData`. + * The underlying array buffer will not change if there is still enough space + * to hold the new buffer line data. + * Returns a boolean indicating, whether a `cleanupMemory` call would free + * excess memory (true after shrinking > CLEANUP_THRESHOLD). + * NOTE only used for testing? + */ + public resize(cols: number, fillCellData: ICellData): boolean { + if (cols === this.length) { + return this.shouldCleanupMemory(); + } + const uint32Cells = cols * CELL_SIZE; + if (cols > this.length) { + /* + if (this.data().buffer.byteLength >= uint32Cells * 4) { + // optimization: avoid alloc and data copy if buffer has enough room + this.data() = new Uint32Array(this.data().buffer, 0, uint32Cells); + } else { + // slow path: new alloc and full data copy + const data = new Uint32Array(uint32Cells); + data.set(this.data()); + this.data() = data; + } + */ + for (let i = this.length; i < cols; ++i) { + this.setCell(i, fillCellData); + } + } else { + // optimization: just shrink the view on existing buffer + /* + this.data() = this.data().subarray(0, uint32Cells); + // Remove any cut off combined data + const keys = Object.keys(this._combined); + for (let i = 0; i < keys.length; i++) { + const key = parseInt(keys[i], 10); + if (key >= cols) { + delete this._combined[key]; + } + } + // remove any cut off extended attributes + const extKeys = Object.keys(this._extendedAttrs); + for (let i = 0; i < extKeys.length; i++) { + const key = parseInt(extKeys[i], 10); + if (key >= cols) { + delete this._extendedAttrs[key]; + } + } + */ + } + this.length = cols; + return this.shouldCleanupMemory(); + } + + /** fill a line with fillCharData */ + public fill(fillCellData: ICellData, respectProtect?: boolean): void { + this.replaceCells(0, this.length, fillCellData, respectProtect); + } + + // @deprecated - not used if usingNewBufferLine() + public copyFrom(xline: BufferLine): void { + alert('copyFrom'); + } + + /** create a new clone */ + public clone(): IBufferLine { + alert('NewBufferLine.clone'); + const newLine = new LogicalBufferLine(0); + return newLine; + } + + public getTrimmedLength(countBackground: boolean = false): number { + let cols = 0; + let skipped = 0; + const startColumn = this.logicalStartColumn(); + const data = this.data(); + const end = this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); + let bg = this._cachedBg(); + for (let idata = startColumn; idata < end; idata++) { + const word = data[idata]; + const kind = BufferLine.wKind(word); + const w = kind === DataKind.CHAR_W2 || kind === DataKind.CLUSTER_START_W2 ? 2 : 1; + let wcols = 0; + switch (kind) { + case DataKind.BG: + bg = word & 0x3ffffff; + break; + case DataKind.FG: + case DataKind.STYLE_FLAGS: + break; + case DataKind.SKIP_COLUMNS: + skipped += BufferLine.wSkipCount(word); + break; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + const clEnd = this.clusterEnd(idata); + wcols = w * (clEnd - idata); + idata = clEnd - 1; + break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + wcols = w; + break; + case DataKind.CLUSTER_CONTINUED: + break; // should be skipped + } + if (wcols) { + cols += skipped + wcols; + skipped = 0; + } + } + return countBackground && bg !== 0 ? this.length : cols; + } + + public getNoBgTrimmedLength(): number { + return this.getTrimmedLength(true); + } + + public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { + // This is used by reflow (window resize). FUTURE: Integrate with pretty-printing. + const cell = new CellData(); + if (applyInReverse) { + for (let i = length - 1; i >= 0; i--) { + src.loadCell(srcCol + i, cell); + this.setCell(destCol + i, cell); + } + } else { + for (let i = 0; i < length; i++) { + src.loadCell(srcCol + i, cell); + this.setCell(destCol + i, cell); + } + } + } + + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { + const lineStart = this.logicalStartColumn(); + const s = this.logicalLine().translateLogicalToString(trimRight, lineStart + startCol, lineStart + endCol, outColumns, skipReplace); + if (outColumns && lineStart !== 0) { + for (let i = outColumns.length; --i >= 0; ) { + outColumns[i] -= lineStart; + } + } + return s; + } +} + +export class LogicalBufferLine extends NewBufferLine implements IBufferLine { + protected _data: Uint32Array; + // Each item in _data is a 4-bit DataKind and 28 bits data. + _dataLength: number; // active length of _data array + logicalWidth: number = 0; // FIXME needs work updating this + reflowNeeded: boolean = false; + + // Maybe move these to LogicalBufferLine? or to Buffer? + _cache1: number = 0; + _cache2: number = 0; + _cache3: number = 0; + _cache4: number = -1; + + constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine, startIndex?: number) { + super(); + // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); + // const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); + if (src) { + const lline = src.logicalLine(); + const oldStart = startIndex || 0; + this._data = lline._data.slice(oldStart); + this._dataLength = lline._dataLength - oldStart; + this._extendedAttrs = lline._extendedAttrs.slice(oldStart); + } else { + this._data = new Uint32Array(cols); + this._dataLength = 0; + } + this.length = cols; + this._isWrapped = false; + if (fillCellData) { this.preInsert(0, fillCellData); } + } + public override logicalLine(): LogicalBufferLine { return this; } + public override logicalStartColumn(): LineColumn { return 0; } + override data(): Uint32Array { return this._data; } + override dataLength(): number { return this._dataLength; } + override _cachedBg(): number { return this._cache2; } + override _cachedFg(): number { return this._cache3; } + + protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } + + // count can be negative + addEmptyDataElements(position: number, count: number): void { + const oldDataLength = this._dataLength; + this.resizeData(oldDataLength + count); + if (count < 0) { + this.data().copyWithin(position, position - count, oldDataLength); + } else { + this.data().copyWithin(position + count, position, oldDataLength); + } + this._dataLength += count; + for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { + if (next.startIndex > position) + {next.startIndex += count;} + } + if (count < 0) { + this._extendedAttrs.copyWithin(position, position - count, oldDataLength); + } else { + this._extendedAttrs.length = this._dataLength + this._extendedAttrs.copyWithin(position + count, position, oldDataLength); + } + if (this._extendedAttrs.length > this._dataLength) { + this._extendedAttrs.length = this._dataLength; + } + } + + resizeData(size: number): void { + if (size > this.data().length) { + // buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); + const dataNew = new Uint32Array((3 * size) >> 1); + dataNew.set(this._data); + this.logicalLine()._data = dataNew; + } + } + + /** + * Cleanup underlying array buffer. + * A cleanup will be triggered if the array buffer exceeds the actual used + * memory by a factor of CLEANUP_THRESHOLD. + * Returns 0 or 1 indicating whether a cleanup happened. + */ + public cleanupMemory(): number { + if (this.shouldCleanupMemory()) { + const data = new Uint32Array(this.dataLength()); + data.set(this.data()); + this._data = data; + return 1; + } + return 0; + } + + // FIXME doesn't properly handle if delete range starts or ends in middle + // of wide character + /** Internal - delete n columns, with no adjust at end of line. */ + public deleteCellsOnly(idata0: number, colOffset0: number, n: number): void { + let todo = n; + const data = this.data(); + let idata = idata0; + let colOffset = colOffset0; + let dskipFirst = idata; let dskipLast = -1; let w; + let fgValue = -1; // cursor.getFg(); + let bgValue = -1; // cursor.getBg(); + let styleValue = -1; + let extended = undefined; // cursor.getStyleFlags(); // FIXME handle extendedattrs + + if (colOffset === 0) { + while (idata > 0) { + let skipItem = true; + const word = data[idata-1]; + switch (BufferLine.wKind(word)) { + case DataKind.BG: bgValue = word & 0x3ffffff; break; + case DataKind.FG: fgValue = word & 0x3ffffff; break; + case DataKind.STYLE_FLAGS: + styleValue = word & 0xfffffff; + extended = (word & StyleFlags.HAS_EXTENDED) !== 0 && this._extendedAttrs[idata - 1]; + break; + default: skipItem = false; + } + if (skipItem) { + idata--; + dskipFirst = idata; + dskipLast = idata0-1; + } else { + break; + } + } + } + + for (; todo > 0 && idata < this.dataLength(); idata++) { + const word = data[idata]; + const kind = BufferLine.wKind(word); + switch (kind) { + case DataKind.FG: + fgValue = word & 0x3ffffff; + dskipLast = idata; + break; + case DataKind.BG: + bgValue = word & 0x3ffffff; + dskipLast = idata; + break; + case DataKind.STYLE_FLAGS: + dskipLast = idata; + styleValue = word & 0xfffffff; + extended = (word & StyleFlags.HAS_EXTENDED) !== 0 && this._extendedAttrs[idata]; + break; + case DataKind.SKIP_COLUMNS: + const wlen = BufferLine.wSkipCount(word); + if (colOffset === 0 && wlen <= todo) { + dskipLast = idata; + todo -= wlen; + } else { + const delta = Math.min(todo, wlen - colOffset); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); + todo -= delta; + } + colOffset = 0; + break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + w = kind - DataKind.CHAR_W1; // 0, or 1 if wide characters + if (colOffset === 0 && (1 << w) <= todo) { + dskipLast = idata; + todo -= 1 << w; + } + break; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + w = kind - DataKind.CLUSTER_START_W1; // 0, or 1 if wide characters + const clEnd = this.clusterEnd(idata); + if (colOffset < (1 << w)) { + idata = clEnd; + dskipLast = idata; + todo -= (1 << w); + } + colOffset = 0; + break; + } + } + idata0 = dskipFirst; + if (bgValue >= 0) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); + } + if (fgValue >= 0 && idata0 !== this._dataLength) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); + } + if (styleValue >= 0 && idata0 !== this._dataLength) { + if ((styleValue & StyleFlags.HAS_EXTENDED) !== 0 && extended) { + if (! extended) throw(new Error("missing extended")); + this._extendedAttrs[idata0] = extended; + } + this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); + } + if (dskipLast >= 0) { + const dcount = dskipLast + 1 - idata0; + this.addEmptyDataElements(idata0, - dcount); + } + } + + public setWrapped(previousLine: NewBufferLine): WrappedBufferLine { + previousLine.moveToColumn(previousLine.length); + const startLine = previousLine.logicalLine(); + startLine.resizeData(this._dataLength + startLine._dataLength); + startLine._data.set(this._data.subarray(0, this._dataLength), startLine._dataLength); + startLine._dataLength += this._dataLength; + for (let i = this._extendedAttrs.length; --i >= 0; ) { + const attr = this._extendedAttrs[i]; + if (attr) { startLine._extendedAttrs[startLine._dataLength + i] = attr; } + } + const newRow = new WrappedBufferLine(previousLine); + newRow.nextRowSameLine = this.nextRowSameLine; + startLine.setStartFromCache(newRow); + for (let following = this.nextRowSameLine; following; + following = following?.nextRowSameLine) { + following.startColumn += newRow.startColumn; + following.startIndex += newRow.startIndex; + } + return newRow; + } + + public translateLogicalToString(trimRight: boolean = false, startCol: number = 0, endCol: number = Infinity, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { + if (outColumns) { + outColumns.length = 0; + } + let s = ''; + let col = 0; + let pendingStart = -1; + let pendingLength = 0; + const data = this.data(); + function pendingForce(handleSkip = ! trimRight): void { + if (pendingStart >= 0 && pendingLength > 0) { + s += utf32ToString(data, pendingStart, pendingStart + pendingLength); + pendingLength = 0; + } else if (handleSkip && pendingLength > 0) { + s += skipReplace.repeat(pendingLength); + pendingLength = 0; + } + pendingStart = -1; + } + function addPendingString(start: number, length: number): void { + if (pendingStart >= 0 && pendingStart + pendingLength === start) { + pendingLength += length; + } else { + pendingForce(true); + pendingStart = start; + pendingLength = length; + } + if (outColumns) { + for (let i = 0; i < length; ++i) { + outColumns.push(col); + } + } + } + function addPendingSkip(length: number): void { + if (pendingStart >= 0) { + pendingForce(); + } + pendingLength += length; + } + for (let idata = 0; idata < this.dataLength() && col < endCol; idata++) { + const word = this.data()[idata]; + const kind = BufferLine.wKind(word); + const wide = kind === DataKind.CHAR_W2 || kind === DataKind.CLUSTER_START_W2 ? 1 : 0; + let wcols; + switch (kind) { + case DataKind.FG: + case DataKind.BG: + case DataKind.STYLE_FLAGS: + break; + case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wSkipCount(word); + if (col + wlen > startCol) { + if (col < startCol) { + wlen -= startCol - col; + col = startCol; + } + if (col + wlen > endCol) { + wlen = endCol - col; + } + addPendingSkip(wlen); + } + col += wlen; + break; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + const clEnd = this.clusterEnd(idata); + wcols = 1 << wide; + if (col >= startCol && col + wcols <= endCol) { + addPendingString(idata, clEnd - idata); + } + idata = clEnd - 1; + col += wcols; + break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + wcols = 1 << wide; + if (col >= startCol && col + wcols <= endCol) { + addPendingString(idata, 1); + } + col += wcols; + break; + } + } + if (col < startCol) { col = startCol; } + if (! trimRight && col < endCol && endCol !== Infinity) { + addPendingSkip(endCol - col); + } + pendingForce(); + return s; + } + + /** for debugging */ + getText(skipReplace: string = ' '): string { + return this.translateLogicalToString(true, 0, this.length, undefined, skipReplace); + } +} + +export class WrappedBufferLine extends NewBufferLine implements IBufferLine { + _logicalLine: LogicalBufferLine; + /** Number of logical columns in previous rows. + * Also: logical column number (column number assuming infinitely-wide + * terminal) corresponding to the start of this row. + * If R is 0 for the previous LogicalBufferLine, R is 1 for first + * WrappedBufferLine and so on, startColumn will *usually* be N*W + * (where W is the width of the terminal in columns) but may be slightly + * different when a wide character at column W-1 must wrap "early". + */ + startColumn: LineColumn = 0; + // DEPRECATE FIXME startIndex doesn't work in the case of when soft line-break is inside a SKIP_COLUMNS. + // startIndex, startFg, startBg, startStyle are primaraily used by _cacheReset + // to optimize moveToColumn on same row. It might be best to get rid of them; + // to migitate the pergfance cost we cann support backwards movement by moveToColumn. + // Changing Data>FG etc to use xor-encoding would help. TODO. + startIndex: number = 0; + startFg: number = 0; + startBg: number = 0; + startStyle: number = -1; + + constructor(prevRow: NewBufferLine) { + super(); + const logicalLine = prevRow.logicalLine(); + prevRow.nextRowSameLine = this; + this._logicalLine = logicalLine; + this._isWrapped = true; + this.length = logicalLine.length; + } + + public override logicalLine(): LogicalBufferLine { return this._logicalLine; } + public override logicalStartColumn(): LineColumn { return this.startColumn; } + protected override data(): Uint32Array { return this._logicalLine.data(); } + public override dataLength(): number { return this._logicalLine.dataLength(); } + public override _cachedBg(): number { return this._logicalLine._cachedBg(); } + public override _cachedFg(): number { return this._logicalLine._cachedFg(); } + addEmptyDataElements(position: number, count: number): void { + this._logicalLine.addEmptyDataElements(position, count); + } + protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF) - this.startColumn; } + protected _cacheReset(): void { + this._cacheSetFgBg(this.startFg, this.startBg); + this._cacheSetStyleFlagsIndex(this.startStyle); + this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); + } + public resizeData(size: number): void { this._logicalLine.resizeData(size); } + public cleanupMemory(): number { return 0;} +} diff --git a/src/common/buffer/BufferReflow.test.ts b/src/common/buffer/BufferReflow.test.ts index b351b89c42..5f0e3c7fdf 100644 --- a/src/common/buffer/BufferReflow.test.ts +++ b/src/common/buffer/BufferReflow.test.ts @@ -10,7 +10,7 @@ import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow'; describe('BufferReflow', () => { describe('reflowSmallerGetNewLineLengths', () => { it('should return correct line lengths for a small line with wide characters', () => { - const line = new BufferLine(4); + const line = BufferLine.make(4); line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]); line.set(1, [0, '', 0, 0]); line.set(2, [0, '语', 2, '语'.charCodeAt(0)]); @@ -20,7 +20,7 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语'); }); it('should return correct line lengths for a large line with wide characters', () => { - const line = new BufferLine(12); + const line = BufferLine.make(12); for (let i = 0; i < 12; i += 4) { line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]); line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]); @@ -42,7 +42,7 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语'); }); it('should return correct line lengths for a string with wide and single characters', () => { - const line = new BufferLine(6); + const line = BufferLine.make(6); line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]); line.set(2, [0, '', 0, 0]); @@ -56,14 +56,14 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b'); }); it('should return correct line lengths for a wrapped line with wide and single characters', () => { - const line1 = new BufferLine(6); + const line1 = BufferLine.make(6); line1.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); line1.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]); line1.set(2, [0, '', 0, 0]); line1.set(3, [0, '语', 2, '语'.charCodeAt(0)]); line1.set(4, [0, '', 0, 0]); line1.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]); - const line2 = new BufferLine(6, undefined, true); + const line2 = BufferLine.make(6, undefined, true); line2.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]); line2.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]); line2.set(2, [0, '', 0, 0]); @@ -78,7 +78,7 @@ describe('BufferReflow', () => { assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b'); }); it('should work on lines ending in null space', () => { - const line = new BufferLine(5); + const line = BufferLine.make(5); line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]); line.set(1, [0, '', 0, 0]); line.set(2, [0, '语', 2, '语'.charCodeAt(0)]); diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 9454c553cf..8a6144a7e2 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -18,13 +18,28 @@ export class CellData extends AttributeData implements ICellData { obj.setFromCharData(value); return obj; } + + public static fromChar(text: string, width: number = -1, fg: number = 0): CellData { + const obj = new CellData(); + obj.setFromChar(text, width, fg); + return obj; + } + /** Primitives from terminal buffer. */ public content = 0; public fg = 0; public bg = 0; public extended: IExtendedAttrs = new ExtendedAttrs(); public combinedData = ''; - /** Whether cell contains a combined string. */ + + public copyFrom(src: CellData): void { + this.content = src.content; + this.fg = src.fg; + this.bg = src.bg; + this.extended = src.extended; + } + + /** Whether cell contains a combined string. DEPRECTED */ public isCombined(): number { return this.content & Content.IS_COMBINED_MASK; } @@ -49,20 +64,30 @@ export class CellData extends AttributeData implements ICellData { * of the last char in string to be in line with code in CharData. */ public getCode(): number { - return (this.isCombined()) - ? this.combinedData.charCodeAt(this.combinedData.length - 1) - : this.content & Content.CODEPOINT_MASK; + if (this.isCombined()) { + const chars = this.getChars(); + return chars.charCodeAt(chars.length - 1); + } + return this.content & Content.CODEPOINT_MASK; } + public setFromChar(text: string, width: number = -1, fg: number = 0) { + width = width >= 0 ? width : stringFromCodePoint.length === 0 ? 0 : 1; + this.fg = fg; + this.bg = 0; + this.content = (text.codePointAt(0) || 0) | (width << Content.WIDTH_SHIFT); + } + /** Set data from CharData */ public setFromCharData(value: CharData): void { this.fg = value[CHAR_DATA_ATTR_INDEX]; this.bg = 0; let combined = false; + const length = value[CHAR_DATA_CHAR_INDEX].length; // surrogates and combined strings need special treatment - if (value[CHAR_DATA_CHAR_INDEX].length > 2) { + if (length > 2) { combined = true; } - else if (value[CHAR_DATA_CHAR_INDEX].length === 2) { + else if (length === 2) { const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); // if the 2-char string is a surrogate create single codepoint // everything else is combined diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 5ce075cf78..e978cc725b 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -4,9 +4,10 @@ */ export const DEFAULT_COLOR = 0; -export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0); +export const DEFAULT_ATTR = 0; export const DEFAULT_EXT = 0; +// Deprecated export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -73,6 +74,8 @@ export const enum Content { WIDTH_SHIFT = 22 } +export const NULL_CELL_WORD = 1 << Content.WIDTH_MASK; + export const enum Attributes { /** * bit 1..8 blue in RGB, color in P256 and P16 @@ -98,6 +101,7 @@ export const enum Attributes { * bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) */ CM_MASK = 0x3000000, + CM_COLOR_MASK = 0x3ffffff, CM_DEFAULT = 0, CM_P16 = 0x1000000, CM_P256 = 0x2000000, @@ -106,30 +110,50 @@ export const enum Attributes { /** * bit 1..24 RGB room */ - RGB_MASK = 0xFFFFFF + RGB_MASK = 0xFFFFFF, + + /** + * bit 27..32 in bg/fg are used for FgFlags/BgFlags (style bits). + * This will probably change. + */ + STYLE_BITS_MASK = 0xFC000000 +} + +export const enum StyleFlags { + INVERSE = 0x4, + BOLD = 0x8, + UNDERLINE = 0x10, + BLINK = 0x20, + INVISIBLE = 0x40, + STRIKETHROUGH = 0x80, + ITALIC = 0x400, + DIM = 0x800, + HAS_EXTENDED = 0x1000, + PROTECTED = 0x2000, + OVERLINE = 0x4000 } -export const enum FgFlags { +export const enum FgFlags { // deprecated /** * bit 27..32 */ - INVERSE = 0x4000000, - BOLD = 0x8000000, - UNDERLINE = 0x10000000, - BLINK = 0x20000000, - INVISIBLE = 0x40000000, - STRIKETHROUGH = 0x80000000, + INVERSE = StyleFlags.INVERSE << 24, // 0x4000000, + BOLD = StyleFlags.BOLD << 24, // 0x8000000, + UNDERLINE = StyleFlags.UNDERLINE << 24, // 0x10000000, + BLINK = StyleFlags.BLINK << 24, // x20000000, + INVISIBLE = StyleFlags.INVISIBLE << 24, // 0x40000000, + STRIKETHROUGH = StyleFlags.STRIKETHROUGH << 24 // 0x80000000 } -export const enum BgFlags { +export const enum BgFlags { // deprecated /** * bit 27..32 (upper 2 unused) */ - ITALIC = 0x4000000, - DIM = 0x8000000, - HAS_EXTENDED = 0x10000000, - PROTECTED = 0x20000000, - OVERLINE = 0x40000000 + ITALIC = StyleFlags.ITALIC << 16, // 0x4000000, + DIM = StyleFlags.DIM << 16, // 0x8000000, + HAS_EXTENDED = StyleFlags.HAS_EXTENDED << 16, // 0x10000000, + PROTECTED = StyleFlags.PROTECTED << 16, // 0x20000000 + OVERLINE = StyleFlags.OVERLINE << 16 // 0x40000000 } export const enum ExtFlags { diff --git a/src/common/buffer/Types.ts b/src/common/buffer/Types.ts index a59c0e177d..5d1673ffad 100644 --- a/src/common/buffer/Types.ts +++ b/src/common/buffer/Types.ts @@ -11,10 +11,31 @@ export type BufferIndex = [number, number]; export interface IBuffer { readonly lines: ICircularList; + /** Number of rows above top visible row. + * Similar to scrollTop (i.e. affected by scrollbar), but in rows. + * FUTURE: We want to handle variable-height rows. Maybe just use scrollTop. + */ ydisp: number; + /** Number of rows in the scrollback buffer, above the home row. */ ybase: number; + + /** Row number relative to the "home" row, zero-origin. + * This is the row number changed/reported by cursor escape sequences, + * except that y is 0-origin: y=0 when we're at the home row. + * Currently assumed to be >= 0, but FUTURE should allow negative - i.e. + * in scroll-back area, as long as ybase+y >= 0. + */ y: number; + + /** Column number, zero-origin. + * Valid range is 0 through C (inclusive), if C is terminal width in columns. + * The first (left-most) column is 0. + * The right-most column is either C-1 (before the right-most column, and + * ready to write in it), or C (after the right-most column, having written + * to it, and ready to wrap). DSR 6 returns C (1-origin) in either case, + */ x: number; + tabs: any; scrollBottom: number; scrollTop: number; @@ -26,6 +47,7 @@ export interface IBuffer { isCursorInViewport: boolean; markers: IMarker[]; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + splitLine(row: number, col: number): void; getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; @@ -35,6 +57,8 @@ export interface IBuffer { addMarker(y: number): IMarker; clearMarkers(y: number): void; clearAllMarkers(): void; + setWrapped(row: number, value: boolean): void; + reflowRegion(startRow: number, endRow: number, maxRows: number): void; } export interface IBufferSet extends IDisposable { diff --git a/src/common/input/TextDecoder.ts b/src/common/input/TextDecoder.ts index 7ec9c7cd20..0fcf7e0d86 100644 --- a/src/common/input/TextDecoder.ts +++ b/src/common/input/TextDecoder.ts @@ -27,6 +27,7 @@ export function utf32ToString(data: Uint32Array, start: number = 0, end: number let result = ''; for (let i = start; i < end; ++i) { let codepoint = data[i]; + codepoint &= 0x1FFFFF; // Needed if data is _data field of BufferLine. if (codepoint > 0xFFFF) { // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate // pair conversion rules: diff --git a/src/common/parser/Types.ts b/src/common/parser/Types.ts index 2ed4acdcaf..49702a9df9 100644 --- a/src/common/parser/Types.ts +++ b/src/common/parser/Types.ts @@ -226,7 +226,7 @@ export interface IDcsParser extends ISubParser> = { @@ -31,6 +32,7 @@ export const DEFAULT_OPTIONS: Readonly> = { linkHandler: null, logLevel: 'info', logger: null, + newBufferLine: true, scrollback: 1000, scrollOnUserInput: true, scrollSensitivity: 1, @@ -86,6 +88,7 @@ export class OptionsService extends Disposable implements IOptionsService { // set up getters and setters for each option this.rawOptions = defaultOptions; this.options = { ... defaultOptions }; + selectNewBufferLine(this.options['newBufferLine']); this._setupOptions(); // Clear out options that could link outside xterm.js as they could easily cause an embedder @@ -179,6 +182,9 @@ export class OptionsService extends Disposable implements IOptionsService { case 'minimumContrastRatio': value = Math.max(1, Math.min(21, Math.round(value * 10) / 10)); break; + case 'newBufferLine': + selectNewBufferLine(!!value); + break; case 'scrollback': value = Math.min(value, 4294967295); if (value < 0) {