mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #1460 from princjef/character-joiner
Add optional character joiner
This commit is contained in:
+13
-1
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -224,12 +224,12 @@ 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.
|
||||
@@ -237,33 +237,33 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* 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';
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { ITerminal, LineData } from '../Types';
|
||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||
|
||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
|
||||
private _characterJoiners: ICharacterJoiner[] = [];
|
||||
private _nextCharacterJoinerId: number = 0;
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
const joiner: ICharacterJoiner = {
|
||||
id: this._nextCharacterJoinerId++,
|
||||
handler
|
||||
};
|
||||
|
||||
this._characterJoiners.push(joiner);
|
||||
return joiner.id;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
for (let i = 0; i < this._characterJoiners.length; i++) {
|
||||
if (this._characterJoiners[i].id === joinerId) {
|
||||
this._characterJoiners.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getJoinedCharacters(row: number): [number, number][] {
|
||||
if (this._characterJoiners.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(row);
|
||||
if (line.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ranges: [number, number][] = [];
|
||||
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
|
||||
|
||||
// Because some cells can be represented by multiple javascript characters,
|
||||
// we track the cell and the string indexes separately. This allows us to
|
||||
// translate the string ranges we get from the joiners back into cell ranges
|
||||
// for use when rendering
|
||||
let rangeStartColumn = 0;
|
||||
let currentStringIndex = 0;
|
||||
let rangeStartStringIndex = 0;
|
||||
let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
for (let x = 0; x < this._terminal.cols; x++) {
|
||||
const charData = line[x];
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
if (width === 0) {
|
||||
// If this character is of width 0, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of range
|
||||
if (attr !== rangeAttr) {
|
||||
// If we ended up with a sequence of more than one character,
|
||||
// look for ranges to join.
|
||||
if (x - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our markers for a new range.
|
||||
rangeStartColumn = x;
|
||||
rangeStartStringIndex = currentStringIndex;
|
||||
rangeAttr = attr;
|
||||
}
|
||||
|
||||
currentStringIndex += chars.length;
|
||||
}
|
||||
|
||||
// Process any trailing ranges.
|
||||
if (this._terminal.cols - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a segment of a line of text, find all ranges of text that should be
|
||||
* joined in a single rendering unit. Ranges are internally converted to
|
||||
* column ranges, rather than string ranges.
|
||||
* @param line String representation of the full line of text
|
||||
* @param startIndex Start position of the range to search in the string (inclusive)
|
||||
* @param endIndex End position of the range to search in the string (exclusive)
|
||||
*/
|
||||
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: LineData, startCol: number): [number, number][] {
|
||||
const text = line.substring(startIndex, endIndex);
|
||||
// At this point we already know that there is at least one joiner so
|
||||
// we can just pull its value and assign it directly rather than
|
||||
// merging it into an empty array, which incurs unnecessary writes.
|
||||
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
|
||||
for (let i = 1; i < this._characterJoiners.length; i++) {
|
||||
// We merge any overlapping ranges across the different joiners
|
||||
const joinerRanges = this._characterJoiners[i].handler(text);
|
||||
for (let j = 0; j < joinerRanges.length; j++) {
|
||||
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
|
||||
}
|
||||
}
|
||||
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
|
||||
return joinedRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the provided ranges in-place to adjust for variations between
|
||||
* string length and cell width so that the range represents a cell range,
|
||||
* rather than the string range the joiner provides.
|
||||
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
||||
* @param line Cell data for the relevant line in the terminal
|
||||
* @param startCol Offset within the line to start from
|
||||
*/
|
||||
private _stringRangesToCellRanges(ranges: [number, number][], line: LineData, startCol: number): void {
|
||||
let currentRangeIndex = 0;
|
||||
let currentRangeStarted = false;
|
||||
let currentStringIndex = 0;
|
||||
let currentRange = ranges[currentRangeIndex];
|
||||
|
||||
// If we got through all of the ranges, stop searching
|
||||
if (!currentRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||
const charData = line[x];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||
|
||||
// We skip zero-width characters when creating the string to join the text
|
||||
// so we do the same here
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust the start of the range
|
||||
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
}
|
||||
|
||||
// Adjust the end of the range
|
||||
if (currentRange[1] <= currentStringIndex) {
|
||||
currentRange[1] = x;
|
||||
|
||||
// We're finished with this range, so we move to the next one
|
||||
currentRange = ranges[++currentRangeIndex];
|
||||
|
||||
// If there are no more ranges left, stop searching
|
||||
if (!currentRange) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Ranges can be on adjacent characters. Because the end index of the
|
||||
// ranges are exclusive, this means that the index for the start of a
|
||||
// range can be the same as the end index of the previous range. To
|
||||
// account for the start of the next range, we check here just in case.
|
||||
if (currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
} else {
|
||||
currentRangeStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the string index based on the character length to line up with
|
||||
// the column adjustment
|
||||
currentStringIndex += length;
|
||||
}
|
||||
|
||||
// If there is still a range left at the end, it must extend all the way to
|
||||
// the end of the line.
|
||||
if (currentRange) {
|
||||
currentRange[1] = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
* @param ranges Existing range list
|
||||
* @param newRange Tuple of two numbers representing the new range to merge in.
|
||||
* @returns The ranges input with the new range merged in place
|
||||
*/
|
||||
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, newRange);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[0] < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
inRange = true;
|
||||
}
|
||||
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
} else {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRange[1];
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRange[1];
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push(newRange);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,14 @@ import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||
import { ITheme } from 'xterm';
|
||||
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
|
||||
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
@@ -23,6 +24,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
@@ -31,12 +33,13 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
this.colorManager = new ColorManager(document, allowTransparency);
|
||||
this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal);
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._characterJoinerRegistry, allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
@@ -67,7 +70,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
|
||||
observer.observe(this._terminal.element);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
@@ -188,7 +191,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
*/
|
||||
private _renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
this._terminal.emit('refresh', {start, end});
|
||||
this._terminal.emit('refresh', { start, end });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,4 +253,12 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
|
||||
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -22,10 +22,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterWidth: number;
|
||||
private _characterFont: string;
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
@@ -52,9 +54,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
terminal: ITerminal,
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
code: number,
|
||||
char: string,
|
||||
chars: string,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -66,22 +69,54 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line[x];
|
||||
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
|
||||
// Can either represent character(s) for a single cell or multiple cells
|
||||
// if indicated by a character joiner.
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by
|
||||
// the char at x-1
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the right is a
|
||||
// space, take ownership of the cell to the right.
|
||||
if (this._isOverlapping(charData)) {
|
||||
// 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.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift();
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
chars = terminal.buffer.translateBufferLineToString(
|
||||
row,
|
||||
true,
|
||||
range[0],
|
||||
range[1]
|
||||
);
|
||||
width = range[1] - range[0];
|
||||
code = Infinity;
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the
|
||||
// right is a space, take ownership of the cell to the right. We skip
|
||||
// this check for joined characters because their rendering likely won't
|
||||
// yield the same result as rendering the last character individually.
|
||||
if (!isJoined && this._isOverlapping(charData)) {
|
||||
// If the character is overlapping, we want to force a re-render on every
|
||||
// frame. This is specifically to work around the case where two
|
||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||
@@ -89,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// get removed, and `a` would not re-render because it thinks it's
|
||||
// already in the correct state.
|
||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
||||
if (lastCharX < line.length - 1 && line[lastCharX + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
||||
width = 2;
|
||||
// this._clearChar(x + 1, y);
|
||||
// The overlapping char's char data will force a clear and render when the
|
||||
@@ -116,7 +151,18 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
callback(code, char, width, x, y, fg, bg, flags);
|
||||
callback(
|
||||
code,
|
||||
chars,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
fg,
|
||||
bg,
|
||||
flags
|
||||
);
|
||||
|
||||
x = lastCharX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +180,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
@@ -176,7 +222,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
if (flags & FLAGS.INVISIBLE) {
|
||||
return;
|
||||
}
|
||||
@@ -190,11 +236,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this.fillBottomLineAtCells(x, y, width);
|
||||
this._ctx.restore();
|
||||
}
|
||||
this.drawChar(
|
||||
terminal, char, code,
|
||||
this.drawChars(
|
||||
terminal, chars, code,
|
||||
width, x, y,
|
||||
fg, bg,
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||
|
||||
+24
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
||||
import { IColorSet } from '../shared/Types';
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface IRenderer extends IEventEmitter, IDisposable {
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
refreshRows(start: number, end: number): void;
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
@@ -101,6 +103,16 @@ export interface IRenderLayer {
|
||||
*/
|
||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
*/
|
||||
@@ -111,3 +123,14 @@ export interface IRenderLayer {
|
||||
*/
|
||||
reset(terminal: ITerminal): void;
|
||||
}
|
||||
|
||||
export interface ICharacterJoiner {
|
||||
id: number;
|
||||
handler: CharacterJoinerHandler;
|
||||
}
|
||||
|
||||
export interface ICharacterJoinerRegistry {
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ interface IGlyphCacheValue {
|
||||
|
||||
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
|
||||
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`;
|
||||
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.chars}`;
|
||||
}
|
||||
|
||||
export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
@@ -216,7 +216,7 @@ export default class DynamicCharAtlas extends BaseCharAtlas {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.char, 0, 0);
|
||||
this._tmpCtx.fillText(glyph.chars, 0, 0);
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
|
||||
@@ -7,7 +7,7 @@ export const INVERTED_DEFAULT_COLOR = -1;
|
||||
export const DIM_OPACITY = 0.5;
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
char: string;
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
|
||||
import { ITerminal } from '../../Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../../Types';
|
||||
import { ITheme } from 'xterm';
|
||||
import { EventEmitter } from '../../EventEmitter';
|
||||
import { ColorManager } from '../ColorManager';
|
||||
@@ -319,4 +319,7 @@ export class DomRenderer extends EventEmitter implements IRenderer {
|
||||
private get _terminalSelector(): string {
|
||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return -1; }
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean { return false; }
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
|
||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener, CharacterJoinerHandler } from '../Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from '../shared/utils/Browser';
|
||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||
@@ -156,6 +156,8 @@ export class MockTerminal implements ITerminal {
|
||||
}
|
||||
return line;
|
||||
}
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
deregisterCharacterJoiner(joinerId: number): void { }
|
||||
}
|
||||
|
||||
export class MockCharMeasure implements ICharMeasure {
|
||||
@@ -344,6 +346,8 @@ export class MockRenderer implements IRenderer {
|
||||
onWindowResize(devicePixelRatio: number): void {}
|
||||
clear(): void {}
|
||||
refreshRows(start: number, end: number): void {}
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
deregisterCharacterJoiner(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
|
||||
Vendored
+38
@@ -470,6 +470,44 @@ declare module 'xterm' {
|
||||
*/
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of
|
||||
* characters to be rendered as a single unit. This is useful in particular
|
||||
* for rendering ligatures and graphemes, among other things.
|
||||
*
|
||||
* Each registered character joiner is called with a string of text
|
||||
* representing a portion of a line in the terminal that can be rendered as
|
||||
* a single unit. The joiner must return a sorted array, where each entry is
|
||||
* itself an array of length two, containing the start (inclusive) and end
|
||||
* (exclusive) index of a substring of the input that should be rendered as
|
||||
* a single unit. When multiple joiners are provided, the results of each
|
||||
* are collected. If there are any overlapping substrings between them, they
|
||||
* are combined into one larger unit that is drawn together.
|
||||
*
|
||||
* All character joiners that are registered get called every time a line is
|
||||
* rendered in the terminal, so it is essential for the handler function to
|
||||
* run as quickly as possible to avoid slowdowns when rendering. Similarly,
|
||||
* joiners should strive to return the smallest possible substrings to
|
||||
* render together, since they aren't drawn as optimally as individual
|
||||
* characters.
|
||||
*
|
||||
* NOTE: character joiners are only used by the canvas renderer.
|
||||
*
|
||||
* @param handler The function that determines character joins. It is called
|
||||
* with a string of text that is eligible for joining and returns an array
|
||||
* where each entry is an array containing the start (inclusive) and end
|
||||
* (exclusive) indexes of ranges that should be rendered as a single unit.
|
||||
* @return The ID of the new joiner, this can be used to deregister
|
||||
*/
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Deregisters the character joiner if one was registered.
|
||||
* NOTE: character joiners are only used by the canvas renderer.
|
||||
* @param joinerId The character joiner's ID (returned after register)
|
||||
*/
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the
|
||||
* alt buffer is active, undefined is returned.
|
||||
|
||||
Reference in New Issue
Block a user