Description
See this branch for a prototype of this proposal. The prototype is not usable: a lot of things work; a lot don't.
The BufferLine data structure contains a _data
field, which is a Uint32Array
with 3 elements per column. This makes for fast O(1) mapping from column number to character/cell information, but it has anumber of disadvantages:
-
It is not memory-effecient.
-
Convoluted encoding due to squeezing attribute values into available bits in the foreground/background elements.
-
Tied to a terminal model with an integral number of fixed-width cells. Supports double-width characters and grapheme clusters (somewhat clumsily), but no variable-width fonts, or any glyphs whose width isn't an integral number of cells. Many languages don't work well with fixed-width characters. For other languages being forced to use fixed-width character is unaestheic or unfriendly. (This includes to some extent English.) (Support for variable-width fonts is not part of the current proposal, but I do have some ideas for what can be done.)
-
Limited extensibility: There aren't a lot of available bits left, and there is no room for properties that require more than a few bits. Anything that doesn't fit has to be added the the extended-attribute object, which is more expensive.
-
Clumsy/expensive reflow when window width changes.
Proposal
Summary
The main fields of a BufferLine
become:
-
A
_text
string that contains all the characters in the line. This is the concatenation of all the content code (for simple characters), and the_combined
elements, and subsumes both. -
The
_data
array is a combination of cell runs (represented as lengths in the_text
string), and bg/fg/attribute values. Each element is a 4-bit "kind" (for exampleSKIP_COLUMNS
for "null" columns), and 28 bits of data (such as a color value or the length of a text run).
The_dataLength
field contains the "current" (active) length of the_data
array, to allow for future insertions.
The_data
array only needs as many elements as actually used, though pre-allocating extra space for "growth" is typically worthwhile.
In the prototype, a "run" is either one or more non-composed BMP characters of the same width; a run of "null" columns; or a single "other" glyph (a cluster or a non-BMP character). If the number columns spanned by the _data
elements before _dataLength
, an implicit SKIP_COLUMNS
represents the rest of the line.
A CellData
contains the current index into the _data
and _text
arrays. Since a single _data
element can represent a run of multiple columns (when all characters are BMP and the same width) the CellData also maintains a column-offset relative to the start of the current _data
element.
The CellData
also contains the bg/fg/attribute state at the current position.
Space efficiency
The _data
array contains elements for changes in bh/fg/attribute value, and for "runs" of text. This is much more efficient than the current _data
array.
Each non-null character is just one or 2 16-bit code units in the _text
string, which is as efficient as you can get.
InputHandler efficiency
In the prototype, most editing operations make use of a CellData
object that represents the current position in the BufferLine
to edit. Given an appropriate CellData, the actual editing operation is comparable in complexity to the existing implementation: The logic is sometimes slightly more complicated, but the amount of work is comparable: either fixed, or proportional to the number of following runs (if elements have to be inserted or deleted). Bulk operations (which are most performance-critical) tend to be at the end of a line, so usually you're modifying the last element or two in the _data
array or appending to the end of it.
However, setting the fields of a CellData
to the correct values representing an arbitrary column position is no longer O(1), but is proportional to the number of "runs" between the start of the line and the desired position. This is an obvious potential performance regression.
Luckily, most output is sequential, not random-access. This is especially the case non-interactive "bulk" output.
The InputHandler
maintains a _workCell
that when valid represents the current state of the current position. In the current implementation, the _workCell
state is initialized at the start of each print
call. A natural optimization is to assume if the _workCell
is "valid" it can be used without initialization. Escape sequences that move the cursor will usually need to invalidate the _workCell
, but plain text and attribute changes do not.
Another possible performance concern is updating the _text
string. In the prototype is this done separately for each character, but it can obviously be "chunked" to larger units.
Rendering efficiency
Rendering does not require random access: Currently each renderer reads each line in sequential order, so using a CellData as an iterator is straightforward and efficient.
Efficiency of selection, serialization and other operations
Other operations generally work with with sequential access, or they are not performance-critical.
Possible refinements
The bg/fg/attribute could be the xor'd values of the corresponding previous values. This would make backwards traversal efficient.
While using a _text
array to contain both simple characters and clusters is very compact, there is some costs in terms of mapping codeunits to strings, and appending the new strings to the _text
string. (On the other hand, operations where string values are needed may be faster than currently, especially where runs of characters are desired (as in the dom renderer) because extracting a substring
from the _text
string is probably relatively inexpensive.) An alternative is to store the codeunits in the _data
array, as in the current implementation.
Line overflow and reflow
Terminology: A (visible) "row" is the text/data on a single row in the terminal. A (logical) "line" is one or more rows which "wrap" into each other.
A tempting follow-up change is for all the rows belonging to the same line to share the _data
array and the _text
array. This means neither _data
or _text
change when text is re-flowed. We just add or remove row objects.
A possible data structure is to use a plain BufferLine
for a line (including the initial rows), and a sub-class BufferOverflowLine
for each wrapped (non-initial) row. Each BufferOverflowLine
points back to the parent BufferLine
. The BufferOverflowLine
also contains the state needed to efficiently initialize a CellData
at the start of that row.
A reflow operations needs to add or remove BufferOverflowLine
children of a parent BufferLine
.
A bonus is cleaner separation between the content "model" (a table of lines with _data
and _text
properties) versus the "view" in a specific viewport (a table of rows).
Different visible and logical row width
Consider a REPL: While typing an input line, you change the line width, either by resizing the viewport or changing the zoom. The terminal send the new line width to the application, which sends a sequence to re-draw the input line with the new width. But by the time the terminal can update itself, there may be a further re-sizing. This can result in a garbled display.
A solution: The terminal does not send the window-resize request to the application. Instead it does a local reflow, just as it would do with old output lines. The tricky part is that the terminal must interpret escape sequences from the application using the old width (since that is what the application believes to be the current width).
The implementation isn't terribly difficult: create aBufferOverflowLine
set based on the actual (visible) line width, and a different set based on the logical (application) line width. Basically you add a flag "use this BufferOverFlowLine during rendering but ignore it during input-handling" and a converse flag "use this during input-handling but ignore it during rendering".
This behavior can be controlled by shell-integration escape sequences: A prompt-end escape sequence turns off window-size change reporting and enables tracking separate visible and logical line width. An input-end (command-start) sequence returns to normal.
The same logic can be used to support variable-width fonts in prompts and command input: The application assumes normal fixed-width characters, and works with the logical width, while the renderer displays a user-preferred variable-width font, wrapping lines based on the actual width. This behavior should be opt-in: It can be enabled by a flag in the shell-integrate escape. This can be set in a user configuration file, but does not require modifying the actual application.
AbstractBufferLine
The prototype has an AbstractBufferLine
class that implements IBufferLine
and is the parent of BufferLine
(and BufferOverflowLine
). The intention is that we might add new sub-classes for "sections" that aren't normal lines. For example, images or raw (but safety-scrubbed) HTML <div>
elements. We may also support lines that have different heights.
Esoteric line types are not part of part of this proposal, though they motivate some design decisions.
Rich output: HTML, SVG, and images
As an example the gnuplot
graphing program has an interactive mode where the output of each command can display a graph in various formats, including SVG. If the gnuplot terminal type is "domterm", it generates SVG, wraps it an some simple escape sequences, and DomTerm displays below the input command.
Another example is a math program emitting MathML which is displayed in the same REPL console as terminal output.
Note this is similar to ipython/Jupyter, but directly in the terminal. This you can mix rich output with full terminal support.
CellData is concrete
Conceptually, the CellData implementation depends on the AbstractBufferLine implementation. However, we would like to pre-allocate CellData helper objects, and re-use the CellData instance for many lines, regardless of whether the line is a regular BufferLine or something else. Hence CellData has various fields with unspecified meaning: _stateM
, _stateN
, _stateA
, _stateB
. These are available for any AbstractBufferLine implementation to use as it sees fit. For example _stateM
and _stateN
are the current indexes in the _data
and _text
arrays,