Skip to content

BufferLine rewrite [WIP] #4928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 88 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
bf4e359
Experimental re-design of BufferLine.
PerBothner Jul 13, 2023
8df267f
Improve new BuferLine implementation.
PerBothner Jul 18, 2023
8d87caa
Merge branch 'master' into buffer-cell-cursor
PerBothner Aug 18, 2023
7f2107a
New implementations of translateToString and getTrimmedLength.
PerBothner Aug 18, 2023
d13d98c
Change some small access functions and tweak documentation+comments.
PerBothner Aug 18, 2023
ede649f
Add debugging functions _showData and getText.
PerBothner Aug 18, 2023
7659c9f
More work on BufferLine re-write.
PerBothner Aug 29, 2023
8de51e9
Add some comments to document IBuffer fields.
PerBothner Sep 8, 2023
231ae07
Merge remote-tracking branch 'upstream/master' into buffer-cell-cursor
PerBothner Sep 8, 2023
ae9ac1d
Merge remote-tracking branch 'upstream/master' into buffer-cell-cursor
PerBothner Sep 18, 2023
3b516f6
Start re-vamping BufferLine re-implementation
PerBothner Sep 27, 2023
6998d1e
Remove setCluster function. Fix thinko.
PerBothner Oct 4, 2023
9a2027b
Change setCellFromCodePoint API and rename to setCellFromCodepoint
PerBothner Sep 26, 2023
9cca703
Extract guts of loadCell into new moveToColumn function.
PerBothner Oct 5, 2023
5983a0c
Get webgl renderer working again (as well as it did before).
PerBothner Oct 6, 2023
68e7631
Remove old buffer DataKind values
PerBothner Oct 6, 2023
71bdedd
Revert canvas and webgl renderers to old API
PerBothner Oct 6, 2023
38fd0dc
Change LineBuffer.setAttributes to take an IAttributeData
PerBothner Oct 7, 2023
6d32c99
new method LineBuffer.insertText
PerBothner Oct 13, 2023
1636425
New help method deleteCellsOnly.
PerBothner Oct 16, 2023
e66a006
implement setCellFromCodepoint for new BufferLine
PerBothner Oct 20, 2023
dd1eea0
More BufferLine cleanup
PerBothner Oct 21, 2023
8d1b31b
Allow choice of either NewBufferLine or OldBufferLine
PerBothner Oct 21, 2023
79ed24f
Basic support for window resize.
PerBothner Nov 2, 2023
d0dfa2a
Merge branch 'master' into buffer-cell-cursor
PerBothner Nov 5, 2023
b01f827
Get rid of some no-longer-used stuff in CellData.
PerBothner Nov 5, 2023
a3020a7
Incomplete re-write of line-wrapping model.
PerBothner Nov 16, 2023
733093e
Basic reflow handling on window-width changes.
PerBothner Nov 23, 2023
ed1b672
_showData debug helper: Don't use hex for counts.
PerBothner Nov 24, 2023
43c8e43
Adjust buffer.y on window resize.
PerBothner Nov 24, 2023
214a820
New eraseCells function. Other fixes.
PerBothner Nov 27, 2023
41db1f2
Various fixes.
PerBothner Nov 29, 2023
632187f
Add logOutput, a debug hook for Terminal.write.
PerBothner Dec 1, 2023
7f2fc37
Fixes to scrolling and line wrapping.
PerBothner Dec 1, 2023
0deb617
Make erase work better.
PerBothner Dec 3, 2023
cfdfb06
Make background color erase (BCE) work
PerBothner Dec 7, 2023
795c388
Fix to scroll handling
PerBothner Dec 7, 2023
2baf3a8
Implement insertCells.
PerBothner Dec 8, 2023
dbc1080
Fix a bunch of lint issues
PerBothner Dec 9, 2023
63d36d5
Add "newBufferLine" option.
PerBothner Dec 10, 2023
040666b
Fix to setting of newBufferLine option
PerBothner Dec 11, 2023
97910e3
Improved logic to update cursor position on resize
PerBothner Dec 17, 2023
cd360f2
Change reflow to be lazy
PerBothner Dec 26, 2023
dc07f28
Merge remote-tracking branch 'upstream/master' into buffer-cell-cursor
PerBothner Dec 26, 2023
33a0054
Fix preInsert when position is in SKIP_SPACES or after end.
PerBothner Dec 28, 2023
0b08b16
Fix bugs from playwright: translateToString and ICH
PerBothner Dec 28, 2023
d09fbc3
Merge branch 'master' into buffer-cell-cursor
PerBothner Dec 29, 2023
6eee37b
Fix testsuite errors when newBufferLine is false
PerBothner Dec 29, 2023
a8aab00
More Lint fixes.
PerBothner Dec 29, 2023
a2c26d6
Merge branch 'master' into buffer-cell-cursor
PerBothner Dec 30, 2023
845d202
Fix translateToString.
PerBothner Dec 31, 2023
da51666
More reflow fixes, including handling saved cursor.
PerBothner Jan 6, 2024
e2da6d5
Testsuite-based fixes and cleanups.
PerBothner Jan 13, 2024
2d2c027
FixImplement extended-attribute handling in deleteCellsOnly.
PerBothner Jan 14, 2024
10719a0
Fixes for extended attributes and setWrapped for test-cases.
PerBothner Jan 22, 2024
c5685bb
More polishing and testsuite fixes.
PerBothner Jan 25, 2024
e0b62dd
Change preInsert to reduce duplicated style entries
PerBothner Jan 26, 2024
f86d308
Merge branch 'xtermjs:master' into buffer-cell-cursor
PerBothner Jan 26, 2024
bc57c28
Adjust showRow and showDataRow.
PerBothner Feb 25, 2024
9aaf516
Fixes mostly related to wrapped lines with wide characters
PerBothner Feb 29, 2024
b25427f
Fixes to Buffer.setWrapped
PerBothner Feb 29, 2024
b351bd1
Merge branch 'master' into buffer-cell-cursor
PerBothner Feb 29, 2024
1d6de8e
Merge branch 'xtermjs:master' into buffer-cell-cursor
PerBothner Jun 9, 2024
f92e6df
Merge branch 'master' into buffer-cell-cursor
PerBothner Jul 25, 2024
8b23f24
Remove 2 unused fields.
PerBothner Aug 5, 2024
544800a
Merge branch 'master' into buffer-cell-cursor
PerBothner Aug 5, 2024
5313b49
Revert "Remove 2 unused fields."
PerBothner Aug 7, 2024
e4d0a68
Merge branch 'master' into buffer-cell-cursor
PerBothner Sep 21, 2024
973ec45
Provide basic implementation of NewBufferLine.resize
PerBothner Oct 5, 2024
f662ef5
Fixes to NewBufferLine. More tests pass.
PerBothner Feb 24, 2025
e7c14ef
Merge branch 'master' into buffer-cell-cursor
PerBothner Feb 25, 2025
3b1e7b7
Fix some problems with wrapped lines.
PerBothner Feb 25, 2025
91e9c28
Move _extendedAttrs field to LogicalBufferLine.
PerBothner Mar 2, 2025
0766db4
Fixes in BufferLine based on tests.
PerBothner Jun 27, 2025
ceee12a
More fixing testsuite errors.
PerBothner Aug 4, 2025
fd24aa9
Use CellData.fromChar for some test cases.
PerBothner Aug 4, 2025
3756541
Implement LogicalBufferLine.clone.
PerBothner Aug 4, 2025
6bcb17b
Merge branch 'master' into buffer-cell-cursor
PerBothner Aug 4, 2025
c4944b8
On a 'scroll' trim old line's _data buffer to actual needed size.
PerBothner Aug 6, 2025
b165a23
Remove support for OldBufferLine.
PerBothner Aug 6, 2025
a168af3
Fixes to showData, eraseCells, getTrimmedLength.
PerBothner Aug 9, 2025
175ead4
More cleanups, especialy when constructing BufferLine objects.
PerBothner Aug 10, 2025
edc509a
Fix to addEmptyDataElements adjustment of startIndex.
PerBothner Aug 14, 2025
6e8c2d0
Clear wrapped state before (rather than after) various operations.
PerBothner Aug 14, 2025
51daae5
Enhane addEmptyDataElements to better support delete.
PerBothner Aug 17, 2025
845dc24
Fix "end logic" for getTrimmedLength.
PerBothner Aug 19, 2025
d482374
Use moveToLineColumn rather than moveToColumn in BufferLine.asUnwrapped.
PerBothner Aug 20, 2025
da0f1a1
Implement insertMode in BufferLine.insertText.
PerBothner Aug 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/browser/TestUtils.test.ts
Original file line number Diff line number Diff line change
@@ -220,6 +220,9 @@ export class MockBuffer implements IBuffer {
public addMarker(y: number): IMarker {
throw new Error('Method not implemented.');
}
public splitLine(row: number, col: number): void {
throw new Error('Method not implemented.');
}
public isCursorInViewport!: boolean;
public lines!: ICircularList<IBufferLine>;
public ydisp!: number;
@@ -240,6 +243,9 @@ export class MockBuffer implements IBuffer {
public getWrappedRangeForLine(y: number): { first: number, last: number } {
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments as any);
}
public reflowRegion(startRow: number, endRow: number, maxRows: number): boolean {
throw new Error('Method not implemented.');
}
public nextStop(x?: number): number {
throw new Error('Method not implemented.');
}
@@ -264,6 +270,9 @@ export class MockBuffer implements IBuffer {
public clearAllMarkers(): void {
throw new Error('Method not implemented.');
}
public setWrapped(row: number, value: boolean): void {
throw new Error('Method not implemented.');
}
}

export class MockRenderer implements IRenderer {
9 changes: 9 additions & 0 deletions src/browser/public/Terminal.ts
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@ export class Terminal extends Disposable implements ITerminalApi {
private _parser: IParser | undefined;
private _buffer: BufferNamespaceApi | undefined;
private _publicOptions: Required<ITerminalOptions>;
public logOutput: boolean = false;

constructor(options?: ITerminalOptions & ITerminalInitOnlyOptions) {
super();
@@ -224,6 +225,14 @@ export class Terminal extends Disposable implements ITerminalApi {
this._core.clear();
}
public write(data: string | Uint8Array, callback?: () => void): void {
if (this.logOutput && data instanceof Uint8Array) {
const thisAny = this as any;
if (! thisAny._decoder) {
thisAny._decoder = new TextDecoder(); // label = "utf-8");
}
const str = thisAny._decoder.decode(data, { stream:true });
console.log('write: '+JSON.stringify(str));
}
this._core.write(data, callback);
}
public writeln(data: string | Uint8Array, callback?: () => void): void {
8 changes: 4 additions & 4 deletions src/browser/renderer/dom/DomRendererRowFactory.test.ts
Original file line number Diff line number Diff line change
@@ -405,7 +405,7 @@ describe('DomRendererRowFactory', () => {
});

it('should handle BCE correctly', () => {
const nullCell = lineData.loadCell(0, new CellData());
const nullCell = CellData.fromChar(' ');
nullCell.bg = Attributes.CM_P16 | 1;
lineData.setCell(2, nullCell);
nullCell.bg = Attributes.CM_P16 | 2;
@@ -418,7 +418,7 @@ describe('DomRendererRowFactory', () => {
});

it('should handle BCE for multiple cells', () => {
const nullCell = lineData.loadCell(0, new CellData());
const nullCell = CellData.fromChar(' ');
nullCell.bg = Attributes.CM_P16 | 1;
lineData.setCell(0, nullCell);
let spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
@@ -451,7 +451,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '€', 1, '€'.charCodeAt(0)]));
lineData.setCell(2, CellData.fromCharData([DEFAULT_ATTR, 'c', 1, 'c'.charCodeAt(0)]));
lineData.setCell(3, CellData.fromCharData([DEFAULT_ATTR, '語', 2, 'c'.charCodeAt(0)]));
lineData.setCell(4, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
lineData.setCell(5, CellData.fromCharData([DEFAULT_ATTR, '𝄞', 1, 'c'.charCodeAt(0)]));
const spans = rowFactory.createRow(lineData, 0, false, undefined, undefined, 0, false, 5, EMPTY_WIDTH, -1, -1);
assert.equal(extractHtml(spans),
'<span>a</span><span style="letter-spacing: 3px;">€</span><span>c語</span><span style="letter-spacing: -2px;">𝄞</span>'
@@ -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]));
}
9 changes: 5 additions & 4 deletions src/browser/renderer/dom/DomRendererRowFactory.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,6 @@
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
@@ -44,7 +43,7 @@

constructor(
private readonly _document: Document,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, // FIXME remove
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@ICoreService private readonly _coreService: ICoreService,
@@ -71,9 +70,9 @@
linkStart: number,
linkEnd: number
): HTMLSpanElement[] {
const cell = this._workCell;

const elements: HTMLSpanElement[] = [];
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
const colors = this._themeService.colors;

let lineLength = lineData.getNoBgTrimmedLength();
@@ -84,7 +83,7 @@
let charElement: HTMLSpanElement | undefined;
let cellAmount = 0;
let text = '';
let i = 0;

Check warning on line 86 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'i' is never reassigned. Use 'const' instead

Check warning on line 86 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'i' is assigned a value but never used
let oldBg = 0;
let oldFg = 0;
let oldExt = 0;
@@ -92,14 +91,14 @@
let oldSpacing = 0;
let oldIsInSelection: boolean = false;
let spacing = 0;
let skipJoinedCheckUntilX = 0;

Check warning on line 94 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'skipJoinedCheckUntilX' is never reassigned. Use 'const' instead
const classes: string[] = [];

const hasHover = linkStart !== -1 && linkEnd !== -1;

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) {
@@ -107,17 +106,18 @@
}

// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;

Check warning on line 109 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'isJoined' is never reassigned. Use 'const' instead

// Indicates whether this cell is part of a joined range that should be ignored as it cannot
// be rendered entirely, like the selection state differs across the range.
let isValidJoinRange = (x >= skipJoinedCheckUntilX);

Check warning on line 113 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'isValidJoinRange' is never reassigned. Use 'const' instead

let lastCharX = x;

Check warning on line 115 in src/browser/renderer/dom/DomRendererRowFactory.ts

GitHub Actions / lint

'lastCharX' is never reassigned. Use 'const' instead

// 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] && isValidJoinRange) {
const range = joinedRanges.shift()!;
@@ -149,6 +149,7 @@
width = cell.getWidth();
}
}
*/

const isInSelection = this._isCellInSelection(x, row);
const isCursorCell = isCursorRow && x === cursorX;
4 changes: 2 additions & 2 deletions src/browser/services/CharacterJoinerService.test.ts
Original file line number Diff line number Diff line change
@@ -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;
1 change: 1 addition & 0 deletions src/browser/services/CharacterJoinerService.ts
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import { CellData } from 'common/buffer/CellData';
import { IBufferService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';

// FIXME should probably just use plain CellData
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
// .content carries no meaning for joined CellData, simply nullify it
20 changes: 10 additions & 10 deletions src/browser/services/SelectionService.test.ts
Original file line number Diff line number Diff line change
@@ -55,15 +55,15 @@ 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)]));
}
return result;
}

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
@@ -190,10 +190,10 @@ 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.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]);
4 changes: 2 additions & 2 deletions src/browser/services/SelectionService.ts
Original file line number Diff line number Diff line change
@@ -864,7 +864,7 @@ export class SelectionService extends Disposable implements ISelectionService {
}

// Expand the string in both directions until a space is hit
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell) as CellData)) {
bufferLine.loadCell(startCol - 1, this._workCell);
const length = this._workCell.getChars().length;
if (this._workCell.getWidth() === 0) {
@@ -880,7 +880,7 @@ export class SelectionService extends Disposable implements ISelectionService {
startIndex--;
startCol--;
}
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell) as CellData)) {
bufferLine.loadCell(endCol + 1, this._workCell);
const length = this._workCell.getChars().length;
if (this._workCell.getWidth() === 2) {
24 changes: 15 additions & 9 deletions src/common/CircularList.ts
Original file line number Diff line number Diff line change
@@ -153,14 +153,16 @@ export class CircularList<T> extends Disposable implements ICircularList<T> {
* @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<T> extends Disposable implements ICircularList<T> {
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.
30 changes: 15 additions & 15 deletions src/common/InputHandler.test.ts
Original file line number Diff line number Diff line change
@@ -483,18 +483,16 @@
);

// 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');

Check warning on line 486 in src/common/InputHandler.test.ts

GitHub Actions / lint

Variable name `a_repeat_cols` must match one of the following formats: camelCase, UPPER_CASE
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(' ')
]);
@@ -1916,17 +1914,19 @@
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', () => {
278 changes: 108 additions & 170 deletions src/common/InputHandler.ts

Large diffs are not rendered by default.

20 changes: 13 additions & 7 deletions src/common/Types.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
*/

import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants'; // eslint-disable-line no-unused-vars
import { Attributes, StyleFlags, UnderlineStyle } from 'common/buffer/Constants'; // eslint-disable-line no-unused-vars
import { IBufferSet } from 'common/buffer/Types';
import { IParams } from 'common/parser/Types';
import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
@@ -101,6 +101,7 @@ export interface ICharset {
[key: string]: string | undefined;
}

// Deprecated
export type CharData = [number, string, number, number];

export interface IColor {
@@ -136,12 +137,12 @@ export interface IOscLinkData {
export interface IAttributeData {
/**
* "fg" is a 32-bit unsigned integer that stores the foreground color of the cell in the 24 least
* significant bits and additional flags in the remaining 8 bits.
* significant bits and additional flags in the remaining 8 bits. @deprecated
*/
fg: number;
/**
* "bg" is a 32-bit unsigned integer that stores the background color of the cell in the 24 least
* significant bits and additional flags in the remaining 8 bits.
* significant bits and additional flags in the remaining 8 bits. @deprecated
*/
bg: number;
/**
@@ -164,6 +165,10 @@ export interface IAttributeData {
isProtected(): number;
isOverline(): number;

getFg(): number; // 26 bits including CM_MASK
getBg(): number; // 26 bits including CM_MASK
getStyleFlags(): StyleFlags;

/**
* The color mode of the foreground color which determines how to decode {@link getFgColor},
* possible values include {@link Attributes.CM_DEFAULT}, {@link Attributes.CM_P16},
@@ -224,21 +229,20 @@ export interface ICellData extends IAttributeData {
*/
export interface IBufferLine {
length: number;
isWrapped: boolean;
/** If the previous line wrapped (overflows) into the current line. */
readonly isWrapped: boolean;
get(index: number): CharData;
set(index: number, value: CharData): void;
loadCell(index: number, cell: ICellData): ICellData;
setCell(index: number, cell: ICellData): void;
setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void;
addCodepointToCell(index: number, codePoint: number, width: number): void;
addCodepointToCell(index: number, codePoint: number, width: number): void; // DEPRECATED
insertCells(pos: number, n: number, ch: ICellData): void;
deleteCells(pos: number, n: number, fill: ICellData): void;
replaceCells(start: number, end: number, fill: ICellData, respectProtect?: boolean): void;
resize(cols: number, fill: ICellData): boolean;
cleanupMemory(): number;
fill(fillCellData: ICellData, respectProtect?: boolean): void;
copyFrom(line: IBufferLine): void;
clone(): IBufferLine;
getTrimmedLength(): number;
getNoBgTrimmedLength(): number;
translateToString(trimRight?: boolean, startCol?: number, endCol?: number, outColumns?: number[]): string;
@@ -449,6 +453,8 @@ export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestor
*/
export interface IInputHandler {
onTitleChange: Event<string>;
readonly unicodeService: IUnicodeService;
precedingJoinState: number;

parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
print(data: Uint32Array, start: number, end: number): void;
5 changes: 3 additions & 2 deletions src/common/WindowsMode.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 10 additions & 1 deletion src/common/buffer/AttributeData.ts
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
*/

import { IAttributeData, IColorRGB, IExtendedAttrs } from 'common/Types';
import { Attributes, FgFlags, BgFlags, UnderlineStyle, ExtFlags } from 'common/buffer/Constants';
import { Attributes, FgFlags, BgFlags, UnderlineStyle, StyleFlags, ExtFlags } from 'common/buffer/Constants';

export class AttributeData implements IAttributeData {
public static toColorRGB(value: number): IColorRGB {
@@ -48,10 +48,19 @@ export class AttributeData implements IAttributeData {
public isStrikethrough(): number { return this.fg & FgFlags.STRIKETHROUGH; }
public isProtected(): number { return this.bg & BgFlags.PROTECTED; }
public isOverline(): number { return this.bg & BgFlags.OVERLINE; }
public getStyleFlags(): StyleFlags { return ((this.fg & 0xFC000000) >>> 24) | ((this.bg & 0xFC000000) >> 16); }
public setStyleFlags(flags: StyleFlags): void {
this.fg = (this.fg & 0x03ffffff) | ((flags << 24) & 0xFC000000);
this.bg = (this.bg & 0x03ffffff) | ((flags << 16) & 0xFC000000);
}

// color modes
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }
public getFg(): number { return this.fg & Attributes.CM_COLOR_MASK; }
public getBg(): number { return this.bg & Attributes.CM_COLOR_MASK; }
public setFg(fg: number): void { this.fg = (fg & 0x3ffffff) | (this.fg & 0xfc000000); }
public setBg(bg: number): void { this.bg = (bg & 0x3ffffff) | (this.bg & 0xfc000000); }
public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }
63 changes: 35 additions & 28 deletions src/common/buffer/Buffer.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
@@ -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);
@@ -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', () => {
@@ -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(), ' ');
@@ -1104,7 +1111,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 +1123,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 +1134,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 +1151,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 +1164,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 +1175,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)]));
536 changes: 285 additions & 251 deletions src/common/buffer/Buffer.ts

Large diffs are not rendered by default.

433 changes: 166 additions & 267 deletions src/common/buffer/BufferLine.test.ts

Large diffs are not rendered by default.

1,645 changes: 1,290 additions & 355 deletions src/common/buffer/BufferLine.ts

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions src/common/buffer/BufferReflow.test.ts
Original file line number Diff line number Diff line change
@@ -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)]);
155 changes: 0 additions & 155 deletions src/common/buffer/BufferReflow.ts
Original file line number Diff line number Diff line change
@@ -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<IBufferLine>, 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<IBufferLine>, 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<IBufferLine>, 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.
62 changes: 43 additions & 19 deletions src/common/buffer/CellData.ts
Original file line number Diff line number Diff line change
@@ -18,13 +18,30 @@ export class CellData extends AttributeData implements ICellData {
obj.setFromCharData(value);
return obj;
}
/** Primitives from terminal buffer. */

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.
* @deprecated
*/
public content = 0;
public fg = 0;
public bg = 0;
public extended: IExtendedAttrs = new ExtendedAttrs();

public combinedData = '';
/** Whether cell contains a combined string. */

public copyFrom(src: CellData): void {
this.content = src.content;
this.fg = src.fg;
this.bg = src.bg;
this.extended = src.extended;
}

/** Whether cell contains a combined string. DEPRECTED */
public isCombined(): number {
return this.content & Content.IS_COMBINED_MASK;
}
@@ -49,27 +66,30 @@ export class CellData extends AttributeData implements ICellData {
* of the last char in string to be in line with code in CharData.
*/
public getCode(): number {
return (this.isCombined())
? this.combinedData.charCodeAt(this.combinedData.length - 1)
: this.content & Content.CODEPOINT_MASK;
if (this.isCombined()) {
const chars = this.getChars();
return chars.charCodeAt(chars.length - 1);
}
return this.content & Content.CODEPOINT_MASK;
}
/** Set data from CharData */
public setFromCharData(value: CharData): void {
this.fg = value[CHAR_DATA_ATTR_INDEX];
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;
let code = text.charCodeAt(0) || 0;
let combined = false;
const length = text.length;
// surrogates and combined strings need special treatment
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
if (length > 2) {
combined = true;
}
else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
else if (length === 2) {
// 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;
@@ -79,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()];
54 changes: 39 additions & 15 deletions src/common/buffer/Constants.ts
Original file line number Diff line number Diff line change
@@ -4,9 +4,10 @@
*/

export const DEFAULT_COLOR = 0;
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
export const DEFAULT_ATTR = 0;
export const DEFAULT_EXT = 0;

// Deprecated
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
@@ -73,6 +74,8 @@ export const enum Content {
WIDTH_SHIFT = 22
}

export const NULL_CELL_WORD = 1 << Content.WIDTH_MASK;

export const enum Attributes {
/**
* bit 1..8 blue in RGB, color in P256 and P16
@@ -98,6 +101,7 @@ export const enum Attributes {
* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)
*/
CM_MASK = 0x3000000,
CM_COLOR_MASK = 0x3ffffff,
CM_DEFAULT = 0,
CM_P16 = 0x1000000,
CM_P256 = 0x2000000,
@@ -106,30 +110,50 @@ export const enum Attributes {
/**
* bit 1..24 RGB room
*/
RGB_MASK = 0xFFFFFF
RGB_MASK = 0xFFFFFF,

/**
* bit 27..32 in bg/fg are used for FgFlags/BgFlags (style bits).
* This will probably change.
*/
STYLE_BITS_MASK = 0xFC000000
}

export const enum StyleFlags {
INVERSE = 0x4,
BOLD = 0x8,
UNDERLINE = 0x10,
BLINK = 0x20,
INVISIBLE = 0x40,
STRIKETHROUGH = 0x80,
ITALIC = 0x400,
DIM = 0x800,
HAS_EXTENDED = 0x1000,
PROTECTED = 0x2000,
OVERLINE = 0x4000
}

export const enum FgFlags {
export const enum FgFlags { // deprecated
/**
* bit 27..32
*/
INVERSE = 0x4000000,
BOLD = 0x8000000,
UNDERLINE = 0x10000000,
BLINK = 0x20000000,
INVISIBLE = 0x40000000,
STRIKETHROUGH = 0x80000000,
INVERSE = StyleFlags.INVERSE << 24, // 0x4000000,
BOLD = StyleFlags.BOLD << 24, // 0x8000000,
UNDERLINE = StyleFlags.UNDERLINE << 24, // 0x10000000,
BLINK = StyleFlags.BLINK << 24, // x20000000,
INVISIBLE = StyleFlags.INVISIBLE << 24, // 0x40000000,
STRIKETHROUGH = StyleFlags.STRIKETHROUGH << 24 // 0x80000000
}

export const enum BgFlags {
export const enum BgFlags { // deprecated
/**
* bit 27..32 (upper 2 unused)
*/
ITALIC = 0x4000000,
DIM = 0x8000000,
HAS_EXTENDED = 0x10000000,
PROTECTED = 0x20000000,
OVERLINE = 0x40000000
ITALIC = StyleFlags.ITALIC << 16, // 0x4000000,
DIM = StyleFlags.DIM << 16, // 0x8000000,
HAS_EXTENDED = StyleFlags.HAS_EXTENDED << 16, // 0x10000000,
PROTECTED = StyleFlags.PROTECTED << 16, // 0x20000000
OVERLINE = StyleFlags.OVERLINE << 16 // 0x40000000
}

export const enum ExtFlags {
24 changes: 24 additions & 0 deletions src/common/buffer/Types.ts
Original file line number Diff line number Diff line change
@@ -11,10 +11,31 @@ export type BufferIndex = [number, number];

export interface IBuffer {
readonly lines: ICircularList<IBufferLine>;
/** Number of rows above top visible row.
* Similar to scrollTop (i.e. affected by scrollbar), but in rows.
* FUTURE: We want to handle variable-height rows. Maybe just use scrollTop.
*/
ydisp: number;
/** Number of rows in the scrollback buffer, above the home row. */
ybase: number;

/** Row number relative to the "home" row, zero-origin.
* This is the row number changed/reported by cursor escape sequences,
* except that y is 0-origin: y=0 when we're at the home row.
* Currently assumed to be >= 0, but FUTURE should allow negative - i.e.
* in scroll-back area, as long as ybase+y >= 0.
*/
y: number;

/** Column number, zero-origin.
* Valid range is 0 through C (inclusive), if C is terminal width in columns.
* The first (left-most) column is 0.
* The right-most column is either C-1 (before the right-most column, and
* ready to write in it), or C (after the right-most column, having written
* to it, and ready to wrap). DSR 6 returns C (1-origin) in either case,
*/
x: number;

tabs: any;
scrollBottom: number;
scrollTop: number;
@@ -26,6 +47,7 @@ export interface IBuffer {
isCursorInViewport: boolean;
markers: IMarker[];
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
splitLine(row: number, col: number): void;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
@@ -35,6 +57,8 @@ export interface IBuffer {
addMarker(y: number): IMarker;
clearMarkers(y: number): void;
clearAllMarkers(): void;
setWrapped(row: number, value: boolean): void;
reflowRegion(startRow: number, endRow: number, maxRows: number): void;
}

export interface IBufferSet extends IDisposable {
1 change: 1 addition & 0 deletions src/common/input/TextDecoder.ts
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion src/common/parser/Types.ts
Original file line number Diff line number Diff line change
@@ -226,7 +226,7 @@ export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerTy
/**
* Interface to denote a specific ESC, CSI or DCS handler slot.
* The values are used to create an integer respresentation during handler
* regristation before passed to the subparsers as `ident`.
* registration before passed to the subparsers as `ident`.
* The integer translation is made to allow a faster handler access
* in `EscapeSequenceParser.parse`.
*/
28 changes: 15 additions & 13 deletions src/common/services/BufferService.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
import { Disposable } from 'vs/base/common/lifecycle';
import { IAttributeData, IBufferLine } from 'common/Types';
import { BufferSet } from 'common/buffer/BufferSet';
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';
@@ -60,31 +61,32 @@ 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 (isWrapped) {
const oldLine = buffer.lines.get(buffer.ybase + buffer.y) as BufferLine;
newLine = new WrappedBufferLine(oldLine);
} else {
const bottom = buffer.lines.get(bottomRow);
newLine = LogicalBufferLine.makeAndTrim(this.cols, buffer.getNullCell(eraseAttr), bottom);
}
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.
const willBufferBeTrimmed = buffer.lines.isFull;

// 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 {
buffer.lines.push(newLine.clone());
buffer.lines.recycle(); // ignore result
buffer.lines.set(bottomRow, 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 +108,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
1 change: 1 addition & 0 deletions src/common/services/OptionsService.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
linkHandler: null,
logLevel: 'info',
logger: null,
newBufferLine: true,
scrollback: 1000,
scrollOnEraseInDisplay: false,
scrollOnUserInput: true,