Skip to content

Commit

Permalink
add ability to register/deregister character joiner
Browse files Browse the repository at this point in the history
  • Loading branch information
princjef committed Jun 2, 2018
1 parent c192a27 commit eabb25c
Show file tree
Hide file tree
Showing 9 changed files with 355 additions and 15 deletions.
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 { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData } from './Types';
import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharData, LineData, CharacterJoinerHandler } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
Expand Down Expand Up @@ -1373,6 +1373,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 @@ -17,6 +17,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
41 changes: 37 additions & 4 deletions src/renderer/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ 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, ICharacterJoiner } from './Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
Expand All @@ -23,6 +23,8 @@ export class Renderer extends EventEmitter implements IRenderer {
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
private _joiners: ICharacterJoiner[] = [];
private _nextJoinerId: number = 0;

public colorManager: ColorManager;
public dimensions: IRenderDimensions;
Expand Down Expand Up @@ -66,7 +68,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);
}
}
Expand Down Expand Up @@ -186,7 +188,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 });
}

/**
Expand Down Expand Up @@ -248,4 +250,35 @@ 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 {
const joiner: ICharacterJoiner = {
id: this._nextJoinerId++,
handler: handler
};

this._renderLayers.forEach(l => {
if (l.registerCharacterJoiner) {
l.registerCharacterJoiner(joiner);
}
});

return joiner.id;
}

public deregisterCharacterJoiner(joinerId: number): boolean {
for (let i = 0; i < this._joiners.length; i++) {
if (this._joiners[i].id === joinerId) {
this._joiners.splice(i, 1);
this._renderLayers.forEach(l => {
if (l.deregisterCharacterJoiner) {
l.deregisterCharacterJoiner(joinerId);
}
});
return true;
}
}

return false;
}
}
153 changes: 145 additions & 8 deletions src/renderer/TextRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
*/

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, ICharacterJoiner } from './Types';
import { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
import { merge } from '../utils/MergeRanges';

/**
* This CharData looks like a null character, which will forc a clear and render
Expand All @@ -22,6 +23,7 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterWidth: number;
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
private _joiners: ICharacterJoiner[] = [];

constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
Expand Down Expand Up @@ -52,6 +54,7 @@ export class TextRenderLayer extends BaseRenderLayer {
terminal: ITerminal,
firstRow: number,
lastRow: number,
foreground: boolean,
callback: (
code: number,
char: string,
Expand All @@ -66,10 +69,12 @@ 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);
let index = 0;
const joinedRanges = foreground ? this._getJoinedCharacters(terminal, row) : [];
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
let charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
let char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
let width: number = charData[CHAR_DATA_WIDTH_INDEX];

Expand All @@ -79,6 +84,41 @@ export class TextRenderLayer extends BaseRenderLayer {
continue;
}

// Just in case we ended up in the middle of a range, lop off any
// ranges that have already passed
while (joinedRanges.length > 0 && joinedRanges[0][0] < x) {
joinedRanges.shift();
}

// 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 lastCharX = x;
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
const range = joinedRanges.shift();

// We need to start the searching at the next character
lastCharX++;
index++;

// Build up the string
for (; lastCharX < terminal.cols && index < range[1]; lastCharX++) {
charData = line[lastCharX];
if (charData[CHAR_DATA_WIDTH_INDEX] !== 0) {
char += charData[CHAR_DATA_CHAR_INDEX];
index++;
}
}

// Update our data accordingly. We use the width of the last character
// for the rest of the checks and decrement our column/index so that
// they align with the last character matched rather than the next
// character in the sequence.
width = charData[CHAR_DATA_WIDTH_INDEX];
lastCharX--;
index--;
}

// 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)) {
Expand All @@ -89,7 +129,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
Expand All @@ -116,7 +156,19 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}

callback(code, char, width, x, y, fg, bg, flags);
callback(
char.length === 1 ? code : Infinity,
char,
char.length + width - 1,
x,
y,
fg,
bg,
flags
);

x = lastCharX;
index++;
}
}
}
Expand All @@ -134,7 +186,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, false, (code, char, 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
Expand Down Expand Up @@ -176,7 +228,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, true, (code, char, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
Expand All @@ -190,7 +242,7 @@ 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(
Expand Down Expand Up @@ -219,6 +271,19 @@ export class TextRenderLayer extends BaseRenderLayer {
this.setTransparency(terminal, terminal.options.allowTransparency);
}

public registerCharacterJoiner(joiner: ICharacterJoiner): void {
this._joiners.push(joiner);
}

public deregisterCharacterJoiner(joinerId: number): void {
for (let i = 0; i < this._joiners.length; i++) {
if (this._joiners[i].id === joinerId) {
this._joiners.splice(i, 1);
return;
}
}
}

/**
* Whether a character is overlapping to the next cell.
*/
Expand Down Expand Up @@ -258,6 +323,78 @@ export class TextRenderLayer extends BaseRenderLayer {
return overlaps;
}

private _getJoinedCharacters(terminal: ITerminal, row: number): [number, number][] {
if (this._joiners.length === 0) {
return [];
}

const line = terminal.buffer.lines.get(row);
if (line.length === 0) {
return [];
}

const ranges: [number, number][] = [];
const lineStr = terminal.buffer.translateBufferLineToString(row, true);

let currentIndex = 0;
let rangeStartIndex = 0;
let rangeAttr = line[0][CHAR_DATA_ATTR_INDEX] >> 9;
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
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 (currentIndex - rangeStartIndex > 1) {
const subRanges = this._getSubRanges(lineStr, rangeStartIndex, currentIndex);
for (let i = 0; i < subRanges.length; i++) {
ranges.push(subRanges[i]);
}
}

// Reset our markers for a new range
rangeStartIndex = x;
rangeAttr = attr;
}

currentIndex++;
}

// Process any trailing ranges
if (currentIndex - rangeStartIndex > 1) {
const subRanges = this._getSubRanges(lineStr, rangeStartIndex, currentIndex);
for (let i = 0; i < subRanges.length; i++) {
ranges.push(subRanges[i]);
}
}

return ranges;
}

private _getSubRanges(line: string, startIndex: number, endIndex: 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 subRanges: [number, number][] = this._joiners[0].handler(text);
for (let i = 1; i < this._joiners.length; i++) {
// We merge any overlapping ranges across the different joiners
const joinerSubRanges = this._joiners[i].handler(text);
for (let j = 0; j < joinerSubRanges.length; j++) {
merge(subRanges, joinerSubRanges[j]);
}
}
return subRanges.map<[number, number]>(range => [range[0] + startIndex, range[1] + endIndex]);
}

/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
Expand Down
19 changes: 18 additions & 1 deletion src/renderer/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { ITerminal } from '../Types';
import { ITerminal, CharacterJoinerHandler } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IColorSet } from '../shared/Types';

Expand Down Expand Up @@ -35,6 +35,8 @@ export interface IRenderer extends IEventEmitter {
onOptionsChanged(): void;
clear(): void;
refreshRows(start: number, end: number): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}

export interface IColorManager {
Expand Down Expand Up @@ -96,6 +98,16 @@ export interface IRenderLayer {
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): 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.
*/
Expand All @@ -106,3 +118,8 @@ export interface IRenderLayer {
*/
reset(terminal: ITerminal): void;
}

export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
Loading

0 comments on commit eabb25c

Please sign in to comment.