From 0c5962c5387e919ef24acbb8a83de282986451ca Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 00:54:31 -0500 Subject: [PATCH] Added support for a basic link parser --- .../src/WebLinkProvider.ts | 349 +++++++++++++++ .../src/WebLinksAddon.ts | 8 +- addons/xterm-addon-web-links/src/charCode.ts | 420 ++++++++++++++++++ .../src/characterClassifier.ts | 117 +++++ .../src/renderLayer/LinkRenderLayer.ts | 3 + src/Terminal.ts | 31 +- src/TestUtils.test.ts | 28 +- src/Types.d.ts | 6 +- src/browser/Linkifier2.ts | 142 ++++++ src/browser/Types.d.ts | 31 ++ src/browser/renderer/LinkRenderLayer.ts | 6 +- src/public/Terminal.ts | 13 +- src/renderer/Renderer.ts | 4 +- src/renderer/dom/DomRenderer.ts | 145 +++--- typings/xterm.d.ts | 85 ++++ 15 files changed, 1283 insertions(+), 105 deletions(-) create mode 100644 addons/xterm-addon-web-links/src/WebLinkProvider.ts create mode 100644 addons/xterm-addon-web-links/src/charCode.ts create mode 100644 addons/xterm-addon-web-links/src/characterClassifier.ts create mode 100644 src/browser/Linkifier2.ts diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts new file mode 100644 index 000000000..ec43b3113 --- /dev/null +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -0,0 +1,349 @@ +import { ILinkProvider, IBufferCellPosition, ILink, Terminal, IBuffer } from 'xterm'; +import { CharCode } from './charCode'; +import { CharacterClassifier } from './characterClassifier'; + + +export default class WebLinkProvider implements ILinkProvider { + + constructor( + private readonly _terminal: Terminal, + private readonly _handler: (event: MouseEvent, uri: string) => void + ) { + + } + + provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { + const link = LinkComputer.computeLink(position, this._terminal.buffer); + + if (link) { + link.handle = this._handler; + } + + callback(link); + } +} + +export const enum State { + INVALID = 0, + START = 1, + H = 2, + HT = 3, + HTT = 4, + HTTP = 5, + F = 6, + FI = 7, + FIL = 8, + BEFORE_COLON = 9, + AFTER_COLON = 10, + ALMOST_THERE = 11, + END = 12, + ACCEPT = 13, + LAST_KNOWN_STATE = 14 // marker, custom states may follow +} + +export type Edge = [State, number, State]; + +export class Uint8Matrix { + + private readonly _data: Uint8Array; + public readonly rows: number; + public readonly cols: number; + + constructor(rows: number, cols: number, defaultValue: number) { + const data = new Uint8Array(rows * cols); + const len = rows * cols; + for (let i = 0; i < len; i++) { + data[i] = defaultValue; + } + + this._data = data; + this.rows = rows; + this.cols = cols; + } + + public get(row: number, col: number): number { + return this._data[row * this.cols + col]; + } + + public set(row: number, col: number, value: number): void { + this._data[row * this.cols + col] = value; + } +} + +export class StateMachine { + + private readonly _states: Uint8Matrix; + private readonly _maxCharCode: number; + + constructor(edges: Edge[]) { + let maxCharCode = 0; + let maxState = State.INVALID; + for (let i = 0; i < edges.length; i++) { + const [from, chCode, to] = edges[i]; + if (chCode > maxCharCode) { + maxCharCode = chCode; + } + if (from > maxState) { + maxState = from; + } + if (to > maxState) { + maxState = to; + } + } + + maxCharCode++; + maxState++; + + const states = new Uint8Matrix(maxState, maxCharCode, State.INVALID); + for (let i = 0; i < edges.length; i++) { + const [from, chCode, to] = edges[i]; + states.set(from, chCode, to); + } + + this._states = states; + this._maxCharCode = maxCharCode; + } + + public nextState(currentState: State, chCode: number): State { + if (chCode < 0 || chCode >= this._maxCharCode) { + return State.INVALID; + } + return this._states.get(currentState, chCode); + } +} + +// State machine for http:// or https:// or file:// +let stateMachine: StateMachine | null = null; +function getStateMachine(): StateMachine { + if (stateMachine === null) { + stateMachine = new StateMachine([ + [State.START, CharCode.h, State.H], + [State.START, CharCode.H, State.H], + [State.START, CharCode.f, State.F], + [State.START, CharCode.F, State.F], + + [State.H, CharCode.t, State.HT], + [State.H, CharCode.T, State.HT], + + [State.HT, CharCode.t, State.HTT], + [State.HT, CharCode.T, State.HTT], + + [State.HTT, CharCode.p, State.HTTP], + [State.HTT, CharCode.P, State.HTTP], + + [State.HTTP, CharCode.s, State.BEFORE_COLON], + [State.HTTP, CharCode.S, State.BEFORE_COLON], + [State.HTTP, CharCode.Colon, State.AFTER_COLON], + + [State.F, CharCode.i, State.FI], + [State.F, CharCode.I, State.FI], + + [State.FI, CharCode.l, State.FIL], + [State.FI, CharCode.L, State.FIL], + + [State.FIL, CharCode.e, State.BEFORE_COLON], + [State.FIL, CharCode.E, State.BEFORE_COLON], + + [State.BEFORE_COLON, CharCode.Colon, State.AFTER_COLON], + + [State.AFTER_COLON, CharCode.Slash, State.ALMOST_THERE], + + [State.ALMOST_THERE, CharCode.Slash, State.END] + ]); + } + return stateMachine; +} + + +const enum CharacterClass { + NONE = 0, + FORCE_TERMINATION = 1, + CANNOT_END_IN = 2 +} + +let classifier: CharacterClassifier | null = null; +function getClassifier(): CharacterClassifier { + if (classifier === null) { + classifier = new CharacterClassifier(CharacterClass.NONE); + + const FORCE_TERMINATION_CHARACTERS = ' \t<>\'\"、。。、,.:;?!@#$%&*‘“〈《「『【〔([{「」}])〕】』」》〉”’`~…'; + for (let i = 0; i < FORCE_TERMINATION_CHARACTERS.length; i++) { + classifier.set(FORCE_TERMINATION_CHARACTERS.charCodeAt(i), CharacterClass.FORCE_TERMINATION); + } + + const CANNOT_END_WITH_CHARACTERS = '.,;'; + for (let i = 0; i < CANNOT_END_WITH_CHARACTERS.length; i++) { + classifier.set(CANNOT_END_WITH_CHARACTERS.charCodeAt(i), CharacterClass.CANNOT_END_IN); + } + } + return classifier; +} + +export class LinkComputer { + + private static _createLink(classifier: CharacterClassifier, line: string, lineNumber: number, linkBeginIndex: number, linkEndIndex: number): ILink { + // Do not allow to end link in certain characters... + let lastIncludedCharIndex = linkEndIndex - 1; + do { + const chCode = line.charCodeAt(lastIncludedCharIndex); + const chClass = classifier.get(chCode); + if (chClass !== CharacterClass.CANNOT_END_IN) { + break; + } + lastIncludedCharIndex--; + } while (lastIncludedCharIndex > linkBeginIndex); + + // Handle links enclosed in parens, square brackets and curlys. + if (linkBeginIndex > 0) { + const charCodeBeforeLink = line.charCodeAt(linkBeginIndex - 1); + const lastCharCodeInLink = line.charCodeAt(lastIncludedCharIndex); + + if ( + (charCodeBeforeLink === CharCode.OpenParen && lastCharCodeInLink === CharCode.CloseParen) + || (charCodeBeforeLink === CharCode.OpenSquareBracket && lastCharCodeInLink === CharCode.CloseSquareBracket) + || (charCodeBeforeLink === CharCode.OpenCurlyBrace && lastCharCodeInLink === CharCode.CloseCurlyBrace) + ) { + // Do not end in ) if ( is before the link start + // Do not end in ] if [ is before the link start + // Do not end in } if { is before the link start + lastIncludedCharIndex--; + } + } + + return { + range: { + start: { + x: linkBeginIndex + 1, + y: lineNumber + }, + end: { + x: lastIncludedCharIndex + 2, + y: lineNumber + } + }, + url: line.substring(linkBeginIndex, lastIncludedCharIndex + 1), + showTooltip: (event: MouseEvent, link: string) => console.log('Show toolip for ' + link), + hideTooltip: (event: MouseEvent, link: string) => console.log('Hide tooltip for ' + link), + handle: (event: MouseEvent, link: string) => { } + }; + } + + public static computeLink(position: IBufferCellPosition, buffer: IBuffer): ILink | undefined { + const stateMachine: StateMachine = getStateMachine(); + const classifier = getClassifier(); + + const bufferLine = buffer.getLine(position.y - 1); + + if (!bufferLine) { + return; + } + + const line = bufferLine.translateToString(); + const len = line.length; + + const i = position.y; + + let linkBeginIndex = position.x - 1; + let state = State.START; + let hasOpenParens = false; + let hasOpenSquareBracket = false; + let hasOpenCurlyBracket = false; + + while (linkBeginIndex >= 0) { + let j = linkBeginIndex; + while (j < len) { + const linkBeginChCode = line.charCodeAt(j); + const chCode = line.charCodeAt(j); + + if (state === State.ACCEPT) { + let chClass: CharacterClass; + switch (chCode) { + case CharCode.OpenParen: + hasOpenParens = true; + chClass = CharacterClass.NONE; + break; + case CharCode.CloseParen: + chClass = (hasOpenParens ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); + break; + case CharCode.OpenSquareBracket: + hasOpenSquareBracket = true; + chClass = CharacterClass.NONE; + break; + case CharCode.CloseSquareBracket: + chClass = (hasOpenSquareBracket ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); + break; + case CharCode.OpenCurlyBrace: + hasOpenCurlyBracket = true; + chClass = CharacterClass.NONE; + break; + case CharCode.CloseCurlyBrace: + chClass = (hasOpenCurlyBracket ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); + break; + /* The following three rules make it that ' or " or ` are allowed inside links if the link began with a different one */ + case CharCode.SingleQuote: + chClass = (linkBeginChCode === CharCode.DoubleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; + break; + case CharCode.DoubleQuote: + chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; + break; + case CharCode.BackTick: + chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.DoubleQuote) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; + break; + case CharCode.Asterisk: + // `*` terminates a link if the link began with `*` + chClass = (linkBeginChCode === CharCode.Asterisk) ? CharacterClass.FORCE_TERMINATION : CharacterClass.NONE; + break; + default: + chClass = classifier.get(chCode); + } + + // Check if character terminates link + if (chClass === CharacterClass.FORCE_TERMINATION) { + return LinkComputer._createLink(classifier, line, i, linkBeginIndex, j); + } + } else if (state === State.END) { + + let chClass: CharacterClass; + if (chCode === CharCode.OpenSquareBracket) { + // Allow for the authority part to contain ipv6 addresses which contain [ and ] + hasOpenSquareBracket = true; + chClass = CharacterClass.NONE; + } else { + chClass = classifier.get(chCode); + } + + // Check if character terminates link + if (chClass === CharacterClass.FORCE_TERMINATION) { + return; + } + + state = State.ACCEPT; + } else { + state = stateMachine.nextState(state, chCode); + if (state === State.INVALID) { + // Two spaces in a row, return + if (chCode === CharCode.Space && j > 0 && line.charCodeAt(j - 1) === CharCode.Space) { + return; + } + + // Reset state machine + state = State.START; + hasOpenParens = false; + hasOpenSquareBracket = false; + hasOpenCurlyBracket = false; + + // Move to the left + linkBeginIndex--; + break; + } + } + + j++; + } + + if (state === State.ACCEPT) { + return LinkComputer._createLink(classifier, line, i, linkBeginIndex, len); + } + } + } +} diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index 26d5904b9..bc6becc89 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon, ILinkProvider } from 'xterm'; +import WebLinkProvider from './WebLinkProvider'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -32,6 +33,7 @@ function handleLink(event: MouseEvent, uri: string): void { export class WebLinksAddon implements ITerminalAddon { private _linkMatcherId: number | undefined; private _terminal: Terminal | undefined; + private _linkProvider: ILinkProvider | undefined; constructor( private _handler: (event: MouseEvent, uri: string) => void = handleLink, @@ -42,7 +44,9 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + this._linkProvider = new WebLinkProvider(this._terminal, this._handler); + // this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + this._terminal.registerLinkProvider(this._linkProvider); } public dispose(): void { diff --git a/addons/xterm-addon-web-links/src/charCode.ts b/addons/xterm-addon-web-links/src/charCode.ts new file mode 100644 index 000000000..f6724a73c --- /dev/null +++ b/addons/xterm-addon-web-links/src/charCode.ts @@ -0,0 +1,420 @@ +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030A, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030B, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030C, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030D, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030E, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030F, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031A, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031B, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031C, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031D, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031E, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031F, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032A, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032B, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032C, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032D, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032E, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032F, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033A, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033B, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033C, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033D, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033E, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033F, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034A, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034B, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034C, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034D, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034E, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034F, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035A, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035B, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035C, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035D, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035E, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035F, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036A, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036B, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036C, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036D, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036E, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036F, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR_2028 = 8232, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00A8, // U+00A8 DIAERESIS + U_MACRON = 0x00AF, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00B4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00B8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02C2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02C3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02C4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02C5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02D2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02D3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02D4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02D5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02D6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02D7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02D8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02D9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02DA, // U+02DA RING ABOVE + U_OGONEK = 0x02DB, // U+02DB OGONEK + U_SMALL_TILDE = 0x02DC, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02DD, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02DE, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02DF, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02E5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02E6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02E7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02E8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02E9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02EA, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02EB, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ED, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02EF, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02F0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02F1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02F2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02F3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02F4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02F5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02F6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02F7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02F8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02F9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02FA, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02FB, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02FC, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02FD, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02FE, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02FF, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1FBD, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1FBF, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1FC0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1FC1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1FCD, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1FCE, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1FCF, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1FDD, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1FDE, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1FDF, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1FED, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1FEE, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1FEF, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1FFD, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1FFE, // U+1FFE GREEK DASIA + + + U_OVERLINE = 0x203E, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279 +} diff --git a/addons/xterm-addon-web-links/src/characterClassifier.ts b/addons/xterm-addon-web-links/src/characterClassifier.ts new file mode 100644 index 000000000..6fb3324bb --- /dev/null +++ b/addons/xterm-addon-web-links/src/characterClassifier.ts @@ -0,0 +1,117 @@ +/** + * A fast character classifier that uses a compact array for ASCII values. + */ +export class CharacterClassifier { + /** + * Maintain a compact (fully initialized ASCII map for quickly classifying ASCII characters - used more often in code). + */ + private _asciiMap: Uint8Array; + + /** + * The entire map (sparse array). + */ + private _map: Map; + + private _defaultValue: number; + + constructor(_defaultValue: T) { + let defaultValue = toUint8(_defaultValue); + + this._defaultValue = defaultValue; + this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue); + this._map = new Map(); + } + + private static _createAsciiMap(defaultValue: number): Uint8Array { + let asciiMap: Uint8Array = new Uint8Array(256); + for (let i = 0; i < 256; i++) { + asciiMap[i] = defaultValue; + } + return asciiMap; + } + + public set(charCode: number, _value: T): void { + let value = toUint8(_value); + + if (charCode >= 0 && charCode < 256) { + this._asciiMap[charCode] = value; + } else { + this._map.set(charCode, value); + } + } + + public get(charCode: number): T { + if (charCode >= 0 && charCode < 256) { + return this._asciiMap[charCode]; + } else { + return (this._map.get(charCode) || this._defaultValue); + } + } +} + +const enum Boolean { + False = 0, + True = 1 +} + +export class CharacterSet { + + private readonly _actual: CharacterClassifier; + + constructor() { + this._actual = new CharacterClassifier(Boolean.False); + } + + public add(charCode: number): void { + this._actual.set(charCode, Boolean.True); + } + + public has(charCode: number): boolean { + return (this._actual.get(charCode) === Boolean.True); + } +} + +export const enum Constants { + /** + * MAX SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MAX_SAFE_SMALL_INTEGER = 1 << 30, + + /** + * MIN SMI (SMall Integer) as defined in v8. + * one bit is lost for boxing/unboxing flag. + * one bit is lost for sign flag. + * See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values + */ + MIN_SAFE_SMALL_INTEGER = -(1 << 30), + + /** + * Max unsigned integer that fits on 8 bits. + */ + MAX_UINT_8 = 255, // 2^8 - 1 + + /** + * Max unsigned integer that fits on 16 bits. + */ + MAX_UINT_16 = 65535, // 2^16 - 1 + + /** + * Max unsigned integer that fits on 32 bits. + */ + MAX_UINT_32 = 4294967295, // 2^32 - 1 + + UNICODE_SUPPLEMENTARY_PLANE_BEGIN = 0x010000 +} + +export function toUint8(v: number): number { + if (v < 0) { + return 0; + } + if (v > Constants.MAX_UINT_8) { + return Constants.MAX_UINT_8; + } + return v | 0; +} diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 118aedc31..0f7e3a11e 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -18,6 +18,9 @@ export class LinkRenderLayer extends BaseRenderLayer { super(container, 'link', zIndex, true, colors); terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); + + terminal.linkifier2.onShowTooltip(e => this._onLinkHover(e)); + terminal.linkifier2.onHideTooltip(e => this._onLinkLeave(e)); } public resize(terminal: Terminal, dim: IRenderDimensions): void { diff --git a/src/Terminal.ts b/src/Terminal.ts index 3e7e55ca9..fb49b10b8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -37,7 +37,7 @@ import * as Strings from 'browser/LocalizableStrings'; import { SoundService } from 'browser/services/SoundService'; import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; -import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; +import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; @@ -58,11 +58,12 @@ import { MouseService } from 'browser/services/MouseService'; import { IParams, IFunctionIdentifier } from 'common/parser/Types'; import { CoreService } from 'common/services/CoreService'; import { LogService } from 'common/services/LogService'; -import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types'; +import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; import { DirtyRowService } from 'common/services/DirtyRowService'; import { InstantiationService } from 'common/services/InstantiationService'; import { CoreMouseService } from 'common/services/CoreMouseService'; import { WriteBuffer } from 'common/input/WriteBuffer'; +import { Linkifier2 } from 'browser/Linkifier2'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -154,6 +155,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _inputHandler: InputHandler; public linkifier: ILinkifier; + public linkifier2: ILinkifier2; public viewport: IViewport; private _compositionHelper: ICompositionHelper; private _mouseZoneManager: IMouseZoneManager; @@ -248,7 +250,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._renderService.dispose(); } this._customKeyEventHandler = null; - this.write = () => {}; + this.write = () => { }; if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } @@ -290,6 +292,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this._inputHandler); this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); + this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); @@ -619,6 +622,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.register(this._mouseZoneManager); this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); + this.linkifier2.attachToDom(this.element, this._mouseService); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); @@ -719,8 +723,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } else { // according to MDN buttons only reports up to button 5 (AUX2) but = ev.buttons & 1 ? CoreMouseButton.LEFT : - ev.buttons & 4 ? CoreMouseButton.MIDDLE : - ev.buttons & 2 ? CoreMouseButton.RIGHT : + ev.buttons & 4 ? CoreMouseButton.MIDDLE : + ev.buttons & 2 ? CoreMouseButton.RIGHT : CoreMouseButton.NONE; // fallback to NONE } break; @@ -769,13 +773,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Note: 'mousedown' currently is "always on" and not managed * by onProtocolChange. */ - const requestedEvents: {[key: string]: ((ev: Event) => void) | null} = { + const requestedEvents: { [key: string]: ((ev: Event) => void) | null } = { mouseup: null, wheel: null, mousedrag: null, mousemove: null }; - const eventListeners: {[key: string]: (ev: Event) => void} = { + const eventListeners: { [key: string]: (ev: Event) => void } = { mouseup: (ev: MouseEvent) => { sendEvent(ev); if (!ev.buttons) { @@ -898,7 +902,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } // Construct and send sequences - const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + ( ev.deltaY < 0 ? 'A' : 'B'); + const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B'); let data = ''; for (let i = 0; i < Math.abs(amount); i++) { data += sequence; @@ -1166,6 +1170,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + if (!this.linkifier2) { + return; + } + return this.linkifier2.registerLinkProvider(linkProvider); + } + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { const joinerId = this._renderService.registerCharacterJoiner(handler); this.refresh(0, this.rows - 1); @@ -1324,8 +1335,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean { const thirdLevelKey = - (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); if (ev.type === 'keypress') { return thirdLevelKey; diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index a70f751c5..26d227d0c 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -9,10 +9,10 @@ import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types' import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; import * as Browser from 'common/Platform'; -import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; +import { IDisposable, IMarker, IEvent, ISelectionPosition, ILinkProvider } from 'xterm'; import { Terminal } from './Terminal'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types'; +import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport, ILinkifier2 } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { EventEmitter } from 'common/EventEmitter'; import { IParams, IFunctionIdentifier } from 'common/parser/Types'; @@ -91,6 +91,9 @@ export class MockTerminal implements ITerminal { deregisterLinkMatcher(matcherId: number): void { throw new Error('Method not implemented.'); } + registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + throw new Error('Method not implemented.'); + } hasSelection(): boolean { throw new Error('Method not implemented.'); } @@ -133,6 +136,7 @@ export class MockTerminal implements ITerminal { bracketedPasteMode: boolean; renderer: IRenderer; linkifier: ILinkifier; + linkifier2: ILinkifier2; isFocused: boolean; options: ITerminalOptions = {}; element: HTMLElement; @@ -384,16 +388,16 @@ export class MockRenderer implements IRenderer { setColors(colors: IColorSet): void { throw new Error('Method not implemented.'); } - onResize(cols: number, rows: number): void {} - onCharSizeChanged(): void {} - onBlur(): void {} - onFocus(): void {} - onSelectionChanged(start: [number, number], end: [number, number]): void {} - onCursorMove(): void {} - onOptionsChanged(): void {} - onDevicePixelRatioChange(): void {} - clear(): void {} - renderRows(start: number, end: number): void {} + onResize(cols: number, rows: number): void { } + onCharSizeChanged(): void { } + onBlur(): void { } + onFocus(): void { } + onSelectionChanged(start: [number, number], end: [number, number]): void { } + onCursorMove(): void { } + onOptionsChanged(): void { } + onDevicePixelRatioChange(): void { } + clear(): void { } + renderRows(start: number, end: number): void { } registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } deregisterCharacterJoiner(): boolean { return true; } } diff --git a/src/Types.d.ts b/src/Types.d.ts index 1b0285b12..088909f69 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm'; +import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm'; import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; -import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types'; +import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; import { IOptionsService } from 'common/services/Services'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IParams, IFunctionIdentifier } from 'common/parser/Types'; @@ -198,6 +198,7 @@ export interface IPublicTerminal extends IDisposable { addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): void; + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; registerCharacterJoiner(handler: (text: string) => [number, number][]): number; deregisterCharacterJoiner(joinerId: number): void; addMarker(cursorYOffset: number): IMarker; @@ -231,6 +232,7 @@ export interface IElementAccessor { export interface ILinkifierAccessor { linkifier: ILinkifier; + linkifier2: ILinkifier2; } // TODO: The options that are not in the public API should be reviewed diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts new file mode 100644 index 000000000..6ccfd5dae --- /dev/null +++ b/src/browser/Linkifier2.ts @@ -0,0 +1,142 @@ +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent } from './Types'; +import { IDisposable } from 'common/Types'; +import { IMouseService } from './services/Services'; +import { IBufferService } from 'common/services/Services'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; + +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +/** + */ +export class Linkifier2 implements ILinkifier2 { + private _element: HTMLElement | undefined; + private _linkProviders: ILinkProvider[] = []; + private _mouseService: IMouseService | undefined; + private _linkCache: ILinkCache[] = []; + + private _onShowTooltip = new EventEmitter(); + public get onShowTooltip(): IEvent { return this._onShowTooltip.event; } + private _onHideTooltip = new EventEmitter(); + public get onHideTooltip(): IEvent { return this._onHideTooltip.event; } + + constructor( + private readonly _bufferService: IBufferService + ) { + + } + + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + this._linkProviders.push(linkProvider); + return { dispose: () => console.log('disposing link providers') }; + } + + public attachToDom(element: HTMLElement, mouseService: IMouseService): void { + this._element = element; + this._mouseService = mouseService; + + this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); + this._element.addEventListener('click', this._onMouseDown.bind(this)); + } + + private _onMouseMove(event: MouseEvent): void { + const position = this._positionFromMouseEvent(event); + + if (!position) { + return; + } + + // Check the cache for a link and determine if we need to show or hide tooltip + let foundLink = false; + this._linkCache.forEach((cachedLink, i) => { + const isInPosition = this._linkAtPosition(cachedLink.link, position); + const range = cachedLink.link.range; + if (isInPosition && !cachedLink.mouseOver) { + // Show the tooltip + this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - 1, range.end.x - 1, range.end.y - 1, undefined)); + this._element!.classList.add('xterm-cursor-pointer'); + cachedLink.link.showTooltip(event, cachedLink.link.url); + + this._linkCache[i].mouseOver = true; + foundLink = true; + } else if (!isInPosition && cachedLink.mouseOver) { + // Hide the tooltip + this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - 1, range.end.x - 1, range.end.y - 1, undefined)); + this._element!.classList.remove('xterm-cursor-pointer'); + cachedLink.link.hideTooltip(event, cachedLink.link.url); + + this._linkCache[i].mouseOver = false; + } + }); + + if (foundLink) { + return; + } + + // The is no link in the cache, so ask for one + this._linkProviders.forEach(linkProvider => { + linkProvider.provideLink(position, this._handleNewLink.bind(this)); + }); + } + + private _onMouseDown(event: MouseEvent): void { + const position = this._positionFromMouseEvent(event); + + if (!position) { + return; + } + + this._linkCache.forEach((cachedLink, i) => { + if (this._linkAtPosition(cachedLink.link, position)) { + cachedLink.link.handle(event, cachedLink.link.url); + } + }); + } + + private _handleNewLink(link: ILink | undefined): void { + if (link && !this._linkCache.find(cachedLink => cachedLink.link = link)) { + this._linkCache.push({ link: link, mouseOver: false }); + } + } + + /** + * Check if the buffer position is within the link + * @param link + * @param position + */ + private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean { + return link.range.start.x <= position.x + && link.range.start.y <= position.y + && link.range.end.x >= position.x + && link.range.end.y >= position.y; + } + + /** + * Get the buffer position from a mouse event + * @param event + */ + private _positionFromMouseEvent(event: MouseEvent): IBufferCellPosition | undefined { + if (!this._element) { + return; + } + + const coords = this._mouseService!.getCoords(event, this._element, this._bufferService.cols, this._bufferService.rows); + if (!coords) { + return; + } + + return { x: coords[0], y: coords[1] + this._bufferService.buffer.ydisp }; + } + + private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { + return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; + } +} + +interface ILinkCache { + link: ILink; + mouseOver: boolean; +} diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 274fc16f2..58da702be 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -5,6 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IDisposable } from 'common/Types'; +import { IMouseService } from './services/Services'; export interface IColorManager { colors: IColorSet; @@ -93,6 +94,14 @@ export interface ILinkifier { deregisterLinkMatcher(matcherId: number): boolean; } +export interface ILinkifier2 { + onShowTooltip: IEvent; + onHideTooltip: IEvent; + + attachToDom(element: HTMLElement, mouseService: IMouseService): void + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; +} + export interface ILinkMatcherOptions { /** * The index of the link from the regex.match(text) call. This defaults to 0 @@ -143,3 +152,25 @@ export interface IMouseZone { leaveCallback: () => any | undefined; willLinkActivate: (e: MouseEvent) => boolean; } + +interface ILinkProvider { + provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void; +} + +interface ILink { + range: IBufferRange; + url: string; + showTooltip(event: MouseEvent, link: string): void; + hideTooltip(event: MouseEvent, link: string): void; + handle(event: MouseEvent, link: string): void; +} + +interface IBufferRange { + start: IBufferCellPosition; + end: IBufferCellPosition; +} + +interface IBufferCellPosition { + x: number; + y: number; +} diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index a7be54ee4..c7e2b1404 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -7,7 +7,7 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; -import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types'; +import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; import { IBufferService, IOptionsService } from 'common/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { @@ -19,12 +19,16 @@ export class LinkRenderLayer extends BaseRenderLayer { colors: IColorSet, rendererId: number, linkifier: ILinkifier, + linkifier2: ILinkifier2, readonly bufferService: IBufferService, readonly optionsService: IOptionsService ) { super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService); linkifier.onLinkHover(e => this._onLinkHover(e)); linkifier.onLinkLeave(e => this._onLinkLeave(e)); + + linkifier2.onShowTooltip(e => this._onLinkHover(e)); + linkifier2.onHideTooltip(e => this._onLinkLeave(e)); } public resize(dim: IRenderDimensions): void { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index c167bed89..5d56a78f8 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -67,6 +67,9 @@ export class Terminal implements ITerminalApi { public deregisterLinkMatcher(matcherId: number): void { this._core.deregisterLinkMatcher(matcherId); } + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + return this._core.registerLinkProvider(linkProvider); + } public registerCharacterJoiner(handler: (text: string) => [number, number][]): number { return this._core.registerCharacterJoiner(handler); } @@ -186,7 +189,7 @@ export class Terminal implements ITerminalApi { } class BufferApiView implements IBufferApi { - constructor(private _buffer: IBuffer) {} + constructor(private _buffer: IBuffer) { } public get cursorY(): number { return this._buffer.y; } public get cursorX(): number { return this._buffer.x; } @@ -203,7 +206,7 @@ class BufferApiView implements IBufferApi { } class BufferLineApiView implements IBufferLineApi { - constructor(private _line: IBufferLine) {} + constructor(private _line: IBufferLine) { } public get isWrapped(): boolean { return this._line.isWrapped; } public getCell(x: number): IBufferCellApi | undefined { @@ -218,13 +221,13 @@ class BufferLineApiView implements IBufferLineApi { } class BufferCellApiView implements IBufferCellApi { - constructor(private _line: IBufferLine, private _x: number) {} + constructor(private _line: IBufferLine, private _x: number) { } public get char(): string { return this._line.getString(this._x); } public get width(): number { return this._line.getWidth(this._x); } } class ParserApi implements IParser { - constructor(private _core: ITerminal) {} + constructor(private _core: ITerminal) { } public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable { return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray())); diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index d3977af37..7ae53d955 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -41,8 +41,8 @@ export class Renderer extends Disposable implements IRenderer { this._renderLayers = [ new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService), new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService), - new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService), - new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService) + new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, this._terminal.linkifier2, bufferService, optionsService), + new CursorRenderLayer(this._terminal.screenElement, 4, this._colors, this._terminal, this._id, bufferService, optionsService) ]; this.dimensions = { scaledCharWidth: null, diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts index ef927f208..83507b676 100644 --- a/src/renderer/dom/DomRenderer.ts +++ b/src/renderer/dom/DomRenderer.ts @@ -80,6 +80,9 @@ export class DomRenderer extends Disposable implements IRenderer { this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); + + this._terminal.linkifier2.onShowTooltip(e => this._onLinkHover(e)); + this._terminal.linkifier2.onHideTooltip(e => this._onLinkLeave(e)); } public dispose(): void { @@ -119,12 +122,12 @@ export class DomRenderer extends Disposable implements IRenderer { } const styles = - `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` + - ` display: inline-block;` + - ` height: 100%;` + - ` vertical-align: top;` + - ` width: ${this.dimensions.actualCellWidth}px` + - `}`; + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` + + ` display: inline-block;` + + ` height: 100%;` + + ` vertical-align: top;` + + ` width: ${this.dimensions.actualCellWidth}px` + + `}`; this._dimensionsStyleElement.innerHTML = styles; @@ -146,85 +149,85 @@ export class DomRenderer extends Disposable implements IRenderer { // Base CSS let styles = - `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + - ` color: ${this._colors.foreground.css};` + - ` background-color: ${this._colors.background.css};` + - ` font-family: ${this._terminal.options.fontFamily};` + - ` font-size: ${this._terminal.options.fontSize}px;` + - `}`; + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + + ` color: ${this._colors.foreground.css};` + + ` background-color: ${this._colors.background.css};` + + ` font-family: ${this._terminal.options.fontFamily};` + + ` font-size: ${this._terminal.options.fontSize}px;` + + `}`; // Text styles styles += - `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + - ` font-weight: ${this._terminal.options.fontWeight};` + - `}` + - `${this._terminalSelector} span.${BOLD_CLASS} {` + - ` font-weight: ${this._terminal.options.fontWeightBold};` + - `}` + - `${this._terminalSelector} span.${ITALIC_CLASS} {` + - ` font-style: italic;` + - `}`; + `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + + ` font-weight: ${this._terminal.options.fontWeight};` + + `}` + + `${this._terminalSelector} span.${BOLD_CLASS} {` + + ` font-weight: ${this._terminal.options.fontWeightBold};` + + `}` + + `${this._terminalSelector} span.${ITALIC_CLASS} {` + + ` font-style: italic;` + + `}`; // Blink animation styles += - `@keyframes blink_box_shadow {` + - ` 50% {` + - ` box-shadow: none;` + - ` }` + - `}`; + `@keyframes blink_box_shadow {` + + ` 50% {` + + ` box-shadow: none;` + + ` }` + + `}`; styles += - `@keyframes blink_block {` + - ` 0% {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + - ` }` + - ` 50% {` + - ` background-color: ${this._colors.cursorAccent.css};` + - ` color: ${this._colors.cursor.css};` + - ` }` + - `}`; + `@keyframes blink_block {` + + ` 0% {` + + ` background-color: ${this._colors.cursor.css};` + + ` color: ${this._colors.cursorAccent.css};` + + ` }` + + ` 50% {` + + ` background-color: ${this._colors.cursorAccent.css};` + + ` color: ${this._colors.cursor.css};` + + ` }` + + `}`; // Cursor styles += - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` outline: 1px solid ${this._colors.cursor.css};` + - ` outline-offset: -1px;` + - `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + - ` animation: blink_box_shadow 1s step-end infinite;` + - `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` animation: blink_block 1s step-end infinite;` + - `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + - ` background-color: ${this._colors.cursor.css};` + - ` color: ${this._colors.cursorAccent.css};` + - `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + - ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + - `}` + - `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + - ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + - `}`; + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + + ` outline: 1px solid ${this._colors.cursor.css};` + + ` outline-offset: -1px;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` + + ` animation: blink_box_shadow 1s step-end infinite;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + + ` animation: blink_block 1s step-end infinite;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` + + ` background-color: ${this._colors.cursor.css};` + + ` color: ${this._colors.cursorAccent.css};` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + + ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + + ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + + `}`; // Selection styles += - `${this._terminalSelector} .${SELECTION_CLASS} {` + - ` position: absolute;` + - ` top: 0;` + - ` left: 0;` + - ` z-index: 1;` + - ` pointer-events: none;` + - `}` + - `${this._terminalSelector} .${SELECTION_CLASS} div {` + - ` position: absolute;` + - ` background-color: ${this._colors.selection.css};` + - `}`; + `${this._terminalSelector} .${SELECTION_CLASS} {` + + ` position: absolute;` + + ` top: 0;` + + ` left: 0;` + + ` z-index: 1;` + + ` pointer-events: none;` + + `}` + + `${this._terminalSelector} .${SELECTION_CLASS} div {` + + ` position: absolute;` + + ` background-color: ${this._colors.selection.css};` + + `}`; // Colors this._colors.ansi.forEach((c, i) => { styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + - `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; + `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; }); styles += - `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + - `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; this._themeStyleElement.innerHTML = styles; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 8ed3caa2b..079b6439e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -539,6 +539,13 @@ declare module 'xterm' { */ deregisterLinkMatcher(matcherId: number): void; + /** + * (EXPERIMENTAL) Registers a link provider, allowing a custom parser to + * be used to match and handle links. + * @param linkProvider + */ + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; + /** * (EXPERIMENTAL) Registers a character joiner, allowing custom sequences of * characters to be rendered as a single unit. This is useful in particular @@ -887,6 +894,84 @@ declare module 'xterm' { row: number; } + /** + * A custom link provider + */ + interface ILinkProvider { + /** + * Provides a link a buffer position + * @param position + * @param callback + */ + provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void; + } + + /** + * A link + */ + interface ILink { + /** + * The buffer range of the link + */ + range: IBufferRange; + + /** + * The url of the link + */ + url: string; + + /** + * The show tooltip callback + * @param event + * @param link + */ + showTooltip(event: MouseEvent, link: string): void; + + /** + * The hide tooltip callback + * @param event + * @param link + */ + hideTooltip(event: MouseEvent, link: string): void; + + /** + * Handles when the link is opened + * @param event + * @param link + */ + handle(event: MouseEvent, link: string): void; + } + + /** + * A range in the buffer + */ + interface IBufferRange { + /** + * The start position of the range + */ + start: IBufferCellPosition; + + /** + * The end position of the range + */ + end: IBufferCellPosition; + } + + /** + * A position in the buffer + */ + interface IBufferCellPosition { + /** + * The x of the buffer position + */ + x: number; + + /** + * The y of the buffer position + */ + y: number; + } + /** * Represents a terminal buffer. */