Skip to content

move BufferLine _data for wrapped lines into a shared LogicalLine #5673

@PerBothner

Description

@PerBothner

This proposal is based on some ideas discussed in issue #4800 and PR #4928, but much simplified. Specifically, we do not change the representation of each cell using three uint32 _data elements.

The proposal is to move the cell data from the BufferLine class to a new LogicalLine class which ignores line-wrapping. If a line is wrapped across multiple BufferLine instances, they would all point to a shared LogicalLine, which would own the _data array.

class LogicalLine {
  protected _data: Uint32Array;
  protected _combined: {[index: number]: string} = {};
  protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};

  firstBufferLine: BufferLine | undefined;
  ...
}

class BufferLine implements IBufferLine {
  public logicalLine: LogicalLine;
  nextBufferLine: BufferLine | undefined;
  
  /** Number of logical columns in previous rows. */
  startColumn: number = 0;
  ...
}

The general rule is that data in the LogicalLine object does not depend on line width, and does not change on reflow. (The exception is the firstBufferLine linked list. Moving that out of the LogicalLine class would allow multiple "views" into the same Buffer "model", but that is a possible future refinement.) It follows that if W is the line width, and R is the "row number" of a BufferLine (where the initial unwrapped line has R==0), the startColumn will usually be R×W. However, if we write a double-wide character in the last column, we do not store a fill character in the _data array; instead the startColumn of the wrapped line will be S0+W-1, where S0 is the startColumn of the previous line.

Thus the obvious benefit: much less work on reflow. It also makes it easier to implement lazy reflow, where we only reflow visible lines.

If theLogicalLine is independent of line width, then there is no need for the size of the _data array to be the line width (or a multiple of the line width when lines are wrapped).

class LogicalLine {
   ...
  /** Logical "trimmed" length of line.
     * Must be no more than this._data.length / 3.
     */
  public length: number;
}
class BufferLine {
   ...
  public getTrimmedLength(): number {
    return (this.nextLogicalLine ? this.nextLogicalLine.startColumn : this.logicalLine.length) - this.startColumn;
  }
}

Buffer management

For best use of space, the _data.length should be 3 times the length of the LogicalLine where the latter tracks actual data (as in getTrimmedLength). However, that could lead to lots of expensive reallocation. Various strategies are possible. One possibility is using a larger _data buffer for the "active line", and trimmed lines for the inactive lines, where we define "active line" as the one for the active buffer's y property. Thus the InputHandler would be responsible for trimming inactive lines.

It might be worthwhile using a global shared empty _data array for empty lines.

Wrapping

Switching a line from unwrapped to wrapped (and vice versa) are significantly more expensive in this implementation. However, making a line wrapped mostly happens when a line is first created, when new lines are scrolled in. The InputHandler manages this, mostly.

Erasing and unwrapping

Changing a line from wrapped to unwrapped is also somewhat expensive, but mostly happens when partial or complete lines are erased, so the extra cost is minor. It helps if we're a bit strategic about combining the erasing and unwrapping operations. For example erasing the rest of the display is mostly allocating new empty LogicalLine objects when needed for wrapped lines.

Background color

When a line (or tail of a line) is erased, any columns erased should display with the current background color.
We can't store that individually in the cells, since they have been "trimmed away". Instead, we use a field in the LogicalLine which is used for the "trimmed part" of the line.

class LogicalLine {
  backgroundColor: number = 0;
}
class BufferLine {
  public getBg(index: number): number {
    const lline = this.logicalLine;
    const lcolumn = index + this.startColumn;
    return lcolumn >= lline.length ? lline.backgroundColor ? lline._data[lcolumn * CELL_SIZE + Cell.BG];
  }
  public getFg(index: number): number {
    const lline = this.logicalLine;
    const lcolumn = index + this.startColumn;
    return lcolumn >= lline.length ? 0 ? lline._data[lcolumn * CELL_SIZE + Cell.FG];
  }
}

Note for the last column of a line where a double-wide character wrapped from the last column, we get the color of the first cell on the wrapped line - which is what we want.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions