@xterm/xterm 5.4.0-beta.2 → 5.4.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xterm/xterm",
3
3
  "description": "Full xterm terminal, in your browser",
4
- "version": "5.4.0-beta.2",
4
+ "version": "5.4.0-beta.21",
5
5
  "main": "lib/xterm.js",
6
6
  "style": "css/xterm.css",
7
7
  "types": "typings/xterm.d.ts",
@@ -10,6 +10,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
10
10
  import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
11
11
  import { IBuffer } from 'common/buffer/Types';
12
12
  import { IInstantiationService } from 'common/services/Services';
13
+ import { addDisposableDomListener } from 'browser/Lifecycle';
13
14
 
14
15
  const MAX_ROWS_TO_READ = 20;
15
16
 
@@ -18,11 +19,17 @@ const enum BoundaryPosition {
18
19
  BOTTOM
19
20
  }
20
21
 
22
+ // Turn this on to unhide the accessibility tree and display it under
23
+ // (instead of overlapping with) the terminal.
24
+ const DEBUG = false;
25
+
21
26
  export class AccessibilityManager extends Disposable {
27
+ private _debugRootContainer: HTMLElement | undefined;
22
28
  private _accessibilityContainer: HTMLElement;
23
29
 
24
30
  private _rowContainer: HTMLElement;
25
31
  private _rowElements: HTMLElement[];
32
+ private _rowColumns: WeakMap<HTMLElement, number[]> = new WeakMap();
26
33
 
27
34
  private _liveRegion: HTMLElement;
28
35
  private _liveRegionLineCount: number = 0;
@@ -80,7 +87,23 @@ export class AccessibilityManager extends Disposable {
80
87
  if (!this._terminal.element) {
81
88
  throw new Error('Cannot enable accessibility before Terminal.open');
82
89
  }
83
- this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
90
+
91
+ if (DEBUG) {
92
+ this._accessibilityContainer.classList.add('debug');
93
+ this._rowContainer.classList.add('debug');
94
+
95
+ // Use a `<div class="xterm">` container so that the css will still apply.
96
+ this._debugRootContainer = document.createElement('div');
97
+ this._debugRootContainer.classList.add('xterm');
98
+
99
+ this._debugRootContainer.appendChild(document.createTextNode('------start a11y------'));
100
+ this._debugRootContainer.appendChild(this._accessibilityContainer);
101
+ this._debugRootContainer.appendChild(document.createTextNode('------end a11y------'));
102
+
103
+ this._terminal.element.insertAdjacentElement('afterend', this._debugRootContainer);
104
+ } else {
105
+ this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityContainer);
106
+ }
84
107
 
85
108
  this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
86
109
  this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
@@ -92,11 +115,16 @@ export class AccessibilityManager extends Disposable {
92
115
  this.register(this._terminal.onKey(e => this._handleKey(e.key)));
93
116
  this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
94
117
  this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
118
+ this.register(addDisposableDomListener(document, 'selectionchange', () => this._handleSelectionChange()));
95
119
  this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));
96
120
 
97
121
  this._refreshRows();
98
122
  this.register(toDisposable(() => {
99
- this._accessibilityContainer.remove();
123
+ if (DEBUG) {
124
+ this._debugRootContainer!.remove();
125
+ } else {
126
+ this._accessibilityContainer.remove();
127
+ }
100
128
  this._rowElements.length = 0;
101
129
  }));
102
130
  }
@@ -149,14 +177,18 @@ export class AccessibilityManager extends Disposable {
149
177
  const buffer: IBuffer = this._terminal.buffer;
150
178
  const setSize = buffer.lines.length.toString();
151
179
  for (let i = start; i <= end; i++) {
152
- const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
180
+ const line = buffer.lines.get(buffer.ydisp + i);
181
+ const columns: number[] = [];
182
+ const lineData = line?.translateToString(true, undefined, undefined, columns) || '';
153
183
  const posInSet = (buffer.ydisp + i + 1).toString();
154
184
  const element = this._rowElements[i];
155
185
  if (element) {
156
186
  if (lineData.length === 0) {
157
187
  element.innerText = '\u00a0';
188
+ this._rowColumns.set(element, [0, 1]);
158
189
  } else {
159
190
  element.textContent = lineData;
191
+ this._rowColumns.set(element, columns);
160
192
  }
161
193
  element.setAttribute('aria-posinset', posInSet);
162
194
  element.setAttribute('aria-setsize', setSize);
@@ -233,6 +265,103 @@ export class AccessibilityManager extends Disposable {
233
265
  e.stopImmediatePropagation();
234
266
  }
235
267
 
268
+ private _handleSelectionChange(): void {
269
+ if (this._rowElements.length === 0) {
270
+ return;
271
+ }
272
+
273
+ const selection = document.getSelection();
274
+ if (!selection) {
275
+ return;
276
+ }
277
+
278
+ if (selection.isCollapsed) {
279
+ // Only do something when the anchorNode is inside the row container. This
280
+ // behavior mirrors what we do with mouse --- if the mouse clicks
281
+ // somewhere outside of the terminal, we don't clear the selection.
282
+ if (this._rowContainer.contains(selection.anchorNode)) {
283
+ this._terminal.clearSelection();
284
+ }
285
+ return;
286
+ }
287
+
288
+ if (!selection.anchorNode || !selection.focusNode) {
289
+ console.error('anchorNode and/or focusNode are null');
290
+ return;
291
+ }
292
+
293
+ // Sort the two selection points in document order.
294
+ let begin = { node: selection.anchorNode, offset: selection.anchorOffset };
295
+ let end = { node: selection.focusNode, offset: selection.focusOffset };
296
+ if ((begin.node.compareDocumentPosition(end.node) & Node.DOCUMENT_POSITION_PRECEDING) || (begin.node === end.node && begin.offset > end.offset) ) {
297
+ [begin, end] = [end, begin];
298
+ }
299
+
300
+ // Clamp begin/end to the inside of the row container.
301
+ if (begin.node.compareDocumentPosition(this._rowElements[0]) & (Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_FOLLOWING)) {
302
+ begin = { node: this._rowElements[0].childNodes[0], offset: 0 };
303
+ }
304
+ if (!this._rowContainer.contains(begin.node)) {
305
+ // This happens when `begin` is below the last row.
306
+ return;
307
+ }
308
+ const lastRowElement = this._rowElements.slice(-1)[0];
309
+ if (end.node.compareDocumentPosition(lastRowElement) & (Node.DOCUMENT_POSITION_CONTAINED_BY | Node.DOCUMENT_POSITION_PRECEDING)) {
310
+ end = {
311
+ node: lastRowElement,
312
+ offset: lastRowElement.textContent?.length ?? 0
313
+ };
314
+ }
315
+ if (!this._rowContainer.contains(end.node)) {
316
+ // This happens when `end` is above the first row.
317
+ return;
318
+ }
319
+
320
+ const toRowColumn = ({ node, offset }: typeof begin): {row: number, column: number} | null => {
321
+ // `node` is either the row element or the Text node inside it.
322
+ const rowElement: any = node instanceof Text ? node.parentNode : node;
323
+ let row = parseInt(rowElement?.getAttribute('aria-posinset'), 10) - 1;
324
+ if (isNaN(row)) {
325
+ console.warn('row is invalid. Race condition?');
326
+ return null;
327
+ }
328
+
329
+ const columns = this._rowColumns.get(rowElement);
330
+ if (!columns) {
331
+ console.warn('columns is null. Race condition?');
332
+ return null;
333
+ }
334
+
335
+ let column = offset < columns.length ? columns[offset] : columns.slice(-1)[0] + 1;
336
+ if (column >= this._terminal.cols) {
337
+ ++row;
338
+ column = 0;
339
+ }
340
+ return {
341
+ row,
342
+ column
343
+ };
344
+ };
345
+
346
+ const beginRowColumn = toRowColumn(begin);
347
+ const endRowColumn = toRowColumn(end);
348
+
349
+ if (!beginRowColumn || !endRowColumn) {
350
+ return;
351
+ }
352
+
353
+ if (beginRowColumn.row > endRowColumn.row || (beginRowColumn.row === endRowColumn.row && beginRowColumn.column >= endRowColumn.column)) {
354
+ // This should not happen unless we have some bugs.
355
+ throw new Error('invalid range');
356
+ }
357
+
358
+ this._terminal.select(
359
+ beginRowColumn.column,
360
+ beginRowColumn.row,
361
+ (endRowColumn.row - beginRowColumn.row) * this._terminal.cols - beginRowColumn.column + endRowColumn.column
362
+ );
363
+ }
364
+
236
365
  private _handleResize(rows: number): void {
237
366
  // Remove bottom boundary listener
238
367
  this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
@@ -4,18 +4,14 @@
4
4
  */
5
5
 
6
6
  import { addDisposableDomListener } from 'browser/Lifecycle';
7
- import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
7
+ import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
8
8
  import { EventEmitter } from 'common/EventEmitter';
9
9
  import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
10
10
  import { IDisposable } from 'common/Types';
11
11
  import { IBufferService } from 'common/services/Services';
12
- import { IMouseService, IRenderService } from './services/Services';
12
+ import { ILinkProviderService, IMouseService, IRenderService } from './services/Services';
13
13
 
14
- export class Linkifier2 extends Disposable implements ILinkifier2 {
15
- private _element: HTMLElement | undefined;
16
- private _mouseService: IMouseService | undefined;
17
- private _renderService: IRenderService | undefined;
18
- private _linkProviders: ILinkProvider[] = [];
14
+ export class Linkifier extends Disposable implements ILinkifier2 {
19
15
  public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
20
16
  protected _currentLink: ILinkWithState | undefined;
21
17
  private _mouseDownLink: ILinkWithState | undefined;
@@ -33,39 +29,24 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
33
29
  public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
34
30
 
35
31
  constructor(
36
- @IBufferService private readonly _bufferService: IBufferService
32
+ private readonly _element: HTMLElement,
33
+ @IMouseService private readonly _mouseService: IMouseService,
34
+ @IRenderService private readonly _renderService: IRenderService,
35
+ @IBufferService private readonly _bufferService: IBufferService,
36
+ @ILinkProviderService private readonly _linkProviderService: ILinkProviderService
37
37
  ) {
38
38
  super();
39
39
  this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
40
40
  this.register(toDisposable(() => {
41
41
  this._lastMouseEvent = undefined;
42
+ // Clear out link providers as they could easily cause an embedder memory leak
43
+ this._activeProviderReplies?.clear();
42
44
  }));
43
45
  // Listen to resize to catch the case where it's resized and the cursor is out of the viewport.
44
46
  this.register(this._bufferService.onResize(() => {
45
47
  this._clearCurrentLink();
46
48
  this._wasResized = true;
47
49
  }));
48
- }
49
-
50
- public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
51
- this._linkProviders.push(linkProvider);
52
- return {
53
- dispose: () => {
54
- // Remove the link provider from the list
55
- const providerIndex = this._linkProviders.indexOf(linkProvider);
56
-
57
- if (providerIndex !== -1) {
58
- this._linkProviders.splice(providerIndex, 1);
59
- }
60
- }
61
- };
62
- }
63
-
64
- public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
65
- this._element = element;
66
- this._mouseService = mouseService;
67
- this._renderService = renderService;
68
-
69
50
  this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
70
51
  this._isMouseOut = true;
71
52
  this._clearCurrentLink();
@@ -78,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
78
59
  private _handleMouseMove(event: MouseEvent): void {
79
60
  this._lastMouseEvent = event;
80
61
 
81
- if (!this._element || !this._mouseService) {
82
- return;
83
- }
84
-
85
62
  const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
86
63
  if (!position) {
87
64
  return;
@@ -142,7 +119,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
142
119
  let linkProvided = false;
143
120
 
144
121
  // There is no link cached, so ask for one
145
- for (const [i, linkProvider] of this._linkProviders.entries()) {
122
+ for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) {
146
123
  if (useLineCache) {
147
124
  const existingReply = this._activeProviderReplies?.get(i);
148
125
  // If there isn't a reply, the provider hasn't responded yet.
@@ -164,7 +141,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
164
141
 
165
142
  // If all providers have responded, remove lower priority links that intersect ranges of
166
143
  // higher priority links
167
- if (this._activeProviderReplies?.size === this._linkProviders.length) {
144
+ if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) {
168
145
  this._removeIntersectingLinks(position.y, this._activeProviderReplies);
169
146
  }
170
147
  });
@@ -220,7 +197,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
220
197
  }
221
198
 
222
199
  // Check if all the providers have responded
223
- if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
200
+ if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) {
224
201
  // Respect the order of the link providers
225
202
  for (let j = 0; j < this._activeProviderReplies.size; j++) {
226
203
  const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
@@ -240,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
240
217
  }
241
218
 
242
219
  private _handleMouseUp(event: MouseEvent): void {
243
- if (!this._element || !this._mouseService || !this._currentLink) {
220
+ if (!this._currentLink) {
244
221
  return;
245
222
  }
246
223
 
@@ -255,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
255
232
  }
256
233
 
257
234
  private _clearCurrentLink(startRow?: number, endRow?: number): void {
258
- if (!this._element || !this._currentLink || !this._lastMouseEvent) {
235
+ if (!this._currentLink || !this._lastMouseEvent) {
259
236
  return;
260
237
  }
261
238
 
@@ -268,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
268
245
  }
269
246
 
270
247
  private _handleNewLink(linkWithState: ILinkWithState): void {
271
- if (!this._element || !this._lastMouseEvent || !this._mouseService) {
248
+ if (!this._lastMouseEvent) {
272
249
  return;
273
250
  }
274
251
 
@@ -299,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
299
276
  if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
300
277
  this._currentLink.state.decorations.pointerCursor = v;
301
278
  if (this._currentLink.state.isHovered) {
302
- this._element?.classList.toggle('xterm-cursor-pointer', v);
279
+ this._element.classList.toggle('xterm-cursor-pointer', v);
303
280
  }
304
281
  }
305
282
  }
@@ -319,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
319
296
 
320
297
  // Listen to viewport changes to re-render the link under the cursor (only when the line the
321
298
  // link is on changes)
322
- if (this._renderService) {
323
- this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
324
- // Sanity check, this shouldn't happen in practice as this listener would be disposed
325
- if (!this._currentLink) {
326
- return;
327
- }
328
- // When start is 0 a scroll most likely occurred, make sure links above the fold also get
329
- // cleared.
330
- const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
331
- const end = this._bufferService.buffer.ydisp + 1 + e.end;
332
- // Only clear the link if the viewport change happened on this line
333
- if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
334
- this._clearCurrentLink(start, end);
335
- if (this._lastMouseEvent && this._element) {
336
- // re-eval previously active link after changes
337
- const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
338
- if (position) {
339
- this._askForLink(position, false);
340
- }
299
+ this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => {
300
+ // Sanity check, this shouldn't happen in practice as this listener would be disposed
301
+ if (!this._currentLink) {
302
+ return;
303
+ }
304
+ // When start is 0 a scroll most likely occurred, make sure links above the fold also get
305
+ // cleared.
306
+ const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
307
+ const end = this._bufferService.buffer.ydisp + 1 + e.end;
308
+ // Only clear the link if the viewport change happened on this line
309
+ if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) {
310
+ this._clearCurrentLink(start, end);
311
+ if (this._lastMouseEvent) {
312
+ // re-eval previously active link after changes
313
+ const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!);
314
+ if (position) {
315
+ this._askForLink(position, false);
341
316
  }
342
317
  }
343
- }));
344
- }
318
+ }
319
+ }));
345
320
  }
346
321
  }
347
322
 
@@ -3,7 +3,8 @@
3
3
  * @license MIT
4
4
  */
5
5
 
6
- import { IBufferRange, ILink, ILinkProvider } from 'browser/Types';
6
+ import { IBufferRange, ILink } from 'browser/Types';
7
+ import { ILinkProvider } from 'browser/services/Services';
7
8
  import { CellData } from 'common/buffer/CellData';
8
9
  import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
9
10
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IRenderDebouncerWithCallback } from 'browser/Types';
7
+ import { ICoreBrowserService } from 'browser/services/Services';
7
8
 
8
9
  /**
9
10
  * Debounces calls to render terminal rows using animation frames.
@@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
16
17
  private _refreshCallbacks: FrameRequestCallback[] = [];
17
18
 
18
19
  constructor(
19
- private _parentWindow: Window,
20
- private _renderCallback: (start: number, end: number) => void
20
+ private _renderCallback: (start: number, end: number) => void,
21
+ private readonly _coreBrowserService: ICoreBrowserService
21
22
  ) {
22
23
  }
23
24
 
24
25
  public dispose(): void {
25
26
  if (this._animationFrame) {
26
- this._parentWindow.cancelAnimationFrame(this._animationFrame);
27
+ this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
27
28
  this._animationFrame = undefined;
28
29
  }
29
30
  }
@@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
31
32
  public addRefreshCallback(callback: FrameRequestCallback): number {
32
33
  this._refreshCallbacks.push(callback);
33
34
  if (!this._animationFrame) {
34
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
35
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
35
36
  }
36
37
  return this._animationFrame;
37
38
  }
@@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
49
50
  return;
50
51
  }
51
52
 
52
- this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh());
53
+ this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
53
54
  }
54
55
 
55
56
  private _innerRefresh(): void {
@@ -23,10 +23,10 @@
23
23
 
24
24
  import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
25
25
  import { addDisposableDomListener } from 'browser/Lifecycle';
26
- import { Linkifier2 } from 'browser/Linkifier2';
26
+ import { Linkifier } from './Linkifier';
27
27
  import * as Strings from 'browser/LocalizableStrings';
28
28
  import { OscLinkProvider } from 'browser/OscLinkProvider';
29
- import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
29
+ import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types';
30
30
  import { Viewport } from 'browser/Viewport';
31
31
  import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
32
32
  import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
@@ -39,7 +39,7 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService';
39
39
  import { MouseService } from 'browser/services/MouseService';
40
40
  import { RenderService } from 'browser/services/RenderService';
41
41
  import { SelectionService } from 'browser/services/SelectionService';
42
- import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
42
+ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
43
43
  import { ThemeService } from 'browser/services/ThemeService';
44
44
  import { color, rgba } from 'common/Color';
45
45
  import { CoreTerminal } from 'common/CoreTerminal';
@@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services';
57
57
  import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm';
58
58
  import { WindowsOptionsReportType } from '../common/InputHandler';
59
59
  import { AccessibilityManager } from './AccessibilityManager';
60
+ import { LinkProviderService } from 'browser/services/LinkProviderService';
60
61
 
61
62
  export class Terminal extends CoreTerminal implements ITerminal {
62
63
  public textarea: HTMLTextAreaElement | undefined;
@@ -69,14 +70,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
69
70
  private _helperContainer: HTMLElement | undefined;
70
71
  private _compositionView: HTMLElement | undefined;
71
72
 
73
+ public linkifier: ILinkifier2 | undefined;
72
74
  private _overviewRulerRenderer: OverviewRulerRenderer | undefined;
73
75
 
74
76
  public browser: IBrowser = Browser as any;
75
77
 
76
78
  private _customKeyEventHandler: CustomKeyEventHandler | undefined;
79
+ private _customWheelEventHandler: CustomWheelEventHandler | undefined;
77
80
 
78
- // browser services
81
+ // Browser services
79
82
  private _decorationService: DecorationService;
83
+ private _linkProviderService: ILinkProviderService;
84
+
85
+ // Optional browser services
80
86
  private _charSizeService: ICharSizeService | undefined;
81
87
  private _coreBrowserService: ICoreBrowserService | undefined;
82
88
  private _mouseService: IMouseService | undefined;
@@ -112,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
112
118
  */
113
119
  private _unprocessedDeadKey: boolean = false;
114
120
 
115
- public linkifier2: ILinkifier2;
116
121
  public viewport: IViewport | undefined;
117
122
  private _compositionHelper: ICompositionHelper | undefined;
118
123
  private _accessibilityManager: MutableDisposable<AccessibilityManager> = this.register(new MutableDisposable());
@@ -148,10 +153,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
148
153
 
149
154
  this._setup();
150
155
 
151
- this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
152
- this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
153
156
  this._decorationService = this._instantiationService.createInstance(DecorationService);
154
157
  this._instantiationService.setService(IDecorationService, this._decorationService);
158
+ this._linkProviderService = this._instantiationService.createInstance(LinkProviderService);
159
+ this._instantiationService.setService(ILinkProviderService, this._linkProviderService);
160
+ this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
155
161
 
156
162
  // Setup InputHandler listeners
157
163
  this.register(this._inputHandler.onRequestBell(() => this._onBell.fire()));
@@ -260,11 +266,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
260
266
  /**
261
267
  * Binds the desired focus behavior on a given terminal object.
262
268
  */
263
- private _handleTextAreaFocus(ev: KeyboardEvent): void {
269
+ private _handleTextAreaFocus(ev: FocusEvent): void {
264
270
  if (this.coreService.decPrivateModes.sendFocus) {
265
271
  this.coreService.triggerDataEvent(C0.ESC + '[I');
266
272
  }
267
- this.updateCursorStyle(ev);
268
273
  this.element!.classList.add('focus');
269
274
  this._showCursor();
270
275
  this._onFocus.fire();
@@ -428,6 +433,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
428
433
 
429
434
  this.screenElement = this._document.createElement('div');
430
435
  this.screenElement.classList.add('xterm-screen');
436
+ this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev)));
431
437
  // Create the container that will hold helpers like the textarea for
432
438
  // capturing DOM Events. Then produce the helpers.
433
439
  this._helperContainer = this._document.createElement('div');
@@ -458,11 +464,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
458
464
  ));
459
465
  this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
460
466
 
461
- this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
467
+ this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev)));
462
468
  this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
463
469
  this._helperContainer.appendChild(this.textarea);
464
470
 
465
-
466
471
  this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
467
472
  this._instantiationService.setService(ICharSizeService, this._charSizeService);
468
473
 
@@ -482,6 +487,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
482
487
  this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
483
488
  this._helperContainer.appendChild(this._compositionView);
484
489
 
490
+ this._mouseService = this._instantiationService.createInstance(MouseService);
491
+ this._instantiationService.setService(IMouseService, this._mouseService);
492
+
493
+ this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement));
494
+
485
495
  // Performance: Add viewport and helper elements from the fragment
486
496
  this.element.appendChild(fragment);
487
497
 
@@ -493,9 +503,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
493
503
  this._renderService.setRenderer(this._createRenderer());
494
504
  }
495
505
 
496
- this._mouseService = this._instantiationService.createInstance(MouseService);
497
- this._instantiationService.setService(IMouseService, this._mouseService);
498
-
499
506
  this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea);
500
507
  this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)),
501
508
  this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
@@ -513,7 +520,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
513
520
  this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
514
521
  this.element,
515
522
  this.screenElement,
516
- this.linkifier2
523
+ this.linkifier
517
524
  ));
518
525
  this._instantiationService.setService(ISelectionService, this._selectionService);
519
526
  this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
@@ -533,7 +540,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
533
540
  }));
534
541
  this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh()));
535
542
 
536
- this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
537
543
  this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
538
544
  this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
539
545
 
@@ -575,7 +581,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
575
581
  }
576
582
 
577
583
  private _createRenderer(): IRenderer {
578
- return this._instantiationService.createInstance(DomRenderer, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2);
584
+ return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!);
579
585
  }
580
586
 
581
587
  /**
@@ -633,6 +639,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
633
639
  but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
634
640
  break;
635
641
  case 'wheel':
642
+ if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) {
643
+ return false;
644
+ }
636
645
  const amount = self.viewport!.getLinesScrolled(ev as WheelEvent);
637
646
 
638
647
  if (amount === 0) {
@@ -792,6 +801,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
792
801
  // do nothing, if app side handles wheel itself
793
802
  if (requestedEvents.wheel) return;
794
803
 
804
+ if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) {
805
+ return false;
806
+ }
807
+
795
808
  if (!this.buffer.hasScrollback) {
796
809
  // Convert wheel events into up/down events when the buffer does not have scrollback, this
797
810
  // enables scrolling in apps hosted in the alt buffer such as vim or tmux.
@@ -847,7 +860,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
847
860
  /**
848
861
  * Change the cursor style for different selection modes
849
862
  */
850
- public updateCursorStyle(ev: KeyboardEvent): void {
863
+ public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void {
851
864
  if (this._selectionService?.shouldColumnSelect(ev)) {
852
865
  this.element!.classList.add('column-select');
853
866
  } else {
@@ -878,21 +891,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
878
891
  paste(data, this.textarea!, this.coreService, this.optionsService);
879
892
  }
880
893
 
881
- /**
882
- * Attaches a custom key event handler which is run before keys are processed,
883
- * giving consumers of xterm.js ultimate control as to what keys should be
884
- * processed by the terminal and what keys should not.
885
- * @param customKeyEventHandler The custom KeyboardEvent handler to attach.
886
- * This is a function that takes a KeyboardEvent, allowing consumers to stop
887
- * propagation and/or prevent the default action. The function returns whether
888
- * the event should be processed by xterm.js.
889
- */
890
894
  public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void {
891
895
  this._customKeyEventHandler = customKeyEventHandler;
892
896
  }
893
897
 
898
+ public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void {
899
+ this._customWheelEventHandler = customWheelEventHandler;
900
+ }
901
+
894
902
  public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
895
- return this.linkifier2.registerLinkProvider(linkProvider);
903
+ return this._linkProviderService.registerLinkProvider(linkProvider);
896
904
  }
897
905
 
898
906
  public registerCharacterJoiner(handler: CharacterJoinerHandler): number {