From 0c5962c5387e919ef24acbb8a83de282986451ca Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 00:54:31 -0500 Subject: [PATCH 01/26] 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 00000000..ec43b311 --- /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 26d5904b..bc6becc8 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 00000000..f6724a73 --- /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 00000000..6fb3324b --- /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 118aedc3..0f7e3a11 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 3e7e55ca..fb49b10b 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 a70f751c..26d227d0 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 1b0285b1..088909f6 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 00000000..6ccfd5da --- /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 274fc16f..58da702b 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 a7be54ee..c7e2b140 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 c167bed8..5d56a78f 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 d3977af3..7ae53d95 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 ef927f20..83507b67 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 8ed3caa2..079b6439 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. */ From 3e2e4ace48245b23affacea60c9db62cb28c1f37 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 01:05:18 -0500 Subject: [PATCH 02/26] Removed file detection from WebLinkProvider --- .../src/WebLinkProvider.ts | 32 ++++--------------- src/browser/Linkifier2.ts | 10 ++++-- src/browser/Types.d.ts | 4 +-- typings/xterm.d.ts | 4 +-- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index ec43b311..db03c0a9 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -13,11 +13,7 @@ export default class WebLinkProvider implements ILinkProvider { } provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { - const link = LinkComputer.computeLink(position, this._terminal.buffer); - - if (link) { - link.handle = this._handler; - } + const link = LinkComputer.computeLink(position, this._terminal.buffer, this._handler); callback(link); } @@ -30,9 +26,6 @@ export const enum State { HT = 3, HTT = 4, HTTP = 5, - F = 6, - FI = 7, - FIL = 8, BEFORE_COLON = 9, AFTER_COLON = 10, ALMOST_THERE = 11, @@ -119,8 +112,6 @@ function getStateMachine(): StateMachine { 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], @@ -135,15 +126,6 @@ function getStateMachine(): StateMachine { [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], @@ -181,7 +163,7 @@ function getClassifier(): CharacterClassifier { export class LinkComputer { - private static _createLink(classifier: CharacterClassifier, line: string, lineNumber: number, linkBeginIndex: number, linkEndIndex: number): ILink { + private static _createLink(classifier: CharacterClassifier, line: string, lineNumber: number, linkBeginIndex: number, linkEndIndex: number, handler: (event: MouseEvent, link: string) => void): ILink { // Do not allow to end link in certain characters... let lastIncludedCharIndex = linkEndIndex - 1; do { @@ -222,13 +204,11 @@ export class LinkComputer { } }, 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) => { } + handle: handler }; } - public static computeLink(position: IBufferCellPosition, buffer: IBuffer): ILink | undefined { + public static computeLink(position: IBufferCellPosition, buffer: IBuffer, handler: (event: MouseEvent, link: string) => void): ILink | undefined { const stateMachine: StateMachine = getStateMachine(); const classifier = getClassifier(); @@ -299,7 +279,7 @@ export class LinkComputer { // Check if character terminates link if (chClass === CharacterClass.FORCE_TERMINATION) { - return LinkComputer._createLink(classifier, line, i, linkBeginIndex, j); + return LinkComputer._createLink(classifier, line, i, linkBeginIndex, j, handler); } } else if (state === State.END) { @@ -342,7 +322,7 @@ export class LinkComputer { } if (state === State.ACCEPT) { - return LinkComputer._createLink(classifier, line, i, linkBeginIndex, len); + return LinkComputer._createLink(classifier, line, i, linkBeginIndex, len, handler); } } } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 6ccfd5da..c9826bcc 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -58,7 +58,10 @@ export class Linkifier2 implements ILinkifier2 { // 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); + + if (cachedLink.link.showTooltip) { + cachedLink.link.showTooltip(event, cachedLink.link.url); + } this._linkCache[i].mouseOver = true; foundLink = true; @@ -66,7 +69,10 @@ export class Linkifier2 implements ILinkifier2 { // 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); + + if (cachedLink.link.hideTooltip) { + cachedLink.link.hideTooltip(event, cachedLink.link.url); + } this._linkCache[i].mouseOver = false; } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 58da702b..2a5e94c7 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -160,8 +160,8 @@ interface ILinkProvider { interface ILink { range: IBufferRange; url: string; - showTooltip(event: MouseEvent, link: string): void; - hideTooltip(event: MouseEvent, link: string): void; + showTooltip?(event: MouseEvent, link: string): void; + hideTooltip?(event: MouseEvent, link: string): void; handle(event: MouseEvent, link: string): void; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 079b6439..f51f74f2 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -925,14 +925,14 @@ declare module 'xterm' { * @param event * @param link */ - showTooltip(event: MouseEvent, link: string): void; + showTooltip?(event: MouseEvent, link: string): void; /** * The hide tooltip callback * @param event * @param link */ - hideTooltip(event: MouseEvent, link: string): void; + hideTooltip?(event: MouseEvent, link: string): void; /** * Handles when the link is opened From 1e877a0f96b6d3146b4adb45d7cc1f12e6802ccc Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 09:18:45 -0500 Subject: [PATCH 03/26] Fix lint errors --- .../src/WebLinkProvider.ts | 46 +-- addons/xterm-addon-web-links/src/charCode.ts | 355 +++++------------- .../src/characterClassifier.ts | 37 +- src/browser/Types.d.ts | 2 +- 4 files changed, 128 insertions(+), 312 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index db03c0a9..f025f2b9 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -124,13 +124,13 @@ function getStateMachine(): StateMachine { [State.HTTP, CharCode.s, State.BEFORE_COLON], [State.HTTP, CharCode.S, State.BEFORE_COLON], - [State.HTTP, CharCode.Colon, State.AFTER_COLON], + [State.HTTP, CharCode.COLON, State.AFTER_COLON], - [State.BEFORE_COLON, CharCode.Colon, State.AFTER_COLON], + [State.BEFORE_COLON, CharCode.COLON, State.AFTER_COLON], - [State.AFTER_COLON, CharCode.Slash, State.ALMOST_THERE], + [State.AFTER_COLON, CharCode.SLASH, State.ALMOST_THERE], - [State.ALMOST_THERE, CharCode.Slash, State.END] + [State.ALMOST_THERE, CharCode.SLASH, State.END] ]); } return stateMachine; @@ -181,9 +181,9 @@ export class LinkComputer { const lastCharCodeInLink = line.charCodeAt(lastIncludedCharIndex); if ( - (charCodeBeforeLink === CharCode.OpenParen && lastCharCodeInLink === CharCode.CloseParen) - || (charCodeBeforeLink === CharCode.OpenSquareBracket && lastCharCodeInLink === CharCode.CloseSquareBracket) - || (charCodeBeforeLink === CharCode.OpenCurlyBrace && lastCharCodeInLink === CharCode.CloseCurlyBrace) + (charCodeBeforeLink === CharCode.OPEN_PAREN && lastCharCodeInLink === CharCode.CLOSE_PAREN) + || (charCodeBeforeLink === CharCode.OPEN_SQUARE_BRACKET && lastCharCodeInLink === CharCode.CLOSE_SQUARE_BRACKET) + || (charCodeBeforeLink === CharCode.OPEN_CURLY_BRACE && lastCharCodeInLink === CharCode.CLOSE_CURLY_BRACE) ) { // Do not end in ) if ( is before the link start // Do not end in ] if [ is before the link start @@ -238,40 +238,40 @@ export class LinkComputer { if (state === State.ACCEPT) { let chClass: CharacterClass; switch (chCode) { - case CharCode.OpenParen: + case CharCode.OPEN_PAREN: hasOpenParens = true; chClass = CharacterClass.NONE; break; - case CharCode.CloseParen: + case CharCode.CLOSE_PAREN: chClass = (hasOpenParens ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); break; - case CharCode.OpenSquareBracket: + case CharCode.OPEN_SQUARE_BRACKET: hasOpenSquareBracket = true; chClass = CharacterClass.NONE; break; - case CharCode.CloseSquareBracket: + case CharCode.CLOSE_SQUARE_BRACKET: chClass = (hasOpenSquareBracket ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); break; - case CharCode.OpenCurlyBrace: + case CharCode.OPEN_CURLY_BRACE: hasOpenCurlyBracket = true; chClass = CharacterClass.NONE; break; - case CharCode.CloseCurlyBrace: + case CharCode.CLOSE_CURLY_BRACE: 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; + case CharCode.SINGLE_QUOTE: + chClass = (linkBeginChCode === CharCode.DOUBLE_QUOTE || linkBeginChCode === CharCode.BACK_TICK) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; break; - case CharCode.DoubleQuote: - chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.BackTick) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; + case CharCode.DOUBLE_QUOTE: + chClass = (linkBeginChCode === CharCode.SINGLE_QUOTE || linkBeginChCode === CharCode.BACK_TICK) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; break; - case CharCode.BackTick: - chClass = (linkBeginChCode === CharCode.SingleQuote || linkBeginChCode === CharCode.DoubleQuote) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; + case CharCode.BACK_TICK: + chClass = (linkBeginChCode === CharCode.SINGLE_QUOTE || linkBeginChCode === CharCode.DOUBLE_QUOTE) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; break; - case CharCode.Asterisk: + case CharCode.ASTERISK: // `*` terminates a link if the link began with `*` - chClass = (linkBeginChCode === CharCode.Asterisk) ? CharacterClass.FORCE_TERMINATION : CharacterClass.NONE; + chClass = (linkBeginChCode === CharCode.ASTERISK) ? CharacterClass.FORCE_TERMINATION : CharacterClass.NONE; break; default: chClass = classifier.get(chCode); @@ -284,7 +284,7 @@ export class LinkComputer { } else if (state === State.END) { let chClass: CharacterClass; - if (chCode === CharCode.OpenSquareBracket) { + if (chCode === CharCode.OPEN_SQUARE_BRACKET) { // Allow for the authority part to contain ipv6 addresses which contain [ and ] hasOpenSquareBracket = true; chClass = CharacterClass.NONE; @@ -302,7 +302,7 @@ export class LinkComputer { 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) { + if (chCode === CharCode.SPACE && j > 0 && line.charCodeAt(j - 1) === CharCode.SPACE) { return; } diff --git a/addons/xterm-addon-web-links/src/charCode.ts b/addons/xterm-addon-web-links/src/charCode.ts index f6724a73..bc72c373 100644 --- a/addons/xterm-addon-web-links/src/charCode.ts +++ b/addons/xterm-addon-web-links/src/charCode.ts @@ -5,124 +5,124 @@ * Please leave the const keyword such that it gets inlined when compiled to JavaScript! */ export const enum CharCode { - Null = 0, - /** + NULL = 0, + /** * The `\b` character. */ - Backspace = 8, - /** + BACKSPACE = 8, + /** * The `\t` character. */ - Tab = 9, - /** + TAB = 9, + /** * The `\n` character. */ - LineFeed = 10, - /** + LINE_FEED = 10, + /** * The `\r` character. */ - CarriageReturn = 13, - Space = 32, - /** + CARRIAGE_RETURN = 13, + SPACE = 32, + /** * The `!` character. */ - ExclamationMark = 33, - /** + EXCLAMATION_MARK = 33, + /** * The `"` character. */ - DoubleQuote = 34, - /** + DOUBLE_QUOTE = 34, + /** * The `#` character. */ - Hash = 35, - /** + HASH = 35, + /** * The `$` character. */ - DollarSign = 36, - /** + DOLLAR_SIGN = 36, + /** * The `%` character. */ - PercentSign = 37, - /** + PERCENT_SIGN = 37, + /** * The `&` character. */ - Ampersand = 38, - /** + AMPERSAND = 38, + /** * The `'` character. */ - SingleQuote = 39, - /** + SINGLE_QUOTE = 39, + /** * The `(` character. */ - OpenParen = 40, - /** + OPEN_PAREN = 40, + /** * The `)` character. */ - CloseParen = 41, - /** + CLOSE_PAREN = 41, + /** * The `*` character. */ - Asterisk = 42, - /** + ASTERISK = 42, + /** * The `+` character. */ - Plus = 43, - /** + PLUS = 43, + /** * The `,` character. */ - Comma = 44, - /** + COMMA = 44, + /** * The `-` character. */ - Dash = 45, - /** + DASH = 45, + /** * The `.` character. */ - Period = 46, - /** + PERIOD = 46, + /** * The `/` character. */ - Slash = 47, + SLASH = 47, - Digit0 = 48, - Digit1 = 49, - Digit2 = 50, - Digit3 = 51, - Digit4 = 52, - Digit5 = 53, - Digit6 = 54, - Digit7 = 55, - Digit8 = 56, - Digit9 = 57, + DIGIT_0 = 48, + DIGIT_1 = 49, + DIGIT_2 = 50, + DIGIT_3 = 51, + DIGIT_4 = 52, + DIGIT_5 = 53, + DIGIT_6 = 54, + DIGIT_7 = 55, + DIGIT_8 = 56, + DIGIT_9 = 57, - /** + /** * The `:` character. */ - Colon = 58, - /** + COLON = 58, + /** * The `;` character. */ - Semicolon = 59, - /** + SEMICOLON = 59, + /** * The `<` character. */ - LessThan = 60, - /** + LESS_THAN = 60, + /** * The `=` character. */ - Equals = 61, - /** + EQUALS = 61, + /** * The `>` character. */ - GreaterThan = 62, - /** + GREATER_THAN = 62, + /** * The `?` character. */ - QuestionMark = 63, - /** + QUESTION_MARK = 63, + /** * The `@` character. */ - AtSign = 64, + AT_SIGN = 64, A = 65, B = 66, @@ -151,30 +151,30 @@ export const enum CharCode { Y = 89, Z = 90, - /** + /** * The `[` character. */ - OpenSquareBracket = 91, - /** + OPEN_SQUARE_BRACKET = 91, + /** * The `\` character. */ - Backslash = 92, - /** + BACK_SLASH = 92, + /** * The `]` character. */ - CloseSquareBracket = 93, - /** + CLOSE_SQUARE_BRACKET = 93, + /** * The `^` character. */ - Caret = 94, - /** + CARET = 94, + /** * The `_` character. */ - Underline = 95, - /** + UNDERLINE = 95, + /** * The ``(`)`` character. */ - BackTick = 96, + BACK_TICK = 96, a = 97, b = 98, @@ -203,215 +203,32 @@ export const enum CharCode { y = 121, z = 122, - /** + /** * The `{` character. */ - OpenCurlyBrace = 123, - /** + OPEN_CURLY_BRACE = 123, + /** * The `|` character. */ - Pipe = 124, - /** + PIPE = 124, + /** * The `}` character. */ - CloseCurlyBrace = 125, - /** + CLOSE_CURLY_BRACE = 125, + /** * The `~` character. */ - Tilde = 126, + 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 diff --git a/addons/xterm-addon-web-links/src/characterClassifier.ts b/addons/xterm-addon-web-links/src/characterClassifier.ts index 6fb3324b..2ca6f2aa 100644 --- a/addons/xterm-addon-web-links/src/characterClassifier.ts +++ b/addons/xterm-addon-web-links/src/characterClassifier.ts @@ -2,20 +2,20 @@ * 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); + constructor(newDefaultValue: T) { + const defaultValue = toUint8(newDefaultValue); this._defaultValue = defaultValue; this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue); @@ -23,15 +23,15 @@ export class CharacterClassifier { } private static _createAsciiMap(defaultValue: number): Uint8Array { - let asciiMap: Uint8Array = new Uint8Array(256); + const 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); + public set(charCode: number, newValue: T): void { + const value = toUint8(newValue); if (charCode >= 0 && charCode < 256) { this._asciiMap[charCode] = value; @@ -43,15 +43,14 @@ export class CharacterClassifier { public get(charCode: number): T { if (charCode >= 0 && charCode < 256) { return this._asciiMap[charCode]; - } else { - return (this._map.get(charCode) || this._defaultValue); } + return (this._map.get(charCode) || this._defaultValue); } } const enum Boolean { - False = 0, - True = 1 + FALSE = 0, + TRUE = 1 } export class CharacterSet { @@ -59,20 +58,20 @@ export class CharacterSet { private readonly _actual: CharacterClassifier; constructor() { - this._actual = new CharacterClassifier(Boolean.False); + this._actual = new CharacterClassifier(Boolean.FALSE); } public add(charCode: number): void { - this._actual.set(charCode, Boolean.True); + this._actual.set(charCode, Boolean.TRUE); } public has(charCode: number): boolean { - return (this._actual.get(charCode) === Boolean.True); + 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. @@ -80,7 +79,7 @@ export const enum Constants { */ 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. @@ -88,17 +87,17 @@ export const enum Constants { */ 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 diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 2a5e94c7..c7b40f34 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -98,7 +98,7 @@ export interface ILinkifier2 { onShowTooltip: IEvent; onHideTooltip: IEvent; - attachToDom(element: HTMLElement, mouseService: IMouseService): void + attachToDom(element: HTMLElement, mouseService: IMouseService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; } From cf082cca04d7c0b53d19e6c94612daf22f304827 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 11:24:21 -0500 Subject: [PATCH 04/26] Fix links when viewport is scrolled --- src/browser/Linkifier2.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index c9826bcc..8f794fc2 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -49,6 +49,8 @@ export class Linkifier2 implements ILinkifier2 { return; } + const scrollOffset = this._bufferService.buffer.ydisp; + // Check the cache for a link and determine if we need to show or hide tooltip let foundLink = false; this._linkCache.forEach((cachedLink, i) => { @@ -56,7 +58,7 @@ export class Linkifier2 implements ILinkifier2 { 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._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); this._element!.classList.add('xterm-cursor-pointer'); if (cachedLink.link.showTooltip) { @@ -67,7 +69,7 @@ export class Linkifier2 implements ILinkifier2 { 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._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); this._element!.classList.remove('xterm-cursor-pointer'); if (cachedLink.link.hideTooltip) { From ec78fc7e7c86ff9b0266e23db3a60945d75b54d0 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 13:42:02 -0500 Subject: [PATCH 05/26] Remove usage of ! operator --- .../src/WebLinkProvider.ts | 310 +----------------- .../src/WebLinksAddon.ts | 8 +- addons/xterm-addon-web-links/src/charCode.ts | 237 ------------- .../src/characterClassifier.ts | 116 ------- src/browser/Linkifier2.ts | 56 ++-- 5 files changed, 46 insertions(+), 681 deletions(-) delete mode 100644 addons/xterm-addon-web-links/src/charCode.ts delete mode 100644 addons/xterm-addon-web-links/src/characterClassifier.ts diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index f025f2b9..9c9a4e6f 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -1,217 +1,23 @@ 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 _regex: RegExp, 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, this._handler); - - callback(link); + callback(LinkComputer.computeLink(position, this._regex, this._terminal.buffer, this._handler)); } } -export const enum State { - INVALID = 0, - START = 1, - H = 2, - HT = 3, - HTT = 4, - HTTP = 5, - 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.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.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, handler: (event: MouseEvent, link: string) => void): 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.OPEN_PAREN && lastCharCodeInLink === CharCode.CLOSE_PAREN) - || (charCodeBeforeLink === CharCode.OPEN_SQUARE_BRACKET && lastCharCodeInLink === CharCode.CLOSE_SQUARE_BRACKET) - || (charCodeBeforeLink === CharCode.OPEN_CURLY_BRACE && lastCharCodeInLink === CharCode.CLOSE_CURLY_BRACE) - ) { - // 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), - handle: handler - }; - } - - public static computeLink(position: IBufferCellPosition, buffer: IBuffer, handler: (event: MouseEvent, link: string) => void): ILink | undefined { - const stateMachine: StateMachine = getStateMachine(); - const classifier = getClassifier(); - + public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handler: (event: MouseEvent, uri: string) => void): ILink | undefined { + const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const bufferLine = buffer.getLine(position.y - 1); if (!bufferLine) { @@ -219,111 +25,13 @@ export class LinkComputer { } const line = bufferLine.translateToString(); - const len = line.length; - const i = position.y; + let match; + let stringIndex = -1; - let linkBeginIndex = position.x - 1; - let state = State.START; - let hasOpenParens = false; - let hasOpenSquareBracket = false; - let hasOpenCurlyBracket = false; + // while ((match = rex.exec(line)) !== null) { + // const uri = match[1]; + // } - 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.OPEN_PAREN: - hasOpenParens = true; - chClass = CharacterClass.NONE; - break; - case CharCode.CLOSE_PAREN: - chClass = (hasOpenParens ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); - break; - case CharCode.OPEN_SQUARE_BRACKET: - hasOpenSquareBracket = true; - chClass = CharacterClass.NONE; - break; - case CharCode.CLOSE_SQUARE_BRACKET: - chClass = (hasOpenSquareBracket ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION); - break; - case CharCode.OPEN_CURLY_BRACE: - hasOpenCurlyBracket = true; - chClass = CharacterClass.NONE; - break; - case CharCode.CLOSE_CURLY_BRACE: - 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.SINGLE_QUOTE: - chClass = (linkBeginChCode === CharCode.DOUBLE_QUOTE || linkBeginChCode === CharCode.BACK_TICK) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; - break; - case CharCode.DOUBLE_QUOTE: - chClass = (linkBeginChCode === CharCode.SINGLE_QUOTE || linkBeginChCode === CharCode.BACK_TICK) ? CharacterClass.NONE : CharacterClass.FORCE_TERMINATION; - break; - case CharCode.BACK_TICK: - chClass = (linkBeginChCode === CharCode.SINGLE_QUOTE || linkBeginChCode === CharCode.DOUBLE_QUOTE) ? 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, handler); - } - } else if (state === State.END) { - - let chClass: CharacterClass; - if (chCode === CharCode.OPEN_SQUARE_BRACKET) { - // 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, handler); - } - } } } diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index bc6becc8..e09e6225 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -44,9 +44,13 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - this._linkProvider = new WebLinkProvider(this._terminal, this._handler); - // this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + + // if ('registerLinkProvider' in this._terminal) { + this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); this._terminal.registerLinkProvider(this._linkProvider); + // } else { + // this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + // } } public dispose(): void { diff --git a/addons/xterm-addon-web-links/src/charCode.ts b/addons/xterm-addon-web-links/src/charCode.ts deleted file mode 100644 index bc72c373..00000000 --- a/addons/xterm-addon-web-links/src/charCode.ts +++ /dev/null @@ -1,237 +0,0 @@ -// 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. - */ - LINE_FEED = 10, - /** - * The `\r` character. - */ - CARRIAGE_RETURN = 13, - SPACE = 32, - /** - * The `!` character. - */ - EXCLAMATION_MARK = 33, - /** - * The `"` character. - */ - DOUBLE_QUOTE = 34, - /** - * The `#` character. - */ - HASH = 35, - /** - * The `$` character. - */ - DOLLAR_SIGN = 36, - /** - * The `%` character. - */ - PERCENT_SIGN = 37, - /** - * The `&` character. - */ - AMPERSAND = 38, - /** - * The `'` character. - */ - SINGLE_QUOTE = 39, - /** - * The `(` character. - */ - OPEN_PAREN = 40, - /** - * The `)` character. - */ - CLOSE_PAREN = 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, - - DIGIT_0 = 48, - DIGIT_1 = 49, - DIGIT_2 = 50, - DIGIT_3 = 51, - DIGIT_4 = 52, - DIGIT_5 = 53, - DIGIT_6 = 54, - DIGIT_7 = 55, - DIGIT_8 = 56, - DIGIT_9 = 57, - - /** - * The `:` character. - */ - COLON = 58, - /** - * The `;` character. - */ - SEMICOLON = 59, - /** - * The `<` character. - */ - LESS_THAN = 60, - /** - * The `=` character. - */ - EQUALS = 61, - /** - * The `>` character. - */ - GREATER_THAN = 62, - /** - * The `?` character. - */ - QUESTION_MARK = 63, - /** - * The `@` character. - */ - AT_SIGN = 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. - */ - OPEN_SQUARE_BRACKET = 91, - /** - * The `\` character. - */ - BACK_SLASH = 92, - /** - * The `]` character. - */ - CLOSE_SQUARE_BRACKET = 93, - /** - * The `^` character. - */ - CARET = 94, - /** - * The `_` character. - */ - UNDERLINE = 95, - /** - * The ``(`)`` character. - */ - BACK_TICK = 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. - */ - OPEN_CURLY_BRACE = 123, - /** - * The `|` character. - */ - PIPE = 124, - /** - * The `}` character. - */ - CLOSE_CURLY_BRACE = 125, - /** - * The `~` character. - */ - TILDE = 126, - - /** - * Unicode Character 'LINE SEPARATOR' (U+2028) - * http://www.fileformat.info/info/unicode/char/2028/index.htm - */ - LINE_SEPARATOR_2028 = 8232, - - 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 deleted file mode 100644 index 2ca6f2aa..00000000 --- a/addons/xterm-addon-web-links/src/characterClassifier.ts +++ /dev/null @@ -1,116 +0,0 @@ -/** - * 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(newDefaultValue: T) { - const defaultValue = toUint8(newDefaultValue); - - this._defaultValue = defaultValue; - this._asciiMap = CharacterClassifier._createAsciiMap(defaultValue); - this._map = new Map(); - } - - private static _createAsciiMap(defaultValue: number): Uint8Array { - const asciiMap: Uint8Array = new Uint8Array(256); - for (let i = 0; i < 256; i++) { - asciiMap[i] = defaultValue; - } - return asciiMap; - } - - public set(charCode: number, newValue: T): void { - const value = toUint8(newValue); - - 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]; - } - 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/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 8f794fc2..aeb3cf6d 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -43,7 +43,11 @@ export class Linkifier2 implements ILinkifier2 { } private _onMouseMove(event: MouseEvent): void { - const position = this._positionFromMouseEvent(event); + if (!this._element || !this._mouseService) { + return; + } + + const position = this._positionFromMouseEvent(event, this._element, this._mouseService); if (!position) { return; @@ -53,32 +57,34 @@ export class Linkifier2 implements ILinkifier2 { // 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) { + for (let i = 0; i < this._linkCache.length; i++) { + const cachedLink = this._linkCache[i].link; + const isInPosition = this._linkAtPosition(cachedLink, position); + const range = cachedLink.range; + + if (isInPosition && !this._linkCache[i].mouseOver) { // Show the tooltip this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); - this._element!.classList.add('xterm-cursor-pointer'); + this._element.classList.add('xterm-cursor-pointer'); - if (cachedLink.link.showTooltip) { - cachedLink.link.showTooltip(event, cachedLink.link.url); + if (cachedLink.showTooltip) { + cachedLink.showTooltip(event, cachedLink.url); } this._linkCache[i].mouseOver = true; foundLink = true; - } else if (!isInPosition && cachedLink.mouseOver) { + } else if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); - this._element!.classList.remove('xterm-cursor-pointer'); + this._element.classList.remove('xterm-cursor-pointer'); - if (cachedLink.link.hideTooltip) { - cachedLink.link.hideTooltip(event, cachedLink.link.url); + if (cachedLink.hideTooltip) { + cachedLink.hideTooltip(event, cachedLink.url); } this._linkCache[i].mouseOver = false; } - }); + } if (foundLink) { return; @@ -91,7 +97,11 @@ export class Linkifier2 implements ILinkifier2 { } private _onMouseDown(event: MouseEvent): void { - const position = this._positionFromMouseEvent(event); + if (!this._element || !this._mouseService) { + return; + } + + const position = this._positionFromMouseEvent(event, this._element, this._mouseService); if (!position) { return; @@ -106,7 +116,7 @@ export class Linkifier2 implements ILinkifier2 { private _handleNewLink(link: ILink | undefined): void { if (link && !this._linkCache.find(cachedLink => cachedLink.link = link)) { - this._linkCache.push({ link: link, mouseOver: false }); + this._linkCache.push({ link, mouseOver: false }); } } @@ -116,22 +126,18 @@ export class Linkifier2 implements ILinkifier2 { * @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; + 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); + private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined { + const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows); if (!coords) { return; } From e57cb8bf7e8df25e79454cff1ae037864adce1d5 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 1 Nov 2019 17:38:08 -0500 Subject: [PATCH 06/26] Use regex parser for web links --- .../src/WebLinkProvider.ts | 37 +++++++++++-- .../src/WebLinksAddon.ts | 12 ++--- src/Terminal.ts | 2 +- src/browser/Linkifier2.ts | 54 +++++++++++++++---- src/browser/Types.d.ts | 2 +- 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index 9c9a4e6f..a5875e9f 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -16,7 +16,7 @@ export default class WebLinkProvider implements ILinkProvider { } export class LinkComputer { - public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handler: (event: MouseEvent, uri: string) => void): ILink | undefined { + public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handle: (event: MouseEvent, uri: string) => void): ILink | undefined { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const bufferLine = buffer.getLine(position.y - 1); @@ -29,9 +29,38 @@ export class LinkComputer { let match; let stringIndex = -1; - // while ((match = rex.exec(line)) !== null) { - // const uri = match[1]; - // } + while ((match = rex.exec(line)) !== null) { + const url = match[1]; + if (!url) { + // something matched but does not comply with the given matchIndex + // since this is most likely a bug the regex itself we simply do nothing here + console.log('match found without corresponding matchIndex'); + break; + } + // Get index, match.index is for the outer match which includes negated chars + // therefore we cannot use match.index directly, instead we search the position + // of the match group in text again + // also correct regex and string search offsets for the next loop run + stringIndex = line.indexOf(url, stringIndex + 1); + rex.lastIndex = stringIndex + url.length; + if (stringIndex < 0) { + // invalid stringIndex (should not have happened) + break; + } + + const range = { + start: { + x: stringIndex + 1, + y: position.y + }, + end: { + x: stringIndex + url.length + 1, + y: position.y + } + }; + + return { range, url, handle }; + } } } diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index e09e6225..4dbcc336 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -45,12 +45,12 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - // if ('registerLinkProvider' in this._terminal) { - this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); - this._terminal.registerLinkProvider(this._linkProvider); - // } else { - // this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); - // } + if ('registerLinkProvider' in this._terminal as any) { + this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); + this._terminal.registerLinkProvider(this._linkProvider); + } else { + this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + } } public dispose(): void { diff --git a/src/Terminal.ts b/src/Terminal.ts index fb49b10b..3b849a8d 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -622,7 +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.linkifier2.attachToDom(this.element, this._viewportElement, this._mouseService); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index aeb3cf6d..6273459a 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -14,6 +14,7 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; */ export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; + private _viewportElement: HTMLElement | undefined; private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; private _linkCache: ILinkCache[] = []; @@ -31,15 +32,22 @@ export class Linkifier2 implements ILinkifier2 { public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { this._linkProviders.push(linkProvider); - return { dispose: () => console.log('disposing link providers') }; + return { + dispose: () => { + // Remove the link provider from the list + this._linkProviders = this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); + } + }; } - public attachToDom(element: HTMLElement, mouseService: IMouseService): void { + public attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void { this._element = element; + this._viewportElement = viewportElement; this._mouseService = mouseService; this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); + this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); } private _onMouseMove(event: MouseEvent): void { @@ -62,6 +70,7 @@ export class Linkifier2 implements ILinkifier2 { const isInPosition = this._linkAtPosition(cachedLink, position); const range = cachedLink.range; + // Check if the mouse position contains a link if (isInPosition && !this._linkCache[i].mouseOver) { // Show the tooltip this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); @@ -75,13 +84,7 @@ export class Linkifier2 implements ILinkifier2 { foundLink = true; } else if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip - this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); - this._element.classList.remove('xterm-cursor-pointer'); - - if (cachedLink.hideTooltip) { - cachedLink.hideTooltip(event, cachedLink.url); - } - + this._hideTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = false; } } @@ -114,9 +117,42 @@ export class Linkifier2 implements ILinkifier2 { }); } + private _onScroll(event: Event): void { + this._invalidateCache(); + } + private _handleNewLink(link: ILink | undefined): void { if (link && !this._linkCache.find(cachedLink => cachedLink.link = link)) { this._linkCache.push({ link, mouseOver: false }); + this._bufferService.buffer.addMarker(link.range.start.y); + } + } + + private _invalidateCache(): void { + if (!this._element) { + return; + } + + // We want to invalidate the cache + // but we need to check if we need to hide any tooltip + for (let i = 0; i < this._linkCache.length; i++) { + if (this._linkCache[i].mouseOver) { + this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); + } + } + + this._linkCache = []; + } + + private _hideTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + const range = link.range; + const scrollOffset = this._bufferService.buffer.ydisp; + + this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + element.classList.remove('xterm-cursor-pointer'); + + if (link.hideTooltip) { + link.hideTooltip(event, link.url); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index c7b40f34..b7072c5c 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -98,7 +98,7 @@ export interface ILinkifier2 { onShowTooltip: IEvent; onHideTooltip: IEvent; - attachToDom(element: HTMLElement, mouseService: IMouseService): void; + attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; } From fdfadfefbc8bed8616db88bb111963f2395c9af3 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Mon, 4 Nov 2019 23:37:05 -0600 Subject: [PATCH 07/26] Fixed link caching errors --- .../src/WebLinksAddon.api.ts | 10 +- src/Terminal.ts | 2 +- src/browser/Linkifier2.ts | 124 +++++++++++++----- 3 files changed, 97 insertions(+), 39 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 3fb1a536..9f2ab508 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -15,7 +15,7 @@ const width = 800; const height = 600; describe('WebLinksAddon', () => { - before(async function(): Promise { + before(async function (): Promise { this.timeout(10000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, @@ -30,22 +30,22 @@ describe('WebLinksAddon', () => { await browser.close(); }); - beforeEach(async function(): Promise { + beforeEach(async function (): Promise { this.timeout(5000); await page.goto(APP); }); - it('.com', async function(): Promise { + it('.com', async function (): Promise { this.timeout(20000); await testHostName('foo.com'); }); - it('.com.au', async function(): Promise { + it('.com.au', async function (): Promise { this.timeout(20000); await testHostName('foo.com.au'); }); - it('.io', async function(): Promise { + it('.io', async function (): Promise { this.timeout(20000); await testHostName('foo.io'); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 3b849a8d..82f9ef9f 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -292,7 +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); + this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService, this._coreService); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 6273459a..8d76dd06 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -1,7 +1,7 @@ -import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent } from './Types'; +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, IBufferRange } from './Types'; import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; -import { IBufferService } from 'common/services/Services'; +import { IBufferService, ICoreService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; /** @@ -17,7 +17,9 @@ export class Linkifier2 implements ILinkifier2 { private _viewportElement: HTMLElement | undefined; private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; - private _linkCache: ILinkCache[] = []; + private _linkCache: ICachedLink[] = []; + private _lastMouseEvent: MouseEvent | undefined; + private _mouseOverLink: boolean = false; private _onShowTooltip = new EventEmitter(); public get onShowTooltip(): IEvent { return this._onShowTooltip.event; } @@ -25,7 +27,8 @@ export class Linkifier2 implements ILinkifier2 { public get onHideTooltip(): IEvent { return this._onHideTooltip.event; } constructor( - private readonly _bufferService: IBufferService + private readonly _bufferService: IBufferService, + private readonly _coreService: ICoreService ) { } @@ -35,7 +38,7 @@ export class Linkifier2 implements ILinkifier2 { return { dispose: () => { // Remove the link provider from the list - this._linkProviders = this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); + this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); } }; } @@ -48,9 +51,13 @@ export class Linkifier2 implements ILinkifier2 { this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); + + this._coreService.onData(this._onData.bind(this)); } private _onMouseMove(event: MouseEvent): void { + this._lastMouseEvent = event; + if (!this._element || !this._mouseService) { return; } @@ -61,38 +68,40 @@ export class Linkifier2 implements ILinkifier2 { return; } - const scrollOffset = this._bufferService.buffer.ydisp; - // Check the cache for a link and determine if we need to show or hide tooltip let foundLink = false; + let mouseOver = false; for (let i = 0; i < this._linkCache.length; i++) { const cachedLink = this._linkCache[i].link; const isInPosition = this._linkAtPosition(cachedLink, position); - const range = cachedLink.range; // Check if the mouse position contains a link - if (isInPosition && !this._linkCache[i].mouseOver) { + // Also check if it isn't the current line + if (isInPosition && !this._linkCache[i].mouseOver && position.y < this._bufferService.buffer.y) { // Show the tooltip - this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); - this._element.classList.add('xterm-cursor-pointer'); - - if (cachedLink.showTooltip) { - cachedLink.showTooltip(event, cachedLink.url); - } - + this._showTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = true; foundLink = true; + this._mouseOverLink = true; } else if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip this._hideTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = false; } + + if (isInPosition) { + mouseOver = true; + } } if (foundLink) { return; } + if (!mouseOver) { + this._mouseOverLink = false; + } + // The is no link in the cache, so ask for one this._linkProviders.forEach(linkProvider => { linkProvider.provideLink(position, this._handleNewLink.bind(this)); @@ -118,30 +127,63 @@ export class Linkifier2 implements ILinkifier2 { } private _onScroll(event: Event): void { - this._invalidateCache(); + if (this._lastMouseEvent && this._mouseOverLink) { + this._hideAllTooltips(); + } + } + + private _onData(e: string): void { + if (this._lastMouseEvent && this._mouseOverLink) { + const index = this._hideAllTooltips(); + this._linkCache.splice(index, 1); + } } private _handleNewLink(link: ILink | undefined): void { - if (link && !this._linkCache.find(cachedLink => cachedLink.link = link)) { - this._linkCache.push({ link, mouseOver: false }); - this._bufferService.buffer.addMarker(link.range.start.y); - } - } - - private _invalidateCache(): void { - if (!this._element) { + if (!link || !this._element || !this._lastMouseEvent || !this._mouseService) { return; } - // We want to invalidate the cache - // but we need to check if we need to hide any tooltip - for (let i = 0; i < this._linkCache.length; i++) { - if (this._linkCache[i].mouseOver) { - this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); - } + // Check if the link at this position is already cached + let linkIndex = this._linkCache.findIndex(cachedLink => { + return cachedLink.link.url === link.url && + cachedLink.link.range.start.x === link.range.start.x && + cachedLink.link.range.start.y === link.range.start.y && + cachedLink.link.range.end.x === link.range.end.x && + cachedLink.link.range.end.y === link.range.end.y; + }); + + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService); + + if (!position) { + return; } - this._linkCache = []; + const linkAtPosition = this._linkAtPosition(link, position); + + if (linkIndex === -1) { + this._linkCache.push({ link, mouseOver: false }); + linkIndex = this._linkCache.length - 1; + } + + // Show the tooltip if the last mouse event was over it + if (linkAtPosition && !this._linkCache[linkIndex].mouseOver) { + this._showTooltip(this._element, link, this._lastMouseEvent); + this._linkCache[linkIndex].mouseOver = true; + this._mouseOverLink = true; + } + } + + private _showTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + const range = link.range; + const scrollOffset = this._bufferService.buffer.ydisp; + + this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + element.classList.add('xterm-cursor-pointer'); + + if (link.showTooltip) { + link.showTooltip(event, link.url); + } } private _hideTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { @@ -156,6 +198,22 @@ export class Linkifier2 implements ILinkifier2 { } } + private _hideAllTooltips(): number { + if (!this._element) { + return -1; + } + + // Hide all the tooltips + for (let i = 0; i < this._linkCache.length; i++) { + if (this._linkCache[i].mouseOver) { + this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); + return i; + } + } + + return -1; + } + /** * Check if the buffer position is within the link * @param link @@ -186,7 +244,7 @@ export class Linkifier2 implements ILinkifier2 { } } -interface ILinkCache { +interface ICachedLink { link: ILink; mouseOver: boolean; } From 28c43ac2bffa5d70eb54abd2ac348bd3e419afdc Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Tue, 5 Nov 2019 13:01:53 -0600 Subject: [PATCH 08/26] Remove link caching --- .../src/WebLinkProvider.ts | 2 +- .../src/WebLinksAddon.ts | 13 ++++---- src/Terminal.ts | 3 -- src/browser/Linkifier2.ts | 30 +++++-------------- 4 files changed, 17 insertions(+), 31 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index a5875e9f..ea14538f 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -1,6 +1,6 @@ import { ILinkProvider, IBufferCellPosition, ILink, Terminal, IBuffer } from 'xterm'; -export default class WebLinkProvider implements ILinkProvider { +export class WebLinkProvider implements ILinkProvider { constructor( private readonly _terminal: Terminal, diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index 4dbcc336..c5e62574 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon, ILinkProvider } from 'xterm'; -import WebLinkProvider from './WebLinkProvider'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon, ILinkProvider, IDisposable } from 'xterm'; +import { WebLinkProvider } from './WebLinkProvider'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -33,7 +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; + private _linkProvider: IDisposable | undefined; constructor( private _handler: (event: MouseEvent, uri: string) => void = handleLink, @@ -46,8 +46,7 @@ export class WebLinksAddon implements ITerminalAddon { this._terminal = terminal; if ('registerLinkProvider' in this._terminal as any) { - this._linkProvider = new WebLinkProvider(this._terminal, strictUrlRegex, this._handler); - this._terminal.registerLinkProvider(this._linkProvider); + this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler)); } else { this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); } @@ -57,5 +56,9 @@ export class WebLinksAddon implements ITerminalAddon { if (this._linkMatcherId !== undefined && this._terminal !== undefined) { this._terminal.deregisterLinkMatcher(this._linkMatcherId); } + + if (this._linkProvider) { + this._linkProvider.dispose(); + } } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 82f9ef9f..30144a62 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1171,9 +1171,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { - if (!this.linkifier2) { - return; - } return this.linkifier2.registerLinkProvider(linkProvider); } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 8d76dd06..d133db8d 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -1,17 +1,14 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, IBufferRange } from './Types'; import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; import { IBufferService, ICoreService } 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 _viewportElement: HTMLElement | undefined; @@ -69,21 +66,13 @@ export class Linkifier2 implements ILinkifier2 { } // Check the cache for a link and determine if we need to show or hide tooltip - let foundLink = false; let mouseOver = false; for (let i = 0; i < this._linkCache.length; i++) { const cachedLink = this._linkCache[i].link; const isInPosition = this._linkAtPosition(cachedLink, position); - // Check if the mouse position contains a link - // Also check if it isn't the current line - if (isInPosition && !this._linkCache[i].mouseOver && position.y < this._bufferService.buffer.y) { - // Show the tooltip - this._showTooltip(this._element, this._linkCache[i].link, event); - this._linkCache[i].mouseOver = true; - foundLink = true; - this._mouseOverLink = true; - } else if (!isInPosition && this._linkCache[i].mouseOver) { + // Check if we need to hide the tooltip + if (!isInPosition && this._linkCache[i].mouseOver) { // Hide the tooltip this._hideTooltip(this._element, this._linkCache[i].link, event); this._linkCache[i].mouseOver = false; @@ -94,12 +83,9 @@ export class Linkifier2 implements ILinkifier2 { } } - if (foundLink) { - return; - } - if (!mouseOver) { this._mouseOverLink = false; + this._linkCache = []; } // The is no link in the cache, so ask for one From ac6c8a5bd3e513f5f52b03fbd6280e5aa2ea37e5 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Tue, 5 Nov 2019 23:35:20 -0600 Subject: [PATCH 09/26] Support multiline links in the WebLinksAddon --- .../src/WebLinkProvider.ts | 70 ++++++++++++++++--- .../src/WebLinksAddon.ts | 2 +- src/browser/Linkifier2.ts | 12 +++- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index ea14538f..4c3702bb 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -1,3 +1,8 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + import { ILinkProvider, IBufferCellPosition, ILink, Terminal, IBuffer } from 'xterm'; export class WebLinkProvider implements ILinkProvider { @@ -11,20 +16,15 @@ export class WebLinkProvider implements ILinkProvider { } provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void { - callback(LinkComputer.computeLink(position, this._regex, this._terminal.buffer, this._handler)); + callback(LinkComputer.computeLink(position, this._regex, this._terminal, this._handler)); } } export class LinkComputer { - public static computeLink(position: IBufferCellPosition, regex: RegExp, buffer: IBuffer, handle: (event: MouseEvent, uri: string) => void): ILink | undefined { + public static computeLink(position: IBufferCellPosition, regex: RegExp, terminal: Terminal, handle: (event: MouseEvent, uri: string) => void): ILink | undefined { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); - const bufferLine = buffer.getLine(position.y - 1); - if (!bufferLine) { - return; - } - - const line = bufferLine.translateToString(); + const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(position.y - 1, false, terminal); let match; let stringIndex = -1; @@ -49,18 +49,66 @@ export class LinkComputer { break; } + let endX = stringIndex + url.length + 1; + let endY = startLineIndex + 1; + + while (endX > terminal.cols) { + endX -= terminal.cols; + endY++; + } + const range = { start: { x: stringIndex + 1, - y: position.y + y: startLineIndex + 1 }, end: { - x: stringIndex + url.length + 1, - y: position.y + x: endX, + y: endY } }; return { range, url, handle }; } } + + /** + * Gets the entire line for the buffer line + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + * @param terminal The terminal + */ + private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] { + let lineString = ''; + let lineWrapsToNext: boolean; + let prevLinesToWrap: boolean; + + do { + const line = terminal.buffer.getLine(lineIndex); + if (!line) { + break; + } + + if (line.isWrapped) { + lineIndex--; + } + + prevLinesToWrap = line.isWrapped; + } while (prevLinesToWrap); + + const startLineIndex = lineIndex; + + do { + const nextLine = terminal.buffer.getLine(lineIndex + 1); + lineWrapsToNext = nextLine ? nextLine.isWrapped : false; + const line = terminal.buffer.getLine(lineIndex); + if (!line) { + break; + } + lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols); + lineIndex++; + } while (lineWrapsToNext); + + return [lineString, startLineIndex]; + } } diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index c5e62574..09183014 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index d133db8d..0ae7c825 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; import { IBufferService, ICoreService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { LinkRenderLayer } from './renderer/LinkRenderLayer'; export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; @@ -206,9 +207,16 @@ export class Linkifier2 implements ILinkifier2 { * @param position */ private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean { - return link.range.start.x <= position.x && + const sameLine = link.range.start.y === link.range.end.y; + const wrappedFromLeft = link.range.start.y < position.y; + const wrappedToRight = link.range.end.y > position.y; + + // If the start and end have the same y, then the position must be between start and end x + // If not, then handle each case seperately, depending on which way it wraps + return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) || + (wrappedFromLeft && link.range.end.x >= position.x) || + (wrappedToRight && link.range.start.x <= position.x)) && link.range.start.y <= position.y && - link.range.end.x >= position.x && link.range.end.y >= position.y; } From b07a6d10f107659dbb061abbfad8188ea9cc75c8 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Tue, 5 Nov 2019 23:44:30 -0600 Subject: [PATCH 10/26] Add test for multiline links --- addons/xterm-addon-web-links/src/WebLinksAddon.api.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 9f2ab508..977f51a6 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -52,7 +52,7 @@ describe('WebLinksAddon', () => { }); async function testHostName(hostname: string): Promise { - await openTerminal({ rendererType: 'dom' }); + await openTerminal({ rendererType: 'dom', cols: 40 }); await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); await page.evaluate(` window.term.writeln(' http://${hostname} '); @@ -62,6 +62,7 @@ async function testHostName(hostname: string): Promise { window.term.writeln('"http://${hostname}/"'); window.term.writeln('\\'http://${hostname}/\\''); window.term.writeln('http://${hostname}/subpath/+/id'); + window.term.writeln('http://${hostname}/subpath/subpath2/subpath3/subpath4/subpath5/+/id'); `); assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`); assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`); @@ -70,6 +71,7 @@ async function testHostName(hostname: string): Promise { assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`); assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`); assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`); + assert.equal(await getLinkAtCell(1, 8) + await getLinkAtCell(1, 9), `http://${hostname}/subpath/subpath2/subpath3/subpath4/subpath5/+/id`); } async function openTerminal(options: ITerminalOptions = {}): Promise { From f18a6fe1eb9424557d7d1164796ad08ed5dcc476 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Wed, 6 Nov 2019 22:52:09 -0600 Subject: [PATCH 11/26] Fixed bug in link detection for wrapped links --- src/browser/Linkifier2.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 0ae7c825..6f2d84c9 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -8,7 +8,6 @@ import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; import { IBufferService, ICoreService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { LinkRenderLayer } from './renderer/LinkRenderLayer'; export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; @@ -106,7 +105,7 @@ export class Linkifier2 implements ILinkifier2 { return; } - this._linkCache.forEach((cachedLink, i) => { + this._linkCache.forEach(cachedLink => { if (this._linkAtPosition(cachedLink.link, position)) { cachedLink.link.handle(event, cachedLink.link.url); } @@ -117,13 +116,16 @@ export class Linkifier2 implements ILinkifier2 { if (this._lastMouseEvent && this._mouseOverLink) { this._hideAllTooltips(); } + + this._linkCache = []; } private _onData(e: string): void { if (this._lastMouseEvent && this._mouseOverLink) { - const index = this._hideAllTooltips(); - this._linkCache.splice(index, 1); + this._hideAllTooltips(); } + + this._linkCache = []; } private _handleNewLink(link: ILink | undefined): void { @@ -185,20 +187,19 @@ export class Linkifier2 implements ILinkifier2 { } } - private _hideAllTooltips(): number { + private _hideAllTooltips(): void { if (!this._element) { - return -1; + return; } // Hide all the tooltips for (let i = 0; i < this._linkCache.length; i++) { if (this._linkCache[i].mouseOver) { this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); - return i; } } - return -1; + return; } /** @@ -215,7 +216,8 @@ export class Linkifier2 implements ILinkifier2 { // If not, then handle each case seperately, depending on which way it wraps return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) || (wrappedFromLeft && link.range.end.x >= position.x) || - (wrappedToRight && link.range.start.x <= position.x)) && + (wrappedToRight && link.range.start.x <= position.x) || + (wrappedFromLeft && wrappedToRight)) && link.range.start.y <= position.y && link.range.end.y >= position.y; } From 700c4acf7537a05043ead250ed72f8bc86d0e0b8 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 8 Nov 2019 02:14:53 -0600 Subject: [PATCH 12/26] Rework link parser to use a single cached link and make other requested changes --- .../src/WebLinksAddon.ts | 9 +- src/Terminal.ts | 6 +- src/browser/Linkifier2.ts | 183 ++++++++---------- src/browser/Types.d.ts | 2 +- src/browser/Viewport.ts | 6 +- 5 files changed, 95 insertions(+), 111 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index 09183014..a37fba65 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -45,10 +45,11 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - if ('registerLinkProvider' in this._terminal as any) { + if ('registerLinkProvider' in this._terminal) { this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler)); } else { - this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options); + // HACK: This is an older version of xterm.js, use registerLinkMatcher + this._linkMatcherId = (this._terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options); } } @@ -57,8 +58,6 @@ export class WebLinksAddon implements ITerminalAddon { this._terminal.deregisterLinkMatcher(this._linkMatcherId); } - if (this._linkProvider) { - this._linkProvider.dispose(); - } + this._linkProvider?.dispose(); } } diff --git a/src/Terminal.ts b/src/Terminal.ts index e6d90b78..e8e68017 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -246,7 +246,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._windowsMode = undefined; this._renderService?.dispose(); this._customKeyEventHandler = null; - this.write = () => {}; + this.write = () => { }; this.element?.parentNode?.removeChild(this.element); } @@ -286,7 +286,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, this._coreService); + this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService, this._coreService, this.onScroll.bind(this)); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); @@ -606,7 +606,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._viewportElement, this._mouseService); + 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))); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 6f2d84c9..b5a06df8 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, IBufferRange } from './Types'; +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent } from './Types'; import { IDisposable } from 'common/Types'; import { IMouseService } from './services/Services'; import { IBufferService, ICoreService } from 'common/services/Services'; @@ -11,12 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; - private _viewportElement: HTMLElement | undefined; private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; - private _linkCache: ICachedLink[] = []; + private _currentLink: ILink | undefined; private _lastMouseEvent: MouseEvent | undefined; - private _mouseOverLink: boolean = false; + private _linkCacheDisposables: IDisposable[] = []; + private _lastBufferCell: IBufferCellPosition | undefined; private _onShowTooltip = new EventEmitter(); public get onShowTooltip(): IEvent { return this._onShowTooltip.event; } @@ -25,7 +25,8 @@ export class Linkifier2 implements ILinkifier2 { constructor( private readonly _bufferService: IBufferService, - private readonly _coreService: ICoreService + private readonly _coreService: ICoreService, + private readonly _onScroll: IEvent ) { } @@ -35,21 +36,21 @@ export class Linkifier2 implements ILinkifier2 { return { dispose: () => { // Remove the link provider from the list - this._linkProviders.splice(this._linkProviders.indexOf(linkProvider), 1); + const providerIndex = this._linkProviders.indexOf(linkProvider); + + if (providerIndex !== -1) { + this._linkProviders.splice(providerIndex, 1); + } } }; } - public attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void { + public attachToDom(element: HTMLElement, mouseService: IMouseService): void { this._element = element; - this._viewportElement = viewportElement; this._mouseService = mouseService; this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); - this._viewportElement.addEventListener('scroll', this._onScroll.bind(this)); - - this._coreService.onData(this._onData.bind(this)); } private _onMouseMove(event: MouseEvent): void { @@ -65,37 +66,61 @@ export class Linkifier2 implements ILinkifier2 { return; } - // Check the cache for a link and determine if we need to show or hide tooltip - let mouseOver = false; - for (let i = 0; i < this._linkCache.length; i++) { - const cachedLink = this._linkCache[i].link; - const isInPosition = this._linkAtPosition(cachedLink, position); - - // Check if we need to hide the tooltip - if (!isInPosition && this._linkCache[i].mouseOver) { - // Hide the tooltip - this._hideTooltip(this._element, this._linkCache[i].link, event); - this._linkCache[i].mouseOver = false; - } - - if (isInPosition) { - mouseOver = true; - } + if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) { + this._onHover(position); + this._lastBufferCell = position; } + } - if (!mouseOver) { - this._mouseOverLink = false; - this._linkCache = []; + private _onHover(position: IBufferCellPosition): void { + if (this._currentLink) { + // Check the if the link is in the mouse position + const isInPosition = this._linkAtPosition(this._currentLink, position); + + // Check if we need to clear the link + if (!isInPosition) { + this._clearCurrentLink(); + } + } else { + const providerReplies: Map = new Map(); + let linkProvided = false; + + // There is no link cached, so ask for one + this._linkProviders.forEach((linkProvider, i) => { + linkProvider.provideLink(position, (link: ILink | undefined) => { + providerReplies.set(i, link); + + // Check if every provider before this one has come back undefined + let hasLinkBefore = false; + for (let j = 0; j < i; j++) { + if (!providerReplies.has(j) || providerReplies.get(j)) { + hasLinkBefore = true; + } + } + + // If all providers with higher priority came back undefined, then this link should be used + if (!hasLinkBefore && link) { + linkProvided = true; + this._handleNewLink(link); + } + + // Check if all the providers have responded + if (providerReplies.size === this._linkProviders.length && !linkProvided) { + // Respect the order of the link providers + for (let j = 0; j < providerReplies.size; j++) { + const currentLink = providerReplies.get(j); + if (currentLink) { + this._handleNewLink(currentLink); + } + } + } + }); + }); } - - // 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 { - if (!this._element || !this._mouseService) { + if (!this._element || !this._mouseService || !this._currentLink) { return; } @@ -105,42 +130,26 @@ export class Linkifier2 implements ILinkifier2 { return; } - this._linkCache.forEach(cachedLink => { - if (this._linkAtPosition(cachedLink.link, position)) { - cachedLink.link.handle(event, cachedLink.link.url); - } - }); - } - - private _onScroll(event: Event): void { - if (this._lastMouseEvent && this._mouseOverLink) { - this._hideAllTooltips(); + if (this._linkAtPosition(this._currentLink, position)) { + this._currentLink.handle(event, this._currentLink.url); } - - this._linkCache = []; } - private _onData(e: string): void { - if (this._lastMouseEvent && this._mouseOverLink) { - this._hideAllTooltips(); - } - - this._linkCache = []; - } - - private _handleNewLink(link: ILink | undefined): void { - if (!link || !this._element || !this._lastMouseEvent || !this._mouseService) { + private _clearCurrentLink(): void { + if (!this._element || !this._currentLink || !this._lastMouseEvent) { return; } - // Check if the link at this position is already cached - let linkIndex = this._linkCache.findIndex(cachedLink => { - return cachedLink.link.url === link.url && - cachedLink.link.range.start.x === link.range.start.x && - cachedLink.link.range.start.y === link.range.start.y && - cachedLink.link.range.end.x === link.range.end.x && - cachedLink.link.range.end.y === link.range.end.y; - }); + this._hideTooltip(this._element, this._currentLink, this._lastMouseEvent); + this._currentLink = undefined; + this._linkCacheDisposables.forEach(l => l.dispose()); + this._linkCacheDisposables = []; + } + + private _handleNewLink(link: ILink): void { + if (!this._element || !this._lastMouseEvent || !this._mouseService) { + return; + } const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService); @@ -148,18 +157,14 @@ export class Linkifier2 implements ILinkifier2 { return; } - const linkAtPosition = this._linkAtPosition(link, position); - - if (linkIndex === -1) { - this._linkCache.push({ link, mouseOver: false }); - linkIndex = this._linkCache.length - 1; - } - - // Show the tooltip if the last mouse event was over it - if (linkAtPosition && !this._linkCache[linkIndex].mouseOver) { + // Show the tooltip if the we have a link at the position + if (this._linkAtPosition(link, position)) { + this._currentLink = link; this._showTooltip(this._element, link, this._lastMouseEvent); - this._linkCache[linkIndex].mouseOver = true; - this._mouseOverLink = true; + + // Add listeners for onData and onScroll + this._linkCacheDisposables.push(this._coreService.onData(() => this._clearCurrentLink())); + this._linkCacheDisposables.push(this._onScroll(() => this._clearCurrentLink())); } } @@ -187,21 +192,6 @@ export class Linkifier2 implements ILinkifier2 { } } - private _hideAllTooltips(): void { - if (!this._element) { - return; - } - - // Hide all the tooltips - for (let i = 0; i < this._linkCache.length; i++) { - if (this._linkCache[i].mouseOver) { - this._hideTooltip(this._element, this._linkCache[i].link, new MouseEvent('invalid event')); - } - } - - return; - } - /** * Check if the buffer position is within the link * @param link @@ -214,8 +204,8 @@ export class Linkifier2 implements ILinkifier2 { // If the start and end have the same y, then the position must be between start and end x // If not, then handle each case seperately, depending on which way it wraps - return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) || - (wrappedFromLeft && link.range.end.x >= position.x) || + return ((sameLine && link.range.start.x <= position.x && link.range.end.x > position.x) || + (wrappedFromLeft && link.range.end.x > position.x) || (wrappedToRight && link.range.start.x <= position.x) || (wrappedFromLeft && wrappedToRight)) && link.range.start.y <= position.y && @@ -239,8 +229,3 @@ export class Linkifier2 implements ILinkifier2 { return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; } } - -interface ICachedLink { - link: ILink; - mouseOver: boolean; -} diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index c1db6242..fb19fead 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -98,7 +98,7 @@ export interface ILinkifier2 { onShowTooltip: IEvent; onHideTooltip: IEvent; - attachToDom(element: HTMLElement, viewportElement: HTMLElement, mouseService: IMouseService): void; + attachToDom(element: HTMLElement, mouseService: IMouseService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index b88f381d..29edce6f 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -167,7 +167,7 @@ export class Viewport extends Disposable implements IViewport { private _bubbleScroll(ev: Event, amount: number): boolean { const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight; if ((amount < 0 && this._viewportElement.scrollTop !== 0) || - (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) { + (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) { if (ev.cancelable) { ev.preventDefault(); } @@ -235,8 +235,8 @@ export class Viewport extends Disposable implements IViewport { const modifier = this._optionsService.options.fastScrollModifier; // Multiply the scroll speed when the modifier is down if ((modifier === 'alt' && ev.altKey) || - (modifier === 'ctrl' && ev.ctrlKey) || - (modifier === 'shift' && ev.shiftKey)) { + (modifier === 'ctrl' && ev.ctrlKey) || + (modifier === 'shift' && ev.shiftKey)) { return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity; } From 8924aa736e42fd27c681ed5233352e9014fc8ae8 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 8 Nov 2019 11:08:50 -0600 Subject: [PATCH 13/26] Use onRender() instead of onData() and onScroll() --- src/Terminal.ts | 4 ++-- src/browser/Linkifier2.ts | 19 ++++++++++--------- src/browser/Types.d.ts | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 704df379..5c9154fc 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -281,7 +281,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, this._coreService, this.onScroll.bind(this)); + this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService); if (this.options.windowsMode) { this._windowsMode = applyWindowsMode(this); @@ -600,7 +600,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.linkifier2.attachToDom(this.element, this._mouseService, this._renderService); // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index b5a06df8..cddb6998 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -5,14 +5,15 @@ import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent } from './Types'; import { IDisposable } from 'common/Types'; -import { IMouseService } from './services/Services'; +import { IMouseService, IRenderService } from './services/Services'; import { IBufferService, ICoreService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; export class Linkifier2 implements ILinkifier2 { private _element: HTMLElement | undefined; - private _linkProviders: ILinkProvider[] = []; private _mouseService: IMouseService | undefined; + private _renderService: IRenderService | undefined; + private _linkProviders: ILinkProvider[] = []; private _currentLink: ILink | undefined; private _lastMouseEvent: MouseEvent | undefined; private _linkCacheDisposables: IDisposable[] = []; @@ -24,9 +25,7 @@ export class Linkifier2 implements ILinkifier2 { public get onHideTooltip(): IEvent { return this._onHideTooltip.event; } constructor( - private readonly _bufferService: IBufferService, - private readonly _coreService: ICoreService, - private readonly _onScroll: IEvent + private readonly _bufferService: IBufferService ) { } @@ -45,9 +44,10 @@ export class Linkifier2 implements ILinkifier2 { }; } - public attachToDom(element: HTMLElement, mouseService: IMouseService): void { + public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void { this._element = element; this._mouseService = mouseService; + this._renderService = renderService; this._element.addEventListener('mousemove', this._onMouseMove.bind(this)); this._element.addEventListener('click', this._onMouseDown.bind(this)); @@ -162,9 +162,10 @@ export class Linkifier2 implements ILinkifier2 { this._currentLink = link; this._showTooltip(this._element, link, this._lastMouseEvent); - // Add listeners for onData and onScroll - this._linkCacheDisposables.push(this._coreService.onData(() => this._clearCurrentLink())); - this._linkCacheDisposables.push(this._onScroll(() => this._clearCurrentLink())); + // Add listener for rerendering + if (this._renderService) { + this._linkCacheDisposables.push(this._renderService.onRender(() => this._clearCurrentLink())); + } } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index fb19fead..387fa32e 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IDisposable } from 'common/Types'; -import { IMouseService } from './services/Services'; +import { IMouseService, IRenderService } from './services/Services'; export interface IColorManager { colors: IColorSet; @@ -98,7 +98,7 @@ export interface ILinkifier2 { onShowTooltip: IEvent; onHideTooltip: IEvent; - attachToDom(element: HTMLElement, mouseService: IMouseService): void; + attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; } From a1906347b07c766b9d889d38ac1d88e032e8cb12 Mon Sep 17 00:00:00 2001 From: Jon Bockhorst Date: Fri, 8 Nov 2019 12:10:00 -0600 Subject: [PATCH 14/26] Only clear link if it was in the range of onRender and fix tests --- src/browser/Linkifier2.ts | 90 ++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index cddb6998..6703e5fa 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -80,45 +80,50 @@ export class Linkifier2 implements ILinkifier2 { // Check if we need to clear the link if (!isInPosition) { this._clearCurrentLink(); + this._askForLink(position); } } else { - const providerReplies: Map = new Map(); - let linkProvided = false; - - // There is no link cached, so ask for one - this._linkProviders.forEach((linkProvider, i) => { - linkProvider.provideLink(position, (link: ILink | undefined) => { - providerReplies.set(i, link); - - // Check if every provider before this one has come back undefined - let hasLinkBefore = false; - for (let j = 0; j < i; j++) { - if (!providerReplies.has(j) || providerReplies.get(j)) { - hasLinkBefore = true; - } - } - - // If all providers with higher priority came back undefined, then this link should be used - if (!hasLinkBefore && link) { - linkProvided = true; - this._handleNewLink(link); - } - - // Check if all the providers have responded - if (providerReplies.size === this._linkProviders.length && !linkProvided) { - // Respect the order of the link providers - for (let j = 0; j < providerReplies.size; j++) { - const currentLink = providerReplies.get(j); - if (currentLink) { - this._handleNewLink(currentLink); - } - } - } - }); - }); + this._askForLink(position); } } + private _askForLink(position: IBufferCellPosition): void { + const providerReplies: Map = new Map(); + let linkProvided = false; + + // There is no link cached, so ask for one + this._linkProviders.forEach((linkProvider, i) => { + linkProvider.provideLink(position, (link: ILink | undefined) => { + providerReplies.set(i, link); + + // Check if every provider before this one has come back undefined + let hasLinkBefore = false; + for (let j = 0; j < i; j++) { + if (!providerReplies.has(j) || providerReplies.get(j)) { + hasLinkBefore = true; + } + } + + // If all providers with higher priority came back undefined, then this link should be used + if (!hasLinkBefore && link) { + linkProvided = true; + this._handleNewLink(link); + } + + // Check if all the providers have responded + if (providerReplies.size === this._linkProviders.length && !linkProvided) { + // Respect the order of the link providers + for (let j = 0; j < providerReplies.size; j++) { + const currentLink = providerReplies.get(j); + if (currentLink) { + this._handleNewLink(currentLink); + } + } + } + }); + }); + } + private _onMouseDown(event: MouseEvent): void { if (!this._element || !this._mouseService || !this._currentLink) { return; @@ -135,15 +140,18 @@ export class Linkifier2 implements ILinkifier2 { } } - private _clearCurrentLink(): void { + private _clearCurrentLink(startRow?: number, endRow?: number): void { if (!this._element || !this._currentLink || !this._lastMouseEvent) { return; } - this._hideTooltip(this._element, this._currentLink, this._lastMouseEvent); - this._currentLink = undefined; - this._linkCacheDisposables.forEach(l => l.dispose()); - this._linkCacheDisposables = []; + // If we have a start and end row, check that the link is within it + if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) { + this._hideTooltip(this._element, this._currentLink, this._lastMouseEvent); + this._currentLink = undefined; + this._linkCacheDisposables.forEach(l => l.dispose()); + this._linkCacheDisposables = []; + } } private _handleNewLink(link: ILink): void { @@ -164,7 +172,9 @@ export class Linkifier2 implements ILinkifier2 { // Add listener for rerendering if (this._renderService) { - this._linkCacheDisposables.push(this._renderService.onRender(() => this._clearCurrentLink())); + this._linkCacheDisposables.push(this._renderService.onRender((e: { start: number, end: number }) => { + this._clearCurrentLink(e.start + 1 + this._bufferService.buffer.ydisp, e.end + 1 + this._bufferService.buffer.ydisp); + })); } } } From a066bbe32fadc46507ade4e2feb9af93bd12e55e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 09:08:07 -0800 Subject: [PATCH 15/26] :lipstick: --- src/Terminal.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index aec51c82..987862b2 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -259,8 +259,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (!this.linkifier) { this.linkifier = new Linkifier(this._bufferService, this._logService, this.optionsService, this.unicodeService); } - - this.linkifier2 = this.linkifier2 || new Linkifier2(this._bufferService); + if (!this.linkifier2) { + this.linkifier2 = new Linkifier2(this._bufferService); + } if (this.options.windowsMode) { this._enableWindowsMode(); From ef071ac7acd4e2165509c5d73d8998c7faac6b00 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 09:30:12 -0800 Subject: [PATCH 16/26] Polish API --- typings/xterm.d.ts | 92 +++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index f1d85600..97d98beb 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -387,10 +387,10 @@ declare module 'xterm' { /** * Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t). - * + * * Most settings have no default implementation, as they heavily rely on * the embedding environment. - * + * * To implement a feature, create a custom CSI hook like this: * ```ts * term.parser.addCsiHandler({final: 't'}, params => { @@ -403,8 +403,8 @@ declare module 'xterm' { * return false; // any Ps that was not handled * }); * ``` - * - * Note on security: + * + * Note on security: * Most features are meant to deal with some information of the host machine * where the terminal runs on. This is seen as a security risk possibly leaking * sensitive data of the host to the program in the terminal. Therefore all options @@ -413,18 +413,18 @@ declare module 'xterm' { */ export interface IWindowOptions { /** - * Ps=1 De-iconify window. + * Ps=1 De-iconify window. * No default implementation. */ restoreWin?: boolean; /** - * Ps=2 Iconify window. + * Ps=2 Iconify window. * No default implementation. */ minimizeWin?: boolean; /** * Ps=3 ; x ; y - * Move window to [x, y]. + * Move window to [x, y]. * No default implementation. */ setWinPosition?: boolean; @@ -432,17 +432,17 @@ declare module 'xterm' { * Ps = 4 ; height ; width * Resize the window to given `height` and `width` in pixels. * Omitted parameters should reuse the current height or width. - * Zero parameters should use the display's height or width. + * Zero parameters should use the display's height or width. * No default implementation. */ setWinSizePixels?: boolean; /** - * Ps=5 Raise the window to the front of the stacking order. + * Ps=5 Raise the window to the front of the stacking order. * No default implementation. */ raiseWin?: boolean; /** - * Ps=6 Lower the xterm window to the bottom of the stacking order. + * Ps=6 Lower the xterm window to the bottom of the stacking order. * No default implementation. */ lowerWin?: boolean; @@ -452,7 +452,7 @@ declare module 'xterm' { * Ps = 8 ; height ; width * Resize the text area to given height and width in characters. * Omitted parameters should reuse the current height or width. - * Zero parameters use the display's height or width. + * Zero parameters use the display's height or width. * No default implementation. */ setWinSizeChars?: boolean; @@ -460,81 +460,81 @@ declare module 'xterm' { * Ps=9 ; 0 Restore maximized window. * Ps=9 ; 1 Maximize window (i.e., resize to screen size). * Ps=9 ; 2 Maximize window vertically. - * Ps=9 ; 3 Maximize window horizontally. + * Ps=9 ; 3 Maximize window horizontally. * No default implementation. */ maximizeWin?: boolean; /** * Ps=10 ; 0 Undo full-screen mode. * Ps=10 ; 1 Change to full-screen. - * Ps=10 ; 2 Toggle full-screen. + * Ps=10 ; 2 Toggle full-screen. * No default implementation. */ fullscreenWin?: boolean; /** Ps=11 Report xterm window state. * If the xterm window is non-iconified, it returns "CSI 1 t". - * If the xterm window is iconified, it returns "CSI 2 t". + * If the xterm window is iconified, it returns "CSI 2 t". * No default implementation. */ getWinState?: boolean; /** * Ps=13 Report xterm window position. Result is "CSI 3 ; x ; y t". - * Ps=13 ; 2 Report xterm text-area position. Result is "CSI 3 ; x ; y t". + * Ps=13 ; 2 Report xterm text-area position. Result is "CSI 3 ; x ; y t". * No default implementation. */ getWinPosition?: boolean; /** * Ps=14 Report xterm text area size in pixels. Result is "CSI 4 ; height ; width t". - * Ps=14 ; 2 Report xterm window size in pixels. Result is "CSI 4 ; height ; width t". + * Ps=14 ; 2 Report xterm window size in pixels. Result is "CSI 4 ; height ; width t". * Has a default implementation. */ getWinSizePixels?: boolean; /** - * Ps=15 Report size of the screen in pixels. Result is "CSI 5 ; height ; width t". + * Ps=15 Report size of the screen in pixels. Result is "CSI 5 ; height ; width t". * No default implementation. */ getScreenSizePixels?: boolean; /** - * Ps=16 Report xterm character cell size in pixels. Result is "CSI 6 ; height ; width t". + * Ps=16 Report xterm character cell size in pixels. Result is "CSI 6 ; height ; width t". * Has a default implementation. */ getCellSizePixels?: boolean; /** - * Ps=18 Report the size of the text area in characters. Result is "CSI 8 ; height ; width t". + * Ps=18 Report the size of the text area in characters. Result is "CSI 8 ; height ; width t". * Has a default implementation. */ getWinSizeChars?: boolean; /** - * Ps=19 Report the size of the screen in characters. Result is "CSI 9 ; height ; width t". + * Ps=19 Report the size of the screen in characters. Result is "CSI 9 ; height ; width t". * No default implementation. */ getScreenSizeChars?: boolean; /** - * Ps=20 Report xterm window's icon label. Result is "OSC L label ST". + * Ps=20 Report xterm window's icon label. Result is "OSC L label ST". * No default implementation. */ getIconTitle?: boolean; /** - * Ps=21 Report xterm window's title. Result is "OSC l label ST". + * Ps=21 Report xterm window's title. Result is "OSC l label ST". * No default implementation. */ getWinTitle?: boolean; /** * Ps=22 ; 0 Save xterm icon and window title on stack. * Ps=22 ; 1 Save xterm icon title on stack. - * Ps=22 ; 2 Save xterm window title on stack. + * Ps=22 ; 2 Save xterm window title on stack. * All variants have a default implementation. */ pushTitle?: boolean; /** * Ps=23 ; 0 Restore xterm icon and window title from stack. * Ps=23 ; 1 Restore xterm icon title from stack. - * Ps=23 ; 2 Restore xterm window title from stack. + * Ps=23 ; 2 Restore xterm window title from stack. * All variants have a default implementation. */ popTitle?: boolean; /** - * Ps>=24 Resize to Ps lines (DECSLPP). + * Ps>=24 Resize to Ps lines (DECSLPP). * DECSLPP is not implemented. This settings is also used to * enable / disable DECCOLM (earlier variant of DECSLPP). */ @@ -739,8 +739,9 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Registers a link provider, allowing a custom parser to - * be used to match and handle links. - * @param linkProvider + * be used to match and handle links. Multiple link providers can be used, + * they will be asked in the order in which they are registered. + * @param linkProvider The link provider to use to detect links. */ registerLinkProvider(linkProvider: ILinkProvider): IDisposable; @@ -1081,79 +1082,80 @@ declare module 'xterm' { } /** - * A custom link provider + * A custom link provider. */ interface ILinkProvider { /** * Provides a link a buffer position - * @param position - * @param callback + * @param position The position of the buffer that is currently active. + * @param callback The callback to be fired with the resulting link or + * `undefined` when ready. */ provideLink(position: IBufferCellPosition, callback: (link: ILink | undefined) => void): void; } /** - * A link + * A link within the terminal. */ interface ILink { /** - * The buffer range of the link + * The buffer range of the link. */ range: IBufferRange; /** - * The url of the link + * The url of the link. */ url: string; /** - * The show tooltip callback - * @param event + * Called when the link's tooltip is ready to show. + * @param event The mouse event triggering the callback. * @param link */ showTooltip?(event: MouseEvent, link: string): void; /** - * The hide tooltip callback - * @param event + * Called when the link's tooltip is ready to hide. + * @param event The mouse event triggering the callback. * @param link */ hideTooltip?(event: MouseEvent, link: string): void; /** - * Handles when the link is opened - * @param event + * Calls when the link is activated. + * @param event The mouse event triggering the callback. * @param link */ handle(event: MouseEvent, link: string): void; } /** - * A range in the buffer + * A range within a buffer. */ interface IBufferRange { /** - * The start position of the range + * The start position of the range. */ start: IBufferCellPosition; /** - * The end position of the range + * The end position of the range. */ end: IBufferCellPosition; } /** - * A position in the buffer + * A position within a buffer. */ interface IBufferCellPosition { /** - * The x of the buffer position + * The x position within the buffer. */ x: number; /** - * The y of the buffer position + * The y position within the buffer. */ y: number; } From c327116e7ff74caf740d97f9fbe532ddde433d5b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 09:39:26 -0800 Subject: [PATCH 17/26] url -> text --- .../xterm-addon-web-links/src/WebLinkProvider.ts | 12 ++++++------ src/Terminal.ts | 4 ++-- src/browser/Linkifier2.ts | 6 +++--- src/browser/Types.d.ts | 8 ++++---- typings/xterm.d.ts | 16 ++++++++-------- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index 4c3702bb..fe35892b 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -30,8 +30,8 @@ export class LinkComputer { let stringIndex = -1; while ((match = rex.exec(line)) !== null) { - const url = match[1]; - if (!url) { + const text = match[1]; + if (!text) { // something matched but does not comply with the given matchIndex // since this is most likely a bug the regex itself we simply do nothing here console.log('match found without corresponding matchIndex'); @@ -42,14 +42,14 @@ export class LinkComputer { // therefore we cannot use match.index directly, instead we search the position // of the match group in text again // also correct regex and string search offsets for the next loop run - stringIndex = line.indexOf(url, stringIndex + 1); - rex.lastIndex = stringIndex + url.length; + stringIndex = line.indexOf(text, stringIndex + 1); + rex.lastIndex = stringIndex + text.length; if (stringIndex < 0) { // invalid stringIndex (should not have happened) break; } - let endX = stringIndex + url.length + 1; + let endX = stringIndex + text.length + 1; let endY = startLineIndex + 1; while (endX > terminal.cols) { @@ -68,7 +68,7 @@ export class LinkComputer { } }; - return { range, url, handle }; + return { range, text, handle }; } } diff --git a/src/Terminal.ts b/src/Terminal.ts index 987862b2..c432e213 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -685,8 +685,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; diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 6703e5fa..b6a7bf75 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -136,7 +136,7 @@ export class Linkifier2 implements ILinkifier2 { } if (this._linkAtPosition(this._currentLink, position)) { - this._currentLink.handle(event, this._currentLink.url); + this._currentLink.handle(event, this._currentLink.text); } } @@ -187,7 +187,7 @@ export class Linkifier2 implements ILinkifier2 { element.classList.add('xterm-cursor-pointer'); if (link.showTooltip) { - link.showTooltip(event, link.url); + link.showTooltip(event, link.text); } } @@ -199,7 +199,7 @@ export class Linkifier2 implements ILinkifier2 { element.classList.remove('xterm-cursor-pointer'); if (link.hideTooltip) { - link.hideTooltip(event, link.url); + link.hideTooltip(event, link.text); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index f6b17bbf..77ef50a3 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -171,10 +171,10 @@ interface ILinkProvider { interface ILink { range: IBufferRange; - url: string; - showTooltip?(event: MouseEvent, link: string): void; - hideTooltip?(event: MouseEvent, link: string): void; - handle(event: MouseEvent, link: string): void; + text: string; + showTooltip?(event: MouseEvent, text: string): void; + hideTooltip?(event: MouseEvent, text: string): void; + handle(event: MouseEvent, text: string): void; } interface IBufferRange { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 97d98beb..cf5c311d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1104,30 +1104,30 @@ declare module 'xterm' { range: IBufferRange; /** - * The url of the link. + * The text of the link. */ - url: string; + text: string; /** * Called when the link's tooltip is ready to show. * @param event The mouse event triggering the callback. - * @param link + * @param text The text of the link. */ - showTooltip?(event: MouseEvent, link: string): void; + showTooltip?(event: MouseEvent, teext: string): void; /** * Called when the link's tooltip is ready to hide. * @param event The mouse event triggering the callback. - * @param link + * @param text The text of the link. */ - hideTooltip?(event: MouseEvent, link: string): void; + hideTooltip?(event: MouseEvent, text: string): void; /** * Calls when the link is activated. * @param event The mouse event triggering the callback. - * @param link + * @param text The text of the link. */ - handle(event: MouseEvent, link: string): void; + handle(event: MouseEvent, text: string): void; } /** From b18ded0e2dba93c2122f429b75a52ce4c9363da0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 09:54:51 -0800 Subject: [PATCH 18/26] Rename tooltip to hover/leave --- .../src/renderLayer/LinkRenderLayer.ts | 4 +-- src/browser/Linkifier2.ts | 28 +++++++++---------- src/browser/Types.d.ts | 8 +++--- src/browser/renderer/LinkRenderLayer.ts | 4 +-- src/browser/renderer/dom/DomRenderer.ts | 4 +-- typings/xterm.d.ts | 28 +++++++++---------- 6 files changed, 38 insertions(+), 38 deletions(-) diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 0f7e3a11..ebf773d3 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -19,8 +19,8 @@ export class LinkRenderLayer extends BaseRenderLayer { 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)); + terminal.linkifier2.onLinkHover(e => this._onLinkHover(e)); + terminal.linkifier2.onLinkLeave(e => this._onLinkLeave(e)); } public resize(terminal: Terminal, dim: IRenderDimensions): void { diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index b6a7bf75..c9cb20e3 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -19,10 +19,10 @@ export class Linkifier2 implements ILinkifier2 { private _linkCacheDisposables: IDisposable[] = []; private _lastBufferCell: IBufferCellPosition | undefined; - private _onShowTooltip = new EventEmitter(); - public get onShowTooltip(): IEvent { return this._onShowTooltip.event; } - private _onHideTooltip = new EventEmitter(); - public get onHideTooltip(): IEvent { return this._onHideTooltip.event; } + private _onLinkHover = new EventEmitter(); + public get onLinkHover(): IEvent { return this._onLinkHover.event; } + private _onLinkLeave = new EventEmitter(); + public get onLinkLeave(): IEvent { return this._onLinkLeave.event; } constructor( private readonly _bufferService: IBufferService @@ -147,7 +147,7 @@ export class Linkifier2 implements ILinkifier2 { // If we have a start and end row, check that the link is within it if (!startRow || !endRow || (this._currentLink.range.start.y >= startRow && this._currentLink.range.end.y <= endRow)) { - this._hideTooltip(this._element, this._currentLink, this._lastMouseEvent); + this._linkLeave(this._element, this._currentLink, this._lastMouseEvent); this._currentLink = undefined; this._linkCacheDisposables.forEach(l => l.dispose()); this._linkCacheDisposables = []; @@ -168,7 +168,7 @@ export class Linkifier2 implements ILinkifier2 { // Show the tooltip if the we have a link at the position if (this._linkAtPosition(link, position)) { this._currentLink = link; - this._showTooltip(this._element, link, this._lastMouseEvent); + this._linkHover(this._element, link, this._lastMouseEvent); // Add listener for rerendering if (this._renderService) { @@ -179,27 +179,27 @@ export class Linkifier2 implements ILinkifier2 { } } - private _showTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + private _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void { const range = link.range; const scrollOffset = this._bufferService.buffer.ydisp; - this._onShowTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + this._onLinkHover.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); element.classList.add('xterm-cursor-pointer'); - if (link.showTooltip) { - link.showTooltip(event, link.text); + if (link.hover) { + link.hover(event, link.text); } } - private _hideTooltip(element: HTMLElement, link: ILink, event: MouseEvent): void { + private _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void { const range = link.range; const scrollOffset = this._bufferService.buffer.ydisp; - this._onHideTooltip.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); + this._onLinkLeave.fire(this._createLinkHoverEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x - 1, range.end.y - scrollOffset - 1, undefined)); element.classList.remove('xterm-cursor-pointer'); - if (link.hideTooltip) { - link.hideTooltip(event, link.text); + if (link.leave) { + link.leave(event, link.text); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 77ef50a3..12676b80 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -107,8 +107,8 @@ export interface ILinkifier { } export interface ILinkifier2 { - onShowTooltip: IEvent; - onHideTooltip: IEvent; + onLinkHover: IEvent; + onLinkLeave: IEvent; attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; @@ -172,9 +172,9 @@ interface ILinkProvider { interface ILink { range: IBufferRange; text: string; - showTooltip?(event: MouseEvent, text: string): void; - hideTooltip?(event: MouseEvent, text: string): void; handle(event: MouseEvent, text: string): void; + hover?(event: MouseEvent, text: string): void; + leave?(event: MouseEvent, text: string): void; } interface IBufferRange { diff --git a/src/browser/renderer/LinkRenderLayer.ts b/src/browser/renderer/LinkRenderLayer.ts index c7e2b140..73e9f85f 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/src/browser/renderer/LinkRenderLayer.ts @@ -27,8 +27,8 @@ export class LinkRenderLayer extends BaseRenderLayer { linkifier.onLinkHover(e => this._onLinkHover(e)); linkifier.onLinkLeave(e => this._onLinkLeave(e)); - linkifier2.onShowTooltip(e => this._onLinkHover(e)); - linkifier2.onHideTooltip(e => this._onLinkLeave(e)); + linkifier2.onLinkHover(e => this._onLinkHover(e)); + linkifier2.onLinkLeave(e => this._onLinkLeave(e)); } public resize(dim: IRenderDimensions): void { diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index fb7abc55..3edbd97f 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -90,8 +90,8 @@ export class DomRenderer extends Disposable implements IRenderer { this._linkifier.onLinkHover(e => this._onLinkHover(e)); this._linkifier.onLinkLeave(e => this._onLinkLeave(e)); - this._linkifier2.onShowTooltip(e => this._onLinkHover(e)); - this._linkifier2.onHideTooltip(e => this._onLinkLeave(e)); + this._linkifier2.onLinkHover(e => this._onLinkHover(e)); + this._linkifier2.onLinkLeave(e => this._onLinkLeave(e)); } public dispose(): void { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cf5c311d..fac30fd6 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1108,26 +1108,26 @@ declare module 'xterm' { */ text: string; - /** - * Called when the link's tooltip is ready to show. - * @param event The mouse event triggering the callback. - * @param text The text of the link. - */ - showTooltip?(event: MouseEvent, teext: string): void; - - /** - * Called when the link's tooltip is ready to hide. - * @param event The mouse event triggering the callback. - * @param text The text of the link. - */ - hideTooltip?(event: MouseEvent, text: string): void; - /** * Calls when the link is activated. * @param event The mouse event triggering the callback. * @param text The text of the link. */ handle(event: MouseEvent, text: string): void; + + /** + * Called when the mouse hovers the link. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + hover?(event: MouseEvent, text: string): void; + + /** + * Called when the mouse leaves the link. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + leave?(event: MouseEvent, text: string): void; } /** From f4a31251cb4773a3a6602ae339c681692d67a734 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 09:59:25 -0800 Subject: [PATCH 19/26] handle -> activate --- addons/xterm-addon-web-links/src/WebLinkProvider.ts | 4 ++-- src/browser/Linkifier2.ts | 2 +- src/browser/Types.d.ts | 2 +- typings/xterm.d.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinkProvider.ts b/addons/xterm-addon-web-links/src/WebLinkProvider.ts index fe35892b..d32a8166 100644 --- a/addons/xterm-addon-web-links/src/WebLinkProvider.ts +++ b/addons/xterm-addon-web-links/src/WebLinkProvider.ts @@ -21,7 +21,7 @@ export class WebLinkProvider implements ILinkProvider { } export class LinkComputer { - public static computeLink(position: IBufferCellPosition, regex: RegExp, terminal: Terminal, handle: (event: MouseEvent, uri: string) => void): ILink | undefined { + public static computeLink(position: IBufferCellPosition, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink | undefined { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(position.y - 1, false, terminal); @@ -68,7 +68,7 @@ export class LinkComputer { } }; - return { range, text, handle }; + return { range, text, activate: handler }; } } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index c9cb20e3..7e065c31 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -136,7 +136,7 @@ export class Linkifier2 implements ILinkifier2 { } if (this._linkAtPosition(this._currentLink, position)) { - this._currentLink.handle(event, this._currentLink.text); + this._currentLink.activate(event, this._currentLink.text); } } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 12676b80..c2b8604e 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -172,7 +172,7 @@ interface ILinkProvider { interface ILink { range: IBufferRange; text: string; - handle(event: MouseEvent, text: string): void; + activate(event: MouseEvent, text: string): void; hover?(event: MouseEvent, text: string): void; leave?(event: MouseEvent, text: string): void; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index fac30fd6..0430d62c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1113,7 +1113,7 @@ declare module 'xterm' { * @param event The mouse event triggering the callback. * @param text The text of the link. */ - handle(event: MouseEvent, text: string): void; + activate(event: MouseEvent, text: string): void; /** * Called when the mouse hovers the link. From 0103c689f7966c4909f51855b90053bdfe15d4cb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 11:16:41 -0800 Subject: [PATCH 20/26] Start registerLinkProvider tests Bugs fixed: - Not breaking on first link match for multiple providers - Hover event wasn't fired when on last character of link --- src/browser/Linkifier2.ts | 7 +-- test/api/Terminal.api.ts | 102 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 7e065c31..38bd12ac 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -117,6 +117,7 @@ export class Linkifier2 implements ILinkifier2 { const currentLink = providerReplies.get(j); if (currentLink) { this._handleNewLink(currentLink); + break; } } } @@ -165,7 +166,7 @@ export class Linkifier2 implements ILinkifier2 { return; } - // Show the tooltip if the we have a link at the position + // Trigger hover if the we have a link at the position if (this._linkAtPosition(link, position)) { this._currentLink = link; this._linkHover(this._element, link, this._lastMouseEvent); @@ -215,8 +216,8 @@ export class Linkifier2 implements ILinkifier2 { // If the start and end have the same y, then the position must be between start and end x // If not, then handle each case seperately, depending on which way it wraps - return ((sameLine && link.range.start.x <= position.x && link.range.end.x > position.x) || - (wrappedFromLeft && link.range.end.x > position.x) || + return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) || + (wrappedFromLeft && link.range.end.x >= position.x) || (wrappedToRight && link.range.start.x <= position.x) || (wrappedFromLeft && wrappedToRight)) && link.range.start.y <= position.y && diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index cdfab61f..6c4fb10c 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -524,6 +524,60 @@ describe('API Integration Tests', function(): void { await page.evaluate(`window.term.dispose()`); assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); }); + + describe.only('registerLinkProvider', () => { + it('should fire provideLink when hovering cells', async () => { + await openTerminal({ rendererType: 'dom' }); + await page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLink: (position, cb) => { + calls.push(position); + cb(undefined); + } + }); + `); + const dims = await getDimensions(); + await moveMouseToCell(page, dims, 1, 1); + await moveMouseToCell(page, dims, 2, 2); + await moveMouseToCell(page, dims, 10, 4); + await pollFor(page, `window.calls`, [{ x: 1, y: 1 }, { x: 2, y: 2 }, { x: 10, y: 4 }]); + await page.evaluate(`window.disposable.dispose()`); + }); + + it('should fire hover and leave events on the link', async () => { + await openTerminal({ rendererType: 'dom' }); + await writeSync(page, 'foo bar baz'); + await page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLink: (position, cb) => { + window.calls.push('provide'); + if (position.x >= 5 && position.x <= 7 && position.y === 1) { + window.calls.push('match'); + cb({ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover'), + leave: () => window.calls.push('leave') + }); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseToCell(page, dims, 5, 1); + await pollFor(page, `window.calls`, ['provide', 'match', 'hover']); + await moveMouseToCell(page, dims, 4, 1); + await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide']); + await moveMouseToCell(page, dims, 7, 1); + await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide', 'provide', 'match', 'hover']); + await moveMouseToCell(page, dims, 8, 1); + await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide', 'provide', 'match', 'hover', 'leave', 'provide']); + await page.evaluate(`window.disposable.dispose()`); + }); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -535,3 +589,51 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { await page.waitForSelector('.xterm-text-layer'); } } + +interface IDimensions { + top: number; + left: number; + renderDimensions: IRenderDimensions; +} + +interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +async function getDimensions(): Promise { + return await page.evaluate(` + (function() { + const rect = document.querySelector('.xterm-rows').getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + renderDimensions: window.term._core._renderService.dimensions + }; + })(); + `); +} + +async function getCellCoordinates(dimensions: IDimensions, col: number, row: number): Promise<{ x: number, y: number }> { + return { + x: dimensions.left + dimensions.renderDimensions.scaledCellWidth * (col - 0.5), + y: dimensions.top + dimensions.renderDimensions.scaledCellHeight * (row - 0.5) + }; +} + +async function moveMouseToCell(page: puppeteer.Page, dimensions: IDimensions, col: number, row: number) { + const coords = await getCellCoordinates(dimensions, col, row); + await page.mouse.move(coords.x, coords.y); + // Timeout is needed here otherwise the browser may drop events + await timeout(0); +} From 7f1f22146d036a7dfa4cc052963daa8238f10e42 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 11:52:58 -0800 Subject: [PATCH 21/26] Add test for activate --- test/api/Terminal.api.ts | 77 ++++++++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 6c4fb10c..28a479d3 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -538,9 +538,9 @@ describe('API Integration Tests', function(): void { }); `); const dims = await getDimensions(); - await moveMouseToCell(page, dims, 1, 1); - await moveMouseToCell(page, dims, 2, 2); - await moveMouseToCell(page, dims, 10, 4); + await moveMouseCell(page, dims, 1, 1); + await moveMouseCell(page, dims, 2, 2); + await moveMouseCell(page, dims, 10, 4); await pollFor(page, `window.calls`, [{ x: 1, y: 1 }, { x: 2, y: 2 }, { x: 10, y: 4 }]); await page.evaluate(`window.disposable.dispose()`); }); @@ -548,11 +548,13 @@ describe('API Integration Tests', function(): void { it('should fire hover and leave events on the link', async () => { await openTerminal({ rendererType: 'dom' }); await writeSync(page, 'foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); await page.evaluate(` window.calls = []; window.disposable = window.term.registerLinkProvider({ provideLink: (position, cb) => { - window.calls.push('provide'); + window.calls.push('provide ' + position.x + ',' + position.y); if (position.x >= 5 && position.x <= 7 && position.y === 1) { window.calls.push('match'); cb({ @@ -567,14 +569,61 @@ describe('API Integration Tests', function(): void { }); `); const dims = await getDimensions(); - await moveMouseToCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide', 'match', 'hover']); - await moveMouseToCell(page, dims, 4, 1); - await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide']); - await moveMouseToCell(page, dims, 7, 1); - await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide', 'provide', 'match', 'hover']); - await moveMouseToCell(page, dims, 8, 1); - await pollFor(page, `window.calls`, ['provide', 'match', 'hover', 'leave', 'provide', 'provide', 'match', 'hover', 'leave', 'provide']); + await moveMouseCell(page, dims, 5, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'hover']); + await moveMouseCell(page, dims, 4, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'hover', 'leave', 'provide 4,1']); + await moveMouseCell(page, dims, 7, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'hover', 'leave', 'provide 4,1', 'provide 7,1', 'match', 'hover']); + await moveMouseCell(page, dims, 8, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'hover', 'leave', 'provide 4,1', 'provide 7,1', 'match', 'hover', 'leave', 'provide 8,1']); + await page.evaluate(`window.disposable.dispose()`); + }); + + it('should fire activate events when clicking the link', async () => { + await openTerminal({ rendererType: 'dom' }); + await writeSync(page, 'a b c'); + + // Wait for renderer to catch up as links are cleared on render + await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); + + // Focus terminal to avoid a render event clearing the active link + const dims = await getDimensions(); + await moveMouseCell(page, dims, 5, 5); + await page.mouse.down(); + await page.mouse.up(); + await timeout(50); // Not sure how to avoid this timeout, checking for xterm-focus doesn't help + + await page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLink: (position, cb) => { + window.calls.push('provide ' + position.x + ',' + position.y); + cb({ + range: { start: position, end: position }, + text: window.term.buffer.getLine(position.y - 1).getCell(position.x - 1).getChars(), + activate: (_, text) => window.calls.push('activate ' + text), + hover: () => window.calls.push('hover'), + leave: () => window.calls.push('leave') + }); + } + }); + `); + await moveMouseCell(page, dims, 3, 1); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover']); + await page.mouse.down(); + await page.mouse.up(); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover', 'activate b']); + await moveMouseCell(page, dims, 1, 1); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover', 'activate b', 'leave', 'provide 1,1', 'hover']); + await page.mouse.down(); + await page.mouse.up(); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover', 'activate b', 'leave', 'provide 1,1', 'hover', 'activate a']); + await moveMouseCell(page, dims, 5, 1); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover', 'activate b', 'leave', 'provide 1,1', 'hover', 'activate a', 'leave', 'provide 5,1', 'hover']); + await page.mouse.down(); + await page.mouse.up(); + await pollFor(page, `window.calls`, ['provide 3,1', 'hover', 'activate b', 'leave', 'provide 1,1', 'hover', 'activate a', 'leave', 'provide 5,1', 'hover', 'activate c']); await page.evaluate(`window.disposable.dispose()`); }); }); @@ -631,9 +680,7 @@ async function getCellCoordinates(dimensions: IDimensions, col: number, row: num }; } -async function moveMouseToCell(page: puppeteer.Page, dimensions: IDimensions, col: number, row: number) { +async function moveMouseCell(page: puppeteer.Page, dimensions: IDimensions, col: number, row: number) { const coords = await getCellCoordinates(dimensions, col, row); await page.mouse.move(coords.x, coords.y); - // Timeout is needed here otherwise the browser may drop events - await timeout(0); } From e4875f26ff95a9d2f6175703ce7a8cffd1357a05 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 7 Feb 2020 11:54:07 -0800 Subject: [PATCH 22/26] Add test for absense of hover/leave --- test/api/Terminal.api.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 28a479d3..36966bfb 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -525,7 +525,7 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); }); - describe.only('registerLinkProvider', () => { + describe('registerLinkProvider', () => { it('should fire provideLink when hovering cells', async () => { await openTerminal({ rendererType: 'dom' }); await page.evaluate(` @@ -580,6 +580,39 @@ describe('API Integration Tests', function(): void { await page.evaluate(`window.disposable.dispose()`); }); + it('should work fine when hover and leave callbacks are not provided', async () => { + await openTerminal({ rendererType: 'dom' }); + await writeSync(page, 'foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLink: (position, cb) => { + window.calls.push('provide ' + position.x + ',' + position.y); + if (position.x >= 5 && position.x <= 7 && position.y === 1) { + window.calls.push('match'); + cb({ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate') + }); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(page, dims, 5, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match']); + await moveMouseCell(page, dims, 4, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'provide 4,1']); + await moveMouseCell(page, dims, 7, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'provide 4,1', 'provide 7,1', 'match']); + await moveMouseCell(page, dims, 8, 1); + await pollFor(page, `window.calls`, ['provide 5,1', 'match', 'provide 4,1', 'provide 7,1', 'match', 'provide 8,1']); + await page.evaluate(`window.disposable.dispose()`); + }); + it('should fire activate events when clicking the link', async () => { await openTerminal({ rendererType: 'dom' }); await writeSync(page, 'a b c'); From 54743eda16fc55784fac6f10241829a4695aaae3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Feb 2020 10:52:36 -0800 Subject: [PATCH 23/26] Make link matcher API the default for web links addon --- addons/xterm-addon-web-links/src/WebLinksAddon.ts | 9 ++++++--- demo/client.ts | 3 ++- src/browser/Linkifier2.ts | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index a1b77c30..c56910ad 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -43,7 +43,8 @@ export class WebLinksAddon implements ITerminalAddon { constructor( private _handler: (event: MouseEvent, uri: string) => void = handleLink, - private _options: ILinkMatcherOptions = {} + private _options: ILinkMatcherOptions = {}, + private _useLinkProvider: boolean = false ) { this._options.matchIndex = 1; } @@ -51,10 +52,12 @@ export class WebLinksAddon implements ITerminalAddon { public activate(terminal: Terminal): void { this._terminal = terminal; - if ('registerLinkProvider' in this._terminal) { + if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) { + console.log('link provider'); this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler)); } else { - // HACK: This is an older version of xterm.js, use registerLinkMatcher + console.log('link matcher'); + // TODO: This should be removed eventually this._linkMatcherId = (this._terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options); } } diff --git a/demo/client.ts b/demo/client.ts index 6174216c..b009a534 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -152,7 +152,8 @@ function createTerminal(): void { addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.unicode11.instance = new Unicode11Addon(); - addons['web-links'].instance = new WebLinksAddon(); + // TODO: Remove arguments when link provider API is the default + addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 38bd12ac..2eeec1df 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -173,7 +173,7 @@ export class Linkifier2 implements ILinkifier2 { // Add listener for rerendering if (this._renderService) { - this._linkCacheDisposables.push(this._renderService.onRender((e: { start: number, end: number }) => { + this._linkCacheDisposables.push(this._renderService.onRender(e => { this._clearCurrentLink(e.start + 1 + this._bufferService.buffer.ydisp, e.end + 1 + this._bufferService.buffer.ydisp); })); } From a7c2d54e53dd8d91fd60891301240e9c6ba97118 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Feb 2020 10:54:29 -0800 Subject: [PATCH 24/26] Document link provider argument in web links addon --- .../typings/xterm-addon-web-links.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts index 1c53bcde..f0564704 100644 --- a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts +++ b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts @@ -15,8 +15,12 @@ declare module 'xterm-addon-web-links' { * Creates a new web links addon. * @param handler The callback when the link is called. * @param options Options for the link matcher. + * @param useLinkProvider Whether to use the new link provider API to create + * the links. This is an option because use of both link matcher (old) and + * link provider (new) may cause issues. Link provider will eventually be + * the default and only option. */ - constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions); + constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions, useLinkProvider?: boolean); /** * Activates the addon From 0e302baa43f5ba2d8b30aa76bf8764b7fabd5f8e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Feb 2020 10:58:58 -0800 Subject: [PATCH 25/26] Mark link matcher API as deprecated --- typings/xterm.d.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0430d62c..4fb50ae3 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -722,6 +722,8 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to * be matched and handled. + * @deprecated The link matcher API is now deprecated in favor of the link + * provider API, see `registerLinkProvider`. * @param regex The regular expression to search for, specifically this * searches the textContent of the rows. You will want to use \s to match a * space ' ' character for example. @@ -733,6 +735,8 @@ declare module 'xterm' { /** * (EXPERIMENTAL) Deregisters a link matcher if it has been registered. + * @deprecated The link matcher API is now deprecated in favor of the link + * provider API, see `registerLinkProvider`. * @param matcherId The link matcher's ID (returned after register) */ deregisterLinkMatcher(matcherId: number): void; From d1d27b53675882a1b79631d51998c4c257c66da9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Feb 2020 11:04:04 -0800 Subject: [PATCH 26/26] Remove logs --- addons/xterm-addon-web-links/src/WebLinksAddon.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index c56910ad..8d0e40ee 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -53,10 +53,8 @@ export class WebLinksAddon implements ITerminalAddon { this._terminal = terminal; if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) { - console.log('link provider'); this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler)); } else { - console.log('link matcher'); // TODO: This should be removed eventually this._linkMatcherId = (this._terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options); }