Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 13 additions & 1 deletion src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* http://linux.die.net/man/7/urxvt
*/

import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types';
import { IMouseZoneManager } from './ui/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
Expand Down Expand Up @@ -1374,6 +1374,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}

public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this.renderer.registerCharacterJoiner(handler);
this.refresh(0, this.rows - 1);
return joinerId;
}

public deregisterCharacterJoiner(joinerId: number): void {
if (this.renderer.deregisterCharacterJoiner(joinerId)) {
this.refresh(0, this.rows - 1);
}
}

public get markers(): IMarker[] {
return this.buffer.markers;
}
Expand Down
2 changes: 2 additions & 0 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;

export type CharacterJoinerHandler = (text: string) => [number, number][];

export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
Expand Down
6 changes: 6 additions & 0 deletions src/public/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export class Terminal implements ITerminalApi {
public deregisterLinkMatcher(matcherId: number): void {
this._core.deregisterLinkMatcher(matcherId);
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
return this._core.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): void {
this._core.deregisterCharacterJoiner(joinerId);
}
public addMarker(cursorYOffset: number): IMarker {
return this._core.addMarker(cursorYOffset);
}
Expand Down
26 changes: 13 additions & 13 deletions src/renderer/BaseRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,46 +224,46 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}

/**
* Draws a character at a cell. If possible this will draw using the character
* atlas to reduce draw time.
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* @param terminal The terminal.
* @param char The character.
* @param chars The character or characters.
* @param code The character code.
* @param width The width of the character.
* @param width The width of the characters.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
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 {
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 {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
fg += drawInBrightColor ? 8 : 0;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);

if (!atlasDidDraw) {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
}
}

/**
* Draws a character at a cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character.
* Draws one or more characters at one or more cells. The character(s) will be
* clipped to ensure that they fit with the cell(s), including the cell to the
* right if the last character is a wide character.
* @param terminal The terminal.
* @param char The character.
* @param chars The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
this._ctx.save();
this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'top';
Expand All @@ -285,7 +285,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
// Draw the character
this._ctx.fillText(
char,
chars,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop);
this._ctx.restore();
Expand Down
278 changes: 278 additions & 0 deletions src/renderer/CharacterJoinerRegistry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { assert } from 'chai';

import { LineData, CharData } from '../Types';
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
import { CircularList } from '../common/CircularList';

import { ICharacterJoinerRegistry } from './Types';
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';

describe('CharacterJoinerRegistry', () => {
let registry: ICharacterJoinerRegistry;

beforeEach(() => {
const terminal = new MockTerminal();
terminal.cols = 16;
terminal.buffer = new MockBuffer();
const lines = new CircularList<LineData>(7);
lines.set(0, lineData('a -> b -> c -> d'));
lines.set(1, lineData('a -> b => c -> d'));
lines.set(2, [...lineData('a -> b -', 0xFFFFFFFF), ...lineData('> c -> d', 0)]);
lines.set(3, lineData('no joined ranges'));
lines.set(4, []);
lines.set(5, [...lineData('a', 0x11111111), ...lineData(' -> b -> c -> '), ...lineData('d', 0x22222222)]);
lines.set(6, [
...lineData('wi'),
[0, '¥', 2, '¥'.charCodeAt(0)],
[0, '', 0, null],
...lineData('deemo'),
[0, '\xf0\x9f\x98\x81', 1, 128513],
[0, ' ', 1, ' '.charCodeAt(0)],
...lineData('jiabc')
]);
(<MockBuffer>terminal.buffer).setLines(lines);
terminal.buffer.ydisp = 0;
registry = new CharacterJoinerRegistry(terminal);
});

it('has no joiners upon creation', () => {
assert.deepEqual(registry.getJoinedCharacters(0), []);
});

it('returns ranges matched by the registered joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});

it('processes the input using all provided joiners', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [12, 14]]
);

registry.registerCharacterJoiner(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);
});

it('removes deregistered joiners from future calls', () => {
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
assert.deepEqual(
registry.getJoinedCharacters(1),
[[2, 4], [7, 9], [12, 14]]
);

registry.deregisterCharacterJoiner(joiner1);
assert.deepEqual(
registry.getJoinedCharacters(1),
[[7, 9]]
);

registry.deregisterCharacterJoiner(joiner2);
assert.deepEqual(
registry.getJoinedCharacters(1),
[]
);
});

it('doesn\'t process joins on differently-styled characters', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[2, 4], [12, 14]]
);
});

it('returns an empty list of ranges if there is nothing to be joined', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(3),
[]
);
});

it('returns an empty list of ranges if the line is empty', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(
registry.getJoinedCharacters(4),
[]
);
});

it('returns false when trying to deregister a joiner that does not exist', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[2, 4], [7, 9], [12, 14]]
);
});

it('doesn\'t process same-styled ranges that only have one character', () => {
registry.registerCharacterJoiner(substringJoiner('a'));
registry.registerCharacterJoiner(substringJoiner('b'));
registry.registerCharacterJoiner(substringJoiner('d'));
assert.deepEqual(
registry.getJoinedCharacters(5),
[[5, 6]]
);
});

it('handles ranges that extend all the way to the end of the line', () => {
registry.registerCharacterJoiner(substringJoiner('-> d'));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[12, 16]]
);
});

it('handles adjacent ranges', () => {
registry.registerCharacterJoiner(substringJoiner('->'));
registry.registerCharacterJoiner(substringJoiner('> c '));
assert.deepEqual(
registry.getJoinedCharacters(2),
[[2, 4], [8, 12], [12, 14]]
);
});

it('handles fullwidth characters in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[0, 6]]
);
});

it('handles fullwidth characters at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('wi¥'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[0, 4]]
);
});

it('handles emojis in the middle of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[6, 13]]
);
});

it('handles emojis at the end of ranges', () => {
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[6, 11]]
);
});

it('handles ranges after wide and emoji characters', () => {
registry.registerCharacterJoiner(substringJoiner('abc'));
assert.deepEqual(
registry.getJoinedCharacters(6),
[[13, 16]]
);
});

describe('range merging', () => {
it('inserts a new range before the existing ones', () => {
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
registry.registerCharacterJoiner(() => [[0, 1]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 1], [1, 2], [2, 3]]
);
});

it('inserts in between two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[2, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [2, 4], [4, 6]]
);
});

it('inserts after the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[6, 8]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [4, 6], [6, 8]]
);
});

it('extends the beginning of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[3, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [3, 6]]
);
});

it('extends the end of a range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 4]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 4], [4, 6]]
);
});

it('extends the last range', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[5, 7]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 2], [4, 7]]
);
});

it('connects two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
registry.registerCharacterJoiner(() => [[1, 5]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 6]]
);
});

it('connects more than two ranges', () => {
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
registry.registerCharacterJoiner(() => [[1, 10]]);
assert.deepEqual(
registry.getJoinedCharacters(0),
[[0, 10], [12, 14]]
);
});
});
});

function lineData(line: string, attr: number = 0): LineData {
return line.split('').map<CharData>(char => [attr, char, 1, char.charCodeAt(0)]);
}

function substringJoiner(substring: string): (sequence: string) => [number, number][] {
return (sequence: string): [number, number][] => {
const ranges: [number, number][] = [];
let searchIndex = 0;
let matchIndex = -1;

while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
const matchEndIndex = matchIndex + substring.length;
searchIndex = matchEndIndex;
ranges.push([matchIndex, matchEndIndex]);
}

return ranges;
};
}
Loading