From bf4e359f28cffdefc239dc7b6245d120bc30171d Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 13 Jul 2023 11:26:27 -0700 Subject: [PATCH 01/73] Experimental re-design of BufferLine. The new data structure is more flexible and more compact. However, mapping column number to cell information is no longer O(1). So we use a cursor as much as possible to reduce the need for that. This is an early check-in to have a backup and baseline. A few basic things work but it is NOT READY FOR USE. --- .../xterm-addon-canvas/src/CanvasRenderer.ts | 4 +- .../src/CursorRenderLayer.ts | 12 +- .../xterm-addon-canvas/src/TextRenderLayer.ts | 13 +- addons/xterm-addon-webgl/src/WebglRenderer.ts | 6 +- .../src/renderLayer/CursorRenderLayer.ts | 12 +- .../renderer/dom/DomRendererRowFactory.ts | 14 +- .../services/CharacterJoinerService.ts | 2 + src/browser/services/SelectionService.ts | 4 +- src/common/InputHandler.ts | 46 +- src/common/Types.d.ts | 33 +- src/common/buffer/AttributeData.ts | 7 + src/common/buffer/BufferLine.test.ts | 16 +- src/common/buffer/BufferLine.ts | 932 ++++++++++++++---- src/common/buffer/CellData.ts | 31 +- src/common/buffer/Constants.ts | 37 +- 15 files changed, 915 insertions(+), 254 deletions(-) diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index 2109654ea2..9c6a92ce11 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -39,7 +39,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { private readonly _bufferService: IBufferService, private readonly _charSizeService: ICharSizeService, private readonly _optionsService: IOptionsService, - characterJoinerService: ICharacterJoinerService, + characterJoinerService: ICharacterJoinerService, // FIXME remove coreService: ICoreService, private readonly _coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, @@ -48,7 +48,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService), + new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService), new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 7efcd74e88..ce0375bdc4 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -30,9 +30,9 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; - private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void}; + private _cursorRenderers: {[key: string]: (x: number, y: number, cell: CellData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; - private _cell: ICellData = new CellData(); + private _cell: CellData = new CellData(); constructor( terminal: Terminal, @@ -208,14 +208,14 @@ export class CursorRenderLayer extends BaseRenderLayer { } } - private _renderBarCursor(x: number, y: number, cell: ICellData): void { + private _renderBarCursor(x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); this._ctx.restore(); } - private _renderBlockCursor(x: number, y: number, cell: ICellData): void { + private _renderBlockCursor(x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); @@ -224,14 +224,14 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.restore(); } - private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void { + private _renderUnderlineCursor(x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } - private _renderBlurCursor(x: number, y: number, cell: ICellData): void { + private _renderBlurCursor(x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index 0066cc7d8e..39ab50aed2 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -12,8 +12,7 @@ import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants import { IColorSet, ReadonlyColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services'; -import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { JoinedCellData } from 'browser/services/CharacterJoinerService'; +import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { color, css } from 'common/Color'; import { Terminal } from 'xterm'; @@ -38,7 +37,6 @@ export class TextRenderLayer extends BaseRenderLayer { alpha: boolean, bufferService: IBufferService, optionsService: IOptionsService, - private readonly _characterJoinerService: ICharacterJoinerService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService, themeService: IThemeService @@ -80,9 +78,11 @@ export class TextRenderLayer extends BaseRenderLayer { for (let y = firstRow; y <= lastRow; y++) { const row = y + this._bufferService.buffer.ydisp; const line = this._bufferService.buffer.lines.get(row); - const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); + if (! line) + continue; + line.scanInit(this._workCell); for (let x = 0; x < this._bufferService.cols; x++) { - line!.loadCell(x, this._workCell); + line.scanNext(this._workCell, 1, 0); let cell = this._workCell; // If true, indicates that the current character(s) to draw were joined. @@ -105,6 +105,7 @@ export class TextRenderLayer extends BaseRenderLayer { // 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. + /* if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { isJoined = true; const range = joinedRanges.shift()!; @@ -120,7 +121,7 @@ export class TextRenderLayer extends BaseRenderLayer { // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; } - + */ // If the character is an overlapping char and the character to the // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 4fc4427fef..8c60720f21 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -391,6 +391,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // 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. + /* FIXME if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { isJoined = true; range = joinedRanges.shift()!; @@ -406,7 +407,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; } - + */ chars = cell.getChars(); code = cell.getCode(); i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; @@ -529,6 +530,7 @@ export class WebglRenderer extends Disposable implements IRenderer { } } +/* // TODO: Share impl with core export class JoinedCellData extends AttributeData implements ICellData { private _width: number; @@ -574,7 +576,7 @@ export class JoinedCellData extends AttributeData implements ICellData { return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; } } - +*/ function clamp(value: number, max: number, min: number = 0): number { return Math.max(Math.min(value, max), min); } diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts index c6a33a5cfc..58faccbe40 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts @@ -29,9 +29,9 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; - private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: ICellData) => void}; + private _cursorRenderers: {[key: string]: (terminal: Terminal, x: number, y: number, cell: CellData) => void}; private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; - private _cell: ICellData = new CellData(); + private _cell: CellData = new CellData(); constructor( terminal: Terminal, @@ -208,14 +208,14 @@ export class CursorRenderLayer extends BaseRenderLayer { } } - private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { + private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); this._ctx.restore(); } - private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { + private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); @@ -224,14 +224,14 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.restore(); } - private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { + private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); this._ctx.restore(); } - private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void { + private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: CellData): void { this._ctx.save(); this._ctx.strokeStyle = this._themeService.colors.cursor.css; this._strokeRectAtCell(x, y, cell.getWidth(), 1); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index a0b44d2cf8..bcdba5019c 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 { color, rgba } from 'common/Color'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; -import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; @@ -35,7 +34,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, @@ -56,7 +55,7 @@ export class DomRendererRowFactory { const fragment = this._document.createDocumentFragment(); - const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); + //const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); // FIXME // Find the line length first, this prevents the need to output a bunch of // empty cells at the end. This cannot easily be integrated into the main // loop below because of the colCount feature (which can be removed after we @@ -64,6 +63,7 @@ export class DomRendererRowFactory { // the viewport). let lineLength = 0; for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { + // FIXME optimize if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; break; @@ -72,10 +72,12 @@ export class DomRendererRowFactory { const colors = this._themeService.colors; let elemIndex = -1; + let cell = this._workCell; + lineData.scanInit(cell); let x = 0; for (; x < lineLength; x++) { - lineData.loadCell(x, this._workCell); + lineData.scanNext(cell, 1, 0); let width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 @@ -92,7 +94,7 @@ export class DomRendererRowFactory { // 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()!; @@ -111,7 +113,7 @@ export class DomRendererRowFactory { // Recalculate width width = cell.getWidth(); } - + */ const charElement = this._document.createElement('span'); if (width > 1) { charElement.style.width = `${cellWidth * width}px`; diff --git a/src/browser/services/CharacterJoinerService.ts b/src/browser/services/CharacterJoinerService.ts index ca4f1984e3..5fb57846bb 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'; +/* export class JoinedCellData extends AttributeData implements ICellData { private _width: number; // .content carries no meaning for joined CellData, simply nullify it @@ -55,6 +56,7 @@ export class JoinedCellData extends AttributeData implements ICellData { return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; } } +*/ export class CharacterJoinerService implements ICharacterJoinerService { public serviceBrand: undefined; diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 486c19419a..50f63e7964 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -862,7 +862,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) { @@ -878,7 +878,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.ts b/src/common/InputHandler.ts index 9a32ea69ad..ced133ea0f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -510,12 +510,16 @@ export class InputHandler extends Disposable implements IInputHandler { let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; this._dirtyRowTracker.markDirty(this._activeBuffer.y); + const cursor = this._workCell; + bufferRow.scanMove(cursor, this._activeBuffer.x); - // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { - bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char + if (this._activeBuffer.x && end - start > 0) { + bufferRow.fixSplitWide(cursor); } + bufferRow.setAttributes(cursor, curAttr.fg, curAttr.bg, curAttr.extended); + for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -545,13 +549,14 @@ export class InputHandler extends Disposable implements IInputHandler { // since they always follow a cell consuming char // therefore we can test for this._activeBuffer.x to avoid overflow left if (!chWidth && this._activeBuffer.x) { - if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { + /*if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { // found 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 - 2, code); - } else { - bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); + } else*/ { + bufferRow.addToPrecedingGrapheme(cursor, code, chWidth); + //bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); } continue; } @@ -582,6 +587,7 @@ export class InputHandler extends Disposable implements IInputHandler { } // row changed, get it again bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + bufferRow.scanMove(cursor, 0); } else { this._activeBuffer.x = cols - 1; if (chWidth === 2) { @@ -605,31 +611,17 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); - - // fullwidth char - also set next cell to placeholder stub and advance cursor - // for graphemes bigger than fullwidth we can simply loop to zero - // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right - if (chWidth > 0) { - while (--chWidth) { - // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); - } - } + bufferRow.setCodePoint(cursor, code, chWidth); + this._activeBuffer.x += chWidth; + bufferRow.scanNext(cursor, chWidth, 0); } // store last char in Parser.precedingCodepoint for REP to work correctly // This needs to check whether: // - fullwidth + surrogates: reset // - combining: only base char gets carried on (bug in xterm?) if (end - start > 0) { - bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell); - if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { - this._parser.precedingCodepoint = 0; - } else if (this._workCell.isCombined()) { - this._parser.precedingCodepoint = this._workCell.getChars().charCodeAt(0); - } else { - this._parser.precedingCodepoint = this._workCell.content; - } + const ch = bufferRow.previousCodePoint(cursor); + this._parser.precedingCodepoint = ch < 0 || ch > 0xffff ? 0 : ch; } // handle wide chars: reset cell to the right if it is second cell of a wide char @@ -1138,7 +1130,6 @@ export class InputHandler extends Disposable implements IInputHandler { start, end, this._activeBuffer.getNullCell(this._eraseAttrData()), - this._eraseAttrData(), respectProtect ); if (clearWrap) { @@ -1553,8 +1544,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.replaceCells( this._activeBuffer.x, this._activeBuffer.x + (params.params[0] || 1), - this._activeBuffer.getNullCell(this._eraseAttrData()), - this._eraseAttrData() + this._activeBuffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowTracker.markDirty(this._activeBuffer.y); } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 70143525fc..e4522d2b40 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -167,6 +167,10 @@ export interface IAttributeData { isProtected(): number; isOverline(): number; + getFg(): number; // 26 bits including CM_MASK + getBg(): number; // 26 bits including CM_MASK + getStyleFlags(): number; + /** * 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}, @@ -212,6 +216,7 @@ export interface IAttributeData { /** Cell data */ export interface ICellData extends IAttributeData { content: number; + column: number; // 0-origin; -1 if unknown combinedData: string; isCombined(): number; getWidth(): number; @@ -227,22 +232,45 @@ export interface ICellData extends IAttributeData { export interface IBufferLine { length: number; isWrapped: boolean; + /** + * Initialize cursot to beginning of line. + */ + scanInit(cursor: ICellData): void; + /** + * Scan to n'th next position, filling in character data in cursor. + * FUTURE: Negative n moves backward (left in left-to-right text). + * Special case n==0 is a "peek" operation: Fill in cursor with + * data from the nextcharacter - return -1 if there is no next character. + * The cursor properties are set from the last character over over. + * By default the next positon is the next column, but this may + * be modified by the flags (future: codepoints, graphemes, words, etc). + * Return 0 on sucess, otherwise the number of characters we couldn't move. + * TODO: Define handling of wide characters, + */ + scanNext(cursor: ICellData, n: number, flags: number): number; + scanMove(cursor: ICellData, column: number): void; 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, fg: number, bg: number, eAttrs: IExtendedAttrs): void; - addCodepointToCell(index: number, codePoint: number): void; + setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + setCodePoint(cursor: ICellData, codePoint: number, width: number): void; + addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void; insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; - replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData, respectProtect?: boolean): void; + replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; + replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect?: boolean): void; resize(cols: number, fill: ICellData): boolean; + fixSplitWide(cell: ICellData): void; cleanupMemory(): number; fill(fillCellData: ICellData, respectProtect?: boolean): void; copyFrom(line: IBufferLine): void; clone(): IBufferLine; getTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; + previousCodePoint(cursor: ICellData): number; /* direct access to cell attrs */ getWidth(index: number): number; @@ -253,6 +281,7 @@ export interface IBufferLine { getCodePoint(index: number): number; isCombined(index: number): number; getString(index: number): string; + _getChars(cursor: ICellData): string; } export interface IMarker extends IDisposable { diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index f4d12c2bcc..0e7b303878 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -48,10 +48,17 @@ 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(): number { return ((this.fg & 0xFC000000) >> 24) | ((this.bg & 0xFC000000) >> 16); } + public setStyleFlags(flags: number): 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 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/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 111aae036c..9234084b8e 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -11,9 +11,9 @@ import { AttributeData } from 'common/buffer/AttributeData'; class TestBufferLine extends BufferLine { - public get combined(): {[index: number]: string} { - return this._combined; - } + //public get combined(): {[index: number]: string} { + // return this._combined; + //} public toArray(): CharData[] { const result = []; @@ -289,12 +289,12 @@ describe('BufferLine', function(): void { 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 { @@ -431,6 +431,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); + /* line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); const cell = line.loadCell(0, new CellData()); // chars contains single combining char @@ -438,12 +439,14 @@ describe('BufferLine', function(): void { assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined 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 cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); line.setCell(0, cell); + /* line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 3 chars @@ -451,12 +454,14 @@ describe('BufferLine', function(): void { assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined 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 cell = line .loadCell(0, new CellData()); cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]); line.setCell(0, cell); + /* line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); line.loadCell(0, cell); // chars contains 2 chars @@ -464,6 +469,7 @@ describe('BufferLine', function(): void { assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); + */ }); }); describe('correct fullwidth handling', () => { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index d5f4384455..ed3dbb3a7e 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -5,36 +5,10 @@ import { CharData, IBufferLine, ICellData, IAttributeData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, BgFlags, FgFlags } from 'common/buffer/Constants'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, StyleFlags, BgFlags, FgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; -/** - * buffer memory layout: - * - * | uint32_t | uint32_t | uint32_t | - * | `content` | `FG` | `BG` | - * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | - */ - - -/** typed array slots taken by one cell */ -const CELL_SIZE = 3; - -/** - * Cell member indices. - * - * Direct access: - * `content = data[column * CELL_SIZE + Cell.CONTENT];` - * `fg = data[column * CELL_SIZE + Cell.FG];` - * `bg = data[column * CELL_SIZE + Cell.BG];` - */ -const enum Cell { - CONTENT = 0, - FG = 1, // currently simply holds all known attrs - BG = 2 // currently unused -} - export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); // Work variables to avoid garbage collection @@ -43,182 +17,456 @@ 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 { - protected _data: Uint32Array; - protected _combined: {[index: number]: string} = {}; - protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; - public length: number; +export abstract class AbstractBufferLine implements IBufferLine { + /** Number of logical columns */ + length: number = 0; + isWrapped: boolean = false; - constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { - 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) { - this.setCell(i, cell); - } - this.length = cols; + scanInit(cursor: ICellData): void { + } + + abstract scanNext(cursor: ICellData, n: number, flags: number): number; + abstract deleteCols(cursor: CellData, n: number, bg: number): void; + abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; + abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; + // abstract addCodepointToCell(index: number, codePoint: number): void; + abstract addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void; + abstract previousCodePoint(cell: ICellData): number; + abstract resize(cols: number, fillCellData: ICellData): boolean; + abstract fill(fillCellData: ICellData, respectProtect: boolean): void; + abstract copyFrom(line: BufferLine): void; + abstract clone(): IBufferLine; + abstract translateToString(trimRight: boolean, startCol: number, endCol: number): string; + abstract getTrimmedLength(): number; + abstract _getChars(cursor: CellData): string; + abstract cleanupMemory(): number; + + scanMove(cursor: ICellData, column: number): void { + // FUTURE - optimize to reuse cursor position. + // However, have to deal with invalidation, perhaps using DirtyTracker. + // if (cursor.lineBuffer === this && column >= cursor.column ) ... scanNext(cursor, cursor - cursor.column) + this.scanInit(cursor); + this.scanNext(cursor, column, 0); } /** + * 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. @deprecated + */ + public loadCell(index: number, cell: ICellData): ICellData { + this.scanInit(cell); + this.scanNext(cell, index + 1, 0); + return cell; + } + + replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { + let cursor = new CellData(); + this.scanInit(cursor); + this.scanNext(cursor, start, 0); + this.replaceCols(cursor, end - start, fillCellData, respectProtect); + } + + /** * Get cell data CharData. * @deprecated */ - public get(index: number): CharData { - const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - return [ - this._data[index * CELL_SIZE + Cell.FG], - (content & Content.IS_COMBINED_MASK) - ? this._combined[index] - : (cp) ? stringFromCodePoint(cp) : '', - content >> Content.WIDTH_SHIFT, - (content & Content.IS_COMBINED_MASK) - ? this._combined[index].charCodeAt(this._combined[index].length - 1) - : cp - ]; + get(index: number): CharData { + let cursor = new CellData(); + this.scanInit(cursor); + this.scanNext(cursor, index, 0); + return cursor.getAsCharData(); } - /** + /** * Set cell data from CharData. * @deprecated */ public set(index: number, value: CharData): void { + // ??? + this.setCellFromCodePoint(index, + value[CHAR_DATA_CHAR_INDEX].charCodeAt(0), + value[CHAR_DATA_WIDTH_INDEX], + value[CHAR_DATA_ATTR_INDEX], + 0, new ExtendedAttrs()); + /* this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { - this._combined[index] = value[1]; + //this._combined[index] = value[1]; FIXME this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } else { this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } + */ } /** * primitive getters - * use these when only one value is needed, otherwise use `loadCell` + * @deprecated use these when only one value is needed, otherwise use `loadCell` */ public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + return this.loadCell(index, new CellData()).content >> Content.WIDTH_SHIFT; } - /** Test whether content has width. */ + /** Test whether content has width. @deprecated */ public hasWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + return this.loadCell(index, new CellData()).content & Content.WIDTH_MASK; } - /** Get FG cell component. */ + /** Get FG cell component. @deprecated */ public getFg(index: number): number { - return this._data[index * CELL_SIZE + Cell.FG]; + return this.loadCell(index, new CellData()).fg; } - /** Get BG cell component. */ + /** Get BG cell component. @deprecated */ public getBg(index: number): number { - return this._data[index * CELL_SIZE + Cell.BG]; + return this.loadCell(index, new CellData()).bg; } /** - * Test whether contains any chars. + * 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._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; + return this.loadCell(index, new CellData()).content & Content.HAS_CONTENT_MASK; + } + + public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + const cursor = new CellData(); + this.scanInit(cursor); + this.scanNext(cursor, index, 0); + this.setFromCodePoint(cursor, codePoint, width, fg, bg, eAttrs); + } + + /** + * Set data at `index` to `cell`. + */ + public setCell(index: number, cell: ICellData): void { + this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell.fg, cell.bg, cell.extended); + } + + /** + * Set cell data from input handler. + * Since the input handler see the incoming chars as UTF32 codepoints, + * it gets an optimized access method. + */ + public setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + this.setAttributes(cursor, fg, bg, eAttrs); + this.setCodePoint(cursor, codePoint, width); } + abstract setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + abstract setCodePoint(cursor: ICellData, codePoint: number, width: number): void; + /** - * Get codepoint of the cell. + * 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 { - const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED_MASK) { - return this._combined[index].charCodeAt(this._combined[index].length - 1); - } - return content & Content.CODEPOINT_MASK; + return this.loadCell(index, new CellData()).getCode(); } /** Test whether the cell contains a combined string. */ public isCombined(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK; + return this.loadCell(index, new CellData()).isCombined(); } - /** Returns the string content of the cell. */ + public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + pos %= this.length; + let cursor = new CellData(); + this.scanInit(cursor); + this.scanNext(cursor, pos, 0); + this.deleteCols(cursor, n, eraseAttr ? eraseAttr.bg & 0x3ffffff : 0); + } + + /** Returns the string content of the cell. @deprecated */ public getString(index: number): string { - const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED_MASK) { - return this._combined[index]; - } - if (content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(content & Content.CODEPOINT_MASK); - } - // return empty string for empty cells - return ''; + const cell = new CellData(); + this.loadCell(index, cell); + return cell.getChars(); } - /** Get state of protected flag. */ + /** Get state of protected flag. @deprecated */ public isProtected(index: number): number { - return this._data[index * CELL_SIZE + Cell.BG] & BgFlags.PROTECTED; + return this.loadCell(index, new CellData()).bg & BgFlags.PROTECTED; } - /** - * 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 fixSplitWide(cursor: ICellData): void { /* do nothing */ } + +} + +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 26 bits is StyleFlags + SKIP_COLUMNS = 4, // empty ("null") columns (20 bit count) + + // The following have 20 bits length (in 16-bit code units). + // Narrow vs wide assumes a monospace font. + // Future will support variable-width fonts; then treat all as narrow. + TEXT_w1 = 5, // Text in basic plane, narrow, 1 character per grapheme + TEXT_w2 = 6, // Text in basic plane, wide, 1 character per grapheme + // The following have 20 bits length and 8 bits (high-order) + // Grapheme-Cluster-Break Property Value (of the last codepoint). + CLUSTER_w1 = 7, // single grapheme cluster, narrow (1 column) + CLUSTER_w2 = 8, // single grapheme cluster, wide (2 columns) + //GENERIC_TEXT = 4, // Text has not been checked for wide or clusters MAYBE UNNEEDED +} + +const NULL_DATA_WORD = DataKind.SKIP_COLUMNS << 28; + +export class BufferLine extends AbstractBufferLine implements IBufferLine { + // Each item in _data is a 4-bit DataKind and 28 bits data. + protected _data: Uint32Array; + protected _dataLength: number; // active length of _data array + protected _text: string; + protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; + public length: number; + + /** Index in _data of "current chunk". */ + private static dataIndex(cell: CellData): number { return cell._stateM; } + /** Index in _text of start of string for "current chunk". */ + private static textIndex(cell: CellData): number { return cell._stateN; } + /** The "current position" is this many columns past into the current chunk. + * Normally non-zero except right after initialization or backwards movement. + * I.e. when at the end of a N-column chunk, column offset should be N, + * rather than offset 0 in the next chunk. */ - public loadCell(index: number, cell: ICellData): ICellData { - $startIndex = index * CELL_SIZE; - cell.content = this._data[$startIndex + Cell.CONTENT]; - cell.fg = this._data[$startIndex + Cell.FG]; - cell.bg = this._data[$startIndex + Cell.BG]; - if (cell.content & Content.IS_COMBINED_MASK) { - cell.combinedData = this._combined[index]; + private static columnOffset(cell: CellData): number { return cell._stateA; } + private static setPosition(cell: CellData, idata: number, itext: number, columnOffset: number): void { + cell._stateM = idata; + cell._stateN = itext; + cell._stateA = columnOffset; + } + /** From a Uint23 in _data, extract the DataKind bits. */ + private static wKind(word: number): DataKind { return word >> 28; } + private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.TEXT_w1 && kind <= DataKind.CLUSTER_w2; } + private static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_w2; } + /** From a Uint23 in _data, extract length of string within _text. + * Assumes kind is TEXT_w1, TEXT_w2, CLUSTER_w1, CLUSTER W2. */ + private static wStrLen(word: number): number { return word & 0xfffff; } + private static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } + //private static wSet2(kind: DataKind, start: number, len: number): number { return (kind << 28) | (start & 0x3ffff) | ((len & 0x3dd) << 18); } + + scanInit(cursor: ICellData): void { + const cell = cursor as CellData; + cell.bufferLine = this; + cell.fg = 0; + cell.bg = 0; + BufferLine.setPosition(cell, 0, 0, 0); + } + + public _getChars(cursor: CellData): string { + if (cursor.textStart === cursor.textEnd) + return ''; + return this._text.substring(cursor.textStart, cursor.textEnd); + } + public _setChars(cell: CellData, textStart: number, textEnd: number, width: number): void { + cell.textStart = textStart; + cell.textEnd = textEnd; + cell.content = width << Content.WIDTH_SHIFT; + const numUnits = textEnd - textStart; + if (numUnits === 1 || numUnits === 2) { + const ch = this._text.codePointAt(textStart); + if (ch && (numUnits === 1) === (ch <= 0xffff)) // single-character cluster + cell.content |= ch; + } + } + + constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { + super(); + // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); + //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); + this._data = new Uint32Array(cols); + this._text = ''; + this._dataLength = 0; + this.length = cols; + } + + resizeData(size: number): void { + if (size > this._data.length) { + //buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); + const new_data = new Uint32Array((3 * size) >> 1); + new_data.set(this._data); + this._data = new_data; + } + } + + addEmptyDataElements(position: number, count: number): void { + this.resizeData(this._dataLength + count); + this._data.copyWithin(position + count, position, this._dataLength); + this._dataLength += count; + } + + /** Check invariants. Useful for debugging. */ + _check(): void { + function error(str: string) { + console.log("ERROR: "+str); } - if (cell.bg & BgFlags.HAS_EXTENDED) { - cell.extended = this._extendedAttrs[index]!; + let itext = 0; + let icol = 0; + if (this._dataLength < 0 || this._dataLength > this._data.length) + error("bad _dataLength"); + const incrementText = (wlen: number) => { + itext += wlen; + if (itext > this._text.length) + error("text length too big"); + }; + for (let idata = 0; idata < this._dataLength; idata++) { + const word = this._data[idata]; + const wlen = BufferLine.wStrLen(word); + const kind = BufferLine.wKind(word); + switch (kind) { + case DataKind.FG: + case DataKind.BG: + break; + case DataKind.STYLE_FLAGS: + break; + case DataKind.SKIP_COLUMNS: + break; + case DataKind.CLUSTER_w1: + case DataKind.CLUSTER_w2: + incrementText(wlen); + break; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + incrementText(wlen); + break; + default: + error("invalid _dataKind"); + } } - return cell; + if (itext < this._text.length) + error("text length too small"); } - /** - * Set data at `index` to `cell`. - */ - public setCell(index: number, cell: ICellData): void { - if (cell.content & Content.IS_COMBINED_MASK) { - this._combined[index] = cell.combinedData; + _checkCursor(cell: CellData): void { + function error(str: string) { + console.log("ERROR: "+str); } - if (cell.bg & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[index] = cell.extended; + let itext = BufferLine.textIndex(cell); + let idata = BufferLine.dataIndex(cell); + if (! (idata >= 0) || idata > this._dataLength) + error("bad data index"); + if (! (itext >= 0) || itext > this._text.length) + error("bad text index"); + if (idata < this._dataLength) { + let word = this._data[idata]; + let kind = BufferLine.wKind(word); + let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; + const wlen = BufferLine.wStrLen(word); + let colOffset = BufferLine.columnOffset(cell); + if (BufferLine.wKindIsTextOrSkip(kind)) { + if (colOffset > wwidth * wlen) + error("bad columnOffset"); + } else { + error("cursor points to style word"); + } } - this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; - this._data[index * CELL_SIZE + Cell.FG] = cell.fg; - this._data[index * CELL_SIZE + Cell.BG] = cell.bg; } /** - * Set cell data from input handler. - * Since the input handler see the incoming chars as UTF32 codepoints, - * it gets an optimized access method. + * Get cell data CharData. + * @deprecated */ - public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { - if (bg & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[index] = eAttrs; + public get(index: number): CharData { + return this.loadCell(index, new CellData()).getAsCharData(); + } + + public setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + const cell = cursor as CellData; + this.fixSplitWide(cell); + let fg_flags = fg & 0xFC000000; + let bg_flags = bg & 0xFC000000; + let style_flags = (fg_flags >> 24) | (bg_flags >> 16); + fg -= fg_flags; + bg -= bg_flags; + const needFg = fg !== cell.getFg(); + const needBg = bg !== cell.getBg(); + let oldExt = cell.hasExtendedAttrs() && cell.extended; + let newExt = (style_flags & StyleFlags.HAS_EXTENDED) && eAttrs; + const needStyle = style_flags !== cell.getStyleFlags() || oldExt !== newExt; + let add = (needBg?1:0) + (needFg?1:0) + (needStyle?1:0); + let idata = BufferLine.dataIndex(cell); + + if (add) { + this.splitWord(cell, add); + idata = BufferLine.dataIndex(cell); + if (needFg) { + this._data[idata++] = BufferLine.wSet1(DataKind.FG, fg); + cell.fg = fg; + } + if (needBg) { + this._data[idata++] = BufferLine.wSet1(DataKind.BG, bg); + cell.bg = bg; + } + if (needStyle) { + if (style_flags & StyleFlags.HAS_EXTENDED) + this._extendedAttrs[idata] = eAttrs; + this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, style_flags); + cell.setStyleFlags(style_flags); + } + let itext = BufferLine.textIndex(cell); + BufferLine.setPosition(cell, idata, itext, 0); } - this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); - this._data[index * CELL_SIZE + Cell.FG] = fg; - this._data[index * CELL_SIZE + Cell.BG] = bg; + } + + public setCodePoint(cursor: ICellData, codePoint: number, width: number): void { + const cell = cursor as CellData; + + let idata = BufferLine.dataIndex(cell); + let itext = BufferLine.textIndex(cell); + let word = idata < this._dataLength ? this._data[idata] : NULL_DATA_WORD; + let kind = BufferLine.wKind(word); + const wlen = BufferLine.wStrLen(word); + let colOffset = BufferLine.columnOffset(cell); + const appending = width * colOffset >= wlen; + codePoint &= Content.HAS_CONTENT_MASK; + if (codePoint === 0) { + if (kind === DataKind.SKIP_COLUMNS && colOffset < wlen) { + } else if (idata === this._dataLength) { + } else if (kind === DataKind.SKIP_COLUMNS && colOffset === wlen) { + this.deleteCols(cell, 1, -1/*???*/); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen + 1); + } else { + this.deleteCols(cell, width, -1/*???*/); + this.addEmptyDataElements(idata, 1); + this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); + BufferLine.setPosition(cell, idata, itext, 0); + } + return; + } + + const newGlyph = stringFromCodePoint(codePoint); + if ((kind === DataKind.TEXT_w1 && width === 1) + || (kind === DataKind.TEXT_w2 && width === 2)) { + if (appending) { + this.deleteCols(cell, width, -1/*???*/); + this._data[idata] = BufferLine.wSet1(kind, wlen+1); + } + const charOffset = itext + (width == 2 ? colOffset >> 1 : colOffset); + this._text = this._text.substring(0, charOffset) + + newGlyph + + (appending ? '' : this._text.substring(charOffset + 1)); + } else { + this.deleteCols(cell, width, -1/*???*/); + if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { + itext += wlen; + this.splitWord(cell, 1); + idata = BufferLine.dataIndex(cell); + } else { + this.addEmptyDataElements(idata, 1); + } + kind = width == 2 ? DataKind.TEXT_w2 + : DataKind.TEXT_w1; + this._data[idata] = BufferLine.wSet1(kind, newGlyph.length); + this._text = this._text.substring(0, itext) + + newGlyph + + (appending ? '' : this._text.substring(itext)); + colOffset = 0; + } + BufferLine.setPosition(cell, idata, itext, colOffset); } /** @@ -227,7 +475,9 @@ export class BufferLine implements IBufferLine { * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addCodepointToCell(index: number, codePoint: number): void { + public addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void { + alert("addToPrecedingGrapheme"); + /* let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED_MASK) { // we already have a combined string, simply add @@ -247,9 +497,34 @@ export class BufferLine implements IBufferLine { } this._data[index * CELL_SIZE + Cell.CONTENT] = content; } + */ + } + + // MERGE INTO addToPrecedingGrapheme + public setCluster(cursor: ICellData, addToPrevious: boolean, newText: string, width: number): void { + const cell = cursor as CellData; + const kind = width === 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1; + this.deleteCols(cell, width, -1/*???*/); + this.splitWord(cell, addToPrevious ? 0 : 1); + let idata = BufferLine.dataIndex(cell); + let itext = BufferLine.textIndex(cell); + if (addToPrevious && idata < this._data.length) { + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + if (kind === DataKind.CLUSTER_w1 || kind === DataKind.CLUSTER_w2) { + this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newText.length); + } else if (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2){ + } + // FIXME + } else { + this._data[idata] = BufferLine.wSet1(kind, newText.length); + } + this._text = this._text.substring(0, itext) + newText + this._text.substring(itext); } public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + alert("insertCells"); + /* pos %= this.length; // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char @@ -275,36 +550,64 @@ export class BufferLine implements IBufferLine { if (this.getWidth(this.length - 1) === 2) { this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); } + */ } - public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { - pos %= this.length; - if (n < this.length - pos) { - const cell = new CellData(); - for (let i = 0; i < this.length - pos - n; ++i) { - this.setCell(pos + i, this.loadCell(pos + n + i, cell)); - } - for (let i = this.length - n; i < this.length; ++i) { - this.setCell(i, fillCellData); + /** Split the the word referenced by 'cell' at the current colOffset. + * Assumes wKindIsTextOrSkip(kind). + */ + splitWord(cell: CellData, extraWordsToAdd: number): void { + let idata = BufferLine.dataIndex(cell); + let itext = BufferLine.textIndex(cell); + const colOffset = BufferLine.columnOffset(cell); + let add = extraWordsToAdd; + if (colOffset === 0) { + if (extraWordsToAdd) { + this.addEmptyDataElements(idata, add); + BufferLine.setPosition(cell, idata, itext, colOffset); } + } else if (idata === this._dataLength) { + this.addEmptyDataElements(idata, add + 1); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, colOffset); + BufferLine.setPosition(cell, idata + 1, itext, 0); } else { - for (let i = pos; i < this.length; ++i) { - this.setCell(i, fillCellData); + const word = this._data[idata]; + const wlen = BufferLine.wStrLen(word); + if (colOffset === wlen) { + if (extraWordsToAdd) { + this.addEmptyDataElements(idata + 1, add); + } + idata++; + itext += colOffset; + } else { + const kind = BufferLine.wKind(word); + this._data[idata] = BufferLine.wSet1(kind, colOffset); + if (kind !== DataKind.SKIP_COLUMNS) { + itext += colOffset; + } + idata++; + this.addEmptyDataElements(idata, add + 1); + this._data[idata+add] = BufferLine.wSet1(kind, wlen - colOffset); } - } - - // handle fullwidth at pos: - // - reset pos-1 if wide char - // - reset pos if width==0 (previous second cell of a wide char) - if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); - } - if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { - this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + BufferLine.setPosition(cell, idata, itext, 0); } } - public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData, respectProtect: boolean = false): void { + replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect: boolean = false): void { + this.fixSplitWide(cursor); + const code = fill.getCode(); + const width = fill.getWidth(); + if (count <= 0) + return; + this.setAttributes(cursor, fill.fg, fill.bg, fill.extended); + for (;;) { + // FIXME check protected + this.setCodePoint(cursor, code, width); + if (--count <= 0) + break; + this.scanNext(cursor, 1, 0); + } + /* // 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)) { @@ -334,6 +637,290 @@ export class BufferLine implements IBufferLine { while (start < end && start < this.length) { this.setCell(start++, fillCellData); } + */ + } + + + + /** + * Move cursor forward the specified number of columns. + */ + scanNext(cell: ICellData, n: number = 1, flags: number = 0): number { + const cursor = cell as CellData; + let idata = BufferLine.dataIndex(cursor); + let itext = BufferLine.textIndex(cursor); + let col = BufferLine.columnOffset(cursor); + let todo = n; + /* + if (inWide) { + todo++; + inWide = false; + } + */ + // while (todo > 0) { + // if (idata >= this._dataLength) + // break; + while (idata < this._dataLength) { + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + const wlen = BufferLine.wStrLen(word); + let w; + switch (kind) { + case DataKind.CLUSTER_w1: + case DataKind.CLUSTER_w2: + w = kind + 1 - DataKind.CLUSTER_w1; + if (col + todo > w) { + todo -= w; + col = 0; + idata++; + itext += wlen; + } else { + if (col === 0) + this._setChars(cursor, itext, itext+wlen, w); + else + this._setChars(cursor, 0, 0, 0); + BufferLine.setPosition(cursor, idata, itext, col + todo); + return 0; + } + break; + case DataKind.FG: + cursor.fg = (word & 0x3ffffff) | (cursor.fg & 0xfc000000); + idata++; + break; + case DataKind.BG: + cursor.bg = (word & 0x3ffffff) | (cursor.bg & 0xfc000000); + idata++; + break; + case DataKind.STYLE_FLAGS: + // FIXME + idata++; + break; + case DataKind.SKIP_COLUMNS: + if (col + todo > wlen) { + todo -= wlen - col; + col = 0; + idata++; + } else { + this._setChars(cursor, 0, 0, 1); + BufferLine.setPosition(cursor, idata, itext, col + todo); + return 0; + } + break; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + w = kind + 1 - DataKind.TEXT_w1; // 1, or 2 if wide characters + if (col + todo > w * wlen) { + todo -= w * wlen - col; + idata++; + itext += wlen; + col = 0; + } else { + const wshift = w > 1 ? 1 : 0; + const start = itext + (col + todo - 1) >> wshift; + if (w > 1 && (col & 1) !== 0) + this._setChars(cursor, 0, 0, 0); + else + this._setChars(cursor, start, start + 1, w); + BufferLine.setPosition(cursor, idata, itext, col + todo); + return 0; + } + } + } + this._setChars(cursor, 0, 0, 1); + BufferLine.setPosition(cursor, idata, itext, todo); + return todo; + } + + public previousCodePoint(cell: ICellData): number { + const cursor = cell as CellData; + const colOffset = BufferLine.columnOffset(cursor); + let idata = BufferLine.dataIndex(cursor); + let itext = BufferLine.textIndex(cursor); + if (idata >= this._data.length) + return -1; + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + const width = (kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2) ? 2 : 1; + const wlen = BufferLine.wStrLen(word); + switch (kind) { + case DataKind.CLUSTER_w1: + case DataKind.CLUSTER_w2: + return colOffset !== width && this._text.codePointAt(itext) || -1; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + const charOffset = width == 2 ? colOffset >> 1 : colOffset; + return colOffset == 0 || (width === 2 && (colOffset & 1) != 0) ? -1 + : this._text.codePointAt(itext + charOffset) || -1; + default: + return -1; + } + } + + public deleteCols(cursor: CellData, n: number, bg: number): void { + this.fixSplitWide(cursor); + let todo = n; + /* + const save_stateA = cursor._stateA; + const save_stateB = cursor._stateB; + const save_stateM = cursor._stateM; + const save_stateN = cursor._stateN; + this.scanNext(cursor, n, 0); + this.fixSplitWide(cursor); + */ + + let idata = BufferLine.dataIndex(cursor); + let itext = BufferLine.textIndex(cursor); + let colOffset = BufferLine.columnOffset(cursor); + let word0 = this._data[idata]; + const wlen = BufferLine.wStrLen(word0); + if (colOffset === 0) { + while (idata > 0) { + const prevKind = BufferLine.wKind(this._data[idata-1]); + if (prevKind === DataKind.FG || prevKind === DataKind.BG + || prevKind === DataKind.STYLE_FLAGS) + idata--; + else + break; + } + } + let fgValue = 0; + let bgValue = 0; + let styleValue = 0; + let dskip_first = -1, dskip_last = 0, tskip_first = -1, tskip_last = -1, w; + + for (; todo > 0 && idata < this._dataLength; idata++) { + let word = this._data[idata]; + const kind = BufferLine.wKind(word); + let wlen = BufferLine.wStrLen(word); + switch (kind) { + case DataKind.FG: fgValue = word; break; + case DataKind.BG: bgValue = word; break; + case DataKind.STYLE_FLAGS: + styleValue = word; + // handle ExtendedAttrs FIXME + break; + case DataKind.SKIP_COLUMNS: + if (colOffset === 0 && wlen <= todo) { + dskip_first = dskip_first < 0 ? idata : dskip_first; + dskip_last = idata; + todo -= wlen; + } else { + let delta = Math.min(todo, wlen - colOffset); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); + todo -= delta; + } + colOffset = 0; + break; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + w = kind - DataKind.TEXT_w1; // 0, or 1 if wide characters + if (colOffset === 0 && (wlen << w) <= todo) { + dskip_first = dskip_first < 0 ? idata : dskip_first; + dskip_last = idata; + if (tskip_first < 0) + tskip_first = itext; + tskip_last = itext + wlen; + todo -= wlen << w; + } else { + const tstart = (colOffset >> w); + const tend = Math.min((colOffset + todo) >> w, wlen); + if (tskip_first < 0) + tskip_first = itext + tstart; + tskip_last = itext + tend; + const delta = tend - tstart; + this._data[idata] = BufferLine.wSet1(kind, wlen - delta); + todo -= delta << w; + } + colOffset = 0; + break; + case DataKind.CLUSTER_w1: + case DataKind.CLUSTER_w2: + w = kind - DataKind.CLUSTER_w1; // 0, or 1 if wide characters + if (colOffset === 0 && wlen << w <= todo) { + dskip_first = dskip_first < 0 ? idata : dskip_first; + dskip_last = idata; + if (tskip_first < 0) + tskip_first = itext; + tskip_last = itext + wlen; + } else { + // FIXME + wlen = Math.min((colOffset + todo) >> w, wlen); + if (tskip_first < 0) + tskip_first = itext + (colOffset >> w); + tskip_last = itext + wlen; + this._data[idata] = BufferLine.wSet1(kind, wlen); + } + todo -= wlen << w; + colOffset = 0; + break; + } + } + if (tskip_first >= 0) { + this._text = this._text.substring(0, tskip_first) + + this._text.substring(tskip_last); + } + if (dskip_first >= 0) { + this._data.copyWithin(dskip_first, dskip_last + 1, this._dataLength); + this._dataLength -= dskip_last + 1 - dskip_first; + } + if (n === -1) { + // deleted an extra column because we ended up inside a wide char + // FIXME insert a SKIP_COLUMNS to compensate + } + if (bg >= 0) { + // FIXME + } + /* + // handle fullwidth at pos: + // - reset pos-1 if wide char + // - reset pos if width==0 (previous second cell of a wide char) + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + } + */ + } + + /** Fix if cursor is in the middle of a wide character or glyph. + * Replace wide glyph by SKIP_COLUMNS. + */ + public fixSplitWide(cell: ICellData): void { + const cursor = cell as CellData; + const colOffset = BufferLine.columnOffset(cursor); + if ((colOffset & 1) === 0) + return; + let idata = BufferLine.dataIndex(cursor); + const itext = BufferLine.textIndex(cursor); + const word = this._data[idata]; + const wkind = BufferLine.wKind(word); + // replace wide character by SKIP_COLUMNS for 2 columns + if (wkind === DataKind.TEXT_w2) { + const wlen = BufferLine.wStrLen(word); + const beforeLen = colOffset >> 1; + const afterLen = wlen - beforeLen - 1; + const expand = (beforeLen > 0 ? 1 : 0) + (afterLen > 0 ? 1 : 0); + if (expand > 0) + this.addEmptyDataElements(idata, expand); + if (beforeLen > 0) { + this._data[idata++] = BufferLine.wSet1(DataKind.TEXT_w2, beforeLen); + } + this._data[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + if (afterLen > 0) { + this._data[idata] = BufferLine.wSet1(DataKind.TEXT_w2, afterLen); + } + this._text = this._text.substring(0, beforeLen) + + this._text.substring(beforeLen + 1) + BufferLine.setPosition(cursor, idata - 1, beforeLen, 1); + } else if (wkind === DataKind.CLUSTER_w2) { + const wlen = BufferLine.wStrLen(word); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + this._text = this._text.substring(0, itext) + + this._text.substring(itext + wlen); + } + // Ideally, if _data[idata-1] or _data[idata+1] is also SKIP_COLUMNS + // we should merge the SKIP_COLUMNS ("normalize"). } /** @@ -344,6 +931,7 @@ export class BufferLine implements IBufferLine { * excess memory (true after shrinking > CLEANUP_THRESHOLD). */ public resize(cols: number, fillCellData: ICellData): boolean { + /* if (cols === this.length) { return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; } @@ -364,6 +952,7 @@ export class BufferLine implements IBufferLine { } 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++) { @@ -379,10 +968,12 @@ export class BufferLine implements IBufferLine { if (key >= cols) { delete this._extendedAttrs[key]; } - } + } + * / } + */ this.length = cols; - return uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; + return this._dataLength * CLEANUP_THRESHOLD < this._data.length; } /** @@ -392,7 +983,7 @@ export class BufferLine implements IBufferLine { * Returns 0 or 1 indicating whether a cleanup happened. */ public cleanupMemory(): number { - if (this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) { + if (this._dataLength * CLEANUP_THRESHOLD < this._data.length) { const data = new Uint32Array(this._data.length); data.set(this._data); this._data = data; @@ -412,9 +1003,7 @@ export class BufferLine implements IBufferLine { } return; } - this._combined = {}; - this._extendedAttrs = {}; - for (let i = 0; i < this.length; ++i) { + for (let i = 0; i < this.length; ++i) { // FIXME OPTIMIZE this.setCell(i, fillCellData); } } @@ -427,11 +1016,9 @@ export class BufferLine implements IBufferLine { // use high speed copy if lengths are equal this._data.set(line._data); } + this._dataLength = line._dataLength; + this._text = line._text; this.length = line.length; - this._combined = {}; - for (const el in line._combined) { - this._combined[el] = line._combined[el]; - } this._extendedAttrs = {}; for (const el in line._extendedAttrs) { this._extendedAttrs[el] = line._extendedAttrs[el]; @@ -443,27 +1030,28 @@ export class BufferLine implements IBufferLine { public clone(): IBufferLine { const newLine = new BufferLine(0); newLine._data = new Uint32Array(this._data); + newLine._text = this._text; newLine.length = this.length; - for (const el in this._combined) { - newLine._combined[el] = this._combined[el]; - } - for (const el in this._extendedAttrs) { - newLine._extendedAttrs[el] = this._extendedAttrs[el]; - } newLine.isWrapped = this.isWrapped; return newLine; } public getTrimmedLength(): number { + return this._text.length; // FIXME + /* for (let i = this.length - 1; i >= 0; --i) { if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } return 0; + */ } 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. + console.log('NOT IMPLEMENTED copyCellsFrom'); + /* const srcData = src._data; if (applyInReverse) { for (let cell = length - 1; cell >= 0; cell--) { @@ -493,9 +1081,12 @@ export class BufferLine implements IBufferLine { this._combined[key - srcCol + destCol] = src._combined[key]; } } + */ } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { + return this._text; // FIXME + /* if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); } @@ -507,5 +1098,6 @@ export class BufferLine implements IBufferLine { startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 } return result; + */ } } diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 9454c553cf..e3cbd4fc83 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CharData, ICellData, IExtendedAttrs } from 'common/Types'; +import { CharData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; @@ -18,7 +18,25 @@ export class CellData extends AttributeData implements ICellData { obj.setFromCharData(value); return obj; } + public bufferLine: IBufferLine | undefined; + + /** Position and state in BufferLine. + * The actual meaning of _stateA/_stateB/_stateM/_stateN depends on + * on the actual class that implements bufferLine. + * See the BufferLine class for the "default" implementation. + * We use these place-holder fields in order to not have to allocate a + * a specfic CellData object depending on the bufferLine class. + */ + public _stateA: any; + public _stateB: any; + public _stateM: number = 0; + public _stateN: number = 0; + + public textStart: number = 0; + public textEnd: number = 0; + /** Primitives from terminal buffer. */ + public column = -1; public content = 0; public fg = 0; public bg = 0; @@ -34,14 +52,11 @@ export class CellData extends AttributeData implements ICellData { } /** JS string of the content. */ public getChars(): string { - if (this.content & Content.IS_COMBINED_MASK) { - return this.combinedData; - } - if (this.content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); - } - return ''; + if (this.textStart === this.textEnd || this.bufferLine === undefined) + return ''; + return this.bufferLine._getChars(this); } + /** * Codepoint of cell * Note this returns the UTF32 codepoint of single chars, diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index f6a31be7b9..53b64594b3 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -98,6 +98,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, @@ -109,27 +110,41 @@ export const enum Attributes { RGB_MASK = 0xFFFFFF } +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 { /** * 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 { /** * 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 { From 8df267fc48f2e138439c12bd769273ad17243d8e Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 18 Jul 2023 10:17:40 -0700 Subject: [PATCH 02/73] Improve new BuferLine implementation. Fix some bugs. Add simple grapheme support. --- .../renderer/dom/DomRendererRowFactory.ts | 14 +- src/common/InputHandler.ts | 33 +-- src/common/Types.d.ts | 4 +- src/common/buffer/BufferLine.ts | 202 ++++++++++++------ src/common/buffer/Constants.ts | 2 + 5 files changed, 162 insertions(+), 93 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index bcdba5019c..bda099bef8 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -54,25 +54,27 @@ export class DomRendererRowFactory { // otherwise the mouse hover logic might mark the wrong elements as underlined. const fragment = this._document.createDocumentFragment(); + let cell = this._workCell; //const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); // FIXME + // Find the line length first, this prevents the need to output a bunch of // empty cells at the end. This cannot easily be integrated into the main // loop below because of the colCount feature (which can be removed after we // properly support reflow and disallow data to go beyond the right-side of // the viewport). - let lineLength = 0; - for (let x = Math.min(lineData.length, cols) - 1; x >= 0; x--) { - // FIXME optimize - if (lineData.loadCell(x, this._workCell).getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { + let lineLength = 0; // FIXME optimize! + lineData.scanInit(cell); + let rawLength = Math.min(lineData.length, cols); + for (let x = 0; x < rawLength; x++) { + lineData.scanNext(cell, 1, 0); + if (cell.getCode() !== NULL_CELL_CODE || (isCursorRow && x === cursorX)) { lineLength = x + 1; - break; } } const colors = this._themeService.colors; let elemIndex = -1; - let cell = this._workCell; lineData.scanInit(cell); let x = 0; diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index ced133ea0f..7e96d9ad2b 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -555,7 +555,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since an empty cell is only set by fullwidth chars bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code); } else*/ { - bufferRow.addToPrecedingGrapheme(cursor, code, chWidth); + bufferRow.addToPrecedingGrapheme(cursor, stringFromCodePoint(code), chWidth); //bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); } continue; @@ -1126,12 +1126,12 @@ export class InputHandler extends Disposable implements IInputHandler { */ 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 - ); + const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); + if (respectProtect) { + line.replaceCells(start, end, fill, respectProtect); + } else { + line.deleteCells(start, end - start, fill); + } if (clearWrap) { line.isWrapped = false; } @@ -1145,7 +1145,12 @@ export class InputHandler extends Disposable implements IInputHandler { private _resetBufferLine(y: number, respectProtect: boolean = false): void { const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); if (line) { - line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect); + const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); + if (respectProtect) { + line.fill(fillData, respectProtect); + } else { + line.deleteCells(0, line.length, fillData); + } this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y); line.isWrapped = false; } @@ -1375,8 +1380,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.deleteCells( this._activeBuffer.x, params.params[0] || 1, - this._activeBuffer.getNullCell(this._eraseAttrData()), - this._eraseAttrData() + this._activeBuffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowTracker.markDirty(this._activeBuffer.y); } @@ -1443,7 +1447,7 @@ 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)!; - line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); @@ -1522,7 +1526,7 @@ 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)!; - line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); @@ -1541,9 +1545,8 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(); const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { - line.replaceCells( - this._activeBuffer.x, - this._activeBuffer.x + (params.params[0] || 1), + line.deleteCells( + this._activeBuffer.x, params.params[0] || 1, this._activeBuffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowTracker.markDirty(this._activeBuffer.y); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index e4522d2b40..868bf2e9a7 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -257,9 +257,9 @@ export interface IBufferLine { setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void; setCodePoint(cursor: ICellData, codePoint: number, width: number): void; - addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void; + addToPrecedingGrapheme(cursor: ICellData, newText: string, width: number): void; insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; - deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void; + deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect?: boolean): void; resize(cols: number, fill: ICellData): boolean; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index ed3dbb3a7e..296ba5faa5 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -5,7 +5,7 @@ import { CharData, IBufferLine, ICellData, IAttributeData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content, StyleFlags, BgFlags, FgFlags } from 'common/buffer/Constants'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, NULL_CELL_WORD, WHITESPACE_CELL_CHAR, Content, StyleFlags, BgFlags, FgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; @@ -30,7 +30,7 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; // abstract addCodepointToCell(index: number, codePoint: number): void; - abstract addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void; + abstract addToPrecedingGrapheme(cursor: ICellData, newText: string, width: number): void; abstract previousCodePoint(cell: ICellData): number; abstract resize(cols: number, fillCellData: ICellData): boolean; abstract fill(fillCellData: ICellData, respectProtect: boolean): void; @@ -104,7 +104,7 @@ export abstract class AbstractBufferLine implements IBufferLine { * @deprecated 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; + return this.loadCell(index, new CellData()).content >>> Content.WIDTH_SHIFT; } /** Test whether content has width. @deprecated */ @@ -150,7 +150,7 @@ export abstract class AbstractBufferLine implements IBufferLine { * Since the input handler see the incoming chars as UTF32 codepoints, * it gets an optimized access method. */ - public setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + public setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { this.setAttributes(cursor, fg, bg, eAttrs); this.setCodePoint(cursor, codePoint, width); } @@ -172,12 +172,12 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).isCombined(); } - public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { pos %= this.length; let cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, pos, 0); - this.deleteCols(cursor, n, eraseAttr ? eraseAttr.bg & 0x3ffffff : 0); + this.deleteCols(cursor, n, fillCellData.bg); } /** Returns the string content of the cell. @deprecated */ @@ -202,9 +202,9 @@ const enum DataKind { // 4 bits STYLE_FLAGS = 3, // lower 26 bits is StyleFlags SKIP_COLUMNS = 4, // empty ("null") columns (20 bit count) - // The following have 20 bits length (in 16-bit code units). + // The following have 20 bits length (number of 16-bit code units). // Narrow vs wide assumes a monospace font. - // Future will support variable-width fonts; then treat all as narrow. + // Future: maybe support variable-width fonts; then treat all as narrow. TEXT_w1 = 5, // Text in basic plane, narrow, 1 character per grapheme TEXT_w2 = 6, // Text in basic plane, wide, 1 character per grapheme // The following have 20 bits length and 8 bits (high-order) @@ -228,25 +228,35 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private static dataIndex(cell: CellData): number { return cell._stateM; } /** Index in _text of start of string for "current chunk". */ private static textIndex(cell: CellData): number { return cell._stateN; } - /** The "current position" is this many columns past into the current chunk. + + /** The "current position" is this many columns into the current chunk. + * If W is the wStrLen of the chunk, then valid values are: + * 0..W (if TEXT_w1); 0..2*W (if TEXT_w2); 0..1 (if CLUSTER_w1); + * 0..2 (if CLUSTER_w2). (Odd values of TEXT_w2 or CLUSTER_w2 are only + * allowed as temporary intermediate positions; except for appending, it is + * an error to try modify *part* of a cluster or a wide character, + * the effect will that the entire cluster or wide character is cleared.) * Normally non-zero except right after initialization or backwards movement. * I.e. when at the end of a N-column chunk, column offset should be N, * rather than offset 0 in the next chunk. */ private static columnOffset(cell: CellData): number { return cell._stateA; } + private static setPosition(cell: CellData, idata: number, itext: number, columnOffset: number): void { cell._stateM = idata; cell._stateN = itext; - cell._stateA = columnOffset; + cell._stateA = columnOffset; // See note at columnOffset } /** From a Uint23 in _data, extract the DataKind bits. */ - private static wKind(word: number): DataKind { return word >> 28; } + private static wKind(word: number): DataKind { return word >>> 28; } private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.TEXT_w1 && kind <= DataKind.CLUSTER_w2; } private static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_w2; } /** From a Uint23 in _data, extract length of string within _text. * Assumes kind is TEXT_w1, TEXT_w2, CLUSTER_w1, CLUSTER W2. */ private static wStrLen(word: number): number { return word & 0xfffff; } - private static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } + private static wSet1(kind: DataKind, value: number): number { + return (kind << 28) | (value & 0x0fffffff); + } //private static wSet2(kind: DataKind, start: number, len: number): number { return (kind << 28) | (start & 0x3ffff) | ((len & 0x3dd) << 18); } scanInit(cursor: ICellData): void { @@ -412,16 +422,17 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } + /** + * Set character following the cursor. + */ public setCodePoint(cursor: ICellData, codePoint: number, width: number): void { const cell = cursor as CellData; - let idata = BufferLine.dataIndex(cell); let itext = BufferLine.textIndex(cell); let word = idata < this._dataLength ? this._data[idata] : NULL_DATA_WORD; let kind = BufferLine.wKind(word); const wlen = BufferLine.wStrLen(word); let colOffset = BufferLine.columnOffset(cell); - const appending = width * colOffset >= wlen; codePoint &= Content.HAS_CONTENT_MASK; if (codePoint === 0) { if (kind === DataKind.SKIP_COLUMNS && colOffset < wlen) { @@ -434,21 +445,24 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.addEmptyDataElements(idata, 1); this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); BufferLine.setPosition(cell, idata, itext, 0); + return; } - return; } const newGlyph = stringFromCodePoint(codePoint); - if ((kind === DataKind.TEXT_w1 && width === 1) - || (kind === DataKind.TEXT_w2 && width === 2)) { - if (appending) { + const newLength = newGlyph.length; + if (newLength === 1 + && ((kind === DataKind.TEXT_w1 && width === 1) + || (kind === DataKind.TEXT_w2 && width === 2 + && (colOffset & 1) === 0))) { + if (width * colOffset >= wlen) { this.deleteCols(cell, width, -1/*???*/); this._data[idata] = BufferLine.wSet1(kind, wlen+1); } const charOffset = itext + (width == 2 ? colOffset >> 1 : colOffset); this._text = this._text.substring(0, charOffset) + newGlyph - + (appending ? '' : this._text.substring(charOffset + 1)); + + this._text.substring(charOffset + 1); } else { this.deleteCols(cell, width, -1/*???*/); if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { @@ -458,25 +472,64 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } else { this.addEmptyDataElements(idata, 1); } - kind = width == 2 ? DataKind.TEXT_w2 - : DataKind.TEXT_w1; - this._data[idata] = BufferLine.wSet1(kind, newGlyph.length); + kind = newLength === 1 + ? (width == 2 ? DataKind.TEXT_w2 : DataKind.TEXT_w1) + : (width == 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1); + this._data[idata] = BufferLine.wSet1(kind, newLength); this._text = this._text.substring(0, itext) + newGlyph - + (appending ? '' : this._text.substring(itext)); + + this._text.substring(itext); colOffset = 0; } BufferLine.setPosition(cell, idata, itext, colOffset); } + // MERGE INTO setCluster /** * Add a codepoint to a cell from input handler. * During input stage combining chars with a width of 0 follow and stack * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addToPrecedingGrapheme(cursor: ICellData, codePoint: number, width: number): void { - alert("addToPrecedingGrapheme"); + public addToPrecedingGrapheme(cell: ICellData, newText: string, width: number): void { + const cursor = cell as CellData; + let itext = BufferLine.textIndex(cursor); + let idata = BufferLine.dataIndex(cursor); + const colOffset = BufferLine.columnOffset(cursor); + if (idata >= this._dataLength || colOffset === 0) { + return; + } + let word = this._data[idata]; + let kind = BufferLine.wKind(word); + let oldWidth = + (kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2) ? 2 : 1; + const newWidth = Math.max(oldWidth, width); + const at = itext + colOffset; + this._text = this._text.substring(0, at) + newText + + this._text.substring(at); + const wlen = BufferLine.wStrLen(word); + const newLength = newText.length; + if (kind === DataKind.CLUSTER_w1 || kind === DataKind.CLUSTER_w2) { + if (colOffset !== oldWidth) { + return; + } + kind = width >= 2 ? DataKind.CLUSTER_w2 : kind; + this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newLength); + } else if (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2) { + if (colOffset !== wlen * oldWidth) { + return; + } + BufferLine.setPosition(cursor, idata, itext, colOffset - oldWidth); + this.splitWord(cursor, 0); + idata = BufferLine.dataIndex(cursor); + itext = BufferLine.textIndex(cursor); + kind = newWidth >= 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1; + word = this._data[idata]; + this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newLength); + } else { + return; + } + BufferLine.setPosition(cursor, idata, itext, newWidth); /* let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED_MASK) { @@ -573,12 +626,16 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } else { const word = this._data[idata]; const wlen = BufferLine.wStrLen(word); - if (colOffset === wlen) { + const kind = BufferLine.wKind(word); + const atEnd = (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2) + ? colOffset === wlen * (kind - DataKind.TEXT_w1 + 1) + : colOffset === (kind - DataKind.CLUSTER_w1 + 1); + if (atEnd) { if (extraWordsToAdd) { this.addEmptyDataElements(idata + 1, add); } idata++; - itext += colOffset; + itext += wlen; } else { const kind = BufferLine.wKind(word); this._data[idata] = BufferLine.wSet1(kind, colOffset); @@ -627,7 +684,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // 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, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || newU733 ExtendedAttrs()); } // 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) { @@ -644,6 +701,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** * Move cursor forward the specified number of columns. + * After, getChars() is the last character whose start edge is traversed. */ scanNext(cell: ICellData, n: number = 1, flags: number = 0): number { const cursor = cell as CellData; @@ -651,15 +709,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let itext = BufferLine.textIndex(cursor); let col = BufferLine.columnOffset(cursor); let todo = n; - /* - if (inWide) { - todo++; - inWide = false; - } - */ - // while (todo > 0) { - // if (idata >= this._dataLength) - // break; while (idata < this._dataLength) { const word = this._data[idata]; const kind = BufferLine.wKind(word); @@ -670,7 +719,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.CLUSTER_w2: w = kind + 1 - DataKind.CLUSTER_w1; if (col + todo > w) { - todo -= w; + todo -= w - col; col = 0; idata++; itext += wlen; @@ -710,18 +759,19 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.TEXT_w2: w = kind + 1 - DataKind.TEXT_w1; // 1, or 2 if wide characters if (col + todo > w * wlen) { - todo -= w * wlen - col; - idata++; - itext += wlen; - col = 0; + todo -= w * wlen - col; + idata++; + itext += wlen; + col = 0; } else { + col += todo; const wshift = w > 1 ? 1 : 0; - const start = itext + (col + todo - 1) >> wshift; + const start = itext + ((col - 1) >> wshift); if (w > 1 && (col & 1) !== 0) - this._setChars(cursor, 0, 0, 0); + this._setChars(cursor, 0, 0, 0); else - this._setChars(cursor, start, start + 1, w); - BufferLine.setPosition(cursor, idata, itext, col + todo); + this._setChars(cursor, start, start + 1, w); + BufferLine.setPosition(cursor, idata, itext, col); return 0; } } @@ -814,43 +864,50 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.TEXT_w1: case DataKind.TEXT_w2: w = kind - DataKind.TEXT_w1; // 0, or 1 if wide characters + const tstart = (colOffset >> w); + const tend = Math.min((colOffset + todo) >> w, wlen); if (colOffset === 0 && (wlen << w) <= todo) { dskip_first = dskip_first < 0 ? idata : dskip_first; dskip_last = idata; - if (tskip_first < 0) - tskip_first = itext; - tskip_last = itext + wlen; todo -= wlen << w; } else { - const tstart = (colOffset >> w); - const tend = Math.min((colOffset + todo) >> w, wlen); - if (tskip_first < 0) - tskip_first = itext + tstart; - tskip_last = itext + tend; const delta = tend - tstart; this._data[idata] = BufferLine.wSet1(kind, wlen - delta); todo -= delta << w; } + if (tskip_first < 0) + tskip_first = itext + tstart; + tskip_last = itext + tend; + itext += wlen; colOffset = 0; break; case DataKind.CLUSTER_w1: case DataKind.CLUSTER_w2: w = kind - DataKind.CLUSTER_w1; // 0, or 1 if wide characters - if (colOffset === 0 && wlen << w <= todo) { + if (colOffset < (1 << w)) { dskip_first = dskip_first < 0 ? idata : dskip_first; dskip_last = idata; if (tskip_first < 0) tskip_first = itext; tskip_last = itext + wlen; + todo -= (1 << w); + } + /* + if (colOffset === 0 && wlen << w <= todo) { + dskip_first = dskip_first < 0 ? idata : dskip_first; + dskip_last = idata; + tskip_last = itext + wlen; } else { - // FIXME + // FIXME - deleting part of grapheme wlen = Math.min((colOffset + todo) >> w, wlen); - if (tskip_first < 0) - tskip_first = itext + (colOffset >> w); tskip_last = itext + wlen; this._data[idata] = BufferLine.wSet1(kind, wlen); } + if (tskip_first < 0) + tskip_first = itext + (colOffset >> w); todo -= wlen << w; + */ + itext += wlen; colOffset = 0; break; } @@ -863,6 +920,22 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data.copyWithin(dskip_first, dskip_last + 1, this._dataLength); this._dataLength -= dskip_last + 1 - dskip_first; } + const deleted = n - todo; + idata = BufferLine.dataIndex(cursor); + if (idata !== this._dataLength && deleted > 0) { + let word0 = this._data[idata]; + //const wlen = BufferLine.wStrLen(word0); + // if kind of idata is SKIP_COLUMN, add deleted + // if kind of idata-1 is SKIP_COLUMN, add deleted, adjust coloffset. + if (BufferLine.wKind(word0) === DataKind.SKIP_COLUMNS) { + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, + BufferLine.wStrLen(word0) + deleted); + } else { + this.splitWord(cursor, 1); + idata = BufferLine.dataIndex(cursor); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, deleted) + } + } if (n === -1) { // deleted an extra column because we ended up inside a wide char // FIXME insert a SKIP_COLUMNS to compensate @@ -994,18 +1067,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** fill a line with fillCharData */ public fill(fillCellData: ICellData, respectProtect: boolean = false): void { - // full branching on respectProtect==true, hopefully getting fast JIT for standard case - if (respectProtect) { - for (let i = 0; i < this.length; ++i) { - if (!this.isProtected(i)) { - this.setCell(i, fillCellData); - } - } - return; - } - for (let i = 0; i < this.length; ++i) { // FIXME OPTIMIZE - this.setCell(i, fillCellData); - } + this.replaceCells(0, this.length, fillCellData, respectProtect); } /** alter to a full copy of line */ diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 53b64594b3..95362978f9 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -73,6 +73,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 From 7f2107aff3f10a476b5e5915ea8197c76790cb01 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 17 Aug 2023 19:45:53 -0700 Subject: [PATCH 03/73] New implementations of translateToString and getTrimmedLength. --- src/common/buffer/BufferLine.ts | 137 +++++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 19 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index b4aa2793e0..b55abfdcc0 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -1100,15 +1100,38 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } public getTrimmedLength(): number { - return this._text.length; // FIXME - /* - for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + let cols = 0; + let skipped = 0; + const text = this._text; + for (let idata = 0; idata < this._dataLength; idata++) { + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + let wlen = BufferLine.wStrLen(word); + const w = kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 2 : 1; + let wcols = 0; + switch (kind) { + case DataKind.FG: + case DataKind.BG: + case DataKind.STYLE_FLAGS: + break; + case DataKind.SKIP_COLUMNS: + skipped += wlen; + break; + case DataKind.CLUSTER_w1: + case DataKind.CLUSTER_w2: + wcols = w; + break; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + wcols = w * wlen; + break; + } + if (wcols) { + cols += skipped + wcols; + skipped = 0; } } - return 0; - */ + return cols; } public getNoBgTrimmedLength(): number { @@ -1160,19 +1183,95 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { - return this._text; // FIXME - /* - if (trimRight) { - endCol = Math.min(endCol, this.getTrimmedLength()); + let s = ''; + let itext = 0; + let col = 0; + let pendingStart = -1; + let pendingLength = 0; + const text = this._text; + function pendingForce(handleSkip = ! trimRight): void { + if (pendingStart >= 0 && pendingLength > 0) { + s += text.substring(pendingStart, pendingStart + pendingLength); + pendingLength = 0; + } else if (handleSkip && pendingLength > 0) { + s += WHITESPACE_CELL_CHAR.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; + } } - let result = ''; - while (startCol < endCol) { - const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; - startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 + function addPendingSkip(length: number): void { + if (pendingStart >= 0) { + pendingForce(); + } + pendingLength += length; } - return result; - */ + for (let idata = 0; idata < this._dataLength && col < endCol; idata++) { + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + let wlen = BufferLine.wStrLen(word); + const wide = kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 1 : 0; + let wcols; + switch (kind) { + case DataKind.FG: + case DataKind.BG: + case DataKind.STYLE_FLAGS: + break; + case DataKind.SKIP_COLUMNS: + 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_w1: + case DataKind.CLUSTER_w2: + wcols = 1 << wide; + if (col >= startCol && col + wcols <= endCol) { + addPendingString(itext, wlen); + } + itext += wlen; + col += wcols; + break; + case DataKind.TEXT_w1: + case DataKind.TEXT_w2: + wcols = wlen << wide; + if (col + wcols > startCol) { + if (col < startCol) { + const skip = (startCol - col) >> wide; + wlen -= skip; + wcols = wlen << wide; + col = startCol; + itext += skip; + } + if (col + wcols > endCol) { + wlen = (endCol - col) >> wide; + } + } + addPendingString(itext, wlen); + itext += wlen; + col += wlen << wide; + break; + } + } + if (! trimRight && col < endCol) { + addPendingSkip(endCol - col); + } + pendingForce(); + return s; } } From d13d98cb90d760417987a0be0262a5e9ffd3f7c7 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 17 Aug 2023 19:56:45 -0700 Subject: [PATCH 04/73] Change some small access functions and tweak documentation+comments. --- src/common/Types.d.ts | 13 +++++++------ src/common/buffer/AttributeData.ts | 8 +++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index bfc82221b1..1c04d65f53 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -9,7 +9,7 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; import { IParams } from 'common/parser/Types'; import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services'; import { IBufferSet } from 'common/buffer/Types'; -import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; +import { Attributes, StyleFlags, UnderlineStyle } from 'common/buffer/Constants'; export interface ICoreTerminal { coreMouseService: ICoreMouseService; @@ -141,12 +141,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; /** @@ -171,7 +171,7 @@ export interface IAttributeData { getFg(): number; // 26 bits including CM_MASK getBg(): number; // 26 bits including CM_MASK - getStyleFlags(): number; + getStyleFlags(): StyleFlags; /** * The color mode of the foreground color which determines how to decode {@link getFgColor}, @@ -218,8 +218,8 @@ export interface IAttributeData { /** Cell data */ export interface ICellData extends IAttributeData { content: number; - column: number; // 0-origin; -1 if unknown - combinedData: string; + //column: number; // 0-origin; -1 if unknown + //combinedData: string; isCombined(): number; getWidth(): number; getChars(): string; @@ -233,6 +233,7 @@ export interface ICellData extends IAttributeData { */ export interface IBufferLine { length: number; + /** If the previous line wrapped (overflows) into the current line. */ isWrapped: boolean; /** * Initialize cursot to beginning of line. diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 0e7b303878..282a3f6f06 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,8 +48,8 @@ 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(): number { return ((this.fg & 0xFC000000) >> 24) | ((this.bg & 0xFC000000) >> 16); } - public setStyleFlags(flags: number): void { + 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); } @@ -59,6 +59,8 @@ export class AttributeData implements IAttributeData { 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; } From ede649ffb79bd1ca13f0f39d5df5445e2bfeff77 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 17 Aug 2023 20:13:48 -0700 Subject: [PATCH 05/73] Add debugging functions _showData and getText. --- src/common/buffer/BufferLine.ts | 52 +++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index b55abfdcc0..8195d4f302 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -310,6 +310,54 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._dataLength += count; } + /** for debugging */ + getText(skipReplace: string = ' '): string { + return this.translateToString(true, 0, this.length, skipReplace); + } + + /* Human-readable display of _data array, for debugging */ + _showData(start = 0, end = this._dataLength) { + let s = '['; + let toffset = 0; + for (let i = 0; i < end; i++) { + const word = this._data[i]; + const kind = BufferLine.wKind(word); + let code: string | number = kind; + const wnum = word & 0xfffffff; + 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'; break; + case DataKind.CLUSTER_w1: code = 'CL1'; break; + case DataKind.CLUSTER_w2: code = 'C2'; break; + case DataKind.TEXT_w1: code = 'T1'; break; + case DataKind.TEXT_w2: code = 'T1'; break; + } + const hasText = BufferLine.wKindIsText(kind); + if (i >= start) { + if (i !== start) { + s += ', '; + } + let value; + if (hasText) { + value = JSON.stringify(this._text.substring(toffset, toffset + wnum)); + } else if (kind === DataKind.BG || kind === DataKind.FG) { + value = (wnum >> 24) + '#' + (wnum & 0xffffff).toString(16); + } else if (kind !== DataKind.SKIP_COLUMNS) { + value = '#' + wnum.toString(16); + } else { + value = wnum.toString(); + } + s += code + ': ' + value; + } + if (hasText) { + toffset += wnum; + } + } + return s + ']'; + } + /** Check invariants. Useful for debugging. */ _check(): void { function error(str: string) { @@ -1182,7 +1230,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { */ } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string { + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, skipReplace: string = WHITESPACE_CELL_CHAR): string { let s = ''; let itext = 0; let col = 0; @@ -1194,7 +1242,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { s += text.substring(pendingStart, pendingStart + pendingLength); pendingLength = 0; } else if (handleSkip && pendingLength > 0) { - s += WHITESPACE_CELL_CHAR.repeat(pendingLength); + s += skipReplace.repeat(pendingLength); pendingLength = 0; } pendingStart = -1; From 7659c9fd8d3c1c3e2ecae8b4baab10b850959018 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 29 Aug 2023 09:01:20 -0700 Subject: [PATCH 06/73] More work on BufferLine re-write. --- .../services/CharacterJoinerService.ts | 3 +- src/common/InputHandler.ts | 27 +- src/common/Types.d.ts | 9 +- src/common/buffer/BufferLine.ts | 270 ++++++++++++------ src/common/buffer/CellData.ts | 40 ++- src/common/buffer/Constants.ts | 4 +- 6 files changed, 239 insertions(+), 114 deletions(-) diff --git a/src/browser/services/CharacterJoinerService.ts b/src/browser/services/CharacterJoinerService.ts index 5fb57846bb..53cc3d48d6 100644 --- a/src/browser/services/CharacterJoinerService.ts +++ b/src/browser/services/CharacterJoinerService.ts @@ -11,7 +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 @@ -56,7 +56,6 @@ export class JoinedCellData extends AttributeData implements ICellData { return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; } } -*/ export class CharacterJoinerService implements ICharacterJoinerService { public serviceBrand: undefined; diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 979120c5c0..e7f3c9d579 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -518,7 +518,7 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.fixSplitWide(cursor); } - bufferRow.setAttributes(cursor, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setAttributes(cursor, curAttr.getFg(), curAttr.getBg(), curAttr.getStyleFlags(), curAttr.extended); for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -613,7 +613,6 @@ export class InputHandler extends Disposable implements IInputHandler { // write current char to buffer and advance cursor bufferRow.setCodePoint(cursor, code, chWidth); this._activeBuffer.x += chWidth; - bufferRow.scanNext(cursor, chWidth, 0); } // store last char in Parser.precedingCodepoint for REP to work correctly // This needs to check whether: @@ -1128,11 +1127,11 @@ export class InputHandler extends Disposable implements IInputHandler { 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)!; const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); - if (respectProtect) { + //if (respectProtect) { line.replaceCells(start, end, fill, respectProtect); - } else { - line.deleteCells(start, end - start, fill); - } + //} else { + // line.deleteCells(start, end - start, fill); + //} if (clearWrap) { line.isWrapped = false; } @@ -1146,11 +1145,11 @@ export class InputHandler extends Disposable implements IInputHandler { private _resetBufferLine(y: number, respectProtect: boolean = false): void { const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); if (line) { - const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); if (respectProtect) { + const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); line.fill(fillData, respectProtect); } else { - line.deleteCells(0, line.length, fillData); + line.eraseAll(this._curAttrData.bg & ~0xFC000000); } this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y); line.isWrapped = false; @@ -1183,12 +1182,15 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean { this._restrictCursor(this._bufferService.cols); - let j; + let j, x; switch (params.params[0]) { case 0: j = this._activeBuffer.y; this._dirtyRowTracker.markDirty(j); - this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); + x = this._activeBuffer.x; + if (x > 0) { + this._eraseInBufferLine(j++, x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); + } for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j, respectProtect); } @@ -1548,8 +1550,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(); const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { - line.deleteCells( - this._activeBuffer.x, params.params[0] || 1, + line.replaceCells( + this._activeBuffer.x, + this._activeBuffer.x + (params.params[0] || 1), this._activeBuffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowTracker.markDirty(this._activeBuffer.y); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 1c04d65f53..207a732e17 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -257,11 +257,15 @@ export interface IBufferLine { loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; - setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void; - setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; + /** + * Add character (codePoint) following cursor position. + * Cursor position is moved to be after added character. + */ setCodePoint(cursor: ICellData, codePoint: number, width: number): void; addToPrecedingGrapheme(cursor: ICellData, newText: string, width: number): void; insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; + eraseAll(bg: number): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect?: boolean): void; @@ -285,7 +289,6 @@ export interface IBufferLine { getCodePoint(index: number): number; isCombined(index: number): number; getString(index: number): string; - _getChars(cursor: ICellData): string; } export interface IMarker extends IDisposable { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 8195d4f302..4cf1781d1d 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -11,6 +11,8 @@ import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); +const EMPTY_DATA = new Uint32Array(0); + // Work variables to avoid garbage collection let $startIndex = 0; @@ -26,7 +28,13 @@ export abstract class AbstractBufferLine implements IBufferLine { } abstract scanNext(cursor: ICellData, n: number, flags: number): number; - abstract deleteCols(cursor: CellData, n: number, bg: number): void; + abstract eraseAll(bg: number): void; + /** + * Delete n colums, sliding following columns "left". + * If endCols >= 0, replace vacated columns (the n columns before endCol) + * with null cells with bg color. + */ + abstract deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; // abstract addCodepointToCell(index: number, codePoint: number): void; @@ -39,7 +47,6 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract translateToString(trimRight: boolean, startCol: number, endCol: number): string; abstract getTrimmedLength(): number; abstract getNoBgTrimmedLength(): number; - abstract _getChars(cursor: CellData): string; abstract cleanupMemory(): number; scanMove(cursor: ICellData, column: number): void { @@ -136,27 +143,23 @@ export abstract class AbstractBufferLine implements IBufferLine { const cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, index, 0); - this.setFromCodePoint(cursor, codePoint, width, fg, bg, eAttrs); + let fg_flags = fg & 0xFC000000; + let bg_flags = bg & 0xFC000000; + let style_flags = (fg_flags >> 24) | (bg_flags >> 16); + fg -= fg_flags; + bg -= bg_flags; + this.setAttributes(cursor, fg, bg, style_flags, eAttrs); + this.setCodePoint(cursor, codePoint, width); } /** * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell.fg, cell.bg, cell.extended); + this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell.fg, cell.bg, cell.extended); } - /** - * Set cell data from input handler. - * Since the input handler see the incoming chars as UTF32 codepoints, - * it gets an optimized access method. - */ - public setFromCodePoint(cursor: ICellData, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { - this.setAttributes(cursor, fg, bg, eAttrs); - this.setCodePoint(cursor, codePoint, width); - } - - abstract setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + abstract setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; abstract setCodePoint(cursor: ICellData, codePoint: number, width: number): void; /** @@ -178,7 +181,7 @@ export abstract class AbstractBufferLine implements IBufferLine { let cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, pos, 0); - this.deleteCols(cursor, n, fillCellData.bg); + this.deleteCols(cursor, n, fillCellData.bg, -1); // FIXME set endCols } /** Returns the string content of the cell. @deprecated */ @@ -222,8 +225,14 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { protected _data: Uint32Array; protected _dataLength: number; // active length of _data array protected _text: string; + // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; public length: number; + /** Used if this a continuation line (WORK-IN-PROGRESS) */ + continuationStart: CellData | undefined; + + /** Color for "rest of line" background, following _dataLength. */ + lineEndBg: number; /** Index in _data of "current chunk". */ private static dataIndex(cell: CellData): number { return cell._stateM; } @@ -262,20 +271,19 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { scanInit(cursor: ICellData): void { const cell = cursor as CellData; - cell.bufferLine = this; - cell.fg = 0; - cell.bg = 0; - BufferLine.setPosition(cell, 0, 0, 0); + if (this.continuationStart) { + cell.copyFrom(this.continuationStart); + } else { + cell.fg = 0; + cell.bg = 0; + BufferLine.setPosition(cell, 0, 0, 0); + } } - public _getChars(cursor: CellData): string { - if (cursor.textStart === cursor.textEnd) - return ''; - return this._text.substring(cursor.textStart, cursor.textEnd); - } public _setChars(cell: CellData, textStart: number, textEnd: number, width: number): void { cell.textStart = textStart; cell.textEnd = textEnd; + cell.textData = this._text; cell.content = width << Content.WIDTH_SHIFT; const numUnits = textEnd - textStart; if (numUnits === 1 || numUnits === 2) { @@ -292,6 +300,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data = new Uint32Array(cols); this._text = ''; this._dataLength = 0; + this.lineEndBg = 0; this.length = cols; } @@ -425,6 +434,22 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } + public atLineEnd(cursor: CellData): boolean { + let idata = BufferLine.dataIndex(cursor); + if (idata === this._dataLength) + return true; + if (idata < this._dataLength - 1) + return false; + const word = this._data[idata]; + const kind = BufferLine.wKind(word); + if (kind <= DataKind.SKIP_COLUMNS) + return true; + const wlen = BufferLine.wStrLen(word); + let colOffset = BufferLine.columnOffset(cursor); + let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; + return colOffset === wlen * wwidth; + } + /** * Get cell data CharData. * @deprecated @@ -433,21 +458,22 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).getAsCharData(); } - public setAttributes(cursor: ICellData, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + public setAttributes(cursor: ICellData, fg: number, bg: number, style_flags: StyleFlags, eAttrs: IExtendedAttrs): void { const cell = cursor as CellData; this.fixSplitWide(cell); - let fg_flags = fg & 0xFC000000; - let bg_flags = bg & 0xFC000000; - let style_flags = (fg_flags >> 24) | (bg_flags >> 16); - fg -= fg_flags; - bg -= bg_flags; - const needFg = fg !== cell.getFg(); - const needBg = bg !== cell.getBg(); + const oldFg = cell.getFg(); + const oldBg = cell.getBg(); + const oldStyle = cell.getStyleFlags(); + const needFg = fg !== oldFg; + const needBg = bg !== oldBg let oldExt = cell.hasExtendedAttrs() && cell.extended; let newExt = (style_flags & StyleFlags.HAS_EXTENDED) && eAttrs; - const needStyle = style_flags !== cell.getStyleFlags() || oldExt !== newExt; - let add = (needBg?1:0) + (needFg?1:0) + (needStyle?1:0); + const needStyle = style_flags !== oldStyle || oldExt !== newExt; let idata = BufferLine.dataIndex(cell); + // FIXME maybe use a (modified?) atLineEnd + const atEnd = idata === this._dataLength; + let add1 = atEnd ? 1 : 2; + let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); if (add) { this.splitWord(cell, add); @@ -466,6 +492,18 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, style_flags); cell.setStyleFlags(style_flags); } + let xdata = idata; + if (! atEnd) { + if (needFg) { + this._data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); + } + if (needStyle) { + this._data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); + } + if (needBg) { + this._data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + } + } let itext = BufferLine.textIndex(cell); BufferLine.setPosition(cell, idata, itext, 0); } @@ -475,6 +513,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { * Set character following the cursor. */ public setCodePoint(cursor: ICellData, codePoint: number, width: number): void { + const moveForwards = true; const cell = cursor as CellData; let idata = BufferLine.dataIndex(cell); let itext = BufferLine.textIndex(cell); @@ -484,18 +523,19 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let colOffset = BufferLine.columnOffset(cell); codePoint &= Content.HAS_CONTENT_MASK; if (codePoint === 0) { - if (kind === DataKind.SKIP_COLUMNS && colOffset < wlen) { + if (kind === DataKind.SKIP_COLUMNS && colOffset + width <= wlen) { } else if (idata === this._dataLength) { } else if (kind === DataKind.SKIP_COLUMNS && colOffset === wlen) { - this.deleteCols(cell, 1, -1/*???*/); + this.deleteCols(cell, 1, cell.getBg(), -1); this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen + 1); } else { - this.deleteCols(cell, width, -1/*???*/); + this.deleteCols(cell, width, cell.getBg(), -1); this.addEmptyDataElements(idata, 1); - this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); - BufferLine.setPosition(cell, idata, itext, 0); - return; + this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, width); + colOffset = 0; } + BufferLine.setPosition(cell, idata, itext, colOffset + width); + return; } const newGlyph = stringFromCodePoint(codePoint); @@ -505,22 +545,25 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { || (kind === DataKind.TEXT_w2 && width === 2 && (colOffset & 1) === 0))) { if (width * colOffset >= wlen) { - this.deleteCols(cell, width, -1/*???*/); + this.deleteCols(cell, width, cell.getBg(), -1); this._data[idata] = BufferLine.wSet1(kind, wlen+1); } const charOffset = itext + (width == 2 ? colOffset >> 1 : colOffset); this._text = this._text.substring(0, charOffset) + newGlyph + this._text.substring(charOffset + 1); + BufferLine.setPosition(cell, idata, itext, colOffset + width); } else { - this.deleteCols(cell, width, -1/*???*/); - if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { - itext += wlen; - this.splitWord(cell, 1); - idata = BufferLine.dataIndex(cell); - } else { - this.addEmptyDataElements(idata, 1); - } + if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { + itext += wlen; + this.splitWord(cell, 1); + idata = BufferLine.dataIndex(cell); + } else if (idata === this._dataLength && colOffset > 0) { + this.splitWord(cell, 1); + idata = BufferLine.dataIndex(cell); + } else { + this.addEmptyDataElements(idata, 1); + } kind = newLength === 1 ? (width == 2 ? DataKind.TEXT_w2 : DataKind.TEXT_w1) : (width == 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1); @@ -529,8 +572,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { + newGlyph + this._text.substring(itext); colOffset = 0; + BufferLine.setPosition(cell, idata, itext, colOffset + width); + this.deleteCols(cell, width, cell.getBg(), -1); } - BufferLine.setPosition(cell, idata, itext, colOffset); } // MERGE INTO setCluster @@ -606,7 +650,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { public setCluster(cursor: ICellData, addToPrevious: boolean, newText: string, width: number): void { const cell = cursor as CellData; const kind = width === 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1; - this.deleteCols(cell, width, -1/*???*/); + this.deleteCols(cell, width, cell.getBg(), -1); this.splitWord(cell, addToPrevious ? 0 : 1); let idata = BufferLine.dataIndex(cell); let itext = BufferLine.textIndex(cell); @@ -676,6 +720,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const word = this._data[idata]; const wlen = BufferLine.wStrLen(word); const kind = BufferLine.wKind(word); + // FIXME use atLineEnd const atEnd = (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2) ? colOffset === wlen * (kind - DataKind.TEXT_w1 + 1) : colOffset === (kind - DataKind.CLUSTER_w1 + 1); @@ -705,13 +750,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const width = fill.getWidth(); if (count <= 0) return; - this.setAttributes(cursor, fill.fg, fill.bg, fill.extended); + this.setAttributes(cursor, fill.getFg(), fill.getBg(), fill.getStyleFlags(), fill.extended); for (;;) { // FIXME check protected this.setCodePoint(cursor, code, width); if (--count <= 0) break; - this.scanNext(cursor, 1, 0); } /* // full branching on respectProtect==true, hopefully getting fast JIT for standard case @@ -782,15 +826,18 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } break; case DataKind.FG: - cursor.fg = (word & 0x3ffffff) | (cursor.fg & 0xfc000000); + cursor.setFg(word); idata++; break; case DataKind.BG: - cursor.bg = (word & 0x3ffffff) | (cursor.bg & 0xfc000000); + cursor.setBg(word); idata++; break; case DataKind.STYLE_FLAGS: - // FIXME + cursor.setStyleFlags(word); + if (word & StyleFlags.HAS_EXTENDED) { + cursor.extended = this._extendedAttrs[idata]!; + } idata++; break; case DataKind.SKIP_COLUMNS: @@ -827,14 +874,15 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } this._setChars(cursor, 0, 0, 1); BufferLine.setPosition(cursor, idata, itext, todo); + cursor.setBg(this.lineEndBg); return todo; } public previousCodePoint(cell: ICellData): number { const cursor = cell as CellData; const colOffset = BufferLine.columnOffset(cursor); - let idata = BufferLine.dataIndex(cursor); - let itext = BufferLine.textIndex(cursor); + const idata = BufferLine.dataIndex(cursor); + const itext = BufferLine.textIndex(cursor); if (idata >= this._data.length) return -1; const word = this._data[idata]; @@ -848,14 +896,22 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.TEXT_w1: case DataKind.TEXT_w2: const charOffset = width == 2 ? colOffset >> 1 : colOffset; - return colOffset == 0 || (width === 2 && (colOffset & 1) != 0) ? -1 + return colOffset === 0 || (width === 2 && (colOffset & 1) !== 0) ? -1 : this._text.codePointAt(itext + charOffset) || -1; default: return -1; } } - public deleteCols(cursor: CellData, n: number, bg: number): void { + public eraseAll(bg: number): void { + // FIXME sometimes better to reuse old _data. + this._data = EMPTY_DATA; + this._dataLength = 0; + this.lineEndBg = bg; + this._text = ''; + } + + public deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void { this.fixSplitWide(cursor); let todo = n; /* @@ -867,26 +923,37 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.fixSplitWide(cursor); */ - let idata = BufferLine.dataIndex(cursor); + let idata0 = BufferLine.dataIndex(cursor); + let idata = idata0; let itext = BufferLine.textIndex(cursor); - let colOffset = BufferLine.columnOffset(cursor); + const colOffset0 = BufferLine.columnOffset(cursor); + let colOffset = colOffset0; let word0 = this._data[idata]; const wlen = BufferLine.wStrLen(word0); + let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; + let fgValue = -1; //cursor.getFg(); + let bgValue = -1; //cursor.getBg(); + let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs + /* if (colOffset === 0) { while (idata > 0) { - const prevKind = BufferLine.wKind(this._data[idata-1]); - if (prevKind === DataKind.FG || prevKind === DataKind.BG - || prevKind === DataKind.STYLE_FLAGS) + let skipItem = true; + switch (BufferLine.wKind(this._data[idata-1])) { + case DataKind.BG: cursor.setBg(-1); break; + case DataKind.FG: cursor.setFg(-1); break; + case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; + default: skipItem = false; + } + if (skipItem) { idata--; - else + dskip_first = idata; + dskip_last = idata0-1; + } else { break; + } } } - let fgValue = 0; - let bgValue = 0; - let styleValue = 0; - let dskip_first = -1, dskip_last = 0, tskip_first = -1, tskip_last = -1, w; - + */ for (; todo > 0 && idata < this._dataLength; idata++) { let word = this._data[idata]; const kind = BufferLine.wKind(word); @@ -900,12 +967,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { break; case DataKind.SKIP_COLUMNS: if (colOffset === 0 && wlen <= todo) { - dskip_first = dskip_first < 0 ? idata : dskip_first; dskip_last = idata; todo -= wlen; } else { let delta = Math.min(todo, wlen - colOffset); this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); + dskip_first = idata + 1; todo -= delta; } colOffset = 0; @@ -916,10 +983,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const tstart = (colOffset >> w); const tend = Math.min((colOffset + todo) >> w, wlen); if (colOffset === 0 && (wlen << w) <= todo) { - dskip_first = dskip_first < 0 ? idata : dskip_first; dskip_last = idata; todo -= wlen << w; } else { + dskip_first = idata + 1; const delta = tend - tstart; this._data[idata] = BufferLine.wSet1(kind, wlen - delta); todo -= delta << w; @@ -934,12 +1001,13 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.CLUSTER_w2: w = kind - DataKind.CLUSTER_w1; // 0, or 1 if wide characters if (colOffset < (1 << w)) { - dskip_first = dskip_first < 0 ? idata : dskip_first; dskip_last = idata; if (tskip_first < 0) tskip_first = itext; tskip_last = itext + wlen; todo -= (1 << w); + } else { + dskip_first = idata + 1; } /* if (colOffset === 0 && wlen << w <= todo) { @@ -965,18 +1033,34 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._text = this._text.substring(0, tskip_first) + this._text.substring(tskip_last); } - if (dskip_first >= 0) { - this._data.copyWithin(dskip_first, dskip_last + 1, this._dataLength); - this._dataLength -= dskip_last + 1 - dskip_first; + //if (dskip_first >= 0) { + idata0 = dskip_first; + if (bgValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); + } + if (fgValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); + } + if (styleValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); + } + if (dskip_last >= 0) { + this._data.copyWithin(idata0, dskip_last + 1, this._dataLength); + this._dataLength -= dskip_last + 1 - idata0; } const deleted = n - todo; - idata = BufferLine.dataIndex(cursor); + idata = idata0; + colOffset = colOffset0; + //idata = BufferLine.dataIndex(cursor); + /* if (idata !== this._dataLength && deleted > 0) { let word0 = this._data[idata]; + let kind0 = BufferLine.wKind(word0); + //const wlen = BufferLine.wStrLen(word0); // if kind of idata is SKIP_COLUMN, add deleted // if kind of idata-1 is SKIP_COLUMN, add deleted, adjust coloffset. - if (BufferLine.wKind(word0) === DataKind.SKIP_COLUMNS) { + if (kind0 === DataKind.SKIP_COLUMNS) { this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, BufferLine.wStrLen(word0) + deleted); } else { @@ -989,10 +1073,32 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // deleted an extra column because we ended up inside a wide char // FIXME insert a SKIP_COLUMNS to compensate } - if (bg >= 0) { - // FIXME + */ + const atEnd = this.atLineEnd(cursor); +// if (! atEnd) { +// this.setAttributes(cursor, fgValue, bgValue, styleValue, cursor.extended/*FIXME*/); + //} else { + // this.lineEndBg = bg; + //this.setAttributes(cursor, cursor.getFg(), bg, cursor.getStyleFlags(), cursor.extended/*FIXME*/); + //} + if (endCol < 0) { + this.lineEndBg = bg; + } else { + // setAttributes(...) FIXME + // save position; move to (endCol - n) + // insert n blanks, with bg set (and fg/style otherwise clear) + // restore position } /* + if (bg !== cursor.getBg()) { + this.splitWord(cursor, 1); + idata = BufferLine.dataIndex(cursor); + this._data[idata] = BufferLine.wSet1(DataKind.BG, bg); + BufferLine.setPosition(cursor, idata + 1, itext, 0); + cursor.bg = (bg & 0x3ffffff) | (cursor.bg & 0xfc000000); + } + */ + /* // handle fullwidth at pos: // - reset pos-1 if wide char // - reset pos if width==0 (previous second cell of a wide char) diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index e3cbd4fc83..86dc39446d 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -18,7 +18,8 @@ export class CellData extends AttributeData implements ICellData { obj.setFromCharData(value); return obj; } - public bufferLine: IBufferLine | undefined; + //public bufferLine: IBufferLine | undefined; + textData: string = ''; /** Position and state in BufferLine. * The actual meaning of _stateA/_stateB/_stateM/_stateN depends on @@ -41,8 +42,23 @@ export class CellData extends AttributeData implements ICellData { public fg = 0; public bg = 0; public extended: IExtendedAttrs = new ExtendedAttrs(); - public combinedData = ''; - /** Whether cell contains a combined string. */ + + public copyFrom(src: CellData) { + //this.bufferLine = src.bufferLine; + this._stateA = src._stateA; + this._stateB = src._stateB; + this._stateN = src._stateM; + this._stateN = src._stateN; + this.textStart = src.textStart; + this.textEnd = src.textEnd; + this.column = src.column; + 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; } @@ -52,9 +68,8 @@ export class CellData extends AttributeData implements ICellData { } /** JS string of the content. */ public getChars(): string { - if (this.textStart === this.textEnd || this.bufferLine === undefined) - return ''; - return this.bufferLine._getChars(this); + return this.textStart === this.textEnd ? '' + : this.textData.substring(this.textStart, this.textEnd); } /** @@ -64,20 +79,19 @@ 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; + return this.content & Content.CODEPOINT_MASK; } /** 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) { - combined = true; + if (length > 2) { + throw new Error('setFromCharData does not allow width > 2'); } - 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 @@ -98,7 +112,7 @@ export class CellData extends AttributeData implements ICellData { this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } if (combined) { - this.combinedData = value[CHAR_DATA_CHAR_INDEX]; + this.textData = value[CHAR_DATA_CHAR_INDEX]; this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 95362978f9..542d1b8257 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -126,7 +126,7 @@ export const enum StyleFlags { OVERLINE = 0x4000 } -export const enum FgFlags { +export const enum FgFlags { // deprecated /** * bit 27..32 */ @@ -138,7 +138,7 @@ export const enum FgFlags { STRIKETHROUGH = StyleFlags.STRIKETHROUGH << 24 // 0x80000000 } -export const enum BgFlags { +export const enum BgFlags { // deprecated /** * bit 27..32 (upper 2 unused) */ From 8de51e922b0d16bc3ef1faecf392a132e1847ddc Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 8 Sep 2023 12:04:43 -0700 Subject: [PATCH 07/73] Add some comments to document IBuffer fields. --- .../out/base64.wasm.js/decode/final.wasm | Bin 654 -> 654 bytes src/common/buffer/Types.d.ts | 9 +++++++++ 2 files changed, 9 insertions(+) diff --git a/addons/xterm-addon-image/inwasm-builds/out/base64.wasm.js/decode/final.wasm b/addons/xterm-addon-image/inwasm-builds/out/base64.wasm.js/decode/final.wasm index c1ac6e8ad7ef12e6efe50ac5a5979513c0932ee6..bc27713ec3e989fff0416f51d3b2bdeff80b3f09 100644 GIT binary patch delta 58 zcmeBU?PJ}*&BSOinTJVRBgHV)%)&G!&BQb%+0@d+B+0}$#mLmeJjKA!*djT_B+)oE N)g(17b#gqDJ^-~H4~+l- delta 58 zcmeBU?PJ}*&BSOvnTJVRBh5I;%*4XNJSEl4(7?zb*}&4oz$7UxHPzBQ$tW!`CB@hv NCC$(vadJG9J^-;*4|xCp diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index 78f01d55d0..ad5c5704b6 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -11,8 +11,17 @@ 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. + * 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. + */ y: number; x: number; tabs: any; From 3b516f6f3299fe2e59207c6447a078a329af5b7a Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 27 Sep 2023 12:04:21 -0700 Subject: [PATCH 08/73] Start re-vamping BufferLine re-implementation - Store character values in _data array. Get rid of _text string. (In progress.) This is likely to be faster. - Add a cache in BufferLine for the current position. (In progress.) - Re-implement loadCell to use above cache. - Revert DOM renderer to use laodCell. (webgl and canvas renderer s still to do.) --- .../renderer/dom/DomRendererRowFactory.ts | 6 +- src/common/InputHandler.ts | 2 +- src/common/Types.d.ts | 3 +- src/common/buffer/BufferLine.ts | 532 ++++++++++-------- src/common/buffer/CellData.ts | 12 +- src/common/input/TextDecoder.ts | 1 + 6 files changed, 301 insertions(+), 255 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index d7accdafb4..5c3e7e315d 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -81,7 +81,6 @@ export class DomRendererRowFactory { lineLength = cursorX + 1; } - lineData.scanInit(cell); let charElement: HTMLSpanElement | undefined; let cellAmount = 0; let text = ''; @@ -96,9 +95,8 @@ export class DomRendererRowFactory { const hasHover = linkStart !== -1 && linkEnd !== -1; - let x = 0; - for (; x < lineLength; x++) { - lineData.scanNext(cell, 1, 0); + for (let x = 0; x < lineLength; x++) { + lineData.loadCell(x, this._workCell); let width = this._workCell.getWidth(); // The character to the left is a wide character, drawing is owned by the char at x-1 diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index cb6f413c42..a0657f475e 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -560,7 +560,7 @@ export class InputHandler extends Disposable implements IInputHandler { // since an empty cell is only set by fullwidth chars bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code); } else*/ { - bufferRow.addToPrecedingGrapheme(cursor, stringFromCodePoint(code), chWidth); + bufferRow.addToPrecedingGrapheme(cursor, code, chWidth); //bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); this._activeBuffer.x += chWidth - oldWidth; } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 3c3158b5cc..1c375599f0 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -263,7 +263,7 @@ export interface IBufferLine { * Cursor position is moved to be after added character. */ setCodePoint(cursor: ICellData, codePoint: number, width: number): void; - addToPrecedingGrapheme(cursor: ICellData, newText: string, width: number): void; + addToPrecedingGrapheme(cursor: ICellData, newCode: number, width: number): void; insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; eraseAll(bg: number): void; deleteCells(pos: number, n: number, fill: ICellData): void; @@ -278,7 +278,6 @@ export interface IBufferLine { getTrimmedLength(): number; getNoBgTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string; - previousCodePoint(cursor: ICellData): number; /* direct access to cell attrs */ getWidth(index: number): number; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 5cbdf1001e..1ff45a7f28 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -7,7 +7,7 @@ import { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from import { AttributeData, ExtendedAttrs } 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, StyleFlags, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; -import { stringFromCodePoint } from 'common/input/TextDecoder'; +import { stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); @@ -37,9 +37,8 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; - // abstract addCodepointToCell(index: number, codePoint: number): void; - abstract addToPrecedingGrapheme(cursor: ICellData, newText: string, width: number): void; - abstract previousCodePoint(cell: ICellData): number; + // abstract addCodepointToCell(index: number, codePoint: number): void; + abstract addToPrecedingGrapheme(cursor: ICellData, newCode: number, width: number): void; abstract resize(cols: number, fillCellData: ICellData): boolean; abstract fill(fillCellData: ICellData, respectProtect: boolean): void; abstract copyFrom(line: BufferLine): void; @@ -204,17 +203,26 @@ 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 26 bits is StyleFlags - SKIP_COLUMNS = 4, // empty ("null") columns (20 bit count) + 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 + + // THE FOLLOWING ARE DEPRECATED // The following have 20 bits length (number of 16-bit code units). // Narrow vs wide assumes a monospace font. // Future: maybe support variable-width fonts; then treat all as narrow. - TEXT_w1 = 5, // Text in basic plane, narrow, 1 character per grapheme - TEXT_w2 = 6, // Text in basic plane, wide, 1 character per grapheme + TEXT_w1 = 13, // Text in basic plane, narrow, 1 character per grapheme + TEXT_w2 = 14, // Text in basic plane, wide, 1 character per grapheme // The following have 20 bits length and 8 bits (high-order) // Grapheme-Cluster-Break Property Value (of the last codepoint). - CLUSTER_w1 = 7, // single grapheme cluster, narrow (1 column) - CLUSTER_w2 = 8, // single grapheme cluster, wide (2 columns) + CLUSTER_w1 = 15, // single grapheme cluster, narrow (1 column) + CLUSTER_w2 = 16, // single grapheme cluster, wide (2 columns) //GENERIC_TEXT = 4, // Text has not been checked for wide or clusters MAYBE UNNEEDED } @@ -224,7 +232,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // Each item in _data is a 4-bit DataKind and 28 bits data. protected _data: Uint32Array; protected _dataLength: number; // active length of _data array - protected _text: string; // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; public length: number; @@ -234,15 +241,28 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** Color for "rest of line" background, following _dataLength. */ lineEndBg: number; + private _cache1: number = 0; + private _cache2: number = 0; + private _cache3: number = 0; + private _cache4: number = 0; + private _cachedColumn(): number { return this._cache1 & 0xFFFF; } + private _cachedDataIndex(): number { return this._cache1 >>> 16; } + //private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED + private _cachedBg(): number { return this._cache2; } + private _cachedFg(): number { return this._cache3; } + // One more than index (in _data) of STYLE_FLAGS; 0 if none. + private _cachedStyleFlagsIndex(): number { return this._cache4; } + private _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } + private _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } + private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } + private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } + /** Index in _data of "current chunk". */ private static dataIndex(cell: CellData): number { return cell._stateM; } - /** Index in _text of start of string for "current chunk". */ - private static textIndex(cell: CellData): number { return cell._stateN; } /** The "current position" is this many columns into the current chunk. - * If W is the wStrLen of the chunk, then valid values are: - * 0..W (if TEXT_w1); 0..2*W (if TEXT_w2); 0..1 (if CLUSTER_w1); - * 0..2 (if CLUSTER_w2). (Odd values of TEXT_w2 or CLUSTER_w2 are only + * 0..1 (if CHAR_w1); 0..1 (if CHAR_w2); 0..1 (if CLUSTER_stART_w1); + * 0..2 (if CLUSTER_START_w2). (Odd values of TEXT_w2 or CLUSTER_w2 are only * allowed as temporary intermediate positions; except for appending, it is * an error to try modify *part* of a cluster or a wide character, * the effect will that the entire cluster or wide character is cleared.) @@ -257,11 +277,13 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { cell._stateN = itext; cell._stateA = columnOffset; // See note at columnOffset } + /** From a Uint23 in _data, extract the DataKind bits. */ private static wKind(word: number): DataKind { return word >>> 28; } - private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.TEXT_w1 && kind <= DataKind.CLUSTER_w2; } + private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_w2; } private static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_w2; } /** From a Uint23 in _data, extract length of string within _text. + * Only SKIP_COLUMNS? * Assumes kind is TEXT_w1, TEXT_w2, CLUSTER_w1, CLUSTER W2. */ private static wStrLen(word: number): number { return word & 0xfffff; } private static wSet1(kind: DataKind, value: number): number { @@ -280,29 +302,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } - public _setChars(cell: CellData, textStart: number, textEnd: number, width: number): void { - cell.textStart = textStart; - cell.textEnd = textEnd; - cell.textData = this._text; - cell.content = width << Content.WIDTH_SHIFT; - const numUnits = textEnd - textStart; - if (numUnits === 1 || numUnits === 2) { - const ch = this._text.codePointAt(textStart); - if (ch && (numUnits === 1) === (ch <= 0xffff)) // single-character cluster - cell.content |= ch; - else - cell.content |= Content.IS_COMBINED_MASK; - } else if (numUnits !== 0) { - cell.content |= Content.IS_COMBINED_MASK; - } - } - constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); this._data = new Uint32Array(cols); - this._text = ''; this._dataLength = 0; this.lineEndBg = 0; this.length = cols; @@ -343,18 +347,40 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.STYLE_FLAGS: code = 'STYLE'; break; case DataKind.SKIP_COLUMNS: code = 'SKIP'; break; case DataKind.CLUSTER_w1: code = 'CL1'; break; - case DataKind.CLUSTER_w2: code = 'C2'; break; + case DataKind.CLUSTER_w2: code = 'CL2'; break; + case DataKind.CLUSTER_START_w1: code = 'CL1'; break; + case DataKind.CLUSTER_START_w2: code = 'CL2'; break; + case DataKind.CLUSTER_CONTINUED: code = 'CL_CONT'; break; case DataKind.TEXT_w1: code = 'T1'; break; case DataKind.TEXT_w2: code = 'T1'; break; + case DataKind.CHAR_w1: code = 'C1'; break; + case DataKind.CHAR_w2: code = 'C2'; break; } - const hasText = BufferLine.wKindIsText(kind); if (i >= start) { if (i !== start) { s += ', '; } let value; - if (hasText) { - value = JSON.stringify(this._text.substring(toffset, toffset + wnum)); + if (kind === DataKind.CHAR_w1 || kind === DataKind.CHAR_w2) { + let count = 1; + while (i + count < end && 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); + } else if (kind === DataKind.CLUSTER_START_w1 + || DataKind.CLUSTER_START_w2) { + // FIXME extract cluster as string + value = '#' + (word & 0x1fffff).toString(16); + } else if (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.SKIP_COLUMNS) { @@ -364,9 +390,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } s += code + ': ' + value; } - if (hasText) { - toffset += wnum; - } } return s + ']'; } @@ -381,13 +404,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { if (this._dataLength < 0 || this._dataLength > this._data.length) error("bad _dataLength"); const incrementText = (wlen: number) => { - itext += wlen; - if (itext > this._text.length) - error("text length too big"); }; for (let idata = 0; idata < this._dataLength; idata++) { const word = this._data[idata]; - const wlen = BufferLine.wStrLen(word); const kind = BufferLine.wKind(word); switch (kind) { case DataKind.FG: @@ -397,36 +416,30 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { break; case DataKind.SKIP_COLUMNS: break; - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: - incrementText(wlen); - break; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: - incrementText(wlen); + 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"); } } - if (itext < this._text.length) - error("text length too small"); } _checkCursor(cell: CellData): void { function error(str: string) { console.log("ERROR: "+str); } - let itext = BufferLine.textIndex(cell); let idata = BufferLine.dataIndex(cell); if (! (idata >= 0) || idata > this._dataLength) error("bad data index"); - if (! (itext >= 0) || itext > this._text.length) - error("bad text index"); if (idata < this._dataLength) { let word = this._data[idata]; let kind = BufferLine.wKind(word); let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; + /* const wlen = BufferLine.wStrLen(word); let colOffset = BufferLine.columnOffset(cell); if (BufferLine.wKindIsTextOrSkip(kind)) { @@ -435,6 +448,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } else { error("cursor points to style word"); } + */ } } @@ -448,10 +462,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const kind = BufferLine.wKind(word); if (kind <= DataKind.SKIP_COLUMNS) return true; - const wlen = BufferLine.wStrLen(word); let colOffset = BufferLine.columnOffset(cursor); + // FIXME handle new DataKind kinds let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; - return colOffset === wlen * wwidth; + return colOffset === wwidth; } /** @@ -474,8 +488,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let newExt = (style_flags & StyleFlags.HAS_EXTENDED) && eAttrs; const needStyle = style_flags !== oldStyle || oldExt !== newExt; let idata = BufferLine.dataIndex(cell); - // FIXME maybe use a (modified?) atLineEnd - const atEnd = idata === this._dataLength; + const atEnd = this.atLineEnd(cell); let add1 = atEnd ? 1 : 2; let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); @@ -508,8 +521,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); } } - let itext = BufferLine.textIndex(cell); - BufferLine.setPosition(cell, idata, itext, 0); + BufferLine.setPosition(cell, idata, -1, 0); } } @@ -520,10 +532,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const moveForwards = true; const cell = cursor as CellData; let idata = BufferLine.dataIndex(cell); - let itext = BufferLine.textIndex(cell); let word = idata < this._dataLength ? this._data[idata] : NULL_DATA_WORD; let kind = BufferLine.wKind(word); - const wlen = BufferLine.wStrLen(word); + const wlen = 1; // BufferLine.wStrLen(word); let colOffset = BufferLine.columnOffset(cell); codePoint &= Content.HAS_CONTENT_MASK; if (codePoint === 0) { @@ -538,11 +549,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, width); colOffset = 0; } - BufferLine.setPosition(cell, idata, itext, colOffset + width); + BufferLine.setPosition(cell, idata, -1, colOffset + width); return; } - const newGlyph = stringFromCodePoint(codePoint); + /*const newGlyph = stringFromCodePoint(codePoint); const newLength = newGlyph.length; if (newLength === 1 && ((kind === DataKind.TEXT_w1 && width === 1) @@ -557,9 +568,8 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { + newGlyph + this._text.substring(charOffset + 1); BufferLine.setPosition(cell, idata, itext, colOffset + width); - } else { + } else*/ { if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { - itext += wlen; this.splitWord(cell, 1); idata = BufferLine.dataIndex(cell); } else if (idata === this._dataLength && colOffset > 0) { @@ -568,19 +578,22 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } else { this.addEmptyDataElements(idata, 1); } - kind = newLength === 1 - ? (width == 2 ? DataKind.TEXT_w2 : DataKind.TEXT_w1) - : (width == 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1); - this._data[idata] = BufferLine.wSet1(kind, newLength); - this._text = this._text.substring(0, itext) - + newGlyph - + this._text.substring(itext); + kind = width == 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; + //kind = newLength === 1 + // ? (width == 2 ? DataKind.TEXT_w2 : DataKind.TEXT_w1) + // : (width == 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1); + this._data[idata] = BufferLine.wSet1(kind, codePoint); colOffset = 0; - BufferLine.setPosition(cell, idata, itext, colOffset + width); - this.deleteCols(cell, width, cell.getBg(), -1); + BufferLine.setPosition(cell, idata, -1, colOffset + width); + //this.deleteCols(cell, width, cell.getBg(), -1); } } + 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); + } + // MERGE INTO setCluster /** * Add a codepoint to a cell from input handler. @@ -588,45 +601,29 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { * onto a leading char. Since we already set the attrs * by the previous `setDataFromCodePoint` call, we can omit it here. */ - public addToPrecedingGrapheme(cell: ICellData, newText: string, width: number): void { + public addToPrecedingGrapheme(cell: ICellData, newCode: number, width: number): void { + const newText = stringFromCodePoint(newCode); const cursor = cell as CellData; - let itext = BufferLine.textIndex(cursor); let idata = BufferLine.dataIndex(cursor); - const colOffset = BufferLine.columnOffset(cursor); + let colOffset = BufferLine.columnOffset(cursor); if (idata >= this._dataLength || colOffset === 0) { return; } let word = this._data[idata]; let kind = BufferLine.wKind(word); let oldWidth = - (kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2) ? 2 : 1; + (kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 || kind === DataKind.CLUSTER_START_w2) ? 2 : 1; const newWidth = Math.max(oldWidth, width); - const wlen = BufferLine.wStrLen(word); - const at = itext + (colOffset === oldWidth ? wlen : colOffset); - this._text = this._text.substring(0, at) + newText - + this._text.substring(at); - const newLength = newText.length; - if (kind === DataKind.CLUSTER_w1 || kind === DataKind.CLUSTER_w2) { - if (colOffset !== oldWidth) { - return; - } - kind = width >= 2 ? DataKind.CLUSTER_w2 : kind; - this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newLength); - } else if (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2) { - if (colOffset !== wlen * oldWidth) { - return; - } - BufferLine.setPosition(cursor, idata, itext, colOffset - oldWidth); - this.splitWord(cursor, 0); - idata = BufferLine.dataIndex(cursor); - itext = BufferLine.textIndex(cursor); - kind = newWidth >= 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1; - word = this._data[idata]; - this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newLength); - } else { - return; - } - BufferLine.setPosition(cursor, idata, itext, newWidth); + const newKind = newWidth === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; + let clEnd = kind === DataKind.CLUSTER_START_w1 || kind === DataKind.CLUSTER_START_w2 ? this.clusterEnd(idata) : idata + 1; + this.addEmptyDataElements(clEnd, 1); + this._data[idata] = BufferLine.wSet1(newKind, (((clEnd - idata) & 0x3f) << 21) | (this._data[idata] & 0x01FFFFF)); + this._data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, newCode); + if (newWidth > oldWidth && colOffset === 1) + colOffset = 2; + BufferLine.setPosition(cursor, idata, -1, colOffset); + return; + /* let content = this._data[index * CELL_SIZE + Cell.CONTENT]; if (content & Content.IS_COMBINED_MASK) { @@ -656,7 +653,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.deleteCols(cell, width, cell.getBg(), -1); this.splitWord(cell, addToPrevious ? 0 : 1); let idata = BufferLine.dataIndex(cell); - let itext = BufferLine.textIndex(cell); if (addToPrevious && idata < this._data.length) { const word = this._data[idata]; const kind = BufferLine.wKind(word); @@ -668,7 +664,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } else { this._data[idata] = BufferLine.wSet1(kind, newText.length); } - this._text = this._text.substring(0, itext) + newText + this._text.substring(itext); } public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { @@ -707,43 +702,43 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { */ splitWord(cell: CellData, extraWordsToAdd: number): void { let idata = BufferLine.dataIndex(cell); - let itext = BufferLine.textIndex(cell); const colOffset = BufferLine.columnOffset(cell); let add = extraWordsToAdd; if (colOffset === 0) { if (extraWordsToAdd) { this.addEmptyDataElements(idata, add); - BufferLine.setPosition(cell, idata, itext, colOffset); + BufferLine.setPosition(cell, idata, -1, colOffset); } } else if (idata === this._dataLength) { this.addEmptyDataElements(idata, add + 1); this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, colOffset); - BufferLine.setPosition(cell, idata + 1, itext, 0); + BufferLine.setPosition(cell, idata + 1, -1, 0); } else { const word = this._data[idata]; - const wlen = BufferLine.wStrLen(word); const kind = BufferLine.wKind(word); // FIXME use atLineEnd - const atEnd = (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2) - ? colOffset === wlen * (kind - DataKind.TEXT_w1 + 1) + // FIXME handle new DataKind types + const atEnd = (kind | 1) === DataKind.CHAR_w2 + ? colOffset === (kind - DataKind.CHAR_w1 + 1) + : (kind | 1) === DataKind.CLUSTER_START_w2 + ? colOffset === (kind & 1) + 1 : colOffset === (kind - DataKind.CLUSTER_w1 + 1); if (atEnd) { - if (extraWordsToAdd) { - this.addEmptyDataElements(idata + 1, add); + if ((kind | 1) === DataKind.CLUSTER_START_w2) + idata = this.clusterEnd(idata); + else + idata++; + if (extraWordsToAdd) { // always true? + this.addEmptyDataElements(idata, add); } - idata++; - itext += wlen; } else { const kind = BufferLine.wKind(word); this._data[idata] = BufferLine.wSet1(kind, colOffset); - if (kind !== DataKind.SKIP_COLUMNS) { - itext += colOffset; - } idata++; this.addEmptyDataElements(idata, add + 1); - this._data[idata+add] = BufferLine.wSet1(kind, wlen - colOffset); + this._data[idata+add] = BufferLine.wSet1(kind, 1 - colOffset); // ??? } - BufferLine.setPosition(cell, idata, itext, 0); + BufferLine.setPosition(cell, idata, -1, 0); } } @@ -794,6 +789,102 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } + public loadCell(index: number, cell: ICellData): ICellData { + let curColumn = this._cachedColumn(); + if (index < curColumn) { + // FIXME can sometimes do better + this._cacheReset(); + curColumn = 0; + } + let cursor = cell as CellData; + 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 (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; + } + 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: + idata++; + styleFlagsIndex = idata; + break; + case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wStrLen(word); + if (todo >= wlen) { + todo -= wlen; + idata++; + curColumn += wlen; + } 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; + let clEnd = this.clusterEnd(idata); + if (todo >= w) { + todo -= w; + curColumn += w; + idata = clEnd; + } else { + // FIXME do this lazily, in CellData.getChars + const str = utf32ToString(this._data, idata, clEnd); + cursor.combinedData = str; + content = 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 + if (todo >= w) { + todo -= w; + idata++; + curColumn += w; + } else { + todo = -1; + content = index !== curColumn ? 0 + : (w << Content.WIDTH_SHIFT) | (word & 0x1fffff); + } + break; + } + } + this._cacheSetColumnDataIndex(curColumn, idata); + this._cacheSetFgBg(fg, bg); + this._cacheSetStyleFlagsIndex(styleFlagsIndex); + + cursor.content = content; + cursor.setFg(fg); + cursor.setBg(bg); + word = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; + cursor.setStyleFlags(word); + if (word & StyleFlags.HAS_EXTENDED) { + cursor.extended = this._extendedAttrs[styleFlagsIndex - 1]!; + } + return cell; + } /** * Move cursor forward the specified number of columns. @@ -802,32 +893,13 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { scanNext(cell: ICellData, n: number = 1, flags: number = 0): number { const cursor = cell as CellData; let idata = BufferLine.dataIndex(cursor); - let itext = BufferLine.textIndex(cursor); let col = BufferLine.columnOffset(cursor); let todo = n; while (idata < this._dataLength) { const word = this._data[idata]; const kind = BufferLine.wKind(word); - const wlen = BufferLine.wStrLen(word); let w; switch (kind) { - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: - w = kind + 1 - DataKind.CLUSTER_w1; - if (col + todo > w) { - todo -= w - col; - col = 0; - idata++; - itext += wlen; - } else { - if (col === 0) - this._setChars(cursor, itext, itext+wlen, w); - else - this._setChars(cursor, 0, 0, 0); - BufferLine.setPosition(cursor, idata, itext, col + todo); - return 0; - } - break; case DataKind.FG: cursor.setFg(word); idata++; @@ -844,74 +916,62 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { idata++; break; case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wStrLen(word); if (col + todo > wlen) { todo -= wlen - col; col = 0; idata++; } else { - this._setChars(cursor, 0, 0, 1); - BufferLine.setPosition(cursor, idata, itext, col + todo); + //this._setChars(cursor, 0, 0, 1); + BufferLine.setPosition(cursor, idata, -1, col + todo); return 0; } break; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: - w = kind + 1 - DataKind.TEXT_w1; // 1, or 2 if wide characters - if (col + todo > w * wlen) { - todo -= w * wlen - col; + case DataKind.CLUSTER_START_w1: + case DataKind.CLUSTER_START_w2: + w = kind + 1 - DataKind.CLUSTER_START_w1; + let clEnd = this.clusterEnd(idata); + if (col + todo > w) { + todo -= w - col; + col = 0; + idata = clEnd; + } else { + // FIXME do this lazily, in CellData.getChars + const str = utf32ToString(this._data, idata, clEnd); + cursor.combinedData = str; + BufferLine.setPosition(cursor, clEnd, -1, col + todo); + return 0; + } + break; + case DataKind.CHAR_w1: + case DataKind.CHAR_w2: + w = kind + 1 - DataKind.CHAR_w1; // 1, or 2 if wide characters + if (col + todo > w) { + todo -= w - col; idata++; - itext += wlen; col = 0; } else { col += todo; const wshift = w > 1 ? 1 : 0; - const start = itext + ((col - 1) >> wshift); - if (w > 1 && (col & 1) !== 0) - this._setChars(cursor, 0, 0, 0); - else - this._setChars(cursor, start, start + 1, w); - BufferLine.setPosition(cursor, idata, itext, col); + cell.content = w << Content.WIDTH_SHIFT + | (word & 0x1fffff); + BufferLine.setPosition(cursor, idata, -1, col); return 0; - } + } + break; } } - this._setChars(cursor, 0, 0, 1); - BufferLine.setPosition(cursor, idata, itext, todo); + //this._setChars(cursor, 0, 0, 1); + BufferLine.setPosition(cursor, idata, -1, todo); cursor.setBg(this.lineEndBg); return todo; } - public previousCodePoint(cell: ICellData): number { - const cursor = cell as CellData; - const colOffset = BufferLine.columnOffset(cursor); - const idata = BufferLine.dataIndex(cursor); - const itext = BufferLine.textIndex(cursor); - if (idata >= this._data.length) - return -1; - const word = this._data[idata]; - const kind = BufferLine.wKind(word); - const width = (kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2) ? 2 : 1; - const wlen = BufferLine.wStrLen(word); - switch (kind) { - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: - return colOffset !== width && this._text.codePointAt(itext) || -1; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: - const charOffset = width == 2 ? colOffset >> 1 : colOffset; - return colOffset === 0 || (width === 2 && (colOffset & 1) !== 0) ? -1 - : this._text.codePointAt(itext + charOffset) || -1; - default: - return -1; - } - } - public eraseAll(bg: number): void { // FIXME sometimes better to reuse old _data. this._data = EMPTY_DATA; this._dataLength = 0; this.lineEndBg = bg; - this._text = ''; } public deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void { @@ -928,11 +988,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let idata0 = BufferLine.dataIndex(cursor); let idata = idata0; - let itext = BufferLine.textIndex(cursor); const colOffset0 = BufferLine.columnOffset(cursor); let colOffset = colOffset0; let word0 = this._data[idata]; - const wlen = BufferLine.wStrLen(word0); let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; let fgValue = -1; //cursor.getFg(); let bgValue = -1; //cursor.getBg(); @@ -960,7 +1018,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { for (; todo > 0 && idata < this._dataLength; idata++) { let word = this._data[idata]; const kind = BufferLine.wKind(word); - let wlen = BufferLine.wStrLen(word); switch (kind) { case DataKind.FG: fgValue = word; break; case DataKind.BG: bgValue = word; break; @@ -969,6 +1026,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // handle ExtendedAttrs FIXME break; case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wStrLen(word); if (colOffset === 0 && wlen <= todo) { dskip_last = idata; todo -= wlen; @@ -980,34 +1038,35 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } colOffset = 0; break; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: + case DataKind.CHAR_w1: + case DataKind.CHAR_w2: w = kind - DataKind.TEXT_w1; // 0, or 1 if wide characters - const tstart = (colOffset >> w); - const tend = Math.min((colOffset + todo) >> w, wlen); - if (colOffset === 0 && (wlen << w) <= todo) { + if (colOffset === 0 && (1 << w) <= todo) { dskip_last = idata; - todo -= wlen << w; + todo -= 1 << w; } else { dskip_first = idata + 1; + /* const delta = tend - tstart; this._data[idata] = BufferLine.wSet1(kind, wlen - delta); todo -= delta << w; + */ } + /* + const tstart = (colOffset >> w); + const tend = Math.min((colOffset + todo) >> w, wlen); if (tskip_first < 0) tskip_first = itext + tstart; tskip_last = itext + tend; itext += wlen; colOffset = 0; + */ break; case DataKind.CLUSTER_w1: case DataKind.CLUSTER_w2: w = kind - DataKind.CLUSTER_w1; // 0, or 1 if wide characters if (colOffset < (1 << w)) { dskip_last = idata; - if (tskip_first < 0) - tskip_first = itext; - tskip_last = itext + wlen; todo -= (1 << w); } else { dskip_first = idata + 1; @@ -1027,15 +1086,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { tskip_first = itext + (colOffset >> w); todo -= wlen << w; */ - itext += wlen; colOffset = 0; break; } } - if (tskip_first >= 0) { - this._text = this._text.substring(0, tskip_first) - + this._text.substring(tskip_last); - } //if (dskip_first >= 0) { idata0 = dskip_first; if (bgValue >= 0) { @@ -1123,14 +1177,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { if ((colOffset & 1) === 0) return; let idata = BufferLine.dataIndex(cursor); - const itext = BufferLine.textIndex(cursor); const word = this._data[idata]; const wkind = BufferLine.wKind(word); // replace wide character by SKIP_COLUMNS for 2 columns - if (wkind === DataKind.TEXT_w2) { - const wlen = BufferLine.wStrLen(word); + if (wkind === DataKind.CHAR_w2) { const beforeLen = colOffset >> 1; - const afterLen = wlen - beforeLen - 1; + const afterLen = 1; const expand = (beforeLen > 0 ? 1 : 0) + (afterLen > 0 ? 1 : 0); if (expand > 0) this.addEmptyDataElements(idata, expand); @@ -1141,14 +1193,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { if (afterLen > 0) { this._data[idata] = BufferLine.wSet1(DataKind.TEXT_w2, afterLen); } - this._text = this._text.substring(0, beforeLen) - + this._text.substring(beforeLen + 1) BufferLine.setPosition(cursor, idata - 1, beforeLen, 1); - } else if (wkind === DataKind.CLUSTER_w2) { - const wlen = BufferLine.wStrLen(word); + } else if (wkind === DataKind.CLUSTER_START_w2) { + //const wlen = BufferLine.wStrLen(word); this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); - this._text = this._text.substring(0, itext) - + this._text.substring(itext + wlen); } // Ideally, if _data[idata-1] or _data[idata+1] is also SKIP_COLUMNS // we should merge the SKIP_COLUMNS ("normalize"). @@ -1237,7 +1285,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._data.set(line._data); } this._dataLength = line._dataLength; - this._text = line._text; this.length = line.length; this._extendedAttrs = {}; for (const el in line._extendedAttrs) { @@ -1250,7 +1297,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { public clone(): IBufferLine { const newLine = new BufferLine(0); newLine._data = new Uint32Array(this._data); - newLine._text = this._text; newLine.length = this.length; newLine.isWrapped = this.isWrapped; return newLine; @@ -1259,12 +1305,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { public getTrimmedLength(): number { let cols = 0; let skipped = 0; - const text = this._text; for (let idata = 0; idata < this._dataLength; idata++) { const word = this._data[idata]; const kind = BufferLine.wKind(word); let wlen = BufferLine.wStrLen(word); - const w = kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 2 : 1; + const w = kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 2 : 1; let wcols = 0; switch (kind) { case DataKind.FG: @@ -1274,13 +1319,17 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.SKIP_COLUMNS: skipped += wlen; break; - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: + case DataKind.CLUSTER_START_w1: + case DataKind.CLUSTER_START_w2: wcols = w; break; case DataKind.TEXT_w1: case DataKind.TEXT_w2: - wcols = w * wlen; + case DataKind.CHAR_w1: + case DataKind.CHAR_w2: + wcols = w * 1; + break; + case DataKind.CLUSTER_CONTINUED: break; } if (wcols) { @@ -1341,14 +1390,15 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, skipReplace: string = WHITESPACE_CELL_CHAR): string { let s = ''; - let itext = 0; let col = 0; let pendingStart = -1; let pendingLength = 0; - const text = this._text; + let pendingSkip = 0; + //const text = this._text; + const data = this._data; function pendingForce(handleSkip = ! trimRight): void { if (pendingStart >= 0 && pendingLength > 0) { - s += text.substring(pendingStart, pendingStart + pendingLength); + s += utf32ToString(data, pendingStart, pendingStart + pendingLength); pendingLength = 0; } else if (handleSkip && pendingLength > 0) { s += skipReplace.repeat(pendingLength); @@ -1375,7 +1425,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const word = this._data[idata]; const kind = BufferLine.wKind(word); let wlen = BufferLine.wStrLen(word); - const wide = kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 1 : 0; + const wide = kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 || kind === DataKind.CLUSTER_START_w2 ? 1 : 0; let wcols; switch (kind) { case DataKind.FG: @@ -1395,33 +1445,23 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } col += wlen; break; - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: + case DataKind.CLUSTER_START_w1: + case DataKind.CLUSTER_START_w2: + let clEnd = this.clusterEnd(idata); wcols = 1 << wide; if (col >= startCol && col + wcols <= endCol) { - addPendingString(itext, wlen); + addPendingString(idata, clEnd - idata); } - itext += wlen; + idata = clEnd - 1; col += wcols; break; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: - wcols = wlen << wide; - if (col + wcols > startCol) { - if (col < startCol) { - const skip = (startCol - col) >> wide; - wlen -= skip; - wcols = wlen << wide; - col = startCol; - itext += skip; - } - if (col + wcols > endCol) { - wlen = (endCol - col) >> wide; - } + case DataKind.CHAR_w1: + case DataKind.CHAR_w2: + wcols = 1 << wide; + if (col >= startCol && col + wcols <= endCol) { + addPendingString(idata, 1); } - addPendingString(itext, wlen); - itext += wlen; - col += wlen << wide; + col += wcols; break; } } diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index e08584c8e4..2e3af4c929 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -42,6 +42,7 @@ export class CellData extends AttributeData implements ICellData { public fg = 0; public bg = 0; public extended: IExtendedAttrs = new ExtendedAttrs(); + public combinedData = ''; public copyFrom(src: CellData) { //this.bufferLine = src.bufferLine; @@ -68,8 +69,15 @@ export class CellData extends AttributeData implements ICellData { } /** JS string of the content. */ public getChars(): string { - return this.textStart === this.textEnd ? '' - : this.textData.substring(this.textStart, this.textEnd); + if (this.content & Content.IS_COMBINED_MASK) { + return this.combinedData; + } + if (this.content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); + } + return ''; + //return this.textStart === this.textEnd ? '' + // : this.textData.substring(this.textStart, this.textEnd); } /** 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: From 6998d1e4f94ea6740c1162e608eeafb40bbc3d43 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 3 Oct 2023 17:49:07 -0700 Subject: [PATCH 09/73] Remove setCluster function. Fix thinko. --- src/common/buffer/BufferLine.ts | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 1ff45a7f28..6761972e62 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -594,7 +594,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return idata + 1 + ((this._data[idata] >> 21) & 0x3F); } - // MERGE INTO setCluster /** * Add a codepoint to a cell from input handler. * During input stage combining chars with a width of 0 follow and stack @@ -646,26 +645,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { */ } - // MERGE INTO addToPrecedingGrapheme - public setCluster(cursor: ICellData, addToPrevious: boolean, newText: string, width: number): void { - const cell = cursor as CellData; - const kind = width === 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1; - this.deleteCols(cell, width, cell.getBg(), -1); - this.splitWord(cell, addToPrevious ? 0 : 1); - let idata = BufferLine.dataIndex(cell); - if (addToPrevious && idata < this._data.length) { - const word = this._data[idata]; - const kind = BufferLine.wKind(word); - if (kind === DataKind.CLUSTER_w1 || kind === DataKind.CLUSTER_w2) { - this._data[idata] = BufferLine.wSet1(kind, BufferLine.wStrLen(word) + newText.length); - } else if (kind === DataKind.TEXT_w1 || kind === DataKind.TEXT_w2){ - } - // FIXME - } else { - this._data[idata] = BufferLine.wSet1(kind, newText.length); - } - } - public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { alert("insertCells"); /* @@ -1040,7 +1019,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { break; case DataKind.CHAR_w1: case DataKind.CHAR_w2: - w = kind - DataKind.TEXT_w1; // 0, or 1 if wide characters + w = kind - DataKind.CHAR_w1; // 0, or 1 if wide characters if (colOffset === 0 && (1 << w) <= todo) { dskip_last = idata; todo -= 1 << w; From 9a2027bb50ba0ffe5f379ae444690baf0a373461 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 25 Sep 2023 18:21:02 -0700 Subject: [PATCH 10/73] Change setCellFromCodePoint API and rename to setCellFromCodepoint Replace fg, bg, and eAttrs parameters by single IAttributeData parameter. In addition to making for a simpler/cleaner API, it also increases flexibility for future CellData or BufferLine changes. (Rename function to reduce risk of using accidentally using old function.) Also removed redundant eraseAttr parameter in deleteCells, insertCells, and replaceCells. --- src/common/InputHandler.ts | 13 ++++++------- src/common/Types.d.ts | 5 +++-- src/common/buffer/BufferLine.ts | 33 ++++++++++++++++----------------- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index a0657f475e..1827c4115f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -605,12 +605,12 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr), curAttr); + bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr)); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell if (bufferRow.getWidth(cols - 1) === 2) { - bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr); } } @@ -623,7 +623,7 @@ export class InputHandler extends Disposable implements IInputHandler { // handle wide chars: reset cell to the right if it is second cell of a wide char if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { - bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr); } this._dirtyRowTracker.markDirty(this._activeBuffer.y); @@ -1356,8 +1356,7 @@ export class InputHandler extends Disposable implements IInputHandler { line.insertCells( this._activeBuffer.x, params.params[0] || 1, - this._activeBuffer.getNullCell(this._eraseAttrData()), - this._eraseAttrData() + this._activeBuffer.getNullCell(this._eraseAttrData()) ); this._dirtyRowTracker.markDirty(this._activeBuffer.y); } @@ -1483,7 +1482,7 @@ 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)!; - line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); @@ -1506,7 +1505,7 @@ 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)!; - line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 1c375599f0..9560e4649f 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -256,7 +256,7 @@ export interface IBufferLine { 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, fg: number, bg: number, eAttrs: IExtendedAttrs): void; + setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; /** * Add character (codePoint) following cursor position. @@ -264,7 +264,8 @@ export interface IBufferLine { */ setCodePoint(cursor: ICellData, codePoint: number, width: number): void; addToPrecedingGrapheme(cursor: ICellData, newCode: number, width: number): void; - insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void; + //addCodepointToCell(index: number, codePoint: number, width: number): void; + insertCells(pos: number, n: number, ch: ICellData): void; eraseAll(bg: number): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6761972e62..0d96584df8 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -4,7 +4,7 @@ */ import { CharData, IAttributeData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; -import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; +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, StyleFlags, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; import { stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; @@ -84,10 +84,10 @@ export abstract class AbstractBufferLine implements IBufferLine { return cursor.getAsCharData(); } - /** + /* * * Set cell data from CharData. * @deprecated - */ + * / public set(index: number, value: CharData): void { // ??? this.setCellFromCodePoint(index, @@ -95,7 +95,6 @@ export abstract class AbstractBufferLine implements IBufferLine { value[CHAR_DATA_WIDTH_INDEX], value[CHAR_DATA_ATTR_INDEX], 0, new ExtendedAttrs()); - /* this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; if (value[CHAR_DATA_CHAR_INDEX].length > 1) { //this._combined[index] = value[1]; FIXME @@ -103,8 +102,8 @@ export abstract class AbstractBufferLine implements IBufferLine { } else { this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } - */ } + */ /** * primitive getters @@ -138,16 +137,16 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).content & Content.HAS_CONTENT_MASK; } - public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number, eAttrs: IExtendedAttrs): void { + public setCellFromCodePoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { const cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, index, 0); - let fg_flags = fg & 0xFC000000; - let bg_flags = bg & 0xFC000000; + let fg_flags = attrs.fg & 0xFC000000; + let bg_flags = attrs.bg & 0xFC000000; let style_flags = (fg_flags >> 24) | (bg_flags >> 16); fg -= fg_flags; bg -= bg_flags; - this.setAttributes(cursor, fg, bg, style_flags, eAttrs); + this.setAttributes(cursor, fg, bg, style_flags, attrs.extended); this.setCodePoint(cursor, codePoint, width); } @@ -155,7 +154,7 @@ export abstract class AbstractBufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell.fg, cell.bg, cell.extended); + this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell); } abstract setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; @@ -645,14 +644,14 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { */ } - public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void { + public insertCells(pos: number, n: number, fillCellData: ICellData): void { alert("insertCells"); /* pos %= this.length; // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); } if (n < this.length - pos) { @@ -671,7 +670,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // handle fullwidth at line end: reset last cell if it is first cell of a wide char if (this.getWidth(this.length - 1) === 2) { - this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData); } */ } @@ -738,10 +737,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // 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, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodepoint(start - 1, 0, 1, fillCellData); } if (end < this.length && this.getWidth(end - 1) === 2 && !this.isProtected(end)) { - this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodepoint(end, 0, 1, fillCellData); } while (start < end && start < this.length) { if (!this.isProtected(start)) { @@ -754,11 +753,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // 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, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || newU733 ExtendedAttrs()); + 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, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); + this.setCellFromCodepoint(end, 0, 1, fillCellData); } while (start < end && start < this.length) { From 9cca7032117b7495e00cc4d6b46b8e616205b671 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 5 Oct 2023 13:08:04 -0700 Subject: [PATCH 11/73] Extract guts of loadCell into new moveToColumn function. --- src/common/buffer/BufferLine.ts | 55 ++++++++++++++++----------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 0d96584df8..9a4f70b949 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -84,26 +84,13 @@ export abstract class AbstractBufferLine implements IBufferLine { return cursor.getAsCharData(); } - /* * + /** * Set cell data from CharData. * @deprecated - * / + */ public set(index: number, value: CharData): void { - // ??? - this.setCellFromCodePoint(index, - value[CHAR_DATA_CHAR_INDEX].charCodeAt(0), - value[CHAR_DATA_WIDTH_INDEX], - value[CHAR_DATA_ATTR_INDEX], - 0, new ExtendedAttrs()); - this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; - if (value[CHAR_DATA_CHAR_INDEX].length > 1) { - //this._combined[index] = value[1]; FIXME - this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } else { - this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } + this.setCell(index, CellData.fromCharData(value)); } - */ /** * primitive getters @@ -137,12 +124,14 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).content & Content.HAS_CONTENT_MASK; } - public setCellFromCodePoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { + public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { const cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, index, 0); - let fg_flags = attrs.fg & 0xFC000000; - let bg_flags = attrs.bg & 0xFC000000; + let fg = attrs.fg; + let bg = attrs.bg; + let fg_flags = fg & 0xFC000000; + let bg_flags = bg & 0xFC000000; let style_flags = (fg_flags >> 24) | (bg_flags >> 16); fg -= fg_flags; bg -= bg_flags; @@ -154,7 +143,7 @@ export abstract class AbstractBufferLine implements IBufferLine { * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { - this.setCellFromCodePoint(index, cell.content, cell.getWidth(), cell); + this.setCellFromCodepoint(index, cell.content, cell.getWidth(), cell); } abstract setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; @@ -767,14 +756,13 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } - public loadCell(index: number, cell: ICellData): ICellData { + public moveToColumn(index: number): number { let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better this._cacheReset(); curColumn = 0; } - let cursor = cell as CellData; let idata = this._cachedDataIndex(); let fg = this._cachedFg(); let bg = this._cachedBg(); @@ -820,15 +808,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.CLUSTER_START_w1: case DataKind.CLUSTER_START_w2: w = kind + 1 - DataKind.CLUSTER_START_w1; - let clEnd = this.clusterEnd(idata); if (todo >= w) { + const clEnd = this.clusterEnd(idata); todo -= w; curColumn += w; idata = clEnd; } else { - // FIXME do this lazily, in CellData.getChars - const str = utf32ToString(this._data, idata, clEnd); - cursor.combinedData = str; content = index !== curColumn ? 0 : (w << Content.WIDTH_SHIFT) | Content.IS_COMBINED_MASK; todo = -1; @@ -852,15 +837,27 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._cacheSetColumnDataIndex(curColumn, idata); this._cacheSetFgBg(fg, bg); this._cacheSetStyleFlagsIndex(styleFlagsIndex); + return content; + } + public loadCell(index: number, cell: ICellData): ICellData { + const cursor = cell as CellData; + const content = this.moveToColumn(index); cursor.content = content; - cursor.setFg(fg); - cursor.setBg(bg); - word = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; + cursor.setFg(this._cachedFg()); + cursor.setBg(this._cachedBg()); + let styleFlagsIndex = this._cachedStyleFlagsIndex(); + const word = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; cursor.setStyleFlags(word); if (word & StyleFlags.HAS_EXTENDED) { cursor.extended = this._extendedAttrs[styleFlagsIndex - 1]!; } + if (content & Content.IS_COMBINED_MASK) { + // FIXME do this lazily, in CellData.getChars + let idata = this._cachedDataIndex(); + const str = utf32ToString(this._data, idata, this.clusterEnd(idata)); + cursor.combinedData = str; + } return cell; } From 5983a0cfc4a3b8f9e0f0e6b3da4854f57a3446c3 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 5 Oct 2023 20:11:54 -0700 Subject: [PATCH 12/73] Get webgl renderer working again (as well as it did before). Also add some optimizations. --- src/common/buffer/BufferLine.ts | 59 +++++++++++++++++++++++++++++---- src/common/buffer/Constants.ts | 8 ++++- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 9a4f70b949..412f6a66b2 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -94,18 +94,18 @@ export abstract class AbstractBufferLine implements IBufferLine { /** * primitive getters - * @deprecated use these when only one value is needed, otherwise use `loadCell` + * 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. @deprecated */ + /** Test whether content has width. */ public hasWidth(index: number): number { return this.loadCell(index, new CellData()).content & Content.WIDTH_MASK; } - /** Get FG cell component. @deprecated */ + /** Get FG cell component. */ public getFg(index: number): number { return this.loadCell(index, new CellData()).fg; } @@ -315,6 +315,49 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._dataLength += count; } + /** + * 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 ? this._data[styleIndex - 1] : 0; + 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 ? this._data[styleIndex - 1] : 0; + 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; + } + /** for debugging */ getText(skipReplace: string = ' '): string { return this.translateToString(true, 0, this.length, skipReplace); @@ -915,6 +958,8 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const str = utf32ToString(this._data, idata, clEnd); cursor.combinedData = str; BufferLine.setPosition(cursor, clEnd, -1, col + todo); + cursor.content = col !== 0 ? 0 + : (w << Content.WIDTH_SHIFT) | Content.IS_COMBINED_MASK; return 0; } break; @@ -926,17 +971,17 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { idata++; col = 0; } else { - col += todo; const wshift = w > 1 ? 1 : 0; - cell.content = w << Content.WIDTH_SHIFT - | (word & 0x1fffff); + cell.content = col !== 0 ? 0 + : w << Content.WIDTH_SHIFT | (word & 0x1fffff); + col += todo; BufferLine.setPosition(cursor, idata, -1, col); return 0; } break; } } - //this._setChars(cursor, 0, 0, 1); + cursor.content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; BufferLine.setPosition(cursor, idata, -1, todo); cursor.setBg(this.lineEndBg); return todo; diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 542d1b8257..7984a52240 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -109,7 +109,13 @@ 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 { From 68e76314e1cd7e61c951176dd32d9a0537a06e02 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 6 Oct 2023 13:54:28 -0700 Subject: [PATCH 13/73] Remove old buffer DataKind values --- src/common/buffer/BufferLine.ts | 110 +++++++++----------------------- 1 file changed, 29 insertions(+), 81 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 412f6a66b2..754c3c9b3c 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -199,19 +199,7 @@ const enum DataKind { // 4 bits // 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 - - // THE FOLLOWING ARE DEPRECATED - // The following have 20 bits length (number of 16-bit code units). - // Narrow vs wide assumes a monospace font. - // Future: maybe support variable-width fonts; then treat all as narrow. - TEXT_w1 = 13, // Text in basic plane, narrow, 1 character per grapheme - TEXT_w2 = 14, // Text in basic plane, wide, 1 character per grapheme - // The following have 20 bits length and 8 bits (high-order) - // Grapheme-Cluster-Break Property Value (of the last codepoint). - CLUSTER_w1 = 15, // single grapheme cluster, narrow (1 column) - CLUSTER_w2 = 16, // single grapheme cluster, wide (2 columns) - //GENERIC_TEXT = 4, // Text has not been checked for wide or clusters MAYBE UNNEEDED + CLUSTER_CONTINUED = 12 // continuation of cluster } const NULL_DATA_WORD = DataKind.SKIP_COLUMNS << 28; @@ -250,7 +238,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** The "current position" is this many columns into the current chunk. * 0..1 (if CHAR_w1); 0..1 (if CHAR_w2); 0..1 (if CLUSTER_stART_w1); - * 0..2 (if CLUSTER_START_w2). (Odd values of TEXT_w2 or CLUSTER_w2 are only + * 0..2 (if CLUSTER_START_w2). (Odd values of CHAR_w2 or CLUSTER_START_w2 are only * allowed as temporary intermediate positions; except for appending, it is * an error to try modify *part* of a cluster or a wide character, * the effect will that the entire cluster or wide character is cleared.) @@ -268,12 +256,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** From a Uint23 in _data, extract the DataKind bits. */ private static wKind(word: number): DataKind { return word >>> 28; } - private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_w2; } - private static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_w2; } + private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_CONTINUED; } + private 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 SKIP_COLUMNS? - * Assumes kind is TEXT_w1, TEXT_w2, CLUSTER_w1, CLUSTER W2. */ - private static wStrLen(word: number): number { return word & 0xfffff; } + * Only for SKIP_COLUMNS. */ + private static wSkipCount(word: number): number { return word & 0xfffff; } private static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } @@ -377,13 +364,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.BG: code = 'BG'; break; case DataKind.STYLE_FLAGS: code = 'STYLE'; break; case DataKind.SKIP_COLUMNS: code = 'SKIP'; break; - case DataKind.CLUSTER_w1: code = 'CL1'; break; - case DataKind.CLUSTER_w2: code = 'CL2'; break; case DataKind.CLUSTER_START_w1: code = 'CL1'; break; case DataKind.CLUSTER_START_w2: code = 'CL2'; break; case DataKind.CLUSTER_CONTINUED: code = 'CL_CONT'; break; - case DataKind.TEXT_w1: code = 'T1'; break; - case DataKind.TEXT_w2: code = 'T1'; break; case DataKind.CHAR_w1: code = 'C1'; break; case DataKind.CHAR_w2: code = 'C2'; break; } @@ -469,9 +452,8 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { if (idata < this._dataLength) { let word = this._data[idata]; let kind = BufferLine.wKind(word); - let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; /* - const wlen = BufferLine.wStrLen(word); + const wlen = BufferLine.wSkipCount(word); let colOffset = BufferLine.columnOffset(cell); if (BufferLine.wKindIsTextOrSkip(kind)) { if (colOffset > wwidth * wlen) @@ -493,10 +475,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const kind = BufferLine.wKind(word); if (kind <= DataKind.SKIP_COLUMNS) return true; - let colOffset = BufferLine.columnOffset(cursor); - // FIXME handle new DataKind kinds - let wwidth = kind === DataKind.CLUSTER_w2 || kind === DataKind.TEXT_w2 ? 2 : 1; - return colOffset === wwidth; + return false; } /** @@ -565,7 +544,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let idata = BufferLine.dataIndex(cell); let word = idata < this._dataLength ? this._data[idata] : NULL_DATA_WORD; let kind = BufferLine.wKind(word); - const wlen = 1; // BufferLine.wStrLen(word); + const wlen = 1; let colOffset = BufferLine.columnOffset(cell); codePoint &= Content.HAS_CONTENT_MASK; if (codePoint === 0) { @@ -584,22 +563,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return; } - /*const newGlyph = stringFromCodePoint(codePoint); - const newLength = newGlyph.length; - if (newLength === 1 - && ((kind === DataKind.TEXT_w1 && width === 1) - || (kind === DataKind.TEXT_w2 && width === 2 - && (colOffset & 1) === 0))) { - if (width * colOffset >= wlen) { - this.deleteCols(cell, width, cell.getBg(), -1); - this._data[idata] = BufferLine.wSet1(kind, wlen+1); - } - const charOffset = itext + (width == 2 ? colOffset >> 1 : colOffset); - this._text = this._text.substring(0, charOffset) - + newGlyph - + this._text.substring(charOffset + 1); - BufferLine.setPosition(cell, idata, itext, colOffset + width); - } else*/ { if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { this.splitWord(cell, 1); idata = BufferLine.dataIndex(cell); @@ -610,14 +573,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.addEmptyDataElements(idata, 1); } kind = width == 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; - //kind = newLength === 1 - // ? (width == 2 ? DataKind.TEXT_w2 : DataKind.TEXT_w1) - // : (width == 2 ? DataKind.CLUSTER_w2 : DataKind.CLUSTER_w1); this._data[idata] = BufferLine.wSet1(kind, codePoint); colOffset = 0; BufferLine.setPosition(cell, idata, -1, colOffset + width); //this.deleteCols(cell, width, cell.getBg(), -1); - } } public clusterEnd(idata: number): number { @@ -642,7 +601,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let word = this._data[idata]; let kind = BufferLine.wKind(word); let oldWidth = - (kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 || kind === DataKind.CLUSTER_START_w2) ? 2 : 1; + (kind === DataKind.CHAR_w2 || kind === DataKind.CLUSTER_START_w2) ? 2 : 1; const newWidth = Math.max(oldWidth, width); const newKind = newWidth === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; let clEnd = kind === DataKind.CLUSTER_START_w1 || kind === DataKind.CLUSTER_START_w2 ? this.clusterEnd(idata) : idata + 1; @@ -732,7 +691,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { ? colOffset === (kind - DataKind.CHAR_w1 + 1) : (kind | 1) === DataKind.CLUSTER_START_w2 ? colOffset === (kind & 1) + 1 - : colOffset === (kind - DataKind.CLUSTER_w1 + 1); + : false; if (atEnd) { if ((kind | 1) === DataKind.CLUSTER_START_w2) idata = this.clusterEnd(idata); @@ -838,7 +797,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { styleFlagsIndex = idata; break; case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wStrLen(word); + let wlen = BufferLine.wSkipCount(word); if (todo >= wlen) { todo -= wlen; idata++; @@ -934,7 +893,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { idata++; break; case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wStrLen(word); + let wlen = BufferLine.wSkipCount(word); if (col + todo > wlen) { todo -= wlen - col; col = 0; @@ -1046,7 +1005,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { // handle ExtendedAttrs FIXME break; case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wStrLen(word); + let wlen = BufferLine.wSkipCount(word); if (colOffset === 0 && wlen <= todo) { dskip_last = idata; todo -= wlen; @@ -1082,10 +1041,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { colOffset = 0; */ break; - case DataKind.CLUSTER_w1: - case DataKind.CLUSTER_w2: - w = kind - DataKind.CLUSTER_w1; // 0, or 1 if wide characters + 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; dskip_last = idata; todo -= (1 << w); } else { @@ -1134,12 +1095,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { let word0 = this._data[idata]; let kind0 = BufferLine.wKind(word0); - //const wlen = BufferLine.wStrLen(word0); + //const wlen = BufferLine.wSkipCount(word0); // if kind of idata is SKIP_COLUMN, add deleted // if kind of idata-1 is SKIP_COLUMN, add deleted, adjust coloffset. if (kind0 === DataKind.SKIP_COLUMNS) { this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, - BufferLine.wStrLen(word0) + deleted); + BufferLine.wSkipCount(word0) + deleted); } else { this.splitWord(cursor, 1); idata = BufferLine.dataIndex(cursor); @@ -1201,22 +1162,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const wkind = BufferLine.wKind(word); // replace wide character by SKIP_COLUMNS for 2 columns if (wkind === DataKind.CHAR_w2) { - const beforeLen = colOffset >> 1; - const afterLen = 1; - const expand = (beforeLen > 0 ? 1 : 0) + (afterLen > 0 ? 1 : 0); - if (expand > 0) - this.addEmptyDataElements(idata, expand); - if (beforeLen > 0) { - this._data[idata++] = BufferLine.wSet1(DataKind.TEXT_w2, beforeLen); - } - this._data[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); - if (afterLen > 0) { - this._data[idata] = BufferLine.wSet1(DataKind.TEXT_w2, afterLen); - } - BufferLine.setPosition(cursor, idata - 1, beforeLen, 1); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); } else if (wkind === DataKind.CLUSTER_START_w2) { - //const wlen = BufferLine.wStrLen(word); this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + const clEnd = this.clusterEnd(idata); + const shrink = clEnd - idata - 1; + this.addEmptyDataElements(idata + 1, - shrink); } // Ideally, if _data[idata-1] or _data[idata+1] is also SKIP_COLUMNS // we should merge the SKIP_COLUMNS ("normalize"). @@ -1328,8 +1279,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { for (let idata = 0; idata < this._dataLength; idata++) { const word = this._data[idata]; const kind = BufferLine.wKind(word); - let wlen = BufferLine.wStrLen(word); - const w = kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 ? 2 : 1; + const w = kind === DataKind.CHAR_w2 || kind === DataKind.CLUSTER_START_w2 ? 2 : 1; let wcols = 0; switch (kind) { case DataKind.FG: @@ -1337,14 +1287,12 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.STYLE_FLAGS: break; case DataKind.SKIP_COLUMNS: - skipped += wlen; + skipped += BufferLine.wSkipCount(word); break; case DataKind.CLUSTER_START_w1: case DataKind.CLUSTER_START_w2: wcols = w; break; - case DataKind.TEXT_w1: - case DataKind.TEXT_w2: case DataKind.CHAR_w1: case DataKind.CHAR_w2: wcols = w * 1; @@ -1444,8 +1392,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { for (let idata = 0; idata < this._dataLength && col < endCol; idata++) { const word = this._data[idata]; const kind = BufferLine.wKind(word); - let wlen = BufferLine.wStrLen(word); - const wide = kind === DataKind.CHAR_w2 || kind === DataKind.TEXT_w2 || kind === DataKind.CLUSTER_w2 || kind === DataKind.CLUSTER_START_w2 ? 1 : 0; + const wide = kind === DataKind.CHAR_w2 || kind === DataKind.CLUSTER_START_w2 ? 1 : 0; let wcols; switch (kind) { case DataKind.FG: @@ -1453,6 +1400,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { case DataKind.STYLE_FLAGS: break; case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wSkipCount(word); if (col + wlen > startCol) { if (col < startCol) { wlen -= startCol - col; From 71bdeddfa304ab57f74a50de60a774354d9b28d7 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 6 Oct 2023 15:07:34 -0700 Subject: [PATCH 14/73] Revert canvas and webgl renderers to old API Using the per-BufferLine cache in loadCell should make this reasonably performant. --- addons/xterm-addon-canvas/src/BaseRenderLayer.ts | 2 +- addons/xterm-addon-canvas/src/CanvasRenderer.ts | 4 ++-- addons/xterm-addon-canvas/src/CursorRenderLayer.ts | 10 +++++----- addons/xterm-addon-canvas/src/TextRenderLayer.ts | 12 +++++------- addons/xterm-addon-webgl/src/WebglRenderer.ts | 9 +++------ 5 files changed, 16 insertions(+), 21 deletions(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 8c1993366a..49b494b69d 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -339,7 +339,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer * @param x The column to draw at. * @param y The row to draw at. */ - protected _fillCharTrueColor(cell: CellData, x: number, y: number): void { + protected _fillCharTrueColor(cell: ICellData, x: number, y: number): void { this._ctx.font = this._getFont(false, false); this._ctx.textBaseline = TEXT_BASELINE; this._clipRow(y); diff --git a/addons/xterm-addon-canvas/src/CanvasRenderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts index bc2d9dbec6..cd05f92169 100644 --- a/addons/xterm-addon-canvas/src/CanvasRenderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -39,7 +39,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { private readonly _bufferService: IBufferService, private readonly _charSizeService: ICharSizeService, private readonly _optionsService: IOptionsService, - characterJoinerService: ICharacterJoinerService, // FIXME remove + characterJoinerService: ICharacterJoinerService, coreService: ICoreService, private readonly _coreBrowserService: ICoreBrowserService, decorationService: IDecorationService, @@ -48,7 +48,7 @@ export class CanvasRenderer extends Disposable implements IRenderer { super(); const allowTransparency = this._optionsService.rawOptions.allowTransparency; this._renderLayers = [ - new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), + new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService), new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService), new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService), new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService) diff --git a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts index f5e7041d4d..c5174f8659 100644 --- a/addons/xterm-addon-canvas/src/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -25,9 +25,9 @@ interface ICursorState { export class CursorRenderLayer extends BaseRenderLayer { private _state: ICursorState; - private _cursorRenderers: {[key: string]: (x: number, y: number, cell: CellData) => void}; + private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void}; private _cursorBlinkStateManager: MutableDisposable = this.register(new MutableDisposable()); - private _cell: CellData = new CellData(); + private _cell: ICellData = new CellData(); constructor( terminal: Terminal, @@ -201,14 +201,14 @@ export class CursorRenderLayer extends BaseRenderLayer { } } - private _renderBarCursor(x: number, y: number, cell: CellData): void { + private _renderBarCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth); this._ctx.restore(); } - private _renderBlockCursor(x: number, y: number, cell: CellData): void { + private _renderBlockCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillCells(x, y, cell.getWidth(), 1); @@ -217,7 +217,7 @@ export class CursorRenderLayer extends BaseRenderLayer { this._ctx.restore(); } - private _renderUnderlineCursor(x: number, y: number, cell: CellData): void { + private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void { this._ctx.save(); this._ctx.fillStyle = this._themeService.colors.cursor.css; this._fillBottomLineAtCells(x, y); diff --git a/addons/xterm-addon-canvas/src/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts index a6b9633bf4..d2dee5831e 100644 --- a/addons/xterm-addon-canvas/src/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -36,6 +36,7 @@ export class TextRenderLayer extends BaseRenderLayer { alpha: boolean, bufferService: IBufferService, optionsService: IOptionsService, + private readonly _characterJoinerService: ICharacterJoinerService, decorationService: IDecorationService, coreBrowserService: ICoreBrowserService, themeService: IThemeService @@ -77,12 +78,10 @@ export class TextRenderLayer extends BaseRenderLayer { for (let y = firstRow; y <= lastRow; y++) { const row = y + this._bufferService.buffer.ydisp; const line = this._bufferService.buffer.lines.get(row); - if (! line) - continue; - line.scanInit(this._workCell); + const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (let x = 0; x < this._bufferService.cols; x++) { - line.scanNext(this._workCell, 1, 0); - let cell = this._workCell; + line!.loadCell(x, this._workCell); + let cell: ICellData = this._workCell; // If true, indicates that the current character(s) to draw were joined. let isJoined = false; @@ -104,7 +103,6 @@ export class TextRenderLayer extends BaseRenderLayer { // 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. - /* if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { isJoined = true; const range = joinedRanges.shift()!; @@ -120,7 +118,7 @@ export class TextRenderLayer extends BaseRenderLayer { // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; } - */ + // If the character is an overlapping char and the character to the // right is a space, take ownership of the cell to the right. We skip // this check for joined characters because their rendering likely won't diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index b2b92c0ffa..8cc81b92cd 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -404,12 +404,11 @@ export class WebglRenderer extends Disposable implements IRenderer { for (y = start; y <= end; y++) { row = y + terminal.buffer.ydisp; line = terminal.buffer.lines.get(row)!; - line.scanInit(cell); this._model.lineLengths[y] = 0; joinedRanges = this._characterJoinerService.getJoinedCharacters(row); for (x = 0; x < terminal.cols; x++) { lastBg = this._cellColorResolver.result.bg; - line.scanNext(cell, 1, 0); + line.loadCell(x, cell); if (x === 0) { lastBg = this._cellColorResolver.result.bg; @@ -422,7 +421,6 @@ export class WebglRenderer extends Disposable implements IRenderer { // 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. - /* FIXME if (joinedRanges.length > 0 && x === joinedRanges[0][0]) { isJoined = true; range = joinedRanges.shift()!; @@ -438,7 +436,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Skip over the cells occupied by this range in the loop lastCharX = range[1] - 1; } - */ + chars = cell.getChars(); code = cell.getCode(); i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; @@ -597,7 +595,6 @@ export class WebglRenderer extends Disposable implements IRenderer { } } -/* // TODO: Share impl with core export class JoinedCellData extends AttributeData implements ICellData { private _width: number; @@ -643,7 +640,7 @@ export class JoinedCellData extends AttributeData implements ICellData { return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; } } -*/ + function clamp(value: number, max: number, min: number = 0): number { return Math.max(Math.min(value, max), min); } From 38fd0dc77a03621924f1e065ebe87aa3f2526cfc Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 7 Oct 2023 12:43:47 -0700 Subject: [PATCH 15/73] Change LineBuffer.setAttributes to take an IAttributeData --- src/common/InputHandler.ts | 2 +- src/common/Types.d.ts | 2 +- src/common/buffer/BufferLine.ts | 42 +++++++++++++++------------------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 1827c4115f..ec1093779c 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -519,7 +519,7 @@ export class InputHandler extends Disposable implements IInputHandler { bufferRow.fixSplitWide(cursor); } - bufferRow.setAttributes(cursor, curAttr.getFg(), curAttr.getBg(), curAttr.getStyleFlags(), curAttr.extended); + bufferRow.setAttributes(cursor, curAttr); let precedingJoinState = this._parser.precedingJoinState; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 9560e4649f..64ecd536dd 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -257,7 +257,7 @@ export interface IBufferLine { loadCell(index: number, cell: ICellData): ICellData; setCell(index: number, cell: ICellData): void; setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; - setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; + setAttributes(cursor: ICellData, attrs: IAttributeData): void; /** * Add character (codePoint) following cursor position. * Cursor position is moved to be after added character. diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 754c3c9b3c..dc0834dff2 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -128,14 +128,7 @@ export abstract class AbstractBufferLine implements IBufferLine { const cursor = new CellData(); this.scanInit(cursor); this.scanNext(cursor, index, 0); - let fg = attrs.fg; - let bg = attrs.bg; - let fg_flags = fg & 0xFC000000; - let bg_flags = bg & 0xFC000000; - let style_flags = (fg_flags >> 24) | (bg_flags >> 16); - fg -= fg_flags; - bg -= bg_flags; - this.setAttributes(cursor, fg, bg, style_flags, attrs.extended); + this.setAttributes(cursor, attrs); this.setCodePoint(cursor, codePoint, width); } @@ -146,7 +139,7 @@ export abstract class AbstractBufferLine implements IBufferLine { this.setCellFromCodepoint(index, cell.content, cell.getWidth(), cell); } - abstract setAttributes(cursor: ICellData, fg: number, bg: number, style: StyleFlags, eAttrs: IExtendedAttrs): void; + abstract setAttributes(cursor: ICellData, attrs: IAttributeData): void; abstract setCodePoint(cursor: ICellData, codePoint: number, width: number): void; /** @@ -486,17 +479,20 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).getAsCharData(); } - public setAttributes(cursor: ICellData, fg: number, bg: number, style_flags: StyleFlags, eAttrs: IExtendedAttrs): void { + public setAttributes(cursor: ICellData, attrs: IAttributeData): void { const cell = cursor as CellData; this.fixSplitWide(cell); + const newFg = attrs.getFg(); + const newBg = attrs.getBg(); + const newStyle = attrs.getStyleFlags(); const oldFg = cell.getFg(); const oldBg = cell.getBg(); const oldStyle = cell.getStyleFlags(); - const needFg = fg !== oldFg; - const needBg = bg !== oldBg + const needFg = newFg !== oldFg; + const needBg = newBg !== oldBg let oldExt = cell.hasExtendedAttrs() && cell.extended; - let newExt = (style_flags & StyleFlags.HAS_EXTENDED) && eAttrs; - const needStyle = style_flags !== oldStyle || oldExt !== newExt; + let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; + const needStyle = newStyle !== oldStyle || oldExt !== newExt; let idata = BufferLine.dataIndex(cell); const atEnd = this.atLineEnd(cell); let add1 = atEnd ? 1 : 2; @@ -506,18 +502,18 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.splitWord(cell, add); idata = BufferLine.dataIndex(cell); if (needFg) { - this._data[idata++] = BufferLine.wSet1(DataKind.FG, fg); - cell.fg = fg; + this._data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); + cell.setFg(newFg); } if (needBg) { - this._data[idata++] = BufferLine.wSet1(DataKind.BG, bg); - cell.bg = bg; + this._data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); + cell.setBg(newBg); } if (needStyle) { - if (style_flags & StyleFlags.HAS_EXTENDED) - this._extendedAttrs[idata] = eAttrs; - this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, style_flags); - cell.setStyleFlags(style_flags); + if (newStyle & StyleFlags.HAS_EXTENDED) + this._extendedAttrs[idata] = attrs.extended; + this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); + cell.setStyleFlags(newStyle); } let xdata = idata; if (! atEnd) { @@ -717,7 +713,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { const width = fill.getWidth(); if (count <= 0) return; - this.setAttributes(cursor, fill.getFg(), fill.getBg(), fill.getStyleFlags(), fill.extended); + this.setAttributes(cursor, fill); for (;;) { // FIXME check protected this.setCodePoint(cursor, code, width); From 6d32c99de3733b87dde9c13e501f5b0664565349 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 13 Oct 2023 16:47:13 -0700 Subject: [PATCH 16/73] new method LineBuffer.insertText InputHandler.print just calls insertText, without a loop. (WIP) --- src/common/InputHandler.ts | 33 ++++++-- src/common/Types.d.ts | 2 + src/common/buffer/BufferLine.ts | 134 +++++++++++++++++++++++++++++++- 3 files changed, 160 insertions(+), 9 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index ec1093779c..094623aad3 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @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'; @@ -125,6 +125,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; } @@ -176,7 +178,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, private readonly _coreMouseService: ICoreMouseService, - private readonly _unicodeService: IUnicodeService, + readonly unicodeService: IUnicodeService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() ) { super(); @@ -500,6 +502,28 @@ export class InputHandler extends Disposable implements IInputHandler { } public print(data: Uint32Array, start: number, end: number): void { + const curAttr = this._curAttrData; + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + if (true) { // FUTURE: if (bufferRow instanceof BufferLineNew) ... + 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 { + this._dirtyRowTracker.markDirty(this._activeBuffer.y); + // if (charset) replace character; FIXME ok to do it in-place? + let col = (bufferRow as BufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this); + //if (insertMode) + // deleteCols at end of line + // else + // deleteCols following inserted characters + //check for line wrap; + 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; @@ -507,8 +531,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); const cursor = this._workCell; @@ -535,8 +557,7 @@ export class InputHandler extends Disposable implements IInputHandler { code = ch.charCodeAt(0); } } - - 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; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 64ecd536dd..a0504e096d 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -484,6 +484,8 @@ export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestor */ export interface IInputHandler { onTitleChange: IEvent; + 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/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index dc0834dff2..04d97bb611 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -3,11 +3,12 @@ * @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, StyleFlags, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; import { stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder'; +import { UnicodeService } from 'common/services/UnicodeService'; export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData()); @@ -214,14 +215,20 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private _cache2: number = 0; private _cache3: number = 0; private _cache4: number = 0; - private _cachedColumn(): number { return this._cache1 & 0xFFFF; } + + /** 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.) + */ private _cachedDataIndex(): number { return this._cache1 >>> 16; } + /** The column number corresponding to _cachedDataIndex(). */ + private _cachedColumn(): number { return this._cache1 & 0xFFFF; } //private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED private _cachedBg(): number { return this._cache2; } private _cachedFg(): number { return this._cache3; } // One more than index (in _data) of STYLE_FLAGS; 0 if none. private _cachedStyleFlagsIndex(): number { return this._cache4; } - private _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } + private _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } private _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } @@ -289,7 +296,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } + // count can be negative addEmptyDataElements(position: number, count: number): void { + // FIXME also adjust _extendedAttr indexes this.resizeData(this._dataLength + count); this._data.copyWithin(position + count, position, this._dataLength); this._dataLength += count; @@ -942,6 +951,125 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return todo; } + public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler): number { + let content = this.moveToColumn(index); + 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 + + // split if in middle of wide FIXME + if ((content >> Content.WIDTH_SHIFT) === 2 + && 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, idata - clEnd - 1); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + } + // FIXME handle after _dataLength or in SKIP_COILUMNS + + // set attributes + const newFg = attrs.getFg(); + const newBg = attrs.getBg(); + const newStyle = attrs.getStyleFlags(); + const oldFg = this._cachedFg(); + const oldBg = this._cachedBg(); + let styleFlagsIndex = this._cachedStyleFlagsIndex(); + const oldStyle = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; + const needFg = newFg !== oldFg; + const needBg = newBg !== oldBg + // FIXME let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && cell.extended; + //let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; + const needStyle = newStyle !== oldStyle // FIXME || oldExt !== newExt; + const atEnd = idata === this._dataLength; + let add1 = atEnd ? 1 : 2; + let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); + + if (add) { + this.addEmptyDataElements(idata, add); + if (needFg) { + this._data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); + } + if (needBg) { + this._data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); + } + if (needStyle) { + if (newStyle & StyleFlags.HAS_EXTENDED) + this._extendedAttrs[idata] = attrs.extended; + this._cacheSetStyleFlagsIndex(idata); + this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); + } + let xdata = idata; // FIXME + if (! atEnd) { + if (needFg) { + this._data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); + } + if (needStyle) { + this._data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); + } + if (needBg) { + this._data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + } + } + this._cacheSetFgBg(newFg, newBg); + } + + let precedingJoinState = inputHandler.precedingJoinState; + let inext; + if (add || idata === this._dataLength || index === curColumn) + inext = idata; + else { + const kind = BufferLine.wKind(this._data[idata]); + if (BufferLine.wKindIsText(kind)) + inext = this.clusterEnd(idata); + else + inext = idata; + } + this.addEmptyDataElements(inext, end - start); + let cellColumn = curColumn; + 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); + const 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); + } + inputHandler.precedingJoinState = precedingJoinState; + this._cacheSetColumnDataIndex(cellColumn, idata); + return curColumn; + } + public eraseAll(bg: number): void { // FIXME sometimes better to reuse old _data. this._data = EMPTY_DATA; From 16364256406f0706374cf7b08d3706ad333e91e1 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 16 Oct 2023 15:33:11 -0700 Subject: [PATCH 17/73] New help method deleteCellsOnly. Use it in insertText and deleteCells. --- src/common/InputHandler.ts | 9 +-- src/common/buffer/BufferLine.ts | 133 ++++++++++++++++++++++++++++---- 2 files changed, 122 insertions(+), 20 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 094623aad3..20089e13bb 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -514,12 +514,9 @@ export class InputHandler extends Disposable implements IInputHandler { private printNew(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { this._dirtyRowTracker.markDirty(this._activeBuffer.y); // if (charset) replace character; FIXME ok to do it in-place? - let col = (bufferRow as BufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this); - //if (insertMode) - // deleteCols at end of line - // else - // deleteCols following inserted characters - //check for line wrap; + const insertMode = this._coreService.modes.insertMode; + let col = (bufferRow as BufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, insertMode); + //FIXME check for line wrap; this._activeBuffer.x = col; } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 04d97bb611..0380ac567b 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -30,12 +30,6 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract scanNext(cursor: ICellData, n: number, flags: number): number; abstract eraseAll(bg: number): void; - /** - * Delete n colums, sliding following columns "left". - * If endCols >= 0, replace vacated columns (the n columns before endCol) - * with null cells with bg color. - */ - abstract deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; // abstract addCodepointToCell(index: number, codePoint: number): void; @@ -157,13 +151,7 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).isCombined(); } - public deleteCells(pos: number, n: number, fillCellData: ICellData): void { - pos %= this.length; - let cursor = new CellData(); - this.scanInit(cursor); - this.scanNext(cursor, pos, 0); - this.deleteCols(cursor, n, fillCellData.bg, -1); // FIXME set endCols - } + abstract deleteCells(pos: number, n: number, fillCellData: ICellData): void; /** Returns the string content of the cell. @deprecated */ public getString(index: number): string { @@ -868,6 +856,118 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return cell; } + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + const content = this.moveToColumn(pos); + let idata = this._cachedDataIndex(); + let curColumn = this._cachedColumn(); + this.deleteCellsOnly(idata, 0, n); + // FIXME + this.lineEndBg = fillCellData.bg; + } + + // FIXME doesn't properly handle if delete range starts or ends in middle + // of wide character + /** Internal - delete n columns, with adjust at end of line. */ + public deleteCellsOnly(idata0: number, colOffset0: number, n: number): void { + let todo = n; + let idata = idata0; + let colOffset = colOffset0; + let word0 = this._data[idata]; + let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; + let fgValue = -1; //cursor.getFg(); + let bgValue = -1; //cursor.getBg(); + let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs + /* + if (colOffset === 0) { + while (idata > 0) { + let skipItem = true; + switch (BufferLine.wKind(this._data[idata-1])) { + case DataKind.BG: cursor.setBg(-1); break; + case DataKind.FG: cursor.setFg(-1); break; + case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; + default: skipItem = false; + } + if (skipItem) { + idata--; + dskip_first = idata; + dskip_last = idata0-1; + } else { + break; + } + } + } + */ + for (; todo > 0 && idata < this._dataLength; idata++) { + let word = this._data[idata]; + const kind = BufferLine.wKind(word); + switch (kind) { + case DataKind.FG: fgValue = word; break; + case DataKind.BG: bgValue = word; break; + case DataKind.STYLE_FLAGS: + styleValue = word; + // handle ExtendedAttrs FIXME + break; + case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wSkipCount(word); + if (colOffset === 0 && wlen <= todo) { + dskip_last = idata; + todo -= wlen; + } else { + let delta = Math.min(todo, wlen - colOffset); + this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); + dskip_first = idata + 1; + 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) { + dskip_last = idata; + todo -= 1 << w; + } else { + dskip_first = idata + 1; + /* + const delta = tend - tstart; + this._data[idata] = BufferLine.wSet1(kind, wlen - delta); + todo -= delta << 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; + dskip_last = idata; + todo -= (1 << w); + } else { + dskip_first = idata + 1; + } + colOffset = 0; + break; + } + } + idata0 = dskip_first; + if (bgValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); + } + if (fgValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); + } + if (styleValue >= 0) { + this._data[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); + } + if (dskip_last >= 0) { + // FIXME maybe use addEmptyDataElements with negative count + this._data.copyWithin(idata0, dskip_last + 1, this._dataLength); + // FIXME also adjust _extendedAttr indexes + this._dataLength -= dskip_last + 1 - idata0; + } + } + /** * Move cursor forward the specified number of columns. * After, getChars() is the last character whose start edge is traversed. @@ -951,9 +1051,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return todo; } - public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler): number { + public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, insertMode: boolean): number { let content = this.moveToColumn(index); let curColumn = this._cachedColumn(); + const startColumn = curColumn; let idata = this._cachedDataIndex(); // CASES: @@ -1067,6 +1168,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } inputHandler.precedingJoinState = precedingJoinState; this._cacheSetColumnDataIndex(cellColumn, idata); + if (! insertMode && idata < this._dataLength) { + this.deleteCellsOnly(inext, 0, curColumn - startColumn); + } return curColumn; } @@ -1077,6 +1181,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.lineEndBg = bg; } + // deprecated public deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void { this.fixSplitWide(cursor); let todo = n; From e66a006e3a0414871142bb4b012bb3713cfa32fb Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 20 Oct 2023 13:29:45 -0700 Subject: [PATCH 18/73] implement setCellFromCodepoint for new BufferLine Get rid of some old cursor stuff in favor of buffer-local cache. --- src/common/InputHandler.ts | 71 ++++--- src/common/Types.d.ts | 11 +- src/common/buffer/BufferLine.ts | 352 ++++++++++---------------------- 3 files changed, 153 insertions(+), 281 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 20089e13bb..8b9619841b 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -530,18 +530,13 @@ export class InputHandler extends Disposable implements IInputHandler { const insertMode = this._coreService.modes.insertMode; this._dirtyRowTracker.markDirty(this._activeBuffer.y); - const cursor = this._workCell; - bufferRow.scanMove(cursor, this._activeBuffer.x); - // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (this._activeBuffer.x && end - start > 0) { - bufferRow.fixSplitWide(cursor); + // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char + if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { + bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr); } - bufferRow.setAttributes(cursor, curAttr); - let precedingJoinState = this._parser.precedingJoinState; - for (let pos = start; pos < end; ++pos) { code = data[pos]; @@ -554,6 +549,7 @@ export class InputHandler extends Disposable implements IInputHandler { code = ch.charCodeAt(0); } } + const currentInfo = this.unicodeService.charProperties(code, precedingJoinState); chWidth = UnicodeService.extractWidth(currentInfo); const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); @@ -567,24 +563,6 @@ export class InputHandler extends Disposable implements IInputHandler { this._oscLinkService.addLineToLink(this._getCurrentLinkId(), this._activeBuffer.ybase + this._activeBuffer.y); } - // insert combining char at last cursor position - // this._activeBuffer.x should never be 0 for a combining char - // since they always follow a cell consuming char - // therefore we can test for this._activeBuffer.x to avoid overflow left - if (shouldJoin && this._activeBuffer.x) { - /*if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { - // found 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 - 2, code); - } else*/ { - bufferRow.addToPrecedingGrapheme(cursor, code, chWidth); - //bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); - this._activeBuffer.x += chWidth - oldWidth; - } - continue; - } - // goto next line if ch would overflow // NOTE: To avoid costly width checks here, // the terminal does not allow a cols < 2. @@ -609,7 +587,16 @@ export class InputHandler extends Disposable implements IInputHandler { } // row changed, get it again bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - bufferRow.scanMove(cursor, 0); + 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); + } } else { this._activeBuffer.x = cols - 1; if (chWidth === 2) { @@ -620,6 +607,23 @@ export class InputHandler extends Disposable implements IInputHandler { } } + // insert combining char at last cursor position + // this._activeBuffer.x should never be 0 for a combining char + // since they always follow a cell consuming char + // therefore we can test for this._activeBuffer.x to avoid overflow left + if (shouldJoin && this._activeBuffer.x) { + const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2; + // 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, + code, chWidth); + for (let delta = chWidth - oldWidth; --delta >= 0; ) { + bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr); + } + continue; + } + // insert mode: move characters to right if (insertMode) { // right shift cells according to the width @@ -633,8 +637,17 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.setCodePoint(cursor, code, chWidth); - this._activeBuffer.x += chWidth; + bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr); + + // fullwidth char - also set next cell to placeholder stub and advance cursor + // for graphemes bigger than fullwidth we can simply loop to zero + // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right + if (chWidth > 0) { + while (--chWidth) { + // other than a regular empty cell a cell following a wide char has no width + bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr); + } + } } this._parser.precedingJoinState = precedingJoinState; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index a0504e096d..8e954f426d 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -251,25 +251,16 @@ export interface IBufferLine { * TODO: Define handling of wide characters, */ scanNext(cursor: ICellData, n: number, flags: number): number; - scanMove(cursor: ICellData, column: number): void; 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; - setAttributes(cursor: ICellData, attrs: IAttributeData): void; - /** - * Add character (codePoint) following cursor position. - * Cursor position is moved to be after added character. - */ - setCodePoint(cursor: ICellData, codePoint: number, width: number): void; - addToPrecedingGrapheme(cursor: ICellData, newCode: number, width: number): void; - //addCodepointToCell(index: number, codePoint: number, width: number): void; + addCodepointToCell(index: number, codePoint: number, width: number): void; insertCells(pos: number, n: number, ch: ICellData): void; eraseAll(bg: number): void; deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; - replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect?: boolean): void; resize(cols: number, fill: ICellData): boolean; fixSplitWide(cell: ICellData): void; cleanupMemory(): number; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 0380ac567b..e82e5cdc4a 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -31,9 +31,7 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract scanNext(cursor: ICellData, n: number, flags: number): number; abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; - abstract replaceCols(cursor: ICellData, count: number, fillCellData: ICellData, respectProtect?: boolean): void; - // abstract addCodepointToCell(index: number, codePoint: number): void; - abstract addToPrecedingGrapheme(cursor: ICellData, newCode: number, width: number): void; + abstract addCodepointToCell(index: number, codePoint: number, width: number): void; abstract resize(cols: number, fillCellData: ICellData): boolean; abstract fill(fillCellData: ICellData, respectProtect: boolean): void; abstract copyFrom(line: BufferLine): void; @@ -43,29 +41,38 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract getNoBgTrimmedLength(): number; abstract cleanupMemory(): number; - scanMove(cursor: ICellData, column: number): void { - // FUTURE - optimize to reuse cursor position. - // However, have to deal with invalidation, perhaps using DirtyTracker. - // if (cursor.lineBuffer === this && column >= cursor.column ) ... scanNext(cursor, cursor - cursor.column) - this.scanInit(cursor); - this.scanNext(cursor, column, 0); - } + abstract loadCell(index: number, cell: ICellData): ICellData; - /** - * 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. @deprecated - */ - public loadCell(index: number, cell: ICellData): ICellData { - this.scanInit(cell); - this.scanNext(cell, index + 1, 0); - return cell; - } + 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; + } - replaceCells(start: number, end: number, fillCellData: ICellData, respectProtect: boolean = false): void { - let cursor = new CellData(); - this.scanInit(cursor); - this.scanNext(cursor, start, 0); - this.replaceCols(cursor, end - start, fillCellData, respectProtect); + // 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); + } } /** @@ -73,10 +80,9 @@ export abstract class AbstractBufferLine implements IBufferLine { * @deprecated */ get(index: number): CharData { - let cursor = new CellData(); - this.scanInit(cursor); - this.scanNext(cursor, index, 0); - return cursor.getAsCharData(); + let cell = new CellData(); + this.loadCell(index, cell); + return cell.getAsCharData(); } /** @@ -119,14 +125,7 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).content & Content.HAS_CONTENT_MASK; } - public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { - const cursor = new CellData(); - this.scanInit(cursor); - this.scanNext(cursor, index, 0); - this.setAttributes(cursor, attrs); - this.setCodePoint(cursor, codePoint, width); - } - + abstract setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; /** * Set data at `index` to `cell`. */ @@ -134,9 +133,6 @@ export abstract class AbstractBufferLine implements IBufferLine { this.setCellFromCodepoint(index, cell.content, cell.getWidth(), cell); } - abstract setAttributes(cursor: ICellData, attrs: IAttributeData): void; - abstract setCodePoint(cursor: ICellData, codePoint: number, width: number): void; - /** * Get codepoint of the cell. @deprecated * To be in line with `code` in CharData this either returns @@ -218,7 +214,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private _cachedStyleFlagsIndex(): number { return this._cache4; } private _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } private _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } - private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } + private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } /** Index in _data of "current chunk". */ @@ -476,158 +472,11 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).getAsCharData(); } - public setAttributes(cursor: ICellData, attrs: IAttributeData): void { - const cell = cursor as CellData; - this.fixSplitWide(cell); - const newFg = attrs.getFg(); - const newBg = attrs.getBg(); - const newStyle = attrs.getStyleFlags(); - const oldFg = cell.getFg(); - const oldBg = cell.getBg(); - const oldStyle = cell.getStyleFlags(); - const needFg = newFg !== oldFg; - const needBg = newBg !== oldBg - let oldExt = cell.hasExtendedAttrs() && cell.extended; - let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; - const needStyle = newStyle !== oldStyle || oldExt !== newExt; - let idata = BufferLine.dataIndex(cell); - const atEnd = this.atLineEnd(cell); - let add1 = atEnd ? 1 : 2; - let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); - - if (add) { - this.splitWord(cell, add); - idata = BufferLine.dataIndex(cell); - if (needFg) { - this._data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); - cell.setFg(newFg); - } - if (needBg) { - this._data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); - cell.setBg(newBg); - } - if (needStyle) { - if (newStyle & StyleFlags.HAS_EXTENDED) - this._extendedAttrs[idata] = attrs.extended; - this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); - cell.setStyleFlags(newStyle); - } - let xdata = idata; - if (! atEnd) { - if (needFg) { - this._data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); - } - if (needStyle) { - this._data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); - } - if (needBg) { - this._data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); - } - } - BufferLine.setPosition(cell, idata, -1, 0); - } - } - - /** - * Set character following the cursor. - */ - public setCodePoint(cursor: ICellData, codePoint: number, width: number): void { - const moveForwards = true; - const cell = cursor as CellData; - let idata = BufferLine.dataIndex(cell); - let word = idata < this._dataLength ? this._data[idata] : NULL_DATA_WORD; - let kind = BufferLine.wKind(word); - const wlen = 1; - let colOffset = BufferLine.columnOffset(cell); - codePoint &= Content.HAS_CONTENT_MASK; - if (codePoint === 0) { - if (kind === DataKind.SKIP_COLUMNS && colOffset + width <= wlen) { - } else if (idata === this._dataLength) { - } else if (kind === DataKind.SKIP_COLUMNS && colOffset === wlen) { - this.deleteCols(cell, 1, cell.getBg(), -1); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen + 1); - } else { - this.deleteCols(cell, width, cell.getBg(), -1); - this.addEmptyDataElements(idata, 1); - this._data[++idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, width); - colOffset = 0; - } - BufferLine.setPosition(cell, idata, -1, colOffset + width); - return; - } - - if (idata < this._dataLength && BufferLine.wKindIsTextOrSkip(kind)) { - this.splitWord(cell, 1); - idata = BufferLine.dataIndex(cell); - } else if (idata === this._dataLength && colOffset > 0) { - this.splitWord(cell, 1); - idata = BufferLine.dataIndex(cell); - } else { - this.addEmptyDataElements(idata, 1); - } - kind = width == 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; - this._data[idata] = BufferLine.wSet1(kind, codePoint); - colOffset = 0; - BufferLine.setPosition(cell, idata, -1, colOffset + width); - //this.deleteCols(cell, width, cell.getBg(), -1); - } - 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); } - /** - * Add a codepoint to a cell from input handler. - * During input stage combining chars with a width of 0 follow and stack - * onto a leading char. Since we already set the attrs - * by the previous `setDataFromCodePoint` call, we can omit it here. - */ - public addToPrecedingGrapheme(cell: ICellData, newCode: number, width: number): void { - const newText = stringFromCodePoint(newCode); - const cursor = cell as CellData; - let idata = BufferLine.dataIndex(cursor); - let colOffset = BufferLine.columnOffset(cursor); - if (idata >= this._dataLength || colOffset === 0) { - return; - } - let word = this._data[idata]; - let kind = BufferLine.wKind(word); - let oldWidth = - (kind === DataKind.CHAR_w2 || kind === DataKind.CLUSTER_START_w2) ? 2 : 1; - const newWidth = Math.max(oldWidth, width); - const newKind = newWidth === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; - let clEnd = kind === DataKind.CLUSTER_START_w1 || kind === DataKind.CLUSTER_START_w2 ? this.clusterEnd(idata) : idata + 1; - this.addEmptyDataElements(clEnd, 1); - this._data[idata] = BufferLine.wSet1(newKind, (((clEnd - idata) & 0x3f) << 21) | (this._data[idata] & 0x01FFFFF)); - this._data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, newCode); - if (newWidth > oldWidth && colOffset === 1) - colOffset = 2; - BufferLine.setPosition(cursor, idata, -1, colOffset); - return; - - /* - let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED_MASK) { - // we already have a combined string, simply add - this._combined[index] += stringFromCodePoint(codePoint); - } else { - if (content & Content.CODEPOINT_MASK) { - // normal case for combining chars: - // - move current leading char + new one into combined string - // - set combined flag - this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 - content |= Content.IS_COMBINED_MASK; - } else { - // should not happen - we actually have no data in the cell yet - // simply set the data in the cell buffer with a width of 1 - content = codePoint | (1 << Content.WIDTH_SHIFT); - } - } - */ - } - public insertCells(pos: number, n: number, fillCellData: ICellData): void { alert("insertCells"); /* @@ -704,53 +553,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } - replaceCols(cursor: ICellData, count: number, fill: ICellData, respectProtect: boolean = false): void { - this.fixSplitWide(cursor); - const code = fill.getCode(); - const width = fill.getWidth(); - if (count <= 0) - return; - this.setAttributes(cursor, fill); - for (;;) { - // FIXME check protected - this.setCodePoint(cursor, code, width); - if (--count <= 0) - break; - } - /* - // 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); - } - */ - } - - public moveToColumn(index: number): number { let curColumn = this._cachedColumn(); if (index < curColumn) { @@ -835,6 +637,10 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { 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. @deprecated + */ public loadCell(index: number, cell: ICellData): ICellData { const cursor = cell as CellData; const content = this.moveToColumn(index); @@ -1051,7 +857,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return todo; } - public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, insertMode: boolean): number { + private preInsert(index: number, attrs: IAttributeData): boolean { let content = this.moveToColumn(index); let curColumn = this._cachedColumn(); const startColumn = curColumn; @@ -1108,6 +914,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this._cacheSetStyleFlagsIndex(idata); this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); } + this._cacheSetColumnDataIndex(index, idata); let xdata = idata; // FIXME if (! atEnd) { if (needFg) { @@ -1122,7 +929,14 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } this._cacheSetFgBg(newFg, newBg); } + return add > 0; + } + public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, insertMode: boolean): number { + const add = this.preInsert(index, attrs); + let curColumn = this._cachedColumn(); + const startColumn = curColumn; + let idata = this._cachedDataIndex(); let precedingJoinState = inputHandler.precedingJoinState; let inext; if (add || idata === this._dataLength || index === curColumn) @@ -1134,7 +948,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { else inext = idata; } + // FIXME optimize of overwriting simple text in-place this.addEmptyDataElements(inext, end - start); + let cellColumn = curColumn; for (let i = start; i < end; i++) { // inext is the insertion point for the current codepoint @@ -1174,6 +990,65 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { return curColumn; } + public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { + if (codePoint === NULL_CELL_CODE) { + if (width === 0) { // i.e. combining character + // FIXME - usually a no-open + } else { // width === 1 - i.e spacing + // FIXME + } + return; + } + const add = this.preInsert(index, attrs); + let curColumn = this._cachedColumn(); + const startColumn = curColumn; + let idata = this._cachedDataIndex(); + let inext; + if (add || idata === this._dataLength || index === 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, 1); + + let cellColumn = curColumn; + let kind = width === 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; + idata = inext; + cellColumn = curColumn; + curColumn += width; + this._data[inext++] = BufferLine.wSet1(kind, codePoint); + this._cacheSetColumnDataIndex(cellColumn, idata); + if (idata < this._dataLength) { + this.deleteCellsOnly(inext, 0, curColumn - startColumn); + } + } + + 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 startChar = this._data[idata] & 0x1FFFFF; + const kind = width === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; + this._data[idata] = BufferLine.wSet1(kind, + startChar + (nContinued << 21)); + this._data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); + } + public eraseAll(bg: number): void { // FIXME sometimes better to reuse old _data. this._data = EMPTY_DATA; @@ -1342,16 +1217,9 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } */ const atEnd = this.atLineEnd(cursor); -// if (! atEnd) { -// this.setAttributes(cursor, fgValue, bgValue, styleValue, cursor.extended/*FIXME*/); - //} else { - // this.lineEndBg = bg; - //this.setAttributes(cursor, cursor.getFg(), bg, cursor.getStyleFlags(), cursor.extended/*FIXME*/); - //} if (endCol < 0) { this.lineEndBg = bg; } else { - // setAttributes(...) FIXME // save position; move to (endCol - n) // insert n blanks, with bg set (and fg/style otherwise clear) // restore position From dd1eea08463a03015f65c28ffd38f83cddbab960 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 20 Oct 2023 17:10:06 -0700 Subject: [PATCH 19/73] More BufferLine cleanup --- src/common/Types.d.ts | 17 -- src/common/buffer/BufferLine.ts | 417 -------------------------------- 2 files changed, 434 deletions(-) diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 8e954f426d..d046d49620 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -235,22 +235,6 @@ export interface IBufferLine { length: number; /** If the previous line wrapped (overflows) into the current line. */ isWrapped: boolean; - /** - * Initialize cursot to beginning of line. - */ - scanInit(cursor: ICellData): void; - /** - * Scan to n'th next position, filling in character data in cursor. - * FUTURE: Negative n moves backward (left in left-to-right text). - * Special case n==0 is a "peek" operation: Fill in cursor with - * data from the nextcharacter - return -1 if there is no next character. - * The cursor properties are set from the last character over over. - * By default the next positon is the next column, but this may - * be modified by the flags (future: codepoints, graphemes, words, etc). - * Return 0 on sucess, otherwise the number of characters we couldn't move. - * TODO: Define handling of wide characters, - */ - scanNext(cursor: ICellData, n: number, flags: number): number; get(index: number): CharData; set(index: number, value: CharData): void; loadCell(index: number, cell: ICellData): ICellData; @@ -262,7 +246,6 @@ export interface IBufferLine { deleteCells(pos: number, n: number, fill: ICellData): void; replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void; resize(cols: number, fill: ICellData): boolean; - fixSplitWide(cell: ICellData): void; cleanupMemory(): number; fill(fillCellData: ICellData, respectProtect?: boolean): void; copyFrom(line: IBufferLine): void; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index e82e5cdc4a..6c8033d04c 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -25,10 +25,6 @@ export abstract class AbstractBufferLine implements IBufferLine { length: number = 0; isWrapped: boolean = false; - scanInit(cursor: ICellData): void { - } - - abstract scanNext(cursor: ICellData, n: number, flags: number): number; abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; @@ -161,8 +157,6 @@ export abstract class AbstractBufferLine implements IBufferLine { return this.loadCell(index, new CellData()).bg & BgFlags.PROTECTED; } - public fixSplitWide(cursor: ICellData): void { /* do nothing */ } - } const enum DataKind { // 4 bits @@ -217,27 +211,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } - /** Index in _data of "current chunk". */ - private static dataIndex(cell: CellData): number { return cell._stateM; } - - /** The "current position" is this many columns into the current chunk. - * 0..1 (if CHAR_w1); 0..1 (if CHAR_w2); 0..1 (if CLUSTER_stART_w1); - * 0..2 (if CLUSTER_START_w2). (Odd values of CHAR_w2 or CLUSTER_START_w2 are only - * allowed as temporary intermediate positions; except for appending, it is - * an error to try modify *part* of a cluster or a wide character, - * the effect will that the entire cluster or wide character is cleared.) - * Normally non-zero except right after initialization or backwards movement. - * I.e. when at the end of a N-column chunk, column offset should be N, - * rather than offset 0 in the next chunk. - */ - private static columnOffset(cell: CellData): number { return cell._stateA; } - - private static setPosition(cell: CellData, idata: number, itext: number, columnOffset: number): void { - cell._stateM = idata; - cell._stateN = itext; - cell._stateA = columnOffset; // See note at columnOffset - } - /** From a Uint23 in _data, extract the DataKind bits. */ private static wKind(word: number): DataKind { return word >>> 28; } private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_CONTINUED; } @@ -248,18 +221,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } - //private static wSet2(kind: DataKind, start: number, len: number): number { return (kind << 28) | (start & 0x3ffff) | ((len & 0x3dd) << 18); } - - scanInit(cursor: ICellData): void { - const cell = cursor as CellData; - if (this.continuationStart) { - cell.copyFrom(this.continuationStart); - } else { - cell.fg = 0; - cell.bg = 0; - BufferLine.setPosition(cell, 0, 0, 0); - } - } constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { super(); @@ -428,42 +389,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } - _checkCursor(cell: CellData): void { - function error(str: string) { - console.log("ERROR: "+str); - } - let idata = BufferLine.dataIndex(cell); - if (! (idata >= 0) || idata > this._dataLength) - error("bad data index"); - if (idata < this._dataLength) { - let word = this._data[idata]; - let kind = BufferLine.wKind(word); - /* - const wlen = BufferLine.wSkipCount(word); - let colOffset = BufferLine.columnOffset(cell); - if (BufferLine.wKindIsTextOrSkip(kind)) { - if (colOffset > wwidth * wlen) - error("bad columnOffset"); - } else { - error("cursor points to style word"); - } - */ - } - } - - public atLineEnd(cursor: CellData): boolean { - let idata = BufferLine.dataIndex(cursor); - if (idata === this._dataLength) - return true; - if (idata < this._dataLength - 1) - return false; - const word = this._data[idata]; - const kind = BufferLine.wKind(word); - if (kind <= DataKind.SKIP_COLUMNS) - return true; - return false; - } - /** * Get cell data CharData. * @deprecated @@ -508,51 +433,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { */ } - /** Split the the word referenced by 'cell' at the current colOffset. - * Assumes wKindIsTextOrSkip(kind). - */ - splitWord(cell: CellData, extraWordsToAdd: number): void { - let idata = BufferLine.dataIndex(cell); - const colOffset = BufferLine.columnOffset(cell); - let add = extraWordsToAdd; - if (colOffset === 0) { - if (extraWordsToAdd) { - this.addEmptyDataElements(idata, add); - BufferLine.setPosition(cell, idata, -1, colOffset); - } - } else if (idata === this._dataLength) { - this.addEmptyDataElements(idata, add + 1); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, colOffset); - BufferLine.setPosition(cell, idata + 1, -1, 0); - } else { - const word = this._data[idata]; - const kind = BufferLine.wKind(word); - // FIXME use atLineEnd - // FIXME handle new DataKind types - const atEnd = (kind | 1) === DataKind.CHAR_w2 - ? colOffset === (kind - DataKind.CHAR_w1 + 1) - : (kind | 1) === DataKind.CLUSTER_START_w2 - ? colOffset === (kind & 1) + 1 - : false; - if (atEnd) { - if ((kind | 1) === DataKind.CLUSTER_START_w2) - idata = this.clusterEnd(idata); - else - idata++; - if (extraWordsToAdd) { // always true? - this.addEmptyDataElements(idata, add); - } - } else { - const kind = BufferLine.wKind(word); - this._data[idata] = BufferLine.wSet1(kind, colOffset); - idata++; - this.addEmptyDataElements(idata, add + 1); - this._data[idata+add] = BufferLine.wSet1(kind, 1 - colOffset); // ??? - } - BufferLine.setPosition(cell, idata, -1, 0); - } - } - public moveToColumn(index: number): number { let curColumn = this._cachedColumn(); if (index < curColumn) { @@ -774,89 +654,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } } - /** - * Move cursor forward the specified number of columns. - * After, getChars() is the last character whose start edge is traversed. - */ - scanNext(cell: ICellData, n: number = 1, flags: number = 0): number { - const cursor = cell as CellData; - let idata = BufferLine.dataIndex(cursor); - let col = BufferLine.columnOffset(cursor); - let todo = n; - while (idata < this._dataLength) { - const word = this._data[idata]; - const kind = BufferLine.wKind(word); - let w; - switch (kind) { - case DataKind.FG: - cursor.setFg(word); - idata++; - break; - case DataKind.BG: - cursor.setBg(word); - idata++; - break; - case DataKind.STYLE_FLAGS: - cursor.setStyleFlags(word); - if (word & StyleFlags.HAS_EXTENDED) { - cursor.extended = this._extendedAttrs[idata]!; - } - idata++; - break; - case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wSkipCount(word); - if (col + todo > wlen) { - todo -= wlen - col; - col = 0; - idata++; - } else { - //this._setChars(cursor, 0, 0, 1); - BufferLine.setPosition(cursor, idata, -1, col + todo); - return 0; - } - break; - case DataKind.CLUSTER_START_w1: - case DataKind.CLUSTER_START_w2: - w = kind + 1 - DataKind.CLUSTER_START_w1; - let clEnd = this.clusterEnd(idata); - if (col + todo > w) { - todo -= w - col; - col = 0; - idata = clEnd; - } else { - // FIXME do this lazily, in CellData.getChars - const str = utf32ToString(this._data, idata, clEnd); - cursor.combinedData = str; - BufferLine.setPosition(cursor, clEnd, -1, col + todo); - cursor.content = col !== 0 ? 0 - : (w << Content.WIDTH_SHIFT) | Content.IS_COMBINED_MASK; - return 0; - } - break; - case DataKind.CHAR_w1: - case DataKind.CHAR_w2: - w = kind + 1 - DataKind.CHAR_w1; // 1, or 2 if wide characters - if (col + todo > w) { - todo -= w - col; - idata++; - col = 0; - } else { - const wshift = w > 1 ? 1 : 0; - cell.content = col !== 0 ? 0 - : w << Content.WIDTH_SHIFT | (word & 0x1fffff); - col += todo; - BufferLine.setPosition(cursor, idata, -1, col); - return 0; - } - break; - } - } - cursor.content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; - BufferLine.setPosition(cursor, idata, -1, todo); - cursor.setBg(this.lineEndBg); - return todo; - } - private preInsert(index: number, attrs: IAttributeData): boolean { let content = this.moveToColumn(index); let curColumn = this._cachedColumn(); @@ -1056,220 +853,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { this.lineEndBg = bg; } - // deprecated - public deleteCols(cursor: CellData, n: number, bg: number, endCol: number): void { - this.fixSplitWide(cursor); - let todo = n; - /* - const save_stateA = cursor._stateA; - const save_stateB = cursor._stateB; - const save_stateM = cursor._stateM; - const save_stateN = cursor._stateN; - this.scanNext(cursor, n, 0); - this.fixSplitWide(cursor); - */ - - let idata0 = BufferLine.dataIndex(cursor); - let idata = idata0; - const colOffset0 = BufferLine.columnOffset(cursor); - let colOffset = colOffset0; - let word0 = this._data[idata]; - let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; - let fgValue = -1; //cursor.getFg(); - let bgValue = -1; //cursor.getBg(); - let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs - /* - if (colOffset === 0) { - while (idata > 0) { - let skipItem = true; - switch (BufferLine.wKind(this._data[idata-1])) { - case DataKind.BG: cursor.setBg(-1); break; - case DataKind.FG: cursor.setFg(-1); break; - case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; - default: skipItem = false; - } - if (skipItem) { - idata--; - dskip_first = idata; - dskip_last = idata0-1; - } else { - break; - } - } - } - */ - for (; todo > 0 && idata < this._dataLength; idata++) { - let word = this._data[idata]; - const kind = BufferLine.wKind(word); - switch (kind) { - case DataKind.FG: fgValue = word; break; - case DataKind.BG: bgValue = word; break; - case DataKind.STYLE_FLAGS: - styleValue = word; - // handle ExtendedAttrs FIXME - break; - case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wSkipCount(word); - if (colOffset === 0 && wlen <= todo) { - dskip_last = idata; - todo -= wlen; - } else { - let delta = Math.min(todo, wlen - colOffset); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); - dskip_first = idata + 1; - 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) { - dskip_last = idata; - todo -= 1 << w; - } else { - dskip_first = idata + 1; - /* - const delta = tend - tstart; - this._data[idata] = BufferLine.wSet1(kind, wlen - delta); - todo -= delta << w; - */ - } - /* - const tstart = (colOffset >> w); - const tend = Math.min((colOffset + todo) >> w, wlen); - if (tskip_first < 0) - tskip_first = itext + tstart; - tskip_last = itext + tend; - itext += wlen; - colOffset = 0; - */ - 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; - dskip_last = idata; - todo -= (1 << w); - } else { - dskip_first = idata + 1; - } - /* - if (colOffset === 0 && wlen << w <= todo) { - dskip_first = dskip_first < 0 ? idata : dskip_first; - dskip_last = idata; - tskip_last = itext + wlen; - } else { - // FIXME - deleting part of grapheme - wlen = Math.min((colOffset + todo) >> w, wlen); - tskip_last = itext + wlen; - this._data[idata] = BufferLine.wSet1(kind, wlen); - } - if (tskip_first < 0) - tskip_first = itext + (colOffset >> w); - todo -= wlen << w; - */ - colOffset = 0; - break; - } - } - //if (dskip_first >= 0) { - idata0 = dskip_first; - if (bgValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); - } - if (fgValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); - } - if (styleValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); - } - if (dskip_last >= 0) { - this._data.copyWithin(idata0, dskip_last + 1, this._dataLength); - this._dataLength -= dskip_last + 1 - idata0; - } - const deleted = n - todo; - idata = idata0; - colOffset = colOffset0; - //idata = BufferLine.dataIndex(cursor); - /* - if (idata !== this._dataLength && deleted > 0) { - let word0 = this._data[idata]; - let kind0 = BufferLine.wKind(word0); - - //const wlen = BufferLine.wSkipCount(word0); - // if kind of idata is SKIP_COLUMN, add deleted - // if kind of idata-1 is SKIP_COLUMN, add deleted, adjust coloffset. - if (kind0 === DataKind.SKIP_COLUMNS) { - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, - BufferLine.wSkipCount(word0) + deleted); - } else { - this.splitWord(cursor, 1); - idata = BufferLine.dataIndex(cursor); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, deleted) - } - } - if (n === -1) { - // deleted an extra column because we ended up inside a wide char - // FIXME insert a SKIP_COLUMNS to compensate - } - */ - const atEnd = this.atLineEnd(cursor); - if (endCol < 0) { - this.lineEndBg = bg; - } else { - // save position; move to (endCol - n) - // insert n blanks, with bg set (and fg/style otherwise clear) - // restore position - } - /* - if (bg !== cursor.getBg()) { - this.splitWord(cursor, 1); - idata = BufferLine.dataIndex(cursor); - this._data[idata] = BufferLine.wSet1(DataKind.BG, bg); - BufferLine.setPosition(cursor, idata + 1, itext, 0); - cursor.bg = (bg & 0x3ffffff) | (cursor.bg & 0xfc000000); - } - */ - /* - // handle fullwidth at pos: - // - reset pos-1 if wide char - // - reset pos if width==0 (previous second cell of a wide char) - if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); - } - if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { - this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0, eraseAttr?.extended || new ExtendedAttrs()); - } - */ - } - - /** Fix if cursor is in the middle of a wide character or glyph. - * Replace wide glyph by SKIP_COLUMNS. - */ - public fixSplitWide(cell: ICellData): void { - const cursor = cell as CellData; - const colOffset = BufferLine.columnOffset(cursor); - if ((colOffset & 1) === 0) - return; - let idata = BufferLine.dataIndex(cursor); - const word = this._data[idata]; - const wkind = BufferLine.wKind(word); - // replace wide character by SKIP_COLUMNS for 2 columns - if (wkind === DataKind.CHAR_w2) { - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); - } else if (wkind === DataKind.CLUSTER_START_w2) { - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); - const clEnd = this.clusterEnd(idata); - const shrink = clEnd - idata - 1; - this.addEmptyDataElements(idata + 1, - shrink); - } - // Ideally, if _data[idata-1] or _data[idata+1] is also SKIP_COLUMNS - // we should merge the SKIP_COLUMNS ("normalize"). - } - /** * Resize BufferLine to `cols` filling excess cells with `fillCellData`. * The underlying array buffer will not change if there is still enough space From 8d1b31b52b6ccdf2a250c4ce0bf53b19446522a8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 20 Oct 2023 22:15:19 -0700 Subject: [PATCH 20/73] Allow choice of either NewBufferLine or OldBufferLine Controlled by USE_NewBufferLine variable --- .../dom/DomRendererRowFactory.test.ts | 2 +- .../services/CharacterJoinerService.test.ts | 4 +- src/browser/services/SelectionService.test.ts | 6 +- src/common/InputHandler.ts | 12 +- src/common/Types.d.ts | 5 +- src/common/buffer/Buffer.test.ts | 12 +- src/common/buffer/Buffer.ts | 8 +- src/common/buffer/BufferLine.test.ts | 4 +- src/common/buffer/BufferLine.ts | 510 +++++++++++++++++- src/common/buffer/BufferReflow.test.ts | 12 +- 10 files changed, 527 insertions(+), 48 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 14c4ade147..503fbe965b 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -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/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/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index 67158def09..e3b461ab02 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 diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 8b9619841b..b8dfdd5734 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -10,7 +10,7 @@ import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter } from 'common/EventEmitter'; 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'; @@ -504,7 +504,7 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { const curAttr = this._curAttrData; let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - if (true) { // FUTURE: if (bufferRow instanceof BufferLineNew) ... + if (bufferRow instanceof NewBufferLine) { this.printNew(data, start, end, bufferRow, curAttr); } else { this.printOld(data, start, end, bufferRow, curAttr); @@ -515,7 +515,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._dirtyRowTracker.markDirty(this._activeBuffer.y); // if (charset) replace character; FIXME ok to do it in-place? const insertMode = this._coreService.modes.insertMode; - let col = (bufferRow as BufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, insertMode); + let col = (bufferRow as NewBufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, insertMode); //FIXME check for line wrap; this._activeBuffer.x = col; } @@ -1174,11 +1174,11 @@ export class InputHandler extends Disposable implements IInputHandler { private _resetBufferLine(y: number, respectProtect: boolean = false): void { const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); if (line) { - if (respectProtect) { + if (line instanceof NewBufferLine && ! respectProtect) { + line.eraseAll(this._curAttrData.bg & ~0xFC000000); + } else { const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); line.fill(fillData, respectProtect); - } else { - line.eraseAll(this._curAttrData.bg & ~0xFC000000); } this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y); line.isWrapped = false; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index d046d49620..77d4e4e2e9 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -218,8 +218,7 @@ export interface IAttributeData { /** Cell data */ export interface ICellData extends IAttributeData { content: number; - //column: number; // 0-origin; -1 if unknown - //combinedData: string; + combinedData: string; // FIXME only if using OldBufferLine isCombined(): number; getWidth(): number; getChars(): string; @@ -242,7 +241,7 @@ export interface IBufferLine { setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; addCodepointToCell(index: number, codePoint: number, width: number): void; insertCells(pos: number, n: number, ch: ICellData): void; - eraseAll(bg: number): void; + //eraseAll(bg: number): 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; diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 39cffb4900..6f14f30c87 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -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 250c96bcc4..6c1c9e2fc9 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -85,7 +85,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 { @@ -180,7 +180,7 @@ export class Buffer implements IBuffer { 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 // 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, @@ -194,7 +194,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)); } } } @@ -330,7 +330,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) { diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 9234084b8e..f0642f7333 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -3,14 +3,14 @@ * @license MIT */ 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 { BufferLine, NewBufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; import { CharData, IBufferLine } from '../Types'; import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; -class TestBufferLine extends BufferLine { +class TestBufferLine extends NewBufferLine/*FIXME*/ { //public get combined(): {[index: number]: string} { // return this._combined; //} diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6c8033d04c..4801c679bc 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -25,7 +25,7 @@ export abstract class AbstractBufferLine implements IBufferLine { length: number = 0; isWrapped: boolean = false; - abstract eraseAll(bg: number): void; + //abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; abstract resize(cols: number, fillCellData: ICellData): boolean; @@ -176,7 +176,497 @@ const enum DataKind { // 4 bits const NULL_DATA_WORD = DataKind.SKIP_COLUMNS << 28; -export class BufferLine extends AbstractBufferLine implements IBufferLine { +export var USE_NewBufferLine = true; + +export abstract class BufferLine extends AbstractBufferLine implements IBufferLine { + static make(cols: number, fillCellData?: ICellData, isWrapped: boolean = false): BufferLine { + if (USE_NewBufferLine) { + return new NewBufferLine(cols, fillCellData, isWrapped); + } else { + return new OldBufferLine(cols, fillCellData, isWrapped); + } + } + + public abstract copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void; + + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, skipReplace: string = WHITESPACE_CELL_CHAR): string { + if (trimRight) { + endCol = Math.min(endCol, this.getTrimmedLength()); + } + let result = ''; + const cell = new CellData(); + while (startCol < endCol) { + this.loadCell(startCol, cell); + const content = cell.content; + const cp = content & Content.CODEPOINT_MASK; + result += cp ? cell.getChars() : skipReplace; + startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1 + } + return result; + } + + // FOLLOWING ONLY USED BY NewBufferLine + /** From a Uint23 in _data, extract the DataKind bits. */ + protected static wKind(word: number): DataKind { return word >>> 28; } + protected static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_CONTINUED; } + protected 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. */ + protected static wSkipCount(word: number): number { return word & 0xfffff; } + protected static wSet1(kind: DataKind, value: number): number { + return (kind << 28) | (value & 0x0fffffff); + } +} + +// FOLLOWING ONLY APPLIES for OldBufferLine + +/** + * buffer memory layout: + * + * | uint32_t | uint32_t | uint32_t | + * | `content` | `FG` | `BG` | + * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | + */ + + +/** typed array slots taken by one cell */ +const CELL_SIZE = 3; + +/** + * Cell member indices. + * + * Direct access: + * `content = data[column * CELL_SIZE + Cell.CONTENT];` + * `fg = data[column * CELL_SIZE + Cell.FG];` + * `bg = data[column * CELL_SIZE + Cell.BG];` + */ +const enum Cell { + CONTENT = 0, + FG = 1, // currently simply holds all known attrs + BG = 2 // currently unused +} + +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) { + super(); + 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) { + this.setCell(i, cell); + } + this.length = cols; + } + + /** + * Get cell data CharData. + * @deprecated + */ + public get(index: number): CharData { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + const cp = content & Content.CODEPOINT_MASK; + return [ + this._data[index * CELL_SIZE + Cell.FG], + (content & Content.IS_COMBINED_MASK) + ? this._combined[index] + : (cp) ? stringFromCodePoint(cp) : '', + content >> Content.WIDTH_SHIFT, + (content & Content.IS_COMBINED_MASK) + ? this._combined[index].charCodeAt(this._combined[index].length - 1) + : cp + ]; + } + + /** + * Set cell data from CharData. + * @deprecated + */ + public set(index: number, value: CharData): void { + this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; + if (value[CHAR_DATA_CHAR_INDEX].length > 1) { + this._combined[index] = value[1]; + this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } else { + this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + } + } + + /** + * primitive getters + * use these when only one value is needed, otherwise use `loadCell` + */ + public getWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; + } + + /** Test whether content has width. */ + public hasWidth(index: number): number { + return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; + } + + /** Get FG cell component. */ + public getFg(index: number): number { + return this._data[index * CELL_SIZE + Cell.FG]; + } + + /** Get BG cell component. */ + public getBg(index: number): number { + return this._data[index * CELL_SIZE + Cell.BG]; + } + + /** + * Test whether contains any chars. + * 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._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; + } + + /** + * Get codepoint of the cell. + * 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 { + const content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED_MASK) { + return this._combined[index].charCodeAt(this._combined[index].length - 1); + } + return content & Content.CODEPOINT_MASK; + } + + /** Test whether the cell contains a combined string. */ + 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]; + if (content & Content.IS_COMBINED_MASK) { + return this._combined[index]; + } + if (content & Content.CODEPOINT_MASK) { + return stringFromCodePoint(content & Content.CODEPOINT_MASK); + } + // return empty string for empty cells + return ''; + } + + /** Get state of protected flag. */ + public isProtected(index: number): number { + return this._data[index * CELL_SIZE + Cell.BG] & BgFlags.PROTECTED; + } + + /** + * 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 { + $startIndex = index * CELL_SIZE; + cell.content = this._data[$startIndex + Cell.CONTENT]; + cell.fg = this._data[$startIndex + Cell.FG]; + cell.bg = this._data[$startIndex + Cell.BG]; + if (cell.content & Content.IS_COMBINED_MASK) { + cell.combinedData = this._combined[index]; + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + cell.extended = this._extendedAttrs[index]!; + } + return cell; + } + + /** + * Set data at `index` to `cell`. + */ + public setCell(index: number, cell: ICellData): void { + if (cell.content & Content.IS_COMBINED_MASK) { + this._combined[index] = cell.combinedData; + } + if (cell.bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = cell.extended; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; + this._data[index * CELL_SIZE + Cell.FG] = cell.fg; + this._data[index * CELL_SIZE + Cell.BG] = cell.bg; + } + + /** + * Set cell data from input handler. + * Since the input handler see the incoming chars as UTF32 codepoints, + * it gets an optimized access method. + */ + public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { + if (attrs.bg & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[index] = attrs.extended; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); + this._data[index * CELL_SIZE + Cell.FG] = attrs.fg; + this._data[index * CELL_SIZE + Cell.BG] = attrs.bg; + } + + /** + * Add a codepoint to a cell from input handler. + * During input stage combining chars with a width of 0 follow and stack + * onto a leading char. Since we already set the attrs + * by the previous `setDataFromCodePoint` call, we can omit it here. + */ + public addCodepointToCell(index: number, codePoint: number, width: number): void { + let content = this._data[index * CELL_SIZE + Cell.CONTENT]; + if (content & Content.IS_COMBINED_MASK) { + // we already have a combined string, simply add + this._combined[index] += stringFromCodePoint(codePoint); + } else { + if (content & Content.CODEPOINT_MASK) { + // normal case for combining chars: + // - move current leading char + new one into combined string + // - set combined flag + this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED_MASK; + } else { + // should not happen - we actually have no data in the cell yet + // simply set the data in the cell buffer with a width of 1 + content = codePoint | (1 << Content.WIDTH_SHIFT); + } + } + if (width) { + content &= ~Content.WIDTH_MASK; + content |= width << Content.WIDTH_SHIFT; + } + this._data[index * CELL_SIZE + Cell.CONTENT] = content; + } + public insertCells(pos: number, n: number, fillCellData: ICellData): void { + pos %= this.length; + + // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); + } + + if (n < this.length - pos) { + const cell = new CellData(); + for (let i = this.length - pos - n - 1; i >= 0; --i) { + this.setCell(pos + n + i, this.loadCell(pos + i, cell)); + } + for (let i = 0; i < n; ++i) { + this.setCell(pos + i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + // handle fullwidth at line end: reset last cell if it is first cell of a wide char + if (this.getWidth(this.length - 1) === 2) { + this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData); + } + } + + public deleteCells(pos: number, n: number, fillCellData: ICellData): void { + pos %= this.length; + if (n < this.length - pos) { + const cell = new CellData(); + for (let i = 0; i < this.length - pos - n; ++i) { + this.setCell(pos + i, this.loadCell(pos + n + i, cell)); + } + for (let i = this.length - n; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } else { + for (let i = pos; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + // handle fullwidth at pos: + // - reset pos-1 if wide char + // - reset pos if width==0 (previous second cell of a wide char) + if (pos && this.getWidth(pos - 1) === 2) { + this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); + } + if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { + this.setCellFromCodepoint(pos, 0, 1,fillCellData); + } + } + + /** + * 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). + */ + public resize(cols: number, fillCellData: ICellData): boolean { + if (cols === this.length) { + return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; + } + 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 uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; + } + + /** + * 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._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) { + const data = new Uint32Array(this._data.length); + data.set(this._data); + this._data = data; + return 1; + } + return 0; + } + + /** fill a line with fillCharData */ + public fill(fillCellData: ICellData, respectProtect: boolean = false): void { + // full branching on respectProtect==true, hopefully getting fast JIT for standard case + if (respectProtect) { + for (let i = 0; i < this.length; ++i) { + if (!this.isProtected(i)) { + this.setCell(i, fillCellData); + } + } + return; + } + this._combined = {}; + this._extendedAttrs = {}; + for (let i = 0; i < this.length; ++i) { + this.setCell(i, fillCellData); + } + } + + /** alter to a full copy of line */ + public copyFrom(xline: BufferLine): void { + const line = xline as OldBufferLine; + if (this.length !== line.length) { + this._data = new Uint32Array(line._data); + } else { + // use high speed copy if lengths are equal + this._data.set(line._data); + } + this.length = line.length; + this._combined = {}; + for (const el in line._combined) { + this._combined[el] = line._combined[el]; + } + this._extendedAttrs = {}; + for (const el in line._extendedAttrs) { + this._extendedAttrs[el] = line._extendedAttrs[el]; + } + this.isWrapped = line.isWrapped; + } + + /** create a new clone */ + public clone(): IBufferLine { + const newLine = new OldBufferLine(0); + newLine._data = new Uint32Array(this._data); + newLine.length = this.length; + for (const el in this._combined) { + newLine._combined[el] = this._combined[el]; + } + for (const el in this._extendedAttrs) { + newLine._extendedAttrs[el] = this._extendedAttrs[el]; + } + newLine.isWrapped = this.isWrapped; + return newLine as IBufferLine; + } + + public getTrimmedLength(): number { + for (let i = this.length - 1; i >= 0; --i) { + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + } + } + return 0; + } + + public getNoBgTrimmedLength(): number { + for (let i = this.length - 1; i >= 0; --i) { + if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) { + return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + } + } + return 0; + } + + 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--) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; + } + } + } else { + for (let cell = 0; cell < length; cell++) { + for (let i = 0; i < CELL_SIZE; i++) { + this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; + } + if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { + this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; + } + } + } + + // Move any combined data over as needed, FIXME: repeat for extended attrs + const srcCombinedKeys = Object.keys(src._combined); + for (let i = 0; i < srcCombinedKeys.length; i++) { + const key = parseInt(srcCombinedKeys[i], 10); + if (key >= srcCol) { + this._combined[key - srcCol + destCol] = src._combined[key]; + } + } + } +} + +export class NewBufferLine extends BufferLine implements IBufferLine { // Each item in _data is a 4-bit DataKind and 28 bits data. protected _data: Uint32Array; protected _dataLength: number; // active length of _data array @@ -211,17 +701,6 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } - /** From a Uint23 in _data, extract the DataKind bits. */ - private static wKind(word: number): DataKind { return word >>> 28; } - private static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_CONTINUED; } - private 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. */ - private static wSkipCount(word: number): number { return word & 0xfffff; } - private static wSet1(kind: DataKind, value: number): number { - return (kind << 28) | (value & 0x0fffffff); - } - constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); @@ -928,7 +1407,8 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { } /** alter to a full copy of line */ - public copyFrom(line: BufferLine): void { + public copyFrom(xline: BufferLine): void { + const line = xline as NewBufferLine; if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { @@ -946,7 +1426,7 @@ export class BufferLine extends AbstractBufferLine implements IBufferLine { /** create a new clone */ public clone(): IBufferLine { - const newLine = new BufferLine(0); + const newLine = new NewBufferLine(0); newLine._data = new Uint32Array(this._data); newLine.length = this.length; newLine.isWrapped = this.isWrapped; 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)]); From 79ed24f14e8bb369728f2f9f407db4a3805355f6 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 2 Nov 2023 16:01:46 -0700 Subject: [PATCH 21/73] Basic support for window resize. (Still a bug with extra lines sometimes being added.) --- src/common/buffer/BufferLine.ts | 69 +++++++++++++-------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 4801c679bc..56405c909f 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -1148,7 +1148,6 @@ export class NewBufferLine extends BufferLine implements IBufferLine { // b. index === curColumn + width // c. otherwise - in middle of wide char - // split if in middle of wide FIXME if ((content >> Content.WIDTH_SHIFT) === 2 && index === curColumn + 1) { // In the middle of a wide character. Well-behaved applications are @@ -1267,12 +1266,9 @@ export class NewBufferLine extends BufferLine implements IBufferLine { } public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { - if (codePoint === NULL_CELL_CODE) { - if (width === 0) { // i.e. combining character - // FIXME - usually a no-open - } else { // width === 1 - i.e spacing - // FIXME - } + if (codePoint === NULL_CELL_CODE && width === 0) { + // i.e. combining character + // FIXME - usually a no-open return; } const add = this.preInsert(index, attrs); @@ -1280,14 +1276,21 @@ export class NewBufferLine extends BufferLine implements IBufferLine { const startColumn = curColumn; let idata = this._cachedDataIndex(); let inext; + if (codePoint === NULL_CELL_CODE && width > 0) { + if (idata === this._dataLength) + return; // maybe set bg FIXME + const kind = BufferLine.wKind(this._data[idata]); + if (kind === DataKind.SKIP_COLUMNS) + return; // maybe set bg FIXME + } if (add || idata === this._dataLength || index === curColumn) - inext = idata; + inext = idata; else { - const kind = BufferLine.wKind(this._data[idata]); - if (BufferLine.wKindIsText(kind)) - inext = this.clusterEnd(idata); - else - inext = idata; + 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, 1); @@ -1297,7 +1300,10 @@ export class NewBufferLine extends BufferLine implements IBufferLine { idata = inext; cellColumn = curColumn; curColumn += width; - this._data[inext++] = BufferLine.wSet1(kind, codePoint); + this._data[inext++] = + codePoint === NULL_CELL_CODE ? + BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1) : + BufferLine.wSet1(kind, codePoint); this._cacheSetColumnDataIndex(cellColumn, idata); if (idata < this._dataLength) { this.deleteCellsOnly(inext, 0, curColumn - startColumn); @@ -1340,6 +1346,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { * excess memory (true after shrinking > CLEANUP_THRESHOLD). */ public resize(cols: number, fillCellData: ICellData): boolean { + console.log("BufferLineNew.resize"); /* if (cols === this.length) { return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; @@ -1482,38 +1489,18 @@ export class NewBufferLine extends BufferLine implements IBufferLine { 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. - console.log('NOT IMPLEMENTED copyCellsFrom'); - /* - const srcData = src._data; + const cell = new CellData(); if (applyInReverse) { - for (let cell = length - 1; cell >= 0; cell--) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; - } - if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } + for (let i = length - 1; i >= 0; i--) { + src.loadCell(srcCol + i, cell); + this.setCell(destCol + i, cell); } } else { - for (let cell = 0; cell < length; cell++) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; - } - if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } - } - } - - // Move any combined data over as needed, FIXME: repeat for extended attrs - const srcCombinedKeys = Object.keys(src._combined); - for (let i = 0; i < srcCombinedKeys.length; i++) { - const key = parseInt(srcCombinedKeys[i], 10); - if (key >= srcCol) { - this._combined[key - srcCol + destCol] = src._combined[key]; + 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, skipReplace: string = WHITESPACE_CELL_CHAR): string { From b01f82717fd539c2fa49884e242fa3b2e4927cc2 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 5 Nov 2023 13:45:36 -0800 Subject: [PATCH 22/73] Get rid of some no-longer-used stuff in CellData. --- src/common/buffer/CellData.ts | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 2e3af4c929..8a0ff9151d 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { CharData, IBufferLine, ICellData, IExtendedAttrs } from 'common/Types'; +import { CharData, ICellData, IExtendedAttrs } from 'common/Types'; import { stringFromCodePoint } from 'common/input/TextDecoder'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, Content } from 'common/buffer/Constants'; import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; @@ -18,26 +18,7 @@ export class CellData extends AttributeData implements ICellData { obj.setFromCharData(value); return obj; } - //public bufferLine: IBufferLine | undefined; - textData: string = ''; - - /** Position and state in BufferLine. - * The actual meaning of _stateA/_stateB/_stateM/_stateN depends on - * on the actual class that implements bufferLine. - * See the BufferLine class for the "default" implementation. - * We use these place-holder fields in order to not have to allocate a - * a specfic CellData object depending on the bufferLine class. - */ - public _stateA: any; - public _stateB: any; - public _stateM: number = 0; - public _stateN: number = 0; - - public textStart: number = 0; - public textEnd: number = 0; - /** Primitives from terminal buffer. */ - public column = -1; public content = 0; public fg = 0; public bg = 0; @@ -45,14 +26,6 @@ export class CellData extends AttributeData implements ICellData { public combinedData = ''; public copyFrom(src: CellData) { - //this.bufferLine = src.bufferLine; - this._stateA = src._stateA; - this._stateB = src._stateB; - this._stateN = src._stateM; - this._stateN = src._stateN; - this.textStart = src.textStart; - this.textEnd = src.textEnd; - this.column = src.column; this.content = src.content; this.fg = src.fg; this.bg = src.bg; @@ -76,10 +49,7 @@ export class CellData extends AttributeData implements ICellData { return stringFromCodePoint(this.content & Content.CODEPOINT_MASK); } return ''; - //return this.textStart === this.textEnd ? '' - // : this.textData.substring(this.textStart, this.textEnd); } - /** * Codepoint of cell * Note this returns the UTF32 codepoint of single chars, @@ -124,7 +94,7 @@ export class CellData extends AttributeData implements ICellData { this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } if (combined) { - this.textData = value[CHAR_DATA_CHAR_INDEX]; + this.combinedData = value[CHAR_DATA_CHAR_INDEX]; this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); } } From a3020a779cf53febac4756ca7819e8862aef394f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 16 Nov 2023 15:47:39 -0800 Subject: [PATCH 23/73] Incomplete re-write of line-wrapping model. New classes LogicalBufferLine and WrappedBufferLine. --- src/browser/TestUtils.test.ts | 6 + src/browser/services/SelectionService.test.ts | 12 +- src/common/InputHandler.ts | 96 ++- src/common/Types.d.ts | 3 +- src/common/WindowsMode.ts | 5 +- src/common/buffer/Buffer.test.ts | 34 +- src/common/buffer/Buffer.ts | 33 +- src/common/buffer/BufferLine.test.ts | 11 +- src/common/buffer/BufferLine.ts | 566 ++++++++++-------- src/common/buffer/Types.d.ts | 2 + src/common/services/BufferService.ts | 41 +- 11 files changed, 500 insertions(+), 309 deletions(-) diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 7b464a3387..4f21de6164 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -214,6 +214,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; @@ -258,6 +261,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/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index e3b461ab02..e6ea6ed008 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -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/common/InputHandler.ts b/src/common/InputHandler.ts index 60b8796348..bc72ab0be1 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -517,11 +517,58 @@ export class InputHandler extends Disposable implements IInputHandler { } 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? const insertMode = this._coreService.modes.insertMode; let col = (bufferRow as NewBufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, insertMode); - //FIXME check for line wrap; + while (col > cols) { + // autowrap - DECAWM + // automatically wraps to the beginning of the next line + if (wraparoundMode) { + const oldRow = bufferRow; + let oldCol = this._activeBuffer.x; + //this._activeBuffer.x = oldWidth; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; + this._bufferService.scroll(this._eraseAttrData(), true); + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + } else { + if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; + } + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + // The line already exists (eg. the initial viewport), mark it as a + // wrapped line + if (bufferRow.isWrapped) { + // FIXME + } else { + this._activeBuffer.splitLine(this._activeBuffer.y, col); + col = col - cols; + } + } + // 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 = ...; + break; // FIXME + } else { + // FIXME delete excess + break; + } + } this._activeBuffer.x = col; } @@ -588,7 +635,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)!; @@ -742,7 +789,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) { @@ -806,7 +853,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: @@ -1159,7 +1206,8 @@ 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)!; + const row = this._activeBuffer.ybase + y; + const line = this._activeBuffer.lines.get(row)!; const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); //if (respectProtect) { line.replaceCells(start, end, fill, respectProtect); @@ -1167,7 +1215,7 @@ export class InputHandler extends Disposable implements IInputHandler { // line.deleteCells(start, end - start, fill); //} if (clearWrap) { - line.isWrapped = false; + this._activeBuffer.setWrapped(row, false); } } @@ -1177,16 +1225,18 @@ export class InputHandler extends Disposable implements IInputHandler { * @param y row index */ private _resetBufferLine(y: number, respectProtect: boolean = false): void { - const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y); + const buffer = this._activeBuffer; + const row = buffer.ybase + y; + const line = buffer.lines.get(row); if (line) { - if (line instanceof NewBufferLine && ! respectProtect) { + /*if (line instanceof NewBufferLine && ! respectProtect) { line.eraseAll(this._curAttrData.bg & ~0xFC000000); - } else { + } else*/ { const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); line.fill(fillData, respectProtect); } - this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y); - line.isWrapped = false; + buffer.clearMarkers(this._activeBuffer.ybase + y); + buffer.setWrapped(row, false); } } @@ -1237,7 +1287,7 @@ 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); @@ -1484,9 +1534,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; @@ -1517,9 +1568,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; @@ -1540,9 +1592,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; @@ -1563,9 +1616,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; @@ -3367,7 +3421,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.d.ts b/src/common/Types.d.ts index 77d4e4e2e9..27a76bf297 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -233,7 +233,8 @@ export interface ICellData extends IAttributeData { export interface IBufferLine { length: number; /** If the previous line wrapped (overflows) into the current line. */ - isWrapped: boolean; + 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; 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/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 6f14f30c87..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', () => { diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 6c1c9e2fc9..cb94f6fe14 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, USE_NewBufferLine, NewBufferLine, 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'; @@ -113,6 +113,37 @@ 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.moveToColumn(bufferService.cols); + // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); + let newRow; + if (nextRow.isWrapped) { + newRow = nextRow as WrappedBufferLine; + } else { + newRow = new WrappedBufferLine(curRow.logicalLine()); + // append nextRow contents to end of curRow.logicalLine() + this.lines.set(this.ybase + row, newRow); + curRow.nextRowSameLine = newRow; + } + curRow.setStartFromCache(newRow); + } + + public setWrapped(row: number, value: boolean): void { + const line = this.lines.get(row); + if (! line || line.isWrapped === value) + return; + if (! USE_NewBufferLine) { + line!._isWrapped = value; + } else if (value) { + // make wrapped FIXME + } else { + // clear wrapped FIXME + } + } + /** * Fills the buffer's viewport with blank lines. */ diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index f0642f7333..07baf3ba2f 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -3,18 +3,23 @@ * @license MIT */ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants'; -import { BufferLine, NewBufferLine } from 'common/buffer//BufferLine'; +import { BufferLine, LogicalBufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; -import { CharData, IBufferLine } from '../Types'; +import { CharData, IAttributeData, IBufferLine } from '../Types'; import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; -class TestBufferLine extends NewBufferLine/*FIXME*/ { +class TestBufferLine extends LogicalBufferLine/*FIXME*/ { //public get combined(): {[index: number]: string} { // return this._combined; //} + constructor(cols: number, fillCellData?: IAttributeData, isWrapped?: boolean) { + super(cols, fillCellData); + if (isWrapped) alert("TestBufferLine with isWrapped set not supported"); + } + public toArray(): CharData[] { const result = []; for (let i = 0; i < this.length; ++i) { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 56405c909f..944895e02d 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -23,8 +23,8 @@ const CLEANUP_THRESHOLD = 2; export abstract class AbstractBufferLine implements IBufferLine { /** Number of logical columns */ length: number = 0; - isWrapped: boolean = false; - + _isWrapped: boolean = false; + get isWrapped(): boolean { return this._isWrapped; } //abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; @@ -181,7 +181,8 @@ export var USE_NewBufferLine = true; export abstract class BufferLine extends AbstractBufferLine implements IBufferLine { static make(cols: number, fillCellData?: ICellData, isWrapped: boolean = false): BufferLine { if (USE_NewBufferLine) { - return new NewBufferLine(cols, fillCellData, isWrapped); + // if (isWrapped) new WrappedBufferLine(...); + return new LogicalBufferLine(cols, fillCellData); } else { return new OldBufferLine(cols, fillCellData, isWrapped); } @@ -252,14 +253,16 @@ export class OldBufferLine extends BufferLine implements IBufferLine { 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) { this.setCell(i, cell); } this.length = cols; + this._isWrapped = isWrapped; } /** @@ -596,7 +599,7 @@ export class OldBufferLine extends 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 */ @@ -610,7 +613,7 @@ export class OldBufferLine extends BufferLine implements IBufferLine { for (const el in this._extendedAttrs) { newLine._extendedAttrs[el] = this._extendedAttrs[el]; } - newLine.isWrapped = this.isWrapped; + newLine._isWrapped = this.isWrapped; return newLine as IBufferLine; } @@ -666,67 +669,60 @@ export class OldBufferLine extends BufferLine implements IBufferLine { } } -export class NewBufferLine extends BufferLine implements IBufferLine { - // Each item in _data is a 4-bit DataKind and 28 bits data. - protected _data: Uint32Array; - protected _dataLength: number; // active length of _data array - // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. - protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; - public length: number; - /** Used if this a continuation line (WORK-IN-PROGRESS) */ - continuationStart: CellData | undefined; - +export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Color for "rest of line" background, following _dataLength. */ - lineEndBg: number; + lineEndBg: number = 0; + nextRowSameLine: WrappedBufferLine | undefined; + + // Maybe move these to LogicalBufferLine? or to Buffer? private _cache1: number = 0; private _cache2: number = 0; private _cache3: number = 0; private _cache4: number = 0; /** The "current" index into the _data array. - * The index must be either _dataLength or wKindIsTextOrSkip must be true. + * The index must be either dataLength() or wKindIsTextOrSkip must be true. * (The index never points to a CLUSTER_CONTINUED item.) */ - private _cachedDataIndex(): number { return this._cache1 >>> 16; } + protected _cachedDataIndex(): number { return this._cache1 >>> 16; } /** The column number corresponding to _cachedDataIndex(). */ private _cachedColumn(): number { return this._cache1 & 0xFFFF; } //private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED - private _cachedBg(): number { return this._cache2; } - private _cachedFg(): number { return this._cache3; } - // One more than index (in _data) of STYLE_FLAGS; 0 if none. - private _cachedStyleFlagsIndex(): number { return this._cache4; } - private _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } - private _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } - private _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } - private _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } - - constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) { + protected _cachedBg(): number { return this._cache2; } + protected _cachedFg(): number { return this._cache3; } + // One more than index (in data()) of STYLE_FLAGS; 0 if none. + protected _cachedStyleFlagsIndex(): number { return this._cache4; } + protected _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } + protected _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } + protected _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } + protected _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } + + public setStartFromCache(wrapRow: WrappedBufferLine): void { + wrapRow.startIndex = this._cachedDataIndex(); + wrapRow.startBg = this._cachedBg(); + wrapRow.startFg = this._cachedFg(); + wrapRow.startStyle = this._cachedStyleFlagsIndex(); + } + + constructor() { super(); - // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); - //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); - this._data = new Uint32Array(cols); - this._dataLength = 0; - this.lineEndBg = 0; - this.length = cols; } - - resizeData(size: number): void { - if (size > this._data.length) { - //buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); - const new_data = new Uint32Array((3 * size) >> 1); - new_data.set(this._data); - this._data = new_data; - } + // Length of data() array. + protected abstract dataLength(): number; + // End of current row in data() array. + protected dataRowEnd(): number { + return this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); } + // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. + protected _extendedAttrs: IExtendedAttrs[] = []; + //protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; + //public length: number; - // count can be negative - addEmptyDataElements(position: number, count: number): void { - // FIXME also adjust _extendedAttr indexes - this.resizeData(this._dataLength + count); - this._data.copyWithin(position + count, position, this._dataLength); - this._dataLength += count; - } + abstract logicalLine(): LogicalBufferLine; + abstract data(): Uint32Array; + abstract resizeData(size: number): void; + abstract addEmptyDataElements(position: number, count: number): void; /** * primitive getters @@ -745,7 +741,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { public getFg(index: number): number { this.moveToColumn(index); const styleIndex = this._cachedStyleFlagsIndex(); - const styleWord = styleIndex > 0 ? this._data[styleIndex - 1] : 0; + const styleWord = styleIndex > 0 ? this.data()[styleIndex - 1] : 0; return this._cachedFg() | ((styleWord << 24) & Attributes.STYLE_BITS_MASK); } @@ -753,7 +749,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { public getBg(index: number): number { this.moveToColumn(index); const styleIndex = this._cachedStyleFlagsIndex(); - const styleWord = styleIndex > 0 ? this._data[styleIndex - 1] : 0; + const styleWord = styleIndex > 0 ? this.data()[styleIndex - 1] : 0; return this._cachedBg() | ((styleWord << 16) & Attributes.STYLE_BITS_MASK); } @@ -776,12 +772,12 @@ export class NewBufferLine extends BufferLine implements IBufferLine { return this.translateToString(true, 0, this.length, skipReplace); } - /* Human-readable display of _data array, for debugging */ - _showData(start = 0, end = this._dataLength) { + /* Human-readable display of data() array, for debugging */ + _showData(start = 0, end = this.dataLength()) { let s = '['; let toffset = 0; for (let i = 0; i < end; i++) { - const word = this._data[i]; + const word = this.data()[i]; const kind = BufferLine.wKind(word); let code: string | number = kind; const wnum = word & 0xfffffff; @@ -803,14 +799,14 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let value; if (kind === DataKind.CHAR_w1 || kind === DataKind.CHAR_w2) { let count = 1; - while (i + count < end && BufferLine.wKind(this._data[i + count]) === kind) { + while (i + count < end && 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); + str = utf32ToString(this.data(), i, i + count); code = code + '*' + count; i += count - 1; } @@ -841,12 +837,12 @@ export class NewBufferLine extends BufferLine implements IBufferLine { } let itext = 0; let icol = 0; - if (this._dataLength < 0 || this._dataLength > this._data.length) + if (this.dataLength() < 0 || this.dataLength() > this.data().length) error("bad _dataLength"); const incrementText = (wlen: number) => { }; - for (let idata = 0; idata < this._dataLength; idata++) { - const word = this._data[idata]; + for (let idata = 0; idata < this.dataLength(); idata++) { + const word = this.data()[idata]; const kind = BufferLine.wKind(word); switch (kind) { case DataKind.FG: @@ -878,7 +874,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { 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); + return idata + 1 + ((this.data()[idata] >> 21) & 0x3F); } public insertCells(pos: number, n: number, fillCellData: ICellData): void { @@ -928,13 +924,13 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let kind; let content = 0; while (todo >= 0) { - if (idata >= this._dataLength) { + if (idata >= this.dataLength()) { word = NULL_DATA_WORD; kind = DataKind.SKIP_COLUMNS; content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; break; } - word = this._data[idata]; + word = this.data()[idata]; kind = BufferLine.wKind(word); let w; switch (kind) { @@ -1007,7 +1003,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { cursor.setFg(this._cachedFg()); cursor.setBg(this._cachedBg()); let styleFlagsIndex = this._cachedStyleFlagsIndex(); - const word = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; + const word = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; cursor.setStyleFlags(word); if (word & StyleFlags.HAS_EXTENDED) { cursor.extended = this._extendedAttrs[styleFlagsIndex - 1]!; @@ -1015,7 +1011,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { if (content & Content.IS_COMBINED_MASK) { // FIXME do this lazily, in CellData.getChars let idata = this._cachedDataIndex(); - const str = utf32ToString(this._data, idata, this.clusterEnd(idata)); + const str = utf32ToString(this.data(), idata, this.clusterEnd(idata)); cursor.combinedData = str; } return cell; @@ -1025,114 +1021,11 @@ export class NewBufferLine extends BufferLine implements IBufferLine { const content = this.moveToColumn(pos); let idata = this._cachedDataIndex(); let curColumn = this._cachedColumn(); - this.deleteCellsOnly(idata, 0, n); + this.logicalLine().deleteCellsOnly(idata, 0, n); // FIXME this.lineEndBg = fillCellData.bg; } - // FIXME doesn't properly handle if delete range starts or ends in middle - // of wide character - /** Internal - delete n columns, with adjust at end of line. */ - public deleteCellsOnly(idata0: number, colOffset0: number, n: number): void { - let todo = n; - let idata = idata0; - let colOffset = colOffset0; - let word0 = this._data[idata]; - let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; - let fgValue = -1; //cursor.getFg(); - let bgValue = -1; //cursor.getBg(); - let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs - /* - if (colOffset === 0) { - while (idata > 0) { - let skipItem = true; - switch (BufferLine.wKind(this._data[idata-1])) { - case DataKind.BG: cursor.setBg(-1); break; - case DataKind.FG: cursor.setFg(-1); break; - case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; - default: skipItem = false; - } - if (skipItem) { - idata--; - dskip_first = idata; - dskip_last = idata0-1; - } else { - break; - } - } - } - */ - for (; todo > 0 && idata < this._dataLength; idata++) { - let word = this._data[idata]; - const kind = BufferLine.wKind(word); - switch (kind) { - case DataKind.FG: fgValue = word; break; - case DataKind.BG: bgValue = word; break; - case DataKind.STYLE_FLAGS: - styleValue = word; - // handle ExtendedAttrs FIXME - break; - case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wSkipCount(word); - if (colOffset === 0 && wlen <= todo) { - dskip_last = idata; - todo -= wlen; - } else { - let delta = Math.min(todo, wlen - colOffset); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); - dskip_first = idata + 1; - 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) { - dskip_last = idata; - todo -= 1 << w; - } else { - dskip_first = idata + 1; - /* - const delta = tend - tstart; - this._data[idata] = BufferLine.wSet1(kind, wlen - delta); - todo -= delta << 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; - dskip_last = idata; - todo -= (1 << w); - } else { - dskip_first = idata + 1; - } - colOffset = 0; - break; - } - } - idata0 = dskip_first; - if (bgValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); - } - if (fgValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); - } - if (styleValue >= 0) { - this._data[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); - } - if (dskip_last >= 0) { - // FIXME maybe use addEmptyDataElements with negative count - this._data.copyWithin(idata0, dskip_last + 1, this._dataLength); - // FIXME also adjust _extendedAttr indexes - this._dataLength -= dskip_last + 1 - idata0; - } - } - private preInsert(index: number, attrs: IAttributeData): boolean { let content = this.moveToColumn(index); let curColumn = this._cachedColumn(); @@ -1140,8 +1033,8 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let idata = this._cachedDataIndex(); // CASES: - // 1. idata === _dataLength - easy. - // 2. _data[idata] is SKIP_COLUMNS + // 1. idata === dataLength() - easy. + // 2. data()[idata] is SKIP_COLUMNS // -- split if curColumnn > 0 && curColumn < wlen // 3. kind is wKindIsText: // a. curColumn===index @@ -1154,9 +1047,9 @@ export class NewBufferLine extends BufferLine implements IBufferLine { // unlikely to do this, so it's not worth optimizing. const clEnd = this.clusterEnd(idata); this.addEmptyDataElements(idata, idata - clEnd - 1); - this._data[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); } - // FIXME handle after _dataLength or in SKIP_COILUMNS + // FIXME handle after dataLength() or in SKIP_COILUMNS // set attributes const newFg = attrs.getFg(); @@ -1165,41 +1058,41 @@ export class NewBufferLine extends BufferLine implements IBufferLine { const oldFg = this._cachedFg(); const oldBg = this._cachedBg(); let styleFlagsIndex = this._cachedStyleFlagsIndex(); - const oldStyle = styleFlagsIndex > 0 ? this._data[styleFlagsIndex - 1] : 0; + const oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; const needFg = newFg !== oldFg; const needBg = newBg !== oldBg // FIXME let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && cell.extended; //let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; const needStyle = newStyle !== oldStyle // FIXME || oldExt !== newExt; - const atEnd = idata === this._dataLength; + const atEnd = idata === this.dataLength(); let add1 = atEnd ? 1 : 2; let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); if (add) { this.addEmptyDataElements(idata, add); if (needFg) { - this._data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); + this.data()[idata++] = BufferLine.wSet1(DataKind.FG, newFg); } if (needBg) { - this._data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); + this.data()[idata++] = BufferLine.wSet1(DataKind.BG, newBg); } if (needStyle) { if (newStyle & StyleFlags.HAS_EXTENDED) this._extendedAttrs[idata] = attrs.extended; this._cacheSetStyleFlagsIndex(idata); - this._data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); + this.data()[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); } this._cacheSetColumnDataIndex(index, idata); let xdata = idata; // FIXME if (! atEnd) { if (needFg) { - this._data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); + this.data()[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); } if (needStyle) { - this._data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); + this.data()[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); } if (needBg) { - this._data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + this.data()[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); } } this._cacheSetFgBg(newFg, newBg); @@ -1214,10 +1107,10 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let idata = this._cachedDataIndex(); let precedingJoinState = inputHandler.precedingJoinState; let inext; - if (add || idata === this._dataLength || index === curColumn) + if (add || idata === this.dataLength() || index === curColumn) inext = idata; else { - const kind = BufferLine.wKind(this._data[idata]); + const kind = BufferLine.wKind(this.data()[idata]); if (BufferLine.wKindIsText(kind)) inext = this.clusterEnd(idata); else @@ -1242,10 +1135,10 @@ export class NewBufferLine extends BufferLine implements IBufferLine { 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; + const oldCount = (this.data()[idata] >> 21) & 0x3F; + const startChar = this.data()[idata] & 0x1FFFFF; // FIXME check for count overflow; - this._data[idata] = BufferLine.wSet1(kind, + this.data()[idata] = BufferLine.wSet1(kind, startChar + ((oldCount + 1) << 21)); kind = DataKind.CLUSTER_CONTINUED; curColumn += chWidth - oldWidth; @@ -1255,12 +1148,12 @@ export class NewBufferLine extends BufferLine implements IBufferLine { cellColumn = curColumn; curColumn += chWidth; } - this._data[inext++] = BufferLine.wSet1(kind, code); + this.data()[inext++] = BufferLine.wSet1(kind, code); } inputHandler.precedingJoinState = precedingJoinState; this._cacheSetColumnDataIndex(cellColumn, idata); - if (! insertMode && idata < this._dataLength) { - this.deleteCellsOnly(inext, 0, curColumn - startColumn); + if (! insertMode && idata < this.dataLength()) { + this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); } return curColumn; } @@ -1277,16 +1170,16 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let idata = this._cachedDataIndex(); let inext; if (codePoint === NULL_CELL_CODE && width > 0) { - if (idata === this._dataLength) + if (idata === this.dataLength()) return; // maybe set bg FIXME - const kind = BufferLine.wKind(this._data[idata]); + const kind = BufferLine.wKind(this.data()[idata]); if (kind === DataKind.SKIP_COLUMNS) return; // maybe set bg FIXME } - if (add || idata === this._dataLength || index === curColumn) + if (add || idata === this.dataLength() || index === curColumn) inext = idata; else { - const kind = BufferLine.wKind(this._data[idata]); + const kind = BufferLine.wKind(this.data()[idata]); if (BufferLine.wKindIsText(kind)) inext = this.clusterEnd(idata); else @@ -1300,13 +1193,13 @@ export class NewBufferLine extends BufferLine implements IBufferLine { idata = inext; cellColumn = curColumn; curColumn += width; - this._data[inext++] = + this.data()[inext++] = codePoint === NULL_CELL_CODE ? BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1) : BufferLine.wSet1(kind, codePoint); this._cacheSetColumnDataIndex(cellColumn, idata); - if (idata < this._dataLength) { - this.deleteCellsOnly(inext, 0, curColumn - startColumn); + if (idata < this.dataLength()) { + this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); } } @@ -1324,18 +1217,11 @@ export class NewBufferLine extends BufferLine implements IBufferLine { const clEnd = this.clusterEnd(idata); this.addEmptyDataElements(clEnd, 1); const nContinued = clEnd - idata; - const startChar = this._data[idata] & 0x1FFFFF; + const startChar = this.data()[idata] & 0x1FFFFF; const kind = width === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; - this._data[idata] = BufferLine.wSet1(kind, + this.data()[idata] = BufferLine.wSet1(kind, startChar + (nContinued << 21)); - this._data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); - } - - public eraseAll(bg: number): void { - // FIXME sometimes better to reuse old _data. - this._data = EMPTY_DATA; - this._dataLength = 0; - this.lineEndBg = bg; + this.data()[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); } /** @@ -1349,25 +1235,25 @@ export class NewBufferLine extends BufferLine implements IBufferLine { console.log("BufferLineNew.resize"); /* if (cols === this.length) { - return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; + return this.data().length * 4 * CLEANUP_THRESHOLD < this.data().buffer.byteLength; } const uint32Cells = cols * CELL_SIZE; if (cols > this.length) { - if (this._data.buffer.byteLength >= uint32Cells * 4) { + 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); + 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; + 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); + this.data() = this.data().subarray(0, uint32Cells); / * // Remove any cut off combined data const keys = Object.keys(this._combined); @@ -1387,25 +1273,9 @@ export class NewBufferLine extends BufferLine implements IBufferLine { } * / } - */ this.length = cols; - return this._dataLength * CLEANUP_THRESHOLD < this._data.length; - } - - /** - * 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._dataLength * CLEANUP_THRESHOLD < this._data.length) { - const data = new Uint32Array(this._data.length); - data.set(this._data); - this._data = data; - return 1; - } - return 0; + */ + return this.dataLength() * CLEANUP_THRESHOLD < this.data().length; } /** fill a line with fillCharData */ @@ -1415,36 +1285,37 @@ export class NewBufferLine extends BufferLine implements IBufferLine { /** alter to a full copy of line */ public copyFrom(xline: BufferLine): void { - const line = xline as NewBufferLine; + alert("copyFrom"); + /* + const line = xline as LogicalBufferLine; // FIXME if (this.length !== line.length) { this._data = new Uint32Array(line._data); } else { // use high speed copy if lengths are equal - this._data.set(line._data); + this.data().set(line.data()); } - this._dataLength = line._dataLength; + this.dataLength() = line.dataLength(); this.length = line.length; this._extendedAttrs = {}; 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 NewBufferLine(0); - newLine._data = new Uint32Array(this._data); - newLine.length = this.length; - newLine.isWrapped = this.isWrapped; + alert("NewBufferLine.clone"); + const newLine = new LogicalBufferLine(0); return newLine; } public getTrimmedLength(): number { let cols = 0; let skipped = 0; - for (let idata = 0; idata < this._dataLength; idata++) { - const word = this._data[idata]; + for (let idata = 0; idata < this.dataLength(); idata++) { + const word = this.data()[idata]; const kind = BufferLine.wKind(word); const w = kind === DataKind.CHAR_w2 || kind === DataKind.CLUSTER_START_w2 ? 2 : 1; let wcols = 0; @@ -1479,8 +1350,8 @@ export class NewBufferLine extends BufferLine implements IBufferLine { return this.getTrimmedLength(); // FIXME /* for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); + if ((this.data()[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this.data()[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) { + return i + (this.data()[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); } } return 0; @@ -1510,7 +1381,7 @@ export class NewBufferLine extends BufferLine implements IBufferLine { let pendingLength = 0; let pendingSkip = 0; //const text = this._text; - const data = this._data; + const data = this.data(); function pendingForce(handleSkip = ! trimRight): void { if (pendingStart >= 0 && pendingLength > 0) { s += utf32ToString(data, pendingStart, pendingStart + pendingLength); @@ -1536,8 +1407,8 @@ export class NewBufferLine extends BufferLine implements IBufferLine { } pendingLength += length; } - for (let idata = 0; idata < this._dataLength && col < endCol; idata++) { - const word = this._data[idata]; + 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; @@ -1587,3 +1458,208 @@ export class NewBufferLine extends BufferLine implements IBufferLine { 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. + protected _dataLength: number; // active length of _data array + // logicalWidth: number; TODO + // reflowNeeded: boolean; TODO + + constructor(cols: number, fillCellData?: IAttributeData) { + super(); + // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); + //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); + this._data = new Uint32Array(cols); + this._dataLength = 0; + this.length = cols; + this._isWrapped = false; + } + logicalLine(): LogicalBufferLine { return this; } + data(): Uint32Array { return this._data; } + dataLength(): number { return this._dataLength; } + + // count can be negative + addEmptyDataElements(position: number, count: number): void { + // FIXME also adjust _extendedAttr indexes + this.resizeData(this.dataLength() + count); + this.data().copyWithin(position + count, position, this._dataLength); + for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { + next.startIndex += count; + } + this._extendedAttrs.copyWithin(position + count, position, this._dataLength); + this._dataLength += count; + // FIXME cleanup old element s in _extendedAttr. + // But is it worth it - only matters for garbage collection? + // if (count > 0) for each element i in _data[position..position+count-1] ( + // if _data[i] & (0xC000000|HAS_EXTENDED) === (STYLE_FLAGS<<28)|HAS_EXTENDED + // delete this._extendedAttrs[i] + // else this._extendedAttr.length = this._dataLength; + } + + resizeData(size: number): void { + if (size > this.data().length) { + //buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); + const new_data = new Uint32Array((3 * size) >> 1); + new_data.set(this._data); + this.logicalLine()._data = new_data; + } + } + + /* + public eraseAll(bg: number): void { + // FIXME sometimes better to reuse old _data. + this._data = EMPTY_DATA; + this._dataLength = 0; + this.lineEndBg = bg; + } + */ + + /** + * 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.dataLength() * CLEANUP_THRESHOLD < this.data().length) { + 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 adjust at end of line. */ + public deleteCellsOnly(idata0: number, colOffset0: number, n: number): void { + let todo = n; + let idata = idata0; + let colOffset = colOffset0; + let word0 = this.data()[idata]; + let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; + let fgValue = -1; //cursor.getFg(); + let bgValue = -1; //cursor.getBg(); + let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs + /* + if (colOffset === 0) { + while (idata > 0) { + let skipItem = true; + switch (BufferLine.wKind(this.data()[idata-1])) { + case DataKind.BG: cursor.setBg(-1); break; + case DataKind.FG: cursor.setFg(-1); break; + case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; + default: skipItem = false; + } + if (skipItem) { + idata--; + dskip_first = idata; + dskip_last = idata0-1; + } else { + break; + } + } + } + */ + for (; todo > 0 && idata < this.dataLength(); idata++) { + let word = this.data()[idata]; + const kind = BufferLine.wKind(word); + switch (kind) { + case DataKind.FG: fgValue = word; break; + case DataKind.BG: bgValue = word; break; + case DataKind.STYLE_FLAGS: + styleValue = word; + // handle ExtendedAttrs FIXME + break; + case DataKind.SKIP_COLUMNS: + let wlen = BufferLine.wSkipCount(word); + if (colOffset === 0 && wlen <= todo) { + dskip_last = idata; + todo -= wlen; + } else { + let delta = Math.min(todo, wlen - colOffset); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); + dskip_first = idata + 1; + 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) { + dskip_last = idata; + todo -= 1 << w; + } else { + dskip_first = idata + 1; + /* + const delta = tend - tstart; + this._data[idata] = BufferLine.wSet1(kind, wlen - delta); + todo -= delta << 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; + dskip_last = idata; + todo -= (1 << w); + } else { + dskip_first = idata + 1; + } + colOffset = 0; + break; + } + } + idata0 = dskip_first; + if (bgValue >= 0) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); + } + if (fgValue >= 0) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); + } + if (styleValue >= 0) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); + } + if (dskip_last >= 0) { + // FIXME maybe use addEmptyDataElements with negative count + this.data().copyWithin(idata0, dskip_last + 1, this.dataLength()); + // FIXME also adjust _extendedAttr indexes + this._dataLength -= dskip_last + 1 - idata0; + } + } +} + +export class WrappedBufferLine extends NewBufferLine implements IBufferLine { + _logicalLine: LogicalBufferLine; + startIndex: number = 0; + startFg: number = 0; + startBg: number = 0; + startStyle: number = 0; + + constructor(logicalLine: LogicalBufferLine) { + super(); + this._logicalLine = logicalLine; + this._isWrapped = true; + } + logicalLine(): LogicalBufferLine { return this._logicalLine; } + data(): Uint32Array { return this._logicalLine.data(); } + dataLength(): number { return this._logicalLine.dataLength(); } + addEmptyDataElements(position: number, count: number): void { + this._logicalLine.addEmptyDataElements(position, count); + } + protected _cacheReset(): void { + this._cacheSetFgBg(this.startFg, this.startBg); + this._cacheSetStyleFlagsIndex(this.startStyle); + this._cacheSetColumnDataIndex(0, this.startIndex); + } + resizeData(size: number): void { this._logicalLine.resizeData(size); } + public cleanupMemory(): number { return 0;} +} diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index ad5c5704b6..bb3c2cadd0 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -35,6 +35,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; @@ -44,6 +45,7 @@ export interface IBuffer { addMarker(y: number): IMarker; clearMarkers(y: number): void; clearAllMarkers(): void; + setWrapped(row: number, value: boolean): void; } export interface IBufferSet extends IDisposable { diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index d20d0ceac0..b2bb18c30f 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -7,6 +7,7 @@ import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; import { BufferSet } from 'common/buffer/BufferSet'; +import { USE_NewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; @@ -60,17 +61,29 @@ export class BufferService extends Disposable implements IBufferService { */ public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { const buffer = this.buffer; + const topRow = buffer.ybase + buffer.scrollTop; + const bottomRow = buffer.ybase + buffer.scrollBottom; let newLine: IBufferLine | undefined; - newLine = this._cachedBlankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { - newLine = buffer.getBlankLine(eraseAttr, isWrapped); - this._cachedBlankLine = newLine; + if (USE_NewBufferLine) { + if (isWrapped) { + const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as NewBufferLine; + newLine = new WrappedBufferLine(oldLine.logicalLine()); + } else { + newLine = new LogicalBufferLine(0, eraseAttr); + } + } else { + newLine = this._cachedBlankLine; + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = buffer.getBlankLine(eraseAttr, isWrapped); + this._cachedBlankLine = newLine; + } + newLine._isWrapped = isWrapped; + if (buffer.scrollTop !== 0 || bottomRow !== buffer.lines.length - 1 + || ! buffer.lines.isFull) { + newLine = newLine.clone(); + } } - newLine.isWrapped = isWrapped; - - const topRow = buffer.ybase + buffer.scrollTop; - const bottomRow = buffer.ybase + buffer.scrollBottom; if (buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. @@ -78,13 +91,15 @@ export class BufferService extends Disposable implements IBufferService { // Insert the line using the fastest method if (bottomRow === buffer.lines.length - 1) { - if (willBufferBeTrimmed) { - buffer.lines.recycle().copyFrom(newLine); + if (! willBufferBeTrimmed) { + buffer.lines.push(newLine); + } else if (USE_NewBufferLine) { + buffer.lines.set(bottomRow, newLine); // ??? } else { - buffer.lines.push(newLine.clone()); + buffer.lines.recycle().copyFrom(newLine); } } else { - buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + buffer.lines.splice(bottomRow + 1, 0, newLine); } // Only adjust ybase and ydisp when the buffer is not trimmed @@ -106,7 +121,7 @@ export class BufferService extends Disposable implements IBufferService { // scrollback, instead we can just shift them in-place. const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - buffer.lines.set(bottomRow, newLine.clone()); + buffer.lines.set(bottomRow, newLine); } // Move the viewport to the bottom of the buffer unless the user is From 733093ec01cdf35da4848cbb4be501e692c976f0 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 22 Nov 2023 16:49:47 -0800 Subject: [PATCH 24/73] Basic reflow handling on window-width changes. This reflows the lines and calculates WrapperBufferLine offsets. It does *not* yet handle updating y, ybase, ydist, calling onDeleteEmitter handler, and similar tasks. --- src/common/InputHandler.ts | 1 - src/common/buffer/Buffer.ts | 60 +++++++++++++++++++- src/common/buffer/BufferLine.ts | 98 ++++++++++++++++++++++----------- 3 files changed, 126 insertions(+), 33 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index bc72ab0be1..93d7d1c736 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -563,7 +563,6 @@ export class InputHandler extends Disposable implements IInputHandler { } */ // col = ...; - break; // FIXME } else { // FIXME delete excess break; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index cb94f6fe14..522b0503b8 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, USE_NewBufferLine, NewBufferLine, WrappedBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, USE_NewBufferLine, 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'; @@ -192,6 +192,17 @@ export class Buffer implements IBuffer { this.lines.maxLength = newMaxLength; } + if (this._cols !== newCols && USE_NewBufferLine) { + const nlines = this.lines.length; + for (let i = 0; i < nlines; i++) { + const line = this.lines.get(i); + if (line instanceof LogicalBufferLine + && (line.nextRowSameLine || line.logicalWidth > newCols)) { + line.reflowNeeded = true; + } + } + } + // The following adjustments should only happen if the buffer has been // initialized/filled. if (this.lines.length > 0) { @@ -328,11 +339,58 @@ export class Buffer implements IBuffer { return this._hasScrollback && !this._optionsService.rawOptions.windowsMode; } + // Only if USE_NewBufferLine + private _reflowRegion(startRow: number, endRow: number, newCols: number): void { + while (startRow > 0 && this.lines.get(startRow)?.isWrapped) { + startRow--; + } + // FIXME don't need to allocate newRows if no lines require more rows + // than before. So better to allocate newRows lazily. + const newRows: IBufferLine[] = []; + let inew = 0; + for (let row = startRow; row < endRow;) { + const line = this.lines.get(row)!; + newRows.push(line); + row++ + if (line instanceof LogicalBufferLine && line.reflowNeeded) { + line.reflowNeeded = false; + let startCol = 0; + let curRow: NewBufferLine = line; + const dataLength = line.dataLength(); + for (;;) { + const content = line.moveToColumn(startCol + newCols, true); + let idata = line._cachedDataIndex(); + if (idata >= dataLength) + break; + startCol = line._cachedColumn(); + let newRow1 = row < endRow && this.lines.get(row); + let newRow = newRow1 instanceof WrappedBufferLine + ? (row++, newRow1) + : new WrappedBufferLine(line); + line.setStartFromCache(newRow); + curRow.nextRowSameLine = newRow; + newRows.push(newRow); + curRow = newRow; + } + while (row < endRow + && this.lines.get(row) instanceof WrappedBufferLine) { + row++; + } + } + } + this.lines.splice(startRow, endRow - startRow, ...newRows); + } + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; } + if (USE_NewBufferLine) { + // FIXME do this lazily + this._reflowRegion(0, this.lines.length, newCols); + return; + } // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { this._reflowLarger(newCols, newRows); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 944895e02d..cf677634e4 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -123,10 +123,10 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; /** - * Set data at `index` to `cell`. + * Set data at `index` to `cell`. FIXME doesn't handle combined chars. */ public setCell(index: number, cell: ICellData): void { - this.setCellFromCodepoint(index, cell.content, cell.getWidth(), cell); + this.setCellFromCodepoint(index, cell.getCode(), cell.getWidth(), cell); } /** @@ -247,6 +247,8 @@ const enum Cell { BG = 2 // currently unused } +// This class will be removed at some point + export class OldBufferLine extends BufferLine implements IBufferLine { protected _data: Uint32Array; protected _combined: {[index: number]: string} = {}; @@ -669,37 +671,35 @@ export class OldBufferLine extends BufferLine implements IBufferLine { } } +// This class will be merged with its parent when OldBufferLine is removed, + export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Color for "rest of line" background, following _dataLength. */ lineEndBg: number = 0; nextRowSameLine: WrappedBufferLine | undefined; - // Maybe move these to LogicalBufferLine? or to Buffer? - private _cache1: number = 0; - private _cache2: number = 0; - private _cache3: number = 0; - private _cache4: number = 0; - /** 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.) */ - protected _cachedDataIndex(): number { return this._cache1 >>> 16; } - /** The column number corresponding to _cachedDataIndex(). */ - private _cachedColumn(): number { return this._cache1 & 0xFFFF; } + _cachedDataIndex(): number { return this.logicalLine()._cache1 >>> 16; } + /** The logical column number corresponding to _cachedDataIndex(). */ + _cachedColumn(): number { return this.logicalLine()._cache1 & 0xFFFF; } + protected abstract _cachedColumnInRow(): number; //private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED - protected _cachedBg(): number { return this._cache2; } - protected _cachedFg(): number { return this._cache3; } + protected _cachedBg(): number { return this.logicalLine()._cache2; } + protected _cachedFg(): number { return this.logicalLine()._cache3; } // One more than index (in data()) of STYLE_FLAGS; 0 if none. - protected _cachedStyleFlagsIndex(): number { return this._cache4; } - protected _cacheReset(): void { this._cache1 = 0; this._cache2 = 0; this._cache3 = 0; this._cache4 = 0; } - protected _cacheSetFgBg(fg: number, bg: number): void { this._cache2 = bg; this._cache3 = fg; } - protected _cacheSetStyleFlagsIndex(index: number): void { this._cache4 = index; } - protected _cacheSetColumnDataIndex(column: number, dataIndex: number): void { this._cache1 = (dataIndex << 16) | (column & 0xFFFF); } + 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 = 0; } + 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: number, 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(); @@ -908,7 +908,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { */ } - public moveToColumn(index: number): number { + /** Move to column 'index'. + * If 'logical' true, 'index' is relative to logical line; + * otherwise relative to (and restricted to) this (screen) row. + * Return encoded 'content'. + */ + public moveToColumn(index: number, logical: boolean = false): number { + const startColumn = ! logical && this instanceof WrappedBufferLine ? this.startColumn : 0; + index += startColumn; let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better @@ -923,8 +930,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let word; let kind; let content = 0; + const end = logical ? this.dataLength() : this.dataRowEnd(); while (todo >= 0) { - if (idata >= this.dataLength()) { + if (idata >= end) { word = NULL_DATA_WORD; kind = DataKind.SKIP_COLUMNS; content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; @@ -1103,6 +1111,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, insertMode: boolean): number { const add = this.preInsert(index, attrs); let curColumn = this._cachedColumn(); + const lline = this.logicalLine(); const startColumn = curColumn; let idata = this._cachedDataIndex(); let precedingJoinState = inputHandler.precedingJoinState; @@ -1155,13 +1164,15 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { if (! insertMode && idata < this.dataLength()) { this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); } + if (curColumn > lline.logicalWidth) + lline.logicalWidth = curColumn; return curColumn; } public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { if (codePoint === NULL_CELL_CODE && width === 0) { // i.e. combining character - // FIXME - usually a no-open + // FIXME - usually a no-op return; } const add = this.preInsert(index, attrs); @@ -1185,21 +1196,28 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { else inext = idata; } - // FIXME optimize of overwriting simple text in-place - this.addEmptyDataElements(inext, 1); - let cellColumn = curColumn; let kind = width === 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; idata = inext; cellColumn = curColumn; curColumn += width; - this.data()[inext++] = - codePoint === NULL_CELL_CODE ? - BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1) : - BufferLine.wSet1(kind, codePoint); + if (codePoint===0 && inext > 0 + && BufferLine.wKind(this.data()[inext-1]) === DataKind.SKIP_COLUMNS) { + console.log("nul setCellFromCodepoint"); + this.data()[inext-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, BufferLine.wSkipCount(this.data()[inext-1]) + 1); + cellColumn = curColumn; + } else { + // FIXME optimize of overwriting simple text in-place + this.addEmptyDataElements(inext, 1); + this.data()[inext++] = + codePoint === NULL_CELL_CODE ? + BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1) : + BufferLine.wSet1(kind, codePoint); + } this._cacheSetColumnDataIndex(cellColumn, idata); if (idata < this.dataLength()) { this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); + } else if (codePoint === 0) { } } @@ -1463,8 +1481,14 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { protected _data: Uint32Array; // Each item in _data is a 4-bit DataKind and 28 bits data. protected _dataLength: number; // active length of _data array - // logicalWidth: number; TODO - // reflowNeeded: boolean; TODO + 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 = 0; constructor(cols: number, fillCellData?: IAttributeData) { super(); @@ -1479,6 +1503,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { data(): Uint32Array { return this._data; } dataLength(): number { return this._dataLength; } + protected _cachedColumnInRow(): number { return (this.logicalLine()._cache1 & 0xFFFF); } + // count can be negative addEmptyDataElements(position: number, count: number): void { // FIXME also adjust _extendedAttr indexes @@ -1640,6 +1666,15 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { export class WrappedBufferLine extends NewBufferLine implements IBufferLine { _logicalLine: LogicalBufferLine; startIndex: number = 0; + /** 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: number = 0; startFg: number = 0; startBg: number = 0; startStyle: number = 0; @@ -1655,10 +1690,11 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { addEmptyDataElements(position: number, count: number): void { this._logicalLine.addEmptyDataElements(position, count); } + protected _cachedColumnInRow(): number { return (this.logicalLine()._cache1 & 0xFFFF) - this.startColumn; } protected _cacheReset(): void { this._cacheSetFgBg(this.startFg, this.startBg); this._cacheSetStyleFlagsIndex(this.startStyle); - this._cacheSetColumnDataIndex(0, this.startIndex); + this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); } resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} From ed1b672669eaa89018f30a432b02799f30c3eed9 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 23 Nov 2023 21:36:06 -0800 Subject: [PATCH 25/73] _showData debug helper: Don't use hex for counts. --- src/common/buffer/BufferLine.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index cf677634e4..20be3d24a6 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -814,13 +814,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } else if (kind === DataKind.CLUSTER_START_w1 || DataKind.CLUSTER_START_w2) { // FIXME extract cluster as string - value = '#' + (word & 0x1fffff).toString(16); + value = '*' + (word & 0x1fffff); } else if (kind === DataKind.CLUSTER_CONTINUED) { - value = '#' + (word & 0x1fffff).toString(16); + value = '*' + (word & 0x1fffff); } else if (kind === DataKind.BG || kind === DataKind.FG) { value = (wnum >> 24) + '#' + (wnum & 0xffffff).toString(16); } else if (kind !== DataKind.SKIP_COLUMNS) { - value = '#' + wnum.toString(16); + value = wnum; } else { value = wnum.toString(); } From 43c8e432dc84cedf5ef3dbb86be407a6b7c8c6bc Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 23 Nov 2023 21:36:49 -0800 Subject: [PATCH 26/73] Adjust buffer.y on window resize. --- src/common/buffer/Buffer.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 522b0503b8..8adb9d3038 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -348,11 +348,15 @@ export class Buffer implements IBuffer { // than before. So better to allocate newRows lazily. const newRows: IBufferLine[] = []; let inew = 0; + const yAbs = this.ybase + this.y; + let deltaSoFar = 0; for (let row = startRow; row < endRow;) { const line = this.lines.get(row)!; newRows.push(line); row++ if (line instanceof LogicalBufferLine && line.reflowNeeded) { + const oldWrapStart = row; + const newWrapStart = newRows.length; line.reflowNeeded = false; let startCol = 0; let curRow: NewBufferLine = line; @@ -376,8 +380,30 @@ export class Buffer implements IBuffer { && this.lines.get(row) instanceof WrappedBufferLine) { row++; } + const oldWrapCount = row - oldWrapStart; + const newWrapCount = newRows.length - newWrapStart; + /* + if (newWrapCount !== oldWrapCount) { + if (yAbs >= row && yAbs < row + oldWrapCount) { + let y = yAbs; + if (y > row + newWrapCount) + y = row + newWrapCount; + this.y = y + deltaSoFar; + } + } + */ + deltaSoFar += newWrapCount - oldWrapCount; } + if (deltaSoFar !== 0 && yAbs === row) { + this.y = yAbs - this.ybase + Math.min(deltaSoFar, newRows.length); + } + } + if (deltaSoFar !== 0) { + if (yAbs >= endRow) + this.y += deltaSoFar; } + // FIXME. This calls onDeleteEmitter and onInsertEmitter events, + // which we want handled at finer granularity. this.lines.splice(startRow, endRow - startRow, ...newRows); } From 214a82005e2c8b78b0633afaa64039ead22153e1 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 27 Nov 2023 10:59:29 -0800 Subject: [PATCH 27/73] New eraseCells function. Other fixes. --- src/common/InputHandler.ts | 18 +++---- src/common/Types.d.ts | 2 +- src/common/buffer/BufferLine.ts | 87 ++++++++++++++++++++------------- 3 files changed, 63 insertions(+), 44 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 93d7d1c736..5af842a9ea 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -10,7 +10,7 @@ import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { BufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, OldBufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter } from 'common/EventEmitter'; 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'; @@ -667,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); @@ -1208,11 +1208,11 @@ export class InputHandler extends Disposable implements IInputHandler { const row = this._activeBuffer.ybase + y; const line = this._activeBuffer.lines.get(row)!; const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); - //if (respectProtect) { + if (! respectProtect && line instanceof NewBufferLine) { + line.eraseCells(start, end - start, fill); + } else { line.replaceCells(start, end, fill, respectProtect); - //} else { - // line.deleteCells(start, end - start, fill); - //} + } if (clearWrap) { this._activeBuffer.setWrapped(row, false); } @@ -1228,9 +1228,9 @@ export class InputHandler extends Disposable implements IInputHandler { const row = buffer.ybase + y; const line = buffer.lines.get(row); if (line) { - /*if (line instanceof NewBufferLine && ! respectProtect) { - line.eraseAll(this._curAttrData.bg & ~0xFC000000); - } else*/ { + if (line instanceof NewBufferLine && ! respectProtect) { + line.eraseCells(0, this._bufferService.cols, this._eraseAttrData()); + } else { const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); line.fill(fillData, respectProtect); } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 27a76bf297..0733a752c0 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -240,7 +240,7 @@ export interface IBufferLine { 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; //eraseAll(bg: number): void; deleteCells(pos: number, n: number, fill: ICellData): void; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 20be3d24a6..51f6ca175e 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -27,7 +27,7 @@ export abstract class AbstractBufferLine implements IBufferLine { get isWrapped(): boolean { return this._isWrapped; } //abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; - abstract addCodepointToCell(index: number, codePoint: number, width: number): void; + abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED abstract resize(cols: number, fillCellData: ICellData): boolean; abstract fill(fillCellData: ICellData, respectProtect: boolean): void; abstract copyFrom(line: BufferLine): void; @@ -812,13 +812,15 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } value = JSON.stringify(str); } else if (kind === DataKind.CLUSTER_START_w1 - || DataKind.CLUSTER_START_w2) { + || kind === DataKind.CLUSTER_START_w2) { // FIXME extract cluster as string value = '*' + (word & 0x1fffff); } else if (kind === DataKind.CLUSTER_CONTINUED) { value = '*' + (word & 0x1fffff); } else if (kind === DataKind.BG || kind === DataKind.FG) { value = (wnum >> 24) + '#' + (wnum & 0xffffff).toString(16); + } else if (DataKind.STYLE_FLAGS) { + value = '#' + (wnum & 0xfffffff).toString(16); } else if (kind !== DataKind.SKIP_COLUMNS) { value = wnum; } else { @@ -1169,10 +1171,41 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return curColumn; } + public eraseCells(start: number, end: number, attrs: IAttributeData): void { + const add = this.preInsert(start, attrs); + let idata = this._cachedDataIndex(); + let colOffset = start - this._cachedColumn(); + const lline = this.logicalLine(); + lline.deleteCellsOnly(idata, colOffset, end - start); + idata = this._cachedDataIndex(); + const data = this.data(); + const count = end - start; + if (idata > 0 && BufferLine.wKind(data[idata-1]) === DataKind.SKIP_COLUMNS) { + if (idata === this.dataLength()) { + end = start; + idata--; + lline._dataLength = idata; + } else { + data[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, BufferLine.wSkipCount(data[idata - 1]) + count); + } + } else { + if (idata === this.dataLength()) { + return; + } + this.addEmptyDataElements(idata, 1); + data[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, count); + } + this._cacheSetColumnDataIndex(end, idata); + } + public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { - if (codePoint === NULL_CELL_CODE && width === 0) { - // i.e. combining character - // FIXME - usually a no-op + 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 add = this.preInsert(index, attrs); @@ -1180,13 +1213,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const startColumn = curColumn; let idata = this._cachedDataIndex(); let inext; - if (codePoint === NULL_CELL_CODE && width > 0) { - if (idata === this.dataLength()) - return; // maybe set bg FIXME - const kind = BufferLine.wKind(this.data()[idata]); - if (kind === DataKind.SKIP_COLUMNS) - return; // maybe set bg FIXME - } if (add || idata === this.dataLength() || index === curColumn) inext = idata; else { @@ -1201,23 +1227,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { idata = inext; cellColumn = curColumn; curColumn += width; - if (codePoint===0 && inext > 0 - && BufferLine.wKind(this.data()[inext-1]) === DataKind.SKIP_COLUMNS) { - console.log("nul setCellFromCodepoint"); - this.data()[inext-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, BufferLine.wSkipCount(this.data()[inext-1]) + 1); - cellColumn = curColumn; - } else { - // FIXME optimize of overwriting simple text in-place - this.addEmptyDataElements(inext, 1); - this.data()[inext++] = - codePoint === NULL_CELL_CODE ? - BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1) : - BufferLine.wSet1(kind, codePoint); - } + // 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, curColumn - startColumn); - } else if (codePoint === 0) { } } @@ -1291,8 +1306,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } * / } - this.length = cols; */ + this.length = cols; return this.dataLength() * CLEANUP_THRESHOLD < this.data().length; } @@ -1480,7 +1495,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { export class LogicalBufferLine extends NewBufferLine implements IBufferLine { protected _data: Uint32Array; // Each item in _data is a 4-bit DataKind and 28 bits data. - protected _dataLength: number; // active length of _data array + _dataLength: number; // active length of _data array logicalWidth: number = 0; // FIXME needs work updating this reflowNeeded: boolean = false; @@ -1509,7 +1524,11 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { addEmptyDataElements(position: number, count: number): void { // FIXME also adjust _extendedAttr indexes this.resizeData(this.dataLength() + count); - this.data().copyWithin(position + count, position, this._dataLength); + if (count < 0) { + this.data().copyWithin(position, position - count, this._dataLength); + } else { + this.data().copyWithin(position + count, position, this._dataLength); + } for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { next.startIndex += count; } @@ -1532,7 +1551,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } - /* + /* ??? public eraseAll(bg: number): void { // FIXME sometimes better to reuse old _data. this._data = EMPTY_DATA; @@ -1571,6 +1590,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { let fgValue = -1; //cursor.getFg(); let bgValue = -1; //cursor.getBg(); let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs + /* if (colOffset === 0) { while (idata > 0) { @@ -1591,6 +1611,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } */ + for (; todo > 0 && idata < this.dataLength(); idata++) { let word = this.data()[idata]; const kind = BufferLine.wKind(word); @@ -1655,10 +1676,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); } if (dskip_last >= 0) { - // FIXME maybe use addEmptyDataElements with negative count - this.data().copyWithin(idata0, dskip_last + 1, this.dataLength()); - // FIXME also adjust _extendedAttr indexes - this._dataLength -= dskip_last + 1 - idata0; + const dcount = dskip_last + 1 - idata0; + this.addEmptyDataElements(idata0, - dcount); } } } From 41db1f288801bf3377ed72ad08821b5f8d7e36bd Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 29 Nov 2023 15:00:46 -0800 Subject: [PATCH 28/73] Various fixes. --- src/common/InputHandler.ts | 2 +- src/common/buffer/BufferLine.ts | 59 ++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 5af842a9ea..ba2119acf5 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1209,7 +1209,7 @@ export class InputHandler extends Disposable implements IInputHandler { const line = this._activeBuffer.lines.get(row)!; const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); if (! respectProtect && line instanceof NewBufferLine) { - line.eraseCells(start, end - start, fill); + line.eraseCells(start, end, fill); } else { line.replaceCells(start, end, fill, respectProtect); } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 51f6ca175e..a100b0c720 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -772,6 +772,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return this.translateToString(true, 0, this.length, skipReplace); } + _showRowData(): string { + return this._showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); + } /* Human-readable display of data() array, for debugging */ _showData(start = 0, end = this.dataLength()) { let s = '['; @@ -922,7 +925,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { if (index < curColumn) { // FIXME can sometimes do better this._cacheReset(); - curColumn = 0; + curColumn = startColumn; } let idata = this._cachedDataIndex(); let fg = this._cachedFg(); @@ -1265,7 +1268,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * excess memory (true after shrinking > CLEANUP_THRESHOLD). */ public resize(cols: number, fillCellData: ICellData): boolean { - console.log("BufferLineNew.resize"); + console.log("BufferLineNew.resize "+this.length+"->"+cols); /* if (cols === this.length) { return this.data().length * 4 * CLEANUP_THRESHOLD < this.data().buffer.byteLength; @@ -1344,17 +1347,23 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return newLine; } - public getTrimmedLength(): number { + public getTrimmedLength(countBackground: boolean = false): number { let cols = 0; let skipped = 0; - for (let idata = 0; idata < this.dataLength(); idata++) { - const word = this.data()[idata]; + const startColumn = this instanceof WrappedBufferLine ? this.startColumn : 0; + const data = this.data(); + const end = this.dataRowEnd(); + 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.FG: case DataKind.BG: + bg = word & 0x3ffffff; + break; + case DataKind.FG: case DataKind.STYLE_FLAGS: break; case DataKind.SKIP_COLUMNS: @@ -1362,33 +1371,27 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { break; case DataKind.CLUSTER_START_w1: case DataKind.CLUSTER_START_w2: - wcols = w; + const clEnd = this.clusterEnd(idata); + wcols = w * (clEnd - idata); + idata = clEnd - 1; break; case DataKind.CHAR_w1: case DataKind.CHAR_w2: - wcols = w * 1; + wcols = w; break; case DataKind.CLUSTER_CONTINUED: - break; + break; // should be skipped } if (wcols) { cols += skipped + wcols; skipped = 0; } } - return cols; + return countBackground && bg !== 0 ? this.length : cols; } public getNoBgTrimmedLength(): number { - return this.getTrimmedLength(); // FIXME - /* - for (let i = this.length - 1; i >= 0; --i) { - if ((this.data()[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this.data()[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) { - return i + (this.data()[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); - } - } - return 0; - */ + return this.getTrimmedLength(true); } public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void { @@ -1530,7 +1533,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.data().copyWithin(position + count, position, this._dataLength); } for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { - next.startIndex += count; + if (next.startIndex > position) + next.startIndex += count; } this._extendedAttrs.copyWithin(position + count, position, this._dataLength); this._dataLength += count; @@ -1583,6 +1587,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { /** Internal - delete n columns, with 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 word0 = this.data()[idata]; @@ -1591,14 +1596,14 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { let bgValue = -1; //cursor.getBg(); let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs - /* if (colOffset === 0) { while (idata > 0) { let skipItem = true; - switch (BufferLine.wKind(this.data()[idata-1])) { - case DataKind.BG: cursor.setBg(-1); break; - case DataKind.FG: cursor.setFg(-1); break; - case DataKind.STYLE_FLAGS: cursor.setStyleFlags(-1 as StyleFlags); break; + const word = data[idata-1]; + switch (BufferLine.wKind(word)) { + case DataKind.BG: bgValue = word; break; + case DataKind.FG: fgValue = word; break; + case DataKind.STYLE_FLAGS: styleValue = word; break; default: skipItem = false; } if (skipItem) { @@ -1610,10 +1615,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } } - */ for (; todo > 0 && idata < this.dataLength(); idata++) { - let word = this.data()[idata]; + let word = data[idata]; const kind = BufferLine.wKind(word); switch (kind) { case DataKind.FG: fgValue = word; break; @@ -1702,6 +1706,7 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { super(); this._logicalLine = logicalLine; this._isWrapped = true; + this.length = logicalLine.length; } logicalLine(): LogicalBufferLine { return this._logicalLine; } data(): Uint32Array { return this._logicalLine.data(); } From 632187f91e0599ea8f76f04372fca8787c640551 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 30 Nov 2023 16:58:39 -0800 Subject: [PATCH 29/73] Add logOutput, a debug hook for Terminal.write. --- src/browser/public/Terminal.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 2c75d7b808..7e712e2caf 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -26,6 +26,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(); @@ -216,6 +217,13 @@ 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 == null) + 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 { From 7f2fc3774aab8a7b95d8eeb5e2f0b295310ea945 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 30 Nov 2023 16:59:45 -0800 Subject: [PATCH 30/73] Fixes to scrolling and line wrapping. --- src/common/InputHandler.ts | 17 ++++++----------- src/common/buffer/Buffer.ts | 26 +++++++++++++++++--------- src/common/buffer/BufferLine.ts | 21 ++++++++++++++++----- src/common/services/BufferService.ts | 4 ++-- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index ba2119acf5..db002beb8b 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -527,28 +527,23 @@ export class InputHandler extends Disposable implements IInputHandler { // autowrap - DECAWM // automatically wraps to the beginning of the next line if (wraparoundMode) { - const oldRow = bufferRow; + const oldRow = bufferRow as NewBufferLine; let oldCol = this._activeBuffer.x; //this._activeBuffer.x = oldWidth; this._activeBuffer.y++; if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { this._activeBuffer.y--; this._bufferService.scroll(this._eraseAttrData(), true); - bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; } else { if (this._activeBuffer.y >= this._bufferService.rows) { this._activeBuffer.y = this._bufferService.rows - 1; } - bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - // The line already exists (eg. the initial viewport), mark it as a - // wrapped line - if (bufferRow.isWrapped) { - // FIXME - } else { - this._activeBuffer.splitLine(this._activeBuffer.y, col); - col = col - cols; - } } + this._activeBuffer.splitLine(this._activeBuffer.y, col); + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.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) { diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 8adb9d3038..b63fe0641e 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -117,31 +117,40 @@ export class Buffer implements IBuffer { 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.moveToColumn(bufferService.cols); + curRow.moveToColumn(curRow.logicalStartColumn() + bufferService.cols, true); // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); let newRow; if (nextRow.isWrapped) { newRow = nextRow as WrappedBufferLine; } else { - newRow = new WrappedBufferLine(curRow.logicalLine()); + newRow = new WrappedBufferLine(curRow); // append nextRow contents to end of curRow.logicalLine() this.lines.set(this.ybase + row, newRow); - curRow.nextRowSameLine = newRow; } curRow.setStartFromCache(newRow); } - public setWrapped(row: number, value: boolean): void { - const line = this.lines.get(row); + public setWrapped(absrow: number, value: boolean): void { + const line = this.lines.get(absrow); if (! line || line.isWrapped === value) return; if (! USE_NewBufferLine) { line!._isWrapped = value; } else if (value) { - // make wrapped FIXME + alert("setWrapped true"); // only used in test cases? } else { // clear wrapped FIXME - } + const prevRow = this.lines.get(absrow - 1) as NewBufferLine; + const curRow = line as WrappedBufferLine; + prevRow.nextRowSameLine = undefined; + const oldLine = prevRow.logicalLine(); + const newRow = new LogicalBufferLine(line.length, undefined, oldLine); + const oldLength = oldLine._dataLength; + const oldStart = curRow.startIndex; + newRow.addEmptyDataElements(oldStart, oldLength - oldStart); + oldLine._dataLength = curRow.startIndex; + this.lines.set(absrow, newRow); + } } /** @@ -370,9 +379,8 @@ export class Buffer implements IBuffer { let newRow1 = row < endRow && this.lines.get(row); let newRow = newRow1 instanceof WrappedBufferLine ? (row++, newRow1) - : new WrappedBufferLine(line); + : new WrappedBufferLine(curRow); line.setStartFromCache(newRow); - curRow.nextRowSameLine = newRow; newRows.push(newRow); curRow = newRow; } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index a100b0c720..e16bd0ce5f 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -720,6 +720,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { //public length: number; abstract logicalLine(): LogicalBufferLine; + abstract logicalStartColumn(): number; abstract data(): Uint32Array; abstract resizeData(size: number): void; abstract addEmptyDataElements(position: number, count: number): void; @@ -1350,7 +1351,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public getTrimmedLength(countBackground: boolean = false): number { let cols = 0; let skipped = 0; - const startColumn = this instanceof WrappedBufferLine ? this.startColumn : 0; + const startColumn = this.logicalStartColumn(); const data = this.data(); const end = this.dataRowEnd(); let bg = this._cachedBg(); @@ -1508,16 +1509,22 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { _cache3: number = 0; _cache4: number = 0; - constructor(cols: number, fillCellData?: IAttributeData) { + constructor(cols: number, fillCellData?: IAttributeData, src?: LogicalBufferLine) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); - this._data = new Uint32Array(cols); - this._dataLength = 0; + if (src) { + this._data = src._data.slice(); + this._dataLength = src._dataLength; + } else { + this._data = new Uint32Array(cols); + this._dataLength = 0; + } this.length = cols; this._isWrapped = false; } logicalLine(): LogicalBufferLine { return this; } + logicalStartColumn(): number { return 0; } data(): Uint32Array { return this._data; } dataLength(): number { return this._dataLength; } @@ -1702,13 +1709,17 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { startBg: number = 0; startStyle: number = 0; - constructor(logicalLine: LogicalBufferLine) { + constructor(prevRow: NewBufferLine) { super(); + const logicalLine = prevRow.logicalLine(); + prevRow.nextRowSameLine = this; this._logicalLine = logicalLine; this._isWrapped = true; this.length = logicalLine.length; } + logicalLine(): LogicalBufferLine { return this._logicalLine; } + logicalStartColumn(): number { return this.startColumn; } data(): Uint32Array { return this._logicalLine.data(); } dataLength(): number { return this._logicalLine.dataLength(); } addEmptyDataElements(position: number, count: number): void { diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index b2bb18c30f..84420f2dc9 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -68,9 +68,9 @@ export class BufferService extends Disposable implements IBufferService { if (USE_NewBufferLine) { if (isWrapped) { const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as NewBufferLine; - newLine = new WrappedBufferLine(oldLine.logicalLine()); + newLine = new WrappedBufferLine(oldLine); } else { - newLine = new LogicalBufferLine(0, eraseAttr); + newLine = new LogicalBufferLine(this.cols, eraseAttr); } } else { newLine = this._cachedBlankLine; From 0deb6172966b677dcc0160bf535fee8d6b89488a Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 3 Dec 2023 14:54:34 -0800 Subject: [PATCH 31/73] Make erase work better. Add RowColumn and LineColumn type aliases for documentation. --- src/common/InputHandler.ts | 6 +- src/common/buffer/Buffer.ts | 6 +- src/common/buffer/BufferLine.ts | 101 +++++++++++++++++++------------- 3 files changed, 65 insertions(+), 48 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index db002beb8b..b92631d320 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1223,11 +1223,11 @@ export class InputHandler extends Disposable implements IInputHandler { const row = buffer.ybase + y; const line = buffer.lines.get(row); if (line) { + const eraseAttrs = this._eraseAttrData(); if (line instanceof NewBufferLine && ! respectProtect) { - line.eraseCells(0, this._bufferService.cols, this._eraseAttrData()); + line.eraseCells(0, this._bufferService.cols, eraseAttrs); } else { - const fillData = this._activeBuffer.getNullCell(this._eraseAttrData()); - line.fill(fillData, respectProtect); + line.fill(this._activeBuffer.getNullCell(eraseAttrs), respectProtect); } buffer.clearMarkers(this._activeBuffer.ybase + y); buffer.setWrapped(row, false); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index b63fe0641e..655da23301 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -117,7 +117,7 @@ export class Buffer implements IBuffer { 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.moveToColumn(curRow.logicalStartColumn() + bufferService.cols, true); + curRow.moveToLineColumn(curRow.logicalStartColumn() + bufferService.cols); // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); let newRow; if (nextRow.isWrapped) { @@ -147,7 +147,7 @@ export class Buffer implements IBuffer { const newRow = new LogicalBufferLine(line.length, undefined, oldLine); const oldLength = oldLine._dataLength; const oldStart = curRow.startIndex; - newRow.addEmptyDataElements(oldStart, oldLength - oldStart); + newRow.addEmptyDataElements(0, - oldStart); oldLine._dataLength = curRow.startIndex; this.lines.set(absrow, newRow); } @@ -371,7 +371,7 @@ export class Buffer implements IBuffer { let curRow: NewBufferLine = line; const dataLength = line.dataLength(); for (;;) { - const content = line.moveToColumn(startCol + newCols, true); + const content = line.moveToLineColumn(startCol + newCols); let idata = line._cachedDataIndex(); if (idata >= dataLength) break; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index e16bd0ce5f..3cf020f1fa 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -12,6 +12,18 @@ import { UnicodeService } from 'common/services/UnicodeService'; 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; + const EMPTY_DATA = new Uint32Array(0); // Work variables to avoid garbage collection @@ -685,8 +697,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { */ _cachedDataIndex(): number { return this.logicalLine()._cache1 >>> 16; } /** The logical column number corresponding to _cachedDataIndex(). */ - _cachedColumn(): number { return this.logicalLine()._cache1 & 0xFFFF; } - protected abstract _cachedColumnInRow(): number; + _cachedColumn(): LineColumn { return this.logicalLine()._cache1 & 0xFFFF; } + protected abstract _cachedColumnInRow(): RowColumn; //private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED protected _cachedBg(): number { return this.logicalLine()._cache2; } protected _cachedFg(): number { return this.logicalLine()._cache3; } @@ -695,7 +707,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { protected _cacheReset(): void { const line = this.logicalLine(); line._cache1 = 0; line._cache2 = 0; line._cache3 = 0; line._cache4 = 0; } 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: number, dataIndex: number): void { this.logicalLine()._cache1 = (dataIndex << 16) | (column & 0xFFFF); } + protected _cacheSetColumnDataIndex(column: LineColumn, dataIndex: number): void { this.logicalLine()._cache1 = (dataIndex << 16) | (column & 0xFFFF); } public setStartFromCache(wrapRow: WrappedBufferLine): void { wrapRow.startIndex = this._cachedDataIndex(); @@ -720,7 +732,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { //public length: number; abstract logicalLine(): LogicalBufferLine; - abstract logicalStartColumn(): number; + abstract logicalStartColumn(): LineColumn; abstract data(): Uint32Array; abstract resizeData(size: number): void; abstract addEmptyDataElements(position: number, count: number): void; @@ -780,7 +792,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { _showData(start = 0, end = this.dataLength()) { let s = '['; let toffset = 0; - for (let i = 0; i < end; i++) { + for (let i = start; i < end; i++) { const word = this.data()[i]; const kind = BufferLine.wKind(word); let code: string | number = kind; @@ -914,19 +926,22 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { */ } - /** Move to column 'index'. - * If 'logical' true, 'index' is relative to logical line; - * otherwise relative to (and restricted to) this (screen) row. + /** Move to column 'index', which is a RowColumn. * Return encoded 'content'. */ - public moveToColumn(index: number, logical: boolean = false): number { - const startColumn = ! logical && this instanceof WrappedBufferLine ? this.startColumn : 0; - index += startColumn; + public moveToColumn(index: RowColumn): number { + return this.moveToLineColumn(index + this.logicalStartColumn(), this.dataRowEnd()); + } + + /** Move to column 'index', which is a LineColumn. + * Return encoded 'content'. + */ + public moveToLineColumn(index: LineColumn, end = this.dataLength()): number { let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better this._cacheReset(); - curColumn = startColumn; + curColumn = this._cachedColumn(); } let idata = this._cachedDataIndex(); let fg = this._cachedFg(); @@ -936,7 +951,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let word; let kind; let content = 0; - const end = logical ? this.dataLength() : this.dataRowEnd(); while (todo >= 0) { if (idata >= end) { word = NULL_DATA_WORD; @@ -1040,8 +1054,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.lineEndBg = fillCellData.bg; } - private preInsert(index: number, attrs: IAttributeData): boolean { - let content = this.moveToColumn(index); + private preInsert(index: LineColumn, attrs: IAttributeData): boolean { + let content = this.moveToLineColumn(index); let curColumn = this._cachedColumn(); const startColumn = curColumn; let idata = this._cachedDataIndex(); @@ -1114,15 +1128,21 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return add > 0; } - public insertText(index: number, data: Uint32Array, start: number, end: number, attrs: IAttributeData, inputHandler: IInputHandler, insertMode: boolean): number { - const add = this.preInsert(index, attrs); + /** 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, insertMode: boolean): RowColumn { + const lstart = this.logicalStartColumn(); + const lindex = index + lstart; + const add = this.preInsert(lindex, attrs); let curColumn = this._cachedColumn(); const lline = this.logicalLine(); - const startColumn = curColumn; + const startColumn: LineColumn = curColumn; let idata = this._cachedDataIndex(); let precedingJoinState = inputHandler.precedingJoinState; let inext; - if (add || idata === this.dataLength() || index === curColumn) + if (add || idata === this.dataLength() || lindex === curColumn) inext = idata; else { const kind = BufferLine.wKind(this.data()[idata]); @@ -1172,18 +1192,23 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (curColumn > lline.logicalWidth) lline.logicalWidth = curColumn; - return curColumn; + return curColumn - lstart; } - public eraseCells(start: number, end: number, attrs: IAttributeData): void { - const add = this.preInsert(start, attrs); + public eraseCells(start: RowColumn, end: RowColumn, attrs: IAttributeData): void { + const startColumn = this.logicalStartColumn(); + const count = end - start; + start += startColumn; + //const add = this.preInsert(start, attrs); + this.moveToLineColumn(start); + const add = 0 // FIXME + end += startColumn; let idata = this._cachedDataIndex(); let colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); - lline.deleteCellsOnly(idata, colOffset, end - start); + lline.deleteCellsOnly(idata, colOffset, count); idata = this._cachedDataIndex(); const data = this.data(); - const count = end - start; if (idata > 0 && BufferLine.wKind(data[idata-1]) === DataKind.SKIP_COLUMNS) { if (idata === this.dataLength()) { end = start; @@ -1202,7 +1227,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this._cacheSetColumnDataIndex(end, idata); } - public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { + public setCellFromCodepoint(index: RowColumn, codePoint: number, width: number, attrs: IAttributeData): void { if (codePoint === NULL_CELL_CODE) { if (width === 0) { // i.e. combining character @@ -1212,12 +1237,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } return; } - const add = this.preInsert(index, attrs); + const lindex = index + this.logicalStartColumn(); + const add = this.preInsert(lindex, attrs); // FIXME let curColumn = this._cachedColumn(); const startColumn = curColumn; let idata = this._cachedDataIndex(); let inext; - if (add || idata === this.dataLength() || index === curColumn) + if (add || idata === this.dataLength() || lindex === curColumn) inext = idata; else { const kind = BufferLine.wKind(this.data()[idata]); @@ -1514,6 +1540,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); if (src) { + //FIXME also copy _extendedAttrs this._data = src._data.slice(); this._dataLength = src._dataLength; } else { @@ -1524,11 +1551,11 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this._isWrapped = false; } logicalLine(): LogicalBufferLine { return this; } - logicalStartColumn(): number { return 0; } + logicalStartColumn(): LineColumn { return 0; } data(): Uint32Array { return this._data; } dataLength(): number { return this._dataLength; } - protected _cachedColumnInRow(): number { return (this.logicalLine()._cache1 & 0xFFFF); } + protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } // count can be negative addEmptyDataElements(position: number, count: number): void { @@ -1641,7 +1668,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } else { let delta = Math.min(todo, wlen - colOffset); this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, wlen - delta); - dskip_first = idata + 1; todo -= delta; } colOffset = 0; @@ -1652,13 +1678,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (colOffset === 0 && (1 << w) <= todo) { dskip_last = idata; todo -= 1 << w; - } else { - dskip_first = idata + 1; - /* - const delta = tend - tstart; - this._data[idata] = BufferLine.wSet1(kind, wlen - delta); - todo -= delta << w; - */ } break; case DataKind.CLUSTER_START_w1: @@ -1669,8 +1688,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { idata = clEnd; dskip_last = idata; todo -= (1 << w); - } else { - dskip_first = idata + 1; } colOffset = 0; break; @@ -1704,7 +1721,7 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { * (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: number = 0; + startColumn: LineColumn = 0; startFg: number = 0; startBg: number = 0; startStyle: number = 0; @@ -1719,13 +1736,13 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { } logicalLine(): LogicalBufferLine { return this._logicalLine; } - logicalStartColumn(): number { return this.startColumn; } + logicalStartColumn(): LineColumn { return this.startColumn; } data(): Uint32Array { return this._logicalLine.data(); } dataLength(): number { return this._logicalLine.dataLength(); } addEmptyDataElements(position: number, count: number): void { this._logicalLine.addEmptyDataElements(position, count); } - protected _cachedColumnInRow(): number { return (this.logicalLine()._cache1 & 0xFFFF) - this.startColumn; } + protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF) - this.startColumn; } protected _cacheReset(): void { this._cacheSetFgBg(this.startFg, this.startBg); this._cacheSetStyleFlagsIndex(this.startStyle); From cfdfb067c6f5096bb81e75a3c840ed308d75827c Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 6 Dec 2023 21:24:38 -0800 Subject: [PATCH 32/73] Make background color erase (BCE) work --- src/common/Types.d.ts | 1 - src/common/buffer/Buffer.ts | 7 +++- src/common/buffer/BufferLine.ts | 73 ++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 0733a752c0..1779658a0b 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -242,7 +242,6 @@ export interface IBufferLine { setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED insertCells(pos: number, n: number, ch: ICellData): void; - //eraseAll(bg: number): 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; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 655da23301..4b62bee405 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -373,8 +373,11 @@ export class Buffer implements IBuffer { for (;;) { const content = line.moveToLineColumn(startCol + newCols); let idata = line._cachedDataIndex(); - if (idata >= dataLength) + if (idata >= dataLength) { + curRow.nextRowSameLine = undefined; + curRow._isWrapped = false; break; + } startCol = line._cachedColumn(); let newRow1 = row < endRow && this.lines.get(row); let newRow = newRow1 instanceof WrappedBufferLine @@ -422,7 +425,7 @@ export class Buffer implements IBuffer { if (USE_NewBufferLine) { // FIXME do this lazily - this._reflowRegion(0, this.lines.length, newCols); + this._reflowRegion(0, this.lines.length, newCols); return; } // Iterate through rows, ignore the last one as it cannot be wrapped diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 3cf020f1fa..7d0c95d8b4 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -37,7 +37,6 @@ export abstract class AbstractBufferLine implements IBufferLine { length: number = 0; _isWrapped: boolean = false; get isWrapped(): boolean { return this._isWrapped; } - //abstract eraseAll(bg: number): void; abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED abstract resize(cols: number, fillCellData: ICellData): boolean; @@ -686,8 +685,6 @@ export class OldBufferLine extends BufferLine implements IBufferLine { // This class will be merged with its parent when OldBufferLine is removed, export abstract class NewBufferLine extends BufferLine implements IBufferLine { - /** Color for "rest of line" background, following _dataLength. */ - lineEndBg: number = 0; nextRowSameLine: WrappedBufferLine | undefined; @@ -1050,8 +1047,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let idata = this._cachedDataIndex(); let curColumn = this._cachedColumn(); this.logicalLine().deleteCellsOnly(idata, 0, n); - // FIXME - this.lineEndBg = fillCellData.bg; } private preInsert(index: LineColumn, attrs: IAttributeData): boolean { @@ -1083,46 +1078,64 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const newFg = attrs.getFg(); const newBg = attrs.getBg(); const newStyle = attrs.getStyleFlags(); - const oldFg = this._cachedFg(); - const oldBg = this._cachedBg(); + let oldFg = this._cachedFg(); + let oldBg = this._cachedBg(); let styleFlagsIndex = this._cachedStyleFlagsIndex(); - const oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; - const needFg = newFg !== oldFg; - const needBg = newBg !== oldBg + let oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; + let needFg = newFg !== oldFg; + let needBg = newBg !== oldBg // FIXME let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && cell.extended; //let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; - const needStyle = newStyle !== oldStyle // FIXME || oldExt !== newExt; + let needStyle = newStyle !== oldStyle // FIXME || oldExt !== newExt; const atEnd = idata === this.dataLength(); let add1 = atEnd ? 1 : 2; - let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); - + let add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); + let data = this.data(); if (add) { - this.addEmptyDataElements(idata, add); + const idata0 = idata; + let skipItem = true; + for (; idata > 0; idata--) { + const word = data[idata-1]; + switch (BufferLine.wKind(word)) { + case DataKind.BG: needBg = true; break; + case DataKind.FG: needFg = true; break; + case DataKind.STYLE_FLAGS: needStyle = true; + delete this._extendedAttrs[idata-1]; + break; + default: skipItem = false; + } + if (! skipItem) { + break; + } + } + add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); + this.addEmptyDataElements(idata, add - (idata0 - idata)); + data = this.data(); if (needFg) { - this.data()[idata++] = BufferLine.wSet1(DataKind.FG, newFg); + data[idata++] = BufferLine.wSet1(DataKind.FG, newFg); } if (needBg) { - this.data()[idata++] = BufferLine.wSet1(DataKind.BG, newBg); + data[idata++] = BufferLine.wSet1(DataKind.BG, newBg); } if (needStyle) { if (newStyle & StyleFlags.HAS_EXTENDED) this._extendedAttrs[idata] = attrs.extended; this._cacheSetStyleFlagsIndex(idata); - this.data()[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); + data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); } this._cacheSetColumnDataIndex(index, idata); let xdata = idata; // FIXME if (! atEnd) { - if (needFg) { - this.data()[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); + if (newFg !== oldFg) { + data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); } if (needStyle) { - this.data()[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); - } - if (needBg) { - this.data()[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); } } + if (needBg) { + data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + } this._cacheSetFgBg(newFg, newBg); } return add > 0; @@ -1199,6 +1212,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const startColumn = this.logicalStartColumn(); const count = end - start; start += startColumn; + if (end == this.length && ! this.nextRowSameLine) { + this.preInsert(start, attrs); + this.logicalLine()._dataLength = this._cachedDataIndex(); + return; + } //const add = this.preInsert(start, attrs); this.moveToLineColumn(start); const add = 0 // FIXME @@ -1589,15 +1607,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } - /* ??? - public eraseAll(bg: number): void { - // FIXME sometimes better to reuse old _data. - this._data = EMPTY_DATA; - this._dataLength = 0; - this.lineEndBg = bg; - } - */ - /** * Cleanup underlying array buffer. * A cleanup will be triggered if the array buffer exceeds the actual used From 795c38806202b5ba281c2224b547d323aad95bce Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 7 Dec 2023 12:04:08 -0800 Subject: [PATCH 33/73] Fix to scroll handling --- src/common/services/BufferService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 84420f2dc9..ba59a8bf3a 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -94,7 +94,8 @@ export class BufferService extends Disposable implements IBufferService { if (! willBufferBeTrimmed) { buffer.lines.push(newLine); } else if (USE_NewBufferLine) { - buffer.lines.set(bottomRow, newLine); // ??? + buffer.lines.recycle(); // ignore result + buffer.lines.set(bottomRow, newLine); } else { buffer.lines.recycle().copyFrom(newLine); } From 2baf3a898475a3b8b1c38170b523920333b917a5 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 7 Dec 2023 21:30:44 -0800 Subject: [PATCH 34/73] Implement insertCells. --- src/common/buffer/BufferLine.ts | 41 +++++++++++++-------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 7d0c95d8b4..fdefe14370 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -893,34 +893,25 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public insertCells(pos: number, n: number, fillCellData: ICellData): void { - alert("insertCells"); - /* - pos %= this.length; - - // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char - if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); + // FIXME handle if start or end in middle of wide character. + const width = this.length; + if (pos >= width) { + return; } - - if (n < this.length - pos) { - const cell = new CellData(); - for (let i = this.length - pos - n - 1; i >= 0; --i) { - this.setCell(pos + n + i, this.loadCell(pos + i, cell)); - } - for (let i = 0; i < n; ++i) { - this.setCell(pos + i, fillCellData); - } + if (pos + n < width) { + this.moveToColumn(pos + n); + let idata = this._cachedDataIndex(); + const colOffset = -1; // ??? + this.logicalLine().deleteCellsOnly(idata, colOffset, width - (pos + n)); } else { - for (let i = pos; i < this.length; ++i) { - this.setCell(i, fillCellData); - } + n = width - pos; } - - // handle fullwidth at line end: reset last cell if it is first cell of a wide char - if (this.getWidth(this.length - 1) === 2) { - this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData); - } - */ + this.preInsert(pos, fillCellData); + let idata = this._cachedDataIndex(); + this.addEmptyDataElements(idata, 1); + // Ideally should optize for adjacent SKIP_COLUMNS (as in eraseCells). + // However, typically is followed by replacing the new empty cells. + this.data()[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); } /** Move to column 'index', which is a RowColumn. From dbc108092f93a5fa9d5484980396d3941a1417ab Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 9 Dec 2023 02:45:14 -0800 Subject: [PATCH 35/73] Fix a bunch of lint issues --- .../xterm-addon-canvas/src/BaseRenderLayer.ts | 1 - src/browser/public/Terminal.ts | 9 +- .../renderer/dom/DomRendererRowFactory.ts | 8 +- src/common/InputHandler.ts | 19 +- src/common/buffer/Buffer.ts | 24 +- src/common/buffer/BufferLine.test.ts | 13 +- src/common/buffer/BufferLine.ts | 352 +++++++++--------- src/common/buffer/CellData.ts | 2 +- 8 files changed, 207 insertions(+), 221 deletions(-) diff --git a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 49b494b69d..a0a5e4bb5a 100644 --- a/addons/xterm-addon-canvas/src/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -16,7 +16,6 @@ import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { isSafari } from 'common/Platform'; import { ICellData } from 'common/Types'; -import { CellData } from 'common/buffer/CellData'; import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; import { Terminal } from 'xterm'; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 7e712e2caf..bcb895c89e 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -219,10 +219,11 @@ export class Terminal extends Disposable implements ITerminalApi { public write(data: string | Uint8Array, callback?: () => void): void { if (this.logOutput && data instanceof Uint8Array) { const thisAny = this as any; - if (thisAny._decoder == null) - thisAny._decoder = new TextDecoder(); //label = "utf-8"); - const str = thisAny._decoder.decode(data, {stream:true}); - console.log("write: "+JSON.stringify(str)); + if (thisAny._decoder === null) { + 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); } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 5c3e7e315d..9e20894d71 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -70,7 +70,7 @@ export class DomRendererRowFactory { linkStart: number, linkEnd: number ): HTMLSpanElement[] { - let cell = this._workCell; + const cell = this._workCell; const elements: HTMLSpanElement[] = []; const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); @@ -97,7 +97,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,8 +105,8 @@ 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 diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b92631d320..9dc4cc694b 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -178,7 +178,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _optionsService: IOptionsService, private readonly _oscLinkService: IOscLinkService, private readonly _coreMouseService: ICoreMouseService, - readonly unicodeService: IUnicodeService, + public readonly unicodeService: IUnicodeService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() ) { super(); @@ -508,15 +508,15 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { const curAttr = this._curAttrData; - let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + const bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; if (bufferRow instanceof NewBufferLine) { - this.printNew(data, start, end, bufferRow, curAttr); + this._printNew(data, start, end, bufferRow, curAttr); } else { - this.printOld(data, start, end, bufferRow, curAttr); + this._printOld(data, start, end, bufferRow, curAttr); } } - private printNew(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { + 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); @@ -528,8 +528,7 @@ export class InputHandler extends Disposable implements IInputHandler { // automatically wraps to the beginning of the next line if (wraparoundMode) { const oldRow = bufferRow as NewBufferLine; - let oldCol = this._activeBuffer.x; - //this._activeBuffer.x = oldWidth; + // this._activeBuffer.x = oldWidth; this._activeBuffer.y++; if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { this._activeBuffer.y--; @@ -542,7 +541,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.splitLine(this._activeBuffer.y, col); bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; // usually same as cols, but may be less in case of wide characters. - const prevCols = (bufferRow as NewBufferLine).logicalStartColumn() - oldRow.logicalStartColumn() + const prevCols = (bufferRow as NewBufferLine).logicalStartColumn() - oldRow.logicalStartColumn(); col = col - prevCols; // row changed, get it again /* @@ -566,7 +565,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.x = col; } - private printOld(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { + private _printOld(data: Uint32Array, start: number, end: number, bufferRow: IBufferLine, curAttr: IAttributeData): void { let code: number; let chWidth: number; const charset = this._charsetService.charset; @@ -1260,7 +1259,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean { this._restrictCursor(this._bufferService.cols); - let j, x; + let j; let x; switch (params.params[0]) { case 0: j = this._activeBuffer.y; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 4b62bee405..c9b3931e6a 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -133,11 +133,11 @@ export class Buffer implements IBuffer { public setWrapped(absrow: number, value: boolean): void { const line = this.lines.get(absrow); if (! line || line.isWrapped === value) - return; + {return;} if (! USE_NewBufferLine) { line!._isWrapped = value; } else if (value) { - alert("setWrapped true"); // only used in test cases? + alert('setWrapped true'); // only used in test cases? } else { // clear wrapped FIXME const prevRow = this.lines.get(absrow - 1) as NewBufferLine; @@ -145,12 +145,11 @@ export class Buffer implements IBuffer { prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); const newRow = new LogicalBufferLine(line.length, undefined, oldLine); - const oldLength = oldLine._dataLength; const oldStart = curRow.startIndex; newRow.addEmptyDataElements(0, - oldStart); oldLine._dataLength = curRow.startIndex; this.lines.set(absrow, newRow); - } + } } /** @@ -356,13 +355,12 @@ export class Buffer implements IBuffer { // FIXME don't need to allocate newRows if no lines require more rows // than before. So better to allocate newRows lazily. const newRows: IBufferLine[] = []; - let inew = 0; const yAbs = this.ybase + this.y; let deltaSoFar = 0; for (let row = startRow; row < endRow;) { const line = this.lines.get(row)!; newRows.push(line); - row++ + row++; if (line instanceof LogicalBufferLine && line.reflowNeeded) { const oldWrapStart = row; const newWrapStart = newRows.length; @@ -371,18 +369,18 @@ export class Buffer implements IBuffer { let curRow: NewBufferLine = line; const dataLength = line.dataLength(); for (;;) { - const content = line.moveToLineColumn(startCol + newCols); - let idata = line._cachedDataIndex(); + line.moveToLineColumn(startCol + newCols); + const idata = line._cachedDataIndex(); if (idata >= dataLength) { curRow.nextRowSameLine = undefined; curRow._isWrapped = false; break; } startCol = line._cachedColumn(); - let newRow1 = row < endRow && this.lines.get(row); - let newRow = newRow1 instanceof WrappedBufferLine - ? (row++, newRow1) - : new WrappedBufferLine(curRow); + 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; @@ -411,7 +409,7 @@ export class Buffer implements IBuffer { } if (deltaSoFar !== 0) { if (yAbs >= endRow) - this.y += deltaSoFar; + {this.y += deltaSoFar;} } // FIXME. This calls onDeleteEmitter and onInsertEmitter events, // which we want handled at finer granularity. diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 07baf3ba2f..595ab0d95d 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -10,14 +10,11 @@ import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; -class TestBufferLine extends LogicalBufferLine/*FIXME*/ { - //public get combined(): {[index: number]: string} { - // return this._combined; - //} +class TestBufferLine extends LogicalBufferLine /* FIXME */ { + constructor(cols: number, fillCellData?: IAttributeData, isWrapped?: boolean) { - constructor(cols: number, fillCellData?: IAttributeData, isWrapped?: boolean) { super(cols, fillCellData); - if (isWrapped) alert("TestBufferLine with isWrapped set not supported"); + if (isWrapped) alert('TestBufferLine with isWrapped set not supported'); } public toArray(): CharData[] { @@ -294,12 +291,12 @@ describe('BufferLine', function(): void { 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 { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index fdefe14370..104a785341 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -24,8 +24,6 @@ type RowColumn = number; */ type LineColumn = number; -const EMPTY_DATA = new Uint32Array(0); - // Work variables to avoid garbage collection let $startIndex = 0; @@ -34,21 +32,21 @@ const CLEANUP_THRESHOLD = 2; export abstract class AbstractBufferLine implements IBufferLine { /** Number of logical columns */ - length: number = 0; + public length: number = 0; _isWrapped: boolean = false; get isWrapped(): boolean { return this._isWrapped; } abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED abstract resize(cols: number, fillCellData: ICellData): boolean; abstract fill(fillCellData: ICellData, respectProtect: boolean): void; - abstract copyFrom(line: BufferLine): void; - abstract clone(): IBufferLine; - abstract translateToString(trimRight: boolean, startCol: number, endCol: number): string; - abstract getTrimmedLength(): number; - abstract getNoBgTrimmedLength(): number; - abstract cleanupMemory(): number; + public abstract copyFrom(line: BufferLine): void; + public abstract clone(): IBufferLine; + public abstract translateToString(trimRight: boolean, startCol: number, endCol: number): string; + public abstract getTrimmedLength(): number; + public abstract getNoBgTrimmedLength(): number; + public abstract cleanupMemory(): number; - abstract loadCell(index: number, cell: ICellData): ICellData; + 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 @@ -82,22 +80,22 @@ export abstract class AbstractBufferLine implements IBufferLine { } } - /** + /** * Get cell data CharData. * @deprecated */ get(index: number): CharData { - let cell = new CellData(); + 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)); + this.setCell(index, CellData.fromCharData(value)); } /** @@ -177,11 +175,11 @@ const enum DataKind { // 4 bits 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 + 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_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 } @@ -194,15 +192,15 @@ export abstract class BufferLine extends AbstractBufferLine implements IBufferLi if (USE_NewBufferLine) { // if (isWrapped) new WrappedBufferLine(...); return new LogicalBufferLine(cols, fillCellData); - } else { - return new OldBufferLine(cols, fillCellData, isWrapped); } + return new OldBufferLine(cols, fillCellData, isWrapped); + } public abstract copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void; public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, skipReplace: string = WHITESPACE_CELL_CHAR): string { - if (trimRight) { + if (trimRight) { endCol = Math.min(endCol, this.getTrimmedLength()); } let result = ''; @@ -219,13 +217,13 @@ export abstract class BufferLine extends AbstractBufferLine implements IBufferLi // FOLLOWING ONLY USED BY NewBufferLine /** From a Uint23 in _data, extract the DataKind bits. */ - protected static wKind(word: number): DataKind { return word >>> 28; } - protected static wKindIsText(kind: DataKind): boolean { return kind >= DataKind.CHAR_w1 && kind <= DataKind.CLUSTER_CONTINUED; } - protected static wKindIsTextOrSkip(kind: DataKind): boolean { return kind >= DataKind.SKIP_COLUMNS && kind <= DataKind.CLUSTER_CONTINUED; } + 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. */ - protected static wSkipCount(word: number): number { return word & 0xfffff; } - protected static wSet1(kind: DataKind, value: number): number { + * 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); } } @@ -696,9 +694,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** 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 - protected _cachedBg(): number { return this.logicalLine()._cache2; } - protected _cachedFg(): number { return this.logicalLine()._cache3; } + // private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED + abstract _cachedBg(): number; + abstract _cachedFg(): number; // One more than index (in data()) of STYLE_FLAGS; 0 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 = 0; } @@ -714,23 +712,20 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { wrapRow.startStyle = this._cachedStyleFlagsIndex(); } - constructor() { - super(); - } // Length of data() array. - protected abstract dataLength(): number; + abstract dataLength(): number; // End of current row in data() array. protected dataRowEnd(): number { return this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); } // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. protected _extendedAttrs: IExtendedAttrs[] = []; - //protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; - //public length: number; + // protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; + // public length: number; - abstract logicalLine(): LogicalBufferLine; - abstract logicalStartColumn(): LineColumn; - abstract data(): Uint32Array; + public abstract logicalLine(): LogicalBufferLine; + public abstract logicalStartColumn(): LineColumn; + protected abstract data(): Uint32Array; abstract resizeData(size: number): void; abstract addEmptyDataElements(position: number, count: number): void; @@ -782,13 +777,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return this.translateToString(true, 0, this.length, skipReplace); } - _showRowData(): string { - return this._showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); + public showRowData(): string { + return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); } /* Human-readable display of data() array, for debugging */ - _showData(start = 0, end = this.dataLength()) { + public showData(start = 0, end = this.dataLength()) { let s = '['; - let toffset = 0; for (let i = start; i < end; i++) { const word = this.data()[i]; const kind = BufferLine.wKind(word); @@ -799,33 +793,33 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.BG: code = 'BG'; break; case DataKind.STYLE_FLAGS: code = 'STYLE'; break; case DataKind.SKIP_COLUMNS: code = 'SKIP'; break; - case DataKind.CLUSTER_START_w1: code = 'CL1'; break; - case DataKind.CLUSTER_START_w2: code = 'CL2'; break; + case DataKind.CLUSTER_START_W1: code = 'CL1'; break; + case DataKind.CLUSTER_START_W2: code = 'CL2'; break; case DataKind.CLUSTER_CONTINUED: code = 'CL_CONT'; break; - case DataKind.CHAR_w1: code = 'C1'; break; - case DataKind.CHAR_w2: code = 'C2'; break; + case DataKind.CHAR_W1: code = 'C1'; break; + case DataKind.CHAR_W2: code = 'C2'; break; } if (i >= start) { if (i !== start) { s += ', '; } let value; - if (kind === DataKind.CHAR_w1 || kind === DataKind.CHAR_w2) { - let count = 1; - while (i + count < end && 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); - } else if (kind === DataKind.CLUSTER_START_w1 - || kind === DataKind.CLUSTER_START_w2) { + if (kind === DataKind.CHAR_W1 || kind === DataKind.CHAR_W2) { + let count = 1; + while (i + count < end && 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); + } else if (kind === DataKind.CLUSTER_START_W1 + || kind === DataKind.CLUSTER_START_W2) { // FIXME extract cluster as string value = '*' + (word & 0x1fffff); } else if (kind === DataKind.CLUSTER_CONTINUED) { @@ -847,15 +841,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Check invariants. Useful for debugging. */ _check(): void { - function error(str: string) { - console.log("ERROR: "+str); + function error(str: string): void { + console.log('ERROR: '+str); } - let itext = 0; - let icol = 0; + const icol = 0; if (this.dataLength() < 0 || this.dataLength() > this.data().length) - error("bad _dataLength"); - const incrementText = (wlen: number) => { - }; + {error('bad _dataLength');} for (let idata = 0; idata < this.dataLength(); idata++) { const word = this.data()[idata]; const kind = BufferLine.wKind(word); @@ -867,14 +858,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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.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"); + error('invalid _dataKind'); } } } @@ -900,14 +891,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (pos + n < width) { this.moveToColumn(pos + n); - let idata = this._cachedDataIndex(); + const idata = this._cachedDataIndex(); const colOffset = -1; // ??? this.logicalLine().deleteCellsOnly(idata, colOffset, width - (pos + n)); } else { n = width - pos; } this.preInsert(pos, fillCellData); - let idata = this._cachedDataIndex(); + const idata = this._cachedDataIndex(); this.addEmptyDataElements(idata, 1); // Ideally should optize for adjacent SKIP_COLUMNS (as in eraseCells). // However, typically is followed by replacing the new empty cells. @@ -921,9 +912,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return this.moveToLineColumn(index + this.logicalStartColumn(), this.dataRowEnd()); } - /** Move to column 'index', which is a LineColumn. - * Return encoded 'content'. - */ + /** Move to column 'index', which is a LineColumn. + * Return encoded 'content'. + */ public moveToLineColumn(index: LineColumn, end = this.dataLength()): number { let curColumn = this._cachedColumn(); if (index < curColumn) { @@ -963,7 +954,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { styleFlagsIndex = idata; break; case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wSkipCount(word); + const wlen = BufferLine.wSkipCount(word); if (todo >= wlen) { todo -= wlen; idata++; @@ -973,9 +964,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { todo = -1; } break; - case DataKind.CLUSTER_START_w1: - case DataKind.CLUSTER_START_w2: - w = kind + 1 - DataKind.CLUSTER_START_w1; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + w = kind + 1 - DataKind.CLUSTER_START_W1; if (todo >= w) { const clEnd = this.clusterEnd(idata); todo -= w; @@ -987,9 +978,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { todo = -1; } break; - case DataKind.CHAR_w1: - case DataKind.CHAR_w2: - w = kind + 1 - DataKind.CHAR_w1; // 1, or 2 if wide characters + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: + w = kind + 1 - DataKind.CHAR_W1; // 1, or 2 if wide characters if (todo >= w) { todo -= w; idata++; @@ -1018,7 +1009,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { cursor.content = content; cursor.setFg(this._cachedFg()); cursor.setBg(this._cachedBg()); - let styleFlagsIndex = this._cachedStyleFlagsIndex(); + const styleFlagsIndex = this._cachedStyleFlagsIndex(); const word = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; cursor.setStyleFlags(word); if (word & StyleFlags.HAS_EXTENDED) { @@ -1026,7 +1017,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (content & Content.IS_COMBINED_MASK) { // FIXME do this lazily, in CellData.getChars - let idata = this._cachedDataIndex(); + const idata = this._cachedDataIndex(); const str = utf32ToString(this.data(), idata, this.clusterEnd(idata)); cursor.combinedData = str; } @@ -1035,14 +1026,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public deleteCells(pos: number, n: number, fillCellData: ICellData): void { const content = this.moveToColumn(pos); - let idata = this._cachedDataIndex(); - let curColumn = this._cachedColumn(); - this.logicalLine().deleteCellsOnly(idata, 0, n); + const idata = this._cachedDataIndex(); + const curColumn = this._cachedColumn(); + this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); } private preInsert(index: LineColumn, attrs: IAttributeData): boolean { - let content = this.moveToLineColumn(index); - let curColumn = this._cachedColumn(); + const content = this.moveToLineColumn(index); + const curColumn = this._cachedColumn(); const startColumn = curColumn; let idata = this._cachedDataIndex(); @@ -1069,17 +1060,17 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const newFg = attrs.getFg(); const newBg = attrs.getBg(); const newStyle = attrs.getStyleFlags(); - let oldFg = this._cachedFg(); - let oldBg = this._cachedBg(); - let styleFlagsIndex = this._cachedStyleFlagsIndex(); - let oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; + const oldFg = this._cachedFg(); + const oldBg = this._cachedBg(); + const styleFlagsIndex = this._cachedStyleFlagsIndex(); + const oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; let needFg = newFg !== oldFg; - let needBg = newBg !== oldBg + let needBg = newBg !== oldBg; // FIXME let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && cell.extended; - //let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; - let needStyle = newStyle !== oldStyle // FIXME || oldExt !== newExt; + // let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; + let needStyle = newStyle !== oldStyle; // FIXME || oldExt !== newExt; const atEnd = idata === this.dataLength(); - let add1 = atEnd ? 1 : 2; + const add1 = atEnd ? 1 : 2; let add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); let data = this.data(); if (add) { @@ -1110,7 +1101,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (needStyle) { if (newStyle & StyleFlags.HAS_EXTENDED) - this._extendedAttrs[idata] = attrs.extended; + {this._extendedAttrs[idata] = attrs.extended;} this._cacheSetStyleFlagsIndex(idata); data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); } @@ -1147,13 +1138,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let precedingJoinState = inputHandler.precedingJoinState; let inext; if (add || idata === this.dataLength() || lindex === curColumn) - inext = idata; + {inext = idata;} else { - const kind = BufferLine.wKind(this.data()[idata]); - if (BufferLine.wKindIsText(kind)) - inext = this.clusterEnd(idata); - else - inext = idata; + 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); @@ -1173,7 +1164,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { precedingJoinState = currentInfo; let kind; if (shouldJoin) { - kind = chWidth === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; + 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; @@ -1182,7 +1173,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { kind = DataKind.CLUSTER_CONTINUED; curColumn += chWidth - oldWidth; } else { - kind = chWidth === 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; + kind = chWidth === 2 ? DataKind.CHAR_W2 : DataKind.CHAR_W1; idata = inext; cellColumn = curColumn; curColumn += chWidth; @@ -1195,7 +1186,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); } if (curColumn > lline.logicalWidth) - lline.logicalWidth = curColumn; + {lline.logicalWidth = curColumn;} return curColumn - lstart; } @@ -1203,17 +1194,17 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const startColumn = this.logicalStartColumn(); const count = end - start; start += startColumn; - if (end == this.length && ! this.nextRowSameLine) { + if (end === this.length && ! this.nextRowSameLine) { this.preInsert(start, attrs); this.logicalLine()._dataLength = this._cachedDataIndex(); return; } - //const add = this.preInsert(start, attrs); + // const add = this.preInsert(start, attrs); this.moveToLineColumn(start); - const add = 0 // FIXME + const add = 0; // FIXME end += startColumn; let idata = this._cachedDataIndex(); - let colOffset = start - this._cachedColumn(); + const colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); lline.deleteCellsOnly(idata, colOffset, count); idata = this._cachedDataIndex(); @@ -1253,16 +1244,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let idata = this._cachedDataIndex(); let inext; if (add || idata === this.dataLength() || lindex === curColumn) - inext = idata; + {inext = idata;} else { const kind = BufferLine.wKind(this.data()[idata]); if (BufferLine.wKindIsText(kind)) - inext = this.clusterEnd(idata); + {inext = this.clusterEnd(idata);} else - inext = idata; + {inext = idata;} } let cellColumn = curColumn; - let kind = width === 2 ? DataKind.CHAR_w2 : DataKind.CHAR_w1; + const kind = width === 2 ? DataKind.CHAR_W2 : DataKind.CHAR_W1; idata = inext; cellColumn = curColumn; curColumn += width; @@ -1290,9 +1281,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.addEmptyDataElements(clEnd, 1); const nContinued = clEnd - idata; const startChar = this.data()[idata] & 0x1FFFFF; - const kind = width === 2 ? DataKind.CLUSTER_START_w2 : DataKind.CLUSTER_START_w1; + const kind = width === 2 ? DataKind.CLUSTER_START_W2 : DataKind.CLUSTER_START_W1; this.data()[idata] = BufferLine.wSet1(kind, - startChar + (nContinued << 21)); + startChar + (nContinued << 21)); this.data()[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); } @@ -1304,7 +1295,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * excess memory (true after shrinking > CLEANUP_THRESHOLD). */ public resize(cols: number, fillCellData: ICellData): boolean { - console.log("BufferLineNew.resize "+this.length+"->"+cols); + console.log('BufferLineNew.resize '+this.length+'->'+cols); /* if (cols === this.length) { return this.data().length * 4 * CLEANUP_THRESHOLD < this.data().buffer.byteLength; @@ -1357,7 +1348,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** alter to a full copy of line */ public copyFrom(xline: BufferLine): void { - alert("copyFrom"); + alert('copyFrom'); /* const line = xline as LogicalBufferLine; // FIXME if (this.length !== line.length) { @@ -1378,7 +1369,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** create a new clone */ public clone(): IBufferLine { - alert("NewBufferLine.clone"); + alert('NewBufferLine.clone'); const newLine = new LogicalBufferLine(0); return newLine; } @@ -1393,7 +1384,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; + const w = kind === DataKind.CHAR_W2 || kind === DataKind.CLUSTER_START_W2 ? 2 : 1; let wcols = 0; switch (kind) { case DataKind.BG: @@ -1405,14 +1396,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.SKIP_COLUMNS: skipped += BufferLine.wSkipCount(word); break; - case DataKind.CLUSTER_START_w1: - case DataKind.CLUSTER_START_w2: + 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: + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: wcols = w; break; case DataKind.CLUSTER_CONTINUED: @@ -1435,8 +1426,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const cell = new CellData(); if (applyInReverse) { for (let i = length - 1; i >= 0; i--) { - src.loadCell(srcCol + i, cell); - this.setCell(destCol + i, cell); + src.loadCell(srcCol + i, cell); + this.setCell(destCol + i, cell); } } else { for (let i = 0; i < length; i++) { @@ -1451,8 +1442,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let col = 0; let pendingStart = -1; let pendingLength = 0; - let pendingSkip = 0; - //const text = this._text; const data = this.data(); function pendingForce(handleSkip = ! trimRight): void { if (pendingStart >= 0 && pendingLength > 0) { @@ -1482,7 +1471,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; + const wide = kind === DataKind.CHAR_W2 || kind === DataKind.CLUSTER_START_W2 ? 1 : 0; let wcols; switch (kind) { case DataKind.FG: @@ -1503,9 +1492,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } col += wlen; break; - case DataKind.CLUSTER_START_w1: - case DataKind.CLUSTER_START_w2: - let clEnd = this.clusterEnd(idata); + 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); @@ -1513,8 +1502,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { idata = clEnd - 1; col += wcols; break; - case DataKind.CHAR_w1: - case DataKind.CHAR_w2: + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: wcols = 1 << wide; if (col >= startCol && col + wcols <= endCol) { addPendingString(idata, 1); @@ -1547,9 +1536,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { constructor(cols: number, fillCellData?: IAttributeData, src?: LogicalBufferLine) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); - //const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); + // const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); if (src) { - //FIXME also copy _extendedAttrs + // FIXME also copy _extendedAttrs this._data = src._data.slice(); this._dataLength = src._dataLength; } else { @@ -1559,10 +1548,12 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.length = cols; this._isWrapped = false; } - logicalLine(): LogicalBufferLine { return this; } - logicalStartColumn(): LineColumn { return 0; } - data(): Uint32Array { return this._data; } - dataLength(): number { return this._dataLength; } + 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); } @@ -1577,7 +1568,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { if (next.startIndex > position) - next.startIndex += count; + {next.startIndex += count;} } this._extendedAttrs.copyWithin(position + count, position, this._dataLength); this._dataLength += count; @@ -1590,12 +1581,12 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } resizeData(size: number): void { - if (size > this.data().length) { - //buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); - const new_data = new Uint32Array((3 * size) >> 1); - new_data.set(this._data); - this.logicalLine()._data = new_data; - } + 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; + } } /** @@ -1624,11 +1615,10 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { const data = this.data(); let idata = idata0; let colOffset = colOffset0; - let word0 = this.data()[idata]; - let dskip_first = idata, dskip_last = -1, tskip_first = -1, tskip_last = -1, w; - let fgValue = -1; //cursor.getFg(); - let bgValue = -1; //cursor.getBg(); - let styleValue = -1; //cursor.getStyleFlags(); // FIXME handle extendedattrs + let dskipFirst = idata; let dskipLast = -1; let w; + let fgValue = -1; // cursor.getFg(); + let bgValue = -1; // cursor.getBg(); + let styleValue = -1; // cursor.getStyleFlags(); // FIXME handle extendedattrs if (colOffset === 0) { while (idata > 0) { @@ -1642,8 +1632,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } if (skipItem) { idata--; - dskip_first = idata; - dskip_last = idata0-1; + dskipFirst = idata; + dskipLast = idata0-1; } else { break; } @@ -1651,7 +1641,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } for (; todo > 0 && idata < this.dataLength(); idata++) { - let word = data[idata]; + const word = data[idata]; const kind = BufferLine.wKind(word); switch (kind) { case DataKind.FG: fgValue = word; break; @@ -1661,39 +1651,39 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { // handle ExtendedAttrs FIXME break; case DataKind.SKIP_COLUMNS: - let wlen = BufferLine.wSkipCount(word); + const wlen = BufferLine.wSkipCount(word); if (colOffset === 0 && wlen <= todo) { - dskip_last = idata; + dskipLast = idata; todo -= wlen; } else { - let delta = Math.min(todo, wlen - colOffset); + 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 + 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) { - dskip_last = idata; + 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 + 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; - dskip_last = idata; + dskipLast = idata; todo -= (1 << w); } colOffset = 0; break; } } - idata0 = dskip_first; + idata0 = dskipFirst; if (bgValue >= 0) { this.data()[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); } @@ -1703,8 +1693,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (styleValue >= 0) { this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); } - if (dskip_last >= 0) { - const dcount = dskip_last + 1 - idata0; + if (dskipLast >= 0) { + const dcount = dskipLast + 1 - idata0; this.addEmptyDataElements(idata0, - dcount); } } @@ -1735,10 +1725,12 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this.length = logicalLine.length; } - logicalLine(): LogicalBufferLine { return this._logicalLine; } - logicalStartColumn(): LineColumn { return this.startColumn; } - data(): Uint32Array { return this._logicalLine.data(); } - dataLength(): number { return this._logicalLine.dataLength(); } + 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); } @@ -1748,6 +1740,6 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this._cacheSetStyleFlagsIndex(this.startStyle); this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); } - resizeData(size: number): void { this._logicalLine.resizeData(size); } + public resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} } diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 8a0ff9151d..ca84eb3179 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -25,7 +25,7 @@ export class CellData extends AttributeData implements ICellData { public extended: IExtendedAttrs = new ExtendedAttrs(); public combinedData = ''; - public copyFrom(src: CellData) { + public copyFrom(src: CellData): void { this.content = src.content; this.fg = src.fg; this.bg = src.bg; From 63d36d5c30725de9ca29d9021aafb953ffd73de6 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 9 Dec 2023 20:32:08 -0800 Subject: [PATCH 36/73] Add "newBufferLine" option. This constrols USE_NewBufferLine. --- src/browser/public/Terminal.ts | 2 +- src/common/InputHandler.ts | 9 +++++++-- src/common/buffer/Buffer.ts | 8 ++++---- src/common/buffer/BufferLine.ts | 6 ++++-- src/common/services/BufferService.ts | 8 ++++---- src/common/services/OptionsService.ts | 6 ++++++ 6 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index bcb895c89e..eacd5df0e9 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -219,7 +219,7 @@ export class Terminal extends Disposable implements ITerminalApi { public write(data: string | Uint8Array, callback?: () => void): void { if (this.logOutput && data instanceof Uint8Array) { const thisAny = this as any; - if (thisAny._decoder === null) { + if (! thisAny._decoder) { thisAny._decoder = new TextDecoder(); // label = "utf-8"); } const str = thisAny._decoder.decode(data, { stream:true }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 9dc4cc694b..455731f451 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -10,7 +10,7 @@ import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; import { Disposable } from 'common/Lifecycle'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder'; -import { BufferLine, OldBufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { usingNewBufferLine, BufferLine, OldBufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { EventEmitter } from 'common/EventEmitter'; 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'; @@ -1223,13 +1223,18 @@ export class InputHandler extends Disposable implements IInputHandler { const line = buffer.lines.get(row); if (line) { const eraseAttrs = this._eraseAttrData(); - if (line instanceof NewBufferLine && ! respectProtect) { + 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(this._activeBuffer.ybase + y); buffer.setWrapped(row, false); + if (wasNewBufferLine !== usingNewBufferLine()) { + const fill = this._activeBuffer.getNullCell(eraseAttrs); + buffer.lines.set(row, BufferLine.make(line.length, fill)); + } } } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index c9b3931e6a..2676027cde 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, USE_NewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine, 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'; @@ -134,7 +134,7 @@ export class Buffer implements IBuffer { const line = this.lines.get(absrow); if (! line || line.isWrapped === value) {return;} - if (! USE_NewBufferLine) { + if (! usingNewBufferLine()) { line!._isWrapped = value; } else if (value) { alert('setWrapped true'); // only used in test cases? @@ -200,7 +200,7 @@ export class Buffer implements IBuffer { this.lines.maxLength = newMaxLength; } - if (this._cols !== newCols && USE_NewBufferLine) { + if (this._cols !== newCols && usingNewBufferLine()) { const nlines = this.lines.length; for (let i = 0; i < nlines; i++) { const line = this.lines.get(i); @@ -421,7 +421,7 @@ export class Buffer implements IBuffer { return; } - if (USE_NewBufferLine) { + if (usingNewBufferLine()) { // FIXME do this lazily this._reflowRegion(0, this.lines.length, newCols); return; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 104a785341..7d63af6e7a 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -185,9 +185,11 @@ const enum DataKind { // 4 bits const NULL_DATA_WORD = DataKind.SKIP_COLUMNS << 28; -export var USE_NewBufferLine = true; - +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(...); diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index ba59a8bf3a..6a38a4be91 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -7,7 +7,7 @@ import { EventEmitter } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { USE_NewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; +import { usingNewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; @@ -65,7 +65,7 @@ export class BufferService extends Disposable implements IBufferService { const bottomRow = buffer.ybase + buffer.scrollBottom; let newLine: IBufferLine | undefined; - if (USE_NewBufferLine) { + if (usingNewBufferLine()) { if (isWrapped) { const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as NewBufferLine; newLine = new WrappedBufferLine(oldLine); @@ -74,7 +74,7 @@ export class BufferService extends Disposable implements IBufferService { } } else { newLine = this._cachedBlankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg || newLine instanceof NewBufferLine) { newLine = buffer.getBlankLine(eraseAttr, isWrapped); this._cachedBlankLine = newLine; } @@ -93,7 +93,7 @@ export class BufferService extends Disposable implements IBufferService { if (bottomRow === buffer.lines.length - 1) { if (! willBufferBeTrimmed) { buffer.lines.push(newLine); - } else if (USE_NewBufferLine) { + } else if (usingNewBufferLine()) { buffer.lines.recycle(); // ignore result buffer.lines.set(bottomRow, newLine); } else { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 3c572445ed..15007f74c3 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -8,6 +8,7 @@ import { Disposable } from 'common/Lifecycle'; import { isMac } from 'common/Platform'; import { CursorStyle, IDisposable } from 'common/Types'; import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services'; +import { selectNewBufferLine } from 'common/buffer/BufferLine'; export const DEFAULT_OPTIONS: Readonly> = { cols: 80, @@ -30,6 +31,7 @@ export const DEFAULT_OPTIONS: Readonly> = { linkHandler: null, logLevel: 'info', logger: null, + newBufferLine: true, scrollback: 1000, scrollOnUserInput: true, scrollSensitivity: 1, @@ -84,6 +86,7 @@ export class OptionsService extends Disposable implements IOptionsService { // set up getters and setters for each option this.rawOptions = defaultOptions; this.options = { ... defaultOptions }; + selectNewBufferLine(options['newBufferLine']); this._setupOptions(); } @@ -170,6 +173,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) { From 040666b26ead5d12a1c40838be109470bec81e82 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 11 Dec 2023 14:43:11 -0800 Subject: [PATCH 37/73] Fix to setting of newBufferLine option --- src/common/services/OptionsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 15007f74c3..b0daf53869 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -86,7 +86,7 @@ export class OptionsService extends Disposable implements IOptionsService { // set up getters and setters for each option this.rawOptions = defaultOptions; this.options = { ... defaultOptions }; - selectNewBufferLine(options['newBufferLine']); + selectNewBufferLine(this.options['newBufferLine']); this._setupOptions(); } From 97910e305dcae60a27cfccb230f3bbe202b19853 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 16 Dec 2023 18:16:36 -0800 Subject: [PATCH 38/73] Improved logic to update cursor position on resize --- src/common/InputHandler.ts | 5 +- src/common/buffer/Buffer.ts | 88 +++++++++++++++++++++++++-------- src/common/buffer/BufferLine.ts | 19 ++++--- src/common/buffer/Types.d.ts | 14 +++++- 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 455731f451..cfbbd39d09 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2709,8 +2709,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; } diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 2676027cde..c79c3d7db1 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -139,14 +139,19 @@ export class Buffer implements IBuffer { } else if (value) { alert('setWrapped true'); // only used in test cases? } else { - // clear wrapped FIXME 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 newRow = new LogicalBufferLine(line.length, undefined, oldLine); + newRow.nextRowSameLine = curRow.nextRowSameLine; const oldStart = curRow.startIndex; newRow.addEmptyDataElements(0, - oldStart); + for (let nextRow = newRow.nextRowSameLine; nextRow; nextRow = nextRow.nextRowSameLine) { + nextRow.startColumn -= oldStartColumn; + nextRow.startIndex -= oldStart; + } oldLine._dataLength = curRow.startIndex; this.lines.set(absrow, newRow); } @@ -279,7 +284,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; @@ -354,26 +359,40 @@ export class Buffer implements IBuffer { } // FIXME don't need to allocate newRows if no lines require more rows // than before. So better to allocate newRows lazily. - const newRows: IBufferLine[] = []; + const newRows: NewBufferLine[] = []; const yAbs = this.ybase + this.y; let deltaSoFar = 0; for (let row = startRow; row < endRow;) { - const line = this.lines.get(row)!; + const line = this.lines.get(row) as NewBufferLine; newRows.push(line); - row++; if (line instanceof LogicalBufferLine && line.reflowNeeded) { - const oldWrapStart = row; + let curRow: NewBufferLine = line; + + let logicalX; + let oldWrapCount = 0; // number of following wrapped lines + let nextRow = curRow; + for (; ; oldWrapCount++) { + if (yAbs === row + oldWrapCount) { + logicalX = nextRow.logicalStartColumn() + this.x; + } + if (! nextRow.nextRowSameLine || row + oldWrapCount + 1 >= endRow) { + break; + } + nextRow = nextRow.nextRowSameLine; + } + + row++; const newWrapStart = newRows.length; line.reflowNeeded = false; let startCol = 0; - let curRow: NewBufferLine = line; const dataLength = line.dataLength(); + for (;;) { line.moveToLineColumn(startCol + newCols); const idata = line._cachedDataIndex(); if (idata >= dataLength) { curRow.nextRowSameLine = undefined; - curRow._isWrapped = false; + //curRow._isWrapped = false; break; } startCol = line._cachedColumn(); @@ -389,22 +408,18 @@ export class Buffer implements IBuffer { && this.lines.get(row) instanceof WrappedBufferLine) { row++; } - const oldWrapCount = row - oldWrapStart; const newWrapCount = newRows.length - newWrapStart; - /* - if (newWrapCount !== oldWrapCount) { - if (yAbs >= row && yAbs < row + oldWrapCount) { - let y = yAbs; - if (y > row + newWrapCount) - y = row + newWrapCount; - this.y = y + deltaSoFar; - } + if (logicalX !== undefined) { // update cursor x and y + let i = newWrapStart; + while (i < newRows.length && newRows[i].logicalStartColumn() <= logicalX) { i++; } + this.y = startRow + i - 1 + deltaSoFar; + this.x = logicalX - newRows[i-1].logicalStartColumn(); } - */ deltaSoFar += newWrapCount - oldWrapCount; - } - if (deltaSoFar !== 0 && yAbs === row) { - this.y = yAbs - this.ybase + Math.min(deltaSoFar, newRows.length); + } else { + if (row === yAbs) + { this.y += deltaSoFar; } + row++; } } if (deltaSoFar !== 0) { @@ -775,4 +790,35 @@ export class Buffer implements IBuffer { this.markers.splice(this.markers.indexOf(marker), 1); } } + + // for DEBUGGING + noteError(msg: string) { + console.log('ERROR: ' + msg); + } + + // for DEBUGGING + checkLines(report = this.noteError): void { + const nlines = this.lines.length; + let prevRow: IBufferLine | undefined; + 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'); } + } else if (curRow instanceof WrappedBufferLine) { + 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.ts b/src/common/buffer/BufferLine.ts index 7d63af6e7a..eeea5bd508 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -275,7 +275,6 @@ export class OldBufferLine extends BufferLine implements IBufferLine { this.setCell(i, cell); } this.length = cols; - this._isWrapped = isWrapped; } /** @@ -847,8 +846,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { console.log('ERROR: '+str); } const icol = 0; - if (this.dataLength() < 0 || this.dataLength() > this.data().length) + 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); @@ -869,7 +876,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { default: error('invalid _dataKind'); } - } + } + */ } /** @@ -902,7 +910,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.preInsert(pos, fillCellData); const idata = this._cachedDataIndex(); this.addEmptyDataElements(idata, 1); - // Ideally should optize for adjacent SKIP_COLUMNS (as in eraseCells). + // Ideally should optimize for adjacent SKIP_COLUMNS (as in eraseCells). // However, typically is followed by replacing the new empty cells. this.data()[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); } @@ -1297,7 +1305,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * excess memory (true after shrinking > CLEANUP_THRESHOLD). */ public resize(cols: number, fillCellData: ICellData): boolean { - console.log('BufferLineNew.resize '+this.length+'->'+cols); /* if (cols === this.length) { return this.data().length * 4 * CLEANUP_THRESHOLD < this.data().buffer.byteLength; @@ -1699,7 +1706,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { const dcount = dskipLast + 1 - idata0; this.addEmptyDataElements(idata0, - dcount); } - } + } } export class WrappedBufferLine extends NewBufferLine implements IBufferLine { diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index bb3c2cadd0..c683388936 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -18,12 +18,24 @@ export interface IBuffer { ydisp: number; /** Number of rows in the scrollback buffer, above the home row. */ ybase: number; - /** Row number relative to the "home" row. + + /** 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; From cd360f24a2a9f1707cf3ac591ef1425b3dfdbc52 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 26 Dec 2023 03:12:21 -0800 Subject: [PATCH 39/73] Change reflow to be lazy --- src/browser/TestUtils.test.ts | 3 + src/common/InputHandler.ts | 1 + src/common/buffer/Buffer.ts | 147 ++++++++++++++++++++------- src/common/buffer/Types.d.ts | 1 + src/common/services/BufferService.ts | 1 + 5 files changed, 117 insertions(+), 36 deletions(-) diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 4f21de6164..99a3e000d5 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -237,6 +237,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.'); } diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index cfbbd39d09..d5bc20a5a2 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -431,6 +431,7 @@ export class InputHandler extends Disposable implements IInputHandler { let cursorStartY = this._activeBuffer.y; let start = 0; const wasPaused = this._parseStack.paused; + this._activeBuffer.reflowRegion(this._activeBuffer.ybase, this._activeBuffer.lines.length, -1); if (wasPaused) { // assumption: _parseBuffer never mutates between async calls diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index c79c3d7db1..9bc198e71c 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -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]); @@ -212,13 +217,14 @@ export class Buffer implements IBuffer { 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++) { @@ -233,7 +239,7 @@ 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(BufferLine.make(newCols, nullCell)); } else { @@ -296,32 +302,42 @@ 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._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._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) { @@ -353,16 +369,31 @@ export class Buffer implements IBuffer { } // Only if USE_NewBufferLine - private _reflowRegion(startRow: number, endRow: number, newCols: number): void { + 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++; } } - // FIXME don't need to allocate newRows if no lines require more rows - // than before. So better to allocate newRows lazily. + // 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 yAbs = this.ybase + this.y; + const yDispOld = this.ydisp; + const yBaseOld = this.ybase; + const yAbsOld = yBaseOld + this.y; + let yAbs = yAbsOld; 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) { @@ -372,7 +403,7 @@ export class Buffer implements IBuffer { let oldWrapCount = 0; // number of following wrapped lines let nextRow = curRow; for (; ; oldWrapCount++) { - if (yAbs === row + oldWrapCount) { + if (yAbsOld === row + oldWrapCount) { logicalX = nextRow.logicalStartColumn() + this.x; } if (! nextRow.nextRowSameLine || row + oldWrapCount + 1 >= endRow) { @@ -380,13 +411,15 @@ export class Buffer implements IBuffer { } 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(); @@ -404,43 +437,82 @@ export class Buffer implements IBuffer { 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++; } - this.y = startRow + i - 1 + deltaSoFar; + yAbs = startRow + i - 1 + deltaSoFar; this.x = logicalX - newRows[i-1].logicalStartColumn(); } deltaSoFar += newWrapCount - oldWrapCount; } else { - if (row === yAbs) - { this.y += deltaSoFar; } + if (row + deltaSoFar === yBaseOld) { this.ybase = yBaseOld + deltaSoFar; } + if (row + deltaSoFar === yDispOld) { this.ydisp = yDispOld + deltaSoFar; } + if (row === yAbsOld) { + yAbs += deltaSoFar; + } row++; } } if (deltaSoFar !== 0) { - if (yAbs >= endRow) - {this.y += deltaSoFar;} + if (yAbsOld >= endRow) { yAbs += deltaSoFar; } + if (yBaseOld >= endRow) { this.ybase = yBaseOld + deltaSoFar; } + if (yDispOld >= endRow) { this.ydisp = yDispOld + deltaSoFar; } } + this.y = yAbs - this.ybase; // 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; + // FIXME migrate Windows conpty handling + if (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; } - if (usingNewBufferLine()) { - // FIXME do this lazily - this._reflowRegion(0, this.lines.length, newCols); - return; - } // Iterate through rows, ignore the last one as it cannot be wrapped if (newCols > this._cols) { this._reflowLarger(newCols, newRows); @@ -449,6 +521,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) { @@ -458,6 +531,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 @@ -481,6 +555,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 diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index c683388936..43de3bc5df 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -58,6 +58,7 @@ export interface IBuffer { 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/services/BufferService.ts b/src/common/services/BufferService.ts index 6a38a4be91..3617a144ae 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -163,5 +163,6 @@ export class BufferService extends Disposable implements IBufferService { if (!suppressScrollEvent) { this._onScroll.fire(buffer.ydisp); } + buffer.reflowRegion(buffer.ydisp, buffer.lines.length, this.rows); } } From 33a0054a510e92fc340c8fda1fad8bb50a087c32 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 27 Dec 2023 21:41:46 -0800 Subject: [PATCH 40/73] Fix preInsert when position is in SKIP_SPACES or after end. Also fix some lint failures. --- .../renderer/dom/DomRendererRowFactory.ts | 1 - src/common/buffer/Buffer.ts | 16 +++---- src/common/buffer/BufferLine.ts | 47 +++++++++++++------ 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index cb047ac9f5..bfd350dada 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -73,7 +73,6 @@ export class DomRendererRowFactory { const cell = this._workCell; const elements: HTMLSpanElement[] = []; - const joinedRanges = this._characterJoinerService.getJoinedCharacters(row); const colors = this._themeService.colors; let lineLength = lineData.getNoBgTrimmedLength(); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 9bc198e71c..deedfec15c 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -146,7 +146,7 @@ export class Buffer implements IBuffer { } else { const prevRow = this.lines.get(absrow - 1) as NewBufferLine; const curRow = line as WrappedBufferLine; - const oldStartColumn = curRow.logicalStartColumn() + const oldStartColumn = curRow.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); const newRow = new LogicalBufferLine(line.length, undefined, oldLine); @@ -307,9 +307,8 @@ export class Buffer implements IBuffer { 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.reflowRegion(reflowNow ? 0 : this.ydisp, this.lines.length, + reflowNow? -1 : newRows); this._fixupPosition(); } else { // !usingNewBufferLine() if (this._isReflowEnabled) { @@ -425,7 +424,6 @@ export class Buffer implements IBuffer { const idata = line._cachedDataIndex(); if (idata >= dataLength) { curRow.nextRowSameLine = undefined; - //curRow._isWrapped = false; break; } startCol = line._cachedColumn(); @@ -462,7 +460,7 @@ export class Buffer implements IBuffer { if (row + deltaSoFar === yBaseOld) { this.ybase = yBaseOld + deltaSoFar; } if (row + deltaSoFar === yDispOld) { this.ydisp = yDispOld + deltaSoFar; } if (row === yAbsOld) { - yAbs += deltaSoFar; + yAbs += deltaSoFar; } row++; } @@ -867,8 +865,8 @@ export class Buffer implements IBuffer { } // for DEBUGGING - noteError(msg: string) { - console.log('ERROR: ' + msg); + noteError(msg: string): void { + console.log('ERROR: ' + msg); } // for DEBUGGING @@ -891,7 +889,7 @@ export class Buffer implements IBuffer { report('bad previous line before Wrapped'); } } else if (! curRow) { - report('undefined line in lines list'); + report('undefined line in lines list'); } prevRow = curRow; } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 22f7a2d252..30409d6f47 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -792,7 +792,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); } /* Human-readable display of data() array, for debugging */ - public showData(start = 0, end = this.dataLength()) { + public showData(start = 0, end = this.dataLength()): string { let s = '['; for (let i = start; i < end; i++) { const word = this.data()[i]; @@ -855,15 +855,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { function error(str: string): void { console.log('ERROR: '+str); } - const icol = 0; 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"); + error('SKIP followed by BG'); } if (this.dataLength() === 1 && data[0] === BufferLine.wSet1(DataKind.BG, 0)) { - error("default BG only"); + error('default BG only'); } /* for (let idata = 0; idata < this.dataLength(); idata++) { @@ -1053,8 +1052,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { private preInsert(index: LineColumn, attrs: IAttributeData): boolean { const content = this.moveToLineColumn(index); - const curColumn = this._cachedColumn(); - const startColumn = curColumn; + let curColumn = this._cachedColumn(); let idata = this._cachedDataIndex(); // CASES: @@ -1066,15 +1064,34 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // b. index === curColumn + width // c. otherwise - in middle of wide char - if ((content >> Content.WIDTH_SHIFT) === 2 - && 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, idata - clEnd - 1); - this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 2); + if (curColumn < index) { + if ((content >> Content.WIDTH_SHIFT) === 2 + && 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, idata - clEnd - 2); + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 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); } - // FIXME handle after dataLength() or in SKIP_COILUMNS // set attributes const newFg = attrs.getFg(); @@ -1524,7 +1541,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const clEnd = this.clusterEnd(idata); wcols = 1 << wide; if (col >= startCol && col + wcols <= endCol) { - addPendingString(idata, clEnd - idata); + addPendingString(idata, clEnd - idata); } idata = clEnd - 1; col += wcols; From 0b08b16d2dcb4e6869260c52f3b8de2b2de2deb0 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 28 Dec 2023 15:58:53 -0800 Subject: [PATCH 41/73] Fix bugs from playwright: translateToString and ICH --- src/common/buffer/BufferLine.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 30409d6f47..8bc33ec484 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -909,10 +909,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return; } if (pos + n < width) { - this.moveToColumn(pos + n); + const endpos = width - n; + this.moveToColumn(endpos); const idata = this._cachedDataIndex(); - const colOffset = -1; // ??? - this.logicalLine().deleteCellsOnly(idata, colOffset, width - (pos + n)); + const colOffset = this._cachedColumn(); + this.logicalLine().deleteCellsOnly(idata, endpos - colOffset, n); } else { n = width - pos; } @@ -921,7 +922,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); } /** Move to column 'index', which is a RowColumn. @@ -1550,7 +1551,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.CHAR_W2: wcols = 1 << wide; if (col >= startCol && col + wcols <= endCol) { - addPendingString(idata, wcols); + addPendingString(idata, 1); } col += wcols; break; From 6eee37b80ae3cc82fedf8543d580856f33ba9152 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 29 Dec 2023 15:23:01 -0800 Subject: [PATCH 42/73] Fix testsuite errors when newBufferLine is false --- .../out/base64.wasm.js/decode/final.wasm | Bin 654 -> 0 bytes src/common/Types.d.ts | 1 + src/common/buffer/BufferLine.test.ts | 163 +++++++++--------- src/common/buffer/BufferLine.ts | 6 +- src/common/buffer/CellData.ts | 2 +- src/common/buffer/Constants.ts | 1 + 6 files changed, 83 insertions(+), 90 deletions(-) delete mode 100644 addons/xterm-addon-image/inwasm-builds/out/base64.wasm.js/decode/final.wasm diff --git a/addons/xterm-addon-image/inwasm-builds/out/base64.wasm.js/decode/final.wasm b/addons/xterm-addon-image/inwasm-builds/out/base64.wasm.js/decode/final.wasm deleted file mode 100644 index bc27713ec3e989fff0416f51d3b2bdeff80b3f09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 654 zcmX|7O>fgc5S^K|+khLTM~)kxjEeI?oDz^KWiL$;5=XA!*qgMDleD%IwNj<@z=0z_ zp}-x9KS-F((p*+=XWqPdZy+w`2mt8f7r+fQQ0T?_V6NxO;*J1?Ab`UX(cP$TK|r7$gRF__tHC~~hg}V4>i)*33uh>4M-b8yK@25?AQV*rJxGJe;ui*^ z#m5GN#kHa+d`E4!+rtmeP+`N9D4kORiAwNh7Ircpqmcp1!iKV8&;SN#aGy)$-cvhDx1qKPz=%staI0iW>foIj*6GozL%rDdYML*3;2e|w2G zuB}MoTbSd3`feDbrtqZjVEyd6Sf;B)7bPAsOmtc5G#uZdo8<9g`l&l;by^)6TvpX} z`L5lbUR9T?aVuHQ+j+jWPsoFrPAZvp_35CWPNIIA^xsC&IO?UZ`_Y@U)9nqCG>Us# uM|z@P!mCCV7gJq*p6Iw*6}qI8#(Y)9<6MW+d^wKu@}yQ?O&6iQt^NU{T#7LO diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index cef7603854..8b345f5002 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -107,6 +107,7 @@ export interface ICharset { [key: string]: string | undefined; } +// Deprecated export type CharData = [number, string, number, number]; export interface IColor { diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 6b0921ce50..6e09a70aa7 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -9,21 +9,12 @@ import { CharData, IAttributeData, IBufferLine } from '../Types'; import { assert } from 'chai'; import { AttributeData } from 'common/buffer/AttributeData'; - -class TestBufferLine extends LogicalBufferLine /* FIXME */ { - constructor(cols: number, fillCellData?: IAttributeData, isWrapped?: boolean) { - - super(cols, fillCellData); - if (isWrapped) alert('TestBufferLine with isWrapped set not supported'); - } - - 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', () => { @@ -164,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)], @@ -209,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)], @@ -225,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)], @@ -241,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); }); @@ -269,37 +260,37 @@ 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😁'); @@ -313,29 +304,29 @@ describe('BufferLine', function(): void { }); 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])); @@ -344,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]); @@ -353,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)])); @@ -374,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)])); @@ -394,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)])); @@ -414,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])); @@ -443,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)])); @@ -459,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), ''); @@ -467,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]); @@ -475,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)); const cell = line.loadCell(0, new CellData()); @@ -487,7 +478,7 @@ describe('BufferLine', function(): void { */ }); 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); @@ -502,7 +493,7 @@ describe('BufferLine', function(): void { */ }); 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); @@ -525,7 +516,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'); @@ -535,7 +526,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¥¥¥ '); @@ -545,7 +536,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'); @@ -555,53 +546,53 @@ 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'); @@ -609,7 +600,7 @@ describe('BufferLine', function(): void { }); describe('extended attributes', () => { 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); @@ -632,7 +623,7 @@ 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)], @@ -650,7 +641,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); @@ -693,7 +684,7 @@ describe('BufferLine', function(): void { assert.notEqual(cell2.extended, cell3.extended); }); 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; @@ -703,7 +694,7 @@ describe('BufferLine', function(): void { assert.equal((line as any)._extendedAttrs[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; @@ -721,7 +712,7 @@ describe('BufferLine', function(): void { assert.equal((line as any)._extendedAttrs[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; @@ -736,7 +727,7 @@ describe('BufferLine', function(): void { assert.equal((line as any)._extendedAttrs[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; @@ -751,7 +742,7 @@ describe('BufferLine', function(): void { assert.equal((line as any)._extendedAttrs[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); @@ -782,7 +773,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); @@ -805,7 +796,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 8bc33ec484..6ab82b6dfb 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -38,7 +38,7 @@ export abstract class AbstractBufferLine implements IBufferLine { abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED abstract resize(cols: number, fillCellData: ICellData): boolean; - abstract fill(fillCellData: ICellData, respectProtect: boolean): void; + 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; @@ -560,7 +560,7 @@ export class OldBufferLine extends 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) { @@ -1379,7 +1379,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } /** fill a line with fillCharData */ - public fill(fillCellData: ICellData, respectProtect: boolean = false): void { + public fill(fillCellData: ICellData, respectProtect?: boolean): void { this.replaceCells(0, this.length, fillCellData, respectProtect); } diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index ca84eb3179..22e9ca1c83 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -71,7 +71,7 @@ export class CellData extends AttributeData implements ICellData { const length = value[CHAR_DATA_CHAR_INDEX].length; // surrogates and combined strings need special treatment if (length > 2) { - throw new Error('setFromCharData does not allow width > 2'); + combined = true; } else if (length === 2) { const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0); diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index a7441f5223..1bcc42b14a 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -7,6 +7,7 @@ export const DEFAULT_COLOR = 0; export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 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; From a8aab0063dce0030b90387c52f66bbd80b6ab4b6 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 29 Dec 2023 15:36:46 -0800 Subject: [PATCH 43/73] More Lint fixes. --- src/common/buffer/Buffer.ts | 4 ++-- src/common/buffer/BufferLine.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index deedfec15c..4fa5da5b4f 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -865,12 +865,12 @@ export class Buffer implements IBuffer { } // for DEBUGGING - noteError(msg: string): void { + public noteError(msg: string): void { console.log('ERROR: ' + msg); } // for DEBUGGING - checkLines(report = this.noteError): void { + public checkLines(report = this.noteError): void { const nlines = this.lines.length; let prevRow: IBufferLine | undefined; for (let i = 0; i < nlines; i++) { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6ab82b6dfb..6dcfe0f0d1 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -34,11 +34,11 @@ export abstract class AbstractBufferLine implements IBufferLine { /** Number of logical columns */ public length: number = 0; _isWrapped: boolean = false; - get isWrapped(): boolean { return this._isWrapped; } - abstract insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void; - abstract addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED - abstract resize(cols: number, fillCellData: ICellData): boolean; - abstract fill(fillCellData: ICellData, respectProtect?: boolean): void; + 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; @@ -1045,7 +1045,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public deleteCells(pos: number, n: number, fillCellData: ICellData): void { - const content = this.moveToColumn(pos); + this.moveToColumn(pos); const idata = this._cachedDataIndex(); const curColumn = this._cachedColumn(); this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); @@ -1089,7 +1089,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, oldSkip - needed); curColumn = index; } else { - console.log("can't insert at column "+index); + console.log(`can't insert at column ${index}`); } this._cacheSetColumnDataIndex(curColumn, idata); } @@ -1742,7 +1742,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { const dcount = dskipLast + 1 - idata0; this.addEmptyDataElements(idata0, - dcount); } - } + } } export class WrappedBufferLine extends NewBufferLine implements IBufferLine { From 845d202e0be7ace4743ec1184ac3b2335e6fd829 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 31 Dec 2023 14:13:20 -0800 Subject: [PATCH 44/73] Fix translateToString. --- src/common/buffer/Buffer.ts | 1 + src/common/buffer/BufferLine.ts | 189 +++++++++++++++++--------------- 2 files changed, 101 insertions(+), 89 deletions(-) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 4fa5da5b4f..c11bc2b685 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -214,6 +214,7 @@ export class Buffer implements IBuffer { 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; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6dcfe0f0d1..37101c3fc5 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -783,11 +783,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return this.moveToColumn(index) & Content.IS_COMBINED_MASK; } - /** for debugging */ - getText(skipReplace: string = ' '): string { - return this.translateToString(true, 0, this.length, undefined, skipReplace); - } - public showRowData(): string { return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); } @@ -1475,92 +1470,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, 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; + 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; } - 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(startCol); - } - } - } - 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 (! trimRight && col < endCol) { - addPendingSkip(endCol - col); - } - pendingForce(); return s; } } @@ -1743,6 +1659,101 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.addEmptyDataElements(idata0, - dcount); } } + + 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 (! 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 { From da516667f466b7485847bf571fb5f1bf2a189bad Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 6 Jan 2024 15:59:04 -0800 Subject: [PATCH 45/73] More reflow fixes, including handling saved cursor. --- src/common/InputHandler.ts | 9 ++++++--- src/common/Types.d.ts | 2 ++ src/common/buffer/Buffer.ts | 25 ++++++++++++++++++++++++- src/common/buffer/BufferLine.ts | 3 +++ src/common/parser/Types.d.ts | 2 +- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index a0df4833fd..1934f758b7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -431,7 +431,7 @@ export class InputHandler extends Disposable implements IInputHandler { let cursorStartY = this._activeBuffer.y; let start = 0; const wasPaused = this._parseStack.paused; - this._activeBuffer.reflowRegion(this._activeBuffer.ybase, this._activeBuffer.lines.length, -1); + usingNewBufferLine() && this._activeBuffer.reflowRegion(this._activeBuffer.ybase, this._activeBuffer.lines.length, -1); if (wasPaused) { // assumption: _parseBuffer never mutates between async calls @@ -1955,7 +1955,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: @@ -2193,7 +2192,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: @@ -2890,6 +2888,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); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 8b345f5002..3e5a8de8e0 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -254,6 +254,8 @@ export interface IBufferLine { clone(): IBufferLine; getTrimmedLength(): number; getNoBgTrimmedLength(): number; + // If not NewBufferLine, avoid (expensive) + isEmpty(): boolean; translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; /* direct access to cell attrs */ diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index c11bc2b685..239e48aa87 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -388,6 +388,8 @@ export class Buffer implements IBuffer { 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) { @@ -399,13 +401,16 @@ export class Buffer implements IBuffer { if (line instanceof LogicalBufferLine && line.reflowNeeded) { let curRow: NewBufferLine = line; - let logicalX; + 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; } @@ -456,6 +461,12 @@ export class Buffer implements IBuffer { 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; } @@ -463,15 +474,20 @@ export class Buffer implements IBuffer { 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; @@ -488,6 +504,13 @@ export class Buffer implements IBuffer { 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; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 37101c3fc5..56dd4bde2e 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -43,6 +43,7 @@ export abstract class AbstractBufferLine implements IBufferLine { public abstract clone(): IBufferLine; public abstract translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; public abstract getTrimmedLength(): number; + public isEmpty(): boolean { return this.getTrimmedLength() === 0; } public abstract getNoBgTrimmedLength(): number; public abstract cleanupMemory(): number; @@ -1517,6 +1518,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { override _cachedFg(): number { return this._cache3; } protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } + public isEmpty(): boolean { return this._dataLength === 0 && ! this.nextRowSameLine; } // count can be negative addEmptyDataElements(position: number, count: number): void { @@ -1796,6 +1798,7 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this._cacheSetStyleFlagsIndex(this.startStyle); this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); } + public override isEmpty(): boolean { return this._logicalLine.dataLength() === this.startIndex && ! this.nextRowSameLine; } public resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} } diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts index 2ed4acdcaf..49702a9df9 100644 --- a/src/common/parser/Types.d.ts +++ b/src/common/parser/Types.d.ts @@ -226,7 +226,7 @@ export interface IDcsParser extends ISubParser Date: Sat, 13 Jan 2024 13:58:15 -0800 Subject: [PATCH 46/73] Testsuite-based fixes and cleanups. New CellData.fromChar method intended to replace CellData.fromCharData. Fix logic errors in erasecells and deletecellsOnly. Fixes to InputHandler.print when wrapping - still needs work. Change _cachedStyleFlagsIndex so -1 (not 0) is "none". --- .../dom/DomRendererRowFactory.test.ts | 4 +- src/common/InputHandler.ts | 16 ++-- src/common/Types.d.ts | 2 + src/common/buffer/AttributeData.ts | 2 +- src/common/buffer/BufferLine.ts | 94 ++++++++----------- src/common/buffer/CellData.ts | 14 +++ src/common/buffer/Constants.ts | 2 +- 7 files changed, 68 insertions(+), 66 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 503fbe965b..b46b265bc3 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); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 1934f758b7..eb2e0f0cf7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -530,17 +530,21 @@ export class InputHandler extends Disposable implements IInputHandler { if (wraparoundMode) { const oldRow = bufferRow as NewBufferLine; // this._activeBuffer.x = oldWidth; - this._activeBuffer.y++; - if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { - this._activeBuffer.y--; + 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) { - this._activeBuffer.y = this._bufferService.rows - 1; + buffer.y = this._bufferService.rows - 1; + // FIXME overwrite last line - not impemented + col = cols; + } else { + buffer.splitLine(buffer.y, col); } } - this._activeBuffer.splitLine(this._activeBuffer.y, col); - bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + 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; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 3e5a8de8e0..6b449741cc 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -250,7 +250,9 @@ export interface IBufferLine { 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; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 85e79b14bd..d40e19a29d 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -48,7 +48,7 @@ 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 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); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 56dd4bde2e..8eb394ee85 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -200,7 +200,8 @@ export abstract class BufferLine extends AbstractBufferLine implements IBufferLi } - public abstract copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void; + // @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. */ @@ -709,9 +710,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // private _cachedColOffset(): number { return this._cache3 >> 24; } // UNUSED abstract _cachedBg(): number; abstract _cachedFg(): number; - // One more than index (in data()) of STYLE_FLAGS; 0 if none. + // 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 = 0; } + 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); } @@ -732,8 +733,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. protected _extendedAttrs: IExtendedAttrs[] = []; - // protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; - // public length: number; public abstract logicalLine(): LogicalBufferLine; public abstract logicalStartColumn(): LineColumn; @@ -758,7 +757,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public getFg(index: number): number { this.moveToColumn(index); const styleIndex = this._cachedStyleFlagsIndex(); - const styleWord = styleIndex > 0 ? this.data()[styleIndex - 1] : 0; + const styleWord = styleIndex < 0 ? 0 : this.data()[styleIndex]; return this._cachedFg() | ((styleWord << 24) & Attributes.STYLE_BITS_MASK); } @@ -766,7 +765,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public getBg(index: number): number { this.moveToColumn(index); const styleIndex = this._cachedStyleFlagsIndex(); - const styleWord = styleIndex > 0 ? this.data()[styleIndex - 1] : 0; + const styleWord = styleIndex < 0 ? 0 : this.data()[styleIndex]; return this._cachedBg() | ((styleWord << 16) & Attributes.STYLE_BITS_MASK); } @@ -833,7 +832,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { value = '*' + (word & 0x1fffff); } else if (kind === DataKind.BG || kind === DataKind.FG) { value = (wnum >> 24) + '#' + (wnum & 0xffffff).toString(16); - } else if (DataKind.STYLE_FLAGS) { + } else if (kind === DataKind.STYLE_FLAGS) { value = '#' + (wnum & 0xfffffff).toString(16); } else if (kind !== DataKind.SKIP_COLUMNS) { value = wnum; @@ -966,8 +965,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { idata++; break; case DataKind.STYLE_FLAGS: - idata++; styleFlagsIndex = idata; + idata++; break; case DataKind.SKIP_COLUMNS: const wlen = BufferLine.wSkipCount(word); @@ -1026,10 +1025,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { cursor.setFg(this._cachedFg()); cursor.setBg(this._cachedBg()); const styleFlagsIndex = this._cachedStyleFlagsIndex(); - const word = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; + const word = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; cursor.setStyleFlags(word); if (word & StyleFlags.HAS_EXTENDED) { - cursor.extended = this._extendedAttrs[styleFlagsIndex - 1]!; + cursor.extended = this._extendedAttrs[styleFlagsIndex]!; } if (content & Content.IS_COMBINED_MASK) { // FIXME do this lazily, in CellData.getChars @@ -1097,12 +1096,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const oldFg = this._cachedFg(); const oldBg = this._cachedBg(); const styleFlagsIndex = this._cachedStyleFlagsIndex(); - const oldStyle = styleFlagsIndex > 0 ? this.data()[styleFlagsIndex - 1] : 0; + const oldStyle = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; let needFg = newFg !== oldFg; let needBg = newBg !== oldBg; - // FIXME let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && cell.extended; - // let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; - let needStyle = newStyle !== oldStyle; // FIXME || oldExt !== newExt; + let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[styleFlagsIndex]; + let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; + let needStyle = newStyle !== oldStyle || oldExt !== newExt; const atEnd = idata === this.dataLength(); const add1 = atEnd ? 1 : 2; let add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); @@ -1228,28 +1227,23 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const startColumn = this.logicalStartColumn(); const count = end - start; start += startColumn; - if (end === this.length && ! this.nextRowSameLine) { - this.preInsert(start, attrs); - this.logicalLine()._dataLength = this._cachedDataIndex(); - return; - } - // const add = this.preInsert(start, attrs); this.moveToLineColumn(start); - const add = 0; // FIXME end += startColumn; let idata = this._cachedDataIndex(); const colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); lline.deleteCellsOnly(idata, colOffset, count); + this.preInsert(start, attrs); 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; + end = start - skipped; idata--; lline._dataLength = idata; } else { - data[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, BufferLine.wSkipCount(data[idata - 1]) + count); + data[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, skipped + count); } } else { if (idata === this.dataLength()) { @@ -1379,25 +1373,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.replaceCells(0, this.length, fillCellData, respectProtect); } - /** alter to a full copy of line */ + // @deprecated - not used if usingNewBufferLine() public copyFrom(xline: BufferLine): void { alert('copyFrom'); - /* - const line = xline as LogicalBufferLine; // FIXME - if (this.length !== line.length) { - this._data = new Uint32Array(line._data); - } else { - // use high speed copy if lengths are equal - this.data().set(line.data()); - } - this.dataLength() = line.dataLength(); - this.length = line.length; - this._extendedAttrs = {}; - for (const el in line._extendedAttrs) { - this._extendedAttrs[el] = line._extendedAttrs[el]; - } - this._isWrapped = line.isWrapped; - */ } /** create a new clone */ @@ -1493,7 +1471,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { _cache1: number = 0; _cache2: number = 0; _cache3: number = 0; - _cache4: number = 0; + _cache4: number = -1; constructor(cols: number, fillCellData?: IAttributeData, src?: LogicalBufferLine) { super(); @@ -1535,12 +1513,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } this._extendedAttrs.copyWithin(position + count, position, this._dataLength); this._dataLength += count; - // FIXME cleanup old element s in _extendedAttr. - // But is it worth it - only matters for garbage collection? - // if (count > 0) for each element i in _data[position..position+count-1] ( - // if _data[i] & (0xC000000|HAS_EXTENDED) === (STYLE_FLAGS<<28)|HAS_EXTENDED - // delete this._extendedAttrs[i] - // else this._extendedAttr.length = this._dataLength; + if (this._extendedAttrs.length > this._dataLength) { + this._extendedAttrs.length = this._dataLength; + } } resizeData(size: number): void { @@ -1572,7 +1547,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { // FIXME doesn't properly handle if delete range starts or ends in middle // of wide character - /** Internal - delete n columns, with adjust at end of line. */ + /** 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(); @@ -1588,8 +1563,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { let skipItem = true; const word = data[idata-1]; switch (BufferLine.wKind(word)) { - case DataKind.BG: bgValue = word; break; - case DataKind.FG: fgValue = word; break; + case DataKind.BG: bgValue = word & 0x3ffffff; break; + case DataKind.FG: fgValue = word & 0x3ffffff; break; case DataKind.STYLE_FLAGS: styleValue = word; break; default: skipItem = false; } @@ -1607,9 +1582,16 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { const word = data[idata]; const kind = BufferLine.wKind(word); switch (kind) { - case DataKind.FG: fgValue = word; break; - case DataKind.BG: bgValue = word; break; + 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; // handle ExtendedAttrs FIXME break; @@ -1650,10 +1632,10 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (bgValue >= 0) { this.data()[idata0++] = BufferLine.wSet1(DataKind.BG, bgValue); } - if (fgValue >= 0) { + if (fgValue >= 0 && idata0 !== this._dataLength) { this.data()[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); } - if (styleValue >= 0) { + if (styleValue >= 0 && idata0 !== this._dataLength) { this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); } if (dskipLast >= 0) { @@ -1772,7 +1754,7 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { startColumn: LineColumn = 0; startFg: number = 0; startBg: number = 0; - startStyle: number = 0; + startStyle: number = -1; constructor(prevRow: NewBufferLine) { super(); diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 22e9ca1c83..8a6144a7e2 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -18,6 +18,13 @@ 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; @@ -63,6 +70,13 @@ export class CellData extends AttributeData implements ICellData { } 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]; diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 1bcc42b14a..e978cc725b 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -4,7 +4,7 @@ */ 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 From 2d2c027d9038facd55c7991366482cfcddfa2a1e Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 13 Jan 2024 18:54:30 -0800 Subject: [PATCH 47/73] FixImplement extended-attribute handling in deleteCellsOnly. Also extended-underline testcase. --- src/common/InputHandler.test.ts | 20 +++++++++++--------- src/common/buffer/BufferLine.ts | 18 +++++++++++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 8f9a988f14..13ad4b998a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -1882,17 +1882,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/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 8eb394ee85..8d23745ad8 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -172,7 +172,7 @@ export abstract class AbstractBufferLine implements IBufferLine { 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 26 bits is StyleFlags + 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 @@ -1556,7 +1556,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { let dskipFirst = idata; let dskipLast = -1; let w; let fgValue = -1; // cursor.getFg(); let bgValue = -1; // cursor.getBg(); - let styleValue = -1; // cursor.getStyleFlags(); // FIXME handle extendedattrs + let styleValue = -1; + let extended = undefined; // cursor.getStyleFlags(); // FIXME handle extendedattrs if (colOffset === 0) { while (idata > 0) { @@ -1565,7 +1566,11 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { switch (BufferLine.wKind(word)) { case DataKind.BG: bgValue = word & 0x3ffffff; break; case DataKind.FG: fgValue = word & 0x3ffffff; break; - case DataKind.STYLE_FLAGS: styleValue = word; break; + case DataKind.STYLE_FLAGS: + styleValue = word & 0xfffffff; + if (word & StyleFlags.HAS_EXTENDED) {} + extended = (word & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[idata - 1]; + break; default: skipItem = false; } if (skipItem) { @@ -1592,8 +1597,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { break; case DataKind.STYLE_FLAGS: dskipLast = idata; - styleValue = word; - // handle ExtendedAttrs FIXME + styleValue = word & 0xfffffff; + extended = (word & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[idata]; break; case DataKind.SKIP_COLUMNS: const wlen = BufferLine.wSkipCount(word); @@ -1636,6 +1641,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.data()[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); } if (styleValue >= 0 && idata0 !== this._dataLength) { + if ((styleValue & StyleFlags.HAS_EXTENDED) && extended) { + this._extendedAttrs[idata0] = extended; + } this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); } if (dskipLast >= 0) { From 10719a0ab675fa9f836561e014181bc2edd7e676 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 22 Jan 2024 15:37:47 -0800 Subject: [PATCH 48/73] Fixes for extended attributes and setWrapped for test-cases. --- src/common/buffer/Buffer.ts | 5 +- src/common/buffer/BufferLine.test.ts | 89 +++++++++-------- src/common/buffer/BufferLine.ts | 137 +++++++++++++++++++++------ 3 files changed, 157 insertions(+), 74 deletions(-) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 239e48aa87..dc0fa01354 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -142,7 +142,10 @@ export class Buffer implements IBuffer { if (! usingNewBufferLine()) { line!._isWrapped = value; } else if (value) { - alert('setWrapped true'); // only used in test cases? + 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; diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 6e09a70aa7..ff639afe12 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -3,11 +3,11 @@ * @license MIT */ import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR, Content, UnderlineStyle, BgFlags, Attributes, FgFlags } from 'common/buffer/Constants'; -import { BufferLine, LogicalBufferLine } from 'common/buffer//BufferLine'; +import { BufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; -import { CharData, IAttributeData, IBufferLine } from '../Types'; +import { CharData, IBufferLine, IExtendedAttrs } from '../Types'; import { assert } from 'chai'; -import { AttributeData } from 'common/buffer/AttributeData'; +import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; function lineToArray(line: IBufferLine): CharData[] { const result = []; @@ -599,6 +599,11 @@ describe('BufferLine', function(): void { }); }); 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 = BufferLine.make(5); const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); @@ -630,9 +635,9 @@ describe('BufferLine', function(): void { [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 @@ -664,24 +669,18 @@ 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 = BufferLine.make(3); @@ -689,9 +688,9 @@ describe('BufferLine', function(): void { 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 = BufferLine.make(5); @@ -699,17 +698,17 @@ describe('BufferLine', function(): void { 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 = BufferLine.make(5); @@ -720,11 +719,11 @@ 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 = BufferLine.make(5); @@ -735,11 +734,11 @@ 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 = BufferLine.make(5); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 8d23745ad8..fb8100a1c6 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -6,7 +6,7 @@ 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, StyleFlags, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants'; +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'; @@ -132,12 +132,8 @@ export abstract class AbstractBufferLine implements IBufferLine { } abstract setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void; - /** - * Set data at `index` to `cell`. FIXME doesn't handle combined chars. - */ - public setCell(index: number, cell: ICellData): void { - this.setCellFromCodepoint(index, cell.getCode(), cell.getWidth(), cell); - } + + public abstract setCell(index: number, cell: ICellData): void; /** * Get codepoint of the cell. @deprecated @@ -825,15 +821,26 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } value = JSON.stringify(str); } else if (kind === DataKind.CLUSTER_START_W1 - || kind === DataKind.CLUSTER_START_W2) { - // FIXME extract cluster as string - value = '*' + (word & 0x1fffff); - } else if (kind === DataKind.CLUSTER_CONTINUED) { - value = '*' + (word & 0x1fffff); + || 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:DDUBLE"; 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 = wnum; } else { @@ -859,7 +866,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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); @@ -868,6 +874,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; @@ -880,8 +890,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { default: error('invalid _dataKind'); } - } - */ + } + } /** @@ -1027,7 +1037,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const styleFlagsIndex = this._cachedStyleFlagsIndex(); const word = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; cursor.setStyleFlags(word); - if (word & StyleFlags.HAS_EXTENDED) { + if ((word & StyleFlags.HAS_EXTENDED) !== 0) { cursor.extended = this._extendedAttrs[styleFlagsIndex]!; } if (content & Content.IS_COMBINED_MASK) { @@ -1050,6 +1060,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const content = this.moveToLineColumn(index); let curColumn = this._cachedColumn(); let idata = this._cachedDataIndex(); + const oldData = this.showData(); // CASES: // 1. idata === dataLength() - easy. @@ -1145,6 +1156,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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); } } @@ -1255,6 +1268,50 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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) { @@ -1268,7 +1325,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const lindex = index + this.logicalStartColumn(); const add = this.preInsert(lindex, attrs); // FIXME let curColumn = this._cachedColumn(); - const startColumn = curColumn; let idata = this._cachedDataIndex(); let inext; if (add || idata === this.dataLength() || lindex === curColumn) @@ -1290,7 +1346,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.data()[inext++] = BufferLine.wSet1(kind, codePoint); this._cacheSetColumnDataIndex(cellColumn, idata); if (idata < this.dataLength()) { - this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); + this.logicalLine().deleteCellsOnly(inext, 0, width); } } @@ -1500,19 +1556,24 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { // count can be negative addEmptyDataElements(position: number, count: number): void { - // FIXME also adjust _extendedAttr indexes - this.resizeData(this.dataLength() + count); + const oldDataLength = this._dataLength; + this.resizeData(oldDataLength + count); if (count < 0) { - this.data().copyWithin(position, position - count, this._dataLength); + this.data().copyWithin(position, position - count, oldDataLength); } else { - this.data().copyWithin(position + count, position, this._dataLength); + 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;} } - this._extendedAttrs.copyWithin(position + count, position, this._dataLength); - this._dataLength += 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; } @@ -1568,8 +1629,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { case DataKind.FG: fgValue = word & 0x3ffffff; break; case DataKind.STYLE_FLAGS: styleValue = word & 0xfffffff; - if (word & StyleFlags.HAS_EXTENDED) {} - extended = (word & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[idata - 1]; + extended = (word & StyleFlags.HAS_EXTENDED) !== 0 && this._extendedAttrs[idata - 1]; break; default: skipItem = false; } @@ -1598,7 +1658,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { case DataKind.STYLE_FLAGS: dskipLast = idata; styleValue = word & 0xfffffff; - extended = (word & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[idata]; + extended = (word & StyleFlags.HAS_EXTENDED) !== 0 && this._extendedAttrs[idata]; break; case DataKind.SKIP_COLUMNS: const wlen = BufferLine.wSkipCount(word); @@ -1641,7 +1701,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this.data()[idata0++] = BufferLine.wSet1(DataKind.FG, fgValue); } if (styleValue >= 0 && idata0 !== this._dataLength) { - if ((styleValue & StyleFlags.HAS_EXTENDED) && extended) { + 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); @@ -1652,6 +1713,26 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } + 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); + 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; From c5685bb489f4f1be8da3e375994e93e15ba4d0b6 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 25 Jan 2024 10:55:24 -0800 Subject: [PATCH 49/73] More polishing and testsuite fixes. --- src/common/InputHandler.test.ts | 10 ++-- src/common/InputHandler.ts | 40 ++++++++-------- src/common/buffer/Buffer.ts | 9 +++- src/common/buffer/BufferLine.test.ts | 14 ++---- src/common/buffer/BufferLine.ts | 69 +++++++++++++++++----------- 5 files changed, 80 insertions(+), 62 deletions(-) diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 13ad4b998a..e352b0ce24 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -449,18 +449,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(' ') ]); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index eb2e0f0cf7..6e7e8d8aa7 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -522,8 +522,7 @@ export class InputHandler extends Disposable implements IInputHandler { const cols = this._bufferService.cols; this._dirtyRowTracker.markDirty(this._activeBuffer.y); // if (charset) replace character; FIXME ok to do it in-place? - const insertMode = this._coreService.modes.insertMode; - let col = (bufferRow as NewBufferLine).insertText(this._activeBuffer.x, data, start, end, curAttr, this, insertMode); + 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 @@ -1207,12 +1206,15 @@ export class InputHandler extends Disposable implements IInputHandler { 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) { + if (clearWrap && start === 0) { this._activeBuffer.setWrapped(row, false); } } @@ -1222,9 +1224,8 @@ 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 { + private _resetBufferLine(row: number, respectProtect: boolean = false): void { const buffer = this._activeBuffer; - const row = buffer.ybase + y; const line = buffer.lines.get(row); if (line) { const eraseAttrs = this._eraseAttrData(); @@ -1234,7 +1235,7 @@ export class InputHandler extends Disposable implements IInputHandler { } else { line.fill(this._activeBuffer.getNullCell(eraseAttrs), respectProtect); } - buffer.clearMarkers(this._activeBuffer.ybase + y); + buffer.clearMarkers(row); buffer.setWrapped(row, false); if (wasNewBufferLine !== usingNewBufferLine()) { const fill = this._activeBuffer.getNullCell(eraseAttrs); @@ -1269,19 +1270,22 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseInDisplay(params: IParams, respectProtect: boolean = false): boolean { this._restrictCursor(this._bufferService.cols); - let j; let x; + // 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); - x = this._activeBuffer.x; - if (x > 0) { - this._eraseInBufferLine(j++, x, this._bufferService.cols, this._activeBuffer.x === 0, respectProtect); + this._dirtyRowTracker.markDirty(y); + while (--j > y || (j === y && x === 0)) { + this._resetBufferLine(buffer.ybase + j, respectProtect); } - for (; j < this._bufferService.rows; j++) { - this._resetBufferLine(j, respectProtect); + if (x > 0) { + this._eraseInBufferLine(y, x, Infinity, false, respectProtect); } - this._dirtyRowTracker.markDirty(j); break; case 1: j = this._activeBuffer.y; @@ -1293,7 +1297,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._activeBuffer.setWrapped(j + 1, false); } while (j--) { - this._resetBufferLine(j, respectProtect); + this._resetBufferLine(buffer.ybase + j, respectProtect); } this._dirtyRowTracker.markDirty(0); break; @@ -1301,7 +1305,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; @@ -1346,13 +1350,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); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index dc0fa01354..4e80ea97d2 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -152,13 +152,13 @@ export class Buffer implements IBuffer { const oldStartColumn = curRow.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); - const newRow = new LogicalBufferLine(line.length, undefined, oldLine); + const newRow = new LogicalBufferLine(line.length, undefined, curRow); newRow.nextRowSameLine = curRow.nextRowSameLine; const oldStart = curRow.startIndex; - newRow.addEmptyDataElements(0, - oldStart); for (let nextRow = newRow.nextRowSameLine; nextRow; nextRow = nextRow.nextRowSameLine) { nextRow.startColumn -= oldStartColumn; nextRow.startIndex -= oldStart; + nextRow._logicalLine = newRow; } oldLine._dataLength = curRow.startIndex; this.lines.set(absrow, newRow); @@ -900,11 +900,16 @@ export class Buffer implements IBuffer { 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) { diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index ff639afe12..549bc540ed 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -7,7 +7,7 @@ import { BufferLine } from 'common/buffer//BufferLine'; import { CellData } from 'common/buffer/CellData'; import { CharData, IBufferLine, IExtendedAttrs } from '../Types'; import { assert } from 'chai'; -import { AttributeData, ExtendedAttrs } from 'common/buffer/AttributeData'; +import { AttributeData } from 'common/buffer/AttributeData'; function lineToArray(line: IBufferLine): CharData[] { const result = []; @@ -467,45 +467,39 @@ describe('BufferLine', function(): void { describe('addCharToCell', () => { it('should set width to 1 for empty cell', () => { 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)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0); const cell = line.loadCell(0, new CellData()); // chars contains single combining char // width is set to 1 assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]); // do not account a single combining char as combined assert.equal(cell.isCombined(), 0); - */ }); it('should add char to combining string in cell', () => { 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); - /* - line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0); line.loadCell(0, cell); // chars contains 3 chars // width is set to 1 assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]); // do not account a single combining char as combined assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); - */ }); it('should create combining string on taken cell', () => { 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); - /* - line.addCodepointToCell(0, '\u0301'.charCodeAt(0)); + line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0); line.loadCell(0, cell); // chars contains 2 chars // width is set to 1 assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]); // do not account a single combining char as combined assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK); - */ }); }); describe('correct fullwidth handling', () => { diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index fb8100a1c6..6b502a123e 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -9,6 +9,7 @@ import { CellData } from 'common/buffer/CellData'; 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()); @@ -834,7 +835,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { else { switch (extended.underlineStyle) { case UnderlineStyle.SINGLE: value += " us:SINGLE"; break; - case UnderlineStyle.DOUBLE: value += " us:DDUBLE"; 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; @@ -1056,11 +1057,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); } - private preInsert(index: LineColumn, attrs: IAttributeData): boolean { + private preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { const content = this.moveToLineColumn(index); let curColumn = this._cachedColumn(); let idata = this._cachedDataIndex(); - const oldData = this.showData(); // CASES: // 1. idata === dataLength() - easy. @@ -1107,15 +1107,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const oldFg = this._cachedFg(); const oldBg = this._cachedBg(); const styleFlagsIndex = this._cachedStyleFlagsIndex(); - const oldStyle = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; + const oldStyle = styleFlagsIndex < 0 ? 0 : (this.data()[styleFlagsIndex] & 0xfffffff); 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 atEnd = idata === this.dataLength(); - const add1 = atEnd ? 1 : 2; - let add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); + const add1 = extendToEnd ? 1 : 2; + let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); let data = this.data(); if (add) { const idata0 = idata; @@ -1134,7 +1133,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { break; } } - add = (needBg?2:0) + (needFg?add1:0) + (needStyle?add1:0); + add =(needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); this.addEmptyDataElements(idata, add - (idata0 - idata)); data = this.data(); if (needFg) { @@ -1151,8 +1150,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } this._cacheSetColumnDataIndex(index, idata); let xdata = idata; // FIXME - if (! atEnd) { - if (newFg !== oldFg) { + if (! extendToEnd) { + if (needFg) { data[xdata++] = BufferLine.wSet1(DataKind.FG, oldFg); } if (needStyle) { @@ -1160,10 +1159,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { {this._extendedAttrs[xdata] = oldExt;} data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); } - } - if (needBg) { - data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); - } + if (needBg) { + data[xdata++] = BufferLine.wSet1(DataKind.BG, oldBg); + } + } this._cacheSetFgBg(newFg, newBg); } return add > 0; @@ -1173,7 +1172,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * @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, insertMode: boolean): RowColumn { + 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; const lstart = this.logicalStartColumn(); const lindex = index + lstart; const add = this.preInsert(lindex, attrs); @@ -1196,6 +1197,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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, @@ -1204,7 +1206,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // then idata === inext. const code = data[i]; const currentInfo = inputHandler.unicodeService.charProperties(code, precedingJoinState); - const chWidth = UnicodeService.extractWidth(currentInfo); + chWidth = UnicodeService.extractWidth(currentInfo); const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0; precedingJoinState = currentInfo; @@ -1226,18 +1228,27 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } this.data()[inext++] = BufferLine.wSet1(kind, code); } + const lastChar = idata; inputHandler.precedingJoinState = precedingJoinState; - this._cacheSetColumnDataIndex(cellColumn, idata); if (! insertMode && idata < this.dataLength()) { this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); } if (curColumn > lline.logicalWidth) {lline.logicalWidth = curColumn;} - return curColumn - lstart; + 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); @@ -1246,7 +1257,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); lline.deleteCellsOnly(idata, colOffset, count); - this.preInsert(start, attrs); + this.preInsert(start, attrs, end === Infinity); idata = this._cachedDataIndex(); const data = this.data(); if (idata > 0 && BufferLine.wKind(data[idata-1]) === DataKind.SKIP_COLUMNS) { @@ -1364,11 +1375,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const clEnd = this.clusterEnd(idata); this.addEmptyDataElements(clEnd, 1); const nContinued = clEnd - idata; - const startChar = this.data()[idata] & 0x1FFFFF; + 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; - this.data()[idata] = BufferLine.wSet1(kind, + data[idata] = BufferLine.wSet1(kind, startChar + (nContinued << 21)); - this.data()[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); + data[clEnd] = BufferLine.wSet1(DataKind.CLUSTER_CONTINUED, codePoint); } /** @@ -1529,14 +1543,16 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { _cache3: number = 0; _cache4: number = -1; - constructor(cols: number, fillCellData?: IAttributeData, src?: LogicalBufferLine) { + constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); // const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); if (src) { - // FIXME also copy _extendedAttrs - this._data = src._data.slice(); - this._dataLength = src._dataLength; + const lline = src.logicalLine(); + const oldStart = src.startIndex; + 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; @@ -1718,6 +1734,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { 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; } From e0b62ddcede6c3d3b7045f1780f01981788e0a03 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 25 Jan 2024 21:27:15 -0800 Subject: [PATCH 50/73] Change preInsert to reduce duplicated style entries --- src/common/buffer/BufferLine.ts | 59 +++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6b502a123e..a1bf551d40 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -934,14 +934,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Move to column 'index', which is a RowColumn. * Return encoded 'content'. */ - public moveToColumn(index: RowColumn): number { - return this.moveToLineColumn(index + this.logicalStartColumn(), this.dataRowEnd()); + public moveToColumn(index: RowColumn, stopEarly: boolean = false): number { + return this.moveToLineColumn(index + this.logicalStartColumn(), this.dataRowEnd(), stopEarly); } /** Move to column 'index', which is a LineColumn. * Return encoded 'content'. */ - public moveToLineColumn(index: LineColumn, end = this.dataLength()): number { + public moveToLineColumn(index: LineColumn, end = this.dataLength(), stopEarly: boolean = false): number { let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better @@ -956,7 +956,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let word; let kind; let content = 0; - while (todo >= 0) { + while (stopEarly ? todo > 0 : todo >= 0) { if (idata >= end) { word = NULL_DATA_WORD; kind = DataKind.SKIP_COLUMNS; @@ -1058,7 +1058,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } private preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { - const content = this.moveToLineColumn(index); + const content = this.moveToLineColumn(index, this.dataLength(), true); let curColumn = this._cachedColumn(); let idata = this._cachedDataIndex(); @@ -1104,10 +1104,34 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const newFg = attrs.getFg(); const newBg = attrs.getBg(); const newStyle = attrs.getStyleFlags(); - const oldFg = this._cachedFg(); - const oldBg = this._cachedBg(); + 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]; @@ -1115,24 +1139,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let needStyle = newStyle !== oldStyle || oldExt !== newExt; const add1 = extendToEnd ? 1 : 2; let add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); - let data = this.data(); - if (add) { - const idata0 = idata; - let skipItem = true; - for (; idata > 0; idata--) { - const word = data[idata-1]; - switch (BufferLine.wKind(word)) { - case DataKind.BG: needBg = true; break; - case DataKind.FG: needFg = true; break; - case DataKind.STYLE_FLAGS: needStyle = true; - delete this._extendedAttrs[idata-1]; - break; - default: skipItem = false; - } - if (! skipItem) { - break; - } - } + if (add ) { add =(needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); this.addEmptyDataElements(idata, add - (idata0 - idata)); data = this.data(); @@ -1164,6 +1171,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } } this._cacheSetFgBg(newFg, newBg); + } else if (idata > idata0) { + this._cacheSetColumnDataIndex(index, idata); } return add > 0; } From bc57c28c8fd1378296940d96ac74d360ae257203 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 25 Feb 2024 09:24:42 -0800 Subject: [PATCH 51/73] Adjust showRow and showDataRow. Handle soft line breaks in the middle of SKIP_COLUMNS. Change showData to take columns bounds rather than data index bounds. --- src/common/buffer/BufferLine.ts | 41 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index a1bf551d40..a62ba3e8f3 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -781,35 +781,40 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public showRowData(): string { - return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); + //return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); + return this.showData(this.logicalStartColumn(), this.nextRowSameLine ? this.nextRowSameLine?.logicalStartColumn() : Infinity); } /* Human-readable display of data() array, for debugging */ - public showData(start = 0, end = this.dataLength()): string { - let s = '['; - for (let i = start; i < end; i++) { + 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'; break; - case DataKind.CLUSTER_START_W1: code = 'CL1'; break; - case DataKind.CLUSTER_START_W2: code = 'CL2'; 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'; break; - case DataKind.CHAR_W2: code = 'C2'; break; + case DataKind.CHAR_W1: code = 'C1'; nextColumn += 1; break; + case DataKind.CHAR_W2: code = 'C2'; nextColumn += 2; break; } - if (i >= start) { - if (i !== start) { + + if (startColumn < nextColumn) { + if (s) { s += ', '; } let value; if (kind === DataKind.CHAR_W1 || kind === DataKind.CHAR_W2) { let count = 1; - while (i + count < end && BufferLine.wKind(this.data()[i + count]) === kind) { + const w = nextColumn - curColumn; + while (curColumn + count * w < endColumn && i + count < this.dataLength() && BufferLine.wKind(this.data()[i + count]) === kind) { count++; } let str; @@ -821,6 +826,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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) { @@ -842,15 +848,20 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } } } - } else if (kind !== DataKind.SKIP_COLUMNS) { - value = wnum; + } 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 + ']'; + return `[${s}]`; } /** Check invariants. Useful for debugging. */ From 9aaf5164aefe716ee7df624221ee867efd171b0b Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 28 Feb 2024 18:33:51 -0800 Subject: [PATCH 52/73] Fixes mostly related to wrapped lines with wide characters --- .../dom/DomRendererRowFactory.test.ts | 2 +- src/common/buffer/Buffer.ts | 1 + src/common/buffer/BufferLine.ts | 88 +++++++++++++------ 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index b46b265bc3..68ebe4e121 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -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語𝄞' diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 4e80ea97d2..5b78c2afd4 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -152,6 +152,7 @@ export class Buffer implements IBuffer { const oldStartColumn = curRow.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); + curRow.startIndex = oldLine._splitIfNeeded(oldStartColumn); const newRow = new LogicalBufferLine(line.length, undefined, curRow); newRow.nextRowSameLine = curRow.nextRowSameLine; const oldStart = curRow.startIndex; diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index a62ba3e8f3..c1eb542cfa 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -724,10 +724,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // Length of data() array. abstract dataLength(): number; - // End of current row in data() array. - protected dataRowEnd(): number { - return this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); - } // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. protected _extendedAttrs: IExtendedAttrs[] = []; @@ -930,11 +926,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.moveToColumn(endpos); const idata = this._cachedDataIndex(); const colOffset = this._cachedColumn(); - this.logicalLine().deleteCellsOnly(idata, endpos - colOffset, n); + this.logicalLine().deleteCellsOnly(idata, this.logicalStartColumn() + endpos - colOffset, n); } else { n = width - pos; } - this.preInsert(pos, fillCellData); + this.preInsert(this.logicalStartColumn() + pos, fillCellData); const idata = this._cachedDataIndex(); this.addEmptyDataElements(idata, 1); // Ideally should optimize for adjacent SKIP_COLUMNS (as in eraseCells). @@ -946,13 +942,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * Return encoded 'content'. */ public moveToColumn(index: RowColumn, stopEarly: boolean = false): number { - return this.moveToLineColumn(index + this.logicalStartColumn(), this.dataRowEnd(), stopEarly); + 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'. + * 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, end = this.dataLength(), stopEarly: boolean = false): number { + public moveToLineColumn(index: LineColumn, endColumn = Infinity, stopEarly: boolean = false): number { let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better @@ -968,12 +967,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let kind; let content = 0; while (stopEarly ? todo > 0 : todo >= 0) { - if (idata >= end) { + 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; @@ -991,11 +991,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { idata++; break; case DataKind.SKIP_COLUMNS: - const wlen = BufferLine.wSkipCount(word); - if (todo >= wlen) { - todo -= wlen; + w = BufferLine.wSkipCount(word); + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { + todo -= w; idata++; - curColumn += wlen; + curColumn += w; } else { content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; todo = -1; @@ -1004,13 +1005,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.CLUSTER_START_W1: case DataKind.CLUSTER_START_W2: w = kind + 1 - DataKind.CLUSTER_START_W1; - if (todo >= w) { + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { const clEnd = this.clusterEnd(idata); todo -= w; - curColumn += w; + curColumn = nextColumn; idata = clEnd; } else { - content = index !== curColumn ? 0 + 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; } @@ -1018,13 +1022,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.CHAR_W1: case DataKind.CHAR_W2: w = kind + 1 - DataKind.CHAR_W1; // 1, or 2 if wide characters - if (todo >= w) { + nextColumn = curColumn + w; + if (todo >= w && nextColumn <= endColumn) { todo -= w; idata++; - curColumn += w; + curColumn = nextColumn; } else { todo = -1; - content = index !== curColumn ? 0 + content = nextColumn > endColumn + ? (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE + : index !== curColumn ? 0 : (w << Content.WIDTH_SHIFT) | (word & 0x1fffff); } break; @@ -1068,8 +1075,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); } - private preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { - const content = this.moveToLineColumn(index, this.dataLength(), true); + public _splitIfNeeded(index: LineColumn): number { + const content = this.logicalLine().moveToLineColumn(index, Infinity, true); let curColumn = this._cachedColumn(); let idata = this._cachedDataIndex(); @@ -1083,13 +1090,29 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // c. otherwise - in middle of wide char if (curColumn < index) { - if ((content >> Content.WIDTH_SHIFT) === 2 + 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, idata - clEnd - 2); - this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); + 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()) { @@ -1110,7 +1133,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } this._cacheSetColumnDataIndex(curColumn, idata); } - + return idata; + } + private preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { + let idata = this._splitIfNeeded(index); // set attributes const newFg = attrs.getFg(); const newBg = attrs.getBg(); @@ -1195,9 +1221,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; - const lstart = this.logicalStartColumn(); - const lindex = index + lstart; + 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; @@ -1480,7 +1508,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let skipped = 0; const startColumn = this.logicalStartColumn(); const data = this.data(); - const end = this.dataRowEnd(); + const end = this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); let bg = this._cachedBg(); for (let idata = startColumn; idata < end; idata++) { const word = data[idata]; @@ -1853,6 +1881,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { break; } } + if (col < startCol) { col = startCol; } if (! trimRight && col < endCol && endCol !== Infinity) { addPendingSkip(endCol - col); } @@ -1868,6 +1897,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { export class WrappedBufferLine extends NewBufferLine implements IBufferLine { _logicalLine: LogicalBufferLine; + // DEPRECATE FIXME startIndex doesn't work in the case of when soft line-break is inside a SKIP_COLUMNS. startIndex: number = 0; /** Number of logical columns in previous rows. * Also: logical column number (column number assuming infinitely-wide From b25427f384d3c608bdbffd326075885fafd1e20b Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 29 Feb 2024 11:04:03 -0800 Subject: [PATCH 53/73] Fixes to Buffer.setWrapped --- src/common/Types.d.ts | 2 -- src/common/buffer/Buffer.ts | 8 +++++--- src/common/buffer/BufferLine.ts | 21 +++++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 6b449741cc..7b8cdd1ab0 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -256,8 +256,6 @@ export interface IBufferLine { clone(): IBufferLine; getTrimmedLength(): number; getNoBgTrimmedLength(): number; - // If not NewBufferLine, avoid (expensive) - isEmpty(): boolean; translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; /* direct access to cell attrs */ diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 5b78c2afd4..e023026bf9 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -152,8 +152,10 @@ export class Buffer implements IBuffer { const oldStartColumn = curRow.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); - curRow.startIndex = oldLine._splitIfNeeded(oldStartColumn); - const newRow = new LogicalBufferLine(line.length, undefined, curRow); + 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) { @@ -161,7 +163,7 @@ export class Buffer implements IBuffer { nextRow.startIndex -= oldStart; nextRow._logicalLine = newRow; } - oldLine._dataLength = curRow.startIndex; + oldLine._dataLength = startIndex; this.lines.set(absrow, newRow); } } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index c1eb542cfa..c062637232 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -44,7 +44,6 @@ export abstract class AbstractBufferLine implements IBufferLine { public abstract clone(): IBufferLine; public abstract translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; public abstract getTrimmedLength(): number; - public isEmpty(): boolean { return this.getTrimmedLength() === 0; } public abstract getNoBgTrimmedLength(): number; public abstract cleanupMemory(): number; @@ -777,7 +776,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public showRowData(): string { - //return this.showData(this instanceof WrappedBufferLine ? this.startIndex : 0, this.dataRowEnd()); return this.showData(this.logicalStartColumn(), this.nextRowSameLine ? this.nextRowSameLine?.logicalStartColumn() : Infinity); } /* Human-readable display of data() array, for debugging */ @@ -1045,7 +1043,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** * 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. @deprecated + * 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; @@ -1135,7 +1133,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } return idata; } - private preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { + protected preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { let idata = this._splitIfNeeded(index); // set attributes const newFg = attrs.getFg(); @@ -1591,13 +1589,13 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { _cache3: number = 0; _cache4: number = -1; - constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine) { + 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 = src.startIndex; + const oldStart = startIndex || 0; this._data = lline._data.slice(oldStart); this._dataLength = lline._dataLength - oldStart; this._extendedAttrs = lline._extendedAttrs.slice(oldStart); @@ -1607,6 +1605,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } this.length = cols; this._isWrapped = false; + if (fillCellData) { this.preInsert(0, fillCellData); } } public override logicalLine(): LogicalBufferLine { return this; } public override logicalStartColumn(): LineColumn { return 0; } @@ -1616,7 +1615,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { override _cachedFg(): number { return this._cache3; } protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } - public isEmpty(): boolean { return this._dataLength === 0 && ! this.nextRowSameLine; } // count can be negative addEmptyDataElements(position: number, count: number): void { @@ -1897,8 +1895,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { export class WrappedBufferLine extends NewBufferLine implements IBufferLine { _logicalLine: LogicalBufferLine; - // DEPRECATE FIXME startIndex doesn't work in the case of when soft line-break is inside a SKIP_COLUMNS. - startIndex: number = 0; /** Number of logical columns in previous rows. * Also: logical column number (column number assuming infinitely-wide * terminal) corresponding to the start of this row. @@ -1908,6 +1904,12 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { * 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; @@ -1936,7 +1938,6 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this._cacheSetStyleFlagsIndex(this.startStyle); this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); } - public override isEmpty(): boolean { return this._logicalLine.dataLength() === this.startIndex && ! this.nextRowSameLine; } public resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} } From 8b23f24e2d6d41039f6121c7ee1fc6bc794a729f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 5 Aug 2024 16:43:25 -0700 Subject: [PATCH 54/73] Remove 2 unused fields. --- src/common/Types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/Types.ts b/src/common/Types.ts index 4bc9ecf730..b3b20bbfe9 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -456,8 +456,6 @@ 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; From 5313b49a7f7ad885fa21b6cb0b463195d2c6a05f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 7 Aug 2024 10:55:35 -0700 Subject: [PATCH 55/73] Revert "Remove 2 unused fields." This reverts commit 8b23f24e2d6d41039f6121c7ee1fc6bc794a729f. --- src/common/Types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/common/Types.ts b/src/common/Types.ts index b3b20bbfe9..4bc9ecf730 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -456,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; From 973ec45c5ef7df6005e5375b17c2960dc2f5d038 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sat, 5 Oct 2024 12:37:55 -0700 Subject: [PATCH 56/73] Provide basic implementation of NewBufferLine.resize This method (now only used for testing) fixes 4 test-cases. --- src/common/buffer/BufferLine.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index c062637232..a6d2a09445 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -731,6 +731,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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 @@ -1437,14 +1441,15 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * 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.data().length * 4 * CLEANUP_THRESHOLD < this.data().buffer.byteLength; + 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); @@ -1454,13 +1459,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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++) { @@ -1477,11 +1483,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { delete this._extendedAttrs[key]; } } - * / + */ } - */ this.length = cols; - return this.dataLength() * CLEANUP_THRESHOLD < this.data().length; + return this.shouldCleanupMemory(); } /** fill a line with fillCharData */ @@ -1657,14 +1662,12 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { * Returns 0 or 1 indicating whether a cleanup happened. */ public cleanupMemory(): number { - /* - if (this.dataLength() * CLEANUP_THRESHOLD < this.data().length) { + if (this.shouldCleanupMemory()) { const data = new Uint32Array(this.dataLength()); data.set(this.data()); this._data = data; return 1; } - */ return 0; } From f662ef5aabe10817671763f61f63295001bee1d9 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 24 Feb 2025 09:15:23 -0800 Subject: [PATCH 57/73] Fixes to NewBufferLine. More tests pass. --- src/common/CircularList.ts | 24 +++-- src/common/InputHandler.ts | 3 +- src/common/buffer/Buffer.test.ts | 17 +++- src/common/buffer/Buffer.ts | 111 ++++++++++++++------- src/common/buffer/BufferLine.ts | 139 +++++++++++++++++++++------ src/common/services/BufferService.ts | 1 - 6 files changed, 209 insertions(+), 86 deletions(-) diff --git a/src/common/CircularList.ts b/src/common/CircularList.ts index a46faca300..7fc782158e 100644 --- a/src/common/CircularList.ts +++ b/src/common/CircularList.ts @@ -153,14 +153,16 @@ export class CircularList extends Disposable implements ICircularList { * @param deleteCount The number of elements to delete. * @param items The items to insert. */ - public splice(start: number, deleteCount: number, ...items: T[]): void { + public spliceNoTrim(start: number, deleteCount: number, items: T[], fireEvents: boolean = true): void { // Delete items if (deleteCount) { for (let i = start; i < this._length - deleteCount; i++) { this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)]; } this._length -= deleteCount; - this.onDeleteEmitter.fire({ index: start, amount: deleteCount }); + if (fireEvents) { + this.onDeleteEmitter.fire({ index: start, amount: deleteCount }); + } } // Add items @@ -170,20 +172,24 @@ export class CircularList extends Disposable implements ICircularList { for (let i = 0; i < items.length; i++) { this._array[this._getCyclicIndex(start + i)] = items[i]; } - if (items.length) { + if (items.length && fireEvents) { this.onInsertEmitter.fire({ index: start, amount: items.length }); } - - // Adjust length as needed - if (this._length + items.length > this._maxLength) { - const countToTrim = (this._length + items.length) - this._maxLength; + this._length += items.length; + } + public trimIfNeeded(): void { + if (this._length > this._maxLength) { + const countToTrim = this._length - this._maxLength; this._startIndex += countToTrim; this._length = this._maxLength; this.onTrimEmitter.fire(countToTrim); - } else { - this._length += items.length; } } + public splice(start: number, deleteCount: number, ...items: T[]): void { + this.spliceNoTrim(start, deleteCount, items); + // Adjust length as needed + this.trimIfNeeded(); + } /** * Trims a number of items from the start of the list. diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index b6a5541f32..5582dfe18c 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -430,7 +430,6 @@ 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 @@ -539,7 +538,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (this._activeBuffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; - // FIXME overwrite last line - not impemented + // FIXME overwrite last line - not implemented col = cols; } else { buffer.splitLine(buffer.y, col); diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts index 7d1455a657..e064d39b07 100644 --- a/src/common/buffer/Buffer.test.ts +++ b/src/common/buffer/Buffer.test.ts @@ -454,8 +454,8 @@ describe('Buffer', () => { assert.equal(buffer.lines.get(1)!.translateToString(), '0123456789'); assert.equal(buffer.lines.get(2)!.translateToString(), 'klmnopqrst'); assert.equal(firstMarker.line, 0, 'first marker should remain unchanged'); - assert.equal(secondMarker.line, 1, 'second marker should be restored to it\'s original line'); - assert.equal(thirdMarker.line, 2, 'third marker should be restored to it\'s original line'); + assert.equal(secondMarker.line, 1, 'second marker should be restored to its original line'); + assert.equal(thirdMarker.line, 2, 'third marker should be restored to its original line'); assert.equal(firstMarker.isDisposed, false); assert.equal(secondMarker.isDisposed, false); assert.equal(thirdMarker.isDisposed, false); @@ -740,9 +740,16 @@ describe('Buffer', () => { it('should adjust the viewport and keep ydisp = ybase', () => { buffer.ydisp = 10; buffer.resize(4, 10); - assert.equal(buffer.y, 9); - assert.equal(buffer.ydisp, 7); - assert.equal(buffer.ybase, 7); + assert.equal(buffer.ybase + buffer.y, 16); + if (false) { + // Old _reflowLargerAdjustViewport modifies ybase and ydisp + // but the logic seems wrong. ??? + assert.equal(buffer.ydisp, 7); + assert.equal(buffer.ybase, 7); + } else { + assert.equal(buffer.ydisp, 10); + assert.equal(buffer.ybase, 10); + } assert.equal(buffer.lines.length, 17); for (let i = 0; i < 10; i++) { assert.equal(buffer.lines.get(i)!.translateToString(), ' '); diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 7f8ae69e97..c079142e82 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -29,6 +29,7 @@ export class Buffer implements IBuffer { public lines: CircularList; public ydisp: number = 0; public ybase: number = 0; + /** Row number, relative to ybase. */ public y: number = 0; public x: number = 0; public scrollBottom: number; @@ -118,11 +119,13 @@ export class Buffer implements IBuffer { return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength; } - public splitLine(row: number, col: number): void { + public splitLine(row: number, col: number): void { // FIXME col is unused 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); + let startColumn = curRow.logicalStartColumn() + bufferService.cols; + curRow.moveToLineColumn(startColumn); + startColumn = curRow._cachedColumn(); // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); let newRow; if (nextRow.isWrapped) { @@ -132,7 +135,7 @@ export class Buffer implements IBuffer { // append nextRow contents to end of curRow.logicalLine() this.lines.set(this.ybase + row, newRow); } - curRow.setStartFromCache(newRow); + newRow.setStartFromCache(curRow, startColumn);`` } public setWrapped(absrow: number, value: boolean): void { @@ -149,21 +152,7 @@ export class Buffer implements IBuffer { } 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; + const newRow = curRow.asUnwrapped(prevRow); this.lines.set(absrow, newRow); } } @@ -231,7 +220,7 @@ export class Buffer implements IBuffer { // The following adjustments should only happen if the buffer has been // initialized/filled. - if (! usingNewBufferLine() && this.lines.length > 0) { + if (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++) { @@ -310,12 +299,26 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; if (usingNewBufferLine()) { - const lazyReflow = true; + const lazyReflow = false; // FUTURE - change to 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); + // Reduce max length if needed after adjustments, this is done after as it + // would otherwise cut data from the bottom of the buffer. + if (newMaxLength < this.lines.maxLength) { + // Trim from the top of the buffer and adjust ybase and ydisp. + const amountToTrim = this.lines.length - newMaxLength; + if (amountToTrim > 0) { + this.setWrapped(amountToTrim, false); + this.lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + this.savedY = Math.max(this.savedY - amountToTrim, 0); + } + this.lines.maxLength = newMaxLength; + } this._fixupPosition(); } else { // !usingNewBufferLine() if (this._isReflowEnabled) { @@ -376,7 +379,7 @@ export class Buffer implements IBuffer { // Only if USE_NewBufferLine public reflowRegion(startRow: number, endRow: number, maxRows: number): void { - if (startRow >= this.lastReflowNeeded) { + if (startRow > this.lastReflowNeeded) { return; } if (endRow >= this.lastReflowNeeded) { @@ -397,6 +400,10 @@ export class Buffer implements IBuffer { const ySavedOld = this.savedY; let ySaved = ySavedOld; let deltaSoFar = 0; + // Record buffer insert/delete events + const insertEvents: IInsertEvent[] = []; + let oldRows: (IBufferLine|undefined)[] = []; + for (let j = 0; j < this.lines.length; j++) { oldRows.push(this.lines.get(j));} for (let row = startRow; row < endRow;) { if (maxRows >= 0 && newRows.length > maxRows) { endRow = row; @@ -407,7 +414,7 @@ export class Buffer implements IBuffer { if (line instanceof LogicalBufferLine && line.reflowNeeded) { let curRow: NewBufferLine = line; - let logicalX, logicalSavedX; + let logicalX, logicalSavedX = this.savedX; let oldWrapCount = 0; // number of following wrapped lines let nextRow = curRow; for (; ; oldWrapCount++) { @@ -443,7 +450,7 @@ export class Buffer implements IBuffer { const newRow = newRow1 instanceof WrappedBufferLine ? (row++, newRow1) : new WrappedBufferLine(curRow); - line.setStartFromCache(newRow); + newRow.setStartFromCache(line, startCol); newRows.push(newRow); curRow = newRow; } @@ -467,12 +474,19 @@ export class Buffer implements IBuffer { yAbs = startRow + i - 1 + deltaSoFar; this.x = logicalX - newRows[i-1].logicalStartColumn(); } - if (logicalSavedX !== undefined) { // update cursor x and y + if (logicalSavedX !== undefined) { // update cursor savedX and savedY let i = newWrapStart; while (i < newRows.length && newRows[i].logicalStartColumn() <= logicalSavedX) { i++; } ySaved = startRow + i - 1 + deltaSoFar; this.savedX = logicalSavedX - newRows[i-1].logicalStartColumn(); } + if (newWrapCount != oldWrapCount) { + // Create insert events for later + insertEvents.push({ + index: lineRow + deltaSoFar + 1, + amount: newWrapCount - oldWrapCount + }); + } deltaSoFar += newWrapCount - oldWrapCount; } else { if (row + deltaSoFar === yBaseOld) { this.ybase = yBaseOld + deltaSoFar; } @@ -494,15 +508,37 @@ export class Buffer implements IBuffer { } 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; + let trimNeeded = oldLinesCount + newRows.length - (endRow - startRow) + - this.lines.maxLength; + if (trimNeeded > 0) { + this.ybase -= trimNeeded; + this.ydisp -= trimNeeded; + if (trimNeeded > startRow) { + const trimNew = trimNeeded - startRow; + const firstNewRow = newRows[trimNew]; + if (firstNewRow instanceof WrappedBufferLine) { + newRows[trimNew] = firstNewRow.asUnwrapped(/*PREVIOUS*/); + } + newRows.splice(0, trimNew); + trimNeeded -= trimNew; + } + } + this.lines.spliceNoTrim(startRow, endRow - startRow, newRows, false); + if (trimNeeded > 0) { + this.setWrapped(trimNeeded,false); + this.lines.trimIfNeeded(); + } + // Update markers + const insertCount = insertEvents.length; + for (let i = 0; i < insertCount; i++) { + const event = insertEvents[i]; + if (event.amount < 0) { + event.amount = - event.amount; + this.lines.onDeleteEmitter.fire(event); + } else { + this.lines.onInsertEmitter.fire(event); + } } this._fixupPosition(); } @@ -527,12 +563,13 @@ export class Buffer implements IBuffer { 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; + const adjust = this.lines.length - this.ybase - rows; + if (adjust > 0) { + this.ybase += adjust; + this.y -= adjust; } - this.ydisp = Math.max(0, Math.min(this.ydisp, this.lines.length - rows)); + const yy=this.ydisp; + this.ydisp = Math.max(0, Math.min(this.ydisp, this.lines.length)); } // DEPRECATED - only if !usingNewBufferLine() diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index a6d2a09445..6a0757c042 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -14,7 +14,7 @@ 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. + * The left-most column is column 0. */ type RowColumn = number; @@ -707,19 +707,20 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; } + _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 { + /*public setStartFromCacheX(wrapRow: WrappedBufferLine, column: LineColumn): void { + wrapRow.startColumn = column; wrapRow.startIndex = this._cachedDataIndex(); - wrapRow.startColumn = this._cachedColumn(); + wrapRow.startIndexColumn = this._cachedColumn(); wrapRow.startBg = this._cachedBg(); wrapRow.startFg = this._cachedFg(); wrapRow.startStyle = this._cachedStyleFlagsIndex(); - } + }*/ // Length of data() array. abstract dataLength(): number; @@ -780,7 +781,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } public showRowData(): string { - return this.showData(this.logicalStartColumn(), this.nextRowSameLine ? this.nextRowSameLine?.logicalStartColumn() : Infinity); + return (this.isWrapped ? '(wrapped)' : '') + + 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 { @@ -943,9 +945,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Move to column 'index', which is a RowColumn. * Return encoded 'content'. */ - public moveToColumn(index: RowColumn, stopEarly: boolean = false): number { + public moveToColumn(index: RowColumn): number { const endColumn = this.nextRowSameLine ? this.nextRowSameLine.logicalStartColumn() : Infinity; - return this.moveToLineColumn(index + this.logicalStartColumn(), endColumn, stopEarly); + return this.moveToLineColumn(index + this.logicalStartColumn(), endColumn, false); } /** Move to column 'index', which is a LineColumn. @@ -1077,6 +1079,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); } + /** Split data element if index is in middle of wide character or SKIP_COLUMNS or after end. */ public _splitIfNeeded(index: LineColumn): number { const content = this.logicalLine().moveToLineColumn(index, Infinity, true); let curColumn = this._cachedColumn(); @@ -1283,8 +1286,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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); @@ -1330,7 +1331,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } /** - * Set data at `index` to `cell`. FIXME doesn't handle combined chars. + * Set data at `index` to `cell`. */ public setCell(index: number, cell: ICellData): void { const width = cell.getWidth(); @@ -1506,13 +1507,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return newLine; } - public getTrimmedLength(countBackground: boolean = false): number { + public getTrimmedLength(countBackground: boolean = false, logical: boolean = false): number { let cols = 0; let skipped = 0; - const startColumn = this.logicalStartColumn(); + const startColumn = logical ? 0 : this.logicalStartColumn(); // FIXME startIndex const data = this.data(); - const end = this.nextRowSameLine ? this.nextRowSameLine.startIndex : this.dataLength(); + const end = this.nextRowSameLine && ! logical ? this.nextRowSameLine.startIndex : this.dataLength(); let bg = this._cachedBg(); + let bgCol = 0; for (let idata = startColumn; idata < end; idata++) { const word = data[idata]; const kind = BufferLine.wKind(word); @@ -1521,6 +1523,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { switch (kind) { case DataKind.BG: bg = word & 0x3ffffff; + bgCol = cols + skipped; break; case DataKind.FG: case DataKind.STYLE_FLAGS: @@ -1546,7 +1549,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { skipped = 0; } } - return countBackground && bg !== 0 ? this.length : cols; + if (countBackground) { + cols += skipped; + cols = bg ? this.length : Math.max(cols, bgCol); + } + return cols; } public getNoBgTrimmedLength(): number { @@ -1569,9 +1576,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } } - public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { + public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = -1, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { const lineStart = this.logicalStartColumn(); - const s = this.logicalLine().translateLogicalToString(trimRight, lineStart + startCol, lineStart + endCol, outColumns, skipReplace); + const logicalEndColumn = endCol >= 0 ? lineStart + endCol + : this.nextRowSameLine ? this.nextRowSameLine.logicalStartColumn() + : lineStart + this.length; + let s = this.logicalLine().translateLogicalToString(trimRight, lineStart + startCol, logicalEndColumn, outColumns, skipReplace); + if (!trimRight && endCol < 0) { s += ' '.repeat(this.length - logicalEndColumn + lineStart)} if (outColumns && lineStart !== 0) { for (let i = outColumns.length; --i >= 0; ) { outColumns[i] -= lineStart; @@ -1584,11 +1595,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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; + _dataLength: number = 0; // active length of _data array + /** Width in collumns if there is no line-wrapping. */ + public get logicalWidth(): number { return this.getTrimmedLength(false, true); } + //logicalWidth: number = 0; // FIXME needs work updating this + public reflowNeeded: boolean = false; - // Maybe move these to LogicalBufferLine? or to Buffer? + // Maybe move these to to Buffer? Would save space. but with API complications. _cache1: number = 0; _cache2: number = 0; _cache3: number = 0; @@ -1779,21 +1792,36 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } public setWrapped(previousLine: NewBufferLine): WrappedBufferLine { - previousLine.moveToColumn(previousLine.length); + const column = this.logicalStartColumn() + previousLine.length; + previousLine.moveToLineColumn(column); + const neededPadding = column - previousLine._cachedColumn(); 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; + let startLength = startLine._dataLength; + const padWithSpaces = false; // use spaces or SKIP_COLUMNS? + let padLength = neededPadding <= 0 ? 0 : padWithSpaces ? neededPadding : 1; + startLine.resizeData(this._dataLength + startLength + padLength); + if (neededPadding > 0) { + if (padWithSpaces) { + while (--padLength >= 0) { + startLine._data[startLength++] = BufferLine.wSet1(DataKind.CHAR_W1, 32); + } + } else { + startLine._data[startLength++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, neededPadding); + } + } + startLine._data.set(this._data.subarray(0, this._dataLength), startLength); + startLine._dataLength = startLength + this._dataLength; for (let i = this._extendedAttrs.length; --i >= 0; ) { const attr = this._extendedAttrs[i]; - if (attr) { startLine._extendedAttrs[startLine._dataLength + i] = attr; } + if (attr) { startLine._extendedAttrs[startLength + i] = attr; } } const newRow = new WrappedBufferLine(previousLine); newRow.nextRowSameLine = this.nextRowSameLine; - startLine.setStartFromCache(newRow); + newRow.setStartFromCache(startLine, column); for (let following = this.nextRowSameLine; following; following = following?.nextRowSameLine) { following.startColumn += newRow.startColumn; + following.startIndexColumn += newRow.startIndexColumn; following.startIndex += newRow.startIndex; } return newRow; @@ -1866,7 +1894,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { case DataKind.CLUSTER_START_W2: const clEnd = this.clusterEnd(idata); wcols = 1 << wide; - if (col >= startCol && col + wcols <= endCol) { + if (col >= startCol && col < endCol) { + //if (col >= startCol && col + wcols <= endCol) { addPendingString(idata, clEnd - idata); } idata = clEnd - 1; @@ -1875,7 +1904,8 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { case DataKind.CHAR_W1: case DataKind.CHAR_W2: wcols = 1 << wide; - if (col >= startCol && col + wcols <= endCol) { + if (col >= startCol && col < endCol) { + //if (col >= startCol && col + wcols <= endCol) { addPendingString(idata, 1); } col += wcols; @@ -1910,9 +1940,14 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { // 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. + // to mitigate the performance cost we could support backwards movement by moveToColumn. // Changing Data>FG etc to use xor-encoding would help. TODO. + /** Index in data array containing first column. */ startIndex: number = 0; + /** Column number corresponding to startIndex. + * Usually the same as startColumn, but may be less if startIndex refers to a SKIP_COLUMNS. + */ + startIndexColumn: number = 0; startFg: number = 0; startBg: number = 0; startStyle: number = -1; @@ -1926,6 +1961,15 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this.length = logicalLine.length; } + public setStartFromCache(line: NewBufferLine, column: LineColumn): void { + this.startColumn = column; + this.startIndex = line._cachedDataIndex(); + this.startIndexColumn = line._cachedColumn(); + this.startBg = line._cachedBg(); + this.startFg = line._cachedFg(); + this.startStyle = line._cachedStyleFlagsIndex(); + } + public override logicalLine(): LogicalBufferLine { return this._logicalLine; } public override logicalStartColumn(): LineColumn { return this.startColumn; } protected override data(): Uint32Array { return this._logicalLine.data(); } @@ -1935,12 +1979,43 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { addEmptyDataElements(position: number, count: number): void { this._logicalLine.addEmptyDataElements(position, count); } - protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF) - this.startColumn; } + protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF) - this.startIndexColumn; } protected _cacheReset(): void { this._cacheSetFgBg(this.startFg, this.startBg); this._cacheSetStyleFlagsIndex(this.startStyle); - this._cacheSetColumnDataIndex(this.startColumn, this.startIndex); + this._cacheSetColumnDataIndex(this.startIndexColumn, this.startIndex); } public resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} + public getPreviousRow(): NewBufferLine { + for (let row: NewBufferLine = this._logicalLine; ;) { + const next = row.nextRowSameLine as NewBufferLine; + if (next === this) { + return row; + } + row = next; + } + } + + public asUnwrapped(prevRow: NewBufferLine = this.getPreviousRow()): LogicalBufferLine { + const oldStartColumn = this.logicalStartColumn(); + prevRow.nextRowSameLine = undefined; + const oldLine = prevRow.logicalLine(); + const startIndex = oldLine._splitIfNeeded(oldStartColumn); + const cell = new CellData(); + this.loadCell(oldStartColumn, cell); + const newRow = new LogicalBufferLine(this.length, cell, this, startIndex); + newRow.nextRowSameLine = this.nextRowSameLine; + const oldStart = this.startIndex; + const oldIndexColumn = this.startIndexColumn; + for (let nextRow = newRow.nextRowSameLine; nextRow; nextRow = nextRow.nextRowSameLine) { + nextRow.startColumn -= oldStartColumn; + nextRow.startIndex -= oldStart; + nextRow.startIndexColumn -= oldIndexColumn; + nextRow._logicalLine = newRow; + } + oldLine._dataLength = startIndex; + return newRow; + + } } diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index bb8cb292fd..55f037a4bd 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -163,6 +163,5 @@ export class BufferService extends Disposable implements IBufferService { if (!suppressScrollEvent) { this._onScroll.fire(buffer.ydisp); } - buffer.reflowRegion(buffer.ydisp, buffer.lines.length, this.rows); } } From 3b1e7b7e4adfa6f14ecc2e3ee3ac43ae2c1223d8 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 25 Feb 2025 12:48:43 -0800 Subject: [PATCH 58/73] Fix some problems with wrapped lines. --- src/common/buffer/Buffer.ts | 12 ++++++------ src/common/buffer/BufferLine.ts | 16 ++++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index d68136fe05..b383d5e3a9 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -124,8 +124,6 @@ export class Buffer implements IBuffer { const curRow = this.lines.get(this.ybase + row - 1) as NewBufferLine; const nextRow = this.lines.get(this.ybase + row) as NewBufferLine; let startColumn = curRow.logicalStartColumn() + bufferService.cols; - curRow.moveToLineColumn(startColumn); - startColumn = curRow._cachedColumn(); // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); let newRow; if (nextRow.isWrapped) { @@ -135,7 +133,8 @@ export class Buffer implements IBuffer { // append nextRow contents to end of curRow.logicalLine() this.lines.set(this.ybase + row, newRow); } - newRow.setStartFromCache(curRow, startColumn);`` + const content = curRow.moveToLineColumn(startColumn); + newRow.setStartFromCache(curRow, startColumn, content); } public setWrapped(absrow: number, value: boolean): void { @@ -439,18 +438,19 @@ export class Buffer implements IBuffer { // Loop over new WrappedBufferLines for current LogicalBufferLine, // based on newCols width. Re-use old WrappedBufferLine if available. for (;;) { - line.moveToLineColumn(startCol + newCols); + const endCol = startCol + newCols; + const content = line.moveToLineColumn(endCol); 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); - newRow.setStartFromCache(line, startCol); + newRow.setStartFromCache(line, endCol, content); + startCol = newRow.startColumn; newRows.push(newRow); curRow = newRow; } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6a0757c042..f38a7fad79 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -952,8 +952,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** 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. + * If index is the middle of a multi-column character: leaves the position before the character; + * the return value specifices the code value 0 and the width 0. + * If index is in the middle of a SKIP_COLUMNS: leaves the position cache before the SKIP_COLUMNS; + * the return value specifices the code value 0 and width 1. + * If index would take us past _dataLength: Set the position to _dataLength; + * the return value specifices the code value 0 and width 1. */ public moveToLineColumn(index: LineColumn, endColumn = Infinity, stopEarly: boolean = false): number { let curColumn = this._cachedColumn(); @@ -1793,7 +1797,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { public setWrapped(previousLine: NewBufferLine): WrappedBufferLine { const column = this.logicalStartColumn() + previousLine.length; - previousLine.moveToLineColumn(column); + const content = previousLine.moveToLineColumn(column); const neededPadding = column - previousLine._cachedColumn(); const startLine = previousLine.logicalLine(); let startLength = startLine._dataLength; @@ -1817,7 +1821,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } const newRow = new WrappedBufferLine(previousLine); newRow.nextRowSameLine = this.nextRowSameLine; - newRow.setStartFromCache(startLine, column); + newRow.setStartFromCache(startLine, column, content); for (let following = this.nextRowSameLine; following; following = following?.nextRowSameLine) { following.startColumn += newRow.startColumn; @@ -1961,8 +1965,8 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { this.length = logicalLine.length; } - public setStartFromCache(line: NewBufferLine, column: LineColumn): void { - this.startColumn = column; + public setStartFromCache(line: NewBufferLine, column: LineColumn, content: number): void { + this.startColumn = content === 0 ? line._cachedColumn() : column; this.startIndex = line._cachedDataIndex(); this.startIndexColumn = line._cachedColumn(); this.startBg = line._cachedBg(); From 91e9c28a00c4167323cd7e7d69b8352b75396877 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 2 Mar 2025 12:10:10 -0800 Subject: [PATCH 59/73] Move _extendedAttrs field to LogicalBufferLine. --- src/common/buffer/BufferLine.ts | 24 ++++++++++++++---------- src/common/buffer/CellData.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index f38a7fad79..876e3cfa21 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -724,8 +724,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // 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; @@ -788,8 +786,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public showData(startColumn = 0, endColumn = Infinity): string { let s = ''; let curColumn = 0; + const data = this.data(); + const lline = this.logicalLine(); for (let i = 0; i < this.dataLength() && curColumn < endColumn; i++) { - const word = this.data()[i]; + const word = data[i]; const kind = BufferLine.wKind(word); let code: string | number = kind; const wnum = word & 0xfffffff; @@ -836,7 +836,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } else if (kind === DataKind.STYLE_FLAGS) { value = '#' + (wnum & 0xfffffff).toString(16); if (wnum & StyleFlags.HAS_EXTENDED) { - const extended = this._extendedAttrs[i]; + const extended = lline._extendedAttrs[i]; if (! extended) { value += " (missing ext)"; } else { switch (extended.underlineStyle) { @@ -879,7 +879,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { error('default BG only'); } for (let idata = 0; idata < this.dataLength(); idata++) { - const word = this.data()[idata]; + const word = data[idata]; const kind = BufferLine.wKind(word); switch (kind) { case DataKind.FG: @@ -887,7 +887,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { break; case DataKind.STYLE_FLAGS: if ((word & StyleFlags.HAS_EXTENDED) != 0 - && ! this._extendedAttrs[idata]) { + && ! this.logicalLine()._extendedAttrs[idata]) { error("missed ExtendedAttributes") } break; @@ -1065,7 +1065,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const word = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; cursor.setStyleFlags(word); if ((word & StyleFlags.HAS_EXTENDED) !== 0) { - cursor.extended = this._extendedAttrs[styleFlagsIndex]!; + cursor.extended = this.logicalLine()._extendedAttrs[styleFlagsIndex]!; } if (content & Content.IS_COMBINED_MASK) { // FIXME do this lazily, in CellData.getChars @@ -1155,6 +1155,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const styleFlagsIndex = this._cachedStyleFlagsIndex(); const oldStyle = styleFlagsIndex < 0 ? 0 : (this.data()[styleFlagsIndex] & 0xfffffff); let data = this.data(); + const extendedAttrs = this.logicalLine()._extendedAttrs; const idata0 = idata; let dataLength = this.dataLength(); for (; idata < dataLength; idata++) { @@ -1180,7 +1181,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } let needFg = newFg !== oldFg; let needBg = newBg !== oldBg; - let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && this._extendedAttrs[styleFlagsIndex]; + let oldExt = (oldStyle & StyleFlags.HAS_EXTENDED) && extendedAttrs[styleFlagsIndex]; let newExt = (newStyle & StyleFlags.HAS_EXTENDED) && attrs.extended; let needStyle = newStyle !== oldStyle || oldExt !== newExt; const add1 = extendToEnd ? 1 : 2; @@ -1197,7 +1198,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (needStyle) { if (newStyle & StyleFlags.HAS_EXTENDED) - {this._extendedAttrs[idata] = attrs.extended;} + {extendedAttrs[idata] = attrs.extended;} this._cacheSetStyleFlagsIndex(idata); data[idata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, newStyle); } @@ -1209,7 +1210,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (needStyle) { if ((oldStyle & StyleFlags.HAS_EXTENDED) !== 0 && oldExt) - {this._extendedAttrs[xdata] = oldExt;} + {extendedAttrs[xdata] = oldExt;} data[xdata++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, oldStyle); } if (needBg) { @@ -1605,6 +1606,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { //logicalWidth: number = 0; // FIXME needs work updating this public reflowNeeded: boolean = false; + // Key is index in _data array that has STYLE_FLAGS kind with HAS_EXTENDED. + _extendedAttrs: IExtendedAttrs[] = []; + // Maybe move these to to Buffer? Would save space. but with API complications. _cache1: number = 0; _cache2: number = 0; diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 8a6144a7e2..d34a20e130 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -29,7 +29,7 @@ export class CellData extends AttributeData implements ICellData { public content = 0; public fg = 0; public bg = 0; - public extended: IExtendedAttrs = new ExtendedAttrs(); + public combinedData = ''; public copyFrom(src: CellData): void { From 0766db45e41716f6f28fb0e47ff34efc5a8ba753 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 27 Jun 2025 14:16:00 -0700 Subject: [PATCH 60/73] Fixes in BufferLine based on tests. --- src/common/buffer/BufferLine.ts | 316 +++++++++++++------------------- 1 file changed, 131 insertions(+), 185 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 876e3cfa21..1304da5fa4 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -204,9 +204,15 @@ export abstract class BufferLine extends AbstractBufferLine implements IBufferLi 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; } + /* Return 1 or 2 assuming wKindIsText(kind). */ + public static wTextWidth(kind: DataKind): number { return (kind & 1) + 1; } /** From a Uint23 in _data, extract length of string within _text. * Only for SKIP_COLUMNS. */ public static wSkipCount(word: number): number { return word & 0xfffff; } + /** Number of following CLUSTER_CONTINUED words. + * Valid if wKindIsText(wKind(word)). Zero if CHAR_W1 or CHAR_W2. + */ + public static wContinuedCount(word: number): number { return (word >> 21) & 0x3F} public static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } @@ -916,7 +922,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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); + return idata + 1 + BufferLine.wContinuedCount(this.data()[idata]); } public insertCells(pos: number, n: number, fillCellData: ICellData): void { @@ -927,10 +933,10 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } if (pos + n < width) { const endpos = width - n; - this.moveToColumn(endpos); + this.moveToColumn(endpos, 2); const idata = this._cachedDataIndex(); const colOffset = this._cachedColumn(); - this.logicalLine().deleteCellsOnly(idata, this.logicalStartColumn() + endpos - colOffset, n); + this.logicalLine().deleteCellsOnly(idata, n); } else { n = width - pos; } @@ -944,10 +950,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { /** Move to column 'index', which is a RowColumn. * Return encoded 'content'. + * @param index Goal column. + * @param splitIfNeeded. As in moveToLineColumn. */ - public moveToColumn(index: RowColumn): number { + public moveToColumn(index: RowColumn, splitIfNeeded: number = 0): number { const endColumn = this.nextRowSameLine ? this.nextRowSameLine.logicalStartColumn() : Infinity; - return this.moveToLineColumn(index + this.logicalStartColumn(), endColumn, false); + return this.moveToLineColumn(index + this.logicalStartColumn(), splitIfNeeded, endColumn); } /** Move to column 'index', which is a LineColumn. @@ -958,8 +966,15 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * the return value specifices the code value 0 and width 1. * If index would take us past _dataLength: Set the position to _dataLength; * the return value specifices the code value 0 and width 1. + * @param index The goal, as a LineColumn. + * @param splitIfNeeded. If splitIfNeeded > 0 and the goal is in the middle + * of a double-wide character, replace letter by two SKIP-COLUMNS entries. + * If splitIfNeeded == 2 and goal is in middle of SKIP_COLUMNS or + * after end of the row, split or add a SKIP_COLUMNS entry. + * @param endColumn Don't move past this LineColumn. + * Used mainly to limit movement to the current row. */ - public moveToLineColumn(index: LineColumn, endColumn = Infinity, stopEarly: boolean = false): number { + public moveToLineColumn(index: LineColumn, splitIfNeeded: number = 0, endColumn = Infinity): number { let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better @@ -974,11 +989,16 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let word; let kind; let content = 0; - while (stopEarly ? todo > 0 : todo >= 0) { - if (idata >= this.dataLength()) { + while (todo >= 0) { + if (idata >= this.dataLength() || curColumn >= endColumn) { word = NULL_DATA_WORD; kind = DataKind.SKIP_COLUMNS; content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; + if (splitIfNeeded > 1 && todo > 0) { + this.addEmptyDataElements(idata, 1); + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, todo); + curColumn += todo; + } break; } let nextColumn = curColumn; @@ -1001,48 +1021,45 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.SKIP_COLUMNS: w = BufferLine.wSkipCount(word); nextColumn = curColumn + w; - if (todo >= w && nextColumn <= endColumn) { + if (todo >= w) { todo -= w; idata++; curColumn += w; } else { + if (splitIfNeeded > 1 && todo < w) { + this.addEmptyDataElements(idata, 1); + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, todo); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, w - todo); + curColumn += todo; + } content = (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE; todo = -1; } break; + case DataKind.CHAR_W1: + case DataKind.CHAR_W2: case DataKind.CLUSTER_START_W1: case DataKind.CLUSTER_START_W2: - w = kind + 1 - DataKind.CLUSTER_START_W1; + w = BufferLine.wTextWidth(kind); nextColumn = curColumn + w; - if (todo >= w && nextColumn <= endColumn) { - const clEnd = this.clusterEnd(idata); + const clEnd = idata + 1 + BufferLine.wContinuedCount(word); + if (todo >= w) { todo -= w; curColumn = nextColumn; idata = clEnd; } else { - content = nextColumn > endColumn - ? (NULL_CELL_WIDTH << Content.WIDTH_SHIFT) | NULL_CELL_CODE - : index !== curColumn ? 0 + if (splitIfNeeded > 0 && index !== curColumn) { + this.addEmptyDataElements(idata, 2 - (clEnd - idata)); + this.data()[idata++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, 1); + curColumn++; + } + content = index !== curColumn ? 0 + : kind <= DataKind.CHAR_W2 ? (w << Content.WIDTH_SHIFT) | (word & 0x1fffff) : (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); @@ -1076,76 +1093,19 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { return cell; } - public deleteCells(pos: number, n: number, fillCellData: ICellData): void { - this.moveToColumn(pos); + public deleteCells(pos: RowColumn, n: number, fillCellData: ICellData): void { + this.moveToColumn(pos, 2); const idata = this._cachedDataIndex(); const curColumn = this._cachedColumn(); - this.logicalLine().deleteCellsOnly(idata, pos - curColumn, n); + this.logicalLine().deleteCellsOnly(idata, n); } - /** Split data element if index is in middle of wide character or SKIP_COLUMNS or after end. */ - 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; - } + /** Insert attributes as necesssary into data array. + * Cached position will be adjusted to at index but with specified attributes. + */ protected preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { - let idata = this._splitIfNeeded(index); + this.moveToLineColumn(index, 2); + let idata = this._cachedDataIndex(); // set attributes const newFg = attrs.getFg(); const newBg = attrs.getBg(); @@ -1158,6 +1118,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const extendedAttrs = this.logicalLine()._extendedAttrs; const idata0 = idata; let dataLength = this.dataLength(); + // Optimization - if followed by fg or bg elements matching attrs, just adjust data index. for (; idata < dataLength; idata++) { const word = data[idata]; let done = true; @@ -1289,7 +1250,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const lastChar = idata; inputHandler.precedingJoinState = precedingJoinState; if (! insertMode && idata < this.dataLength()) { - this.logicalLine().deleteCellsOnly(inext, 0, curColumn - startColumn); + this.logicalLine().deleteCellsOnly(inext, curColumn - startColumn); } curColumn -= lstart; if (curColumn > this.length && ! wraparoundMode) { @@ -1307,12 +1268,11 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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 colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); - lline.deleteCellsOnly(idata, colOffset, count); + lline.deleteCellsOnly(idata, count); this.preInsert(start, attrs, end === Infinity); idata = this._cachedDataIndex(); const data = this.data(); @@ -1371,7 +1331,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } this._cacheSetColumnDataIndex(cellColumn, idata); if (idata < this.dataLength()) { - this.logicalLine().deleteCellsOnly(inext, 0, width); + this.logicalLine().deleteCellsOnly(inext, width); } } } else { @@ -1413,7 +1373,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.data()[inext++] = BufferLine.wSet1(kind, codePoint); this._cacheSetColumnDataIndex(cellColumn, idata); if (idata < this.dataLength()) { - this.logicalLine().deleteCellsOnly(inext, 0, width); + this.logicalLine().deleteCellsOnly(inext, width); } } @@ -1538,9 +1498,9 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { break; case DataKind.CLUSTER_START_W1: case DataKind.CLUSTER_START_W2: - const clEnd = this.clusterEnd(idata); - wcols = w * (clEnd - idata); - idata = clEnd - 1; + const ncontinued = BufferLine.wContinuedCount(word); + wcols = w * (ncontinued + 1); + idata += ncontinued; break; case DataKind.CHAR_W1: case DataKind.CHAR_W2: @@ -1694,41 +1654,44 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { // 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 { + /** Internal - delete n columns, with no adjust at end of line. + * idata0 - index in _data array of start of deletion. + * n - number of columns to delete. + */ + deleteCellsOnly(idata0: 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 { + // Move start backwards before style entries. + // The goal is to remove no-longer needed style entries. + 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; } } + let skipNeeded = 0; for (; todo > 0 && idata < this.dataLength(); idata++) { const word = data[idata]; const kind = BufferLine.wKind(word); @@ -1748,34 +1711,29 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { break; case DataKind.SKIP_COLUMNS: const wlen = BufferLine.wSkipCount(word); - if (colOffset === 0 && wlen <= todo) { + if (wlen <= todo) { // FIXME dskipLast = idata; todo -= wlen; } else { - const delta = Math.min(todo, wlen - colOffset); + const delta = Math.min(todo, wlen); 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; - } + dskipLast = idata; + todo -= 1 << w; + if (todo < 0) { skipNeeded = -todo; } 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; + idata = clEnd; + dskipLast = idata; + todo -= (1 << w); break; } } @@ -1793,6 +1751,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } this.data()[idata0++] = BufferLine.wSet1(DataKind.STYLE_FLAGS, styleValue); } + if (skipNeeded > 0) { + this.data()[idata0++] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, skipNeeded); + } if (dskipLast >= 0) { const dcount = dskipLast + 1 - idata0; this.addEmptyDataElements(idata0, - dcount); @@ -1841,39 +1802,23 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } let s = ''; let col = 0; + // assuming skipReplace is ' ' or ''. + const skipReplaceLength = skipReplace.length; + let pendingSkips = 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 emitPendingSkips(): void { + if (pendingSkips > 0) { + s += skipReplace.repeat(pendingSkips); + if (outColumns) { + for (let i = 0; i < pendingSkips; ++i) { + outColumns.push(col + i * skipReplaceLength); + } } + pendingSkips = 0; } } - 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); @@ -1894,37 +1839,37 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (col + wlen > endCol) { wlen = endCol - col; } - addPendingSkip(wlen); + pendingSkips += 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 < endCol) { - //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; + case DataKind.CLUSTER_START_W1: + case DataKind.CLUSTER_START_W2: + const wcols = BufferLine.wTextWidth(kind); + const ncontinued = BufferLine.wContinuedCount(word); if (col >= startCol && col < endCol) { - //if (col >= startCol && col + wcols <= endCol) { - addPendingString(idata, 1); + emitPendingSkips(); + const t = utf32ToString(data, idata, idata + ncontinued + 1); + if (outColumns) { + for (let i = t.length; --i >= 0; ) { outColumns.push(col); } + } + s += t; } + idata += ncontinued; col += wcols; break; } } if (col < startCol) { col = startCol; } if (! trimRight && col < endCol && endCol !== Infinity) { - addPendingSkip(endCol - col); + pendingSkips += endCol - col; + } + if (outColumns) { + outColumns.push(col); } - pendingForce(); + if (! trimRight) { emitPendingSkips(); } return s; } @@ -2009,7 +1954,8 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { const oldStartColumn = this.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); - const startIndex = oldLine._splitIfNeeded(oldStartColumn); + oldLine.moveToColumn(oldStartColumn, 1); + const startIndex = oldLine._cachedDataIndex(); const cell = new CellData(); this.loadCell(oldStartColumn, cell); const newRow = new LogicalBufferLine(this.length, cell, this, startIndex); From ceee12a51a395ce4eb31af6f6cd493a6e2575102 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 4 Aug 2025 14:10:38 -0700 Subject: [PATCH 61/73] More fixing testsuite errors. --- src/browser/services/SelectionService.test.ts | 2 +- src/common/Types.ts | 2 +- src/common/buffer/BufferLine.test.ts | 2 +- src/common/buffer/BufferLine.ts | 157 ++++++++++++------ src/common/buffer/CellData.ts | 4 +- 5 files changed, 113 insertions(+), 54 deletions(-) diff --git a/src/browser/services/SelectionService.test.ts b/src/browser/services/SelectionService.test.ts index e6ea6ed008..80bc1d6762 100644 --- a/src/browser/services/SelectionService.test.ts +++ b/src/browser/services/SelectionService.test.ts @@ -190,7 +190,7 @@ describe('SelectionService', () => { selectionService.selectWordAt([15, 0]); assert.equal(selectionService.selectionText, 'ij"'); }); - it('should expand upwards or downards for wrapped lines', () => { + it('should expand upwards or downwards for wrapped lines', () => { buffer.lines.set(0, stringToRow(' foo')); buffer.lines.set(1, stringToRow('bar ')); buffer.setWrapped(1, true); diff --git a/src/common/Types.ts b/src/common/Types.ts index 6afdcfbc40..ac4d327f53 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -215,7 +215,7 @@ export interface IAttributeData { /** Cell data */ export interface ICellData extends IAttributeData { content: number; - combinedData: string; // FIXME only if using OldBufferLine + combinedData: string; isCombined(): number; getWidth(): number; getChars(): string; diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 549bc540ed..bd5ed543d4 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -477,7 +477,7 @@ describe('BufferLine', function(): void { }); it('should add char to combining string in cell', () => { 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()); + const cell = line.loadCell(0, new CellData()); cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]); line.setCell(0, cell); line.addCodepointToCell(0, '\u0301'.charCodeAt(0), 0); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 1304da5fa4..f929006762 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -812,7 +812,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { case DataKind.CHAR_W2: code = 'C2'; nextColumn += 2; break; } - if (startColumn < nextColumn) { + if (startColumn <= nextColumn) { if (s) { s += ', '; } @@ -940,12 +940,34 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } 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); + this.insertCellsOnly(pos, n, fillCellData); + } + + private insertCellsOnly(pos: RowColumn, n: number, fillCellData: ICellData): void { + if (!(fillCellData.content & Content.IS_COMBINED_MASK) + && fillCellData.getWidth() === 1) { + // Optimization + this.preInsert(this.logicalStartColumn() + pos, fillCellData); + const idata = this._cachedDataIndex(); + const code = fillCellData.getCode(); + if (code === NULL_CELL_CODE) { + this.addEmptyDataElements(idata, 1); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); + } else { + this.addEmptyDataElements(idata, n); + for (let i = 0; i < n; ++i) { + this.data()[idata+i] = BufferLine.wSet1(DataKind.CHAR_W1, code); + } + } + } else { + this.moveToColumn(this.logicalStartColumn() + pos); + const idata = this._cachedDataIndex(); + this.addEmptyDataElements(idata, 1); + this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); + for (let i = 0; i < n; ++i) { + this.setCell(pos + i, fillCellData); + } + } } /** Move to column 'index', which is a RowColumn. @@ -969,12 +991,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { * @param index The goal, as a LineColumn. * @param splitIfNeeded. If splitIfNeeded > 0 and the goal is in the middle * of a double-wide character, replace letter by two SKIP-COLUMNS entries. - * If splitIfNeeded == 2 and goal is in middle of SKIP_COLUMNS or + * If splitIfNeeded >= 2 and goal is in middle of SKIP_COLUMNS or * after end of the row, split or add a SKIP_COLUMNS entry. + * If splitIfNeeded >= 3, stop early, before style words. * @param endColumn Don't move past this LineColumn. * Used mainly to limit movement to the current row. */ public moveToLineColumn(index: LineColumn, splitIfNeeded: number = 0, endColumn = Infinity): number { + const stopEarly = splitIfNeeded >= 3; let curColumn = this._cachedColumn(); if (index < curColumn) { // FIXME can sometimes do better @@ -989,7 +1013,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let word; let kind; let content = 0; - while (todo >= 0) { + while (stopEarly ? todo > 0 : todo >= 0) { if (idata >= this.dataLength() || curColumn >= endColumn) { word = NULL_DATA_WORD; kind = DataKind.SKIP_COLUMNS; @@ -1098,13 +1122,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const idata = this._cachedDataIndex(); const curColumn = this._cachedColumn(); this.logicalLine().deleteCellsOnly(idata, n); + this.insertCellsOnly(this.length - n, n, fillCellData); // FIXME logical } /** Insert attributes as necesssary into data array. * Cached position will be adjusted to at index but with specified attributes. */ protected preInsert(index: LineColumn, attrs: IAttributeData, extendToEnd: boolean = false): boolean { - this.moveToLineColumn(index, 2); + this.moveToLineColumn(index, 3); let idata = this._cachedDataIndex(); // set attributes const newFg = attrs.getFg(); @@ -1113,7 +1138,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { let oldFg = this._cachedFg(); let oldBg = this._cachedBg(); const styleFlagsIndex = this._cachedStyleFlagsIndex(); - const oldStyle = styleFlagsIndex < 0 ? 0 : (this.data()[styleFlagsIndex] & 0xfffffff); + let oldStyle = styleFlagsIndex < 0 ? 0 : (this.data()[styleFlagsIndex] & 0xfffffff); let data = this.data(); const extendedAttrs = this.logicalLine()._extendedAttrs; const idata0 = idata; @@ -1129,12 +1154,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { done = false; } break; - case DataKind.FG: - if ((word & 0x3ffffff) === newFg) { - oldFg = newFg; - done = false; - } - break; + case DataKind.FG: + if ((word & 0x3ffffff) === newFg) { + oldFg = newFg; + done = false; + } + break; + // FIXME StyleFlags } if (done) { break; @@ -1147,8 +1173,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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); + if (add) { + add = (needBg?add1:0) + (needFg?add1:0) + (needStyle?add1:0); this.addEmptyDataElements(idata, add - (idata0 - idata)); data = this.data(); if (needFg) { @@ -1265,12 +1291,12 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public eraseCells(start: RowColumn, end: RowColumn, attrs: IAttributeData): void { const startColumn = this.logicalStartColumn(); - if (end === Infinity && this.nextRowSameLine) { end = this.length; } + end = Math.min(end, this.length); const count = end - start; start += startColumn; end += startColumn; + this.moveToLineColumn(start, 2); let idata = this._cachedDataIndex(); - //const colOffset = start - this._cachedColumn(); const lline = this.logicalLine(); lline.deleteCellsOnly(idata, count); this.preInsert(start, attrs, end === Infinity); @@ -1386,19 +1412,30 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { // 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); + const cell = this.loadCell(index, new CellData()); + let content = cell.content; + if (cell.content & Content.IS_COMBINED_MASK) { + cell.combinedData += stringFromCodePoint(codePoint); + } else { + if (content & Content.CODEPOINT_MASK) { + // normal case for combining chars: + // - move current leading char + new one into combined string + // - set combined flag + cell.combinedData = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); + content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 + content |= Content.IS_COMBINED_MASK; + } else { + // should not happen - we actually have no data in the cell yet + // simply set the data in the cell buffer with a width of 1 + content = codePoint | (1 << Content.WIDTH_SHIFT); + } + } + if (width) { + content &= ~Content.WIDTH_MASK; + content |= width << Content.WIDTH_SHIFT; + } + cell.content = content; + this.setCell(index, cell); } /** @@ -1475,12 +1512,13 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public getTrimmedLength(countBackground: boolean = false, logical: boolean = false): number { let cols = 0; let skipped = 0; - const startColumn = logical ? 0 : this.logicalStartColumn(); // FIXME startIndex + const startIndex = !logical && this instanceof WrappedBufferLine + ? this.startIndex : 0; const data = this.data(); const end = this.nextRowSameLine && ! logical ? this.nextRowSameLine.startIndex : this.dataLength(); let bg = this._cachedBg(); let bgCol = 0; - for (let idata = startColumn; idata < end; idata++) { + for (let idata = startIndex; 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; @@ -1495,12 +1533,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { break; case DataKind.SKIP_COLUMNS: skipped += BufferLine.wSkipCount(word); + if (idata === startIndex && this instanceof WrappedBufferLine) { + skipped -= this.startColumn = this.startIndexColumn; + } break; case DataKind.CLUSTER_START_W1: case DataKind.CLUSTER_START_W2: - const ncontinued = BufferLine.wContinuedCount(word); - wcols = w * (ncontinued + 1); - idata += ncontinued; + idata += BufferLine.wContinuedCount(word); + wcols = w; break; case DataKind.CHAR_W1: case DataKind.CHAR_W2: @@ -1543,10 +1583,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = -1, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { const lineStart = this.logicalStartColumn(); - const logicalEndColumn = endCol >= 0 ? lineStart + endCol + let logicalEndColumn = endCol >= 0 ? lineStart + endCol : this.nextRowSameLine ? this.nextRowSameLine.logicalStartColumn() : lineStart + this.length; - let s = this.logicalLine().translateLogicalToString(trimRight, lineStart + startCol, logicalEndColumn, outColumns, skipReplace); + if (trimRight) { + logicalEndColumn = Math.min(logicalEndColumn, lineStart + this.getTrimmedLength()); + } + + let s = this.logicalLine().translateLogicalToString(false, lineStart + startCol, logicalEndColumn, outColumns, skipReplace); if (!trimRight && endCol < 0) { s += ' '.repeat(this.length - logicalEndColumn + lineStart)} if (outColumns && lineStart !== 0) { for (let i = outColumns.length; --i >= 0; ) { @@ -1669,7 +1713,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { let extended = undefined; // cursor.getStyleFlags(); // FIXME handle extendedattrs // Move start backwards before style entries. - // The goal is to remove no-longer needed style entries. + // The goal is to remove no-longer needed style entries. while (idata > 0) { let skipItem = true; const word = data[idata-1]; @@ -1812,8 +1856,9 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (pendingSkips > 0) { s += skipReplace.repeat(pendingSkips); if (outColumns) { + const col0 = col - pendingSkips; for (let i = 0; i < pendingSkips; ++i) { - outColumns.push(col + i * skipReplaceLength); + outColumns.push(col0 + i * skipReplaceLength); } } pendingSkips = 0; @@ -1865,11 +1910,15 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { if (col < startCol) { col = startCol; } if (! trimRight && col < endCol && endCol !== Infinity) { pendingSkips += endCol - col; + col = endCol; + } + if (! trimRight) { + //col += pendingSkips; + emitPendingSkips(); } if (outColumns) { outColumns.push(col); } - if (! trimRight) { emitPendingSkips(); } return s; } @@ -1881,6 +1930,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { 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. @@ -1890,17 +1940,24 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { * 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 mitigate the performance cost we could support backwards movement by moveToColumn. - // Changing Data>FG etc to use xor-encoding would help. TODO. - /** Index in data array containing first column. */ + + /** Index in data array containing first column. + * If _data[startIndex] is a SKIP_COLUMNS, some columns might be the + * end of the previous row, and some might be the current row. + * In that case, startColumn-startIndexColumn is the number of columns + * in the previous row. + */ startIndex: number = 0; + /** Column number corresponding to startIndex. * Usually the same as startColumn, but may be less if startIndex refers to a SKIP_COLUMNS. */ startIndexColumn: number = 0; + + // startIndex, startFg, startBg, startStyle are used by _cacheReset + // to optimize moveToColumn on same row. It might be best to get rid of them; + // to mitigate the performance cost we could support backwards movement by moveToColumn. + // Changing Data>FG etc to use xor-encoding would help. TODO. startFg: number = 0; startBg: number = 0; startStyle: number = -1; diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index d34a20e130..7cb987f4f2 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -25,7 +25,9 @@ export class CellData extends AttributeData implements ICellData { return obj; } - /** Primitives from terminal buffer. */ + /** Primitives from terminal buffer. + * @deprecated + */ public content = 0; public fg = 0; public bg = 0; From fd24aa945eb061e2cda218b749036d6fdf629efa Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 4 Aug 2025 16:35:08 -0700 Subject: [PATCH 62/73] Use CellData.fromChar for some test cases. Fix test cases to use width 1 raher than width 0 for normal 1-column characters. --- src/common/buffer/BufferLine.test.ts | 103 ++++++++++++++------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index bd5ed543d4..79a1a062e4 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -173,71 +173,72 @@ describe('BufferLine', function(): void { }); it('insertCells', function(): void { 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)])); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.insertCells(1, 3, CellData.fromChar('d', 1, 4)); assert.deepEqual(lineToArray(line), [ - [1, 'a', 0, 'a'.charCodeAt(0)], - [4, 'd', 0, 'd'.charCodeAt(0)], - [4, 'd', 0, 'd'.charCodeAt(0)] + [1, 'a', 1, 'a'.charCodeAt(0)], + [4, 'd', 1, 'd'.charCodeAt(0)], + [4, 'd', 1, 'd'.charCodeAt(0)] ]); + (line as any).xyz = 0; }); it('deleteCells', function(): void { 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)])); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.setCell(3, CellData.fromChar('d', 1, 4)); + line.setCell(4, CellData.fromChar('e', 1, 5)); + line.deleteCells(1, 2, CellData.fromChar('f', 1, 6)); assert.deepEqual(lineToArray(line), [ - [1, 'a', 0, 'a'.charCodeAt(0)], - [4, 'd', 0, 'd'.charCodeAt(0)], - [5, 'e', 0, 'e'.charCodeAt(0)], - [6, 'f', 0, 'f'.charCodeAt(0)], - [6, 'f', 0, 'f'.charCodeAt(0)] + [1, 'a', 1, 'a'.charCodeAt(0)], + [4, 'd', 1, 'd'.charCodeAt(0)], + [5, 'e', 1, 'e'.charCodeAt(0)], + [6, 'f', 1, 'f'.charCodeAt(0)], + [6, 'f', 1, 'f'.charCodeAt(0)] ]); }); it('replaceCells', function(): void { 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)])); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.setCell(3, CellData.fromChar('d', 1, 4)); + line.setCell(4, CellData.fromChar('e', 1, 5)); + line.replaceCells(2, 4, CellData.fromChar('f', 1, 6)); assert.deepEqual(lineToArray(line), [ - [1, 'a', 0, 'a'.charCodeAt(0)], - [2, 'b', 0, 'b'.charCodeAt(0)], - [6, 'f', 0, 'f'.charCodeAt(0)], - [6, 'f', 0, 'f'.charCodeAt(0)], - [5, 'e', 0, 'e'.charCodeAt(0)] + [1, 'a', 1, 'a'.charCodeAt(0)], + [2, 'b', 1, 'b'.charCodeAt(0)], + [6, 'f', 1, 'f'.charCodeAt(0)], + [6, 'f', 1, 'f'.charCodeAt(0)], + [5, 'e', 1, 'e'.charCodeAt(0)] ]); }); it('fill', function(): void { 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)])); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.setCell(3, CellData.fromChar('d', 1, 4)); + line.setCell(4, CellData.fromChar('e', 1, 5)); + line.fill(CellData.fromChar('z', 1, 123)); assert.deepEqual(lineToArray(line), [ - [123, 'z', 0, 'z'.charCodeAt(0)], - [123, 'z', 0, 'z'.charCodeAt(0)], - [123, 'z', 0, 'z'.charCodeAt(0)], - [123, 'z', 0, 'z'.charCodeAt(0)], - [123, 'z', 0, 'z'.charCodeAt(0)] + [123, 'z', 1, 'z'.charCodeAt(0)], + [123, 'z', 1, 'z'.charCodeAt(0)], + [123, 'z', 1, 'z'.charCodeAt(0)], + [123, 'z', 1, 'z'.charCodeAt(0)], + [123, 'z', 1, 'z'.charCodeAt(0)] ]); }); it('clone', function(): void { 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)])); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.setCell(3, CellData.fromChar('d', 1, 4)); + line.setCell(4, CellData.fromChar('e', 1, 5)); const line2 = line.clone(); assert.deepEqual(lineToArray(line2), lineToArray(line)); assert.equal(line2.length, line.length); @@ -245,12 +246,12 @@ describe('BufferLine', function(): void { }); it('copyFrom', function(): void { 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 = BufferLine.make(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true); + line.setCell(0, CellData.fromChar('a', 1, 1)); + line.setCell(1, CellData.fromChar('b', 1, 2)); + line.setCell(2, CellData.fromChar('c', 1, 3)); + line.setCell(3, CellData.fromChar('d', 1, 4)); + line.setCell(4, CellData.fromChar('e', 1, 5)); + const line2 = BufferLine.make(5, CellData.fromChar('a', 1, 1), true); line2.copyFrom(line); assert.deepEqual(lineToArray(line2), lineToArray(line)); assert.equal(line2.length, line.length); From 3756541d7347d3ca1f5dcda7836348fe4e1e1b50 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 4 Aug 2025 16:36:23 -0700 Subject: [PATCH 63/73] Implement LogicalBufferLine.clone. --- src/common/buffer/BufferLine.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index f929006762..8b2323591b 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -1101,6 +1101,8 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { const content = this.moveToColumn(index); cursor.content = content; cursor.setFg(this._cachedFg()); + cursor.content = content; + cursor.setFg(this._cachedFg()); cursor.setBg(this._cachedBg()); const styleFlagsIndex = this._cachedStyleFlagsIndex(); const word = styleFlagsIndex < 0 ? 0 : this.data()[styleFlagsIndex]; @@ -1646,6 +1648,20 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } + /** create a new clone */ + public clone(): IBufferLine { + const newLine = new LogicalBufferLine(0); + newLine._data = new Uint32Array(this._data); + newLine._dataLength = this._dataLength; + newLine.length = this.length; + for (const el in this._extendedAttrs) { + newLine._extendedAttrs[el] = this._extendedAttrs[el]; + } + newLine._isWrapped = this.isWrapped; + return newLine; + } + + // count can be negative addEmptyDataElements(position: number, count: number): void { const oldDataLength = this._dataLength; From c4944b841aeaaa21a0d229cc444f694e30ea55a3 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 6 Aug 2025 08:03:22 -0700 Subject: [PATCH 64/73] On a 'scroll' trim old line's _data buffer to actual needed size. Re-use old _data buffer for new line. --- src/common/buffer/BufferLine.ts | 26 +++++++++++++++++++++----- src/common/services/BufferService.ts | 8 +++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 8b2323591b..45abd8c455 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -1621,18 +1621,18 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { _cache3: number = 0; _cache4: number = -1; - constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine, startIndex?: number) { + constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine, startIndex: number = 0, data: Uint32Array = new Uint32Array(cols)) { 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; + const oldStart = startIndex; this._data = lline._data.slice(oldStart); this._dataLength = lline._dataLength - oldStart; this._extendedAttrs = lline._extendedAttrs.slice(oldStart); } else { - this._data = new Uint32Array(cols); + this._data = data; this._dataLength = 0; } this.length = cols; @@ -1648,6 +1648,23 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } + /** Creates a new LogicalBufferLine but reuses old _data buffer. + * The oldLine_data buffer is resized to _dataLength, + * while the old _data buffer is reused for the new line. + */ + public static makeAndTrim(cols: number, fillCellData?: IAttributeData, oldLine?: LogicalBufferLine): LogicalBufferLine { + if (oldLine && oldLine._data.length > oldLine._dataLength) { + const oldData = oldLine._data; + oldLine._data = oldData.slice(0, oldLine._dataLength); + const newLine = new LogicalBufferLine(cols, undefined, undefined, 0, oldData); + newLine._data = oldData; + if (fillCellData) { newLine.preInsert(0, fillCellData); } + return newLine; + } else { + return new LogicalBufferLine(cols, fillCellData); + } + } + /** create a new clone */ public clone(): IBufferLine { const newLine = new LogicalBufferLine(0); @@ -1661,7 +1678,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { return newLine; } - // count can be negative addEmptyDataElements(position: number, count: number): void { const oldDataLength = this._dataLength; @@ -1690,7 +1706,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { resizeData(size: number): void { if (size > this.data().length) { // buffer = new ArrayBuffer(buffer.byteLength, { maxByteLength: 6 * size }); - const dataNew = new Uint32Array((3 * size) >> 1); + const dataNew = new Uint32Array((3 * size) >> 1); // FIXME dataNew.set(this._data); this.logicalLine()._data = dataNew; } diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 55f037a4bd..daa21b9408 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -70,7 +70,13 @@ export class BufferService extends Disposable implements IBufferService { const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as NewBufferLine; newLine = new WrappedBufferLine(oldLine); } else { - newLine = new LogicalBufferLine(this.cols, eraseAttr); + const bottom = buffer.lines.get(bottomRow); + if (bottom instanceof NewBufferLine) { + // trim bottoms _data buffer, and reuse for newLine. + newLine = LogicalBufferLine.makeAndTrim(this.cols, eraseAttr, bottom.logicalLine()); + } else { + newLine = new LogicalBufferLine(this.cols, eraseAttr); + } } } else { newLine = this._cachedBlankLine; From b165a2384379346c1fb1ca65d6d12aad58be1fdf Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 6 Aug 2025 16:17:55 -0700 Subject: [PATCH 65/73] Remove support for OldBufferLine. No dynamic switching bteween old and new BufferLine implementation. --- src/common/InputHandler.ts | 169 +------ src/common/Types.ts | 5 +- src/common/buffer/Buffer.ts | 337 ++----------- src/common/buffer/BufferLine.ts | 675 ++------------------------ src/common/buffer/BufferReflow.ts | 155 ------ src/common/services/BufferService.ts | 34 +- src/common/services/OptionsService.ts | 5 - 7 files changed, 95 insertions(+), 1285 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 9b079c5389..e638570808 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -10,7 +10,7 @@ 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 { usingNewBufferLine, BufferLine, OldBufferLine, NewBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { BufferLine, 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'; @@ -510,25 +510,17 @@ 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 { + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; 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); + let col = (bufferRow as BufferLine).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; + const oldRow = bufferRow as BufferLine; // this._activeBuffer.x = oldWidth; const buffer = this._activeBuffer if (buffer.y === this._activeBuffer.scrollBottom) { @@ -546,7 +538,7 @@ export class InputHandler extends Disposable implements IInputHandler { } 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(); + const prevCols = (bufferRow as BufferLine).logicalStartColumn() - oldRow.logicalStartColumn(); col = col - prevCols; // row changed, get it again /* @@ -570,146 +562,6 @@ export class InputHandler extends Disposable implements IInputHandler { 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; - const screenReaderMode = this._optionsService.rawOptions.screenReaderMode; - const cols = this._bufferService.cols; - const wraparoundMode = this._coreService.decPrivateModes.wraparound; - const insertMode = this._coreService.modes.insertMode; - - this._dirtyRowTracker.markDirty(this._activeBuffer.y); - - // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { - bufferRow.setCellFromCodepoint(this._activeBuffer.x - 1, 0, 1, curAttr); - } - - let precedingJoinState = this._parser.precedingJoinState; - for (let pos = start; pos < end; ++pos) { - code = data[pos]; - - // get charset replacement character - // charset is only defined for ASCII, therefore we only - // search for an replacement char if code < 127 - if (code < 127 && charset) { - const ch = charset[String.fromCharCode(code)]; - if (ch) { - code = ch.charCodeAt(0); - } - } - - const currentInfo = this.unicodeService.charProperties(code, precedingJoinState); - chWidth = UnicodeService.extractWidth(currentInfo); - const shouldJoin = UnicodeService.extractShouldJoin(currentInfo); - const oldWidth = shouldJoin ? UnicodeService.extractWidth(precedingJoinState) : 0; - precedingJoinState = currentInfo; - - if (screenReaderMode) { - this._onA11yChar.fire(stringFromCodePoint(code)); - } - if (this._getCurrentLinkId()) { - this._oscLinkService.addLineToLink(this._getCurrentLinkId(), this._activeBuffer.ybase + this._activeBuffer.y); - } - - // goto next line if ch would overflow - // NOTE: To avoid costly width checks here, - // the terminal does not allow a cols < 2. - if (this._activeBuffer.x + chWidth - oldWidth > cols) { - // autowrap - DECAWM - // automatically wraps to the beginning of the next line - if (wraparoundMode) { - const oldRow = bufferRow; - let oldCol = this._activeBuffer.x - oldWidth; - this._activeBuffer.x = oldWidth; - this._activeBuffer.y++; - if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { - this._activeBuffer.y--; - this._bufferService.scroll(this._eraseAttrData(), true); - } else { - if (this._activeBuffer.y >= this._bufferService.rows) { - this._activeBuffer.y = this._bufferService.rows - 1; - } - // The line already exists (eg. the initial viewport), mark it as a - // wrapped line - 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)!; - 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); - } - } else { - this._activeBuffer.x = cols - 1; - if (chWidth === 2) { - // FIXME: check for xterm behavior - // What to do here? We got a wide char that does not fit into last cell - continue; - } - } - } - - // insert combining char at last cursor position - // this._activeBuffer.x should never be 0 for a combining char - // since they always follow a cell consuming char - // therefore we can test for this._activeBuffer.x to avoid overflow left - if (shouldJoin && this._activeBuffer.x) { - const offset = bufferRow.getWidth(this._activeBuffer.x - 1) ? 1 : 2; - // 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 as OldBufferLine).addCodepointToCell(this._activeBuffer.x - offset, - code, chWidth); - for (let delta = chWidth - oldWidth; --delta >= 0; ) { - bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr); - } - continue; - } - - // insert mode: move characters to right - if (insertMode) { - // right shift cells according to the width - bufferRow.insertCells(this._activeBuffer.x, chWidth - oldWidth, this._activeBuffer.getNullCell(curAttr)); - // test last cell - since the last cell has only room for - // a halfwidth char any fullwidth shifted there is lost - // and will be set to empty cell - if (bufferRow.getWidth(cols - 1) === 2) { - bufferRow.setCellFromCodepoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr); - } - } - - // write current char to buffer and advance cursor - bufferRow.setCellFromCodepoint(this._activeBuffer.x++, code, chWidth, curAttr); - - // fullwidth char - also set next cell to placeholder stub and advance cursor - // for graphemes bigger than fullwidth we can simply loop to zero - // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right - if (chWidth > 0) { - while (--chWidth) { - // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodepoint(this._activeBuffer.x++, 0, 0, curAttr); - } - } - } - - this._parser.precedingJoinState = precedingJoinState; - - // handle wide chars: reset cell to the right if it is second cell of a wide char - if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { - bufferRow.setCellFromCodepoint(this._activeBuffer.x, 0, 1, curAttr); - } - - this._dirtyRowTracker.markDirty(this._activeBuffer.y); - } - /** * Forward registerCsiHandler from parser. */ @@ -1205,12 +1057,12 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void { const row = this._activeBuffer.ybase + y; - const line = this._activeBuffer.lines.get(row)!; + const line = this._activeBuffer.lines.get(row) as BufferLine; const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); if (clearWrap && end === Infinity) { this._activeBuffer.setWrapped(row + 1, false); } - if (! respectProtect && line instanceof NewBufferLine) { + if (! respectProtect) { line.eraseCells(start, end, fill); } else { line.replaceCells(start, end, fill, respectProtect); @@ -1230,18 +1082,13 @@ export class InputHandler extends Disposable implements IInputHandler { const line = buffer.lines.get(row); if (line) { const eraseAttrs = this._eraseAttrData(); - const wasNewBufferLine = line instanceof NewBufferLine; - if (wasNewBufferLine && ! respectProtect) { + if (! 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)); - } } } diff --git a/src/common/Types.ts b/src/common/Types.ts index ac4d327f53..83c457f9e2 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -231,7 +231,6 @@ export interface IBufferLine { length: number; /** 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; @@ -244,9 +243,9 @@ export interface IBufferLine { resize(cols: number, fill: ICellData): boolean; cleanupMemory(): number; fill(fillCellData: ICellData, respectProtect?: boolean): void; - // @deprecated - only if !usingNewBufferLine() + // @deprecated copyFrom(line: IBufferLine): void; - // @deprecated - only if !usingNewBufferLine() + // @deprecated clone(): IBufferLine; getTrimmedLength(): number; getNoBgTrimmedLength(): number; diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index b383d5e3a9..b0dc76d8fb 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -7,8 +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, usingNewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow'; +import { BufferLine, LogicalBufferLine, WrappedBufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; 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'; import { Marker } from 'common/buffer/Marker'; @@ -121,8 +120,8 @@ export class Buffer implements IBuffer { public splitLine(row: number, col: number): void { // FIXME col is unused 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; + const curRow = this.lines.get(this.ybase + row - 1) as BufferLine; + const nextRow = this.lines.get(this.ybase + row) as BufferLine; let startColumn = curRow.logicalStartColumn() + bufferService.cols; // FIXME: nextRow.logicalLine().deleteCellsOnly(bufferService.cols - col); let newRow; @@ -141,15 +140,13 @@ export class Buffer implements IBuffer { 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; + if (value) { + const prevRow = this.lines.get(absrow - 1) as BufferLine; 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 prevRow = this.lines.get(absrow - 1) as BufferLine; const curRow = line as WrappedBufferLine; const newRow = curRow.asUnwrapped(prevRow); this.lines.set(absrow, newRow); @@ -204,7 +201,7 @@ export class Buffer implements IBuffer { this.lines.maxLength = newMaxLength; } - if (this._cols !== newCols && usingNewBufferLine()) { + if (this._cols !== newCols) { const nlines = this.lines.length; for (let i = 0; i < nlines; i++) { const line = this.lines.get(i); @@ -297,75 +294,27 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (usingNewBufferLine()) { - const lazyReflow = false; // FUTURE - change to 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); - // Reduce max length if needed after adjustments, this is done after as it - // would otherwise cut data from the bottom of the buffer. - if (newMaxLength < this.lines.maxLength) { - // Trim from the top of the buffer and adjust ybase and ydisp. - const amountToTrim = this.lines.length - newMaxLength; - if (amountToTrim > 0) { - this.setWrapped(amountToTrim, false); - this.lines.trimStart(amountToTrim); - this.ybase = Math.max(this.ybase - amountToTrim, 0); - this.ydisp = Math.max(this.ydisp - amountToTrim, 0); - this.savedY = Math.max(this.savedY - amountToTrim, 0); - } - this.lines.maxLength = newMaxLength; - } - 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()); - } - } - } - - // DEPRECATED - only if !usingNewBufferLine() - private _memoryCleanupQueue = new IdleTaskQueue(); - private _memoryCleanupPosition = 0; - private _batchedMemoryCleanup(): boolean { - let normalRun = true; - if (this._memoryCleanupPosition >= this.lines.length) { - // cleanup made it once through all lines, thus rescan in loop below to also catch shifted - // lines, which should finish rather quick if there are no more cleanups pending - this._memoryCleanupPosition = 0; - normalRun = false; - } - let counted = 0; - while (this._memoryCleanupPosition < this.lines.length) { - counted += this.lines.get(this._memoryCleanupPosition++)!.cleanupMemory(); - // cleanup max 100 lines per batch - if (counted > 100) { - return true; + const lazyReflow = false; // FUTURE - change to 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); + // Reduce max length if needed after adjustments, this is done after as it + // would otherwise cut data from the bottom of the buffer. + if (newMaxLength < this.lines.maxLength) { + // Trim from the top of the buffer and adjust ybase and ydisp. + const amountToTrim = this.lines.length - newMaxLength; + if (amountToTrim > 0) { + this.setWrapped(amountToTrim, false); + this.lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + this.savedY = Math.max(this.savedY - amountToTrim, 0); } + this.lines.maxLength = newMaxLength; } - // normal runs always need another rescan afterwards - // if we made it here with normalRun=false, we are in a final run - // and can end the cleanup task for sure - return normalRun; + this._fixupPosition(); } private get _isReflowEnabled(): boolean { @@ -376,7 +325,6 @@ 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; @@ -391,7 +339,7 @@ export class Buffer implements IBuffer { } // 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 newRows: BufferLine[] = []; const yDispOld = this.ydisp; const yBaseOld = this.ybase; const yAbsOld = yBaseOld + this.y; @@ -408,10 +356,10 @@ export class Buffer implements IBuffer { endRow = row; break; } - const line = this.lines.get(row) as NewBufferLine; + const line = this.lines.get(row) as BufferLine; newRows.push(line); if (line instanceof LogicalBufferLine && line.reflowNeeded) { - let curRow: NewBufferLine = line; + let curRow: BufferLine = line; let logicalX, logicalSavedX = this.savedX; let oldWrapCount = 0; // number of following wrapped lines @@ -572,231 +520,6 @@ export class Buffer implements IBuffer { this.ydisp = Math.max(0, Math.min(this.ydisp, this.lines.length)); } - // DEPRECATED - only if !usingNewBufferLine() - private _reflow(newCols: number, newRows: number): void { - if (this._cols === newCols) { - return; - } - - // Iterate through rows, ignore the last one as it cannot be wrapped - if (newCols > this._cols) { - this._reflowLarger(newCols, newRows); - } else { - this._reflowSmaller(newCols, newRows); - } - } - - // DEPRECATED - only if !usingNewBufferLine() - private _reflowLarger(newCols: number, newRows: number): void { - const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine; - const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA), reflowCursorLine); - if (toRemove.length > 0) { - const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove); - reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout); - this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved); - } - } - - // 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 - let viewportAdjustments = countRemoved; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - if (this.y > 0) { - this.y--; - } - if (this.lines.length < newRows) { - // Add an extra row at the bottom of the viewport - this.lines.push(BufferLine.make(newCols, nullCell)); - } - } else { - if (this.ydisp === this.ybase) { - this.ydisp--; - } - this.ybase--; - } - } - this.savedY = Math.max(this.savedY - countRemoved, 0); - } - - // DEPRECATED - only if !usingNewBufferLine() - private _reflowSmaller(newCols: number, newRows: number): void { - const reflowCursorLine = this._optionsService.rawOptions.reflowCursorLine; - const nullCell = this.getNullCell(DEFAULT_ATTR_DATA); - // Gather all BufferLines that need to be inserted into the Buffer here so that they can be - // batched up and only committed once - const toInsert = []; - let countToInsert = 0; - // Go backwards as many lines may be trimmed and this will avoid considering them - for (let y = this.lines.length - 1; y >= 0; y--) { - // Check whether this line is a problem - let nextLine = this.lines.get(y) as BufferLine; - if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) { - continue; - } - - // Gather wrapped lines and adjust y to be the starting line - const wrappedLines: BufferLine[] = [nextLine]; - while (nextLine.isWrapped && y > 0) { - nextLine = this.lines.get(--y) as BufferLine; - wrappedLines.unshift(nextLine); - } - - if (!reflowCursorLine) { - // If these lines contain the cursor don't touch them, the program will handle fixing up - // wrapped lines with the cursor - const absoluteY = this.ybase + this.y; - if (absoluteY >= y && absoluteY < y + wrappedLines.length) { - continue; - } - } - - const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength(); - const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols); - const linesToAdd = destLineLengths.length - wrappedLines.length; - let trimmedLines: number; - if (this.ybase === 0 && this.y !== this.lines.length - 1) { - // If the top section of the buffer is not yet filled - trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd); - } else { - trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd); - } - - // Add the new lines - const newLines: BufferLine[] = []; - for (let i = 0; i < linesToAdd; i++) { - const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine; - newLines.push(newLine); - } - if (newLines.length > 0) { - toInsert.push({ - // countToInsert here gets the actual index, taking into account other inserted items. - // using this we can iterate through the list forwards - start: y + wrappedLines.length + countToInsert, - newLines - }); - countToInsert += newLines.length; - } - wrappedLines.push(...newLines); - - // Copy buffer data to new locations, this needs to happen backwards to do in-place - let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols); - let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols; - if (destCol === 0) { - destLineIndex--; - destCol = destLineLengths[destLineIndex]; - } - let srcLineIndex = wrappedLines.length - linesToAdd - 1; - let srcCol = lastLineLength; - while (srcLineIndex >= 0) { - const cellsToCopy = Math.min(srcCol, destCol); - if (wrappedLines[destLineIndex] === undefined) { - // Sanity check that the line exists, this has been known to fail for an unknown reason - // which would stop the reflow from happening if an exception would throw. - break; - } - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true); - destCol -= cellsToCopy; - if (destCol === 0) { - destLineIndex--; - destCol = destLineLengths[destLineIndex]; - } - srcCol -= cellsToCopy; - if (srcCol === 0) { - srcLineIndex--; - const wrappedLinesIndex = Math.max(srcLineIndex, 0); - srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols); - } - } - - // Null out the end of the line ends if a wide character wrapped to the following line - for (let i = 0; i < wrappedLines.length; i++) { - if (destLineLengths[i] < newCols) { - wrappedLines[i].setCell(destLineLengths[i], nullCell); - } - } - - // Adjust viewport as needed - let viewportAdjustments = linesToAdd - trimmedLines; - while (viewportAdjustments-- > 0) { - if (this.ybase === 0) { - if (this.y < newRows - 1) { - this.y++; - this.lines.pop(); - } else { - this.ybase++; - this.ydisp++; - } - } else { - // Ensure ybase does not exceed its maximum value - if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) { - if (this.ybase === this.ydisp) { - this.ydisp++; - } - this.ybase++; - } - } - } - this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1); - } - - // Rearrange lines in the buffer if there are any insertions, this is done at the end rather - // than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many - // costly calls to CircularList.splice. - if (toInsert.length > 0) { - // Record buffer insert events and then play them back backwards so that the indexes are - // correct - const insertEvents: IInsertEvent[] = []; - - // Record original lines so they don't get overridden when we rearrange the list - const originalLines: BufferLine[] = []; - for (let i = 0; i < this.lines.length; i++) { - originalLines.push(this.lines.get(i) as BufferLine); - } - const originalLinesLength = this.lines.length; - - let originalLineIndex = originalLinesLength - 1; - let nextToInsertIndex = 0; - let nextToInsert = toInsert[nextToInsertIndex]; - this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert); - let countInsertedSoFar = 0; - for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) { - if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) { - // Insert extra lines here, adjusting i as needed - for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) { - this.lines.set(i--, nextToInsert.newLines[nextI]); - } - i++; - - // Create insert events for later - insertEvents.push({ - index: originalLineIndex + 1, - amount: nextToInsert.newLines.length - }); - - countInsertedSoFar += nextToInsert.newLines.length; - nextToInsert = toInsert[++nextToInsertIndex]; - } else { - this.lines.set(i, originalLines[originalLineIndex--]); - } - } - - // Update markers - let insertCountEmitted = 0; - for (let i = insertEvents.length - 1; i >= 0; i--) { - insertEvents[i].index += insertCountEmitted; - this.lines.onInsertEmitter.fire(insertEvents[i]); - insertCountEmitted += insertEvents[i].amount; - } - const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength); - if (amountToTrim > 0) { - this.lines.onTrimEmitter.fire(amountToTrim); - } - } - } - /** * Translates a buffer line to a string, with optional start and end columns. * Wide characters will count as two columns in the resulting string. This @@ -955,7 +678,7 @@ export class Buffer implements IBuffer { report('wrapped line points to wrong logical line') } if (! curRow.isWrapped) { report('wrapped should be set'); } - if (prevRow instanceof NewBufferLine) { + if (prevRow instanceof BufferLine) { if (prevRow.nextRowSameLine !== curRow) { report('bad previous nextRowSameLine'); } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 45abd8c455..99e6c7b791 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -31,24 +31,29 @@ let $startIndex = 0; /** Factor when to cleanup underlying array buffer after shrinking. */ const CLEANUP_THRESHOLD = 2; -export abstract class AbstractBufferLine implements IBufferLine { +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; + +export abstract class BufferLine 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 get isWrapped(): boolean; 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) { @@ -81,16 +86,6 @@ export abstract class AbstractBufferLine implements IBufferLine { } } - /** - * 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 @@ -103,37 +98,6 @@ export abstract class AbstractBufferLine implements IBufferLine { * 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 @@ -144,13 +108,6 @@ export abstract class AbstractBufferLine implements IBufferLine { 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(); @@ -163,43 +120,11 @@ export abstract class AbstractBufferLine implements IBufferLine { 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); - + // if (isWrapped) new WrappedBufferLine(...); + return new LogicalBufferLine(cols, fillCellData); } - // @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; } @@ -216,489 +141,6 @@ export abstract class BufferLine extends AbstractBufferLine implements IBufferLi public static wSet1(kind: DataKind, value: number): number { return (kind << 28) | (value & 0x0fffffff); } -} - -// FOLLOWING ONLY APPLIES for OldBufferLine - -/** - * buffer memory layout: - * - * | uint32_t | uint32_t | uint32_t | - * | `content` | `FG` | `BG` | - * | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) | - */ - - -/** typed array slots taken by one cell */ -const CELL_SIZE = 3; - -/** - * Cell member indices. - * - * Direct access: - * `content = data[column * CELL_SIZE + Cell.CONTENT];` - * `fg = data[column * CELL_SIZE + Cell.FG];` - * `bg = data[column * CELL_SIZE + Cell.BG];` - */ -const enum Cell { - CONTENT = 0, - FG = 1, // currently simply holds all known attrs - BG = 2 // currently unused -} - -// This class will be removed at some point - -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, 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) { - this.setCell(i, cell); - } - this.length = cols; - } - - /** - * Get cell data CharData. - * @deprecated - */ - public get(index: number): CharData { - const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - return [ - this._data[index * CELL_SIZE + Cell.FG], - (content & Content.IS_COMBINED_MASK) - ? this._combined[index] - : (cp) ? stringFromCodePoint(cp) : '', - content >> Content.WIDTH_SHIFT, - (content & Content.IS_COMBINED_MASK) - ? this._combined[index].charCodeAt(this._combined[index].length - 1) - : cp - ]; - } - - /** - * Set cell data from CharData. - * @deprecated - */ - public set(index: number, value: CharData): void { - this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; - if (value[CHAR_DATA_CHAR_INDEX].length > 1) { - this._combined[index] = value[1]; - this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } else { - this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } - } - - /** - * primitive getters - * use these when only one value is needed, otherwise use `loadCell` - */ - public getWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT; - } - - /** Test whether content has width. */ - public hasWidth(index: number): number { - return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK; - } - - /** Get FG cell component. */ - public getFg(index: number): number { - return this._data[index * CELL_SIZE + Cell.FG]; - } - - /** Get BG cell component. */ - public getBg(index: number): number { - return this._data[index * CELL_SIZE + Cell.BG]; - } - - /** - * Test whether contains any chars. - * 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._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK; - } - - /** - * Get codepoint of the cell. - * 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 { - const content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED_MASK) { - return this._combined[index].charCodeAt(this._combined[index].length - 1); - } - return content & Content.CODEPOINT_MASK; - } - - /** Test whether the cell contains a combined string. */ - 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]; - if (content & Content.IS_COMBINED_MASK) { - return this._combined[index]; - } - if (content & Content.CODEPOINT_MASK) { - return stringFromCodePoint(content & Content.CODEPOINT_MASK); - } - // return empty string for empty cells - return ''; - } - - /** Get state of protected flag. */ - public isProtected(index: number): number { - return this._data[index * CELL_SIZE + Cell.BG] & BgFlags.PROTECTED; - } - - /** - * 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 { - $startIndex = index * CELL_SIZE; - cell.content = this._data[$startIndex + Cell.CONTENT]; - cell.fg = this._data[$startIndex + Cell.FG]; - cell.bg = this._data[$startIndex + Cell.BG]; - if (cell.content & Content.IS_COMBINED_MASK) { - cell.combinedData = this._combined[index]; - } - if (cell.bg & BgFlags.HAS_EXTENDED) { - cell.extended = this._extendedAttrs[index]!; - } - return cell; - } - - /** - * Set data at `index` to `cell`. - */ - public setCell(index: number, cell: ICellData): void { - if (cell.content & Content.IS_COMBINED_MASK) { - this._combined[index] = cell.combinedData; - } - if (cell.bg & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[index] = cell.extended; - } - this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content; - this._data[index * CELL_SIZE + Cell.FG] = cell.fg; - this._data[index * CELL_SIZE + Cell.BG] = cell.bg; - } - - /** - * Set cell data from input handler. - * Since the input handler see the incoming chars as UTF32 codepoints, - * it gets an optimized access method. - */ - public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { - if (attrs.bg & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[index] = attrs.extended; - } - this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); - this._data[index * CELL_SIZE + Cell.FG] = attrs.fg; - this._data[index * CELL_SIZE + Cell.BG] = attrs.bg; - } - - /** - * Add a codepoint to a cell from input handler. - * During input stage combining chars with a width of 0 follow and stack - * onto a leading char. Since we already set the attrs - * by the previous `setDataFromCodePoint` call, we can omit it here. - */ - public addCodepointToCell(index: number, codePoint: number, width: number): void { - let content = this._data[index * CELL_SIZE + Cell.CONTENT]; - if (content & Content.IS_COMBINED_MASK) { - // we already have a combined string, simply add - this._combined[index] += stringFromCodePoint(codePoint); - } else { - if (content & Content.CODEPOINT_MASK) { - // normal case for combining chars: - // - move current leading char + new one into combined string - // - set combined flag - this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); - content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 - content |= Content.IS_COMBINED_MASK; - } else { - // should not happen - we actually have no data in the cell yet - // simply set the data in the cell buffer with a width of 1 - content = codePoint | (1 << Content.WIDTH_SHIFT); - } - } - if (width) { - content &= ~Content.WIDTH_MASK; - content |= width << Content.WIDTH_SHIFT; - } - this._data[index * CELL_SIZE + Cell.CONTENT] = content; - } - public insertCells(pos: number, n: number, fillCellData: ICellData): void { - pos %= this.length; - - // handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char - if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); - } - - if (n < this.length - pos) { - const cell = new CellData(); - for (let i = this.length - pos - n - 1; i >= 0; --i) { - this.setCell(pos + n + i, this.loadCell(pos + i, cell)); - } - for (let i = 0; i < n; ++i) { - this.setCell(pos + i, fillCellData); - } - } else { - for (let i = pos; i < this.length; ++i) { - this.setCell(i, fillCellData); - } - } - - // handle fullwidth at line end: reset last cell if it is first cell of a wide char - if (this.getWidth(this.length - 1) === 2) { - this.setCellFromCodepoint(this.length - 1, 0, 1, fillCellData); - } - } - - public deleteCells(pos: number, n: number, fillCellData: ICellData): void { - pos %= this.length; - if (n < this.length - pos) { - const cell = new CellData(); - for (let i = 0; i < this.length - pos - n; ++i) { - this.setCell(pos + i, this.loadCell(pos + n + i, cell)); - } - for (let i = this.length - n; i < this.length; ++i) { - this.setCell(i, fillCellData); - } - } else { - for (let i = pos; i < this.length; ++i) { - this.setCell(i, fillCellData); - } - } - - // handle fullwidth at pos: - // - reset pos-1 if wide char - // - reset pos if width==0 (previous second cell of a wide char) - if (pos && this.getWidth(pos - 1) === 2) { - this.setCellFromCodepoint(pos - 1, 0, 1, fillCellData); - } - if (this.getWidth(pos) === 0 && !this.hasContent(pos)) { - this.setCellFromCodepoint(pos, 0, 1,fillCellData); - } - } - - /** - * 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). - */ - public resize(cols: number, fillCellData: ICellData): boolean { - if (cols === this.length) { - return this._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; - } - 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 uint32Cells * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength; - } - - /** - * 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._data.length * 4 * CLEANUP_THRESHOLD < this._data.buffer.byteLength) { - const data = new Uint32Array(this._data.length); - data.set(this._data); - this._data = data; - return 1; - } - return 0; - } - - /** fill a line with fillCharData */ - 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) { - if (!this.isProtected(i)) { - this.setCell(i, fillCellData); - } - } - return; - } - this._combined = {}; - this._extendedAttrs = {}; - for (let i = 0; i < this.length; ++i) { - this.setCell(i, fillCellData); - } - } - - /** alter to a full copy of line */ - public copyFrom(xline: BufferLine): void { - const line = xline as OldBufferLine; - if (this.length !== line.length) { - this._data = new Uint32Array(line._data); - } else { - // use high speed copy if lengths are equal - this._data.set(line._data); - } - this.length = line.length; - this._combined = {}; - for (const el in line._combined) { - this._combined[el] = line._combined[el]; - } - this._extendedAttrs = {}; - for (const el in line._extendedAttrs) { - this._extendedAttrs[el] = line._extendedAttrs[el]; - } - this._isWrapped = line.isWrapped; - } - - /** create a new clone */ - public clone(): IBufferLine { - const newLine = new OldBufferLine(0); - newLine._data = new Uint32Array(this._data); - newLine.length = this.length; - for (const el in this._combined) { - newLine._combined[el] = this._combined[el]; - } - for (const el in this._extendedAttrs) { - newLine._extendedAttrs[el] = this._extendedAttrs[el]; - } - newLine._isWrapped = this.isWrapped; - return newLine as IBufferLine; - } - - public getTrimmedLength(): number { - for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); - } - } - return 0; - } - - public getNoBgTrimmedLength(): number { - for (let i = this.length - 1; i >= 0; --i) { - if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * CELL_SIZE + Cell.BG] & Attributes.CM_MASK)) { - return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT); - } - } - return 0; - } - - 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--) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; - } - if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } - } - } else { - for (let cell = 0; cell < length; cell++) { - for (let i = 0; i < CELL_SIZE; i++) { - this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i]; - } - if (srcData[(srcCol + cell) * CELL_SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { - this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell]; - } - } - } - - // Move any combined data over as needed, FIXME: repeat for extended attrs - const srcCombinedKeys = Object.keys(src._combined); - for (let i = 0; i < srcCombinedKeys.length; i++) { - const key = parseInt(srcCombinedKeys[i], 10); - if (key >= srcCol) { - this._combined[key - srcCol + destCol] = src._combined[key]; - } - } - } - - 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()); - } - if (outColumns) { - outColumns.length = 0; - } - let result = ''; - while (startCol < endCol) { - const content = this._data[startCol * CELL_SIZE + Cell.CONTENT]; - const cp = content & Content.CODEPOINT_MASK; - const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR; - result += chars; - if (outColumns) { - for (let i = 0; i < chars.length; ++i) { - outColumns.push(startCol); - } - } - startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by at least 1 - } - if (outColumns) { - outColumns.push(startCol); - } - 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. @@ -1405,13 +847,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } } - 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 cell = this.loadCell(index, new CellData()); @@ -1452,7 +887,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { if (cols === this.length) { return this.shouldCleanupMemory(); } - const uint32Cells = cols * CELL_SIZE; + const uint32Cells = cols * 2; // FIXME if (cols > this.length) { /* if (this.data().buffer.byteLength >= uint32Cells * 4) { @@ -1499,14 +934,14 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { this.replaceCells(0, this.length, fillCellData, respectProtect); } - // @deprecated - not used if usingNewBufferLine() + // @deprecated - only used for testing public copyFrom(xline: BufferLine): void { alert('copyFrom'); } /** create a new clone */ public clone(): IBufferLine { - alert('NewBufferLine.clone'); + alert('BufferLine.clone'); const newLine = new LogicalBufferLine(0); return newLine; } @@ -1567,22 +1002,6 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { 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 = -1, outColumns?: number[], skipReplace: string = WHITESPACE_CELL_CHAR): string { const lineStart = this.logicalStartColumn(); let logicalEndColumn = endCol >= 0 ? lineStart + endCol @@ -1603,7 +1022,7 @@ export abstract class NewBufferLine extends BufferLine implements IBufferLine { } } -export class LogicalBufferLine extends NewBufferLine implements IBufferLine { +export class LogicalBufferLine extends BufferLine implements IBufferLine { protected _data: Uint32Array; // Each item in _data is a 4-bit DataKind and 28 bits data. _dataLength: number = 0; // active length of _data array @@ -1636,7 +1055,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { this._dataLength = 0; } this.length = cols; - this._isWrapped = false; if (fillCellData) { this.preInsert(0, fillCellData); } } public override logicalLine(): LogicalBufferLine { return this; } @@ -1645,6 +1063,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { override dataLength(): number { return this._dataLength; } override _cachedBg(): number { return this._cache2; } override _cachedFg(): number { return this._cache3; } + public get isWrapped(): boolean { return false; } protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF); } @@ -1652,17 +1071,19 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { * The oldLine_data buffer is resized to _dataLength, * while the old _data buffer is reused for the new line. */ - public static makeAndTrim(cols: number, fillCellData?: IAttributeData, oldLine?: LogicalBufferLine): LogicalBufferLine { - if (oldLine && oldLine._data.length > oldLine._dataLength) { - const oldData = oldLine._data; - oldLine._data = oldData.slice(0, oldLine._dataLength); - const newLine = new LogicalBufferLine(cols, undefined, undefined, 0, oldData); - newLine._data = oldData; - if (fillCellData) { newLine.preInsert(0, fillCellData); } - return newLine; - } else { - return new LogicalBufferLine(cols, fillCellData); + public static makeAndTrim(cols: number, fillCellData?: IAttributeData, oldRow?: IBufferLine): LogicalBufferLine { + if (oldRow) { + const oldLine = (oldRow as BufferLine).logicalLine(); + if (oldLine._data.length > oldLine._dataLength) { + const oldData = oldLine._data; + oldLine._data = oldData.slice(0, oldLine._dataLength); + const newLine = new LogicalBufferLine(cols, undefined, undefined, 0, oldData); + newLine._data = oldData; + if (fillCellData) { newLine.preInsert(0, fillCellData); } + return newLine; + } } + return new LogicalBufferLine(cols, fillCellData); } /** create a new clone */ @@ -1674,7 +1095,6 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { for (const el in this._extendedAttrs) { newLine._extendedAttrs[el] = this._extendedAttrs[el]; } - newLine._isWrapped = this.isWrapped; return newLine; } @@ -1836,7 +1256,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } - public setWrapped(previousLine: NewBufferLine): WrappedBufferLine { + public setWrapped(previousLine: BufferLine): WrappedBufferLine { const column = this.logicalStartColumn() + previousLine.length; const content = previousLine.moveToLineColumn(column); const neededPadding = column - previousLine._cachedColumn(); @@ -1960,7 +1380,7 @@ export class LogicalBufferLine extends NewBufferLine implements IBufferLine { } } -export class WrappedBufferLine extends NewBufferLine implements IBufferLine { +export class WrappedBufferLine extends BufferLine implements IBufferLine { _logicalLine: LogicalBufferLine; /** Number of logical columns in previous rows. @@ -1994,16 +1414,17 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { startBg: number = 0; startStyle: number = -1; - constructor(prevRow: NewBufferLine) { + constructor(prevRow: BufferLine) { super(); const logicalLine = prevRow.logicalLine(); prevRow.nextRowSameLine = this; this._logicalLine = logicalLine; - this._isWrapped = true; this.length = logicalLine.length; } - public setStartFromCache(line: NewBufferLine, column: LineColumn, content: number): void { + public get isWrapped(): boolean { return true; } + + public setStartFromCache(line: BufferLine, column: LineColumn, content: number): void { this.startColumn = content === 0 ? line._cachedColumn() : column; this.startIndex = line._cachedDataIndex(); this.startIndexColumn = line._cachedColumn(); @@ -2029,9 +1450,9 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { } public resizeData(size: number): void { this._logicalLine.resizeData(size); } public cleanupMemory(): number { return 0;} - public getPreviousRow(): NewBufferLine { - for (let row: NewBufferLine = this._logicalLine; ;) { - const next = row.nextRowSameLine as NewBufferLine; + public getPreviousRow(): BufferLine { + for (let row: BufferLine = this._logicalLine; ;) { + const next = row.nextRowSameLine as BufferLine; if (next === this) { return row; } @@ -2039,7 +1460,7 @@ export class WrappedBufferLine extends NewBufferLine implements IBufferLine { } } - public asUnwrapped(prevRow: NewBufferLine = this.getPreviousRow()): LogicalBufferLine { + public asUnwrapped(prevRow: BufferLine = this.getPreviousRow()): LogicalBufferLine { const oldStartColumn = this.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); diff --git a/src/common/buffer/BufferReflow.ts b/src/common/buffer/BufferReflow.ts index 44aa0976fe..8474761216 100644 --- a/src/common/buffer/BufferReflow.ts +++ b/src/common/buffer/BufferReflow.ts @@ -7,161 +7,6 @@ import { BufferLine } from 'common/buffer/BufferLine'; import { CircularList } from 'common/CircularList'; import { IBufferLine, ICellData } from 'common/Types'; -export interface INewLayoutResult { - layout: number[]; - countRemoved: number; -} - -/** - * Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed - * when a wrapped line unwraps. - * @param lines The buffer lines. - * @param oldCols The columns before resize - * @param newCols The columns after resize. - * @param bufferAbsoluteY The absolute y position of the cursor (baseY + cursorY). - * @param nullCell The cell data to use when filling in empty cells. - * @param reflowCursorLine Whether to reflow the line containing the cursor. - */ -export function reflowLargerGetLinesToRemove(lines: CircularList, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData, reflowCursorLine: boolean): number[] { - // Gather all BufferLines that need to be removed from the Buffer here so that they can be - // batched up and only committed once - const toRemove: number[] = []; - - for (let y = 0; y < lines.length - 1; y++) { - // Check if this row is wrapped - let i = y; - let nextLine = lines.get(++i) as BufferLine; - if (!nextLine.isWrapped) { - continue; - } - - // Check how many lines it's wrapped for - const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine]; - while (i < lines.length && nextLine.isWrapped) { - wrappedLines.push(nextLine); - nextLine = lines.get(++i) as BufferLine; - } - - if (!reflowCursorLine) { - // If these lines contain the cursor don't touch them, the program will handle fixing up - // wrapped lines with the cursor - if (bufferAbsoluteY >= y && bufferAbsoluteY < i) { - y += wrappedLines.length - 1; - continue; - } - } - - // Copy buffer data to new locations - let destLineIndex = 0; - let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols); - let srcLineIndex = 1; - let srcCol = 0; - while (srcLineIndex < wrappedLines.length) { - const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols); - const srcRemainingCells = srcTrimmedTineLength - srcCol; - const destRemainingCells = newCols - destCol; - const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells); - - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false); - - destCol += cellsToCopy; - if (destCol === newCols) { - destLineIndex++; - destCol = 0; - } - srcCol += cellsToCopy; - if (srcCol === srcTrimmedTineLength) { - srcLineIndex++; - srcCol = 0; - } - - // Make sure the last cell isn't wide, if it is copy it to the current dest - if (destCol === 0 && destLineIndex !== 0) { - if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) { - wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false); - // Null out the end of the last row - wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell); - } - } - } - - // Clear out remaining cells or fragments could remain; - wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell); - - // Work backwards and remove any rows at the end that only contain null cells - let countToRemove = 0; - for (let i = wrappedLines.length - 1; i > 0; i--) { - if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) { - countToRemove++; - } else { - break; - } - } - - if (countToRemove > 0) { - toRemove.push(y + wrappedLines.length - countToRemove); // index - toRemove.push(countToRemove); - } - - y += wrappedLines.length - 1; - } - return toRemove; -} - -/** - * Creates and return the new layout for lines given an array of indexes to be removed. - * @param lines The buffer lines. - * @param toRemove The indexes to remove. - */ -export function reflowLargerCreateNewLayout(lines: CircularList, toRemove: number[]): INewLayoutResult { - const layout: number[] = []; - // First iterate through the list and get the actual indexes to use for rows - let nextToRemoveIndex = 0; - let nextToRemoveStart = toRemove[nextToRemoveIndex]; - let countRemovedSoFar = 0; - for (let i = 0; i < lines.length; i++) { - if (nextToRemoveStart === i) { - const countToRemove = toRemove[++nextToRemoveIndex]; - - // Tell markers that there was a deletion - lines.onDeleteEmitter.fire({ - index: i - countRemovedSoFar, - amount: countToRemove - }); - - i += countToRemove - 1; - countRemovedSoFar += countToRemove; - nextToRemoveStart = toRemove[++nextToRemoveIndex]; - } else { - layout.push(i); - } - } - return { - layout, - countRemoved: countRemovedSoFar - }; -} - -/** - * Applies a new layout to the buffer. This essentially does the same as many splice calls but it's - * done all at once in a single iteration through the list since splice is very expensive. - * @param lines The buffer lines. - * @param newLayout The new layout to apply. - */ -export function reflowLargerApplyNewLayout(lines: CircularList, newLayout: number[]): void { - // Record original lines so they don't get overridden when we rearrange the list - const newLayoutLines: BufferLine[] = []; - for (let i = 0; i < newLayout.length; i++) { - newLayoutLines.push(lines.get(newLayout[i]) as BufferLine); - } - - // Rearrange the list - for (let i = 0; i < newLayoutLines.length; i++) { - lines.set(i, newLayoutLines[i]); - } - lines.length = newLayout.length; -} - /** * Gets the new line lengths for a given wrapped line. The purpose of this function it to pre- * compute the wrapping points since wide characters may need to be wrapped onto the following line. diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index daa21b9408..447bacfbe0 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -6,7 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IAttributeData, IBufferLine } from 'common/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { usingNewBufferLine, NewBufferLine, LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; +import { LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { Emitter } from 'vs/base/common/event'; @@ -65,30 +65,12 @@ export class BufferService extends Disposable implements IBufferService { const bottomRow = buffer.ybase + buffer.scrollBottom; let newLine: IBufferLine | undefined; - if (usingNewBufferLine()) { - if (isWrapped) { - const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as NewBufferLine; - newLine = new WrappedBufferLine(oldLine); - } else { - const bottom = buffer.lines.get(bottomRow); - if (bottom instanceof NewBufferLine) { - // trim bottoms _data buffer, and reuse for newLine. - newLine = LogicalBufferLine.makeAndTrim(this.cols, eraseAttr, bottom.logicalLine()); - } else { - newLine = new LogicalBufferLine(this.cols, eraseAttr); - } - } + if (isWrapped) { + const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as BufferLine; + newLine = new WrappedBufferLine(oldLine); } else { - newLine = this._cachedBlankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg || newLine instanceof NewBufferLine) { - newLine = buffer.getBlankLine(eraseAttr, isWrapped); - this._cachedBlankLine = newLine; - } - newLine._isWrapped = isWrapped; - if (buffer.scrollTop !== 0 || bottomRow !== buffer.lines.length - 1 - || ! buffer.lines.isFull) { - newLine = newLine.clone(); - } + const bottom = buffer.lines.get(bottomRow); + newLine = LogicalBufferLine.makeAndTrim(this.cols, eraseAttr, bottom); } if (buffer.scrollTop === 0) { @@ -99,11 +81,9 @@ export class BufferService extends Disposable implements IBufferService { if (bottomRow === buffer.lines.length - 1) { if (! willBufferBeTrimmed) { buffer.lines.push(newLine); - } else if (usingNewBufferLine()) { + } else { buffer.lines.recycle(); // ignore result buffer.lines.set(bottomRow, newLine); - } else { - buffer.lines.recycle().copyFrom(newLine); } } else { buffer.lines.splice(bottomRow + 1, 0, newLine); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 03f8d01f4d..2edf521f07 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -7,7 +7,6 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { isMac } from 'common/Platform'; import { CursorStyle, IDisposable } from 'common/Types'; import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services'; -import { selectNewBufferLine } from 'common/buffer/BufferLine'; import { Emitter } from 'vs/base/common/event'; export const DEFAULT_OPTIONS: Readonly> = { @@ -90,7 +89,6 @@ 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 @@ -184,9 +182,6 @@ 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) { From a168af36a3cc0ac436e7cc16c6e1816c0228d3a7 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 8 Aug 2025 18:45:51 -0700 Subject: [PATCH 66/73] Fixes to showData, eraseCells, getTrimmedLength. Further test-suite improvements. --- src/common/InputHandler.ts | 2 +- src/common/buffer/BufferLine.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index e638570808..3e43bd96fe 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1083,7 +1083,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (line) { const eraseAttrs = this._eraseAttrData(); if (! respectProtect) { - line.eraseCells(0, this._bufferService.cols, eraseAttrs); + (line as BufferLine).eraseCells(0, this._bufferService.cols, eraseAttrs); } else { line.fill(this._activeBuffer.getNullCell(eraseAttrs), respectProtect); } diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 99e6c7b791..30f3b8507b 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -242,11 +242,15 @@ export abstract class BufferLine implements IBufferLine { let code: string | number = kind; const wnum = word & 0xfffffff; let nextColumn = curColumn; + let skip = curColumn < startColumn; 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.SKIP_COLUMNS: code = 'SKIP'; + nextColumn += wnum; + skip = nextColumn <= startColumn; + 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; @@ -254,7 +258,7 @@ export abstract class BufferLine implements IBufferLine { case DataKind.CHAR_W2: code = 'C2'; nextColumn += 2; break; } - if (startColumn <= nextColumn) { + if (! skip) { if (s) { s += ', '; } @@ -753,6 +757,10 @@ export abstract class BufferLine implements IBufferLine { idata--; lline._dataLength = idata; } else { + if (this instanceof WrappedBufferLine && idata === this.startIndex) { + this.startIndex--; + this.startIndexColumn -= skipped; + } data[idata-1] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, skipped + count); } } else { @@ -971,7 +979,7 @@ export abstract class BufferLine implements IBufferLine { case DataKind.SKIP_COLUMNS: skipped += BufferLine.wSkipCount(word); if (idata === startIndex && this instanceof WrappedBufferLine) { - skipped -= this.startColumn = this.startIndexColumn; + skipped -= this.startColumn - this.startIndexColumn; } break; case DataKind.CLUSTER_START_W1: From 175ead4193ed7a1634e25319eb3ed5521a75c188 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 10 Aug 2025 09:28:57 -0700 Subject: [PATCH 67/73] More cleanups, especialy when constructing BufferLine objects. Remove unused copyFrom and clone methods, which were only used in tests. --- src/common/Types.ts | 4 - src/common/buffer/BufferLine.test.ts | 140 +++++---------------------- src/common/buffer/BufferLine.ts | 42 +++----- src/common/buffer/CellData.ts | 29 +++--- src/common/services/BufferService.ts | 4 +- 5 files changed, 50 insertions(+), 169 deletions(-) diff --git a/src/common/Types.ts b/src/common/Types.ts index 83c457f9e2..e0e0ac0bb4 100644 --- a/src/common/Types.ts +++ b/src/common/Types.ts @@ -243,10 +243,6 @@ export interface IBufferLine { resize(cols: number, fill: ICellData): boolean; cleanupMemory(): number; fill(fillCellData: ICellData, respectProtect?: boolean): void; - // @deprecated - copyFrom(line: IBufferLine): void; - // @deprecated - clone(): IBufferLine; getTrimmedLength(): number; getNoBgTrimmedLength(): number; translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string; diff --git a/src/common/buffer/BufferLine.test.ts b/src/common/buffer/BufferLine.test.ts index 79a1a062e4..4302ba181e 100644 --- a/src/common/buffer/BufferLine.test.ts +++ b/src/common/buffer/BufferLine.test.ts @@ -166,9 +166,9 @@ describe('BufferLine', function(): void { 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 = BufferLine.make(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true); + line = BufferLine.make(10, CellData.fromCharData([123, 'a', 1, 'a'.charCodeAt(0)]), true); assert.equal(line.length, 10); - assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 456, 'a'.charCodeAt(0)]); + assert.deepEqual(line.loadCell(0, new CellData()).getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]); assert.equal(line.isWrapped, true); }); it('insertCells', function(): void { @@ -232,58 +232,28 @@ describe('BufferLine', function(): void { [123, 'z', 1, 'z'.charCodeAt(0)] ]); }); - it('clone', function(): void { - const line = BufferLine.make(5, undefined, true); - line.setCell(0, CellData.fromChar('a', 1, 1)); - line.setCell(1, CellData.fromChar('b', 1, 2)); - line.setCell(2, CellData.fromChar('c', 1, 3)); - line.setCell(3, CellData.fromChar('d', 1, 4)); - line.setCell(4, CellData.fromChar('e', 1, 5)); - const line2 = line.clone(); - assert.deepEqual(lineToArray(line2), lineToArray(line)); - assert.equal(line2.length, line.length); - assert.equal(line2.isWrapped, line.isWrapped); - }); - it('copyFrom', function(): void { - const line = BufferLine.make(5); - line.setCell(0, CellData.fromChar('a', 1, 1)); - line.setCell(1, CellData.fromChar('b', 1, 2)); - line.setCell(2, CellData.fromChar('c', 1, 3)); - line.setCell(3, CellData.fromChar('d', 1, 4)); - line.setCell(4, CellData.fromChar('e', 1, 5)); - const line2 = BufferLine.make(5, CellData.fromChar('a', 1, 1), true); - line2.copyFrom(line); - assert.deepEqual(lineToArray(line2), lineToArray(line)); - assert.equal(line2.length, line.length); - assert.equal(line2.isWrapped, line.isWrapped); - }); it('should support combining chars', 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 = 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(lineToArray(line2), lineToArray(line)); - const line3 = line.clone(); - assert.deepEqual(lineToArray(line3), lineToArray(line)); + const line = BufferLine.make(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)])); + assert.deepEqual(lineToArray(line), [[1, 'e\u0301', 1, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]]); }); describe('resize', function(): void { it('enlarge(false)', function(): void { - 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(lineToArray(line), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + const line = BufferLine.make(5, CellData.fromChar('a', 1, 1), false); + line.resize(10, CellData.fromChar('a', 1, 1)); + assert.deepEqual(lineToArray(line), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)])); }); it('enlarge(true)', function(): void { - 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(lineToArray(line), (Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + const line = BufferLine.make(5, CellData.fromChar('a', 1, 1), false); + line.resize(10, CellData.fromChar('a', 1, 1)); + assert.deepEqual(lineToArray(line), (Array(10) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)])); }); it('shrink(true) - should apply new size', function(): void { - 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(lineToArray(line), (Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)])); + const line = BufferLine.make(10, CellData.fromChar('a', 1, 1)); + line.resize(5, CellData.fromChar('a', 1, 1)); + assert.deepEqual(lineToArray(line), (Array(5) as any).fill([1, 'a', 1, 'a'.charCodeAt(0)])); }); it('shrink to 0 length', function(): void { const line = BufferLine.make(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false); @@ -601,7 +571,7 @@ describe('BufferLine', function(): void { } it('setCells', function(): void { const line = BufferLine.make(5); - const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); + const cell = CellData.fromChar('a', 1, 1); // no eAttrs line.setCell(0, cell); @@ -624,21 +594,21 @@ describe('BufferLine', function(): void { line.setCell(4, cell); 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)] + [1, 'a', 1, 'a'.charCodeAt(0)], + [1, 'a', 1, 'a'.charCodeAt(0)], + [1, 'A', 1, 'A'.charCodeAt(0)], + [1, 'A', 1, 'A'.charCodeAt(0)], + [1, 'A', 1, 'A'.charCodeAt(0)] ]); 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); + assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.DOTTED); + assert.equal(extendedAttributes(line, 4)?.underlineStyle, undefined); // should be ref to the same object - assert.equal((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[2]); + assert.equal(extendedAttributes(line, 1), extendedAttributes(line, 2)); // should be a different obj - assert.notEqual((line as any)._extendedAttrs[1], (line as any)._extendedAttrs[3]); + assert.notEqual(extendedAttributes(line, 1), extendedAttributes(line, 3)); }); it('loadCell', () => { const line = BufferLine.make(5); @@ -735,69 +705,5 @@ describe('BufferLine', function(): void { assert.equal(extendedAttributes(line, 3)?.underlineStyle, UnderlineStyle.CURLY); assert.equal(extendedAttributes(line, 4)?.underlineStyle, UnderlineStyle.CURLY); }); - it('clone', () => { - const line = BufferLine.make(5); - const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); - // no eAttrs - line.setCell(0, cell); - - // some underline style - cell.extended.underlineStyle = UnderlineStyle.CURLY; - cell.bg |= BgFlags.HAS_EXTENDED; - line.setCell(1, cell); - - // same eAttr, different codepoint - cell.content = 65; // 'A' - line.setCell(2, cell); - - // different eAttr - cell.extended = cell.extended.clone(); - cell.extended.underlineStyle = UnderlineStyle.DOTTED; - line.setCell(3, cell); - - // no eAttrs again - cell.bg &= ~BgFlags.HAS_EXTENDED; - line.setCell(4, cell); - - const nLine = line.clone(); - assert.equal((nLine as any)._extendedAttrs[0], (line as any)._extendedAttrs[0]); - assert.equal((nLine as any)._extendedAttrs[1], (line as any)._extendedAttrs[1]); - assert.equal((nLine as any)._extendedAttrs[2], (line as any)._extendedAttrs[2]); - assert.equal((nLine as any)._extendedAttrs[3], (line as any)._extendedAttrs[3]); - assert.equal((nLine as any)._extendedAttrs[4], (line as any)._extendedAttrs[4]); - }); - it('copyFrom', () => { - const initial = BufferLine.make(5); - const cell = CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]); - // no eAttrs - initial.setCell(0, cell); - - // some underline style - cell.extended.underlineStyle = UnderlineStyle.CURLY; - cell.bg |= BgFlags.HAS_EXTENDED; - initial.setCell(1, cell); - - // same eAttr, different codepoint - cell.content = 65; // 'A' - initial.setCell(2, cell); - - // different eAttr - cell.extended = cell.extended.clone(); - cell.extended.underlineStyle = UnderlineStyle.DOTTED; - initial.setCell(3, cell); - - // no eAttrs again - cell.bg &= ~BgFlags.HAS_EXTENDED; - initial.setCell(4, cell); - - 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]); - assert.equal((line as any)._extendedAttrs[1], (initial as any)._extendedAttrs[1]); - assert.equal((line as any)._extendedAttrs[2], (initial as any)._extendedAttrs[2]); - assert.equal((line as any)._extendedAttrs[3], (initial as any)._extendedAttrs[3]); - assert.equal((line as any)._extendedAttrs[4], (initial as any)._extendedAttrs[4]); - }); }); }); diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 30f3b8507b..79c94a6b38 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -121,8 +121,13 @@ export abstract class BufferLine implements IBufferLine { } static make(cols: number, fillCellData?: ICellData, isWrapped: boolean = false): BufferLine { - // if (isWrapped) new WrappedBufferLine(...); - return new LogicalBufferLine(cols, fillCellData); + const line = new LogicalBufferLine(cols, fillCellData); + if (isWrapped) { + const wline = new WrappedBufferLine(line); + if (fillCellData) { wline.replaceCells(0, cols, fillCellData); } + return wline; + } + return line; } /** From a Uint23 in _data, extract the DataKind bits. */ @@ -942,18 +947,6 @@ export abstract class BufferLine implements IBufferLine { this.replaceCells(0, this.length, fillCellData, respectProtect); } - // @deprecated - only used for testing - public copyFrom(xline: BufferLine): void { - alert('copyFrom'); - } - - /** create a new clone */ - public clone(): IBufferLine { - alert('BufferLine.clone'); - const newLine = new LogicalBufferLine(0); - return newLine; - } - public getTrimmedLength(countBackground: boolean = false, logical: boolean = false): number { let cols = 0; let skipped = 0; @@ -1048,22 +1041,23 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { _cache3: number = 0; _cache4: number = -1; - constructor(cols: number, fillCellData?: IAttributeData, src?: WrappedBufferLine, startIndex: number = 0, data: Uint32Array = new Uint32Array(cols)) { + constructor(cols: number, fillCellData?: ICellData, src?: WrappedBufferLine, startIndex: number = 0, data: Uint32Array = new Uint32Array(cols)) { super(); // MAYBE: const buffer = new ArrayBuffer(0, { maxByteLength: 6 * cols }); // const buffer = new ArrayBuffer(4 * cols, { maxByteLength: 6 * cols }); + this.length = cols; if (src) { const lline = src.logicalLine(); const oldStart = startIndex; this._data = lline._data.slice(oldStart); this._dataLength = lline._dataLength - oldStart; this._extendedAttrs = lline._extendedAttrs.slice(oldStart); + if (fillCellData) { this.preInsert(0, fillCellData); } } else { this._data = data; this._dataLength = 0; + if (fillCellData) { this.replaceCells(0, this.length, fillCellData, false); } } - this.length = cols; - if (fillCellData) { this.preInsert(0, fillCellData); } } public override logicalLine(): LogicalBufferLine { return this; } public override logicalStartColumn(): LineColumn { return 0; } @@ -1079,7 +1073,7 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { * The oldLine_data buffer is resized to _dataLength, * while the old _data buffer is reused for the new line. */ - public static makeAndTrim(cols: number, fillCellData?: IAttributeData, oldRow?: IBufferLine): LogicalBufferLine { + public static makeAndTrim(cols: number, fillCellData?: ICellData, oldRow?: IBufferLine): LogicalBufferLine { if (oldRow) { const oldLine = (oldRow as BufferLine).logicalLine(); if (oldLine._data.length > oldLine._dataLength) { @@ -1094,18 +1088,6 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { return new LogicalBufferLine(cols, fillCellData); } - /** create a new clone */ - public clone(): IBufferLine { - const newLine = new LogicalBufferLine(0); - newLine._data = new Uint32Array(this._data); - newLine._dataLength = this._dataLength; - newLine.length = this.length; - for (const el in this._extendedAttrs) { - newLine._extendedAttrs[el] = this._extendedAttrs[el]; - } - return newLine; - } - // count can be negative addEmptyDataElements(position: number, count: number): void { const oldDataLength = this._dataLength; diff --git a/src/common/buffer/CellData.ts b/src/common/buffer/CellData.ts index 7cb987f4f2..6100ba7b9f 100644 --- a/src/common/buffer/CellData.ts +++ b/src/common/buffer/CellData.ts @@ -76,27 +76,20 @@ export class CellData extends AttributeData implements ICellData { 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 code = text.charCodeAt(0) || 0; let combined = false; - const length = value[CHAR_DATA_CHAR_INDEX].length; + const length = text.length; // surrogates and combined strings need special treatment if (length > 2) { combined = true; } 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 if (0xD800 <= code && code <= 0xDBFF) { - const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1); + const second = text.charCodeAt(1); if (0xDC00 <= second && second <= 0xDFFF) { - this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + code = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000); } else { combined = true; @@ -106,14 +99,18 @@ export class CellData extends AttributeData implements ICellData { combined = true; } } - else { - this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); - } if (combined) { - this.combinedData = value[CHAR_DATA_CHAR_INDEX]; - this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); + this.combinedData = text; + code |= Content.IS_COMBINED_MASK; } + this.content = code | (width << Content.WIDTH_SHIFT); } + + /** Set data from CharData */ + public setFromCharData(value: CharData): void { + this.setFromChar(value[CHAR_DATA_CHAR_INDEX], value[CHAR_DATA_WIDTH_INDEX], value[CHAR_DATA_ATTR_INDEX]); + } + /** Get data as CharData. */ public getAsCharData(): CharData { return [this.fg, this.getChars(), this.getWidth(), this.getCode()]; diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 447bacfbe0..45f351e6af 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -6,7 +6,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IAttributeData, IBufferLine } from 'common/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; +import { BufferLine, LogicalBufferLine, WrappedBufferLine } from 'common/buffer/BufferLine'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; import { Emitter } from 'vs/base/common/event'; @@ -70,7 +70,7 @@ export class BufferService extends Disposable implements IBufferService { newLine = new WrappedBufferLine(oldLine); } else { const bottom = buffer.lines.get(bottomRow); - newLine = LogicalBufferLine.makeAndTrim(this.cols, eraseAttr, bottom); + newLine = LogicalBufferLine.makeAndTrim(this.cols, buffer.getNullCell(eraseAttr), bottom); } if (buffer.scrollTop === 0) { From edc509ad3957bdc488723eb032bc798007b7f09a Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 14 Aug 2025 12:03:01 -0700 Subject: [PATCH 68/73] Fix to addEmptyDataElements adjustment of startIndex. --- src/common/buffer/BufferLine.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 79c94a6b38..6a051368fd 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -232,7 +232,7 @@ export abstract class BufferLine implements IBufferLine { } public showRowData(): string { - return (this.isWrapped ? '(wrapped)' : '') + return (this instanceof WrappedBufferLine ? '(wrapped)' : '') + this.showData(this.logicalStartColumn(), this.nextRowSameLine ? this.nextRowSameLine?.logicalStartColumn() : Infinity); } /* Human-readable display of data() array, for debugging */ @@ -1099,7 +1099,7 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { } this._dataLength += count; for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { - if (next.startIndex > position) + if (count < 0 ? next.startIndex >= position - count: next.startIndex > position) {next.startIndex += count;} } if (count < 0) { From 6e8c2d09d166d2d94c73868321e8393af298cf6f Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Thu, 14 Aug 2025 12:30:18 -0700 Subject: [PATCH 69/73] Clear wrapped state before (rather than after) various operations. This seems likely to be slightly more efficient. --- src/common/InputHandler.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 3e43bd96fe..02de73635f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -1057,19 +1057,19 @@ export class InputHandler extends Disposable implements IInputHandler { */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false, respectProtect: boolean = false): void { const row = this._activeBuffer.ybase + y; - const line = this._activeBuffer.lines.get(row) as BufferLine; - const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); + if (clearWrap && start === 0) { + this._activeBuffer.setWrapped(row, false); + } if (clearWrap && end === Infinity) { this._activeBuffer.setWrapped(row + 1, false); } + const line = this._activeBuffer.lines.get(row) as BufferLine; + const fill = this._activeBuffer.getNullCell(this._eraseAttrData()); if (! respectProtect) { line.eraseCells(start, end, fill); } else { line.replaceCells(start, end, fill, respectProtect); } - if (clearWrap && start === 0) { - this._activeBuffer.setWrapped(row, false); - } } /** @@ -1439,9 +1439,9 @@ 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 row = this._activeBuffer.ybase + y; + this._activeBuffer.setWrapped(row, false); const line = this._activeBuffer.lines.get(row)!; line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1463,9 +1463,9 @@ 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 row = this._activeBuffer.ybase + y; + this._activeBuffer.setWrapped(row, false); const line = this._activeBuffer.lines.get(row)!; line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; @@ -1487,9 +1487,9 @@ 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 row = this._activeBuffer.ybase + y; + this._activeBuffer.setWrapped(row, false); const line = this._activeBuffer.lines.get(row)!; line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData())); - this._activeBuffer.setWrapped(row, false); } this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; From 51daae5ab8c3a04e0280b98e5e51f2cb51df248d Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Sun, 17 Aug 2025 11:00:41 -0700 Subject: [PATCH 70/73] Enhane addEmptyDataElements to better support delete. --- src/common/buffer/BufferLine.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 6a051368fd..0dbeb6efc5 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -182,7 +182,7 @@ export abstract class BufferLine implements IBufferLine { public abstract logicalStartColumn(): LineColumn; protected abstract data(): Uint32Array; abstract resizeData(size: number): void; - abstract addEmptyDataElements(position: number, count: number): void; + abstract addEmptyDataElements(position: number, count: number, insertBeforeEnd?: boolean): void; protected shouldCleanupMemory(): boolean { return this.dataLength() * CLEANUP_THRESHOLD < this.data().length; } @@ -402,7 +402,7 @@ export abstract class BufferLine implements IBufferLine { const idata = this._cachedDataIndex(); const code = fillCellData.getCode(); if (code === NULL_CELL_CODE) { - this.addEmptyDataElements(idata, 1); + this.addEmptyDataElements(idata, 1, true); this.data()[idata] = BufferLine.wSet1(DataKind.SKIP_COLUMNS, n); } else { this.addEmptyDataElements(idata, n); @@ -1089,7 +1089,7 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { } // count can be negative - addEmptyDataElements(position: number, count: number): void { + addEmptyDataElements(position: number, count: number, insertBeforeEnd?: boolean) { const oldDataLength = this._dataLength; this.resizeData(oldDataLength + count); if (count < 0) { @@ -1099,7 +1099,7 @@ export class LogicalBufferLine extends BufferLine implements IBufferLine { } this._dataLength += count; for (let next = this.nextRowSameLine; next; next = next.nextRowSameLine) { - if (count < 0 ? next.startIndex >= position - count: next.startIndex > position) + if (count < 0 ? next.startIndex >= position - count: insertBeforeEnd ? next.startIndex >= position : next.startIndex > position) {next.startIndex += count;} } if (count < 0) { @@ -1429,8 +1429,8 @@ export class WrappedBufferLine extends BufferLine implements IBufferLine { 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); + addEmptyDataElements(position: number, count: number, insertBeforeEnd?: boolean): void { + this._logicalLine.addEmptyDataElements(position, count, insertBeforeEnd); } protected _cachedColumnInRow(): RowColumn { return (this.logicalLine()._cache1 & 0xFFFF) - this.startIndexColumn; } protected _cacheReset(): void { From 845dc248fe289490eb4789476b9a96dbca661b86 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 18 Aug 2025 21:11:38 -0700 Subject: [PATCH 71/73] Fix "end logic" for getTrimmedLength. --- src/common/buffer/BufferLine.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 0dbeb6efc5..610ebacce4 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -953,10 +953,13 @@ export abstract class BufferLine implements IBufferLine { const startIndex = !logical && this instanceof WrappedBufferLine ? this.startIndex : 0; const data = this.data(); - const end = this.nextRowSameLine && ! logical ? this.nextRowSameLine.startIndex : this.dataLength(); - let bg = this._cachedBg(); + const dlen = this.dataLength(); + const endColumn = this.nextRowSameLine && ! logical ? this.nextRowSameLine.startColumn : Infinity; + const startColumn = !logical && this instanceof WrappedBufferLine ? this.startColumn : 0; + let bg = this._cachedBg(); // FIXME ? let bgCol = 0; - for (let idata = startIndex; idata < end; idata++) { + let col = startColumn; + for (let idata = startIndex; col < endColumn && idata < dlen; idata++) { const word = data[idata]; const kind = BufferLine.wKind(word); const w = kind === DataKind.CHAR_W2 || kind === DataKind.CLUSTER_START_W2 ? 2 : 1; @@ -970,10 +973,12 @@ export abstract class BufferLine implements IBufferLine { case DataKind.STYLE_FLAGS: break; case DataKind.SKIP_COLUMNS: - skipped += BufferLine.wSkipCount(word); + let skip = BufferLine.wSkipCount(word); if (idata === startIndex && this instanceof WrappedBufferLine) { - skipped -= this.startColumn - this.startIndexColumn; + skip -= this.startColumn - this.startIndexColumn; } + col += skip; + skipped += skip; break; case DataKind.CLUSTER_START_W1: case DataKind.CLUSTER_START_W2: @@ -987,6 +992,7 @@ export abstract class BufferLine implements IBufferLine { case DataKind.CLUSTER_CONTINUED: break; // should be skipped } + col += wcols; if (wcols) { cols += skipped + wcols; skipped = 0; From d4823745ce0858b5aa6bc7c23966780fef38465a Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Tue, 19 Aug 2025 18:17:32 -0700 Subject: [PATCH 72/73] Use moveToLineColumn rather than moveToColumn in BufferLine.asUnwrapped. --- src/common/buffer/BufferLine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 610ebacce4..69f7884737 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -1460,7 +1460,7 @@ export class WrappedBufferLine extends BufferLine implements IBufferLine { const oldStartColumn = this.logicalStartColumn(); prevRow.nextRowSameLine = undefined; const oldLine = prevRow.logicalLine(); - oldLine.moveToColumn(oldStartColumn, 1); + oldLine.moveToLineColumn(oldStartColumn, 1); const startIndex = oldLine._cachedDataIndex(); const cell = new CellData(); this.loadCell(oldStartColumn, cell); From da0f1a1db55400ec2a73819b153dd00f81e4332c Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Wed, 20 Aug 2025 10:24:54 -0700 Subject: [PATCH 73/73] Implement insertMode in BufferLine.insertText. --- src/common/buffer/BufferLine.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts index 69f7884737..53e3818ca8 100644 --- a/src/common/buffer/BufferLine.ts +++ b/src/common/buffer/BufferLine.ts @@ -728,7 +728,12 @@ export abstract class BufferLine implements IBufferLine { } const lastChar = idata; inputHandler.precedingJoinState = precedingJoinState; - if (! insertMode && idata < this.dataLength()) { + if (insertMode) { + let deleteStartColumn = lstart + this.length; + this.moveToLineColumn(deleteStartColumn, 1); + inext = this._cachedDataIndex(); + this.logicalLine().deleteCellsOnly(inext, curColumn - startColumn); + } else if (idata < this.dataLength()) { this.logicalLine().deleteCellsOnly(inext, curColumn - startColumn); } curColumn -= lstart;