Skip to content

Commit

Permalink
Merge aa6708c into 990bf5c
Browse files Browse the repository at this point in the history
  • Loading branch information
jerch committed Sep 28, 2018
2 parents 990bf5c + aa6708c commit 15227ee
Show file tree
Hide file tree
Showing 10 changed files with 917 additions and 9 deletions.
7 changes: 5 additions & 2 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ function initOptions(term: TerminalType): void {
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
rendererType: ['dom', 'canvas']
rendererType: ['dom', 'canvas'],
unicodeVersion: (term as any)._core.unicodeProvider.registeredVersions()
};
const options = Object.keys((<any>term)._core.options);
const booleanOptions = [];
Expand All @@ -219,7 +220,9 @@ function initOptions(term: TerminalType): void {
booleanOptions.push(o);
break;
case 'number':
numberOptions.push(o);
if (o !== 'unicodeVersion') {
numberOptions.push(o);
}
break;
default:
if (Object.keys(stringOptions).indexOf(o) === -1) {
Expand Down
2 changes: 1 addition & 1 deletion src/Buffer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ describe('Buffer', () => {
});

it('fullwidth combining with emoji - match emoji cell', () => {
const input = 'Lots of ¥\u0301 make me 😃.';
const input = 'Lots of ¥\u0301 make me very 😃.';
terminal.writeSync(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
Expand Down
4 changes: 2 additions & 2 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
Expand Down Expand Up @@ -364,7 +363,8 @@ export class InputHandler extends Disposable implements IInputHandler {

// calculate print space
// expensive call, therefore we save width in line buffer
chWidth = wcwidth(code);
// chWidth = wcwidth(code);
chWidth = this._terminal.unicodeProvider.wcwidth(code);

// get charset replacement character
if (charset) {
Expand Down
6 changes: 4 additions & 2 deletions src/Linkifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes,
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './common/EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
import { getStringCellWidth } from './CharWidth';

/**
* The Linkifier applies links to rows shortly after they have been refreshed.
Expand Down Expand Up @@ -256,7 +255,10 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param fg The link color for hover event.
*/
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
const width = getStringCellWidth(uri);
// FIXME: to make runtime changes of the unicode version possible
// this may not rely on getStringCellWidth anymore
// instead sum widths saved in the buffer
const width = (this._terminal as any).unicodeProvider.getStringCellWidth(uri);
const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + width) % this._terminal.cols;
Expand Down
7 changes: 6 additions & 1 deletion src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { IKeyboardEvent } from './common/Types';
import { evaluateKeyboardEvent } from './core/input/Keyboard';
import { KeyboardResultType, ICharset } from './core/Types';
import { BufferLine } from './BufferLine';
import { UnicodeProvider } from './UnicodeProvider';

// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
Expand Down Expand Up @@ -106,7 +107,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
tabStopWidth: 8,
theme: null,
rightClickSelectsWord: Browser.isMac,
rendererType: 'canvas'
rendererType: 'canvas',
unicodeVersion: 11
};

export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal {
Expand Down Expand Up @@ -194,6 +196,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
private _userScrolling: boolean;

private _inputHandler: InputHandler;
public unicodeProvider: UnicodeProvider;
public soundManager: SoundManager;
public renderer: IRenderer;
public selectionManager: SelectionManager;
Expand Down Expand Up @@ -300,6 +303,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// this._writeStopped = false;
this._userScrolling = false;

this.unicodeProvider = new UnicodeProvider();
this._inputHandler = new InputHandler(this);
this.register(this._inputHandler);
// Reuse renderer if the Terminal is being recreated via a reset call.
Expand Down Expand Up @@ -493,6 +497,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'unicodeVersion': this.unicodeProvider.setActiveVersion(parseFloat(value)); break;
}
// Inform renderer of changes
if (this.renderer) {
Expand Down
18 changes: 18 additions & 0 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
handleTitle(title: string): void;
index(): void;
reverseIndex(): void;
unicodeProvider: IUnicodeProvider;
}

export interface IViewport extends IDisposable {
Expand Down Expand Up @@ -523,3 +524,20 @@ export interface IBufferLine {
deleteCells(pos: number, n: number, fill: CharData): void;
replaceCells(start: number, end: number, fill: CharData): void;
}

/**
* Interface for unicode version implementations.
*/
export interface IUnicodeImplementation {
version: number;
wcwidth(ucs: number): number;
}

export interface IUnicodeProvider {
onRegister(callback: (version: number, provider: IUnicodeProvider) => void): void;
registeredVersions(): number[];
getActiveVersion(): number;
setActiveVersion(version: number, mode?: 'exact' | 'closest' | 'next' | 'previous'): number;
wcwidth(ucs: number): number;
getStringCellWidth(s: string): number;
}
147 changes: 147 additions & 0 deletions src/UnicodeProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IUnicodeImplementation, IUnicodeProvider } from './Types';
import { v6 } from './unicode/v6';
import { v11 } from './unicode/v11';

/**
* Class to provide access to different unicode version implementations.
*
* The version related implementations are stored statically
* to avoid recreating them for every single instance.
*
* An instance of this class is meant to serve unicode specific implementations
* for a single terminal instance. This way multiple terminals can have
* different unicode settings active while still referring to the
* same underlying implementations.
*/
export class UnicodeProvider implements IUnicodeProvider {
static versions: {[key: string]: IUnicodeImplementation} = {};
private static _registerCallbacks: ((version: number) => void)[] = [];

static onRegister(callback: (version: number) => void): void {
UnicodeProvider._registerCallbacks.push(callback);
}

/**
* Register an unicode implementation.
* Possible entry point for unicode addons.
* In conjuction with `onRegister` it can be used
* to load implementations lazy.
*/
static registerVersion(impl: IUnicodeImplementation): void {
if (UnicodeProvider.versions[impl.version]) {
throw new Error(`unicode version "${impl.version}" already registered`);
}
UnicodeProvider.versions[impl.version] = impl;
UnicodeProvider._registerCallbacks.forEach(cb => cb(impl.version));
}

static registeredVersions(): number[] {
return Object.getOwnPropertyNames(UnicodeProvider.versions).map(parseFloat).sort((a, b) => a - b);
}

private _version: number;
public wcwidth: (ucs: number) => number;

// defaults to the highest available version
constructor(version: number = 20) {
this.setActiveVersion(version);
}

/**
* Callback to run when a version got registered.
* Gets the newly registered version and
* the `UnicodeProvider` instance as arguments.
*/
public onRegister(callback: (version: number, provider: UnicodeProvider) => void): void {
UnicodeProvider.onRegister((version) => callback(version, this));
}

/**
* Get a list of currently registered unicode versions.
*/
public registeredVersions(): number[] {
return Object.getOwnPropertyNames(UnicodeProvider.versions).map(parseFloat).sort((a, b) => a - b);
}

/**
* Get the currently active unicode version.
*/
public getActiveVersion(): number {
return this._version;
}

/**
* Activate a registered unicode version. By default the closest version will be activated
* (can be higher or lower). Setting `mode` to 'next' tries to get at least that version,
* 'previous' tries to get the closest lower version.
* Unless there is no version registered this method will always succeed.
* Returns the activated version number.
*/
public setActiveVersion(version: number, mode?: 'exact' | 'closest' | 'next' | 'previous'): number {
if (!this.registeredVersions().length) {
throw new Error('no unicode versions registered');
}

// find closest matching version
// Although not quite correct for typical versioning schemes 5.9 is treated closer to 6.0 than to 5.7.
// Typically we will not ship subversions so this approximation should be close enough.
const versions = this.registeredVersions();
const distances = versions.map(el => Math.abs(version - el));
const closestIndex = distances.reduce((iMin, x, i, arr) => x < arr[iMin] ? i : iMin, 0);
let newVersion = versions[closestIndex];

if (mode === 'exact') {
// exact version match requested
if (version !== newVersion) {
throw new Error(`unicode version "${version}" not registered`);
}
} else {
// take the higher one if available
if (mode === 'next') {
if (newVersion < version && closestIndex < versions.length - 1) {
newVersion = versions[closestIndex + 1];
}
// take the lower one if available
} else if (mode === 'previous') {
if (newVersion > version && closestIndex) {
newVersion = versions[closestIndex - 1];
}
}
}

// swap wcwidth impl
this.wcwidth = UnicodeProvider.versions[newVersion].wcwidth;
this._version = newVersion;
return this._version;
}

/**
* Get the terminal cell width for a string.
*/
public getStringCellWidth(s: string): number {
let result = 0;
for (let i = 0; i < s.length; ++i) {
let code = s.charCodeAt(i);
if (0xD800 <= code && code <= 0xDBFF) {
const low = s.charCodeAt(i + 1);
if (isNaN(low)) {
return result;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
result += this.wcwidth(code);
}
return result;
}
}

// register statically shipped versions
UnicodeProvider.registerVersion(v6);
UnicodeProvider.registerVersion(v11);
Loading

0 comments on commit 15227ee

Please sign in to comment.