mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #2309 from Tyriar/layering
Move several components to sub-projects
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ILinkifierAccessor } from '../../../../src/Types';
|
||||
import { ILinkifierAccessor } from '../../../../src/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from '../atlas/CharAtlasUtils';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
@@ -45,9 +45,9 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (is256Color(e.fg)) {
|
||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
+24
-30
@@ -3,10 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IMouseService } from 'browser/services/Services';
|
||||
import { IMouseService, ISelectionService } from 'browser/services/Services';
|
||||
import { IMouseZoneManager, IMouseZone } from 'browser/Types';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
const HOVER_DURATION = 500;
|
||||
|
||||
@@ -32,12 +33,15 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _initialSelectionLength: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _mouseService: IMouseService
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _bufferService: IBufferService,
|
||||
private readonly _mouseService: IMouseService,
|
||||
private readonly _selectionService: ISelectionService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e)));
|
||||
this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e)));
|
||||
|
||||
// These events are expensive, only listen to it when mouse zones are active
|
||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||
@@ -66,7 +70,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
// Clear all if start/end weren't set
|
||||
if (!end) {
|
||||
start = 0;
|
||||
end = this._terminal.rows - 1;
|
||||
end = this._bufferService.rows - 1;
|
||||
}
|
||||
|
||||
// Iterate through zones and clear them out if they're within the range
|
||||
@@ -92,18 +96,18 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _activate(): void {
|
||||
if (!this._areZonesActive) {
|
||||
this._areZonesActive = true;
|
||||
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.addEventListener('click', this._clickListener);
|
||||
this._element.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._element.addEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
private _deactivate(): void {
|
||||
if (this._areZonesActive) {
|
||||
this._areZonesActive = false;
|
||||
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.removeEventListener('click', this._clickListener);
|
||||
this._element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._element.removeEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +165,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
// Store current terminal selection length, to check if we're performing
|
||||
// a selection operation
|
||||
this._initialSelectionLength = this._terminal.getSelection().length;
|
||||
this._initialSelectionLength = this._getSelectionLength();
|
||||
|
||||
// Ignore the event if there are no zones active
|
||||
if (!this._areZonesActive) {
|
||||
@@ -195,7 +199,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
// Find the active zone and click it if found and no selection was
|
||||
// being performed
|
||||
const zone = this._findZoneEventAt(e);
|
||||
const currentSelectionLength = this._terminal.getSelection().length;
|
||||
const currentSelectionLength = this._getSelectionLength();
|
||||
|
||||
if (zone && currentSelectionLength === this._initialSelectionLength) {
|
||||
zone.clickCallback(e);
|
||||
@@ -204,8 +208,13 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
private _getSelectionLength(): number {
|
||||
const selectionText = this._selectionService.selectionText;
|
||||
return selectionText ? selectionText.length : 0;
|
||||
}
|
||||
|
||||
private _findZoneEventAt(e: MouseEvent): IMouseZone {
|
||||
const coords = this._mouseService.getCoords(e, this._terminal.screenElement, this._terminal.cols, this._terminal.rows);
|
||||
const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows);
|
||||
if (!coords) {
|
||||
return null;
|
||||
}
|
||||
@@ -230,18 +239,3 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseZone implements IMouseZone {
|
||||
constructor(
|
||||
public x1: number,
|
||||
public y1: number,
|
||||
public x2: number,
|
||||
public y2: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+125
-7
@@ -4,20 +4,18 @@
|
||||
*/
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from './TestUtils.test';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { wcwidth } from 'common/CharWidth';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { Linkifier } from 'browser/Linkifier';
|
||||
import { MockLogService } from 'common/TestUtils.test';
|
||||
import { IRegisteredLinkMatcher, IMouseZoneManager, IMouseZone } from 'browser/Types';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
public keyDown(ev: any): boolean { return this._keyDown(ev); }
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('Terminal', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
@@ -1024,4 +1022,124 @@ describe('Terminal', () => {
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
|
||||
describe('Linkifier unicode handling', () => {
|
||||
let terminal: TestTerminal;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
// other than the tests above unicode testing needs the full terminal instance
|
||||
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({ cols: 10, rows: 5 });
|
||||
linkifier = new TestLinkifier((terminal as any)._bufferService);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
linkifier.attachToDom({} as any, mouseZoneManager);
|
||||
});
|
||||
|
||||
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||
terminal.writeSync(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('unicode before the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
describe('unicode within the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(bufferService: IBufferService) {
|
||||
super(bufferService, new MockLogService());
|
||||
Linkifier._timeBeforeLatency = 0;
|
||||
}
|
||||
|
||||
public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; }
|
||||
public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); }
|
||||
}
|
||||
|
||||
class TestMouseZoneManager implements IMouseZoneManager {
|
||||
dispose(): void {
|
||||
}
|
||||
public clears: number = 0;
|
||||
public zones: IMouseZone[] = [];
|
||||
add(zone: IMouseZone): void {
|
||||
this.zones.push(zone);
|
||||
}
|
||||
clearAll(): void {
|
||||
this.clears++;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-11
@@ -21,15 +21,15 @@
|
||||
* http://linux.die.net/man/7/urxvt
|
||||
*/
|
||||
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, IMouseZoneManager } from './Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, CustomKeyEventHandler } from './Types';
|
||||
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { Viewport } from './Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './Clipboard';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from 'browser/Clipboard';
|
||||
import { C0 } from 'common/data/EscapeSequences';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { Renderer } from './renderer/Renderer';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { Linkifier } from 'browser/Linkifier';
|
||||
import { SelectionService } from './browser/services/SelectionService';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
@@ -59,6 +59,7 @@ import { MouseService } from 'browser/services/MouseService';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
import { CoreService } from 'common/services/CoreService';
|
||||
import { LogService } from 'common/services/LogService';
|
||||
import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions } from 'browser/Types';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document = (typeof window !== 'undefined') ? window.document : null;
|
||||
@@ -304,9 +305,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
|
||||
this.register(this._inputHandler);
|
||||
|
||||
this._selectionService = this._selectionService || null;
|
||||
this.linkifier = this.linkifier || new Linkifier(this, this._logService);
|
||||
this._mouseZoneManager = this._mouseZoneManager || null;
|
||||
this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService);
|
||||
|
||||
if (this.options.windowsMode) {
|
||||
this._windowsMode = applyWindowsMode(this);
|
||||
@@ -600,11 +599,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
this._soundService = new SoundService(this.optionsService);
|
||||
this._mouseService = new MouseService(this._renderService, this._charSizeService);
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this, this._mouseService);
|
||||
this.register(this._mouseZoneManager);
|
||||
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
|
||||
this.linkifier.attachToDom(this._mouseZoneManager);
|
||||
|
||||
this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this._renderService.dimensions, this._charSizeService);
|
||||
this.viewport.onThemeChange(this._colorManager.colors);
|
||||
this.register(this.viewport);
|
||||
@@ -637,6 +631,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService.refresh()));
|
||||
|
||||
this._mouseZoneManager = new MouseZoneManager(this.element, this.screenElement, this._bufferService, this._mouseService, this._selectionService);
|
||||
this.register(this._mouseZoneManager);
|
||||
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
|
||||
this.linkifier.attachToDom(this.element, this._mouseZoneManager);
|
||||
|
||||
// apply mouse event classes set by escape codes before terminal was attached
|
||||
this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
|
||||
if (this.mouseEvents) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions, ILinkifier, ILinkMatcherOptions } from './Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
@@ -12,7 +12,7 @@ import * as Browser from 'common/Platform';
|
||||
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
|
||||
import { Terminal } from './Terminal';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorManager, IColorSet } from 'browser/Types';
|
||||
import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
@@ -23,6 +23,8 @@ export class TestTerminal extends Terminal {
|
||||
this.writeBuffer.push(data);
|
||||
this._innerWrite();
|
||||
}
|
||||
keyDown(ev: any): boolean { return this._keyDown(ev); }
|
||||
keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
|
||||
Vendored
+1
-87
@@ -6,7 +6,7 @@
|
||||
import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { ICharset, IAttributeData, CharData } from 'common/Types';
|
||||
import { IEvent, IEventEmitter } from 'common/EventEmitter';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ILinkifier, ILinkMatcherOptions } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
@@ -15,9 +15,6 @@ export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type LineData = CharData[];
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
/**
|
||||
* This interface encapsulates everything needed from the Terminal by the
|
||||
* InputHandler. This cleanly separates the large amount of methods needed by
|
||||
@@ -167,27 +164,6 @@ export interface IInputHandler {
|
||||
/** ESC # 8 */ screenAlignmentPattern(): void;
|
||||
}
|
||||
|
||||
export interface ILinkMatcher {
|
||||
id: number;
|
||||
regex: RegExp;
|
||||
handler: LinkMatcherHandler;
|
||||
hoverTooltipCallback?: LinkMatcherHandler;
|
||||
hoverLeaveCallback?: () => void;
|
||||
matchIndex?: number;
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
priority?: number;
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifierEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number;
|
||||
}
|
||||
|
||||
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
screenElement: HTMLElement;
|
||||
browser: IBrowser;
|
||||
@@ -285,51 +261,6 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
useFlowControl?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
export interface ILinkMatcherOptions {
|
||||
/**
|
||||
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||
* (for regular expressions without capture groups).
|
||||
*/
|
||||
matchIndex?: number;
|
||||
/**
|
||||
* A callback that validates an individual link, returning true if valid and
|
||||
* 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 {
|
||||
isNode: boolean;
|
||||
userAgent: string;
|
||||
@@ -340,20 +271,3 @@ export interface IBrowser {
|
||||
isIphone: boolean;
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
export interface IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as Clipboard from './Clipboard';
|
||||
import * as Clipboard from 'browser/Clipboard';
|
||||
|
||||
describe('evaluatePastedTextProcessing', () => {
|
||||
it('should replace carriage return and/or line feed with carriage return', () => {
|
||||
@@ -29,7 +29,9 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
||||
* @param ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionService): void {
|
||||
ev.clipboardData.setData('text/plain', selectionService.selectionText);
|
||||
if (ev.clipboardData) {
|
||||
ev.clipboardData.setData('text/plain', selectionService.selectionText);
|
||||
}
|
||||
// Prevent or the original text will be copied.
|
||||
ev.preventDefault();
|
||||
}
|
||||
@@ -4,23 +4,22 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types';
|
||||
import { IBufferLine } from 'common/Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { CircularList } from 'common/CircularList';
|
||||
import { Linkifier } from 'browser/Linkifier';
|
||||
import { BufferLine } from 'common/buffer/BufferLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockLogService } from 'common/TestUtils.test';
|
||||
import { MockLogService, MockBufferService } from 'common/TestUtils.test';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(terminal: ITerminal) {
|
||||
super(terminal, new MockLogService());
|
||||
constructor(bufferService: IBufferService) {
|
||||
super(bufferService, new MockLogService());
|
||||
Linkifier._timeBeforeLatency = 0;
|
||||
}
|
||||
|
||||
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
|
||||
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
|
||||
public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; }
|
||||
public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); }
|
||||
}
|
||||
|
||||
class TestMouseZoneManager implements IMouseZoneManager {
|
||||
@@ -37,18 +36,13 @@ class TestMouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let terminal: ITerminal;
|
||||
let bufferService: IBufferService;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
(terminal as any).cols = 100;
|
||||
(terminal as any).rows = 10;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
bufferService = new MockBufferService(100, 10);
|
||||
linkifier = new TestLinkifier(bufferService);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
});
|
||||
|
||||
@@ -61,13 +55,12 @@ describe('Linkifier', () => {
|
||||
}
|
||||
|
||||
function addRow(text: string): void {
|
||||
terminal.buffer.lines.push(stringToRow(text));
|
||||
bufferService.buffer.lines.push(stringToRow(text));
|
||||
}
|
||||
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
(terminal as any).rows = terminal.buffer.lines.length - 1;
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
@@ -75,8 +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].y1, terminal.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y1, bufferService.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, bufferService.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
@@ -111,7 +104,7 @@ describe('Linkifier', () => {
|
||||
|
||||
describe('after attachToDom', () => {
|
||||
beforeEach(() => {
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
linkifier.attachToDom({} as any, mouseZoneManager);
|
||||
});
|
||||
|
||||
describe('link matcher', () => {
|
||||
@@ -144,19 +137,23 @@ describe('Linkifier', () => {
|
||||
});
|
||||
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 as any).cols = 4;
|
||||
bufferService.resize(4, bufferService.rows);
|
||||
bufferService.buffer.lines.length = 0;
|
||||
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 as any).cols = 4;
|
||||
bufferService.resize(4, bufferService.rows);
|
||||
bufferService.buffer.lines.length = 0;
|
||||
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 as any).cols = 4;
|
||||
bufferService.resize(4, bufferService.rows);
|
||||
bufferService.buffer.lines.length = 0;
|
||||
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 as any).cols = 4;
|
||||
bufferService.resize(4, bufferService.rows);
|
||||
bufferService.buffer.lines.length = 0;
|
||||
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||
});
|
||||
});
|
||||
@@ -164,6 +161,7 @@ describe('Linkifier', () => {
|
||||
|
||||
describe('validationCallback', () => {
|
||||
it('should enable link if true', done => {
|
||||
bufferService.buffer.lines.length = 0;
|
||||
addRow('test');
|
||||
linkifier.registerLinkMatcher(/test/, () => done(), {
|
||||
validationCallback: (url, cb) => {
|
||||
@@ -242,98 +240,4 @@ describe('Linkifier', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('unicode handling', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
// other than the tests above unicode testing needs the full terminal instance
|
||||
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({cols: 10, rows: 5});
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
});
|
||||
|
||||
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||
terminal.writeSync(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('unicode before the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
describe('unicode within the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,12 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IMouseZoneManager } from './Types';
|
||||
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types';
|
||||
import { IBufferStringIteratorResult } from 'common/buffer/Types';
|
||||
import { MouseZone } from './MouseZoneManager';
|
||||
import { getStringCellWidth } from 'common/CharWidth';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { ILogService } from 'common/services/Services';
|
||||
import { ILogService, IBufferService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* Limit of the unwrapping line expansion (overscan) at the top and bottom
|
||||
@@ -28,12 +27,14 @@ export class Linkifier implements ILinkifier {
|
||||
*/
|
||||
protected static _timeBeforeLatency = 200;
|
||||
|
||||
protected _linkMatchers: ILinkMatcher[] = [];
|
||||
protected _linkMatchers: IRegisteredLinkMatcher[] = [];
|
||||
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _mouseZoneManager: IMouseZoneManager | undefined;
|
||||
private _element: HTMLElement | undefined;
|
||||
|
||||
private _rowsTimeoutId: number | undefined;
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
private _rowsToLinkify: { start: number | undefined, end: number | undefined };
|
||||
|
||||
private _onLinkHover = new EventEmitter<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
@@ -43,12 +44,12 @@ export class Linkifier implements ILinkifier {
|
||||
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal,
|
||||
private _logService: ILogService
|
||||
protected readonly _bufferService: IBufferService,
|
||||
private readonly _logService: ILogService
|
||||
) {
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
start: undefined,
|
||||
end: undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,7 +57,8 @@ export class Linkifier implements ILinkifier {
|
||||
* Attaches the linkifier to the DOM, enabling linkification.
|
||||
* @param mouseZoneManager The mouse zone manager to register link zones with.
|
||||
*/
|
||||
public attachToDom(mouseZoneManager: IMouseZoneManager): void {
|
||||
public attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void {
|
||||
this._element = element;
|
||||
this._mouseZoneManager = mouseZoneManager;
|
||||
}
|
||||
|
||||
@@ -72,7 +74,7 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
|
||||
// Increase range to linkify
|
||||
if (this._rowsToLinkify.start === null) {
|
||||
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
|
||||
this._rowsToLinkify.start = start;
|
||||
this._rowsToLinkify.end = end;
|
||||
} else {
|
||||
@@ -94,8 +96,13 @@ export class Linkifier implements ILinkifier {
|
||||
* Linkifies the rows requested.
|
||||
*/
|
||||
private _linkifyRows(): void {
|
||||
this._rowsTimeoutId = null;
|
||||
const buffer = this._terminal.buffer;
|
||||
this._rowsTimeoutId = undefined;
|
||||
const buffer = this._bufferService.buffer;
|
||||
|
||||
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
|
||||
this._logService.debug('_rowToLinkify was unset before _linkifyRows was called');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the start row exists
|
||||
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
|
||||
@@ -104,7 +111,7 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
|
||||
// Invalidate bad end row values (if a resize happened)
|
||||
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
|
||||
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._bufferService.rows) + 1;
|
||||
|
||||
// Iterate over the range of unwrapped content strings within start..end
|
||||
// (excluding).
|
||||
@@ -116,8 +123,8 @@ export class Linkifier implements ILinkifier {
|
||||
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
|
||||
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
|
||||
// chars will not match anymore at the viewport borders.
|
||||
const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._terminal.cols);
|
||||
const iterator = this._terminal.buffer.iterator(
|
||||
const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._bufferService.cols);
|
||||
const iterator = this._bufferService.buffer.iterator(
|
||||
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
|
||||
while (iterator.hasNext()) {
|
||||
const lineData: IBufferStringIteratorResult = iterator.next();
|
||||
@@ -126,8 +133,8 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
}
|
||||
|
||||
this._rowsToLinkify.start = null;
|
||||
this._rowsToLinkify.end = null;
|
||||
this._rowsToLinkify.start = undefined;
|
||||
this._rowsToLinkify.end = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,7 +151,7 @@ export class Linkifier implements ILinkifier {
|
||||
if (!handler) {
|
||||
throw new Error('handler must be defined');
|
||||
}
|
||||
const matcher: ILinkMatcher = {
|
||||
const matcher: IRegisteredLinkMatcher = {
|
||||
id: this._nextLinkMatcherId++,
|
||||
regex,
|
||||
handler,
|
||||
@@ -165,7 +172,7 @@ export class Linkifier implements ILinkifier {
|
||||
* considered after older link matchers.
|
||||
* @param matcher The link matcher to be added.
|
||||
*/
|
||||
private _addLinkMatcherToList(matcher: ILinkMatcher): void {
|
||||
private _addLinkMatcherToList(matcher: IRegisteredLinkMatcher): void {
|
||||
if (this._linkMatchers.length === 0) {
|
||||
this._linkMatchers.push(matcher);
|
||||
return;
|
||||
@@ -228,19 +235,20 @@ export class Linkifier implements ILinkifier {
|
||||
}
|
||||
|
||||
// get the buffer index as [absolute row, col] for the match
|
||||
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
const bufferIndex = this._bufferService.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
if (bufferIndex[0] < 0) {
|
||||
// invalid bufferIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||
const attr = line.getFg(bufferIndex[1]);
|
||||
let fg: number | undefined;
|
||||
if (attr) {
|
||||
fg = (attr >> 9) & 0x1ff;
|
||||
const line = this._bufferService.buffer.lines.get(bufferIndex[0]);
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
|
||||
const attr = line.getFg(bufferIndex[1]);
|
||||
const fg = attr ? (attr >> 9) & 0x1ff : undefined;
|
||||
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
// Discard link if the line has already changed
|
||||
@@ -248,11 +256,11 @@ export class Linkifier implements ILinkifier {
|
||||
return;
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,14 +273,18 @@ export class Linkifier implements ILinkifier {
|
||||
* @param matcher The link matcher for the link.
|
||||
* @param fg The link color for hover event.
|
||||
*/
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number | undefined): void {
|
||||
if (!this._mouseZoneManager || !this._element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = getStringCellWidth(uri);
|
||||
const x1 = x % this._terminal.cols;
|
||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||
let x2 = (x1 + width) % this._terminal.cols;
|
||||
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
|
||||
const x1 = x % this._bufferService.cols;
|
||||
const y1 = y + Math.floor(x / this._bufferService.cols);
|
||||
let x2 = (x1 + width) % this._bufferService.cols;
|
||||
let y2 = y1 + Math.floor((x1 + width) / this._bufferService.cols);
|
||||
if (x2 === 0) {
|
||||
x2 = this._terminal.cols;
|
||||
x2 = this._bufferService.cols;
|
||||
y2--;
|
||||
}
|
||||
|
||||
@@ -289,7 +301,7 @@ export class Linkifier implements ILinkifier {
|
||||
},
|
||||
() => {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
this._element!.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
@@ -299,7 +311,7 @@ export class Linkifier implements ILinkifier {
|
||||
},
|
||||
() => {
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
this._element!.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
@@ -313,7 +325,22 @@ export class Linkifier implements ILinkifier {
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
|
||||
return { x1, y1, x2, y2, cols: this._bufferService.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
export class MouseZone implements IMouseZone {
|
||||
constructor(
|
||||
public x1: number,
|
||||
public y1: number,
|
||||
public x2: number,
|
||||
public y2: number,
|
||||
public clickCallback: (e: MouseEvent) => any,
|
||||
public hoverCallback: (e: MouseEvent) => any,
|
||||
public tooltipCallback: (e: MouseEvent) => any,
|
||||
public leaveCallback: () => void,
|
||||
public willLinkActivate: (e: MouseEvent) => boolean
|
||||
) {
|
||||
}
|
||||
}
|
||||
Vendored
+93
@@ -3,6 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export interface IColorManager {
|
||||
colors: IColorSet;
|
||||
}
|
||||
@@ -20,3 +23,93 @@ export interface IColorSet {
|
||||
selection: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
export interface ILinkMatcher {
|
||||
id: number;
|
||||
regex: RegExp;
|
||||
handler: LinkMatcherHandler;
|
||||
hoverTooltipCallback?: LinkMatcherHandler;
|
||||
hoverLeaveCallback?: () => void;
|
||||
matchIndex?: number;
|
||||
validationCallback?: LinkMatcherValidationCallback;
|
||||
priority?: number;
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface IRegisteredLinkMatcher extends ILinkMatcher {
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface ILinkifierEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number | undefined;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
export interface ILinkMatcherOptions {
|
||||
/**
|
||||
* The index of the link from the regex.match(text) call. This defaults to 0
|
||||
* (for regular expressions without capture groups).
|
||||
*/
|
||||
matchIndex?: number;
|
||||
/**
|
||||
* A callback that validates an individual link, returning true if valid and
|
||||
* 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 IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5",
|
||||
"es2015",
|
||||
],
|
||||
"outDir": "../../out",
|
||||
"types": [
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifierEvent, ITerminal, ILinkifierAccessor } from '../Types';
|
||||
import { ITerminal, ILinkifierAccessor } from '../Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent = null;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
|
||||
import { ILinkifierEvent, ITerminal } from '../../Types';
|
||||
import { ITerminal } from '../../Types';
|
||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user