Skip to content

Commit 0adf426

Browse files
committed
Factor logic into registry, make matches cell-based and document
1 parent 5e3ebbd commit 0adf426

11 files changed

Lines changed: 648 additions & 290 deletions

src/renderer/BaseRenderLayer.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -224,46 +224,46 @@ export abstract class BaseRenderLayer implements IRenderLayer {
224224
}
225225

226226
/**
227-
* Draws a character at a cell. If possible this will draw using the character
228-
* atlas to reduce draw time.
227+
* Draws one or more characters at a cell. If possible this will draw using
228+
* the character atlas to reduce draw time.
229229
* @param terminal The terminal.
230-
* @param char The character.
230+
* @param chars The character or characters.
231231
* @param code The character code.
232-
* @param width The width of the character.
232+
* @param width The width of the characters.
233233
* @param x The column to draw at.
234234
* @param y The row to draw at.
235235
* @param fg The foreground color, in the format stored within the attributes.
236236
* @param bg The background color, in the format stored within the attributes.
237237
* This is used to validate whether a cached image can be used.
238238
* @param bold Whether the text is bold.
239239
*/
240-
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
240+
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
241241
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
242242
fg += drawInBrightColor ? 8 : 0;
243243
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
244244
this._ctx,
245-
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
245+
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
246246
x * this._scaledCellWidth + this._scaledCharLeft,
247247
y * this._scaledCellHeight + this._scaledCharTop
248248
);
249249

250250
if (!atlasDidDraw) {
251-
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
251+
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
252252
}
253253
}
254254

255255
/**
256-
* Draws a character at a cell. The character will be clipped to
257-
* ensure that it fits with the cell, including the cell to the right if it's
258-
* a wide character.
256+
* Draws one or more characters at one or more cells. The character(s) will be
257+
* clipped to ensure that they fit with the cell(s), including the cell to the
258+
* right if the last character is a wide character.
259259
* @param terminal The terminal.
260-
* @param char The character.
260+
* @param chars The character.
261261
* @param width The width of the character.
262262
* @param fg The foreground color, in the format stored within the attributes.
263263
* @param x The column to draw at.
264264
* @param y The row to draw at.
265265
*/
266-
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
266+
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
267267
this._ctx.save();
268268
this._ctx.font = this._getFont(terminal, bold, italic);
269269
this._ctx.textBaseline = 'top';
@@ -285,7 +285,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
285285
}
286286
// Draw the character
287287
this._ctx.fillText(
288-
char,
288+
chars,
289289
x * this._scaledCellWidth + this._scaledCharLeft,
290290
y * this._scaledCellHeight + this._scaledCharTop);
291291
this._ctx.restore();
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { assert } from 'chai';
2+
3+
import { LineData, CharData } from '../Types';
4+
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
5+
import { CircularList } from '../common/CircularList';
6+
7+
import { ICharacterJoinerRegistry } from './Types';
8+
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
9+
10+
describe('CharacterJoinerRegistry', () => {
11+
let registry: ICharacterJoinerRegistry;
12+
13+
beforeEach(() => {
14+
const terminal = new MockTerminal();
15+
terminal.cols = 16;
16+
terminal.buffer = new MockBuffer();
17+
const lines = new CircularList<LineData>(7);
18+
lines.set(0, lineData('a -> b -> c -> d'));
19+
lines.set(1, lineData('a -> b => c -> d'));
20+
lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]);
21+
lines.set(3, lineData('no joined ranges'));
22+
lines.set(4, []);
23+
lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]);
24+
lines.set(6, [
25+
...lineData('wi'),
26+
[0, '¥', 2, '¥'.charCodeAt(0)],
27+
[0, '', 0, null],
28+
...lineData('deemo'),
29+
[0, '\xf0\x9f\x98\x81', 1, 128513],
30+
[0, ' ', 1, ' '.charCodeAt(0)],
31+
...lineData('jiabc')
32+
]);
33+
(<MockBuffer>terminal.buffer).setLines(lines);
34+
terminal.buffer.ydisp = 0;
35+
registry = new CharacterJoinerRegistry(terminal);
36+
});
37+
38+
it('has no joiners upon creation', () => {
39+
assert.deepEqual(registry.getJoinedCharacters(0), []);
40+
});
41+
42+
it('returns ranges matched by the registered joiners', () => {
43+
registry.registerCharacterJoiner(substringJoiner('->'));
44+
assert.deepEqual(
45+
registry.getJoinedCharacters(0),
46+
[[2, 4], [7, 9], [12, 14]]
47+
);
48+
});
49+
50+
it('processes the input using all provided joiners', () => {
51+
registry.registerCharacterJoiner(substringJoiner('->'));
52+
assert.deepEqual(
53+
registry.getJoinedCharacters(1),
54+
[[2, 4], [12, 14]]
55+
);
56+
57+
registry.registerCharacterJoiner(substringJoiner('=>'));
58+
assert.deepEqual(
59+
registry.getJoinedCharacters(1),
60+
[[2, 4], [7, 9], [12, 14]]
61+
);
62+
});
63+
64+
it('removes deregistered joiners from future calls', () => {
65+
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
66+
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
67+
assert.deepEqual(
68+
registry.getJoinedCharacters(1),
69+
[[2, 4], [7, 9], [12, 14]]
70+
);
71+
72+
registry.deregisterCharacterJoiner(joiner1);
73+
assert.deepEqual(
74+
registry.getJoinedCharacters(1),
75+
[[7, 9]]
76+
);
77+
78+
registry.deregisterCharacterJoiner(joiner2);
79+
assert.deepEqual(
80+
registry.getJoinedCharacters(1),
81+
[]
82+
);
83+
});
84+
85+
it('doesn\'t process joins on differently-styled characters', () => {
86+
registry.registerCharacterJoiner(substringJoiner('->'));
87+
assert.deepEqual(
88+
registry.getJoinedCharacters(2),
89+
[[2, 4], [12, 14]]
90+
);
91+
});
92+
93+
it('returns an empty list of ranges if there is nothing to be joined', () => {
94+
registry.registerCharacterJoiner(substringJoiner('->'));
95+
assert.deepEqual(
96+
registry.getJoinedCharacters(3),
97+
[]
98+
);
99+
});
100+
101+
it('returns an empty list of ranges if the line is empty', () => {
102+
registry.registerCharacterJoiner(substringJoiner('->'));
103+
assert.deepEqual(
104+
registry.getJoinedCharacters(4),
105+
[]
106+
);
107+
});
108+
109+
it('returns false when trying to deregister a joiner that does not exist', () => {
110+
registry.registerCharacterJoiner(substringJoiner('->'));
111+
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
112+
assert.deepEqual(
113+
registry.getJoinedCharacters(0),
114+
[[2, 4], [7, 9], [12, 14]]
115+
);
116+
});
117+
118+
it('doesn\'t process same-styled ranges that only have one character', () => {
119+
registry.registerCharacterJoiner(substringJoiner('a'));
120+
registry.registerCharacterJoiner(substringJoiner('b'));
121+
registry.registerCharacterJoiner(substringJoiner('d'));
122+
assert.deepEqual(
123+
registry.getJoinedCharacters(5),
124+
[[5, 6]]
125+
);
126+
});
127+
128+
it('handles ranges that extend all the way to the end of the line', () => {
129+
registry.registerCharacterJoiner(substringJoiner('-> d'));
130+
assert.deepEqual(
131+
registry.getJoinedCharacters(2),
132+
[[12, 16]]
133+
);
134+
});
135+
136+
it('handles adjacent ranges', () => {
137+
registry.registerCharacterJoiner(substringJoiner('->'));
138+
registry.registerCharacterJoiner(substringJoiner('> c '));
139+
assert.deepEqual(
140+
registry.getJoinedCharacters(2),
141+
[[2, 4], [8, 12], [12, 14]]
142+
);
143+
});
144+
145+
it('handles fullwidth characters in the middle of ranges', () => {
146+
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
147+
assert.deepEqual(
148+
registry.getJoinedCharacters(6),
149+
[[0, 6]]
150+
);
151+
});
152+
153+
it('handles fullwidth characters at the end of ranges', () => {
154+
registry.registerCharacterJoiner(substringJoiner('wi¥'));
155+
assert.deepEqual(
156+
registry.getJoinedCharacters(6),
157+
[[0, 4]]
158+
);
159+
});
160+
161+
it('handles emojis in the middle of ranges', () => {
162+
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
163+
assert.deepEqual(
164+
registry.getJoinedCharacters(6),
165+
[[6, 13]]
166+
);
167+
});
168+
169+
it('handles emojis at the end of ranges', () => {
170+
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
171+
assert.deepEqual(
172+
registry.getJoinedCharacters(6),
173+
[[6, 11]]
174+
);
175+
});
176+
177+
it('handles ranges after wide and emoji characters', () => {
178+
registry.registerCharacterJoiner(substringJoiner('abc'));
179+
assert.deepEqual(
180+
registry.getJoinedCharacters(6),
181+
[[13, 16]]
182+
);
183+
});
184+
185+
describe('range merging', () => {
186+
it('inserts a new range before the existing ones', () => {
187+
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
188+
registry.registerCharacterJoiner(() => [[0, 1]]);
189+
assert.deepEqual(
190+
registry.getJoinedCharacters(0),
191+
[[0, 1], [1, 2], [2, 3]]
192+
);
193+
});
194+
195+
it('inserts in between two ranges', () => {
196+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
197+
registry.registerCharacterJoiner(() => [[2, 4]]);
198+
assert.deepEqual(
199+
registry.getJoinedCharacters(0),
200+
[[0, 2], [2, 4], [4, 6]]
201+
);
202+
});
203+
204+
it('inserts after the last range', () => {
205+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
206+
registry.registerCharacterJoiner(() => [[6, 8]]);
207+
assert.deepEqual(
208+
registry.getJoinedCharacters(0),
209+
[[0, 2], [4, 6], [6, 8]]
210+
);
211+
});
212+
213+
it('extends the beginning of a range', () => {
214+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
215+
registry.registerCharacterJoiner(() => [[3, 5]]);
216+
assert.deepEqual(
217+
registry.getJoinedCharacters(0),
218+
[[0, 2], [3, 6]]
219+
);
220+
});
221+
222+
it('extends the end of a range', () => {
223+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
224+
registry.registerCharacterJoiner(() => [[1, 4]]);
225+
assert.deepEqual(
226+
registry.getJoinedCharacters(0),
227+
[[0, 4], [4, 6]]
228+
);
229+
});
230+
231+
it('extends the last range', () => {
232+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
233+
registry.registerCharacterJoiner(() => [[5, 7]]);
234+
assert.deepEqual(
235+
registry.getJoinedCharacters(0),
236+
[[0, 2], [4, 7]]
237+
);
238+
});
239+
240+
it('connects two ranges', () => {
241+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
242+
registry.registerCharacterJoiner(() => [[1, 5]]);
243+
assert.deepEqual(
244+
registry.getJoinedCharacters(0),
245+
[[0, 6]]
246+
);
247+
});
248+
249+
it('connects more than two ranges', () => {
250+
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
251+
registry.registerCharacterJoiner(() => [[1, 10]]);
252+
assert.deepEqual(
253+
registry.getJoinedCharacters(0),
254+
[[0, 10], [12, 14]]
255+
);
256+
});
257+
});
258+
});
259+
260+
function lineData(line: string, attr: number = 0): LineData {
261+
return line.split('').map<CharData>(char => [attr, char, 1, char.charCodeAt(0)]);
262+
}
263+
264+
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
265+
return (sequence: string): [number, number][] => {
266+
const ranges: [number, number][] = [];
267+
let searchIndex = 0;
268+
let matchIndex = -1;
269+
270+
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
271+
const matchEndIndex = matchIndex + substring.length;
272+
searchIndex = matchEndIndex;
273+
ranges.push([matchIndex, matchEndIndex]);
274+
}
275+
276+
return ranges;
277+
};
278+
}

0 commit comments

Comments
 (0)