From 47845d9cf4ff9ad31a3c31df7fd589f46b7b2f46 Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Mon, 14 Jan 2019 18:19:48 -0800 Subject: [PATCH] Updated prototype for hyperlink ansi ecsapes (issue #1134). --- src/InputHandler.ts | 38 +++++++++++++++++++++++++++++++-- src/Linkifier.ts | 41 +++++++++++++++++++++++------------ src/Types.ts | 46 ++++++++++++++++++++++------------------ src/ui/TestUtils.test.ts | 1 + 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index eb1ca1050b..2d6661a37f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,10 +4,10 @@ * @license MIT */ -import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal, ILinkOptions } from './Types'; import { C0, C1 } from './common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; import { EscapeSequenceParser } from './EscapeSequenceParser'; @@ -115,6 +115,9 @@ class DECRQSS implements IDcsHandler { */ export class InputHandler extends Disposable implements IInputHandler { private _surrogateFirst: string; + private _linkStartX: number; + private _linkStartY: number; + private _linkUri: string; constructor( protected _terminal: IInputHandlingTerminal, @@ -219,6 +222,8 @@ export class InputHandler extends Disposable implements IInputHandler { // 5 - Change Special Color Number // 6 - Enable/disable Special Color Number c // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 8 - create hyperlink (https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) + this._parser.setOscHandler(8, (data) => this.createHyperLink(data)); // 10 - Change VT100 text foreground color to Pt. // 11 - Change VT100 text background color to Pt. // 12 - Change text cursor color to Pt. @@ -1906,6 +1911,35 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.handleTitle(data); } + public createHyperLink(data: string): void { + const m = data.match(/([^;]*);(.*)/); + if (!m) { + return; + } + // const params = m[1]; + const uri = m[2]; + if (uri) { + this._linkStartX = this._terminal.buffer.x; + this._linkStartY = this._terminal.buffer.y; + this._linkUri = uri; + } else { + const startX = this._linkStartX; + const startY = this._linkStartY; + const endX = this._terminal.buffer.x; + const endY = this._terminal.buffer.y; + const uri = this._linkUri; + const line = this._terminal.buffer.lines.get(startY); + const char = line.get(startX); + let fg: number | undefined; + if (char) { + const attr: number = char[CHAR_DATA_ATTR_INDEX]; + fg = (attr >> 9) & 0x1ff; + } + const options: ILinkOptions = {}; + this._terminal.linkifier.doAddLink(startX, startY, endX, endY, options, uri, fg); + } + } + /** * ESC E * C1.NEL diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 53247c95f8..67d8baed5c 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,7 +4,7 @@ */ import { IMouseZoneManager } from './ui/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkOptions, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types'; import { MouseZone } from './ui/MouseZoneManager'; import { EventEmitter } from './common/EventEmitter'; import { CHAR_DATA_ATTR_INDEX } from './Buffer'; @@ -34,6 +34,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _rowsTimeoutId: number; private _nextLinkMatcherId = 0; private _rowsToLinkify: { start: number, end: number }; + private _pendingLinks = new Array(); constructor( protected _terminal: ITerminal @@ -118,6 +119,9 @@ export class Linkifier extends EventEmitter implements ILinkifier { this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]); } } + for (let i = this._pendingLinks.length; --i >= 0; ) { + this._mouseZoneManager.add(this._pendingLinks.pop()); + } this._rowsToLinkify.start = null; this._rowsToLinkify.end = null; @@ -140,11 +144,11 @@ export class Linkifier extends EventEmitter implements ILinkifier { const matcher: ILinkMatcher = { id: this._nextLinkMatcherId++, regex, - handler, + handler: handler || options.handler, matchIndex: options.matchIndex, validationCallback: options.validationCallback, - hoverTooltipCallback: options.tooltipCallback, - hoverLeaveCallback: options.leaveCallback, + tooltipCallback: options.tooltipCallback, + leaveCallback: options.leaveCallback, willLinkActivate: options.willLinkActivate, priority: options.priority || 0 }; @@ -273,15 +277,19 @@ export class Linkifier extends EventEmitter implements ILinkifier { x2 = this._terminal.cols; y2--; } + this._mouseZoneManager.add(this._createLink(x1, y1, x2, y2, matcher, uri, fg)); + } - this._mouseZoneManager.add(new MouseZone( + private _createLink(x1: number, y1: number, x2: number, y2: number, + options: ILinkOptions, uri: string, fg: number): MouseZone { + return new MouseZone( x1 + 1, y1 + 1, x2 + 1, y2 + 1, e => { - if (matcher.handler) { - return matcher.handler(e, uri); + if (options.handler) { + return options.handler(e, uri); } window.open(uri, '_blank'); }, @@ -291,24 +299,29 @@ export class Linkifier extends EventEmitter implements ILinkifier { }, e => { this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); - if (matcher.hoverTooltipCallback) { - matcher.hoverTooltipCallback(e, uri); + if (options.tooltipCallback) { + options.tooltipCallback(e, uri); } }, () => { this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._terminal.element.classList.remove('xterm-cursor-pointer'); - if (matcher.hoverLeaveCallback) { - matcher.hoverLeaveCallback(); + if (options.leaveCallback) { + options.leaveCallback(); } }, e => { - if (matcher.willLinkActivate) { - return matcher.willLinkActivate(e, uri); + if (options.willLinkActivate) { + return options.willLinkActivate(e, uri); } return true; } - )); + ); + } + + doAddLink(x1: number, y1: number, x2: number, y2: number, + options: ILinkOptions, uri: string, fg: number): void { + this._pendingLinks.push(this._createLink(x1, y1, x2, y2, options, uri, fg)); } private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent { diff --git a/src/Types.ts b/src/Types.ts index c6395ea3ca..256f67316b 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -14,6 +14,7 @@ export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; export type CharData = [number, string, number, number]; export type LineData = CharData[]; +// FIXME rename to LinkHandler export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; @@ -59,6 +60,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { sgrMouse: boolean; urxvtMouse: boolean; cursorHidden: boolean; + linkifier: ILinkifier; buffers: IBufferSet; buffer: IBuffer; @@ -182,16 +184,31 @@ export interface IInputHandler { ESC ~ */ setgLevel(level: number): void; } -export interface ILinkMatcher { +export interface ILinkOptions { + handler?: LinkMatcherHandler; + /** + * A callback that fires when the mouse hovers over a link. + */ + tooltipCallback?: LinkMatcherHandler; + /** + * A callback that fires when the mouse leaves a link that was hovered. + */ + leaveCallback?: () => void; + /** + * A callback that fires when the mousedown and click events occur that + * determines whether a link will be activated upon click. This enables + * only activating a link when a certain modifier is held down, if not the + * mouse event will continue propagation (eg. double click to select word). + */ + willLinkActivate?: (event: MouseEvent, uri: string) => boolean; +} + +export interface ILinkMatcher extends ILinkOptions { id: number; regex: RegExp; - handler: LinkMatcherHandler; - hoverTooltipCallback?: LinkMatcherHandler; - hoverLeaveCallback?: () => void; matchIndex?: number; validationCallback?: LinkMatcherValidationCallback; priority?: number; - willLinkActivate?: (event: MouseEvent, uri: string) => boolean; } export interface ILinkHoverEvent { @@ -326,9 +343,11 @@ export interface ILinkifier extends IEventEmitter { linkifyRows(start: number, end: number): void; registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; deregisterLinkMatcher(matcherId: number): boolean; + doAddLink(x1: number, y1: number, x2: number, y2: number, + matcher: ILinkOptions, uri: string, fg: number): void; } -export interface ILinkMatcherOptions { +export interface ILinkMatcherOptions extends ILinkOptions { /** * The index of the link from the regex.match(text) call. This defaults to 0 * (for regular expressions without capture groups). @@ -339,27 +358,12 @@ export interface ILinkMatcherOptions { * false if invalid. */ validationCallback?: LinkMatcherValidationCallback; - /** - * A callback that fires when the mouse hovers over a link. - */ - tooltipCallback?: LinkMatcherHandler; - /** - * A callback that fires when the mouse leaves a link that was hovered. - */ - leaveCallback?: () => void; /** * The priority of the link matcher, this defines the order in which the link * matcher is evaluated relative to others, from highest to lowest. The * default value is 0. */ priority?: number; - /** - * A callback that fires when the mousedown and click events occur that - * determines whether a link will be activated upon click. This enables - * only activating a link when a certain modifier is held down, if not the - * mouse event will continue propagation (eg. double click to select word). - */ - willLinkActivate?: (event: MouseEvent, uri: string) => boolean; } export interface IBrowser { diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index e6e4aaa32f..53fac0eb8b 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -198,6 +198,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { sgrMouse: boolean; urxvtMouse: boolean; cursorHidden: boolean; + linkifier: ILinkifier; buffers: IBufferSet; buffer: IBuffer = new MockBuffer(); viewport: IViewport;