From 2e3f9e709495d5599ebf1d43b8a7914befe36968 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 27 Feb 2018 08:37:16 -0800 Subject: [PATCH 01/11] Multi-line links prototype Part of #24 --- src/Linkifier.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index a9617952..700b05f4 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -204,7 +204,19 @@ export class Linkifier extends EventEmitter implements ILinkifier { if (absoluteRowIndex >= this._terminal.buffer.lines.length) { return; } - const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); + if ((this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) { + // TODO: Make sure if a wrapped line is requested to be linkified it gets backtracked to ensure the link is filled + return; + } + + let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); + + // Check if the line is wrapped + if (absoluteRowIndex + 1 < this._terminal.buffer.lines.length && (this._terminal.buffer.lines.get(absoluteRowIndex + 1)).isWrapped) { + // TODO: Do this for lines that wrap over more than 2 lines + text += this._terminal.buffer.translateBufferLineToString(absoluteRowIndex + 1, false); + } + for (let i = 0; i < this._linkMatchers.length; i++) { this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]); } @@ -265,6 +277,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param matcher The link matcher for the link. */ private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { + // TODO: Make MouseZone's work over multiple lines this._mouseZoneManager.add(new MouseZone( x + 1, x + 1 + uri.length, From f456b14b3fe0a38e9ffdbaf4183fdaea2a39e3cd Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Mar 2018 06:42:40 -0800 Subject: [PATCH 02/11] Get multi-line link hover working --- src/Linkifier.ts | 5 +++-- src/input/MouseZoneManager.ts | 21 +++++++++++++++++---- src/input/Types.ts | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 745c64de..b9cdaaa3 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -27,7 +27,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { private _rowsToLinkify: {start: number, end: number}; constructor( - protected _terminal: IBufferAccessor & IElementAccessor + protected _terminal: ITerminal ) { super(); this._rowsToLinkify = { @@ -236,8 +236,9 @@ export class Linkifier extends EventEmitter implements ILinkifier { // TODO: Make MouseZone's work over multiple lines this._mouseZoneManager.add(new MouseZone( x + 1, - x + 1 + uri.length, y + 1, + (x + 1 + uri.length) % this._terminal.cols, + y + 1 + Math.floor((x + 1 + uri.length) / this._terminal.cols), e => { if (matcher.handler) { return matcher.handler(e, uri); diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 3ab86e7c..76a5a366 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -59,7 +59,7 @@ export class MouseZoneManager implements IMouseZoneManager { // Iterate through zones and clear them out if they're within the range for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; - if (zone.y > start && zone.y <= end + 1) { + if (zone.y1 > start && zone.y1 <= end + 1) { if (this._currentZone && this._currentZone === zone) { this._currentZone.leaveCallback(); this._currentZone = null; @@ -173,10 +173,22 @@ export class MouseZoneManager implements IMouseZoneManager { if (!coords) { return null; } + const x = coords[0]; + const y = coords[1]; for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; - if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) { - return zone; + if (zone.y1 === zone.y2) { + // Single line link + if (y === zone.y1 && x >= zone.x1 && x < zone.x2) { + return zone; + } + } else { + // Multi-line link + if ((y === zone.y1 && x >= zone.x1) || + (y === zone.y2 && x < zone.x2) || + (y > zone.y1 && y < zone.y2)) { + return zone; + } } } return null; @@ -186,8 +198,9 @@ export class MouseZoneManager implements IMouseZoneManager { export class MouseZone implements IMouseZone { constructor( public x1: number, + public y1: number, public x2: number, - public y: number, + public y2: number, public clickCallback: (e: MouseEvent) => any, public hoverCallback: (e: MouseEvent) => any, public tooltipCallback: (e: MouseEvent) => any, diff --git a/src/input/Types.ts b/src/input/Types.ts index f1398464..2bd805bf 100644 --- a/src/input/Types.ts +++ b/src/input/Types.ts @@ -11,7 +11,8 @@ export interface IMouseZoneManager { export interface IMouseZone { x1: number; x2: number; - y: number; + y1: number; + y2: number; clickCallback: (e: MouseEvent) => any; hoverCallback: (e: MouseEvent) => any | undefined; tooltipCallback: (e: MouseEvent) => any | undefined; From 021f8c1ba096976b36df3aeefb357635326fc88a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Mar 2018 07:00:37 -0800 Subject: [PATCH 03/11] Draw and clear link cells --- src/Linkifier.ts | 17 ++++++++++++++--- src/Types.ts | 8 +++++--- src/input/MouseZoneManager.ts | 4 +++- src/renderer/LinkRenderLayer.ts | 19 +++++++++++++++++-- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index b9cdaaa3..c67ab5ee 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -246,17 +246,17 @@ export class Linkifier extends EventEmitter implements ILinkifier { window.open(uri, '_blank'); }, e => { - this.emit(LinkHoverEventTypes.HOVER, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x, y, uri)); this._terminal.element.style.cursor = 'pointer'; }, e => { - this.emit(LinkHoverEventTypes.TOOLTIP, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x, y, uri)); if (matcher.hoverTooltipCallback) { matcher.hoverTooltipCallback(e, uri); } }, () => { - this.emit(LinkHoverEventTypes.LEAVE, { x, y, length: uri.length}); + this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x, y, uri)); this._terminal.element.style.cursor = ''; if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); @@ -270,4 +270,15 @@ export class Linkifier extends EventEmitter implements ILinkifier { } )); } + + private _createLinkHoverEvent(x: number, y: number, uri: string): ILinkHoverEvent { + return { + x1: x, + y1: y, + // TODO: Verify links on boundary work fine (x vs x + 1) + x2: (x + uri.length) % this._terminal.cols, + y2: y + Math.floor((x + uri.length) / this._terminal.cols), + cols: this._terminal.cols + }; + } } diff --git a/src/Types.ts b/src/Types.ts index 6bc8b963..e400d1c7 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -172,9 +172,11 @@ export interface ICharset { } export interface ILinkHoverEvent { - x: number; - y: number; - length: number; + x1: number; + y1: number; + x2: number; + y2: number; + cols: number; } export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { diff --git a/src/input/MouseZoneManager.ts b/src/input/MouseZoneManager.ts index 76a5a366..65fe74de 100644 --- a/src/input/MouseZoneManager.ts +++ b/src/input/MouseZoneManager.ts @@ -59,7 +59,9 @@ export class MouseZoneManager implements IMouseZoneManager { // Iterate through zones and clear them out if they're within the range for (let i = 0; i < this._zones.length; i++) { const zone = this._zones[i]; - if (zone.y1 > start && zone.y1 <= end + 1) { + if ((zone.y1 > start && zone.y1 <= end + 1) || + (zone.y2 > start && zone.y2 <= end + 1) || + (zone.y1 < start && zone.y2 > end + 1)) { if (this._currentZone && this._currentZone === zone) { this._currentZone.leaveCallback(); this._currentZone = null; diff --git a/src/renderer/LinkRenderLayer.ts b/src/renderer/LinkRenderLayer.ts index 10a33d11..805faf87 100644 --- a/src/renderer/LinkRenderLayer.ts +++ b/src/renderer/LinkRenderLayer.ts @@ -31,14 +31,29 @@ export class LinkRenderLayer extends BaseRenderLayer { private _clearCurrentLink(): void { if (this._state) { - this.clearCells(this._state.x, this._state.y, this._state.length, 1); + this.clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1); + const middleRowCount = this._state.y2 - this._state.y1 - 1; + if (middleRowCount > 0) { + this.clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount); + } + this.clearCells(0, this._state.y2, this._state.x2, 1); this._state = null; } } private _onLinkHover(e: ILinkHoverEvent): void { this._ctx.fillStyle = this._colors.foreground; - this.fillBottomLineAtCells(e.x, e.y, e.length); + if (e.y1 === e.y2) { + // Single line link + this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1); + } else { + // Multi-line link + this.fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1); + for (let y = e.y1 + 1; y < e.y2; y++) { + this.fillBottomLineAtCells(0, y, e.cols); + } + this.fillBottomLineAtCells(0, e.y2, e.x2); + } this._state = e; } From 421ccc5695a3431930f021e300df5f69e2500eff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Mar 2018 09:25:08 -0800 Subject: [PATCH 04/11] Join lines that wrap over more than 2 lines --- src/Linkifier.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index c67ab5ee..1ec53315 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -168,10 +168,12 @@ export class Linkifier extends EventEmitter implements ILinkifier { let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); - // Check if the line is wrapped - if (absoluteRowIndex + 1 < this._terminal.buffer.lines.length && (this._terminal.buffer.lines.get(absoluteRowIndex + 1)).isWrapped) { - // TODO: Do this for lines that wrap over more than 2 lines - text += this._terminal.buffer.translateBufferLineToString(absoluteRowIndex + 1, false); + // Construct full unwrapped line text + if (absoluteRowIndex + 1 < this._terminal.buffer.lines.length) { + let currentIndex = absoluteRowIndex + 1; + while ((this._terminal.buffer.lines.get(currentIndex)).isWrapped) { + text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false); + } } for (let i = 0; i < this._linkMatchers.length; i++) { From 59be054ca167d6995b9504d6dafe5abccadc99c3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Mar 2018 11:12:59 -0800 Subject: [PATCH 05/11] Fix exception for links on bottom line --- src/Linkifier.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 1ec53315..349ecb07 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -169,11 +169,10 @@ export class Linkifier extends EventEmitter implements ILinkifier { let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); // Construct full unwrapped line text - if (absoluteRowIndex + 1 < this._terminal.buffer.lines.length) { - let currentIndex = absoluteRowIndex + 1; - while ((this._terminal.buffer.lines.get(currentIndex)).isWrapped) { - text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false); - } + let currentIndex = absoluteRowIndex + 1; + while (currentIndex < this._terminal.buffer.lines.length && + (this._terminal.buffer.lines.get(currentIndex)).isWrapped) { + text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false); } for (let i = 0; i < this._linkMatchers.length; i++) { From 4ddf91dbbe50664c37fb92f0be062b157192f072 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 8 Mar 2018 11:20:42 -0800 Subject: [PATCH 06/11] Fix tests --- src/Linkifier.test.ts | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index ce1635a3..89c9bd5c 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -7,11 +7,11 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './input/Types'; import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types'; import { Linkifier } from './Linkifier'; -import { MockBuffer } from './utils/TestUtils.test'; +import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor(_terminal: IBufferAccessor & IElementAccessor) { + constructor(_terminal: ITerminal) { super(_terminal); Linkifier.TIME_BEFORE_LINKIFY = 0; } @@ -32,15 +32,14 @@ class TestMouseZoneManager implements IMouseZoneManager { } describe('Linkifier', () => { - let terminal: IBufferAccessor & IElementAccessor; + let terminal: ITerminal; let linkifier: TestLinkifier; let mouseZoneManager: TestMouseZoneManager; beforeEach(() => { - terminal = { - buffer: new MockBuffer(), - element: {} - }; + terminal = new MockTerminal(); + terminal.cols = 100; + terminal.buffer = new MockBuffer(); terminal.buffer.lines = new CircularList(20); terminal.buffer.ydisp = 0; linkifier = new TestLinkifier(terminal); @@ -59,17 +58,6 @@ describe('Linkifier', () => { terminal.buffer.lines.push(stringToRow(text)); } - function assertLinkifiesEntireRow(uri: string, done: MochaDone): void { - addRow(uri); - linkifier.linkifyRows(); - setTimeout(() => { - assert.equal(mouseZoneManager.zones[0].x1, 1); - assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1); - assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length); - done(); - }, 0); - } - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void { addRow(rowText); linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); @@ -80,7 +68,8 @@ describe('Linkifier', () => { links.forEach((l, i) => { assert.equal(mouseZoneManager.zones[i].x1, l.x + 1); assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1); - assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length); + assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length); + assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length); }); done(); }, 0); @@ -141,7 +130,8 @@ describe('Linkifier', () => { assert.equal(mouseZoneManager.zones.length, 1); assert.equal(mouseZoneManager.zones[0].x1, 1); assert.equal(mouseZoneManager.zones[0].x2, 5); - assert.equal(mouseZoneManager.zones[0].y, 1); + assert.equal(mouseZoneManager.zones[0].y1, 1); + assert.equal(mouseZoneManager.zones[0].y2, 1); // Fires done() mouseZoneManager.zones[0].clickCallback({}); } From b44a11c42eaebf2115dd0b91be71ebc3f91544ff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 9 Mar 2018 10:56:54 -0800 Subject: [PATCH 07/11] Remove TODOs, polish --- src/Linkifier.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 349ecb07..1485c095 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -157,18 +157,19 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param rowIndex The index of the row to linkify. */ private _linkifyRow(rowIndex: number): void { + // Ensure the row exists const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex; if (absoluteRowIndex >= this._terminal.buffer.lines.length) { return; } + + // Only attempt to linkify rows that start in the viewport if ((this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) { - // TODO: Make sure if a wrapped line is requested to be linkified it gets backtracked to ensure the link is filled return; } - let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); - // Construct full unwrapped line text + let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false); let currentIndex = absoluteRowIndex + 1; while (currentIndex < this._terminal.buffer.lines.length && (this._terminal.buffer.lines.get(currentIndex)).isWrapped) { @@ -234,7 +235,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param matcher The link matcher for the link. */ private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { - // TODO: Make MouseZone's work over multiple lines this._mouseZoneManager.add(new MouseZone( x + 1, y + 1, @@ -276,7 +276,6 @@ export class Linkifier extends EventEmitter implements ILinkifier { return { x1: x, y1: y, - // TODO: Verify links on boundary work fine (x vs x + 1) x2: (x + uri.length) % this._terminal.cols, y2: y + Math.floor((x + uri.length) / this._terminal.cols), cols: this._terminal.cols From dd41ab38bb9f10c47eaf4d4d15feafebcac00ace Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 9 Mar 2018 10:58:57 -0800 Subject: [PATCH 08/11] Fix imports after noUnusedLocals was merged in --- src/Linkifier.test.ts | 2 +- src/Linkifier.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 96868d2c..78fc3f32 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { IMouseZoneManager, IMouseZone } from './input/Types'; -import { ILinkMatcher, LineData, IBufferAccessor, IElementAccessor } from './Types'; +import { ILinkMatcher, LineData, ITerminal } from './Types'; import { Linkifier } from './Linkifier'; import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; diff --git a/src/Linkifier.ts b/src/Linkifier.ts index dc4596aa..dace30ae 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -4,7 +4,7 @@ */ import { IMouseZoneManager } from './input/Types'; -import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, IBufferAccessor, ILinkifier, IElementAccessor } from './Types'; +import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types'; import { MouseZone } from './input/MouseZoneManager'; import { EventEmitter } from './EventEmitter'; From e5f5c2baf25bbb7d0c28e07c9c86f8b98457a43f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 10 Mar 2018 16:08:50 -0800 Subject: [PATCH 09/11] Fix links that start on wrapped lines --- src/Linkifier.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index dace30ae..c7d64e9d 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -232,11 +232,13 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param matcher The link matcher for the link. */ private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { + const wrappedX = x % this._terminal.cols; + const wrappedY = y + Math.floor(x / this._terminal.cols); this._mouseZoneManager.add(new MouseZone( - x + 1, - y + 1, - (x + 1 + uri.length) % this._terminal.cols, - y + 1 + Math.floor((x + 1 + uri.length) / this._terminal.cols), + wrappedX + 1, + wrappedY + 1, + (wrappedX + 1 + uri.length) % this._terminal.cols, + wrappedY + 1 + Math.floor((wrappedX + 1 + uri.length) / this._terminal.cols), e => { if (matcher.handler) { return matcher.handler(e, uri); @@ -244,17 +246,17 @@ export class Linkifier extends EventEmitter implements ILinkifier { window.open(uri, '_blank'); }, e => { - this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x, y, uri)); + this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); this._terminal.element.style.cursor = 'pointer'; }, e => { - this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x, y, uri)); + this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); if (matcher.hoverTooltipCallback) { matcher.hoverTooltipCallback(e, uri); } }, () => { - this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x, y, uri)); + this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); this._terminal.element.style.cursor = ''; if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); From c8ab687fa0cef2cdf6ae0b69a931263b9e4397a3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 11 Mar 2018 12:34:27 -0700 Subject: [PATCH 10/11] Add tests --- src/Linkifier.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/Linkifier.ts | 35 ++++++++++++++++++----------------- 2 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 78fc3f32..0e67403c 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -75,6 +75,23 @@ describe('Linkifier', () => { }, 0); } + function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void { + addRow(rowText); + linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); + linkifier.linkifyRows(); + // Allow linkify to happen + setTimeout(() => { + assert.equal(mouseZoneManager.zones.length, links.length); + links.forEach((l, i) => { + assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); + assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); + assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); + assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); + }); + done(); + }, 0); + } + describe('before attachToDom', () => { it('should allow link matcher registration', done => { assert.doesNotThrow(() => { @@ -118,6 +135,24 @@ describe('Linkifier', () => { // character (U+1F537) which caused the path to be duplicated. See #642. assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done); }); + describe('multi-line links', () => { + it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { + terminal.cols = 4; + assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); + }); + it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { + terminal.cols = 4; + assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); + }); + it('should match links that start and end on line 2/2 of a wrapped line', done => { + terminal.cols = 4; + assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); + }); + it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { + terminal.cols = 4; + assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); + }); + }); }); describe('validationCallback', () => { diff --git a/src/Linkifier.ts b/src/Linkifier.ts index c7d64e9d..8552d8a5 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -232,13 +232,20 @@ export class Linkifier extends EventEmitter implements ILinkifier { * @param matcher The link matcher for the link. */ private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void { - const wrappedX = x % this._terminal.cols; - const wrappedY = y + Math.floor(x / this._terminal.cols); + const x1 = x % this._terminal.cols; + const y1 = y + Math.floor(x / this._terminal.cols); + let x2 = (x1 + uri.length) % this._terminal.cols; + let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols); + if (x2 === 0) { + x2 = this._terminal.cols; + y2--; + } + this._mouseZoneManager.add(new MouseZone( - wrappedX + 1, - wrappedY + 1, - (wrappedX + 1 + uri.length) % this._terminal.cols, - wrappedY + 1 + Math.floor((wrappedX + 1 + uri.length) / this._terminal.cols), + x1 + 1, + y1 + 1, + x2 + 1, + y2 + 1, e => { if (matcher.handler) { return matcher.handler(e, uri); @@ -246,17 +253,17 @@ export class Linkifier extends EventEmitter implements ILinkifier { window.open(uri, '_blank'); }, e => { - this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); + this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2)); this._terminal.element.style.cursor = 'pointer'; }, e => { - this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); + this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2)); if (matcher.hoverTooltipCallback) { matcher.hoverTooltipCallback(e, uri); } }, () => { - this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(wrappedX, wrappedY, uri)); + this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2)); this._terminal.element.style.cursor = ''; if (matcher.hoverLeaveCallback) { matcher.hoverLeaveCallback(); @@ -271,13 +278,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { )); } - private _createLinkHoverEvent(x: number, y: number, uri: string): ILinkHoverEvent { - return { - x1: x, - y1: y, - x2: (x + uri.length) % this._terminal.cols, - y2: y + Math.floor((x + uri.length) / this._terminal.cols), - cols: this._terminal.cols - }; + private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent { + return { x1, y1, x2, y2, cols: this._terminal.cols }; } } From 3716aad202f5f0587c735b66acd4e797881e5028 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 16 Mar 2018 07:21:14 -0700 Subject: [PATCH 11/11] Backtrack row to be linkified if the first row is wrapped --- src/Linkifier.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 8552d8a5..21201a6d 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -158,14 +158,21 @@ export class Linkifier extends EventEmitter implements ILinkifier { */ private _linkifyRow(rowIndex: number): void { // Ensure the row exists - const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex; + let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex; if (absoluteRowIndex >= this._terminal.buffer.lines.length) { return; } - // Only attempt to linkify rows that start in the viewport if ((this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) { - return; + // Only attempt to linkify rows that start in the viewport + if (rowIndex !== 0) { + return; + } + // If the first row is wrapped, backtrack to find the origin row and linkify that + do { + rowIndex--; + absoluteRowIndex--; + } while ((this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped); } // Construct full unwrapped line text