Files
xterm.js/src/browser/services/CharacterJoinerService.test.ts
T
Cursor AgentandDaniel Imms 5070a08b58 Move Color to base; slim services/Types; colocate ICoreTerminal
- Restore Color.ts under common/base/ (not services)
- Add base/ColorTypes (IColor, IColorRGB)
- Add terminal/CoreTerminalTypes (ICoreTerminal, IScrollEvent, XtermListener)
- Add terminal/InputHandlerTypes and input/WindowOptions for handler types
- services/Types.ts: service-only (modes, mouse, osc link, cursor styles)
- Update imports across src, addons, browser; re-export KeyboardResultType removed
- browser tsconfig: exclude unit tests from composite build

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-30 17:45:15 +00:00

299 lines
8.8 KiB
TypeScript

/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import type { IBufferLine } from 'common/buffer/CellTypes';
import { CellData } from 'common/buffer/CellData';
import { MockBufferService, createCellData } from 'common/terminal/TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
describe('CharacterJoinerService', () => {
let service: ICharacterJoinerService;
beforeEach(() => {
const bufferService = new MockBufferService(16, 10);
const lines = bufferService.buffer.lines;
lines.set(0, lineData([['a -> b -> c -> d']]));
lines.set(1, lineData([['a -> b => c -> d']]));
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
lines.set(3, lineData([['no joined ranges']]));
lines.set(4, new BufferLine(TEST_STRING_CACHE, 0));
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
const line6 = lineData([['wi']]);
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
line6.resize(line6.length + 1, createCellData(0, '', 0));
let sub = lineData([['deemo']]);
let oldSize = line6.length;
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
line6.resize(line6.length + 1, CellData.fromCharData([0, '\xf0\x9f\x98\x81', 1, 128513]));
line6.resize(line6.length + 1, createCellData(0, ' ', 1));
sub = lineData([['jiabc']]);
oldSize = line6.length;
line6.resize(oldSize + sub.length, createCellData(0, '', 0));
for (let i = 0; i < sub.length; ++i) line6.setCell(i + oldSize, sub.loadCell(i, new CellData()));
lines.set(6, line6);
service = new CharacterJoinerService(bufferService);
});
it('has no joiners upon creation', () => {
assert.deepEqual(service.getJoinedCharacters(0), []);
});
it('returns ranges matched by the registered joiners', () => {
service.register(substringJoiner('->'));
assert.deepEqual(
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('processes the input using all provided joiners', () => {
service.register(substringJoiner('->'));
assert.deepEqual(
service.getJoinedCharacters(1),
[[2, 4], [12, 14]]
);
service.register(substringJoiner('=>'));
assert.deepEqual(
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
});
it('removes deregistered joiners from future calls', () => {
const joiner1 = service.register(substringJoiner('->'));
const joiner2 = service.register(substringJoiner('=>'));
assert.deepEqual(
service.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
service.deregister(joiner1);
assert.deepEqual(
service.getJoinedCharacters(1),
[[7, 9]]
);
service.deregister(joiner2);
assert.deepEqual(
service.getJoinedCharacters(1),
[]
);
});
it('doesn\'t process joins on differently-styled characters', () => {
service.register(substringJoiner('->'));
assert.deepEqual(
service.getJoinedCharacters(2),
[[2, 4], [12, 14]]
);
});
it('returns an empty list of ranges if there is nothing to be joined', () => {
service.register(substringJoiner('->'));
assert.deepEqual(
service.getJoinedCharacters(3),
[]
);
});
it('returns an empty list of ranges if the line is empty', () => {
service.register(substringJoiner('->'));
assert.deepEqual(
service.getJoinedCharacters(4),
[]
);
});
it('returns false when trying to deregister a joiner that does not exist', () => {
service.register(substringJoiner('->'));
assert.deepEqual(service.deregister(123), false);
assert.deepEqual(
service.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});
it('doesn\'t process same-styled ranges that only have one character', () => {
service.register(substringJoiner('a'));
service.register(substringJoiner('b'));
service.register(substringJoiner('d'));
assert.deepEqual(
service.getJoinedCharacters(5),
[[5, 6]]
);
});
it('handles ranges that extend all the way to the end of the line', () => {
service.register(substringJoiner('-> d'));
assert.deepEqual(
service.getJoinedCharacters(2),
[[12, 16]]
);
});
it('handles adjacent ranges', () => {
service.register(substringJoiner('->'));
service.register(substringJoiner('> c '));
assert.deepEqual(
service.getJoinedCharacters(2),
[[2, 4], [8, 12], [12, 14]]
);
});
it('handles fullwidth characters in the middle of ranges', () => {
service.register(substringJoiner('wi¥de'));
assert.deepEqual(
service.getJoinedCharacters(6),
[[0, 6]]
);
});
it('handles fullwidth characters at the end of ranges', () => {
service.register(substringJoiner('wi¥'));
assert.deepEqual(
service.getJoinedCharacters(6),
[[0, 4]]
);
});
it('handles emojis in the middle of ranges', () => {
service.register(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
assert.deepEqual(
service.getJoinedCharacters(6),
[[6, 13]]
);
});
it('handles emojis at the end of ranges', () => {
service.register(substringJoiner('emo\xf0\x9f\x98\x81 '));
assert.deepEqual(
service.getJoinedCharacters(6),
[[6, 11]]
);
});
it('handles ranges after wide and emoji characters', () => {
service.register(substringJoiner('abc'));
assert.deepEqual(
service.getJoinedCharacters(6),
[[13, 16]]
);
});
describe('range merging', () => {
it('inserts a new range before the existing ones', () => {
service.register(() => [[1, 2], [2, 3]]);
service.register(() => [[0, 1]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 1], [1, 2], [2, 3]]
);
});
it('inserts in between two ranges', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[2, 4]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 2], [2, 4], [4, 6]]
);
});
it('inserts after the last range', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[6, 8]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 2], [4, 6], [6, 8]]
);
});
it('extends the beginning of a range', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[3, 5]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 2], [3, 6]]
);
});
it('extends the end of a range', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 4]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 4], [4, 6]]
);
});
it('extends the last range', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[5, 7]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 2], [4, 7]]
);
});
it('connects two ranges', () => {
service.register(() => [[0, 2], [4, 6]]);
service.register(() => [[1, 5]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 6]]
);
});
it('connects more than two ranges', () => {
service.register(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
service.register(() => [[1, 10]]);
assert.deepEqual(
service.getJoinedCharacters(0),
[[0, 10], [12, 14]]
);
});
});
});
type IPartialLineData = ([string] | [string, number]);
function lineData(data: IPartialLineData[]): IBufferLine {
const tline = new BufferLine(TEST_STRING_CACHE, 0);
for (let i = 0; i < data.length; ++i) {
const line = data[i][0];
const attr = (data[i][1] || 0) as number;
const offset = tline.length;
tline.resize(tline.length + line.split('').length, createCellData(0, '', 0));
line.split('').map((char, idx) => tline.setCell(idx + offset, createCellData(attr, char, 1)));
}
return tline;
}
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
return (sequence: string): [number, number][] => {
const ranges: [number, number][] = [];
let searchIndex = 0;
let matchIndex;
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
const matchEndIndex = matchIndex + substring.length;
searchIndex = matchEndIndex;
ranges.push([matchIndex, matchEndIndex]);
}
return ranges;
};
}