Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic link parser for better link detection #2530

Merged
merged 31 commits into from
Feb 8, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c5962c
Added support for a basic link parser
jmbockhorst Nov 1, 2019
3e2e4ac
Removed file detection from WebLinkProvider
jmbockhorst Nov 1, 2019
1e877a0
Fix lint errors
jmbockhorst Nov 1, 2019
cf082cc
Fix links when viewport is scrolled
jmbockhorst Nov 1, 2019
ec78fc7
Remove usage of ! operator
jmbockhorst Nov 1, 2019
e57cb8b
Use regex parser for web links
jmbockhorst Nov 1, 2019
fdfadfe
Fixed link caching errors
jmbockhorst Nov 5, 2019
28c43ac
Remove link caching
jmbockhorst Nov 5, 2019
b3d95d2
Merge branch 'master' of https://github.com/xtermjs/xterm.js into lin…
jmbockhorst Nov 5, 2019
ac6c8a5
Support multiline links in the WebLinksAddon
jmbockhorst Nov 6, 2019
b07a6d1
Add test for multiline links
jmbockhorst Nov 6, 2019
f18a6fe
Fixed bug in link detection for wrapped links
jmbockhorst Nov 7, 2019
700c4ac
Rework link parser to use a single cached link and make other request…
jmbockhorst Nov 8, 2019
f93939e
Merge remote-tracking branch 'ups/master' into pr/jmbockhorst/2530
Tyriar Nov 8, 2019
8924aa7
Use onRender() instead of onData() and onScroll()
jmbockhorst Nov 8, 2019
a190634
Only clear link if it was in the range of onRender and fix tests
jmbockhorst Nov 8, 2019
a95b1b4
Merge branch 'master' of https://github.com/xtermjs/xterm.js into lin…
jmbockhorst Feb 4, 2020
e252b4b
Merge branch 'master' into linkParser
Tyriar Feb 7, 2020
a066bbe
:lipstick:
Tyriar Feb 7, 2020
ef071ac
Polish API
Tyriar Feb 7, 2020
c327116
url -> text
Tyriar Feb 7, 2020
b18ded0
Rename tooltip to hover/leave
Tyriar Feb 7, 2020
f4a3125
handle -> activate
Tyriar Feb 7, 2020
0103c68
Start registerLinkProvider tests
Tyriar Feb 7, 2020
7f1f221
Add test for activate
Tyriar Feb 7, 2020
e4875f2
Add test for absense of hover/leave
Tyriar Feb 7, 2020
660cf26
Merge branch 'master' into linkParser
Tyriar Feb 7, 2020
54743ed
Make link matcher API the default for web links addon
Tyriar Feb 8, 2020
a7c2d54
Document link provider argument in web links addon
Tyriar Feb 8, 2020
0e302ba
Mark link matcher API as deprecated
Tyriar Feb 8, 2020
d1d27b5
Remove logs
Tyriar Feb 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions addons/xterm-addon-web-links/src/WebLinkProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ILinkProvider, IBufferCellPosition, ILink, Terminal, IBuffer } from 'xterm';
Tyriar marked this conversation as resolved.
Show resolved Hide resolved

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 {
callback(LinkComputer.computeLink(position, this._regex, this._terminal.buffer, this._handler));
}
}

export class LinkComputer {
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);

if (!bufferLine) {
return;
}

const line = bufferLine.translateToString();

let match;
let stringIndex = -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 };
}
}
}
10 changes: 5 additions & 5 deletions addons/xterm-addon-web-links/src/WebLinksAddon.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const width = 800;
const height = 600;

describe('WebLinksAddon', () => {
before(async function(): Promise<any> {
before(async function (): Promise<any> {
this.timeout(10000);
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
Expand All @@ -30,22 +30,22 @@ describe('WebLinksAddon', () => {
await browser.close();
});

beforeEach(async function(): Promise<any> {
beforeEach(async function (): Promise<any> {
this.timeout(5000);
await page.goto(APP);
});

it('.com', async function(): Promise<any> {
it('.com', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.com');
});

it('.com.au', async function(): Promise<any> {
it('.com.au', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.com.au');
});

it('.io', async function(): Promise<any> {
it('.io', async function (): Promise<any> {
this.timeout(20000);
await testHostName('foo.io');
});
Expand Down
12 changes: 10 additions & 2 deletions addons/xterm-addon-web-links/src/WebLinksAddon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* @license MIT
*/

import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
import { Terminal, ILinkMatcherOptions, ITerminalAddon, ILinkProvider } from 'xterm';
import WebLinkProvider from './WebLinkProvider';
Tyriar marked this conversation as resolved.
Show resolved Hide resolved

const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
Expand Down Expand Up @@ -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,
Expand All @@ -42,7 +44,13 @@ export class WebLinksAddon implements ITerminalAddon {

public activate(terminal: Terminal): void {
this._terminal = terminal;
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);
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
} else {
this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options);
}
}

public dispose(): void {
Expand Down
3 changes: 3 additions & 0 deletions addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
31 changes: 21 additions & 10 deletions src/Terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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, this._coreService);

if (this.options.windowsMode) {
this._windowsMode = applyWindowsMode(this);
Expand Down Expand Up @@ -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._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)));
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1166,6 +1170,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}

public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
if (!this.linkifier2) {
return;
}
Tyriar marked this conversation as resolved.
Show resolved Hide resolved
return this.linkifier2.registerLinkProvider(linkProvider);
}

public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
const joinerId = this._renderService.registerCharacterJoiner(handler);
this.refresh(0, this.rows - 1);
Expand Down Expand Up @@ -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;
Expand Down
28 changes: 16 additions & 12 deletions src/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -133,6 +136,7 @@ export class MockTerminal implements ITerminal {
bracketedPasteMode: boolean;
renderer: IRenderer;
linkifier: ILinkifier;
linkifier2: ILinkifier2;
isFocused: boolean;
options: ITerminalOptions = {};
element: HTMLElement;
Expand Down Expand Up @@ -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; }
}
Expand Down
6 changes: 4 additions & 2 deletions src/Types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Loading