@xterm/xterm 6.1.0-beta.20 → 6.1.0-beta.200

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.
Files changed (157) hide show
  1. package/README.md +60 -38
  2. package/css/xterm.css +29 -22
  3. package/lib/xterm.js +1 -1
  4. package/lib/xterm.js.map +1 -1
  5. package/lib/xterm.mjs +8 -34
  6. package/lib/xterm.mjs.map +4 -4
  7. package/package.json +25 -14
  8. package/src/browser/AccessibilityManager.ts +6 -3
  9. package/src/browser/Clipboard.ts +6 -3
  10. package/src/browser/CoreBrowserTerminal.ts +147 -318
  11. package/src/browser/Dom.ts +178 -0
  12. package/src/browser/Linkifier.ts +11 -11
  13. package/src/browser/OscLinkProvider.ts +3 -1
  14. package/src/browser/RenderDebouncer.ts +2 -2
  15. package/src/browser/TimeBasedDebouncer.ts +2 -2
  16. package/src/browser/Types.ts +12 -11
  17. package/src/browser/Viewport.ts +55 -20
  18. package/src/browser/decorations/BufferDecorationRenderer.ts +1 -1
  19. package/src/browser/decorations/OverviewRulerRenderer.ts +33 -17
  20. package/src/browser/input/CompositionHelper.ts +44 -8
  21. package/src/browser/public/Terminal.ts +25 -28
  22. package/src/browser/renderer/dom/DomRenderer.ts +205 -41
  23. package/src/browser/renderer/dom/DomRendererRowFactory.ts +19 -13
  24. package/src/browser/renderer/dom/WidthCache.ts +54 -52
  25. package/src/browser/renderer/shared/Constants.ts +7 -0
  26. package/src/browser/renderer/shared/TextBlinkStateManager.ts +97 -0
  27. package/src/browser/renderer/shared/Types.ts +8 -2
  28. package/src/browser/scrollable/abstractScrollbar.ts +300 -0
  29. package/src/browser/scrollable/fastDomNode.ts +126 -0
  30. package/src/browser/scrollable/globalPointerMoveMonitor.ts +90 -0
  31. package/src/browser/scrollable/horizontalScrollbar.ts +85 -0
  32. package/src/browser/scrollable/mouseEvent.ts +292 -0
  33. package/src/browser/scrollable/scrollable.ts +486 -0
  34. package/src/browser/scrollable/scrollableElement.ts +579 -0
  35. package/src/browser/scrollable/scrollableElementOptions.ts +161 -0
  36. package/src/browser/scrollable/scrollbarArrow.ts +110 -0
  37. package/src/browser/scrollable/scrollbarState.ts +246 -0
  38. package/src/browser/scrollable/scrollbarVisibilityController.ts +113 -0
  39. package/src/browser/scrollable/touch.ts +485 -0
  40. package/src/browser/scrollable/verticalScrollbar.ts +143 -0
  41. package/src/browser/scrollable/widget.ts +23 -0
  42. package/src/browser/services/CharSizeService.ts +2 -2
  43. package/src/browser/services/CoreBrowserService.ts +7 -5
  44. package/src/browser/services/KeyboardService.ts +67 -0
  45. package/src/browser/services/LinkProviderService.ts +1 -1
  46. package/src/browser/services/MouseCoordsService.ts +47 -0
  47. package/src/browser/services/MouseService.ts +518 -25
  48. package/src/browser/services/RenderService.ts +22 -15
  49. package/src/browser/services/SelectionService.ts +16 -8
  50. package/src/browser/services/Services.ts +40 -17
  51. package/src/browser/services/ThemeService.ts +2 -2
  52. package/src/common/Async.ts +105 -0
  53. package/src/common/CircularList.ts +2 -2
  54. package/src/common/Color.ts +8 -0
  55. package/src/common/CoreTerminal.ts +28 -18
  56. package/src/common/Event.ts +118 -0
  57. package/src/common/InputHandler.ts +256 -36
  58. package/src/common/Lifecycle.ts +113 -0
  59. package/src/common/Platform.ts +13 -3
  60. package/src/common/SortedList.ts +7 -3
  61. package/src/common/TaskQueue.ts +14 -5
  62. package/src/common/Types.ts +35 -15
  63. package/src/common/Version.ts +9 -0
  64. package/src/common/buffer/Buffer.ts +20 -14
  65. package/src/common/buffer/BufferLine.ts +4 -5
  66. package/src/common/buffer/BufferSet.ts +7 -6
  67. package/src/common/buffer/CellData.ts +57 -0
  68. package/src/common/buffer/Marker.ts +2 -2
  69. package/src/common/buffer/Types.ts +6 -2
  70. package/src/common/data/EscapeSequences.ts +71 -70
  71. package/src/common/input/Keyboard.ts +14 -7
  72. package/src/common/input/KittyKeyboard.ts +519 -0
  73. package/src/common/input/Win32InputMode.ts +297 -0
  74. package/src/common/input/WriteBuffer.ts +34 -2
  75. package/src/common/input/XParseColor.ts +2 -2
  76. package/src/common/parser/ApcParser.ts +245 -0
  77. package/src/common/parser/Constants.ts +22 -4
  78. package/src/common/parser/DcsParser.ts +5 -5
  79. package/src/common/parser/EscapeSequenceParser.ts +155 -43
  80. package/src/common/parser/OscParser.ts +5 -5
  81. package/src/common/parser/Types.ts +34 -1
  82. package/src/common/public/BufferLineApiView.ts +2 -2
  83. package/src/common/public/BufferNamespaceApi.ts +2 -2
  84. package/src/common/public/ParserApi.ts +3 -0
  85. package/src/common/services/BufferService.ts +8 -5
  86. package/src/common/services/CharsetService.ts +4 -0
  87. package/src/common/services/CoreService.ts +18 -4
  88. package/src/common/services/DecorationService.ts +24 -8
  89. package/src/common/services/LogService.ts +1 -31
  90. package/src/common/services/{CoreMouseService.ts → MouseStateService.ts} +21 -132
  91. package/src/common/services/OptionsService.ts +13 -4
  92. package/src/common/services/Services.ts +47 -40
  93. package/src/common/services/UnicodeService.ts +1 -1
  94. package/typings/xterm.d.ts +316 -32
  95. package/src/common/TypedArrayUtils.ts +0 -17
  96. package/src/vs/base/browser/browser.ts +0 -141
  97. package/src/vs/base/browser/canIUse.ts +0 -49
  98. package/src/vs/base/browser/dom.ts +0 -2369
  99. package/src/vs/base/browser/fastDomNode.ts +0 -316
  100. package/src/vs/base/browser/globalPointerMoveMonitor.ts +0 -112
  101. package/src/vs/base/browser/iframe.ts +0 -135
  102. package/src/vs/base/browser/keyboardEvent.ts +0 -213
  103. package/src/vs/base/browser/mouseEvent.ts +0 -229
  104. package/src/vs/base/browser/touch.ts +0 -372
  105. package/src/vs/base/browser/ui/scrollbar/abstractScrollbar.ts +0 -303
  106. package/src/vs/base/browser/ui/scrollbar/horizontalScrollbar.ts +0 -114
  107. package/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +0 -720
  108. package/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +0 -165
  109. package/src/vs/base/browser/ui/scrollbar/scrollbarArrow.ts +0 -114
  110. package/src/vs/base/browser/ui/scrollbar/scrollbarState.ts +0 -243
  111. package/src/vs/base/browser/ui/scrollbar/scrollbarVisibilityController.ts +0 -118
  112. package/src/vs/base/browser/ui/scrollbar/verticalScrollbar.ts +0 -116
  113. package/src/vs/base/browser/ui/widget.ts +0 -57
  114. package/src/vs/base/browser/window.ts +0 -14
  115. package/src/vs/base/common/arrays.ts +0 -887
  116. package/src/vs/base/common/arraysFind.ts +0 -202
  117. package/src/vs/base/common/assert.ts +0 -71
  118. package/src/vs/base/common/async.ts +0 -1992
  119. package/src/vs/base/common/cancellation.ts +0 -148
  120. package/src/vs/base/common/charCode.ts +0 -450
  121. package/src/vs/base/common/collections.ts +0 -140
  122. package/src/vs/base/common/decorators.ts +0 -130
  123. package/src/vs/base/common/equals.ts +0 -146
  124. package/src/vs/base/common/errors.ts +0 -303
  125. package/src/vs/base/common/event.ts +0 -1778
  126. package/src/vs/base/common/functional.ts +0 -32
  127. package/src/vs/base/common/hash.ts +0 -316
  128. package/src/vs/base/common/iterator.ts +0 -159
  129. package/src/vs/base/common/keyCodes.ts +0 -526
  130. package/src/vs/base/common/keybindings.ts +0 -284
  131. package/src/vs/base/common/lazy.ts +0 -47
  132. package/src/vs/base/common/lifecycle.ts +0 -801
  133. package/src/vs/base/common/linkedList.ts +0 -142
  134. package/src/vs/base/common/map.ts +0 -202
  135. package/src/vs/base/common/numbers.ts +0 -98
  136. package/src/vs/base/common/observable.ts +0 -76
  137. package/src/vs/base/common/observableInternal/api.ts +0 -31
  138. package/src/vs/base/common/observableInternal/autorun.ts +0 -281
  139. package/src/vs/base/common/observableInternal/base.ts +0 -489
  140. package/src/vs/base/common/observableInternal/debugName.ts +0 -145
  141. package/src/vs/base/common/observableInternal/derived.ts +0 -428
  142. package/src/vs/base/common/observableInternal/lazyObservableValue.ts +0 -146
  143. package/src/vs/base/common/observableInternal/logging.ts +0 -328
  144. package/src/vs/base/common/observableInternal/promise.ts +0 -209
  145. package/src/vs/base/common/observableInternal/utils.ts +0 -610
  146. package/src/vs/base/common/platform.ts +0 -281
  147. package/src/vs/base/common/scrollable.ts +0 -522
  148. package/src/vs/base/common/sequence.ts +0 -34
  149. package/src/vs/base/common/stopwatch.ts +0 -43
  150. package/src/vs/base/common/strings.ts +0 -557
  151. package/src/vs/base/common/symbols.ts +0 -9
  152. package/src/vs/base/common/uint.ts +0 -59
  153. package/src/vs/patches/nls.ts +0 -90
  154. package/src/vs/typings/base-common.d.ts +0 -20
  155. package/src/vs/typings/require.d.ts +0 -42
  156. package/src/vs/typings/vscode-globals-nls.d.ts +0 -36
  157. package/src/vs/typings/vscode-globals-product.d.ts +0 -33
@@ -30,6 +30,12 @@ export class CompositionHelper {
30
30
  */
31
31
  private _compositionPosition: IPosition;
32
32
 
33
+ /**
34
+ * Text that existed after the composing range when composition started.
35
+ * This is used to avoid treating existing trailing text as new input.
36
+ */
37
+ private _compositionSuffix: string;
38
+
33
39
  /**
34
40
  * Whether a composition is in the process of being sent, setting this to false will cancel any
35
41
  * in-progress composition.
@@ -41,6 +47,11 @@ export class CompositionHelper {
41
47
  */
42
48
  private _dataAlreadySent: string;
43
49
 
50
+ /**
51
+ * The pending textarea change timer, if any.
52
+ */
53
+ private _textareaChangeTimer?: number;
54
+
44
55
  constructor(
45
56
  private readonly _textarea: HTMLTextAreaElement,
46
57
  private readonly _compositionView: HTMLElement,
@@ -52,6 +63,7 @@ export class CompositionHelper {
52
63
  this._isComposing = false;
53
64
  this._isSendingComposition = false;
54
65
  this._compositionPosition = { start: 0, end: 0 };
66
+ this._compositionSuffix = '';
55
67
  this._dataAlreadySent = '';
56
68
  }
57
69
 
@@ -60,7 +72,13 @@ export class CompositionHelper {
60
72
  */
61
73
  public compositionstart(): void {
62
74
  this._isComposing = true;
63
- this._compositionPosition.start = this._textarea.value.length;
75
+ // It's important to use the selection here instead of textarea length to avoid conflicts with
76
+ // screen reader mode
77
+ const start = this._textarea.selectionStart ?? this._textarea.value.length;
78
+ const end = this._textarea.selectionEnd ?? start;
79
+ this._compositionPosition.start = Math.min(start, end);
80
+ this._compositionPosition.end = Math.max(start, end);
81
+ this._compositionSuffix = this._textarea.value.substring(this._compositionPosition.end);
64
82
  this._compositionView.textContent = '';
65
83
  this._dataAlreadySent = '';
66
84
  this._compositionView.classList.add('active');
@@ -71,10 +89,13 @@ export class CompositionHelper {
71
89
  * @param ev The event.
72
90
  */
73
91
  public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
74
- this._compositionView.textContent = ev.data;
92
+ // Mark text as LTR, direction=rtl is used in CSS so the end of the text is followed for long
93
+ // compositions
94
+ this._compositionView.textContent = `\u200E${ev.data}\u200E`;
75
95
  this.updateCompositionElements();
76
96
  setTimeout(() => {
77
- this._compositionPosition.end = this._textarea.value.length;
97
+ const end = this._textarea.selectionEnd ?? this._textarea.value.length;
98
+ this._compositionPosition.end = Math.max( this._compositionPosition.start, end);
78
99
  }, 0);
79
100
  }
80
101
 
@@ -141,6 +162,7 @@ export class CompositionHelper {
141
162
  start: this._compositionPosition.start,
142
163
  end: this._compositionPosition.end
143
164
  };
165
+ const currentCompositionSuffix = this._compositionSuffix;
144
166
 
145
167
  // Since composition* events happen before the changes take place in the textarea on most
146
168
  // browsers, use a setTimeout with 0ms time to allow the native compositionend event to
@@ -164,10 +186,14 @@ export class CompositionHelper {
164
186
  // if a new composition has started.
165
187
  input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
166
188
  } else {
167
- // Don't use the end position here in order to pick up any characters after the
168
- // composition has finished, for example when typing a non-composition character
169
- // (eg. 2) after a composition character.
170
- input = this._textarea.value.substring(currentCompositionPosition.start);
189
+ // Keep support for non-composition characters typed immediately after composition end
190
+ // while avoiding re-sending the trailing text that was already present
191
+ // before composition started.
192
+ const value = this._textarea.value;
193
+ const valueEnd = currentCompositionSuffix.length > 0 && value.endsWith(currentCompositionSuffix)
194
+ ? value.length - currentCompositionSuffix.length
195
+ : value.length;
196
+ input = value.substring(currentCompositionPosition.start, Math.max(currentCompositionPosition.start, valueEnd));
171
197
  }
172
198
  if (input.length > 0) {
173
199
  this._coreService.triggerDataEvent(input, true);
@@ -184,8 +210,12 @@ export class CompositionHelper {
184
210
  * IME is active.
185
211
  */
186
212
  private _handleAnyTextareaChanges(): void {
213
+ if (this._textareaChangeTimer) {
214
+ return;
215
+ }
187
216
  const oldValue = this._textarea.value;
188
- setTimeout(() => {
217
+ this._textareaChangeTimer = window.setTimeout(() => {
218
+ this._textareaChangeTimer = undefined;
189
219
  // Ignore if a composition has started since the timeout
190
220
  if (!this._isComposing) {
191
221
  const newValue = this._textarea.value;
@@ -230,6 +260,12 @@ export class CompositionHelper {
230
260
  this._compositionView.style.lineHeight = cellHeight + 'px';
231
261
  this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
232
262
  this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
263
+ // Limit the composition view width to the space between the cursor and
264
+ // the terminal's right edge, preventing it from overflowing the terminal.
265
+ const maxWidth = this._bufferService.cols * this._renderService.dimensions.css.cell.width - cursorLeft;
266
+ this._compositionView.style.maxWidth = maxWidth + 'px';
267
+ this._compositionView.style.overflow = 'hidden';
268
+ this._compositionView.style.direction = 'rtl';
233
269
  // Sync the textarea to the exact position of the composition view so the IME knows where the
234
270
  // text is.
235
271
  const compositionViewBounds = this._compositionView.getBoundingClientRect();
@@ -6,14 +6,14 @@
6
6
  import * as Strings from 'browser/LocalizableStrings';
7
7
  import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal';
8
8
  import { IBufferRange, ITerminal } from 'browser/Types';
9
- import { Disposable } from 'vs/base/common/lifecycle';
9
+ import { Disposable } from 'common/Lifecycle';
10
10
  import { ITerminalOptions } from 'common/Types';
11
11
  import { AddonManager } from 'common/public/AddonManager';
12
12
  import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
13
13
  import { ParserApi } from 'common/public/ParserApi';
14
14
  import { UnicodeApi } from 'common/public/UnicodeApi';
15
- import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
16
- import type { Event } from 'vs/base/common/event';
15
+ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
16
+ import type { IEvent } from 'common/Event';
17
17
 
18
18
  /**
19
19
  * The set of options that only have an effect when set in the Terminal constructor.
@@ -68,25 +68,24 @@ export class Terminal extends Disposable implements ITerminalApi {
68
68
  }
69
69
  }
70
70
 
71
- public get onBell(): Event<void> { return this._core.onBell; }
72
- public get onBinary(): Event<string> { return this._core.onBinary; }
73
- public get onCursorMove(): Event<void> { return this._core.onCursorMove; }
74
- public get onData(): Event<string> { return this._core.onData; }
75
- public get onKey(): Event<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
76
- public get onLineFeed(): Event<void> { return this._core.onLineFeed; }
77
- public get onRender(): Event<{ start: number, end: number }> { return this._core.onRender; }
78
- public get onResize(): Event<{ cols: number, rows: number }> { return this._core.onResize; }
79
- public get onScroll(): Event<number> { return this._core.onScroll; }
80
- public get onSelectionChange(): Event<void> { return this._core.onSelectionChange; }
81
- public get onTitleChange(): Event<string> { return this._core.onTitleChange; }
82
- public get onWriteParsed(): Event<void> { return this._core.onWriteParsed; }
71
+ public get onBell(): IEvent<void> { return this._core.onBell; }
72
+ public get onBinary(): IEvent<string> { return this._core.onBinary; }
73
+ public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
74
+ public get onData(): IEvent<string> { return this._core.onData; }
75
+ public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
76
+ public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
77
+ public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
78
+ public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
79
+ public get onScroll(): IEvent<number> { return this._core.onScroll; }
80
+ public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
81
+ public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
82
+ public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
83
+ public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._core.onDimensionsChange; }
83
84
 
84
85
  public get element(): HTMLElement | undefined { return this._core.element; }
86
+ public get screenElement(): HTMLElement | undefined { return this._core.screenElement; }
85
87
  public get parser(): IParser {
86
- if (!this._parser) {
87
- this._parser = new ParserApi(this._core);
88
- }
89
- return this._parser;
88
+ return this._parser ??= new ParserApi(this._core);
90
89
  }
91
90
  public get unicode(): IUnicodeHandling {
92
91
  this._checkProposedApi();
@@ -96,19 +95,15 @@ export class Terminal extends Disposable implements ITerminalApi {
96
95
  public get rows(): number { return this._core.rows; }
97
96
  public get cols(): number { return this._core.cols; }
98
97
  public get buffer(): IBufferNamespaceApi {
99
- if (!this._buffer) {
100
- this._buffer = this._register(new BufferNamespaceApi(this._core));
101
- }
102
- return this._buffer;
98
+ return this._buffer ??= this._register(new BufferNamespaceApi(this._core));
103
99
  }
104
100
  public get markers(): ReadonlyArray<IMarker> {
105
- this._checkProposedApi();
106
101
  return this._core.markers;
107
102
  }
108
103
  public get modes(): IModes {
109
104
  const m = this._core.coreService.decPrivateModes;
110
105
  let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
111
- switch (this._core.coreMouseService.activeProtocol) {
106
+ switch (this._core.mouseStateService.activeProtocol) {
112
107
  case 'X10': mouseTrackingMode = 'x10'; break;
113
108
  case 'VT200': mouseTrackingMode = 'vt200'; break;
114
109
  case 'DRAG': mouseTrackingMode = 'drag'; break;
@@ -123,10 +118,15 @@ export class Terminal extends Disposable implements ITerminalApi {
123
118
  originMode: m.origin,
124
119
  reverseWraparoundMode: m.reverseWraparound,
125
120
  sendFocusMode: m.sendFocus,
121
+ showCursor: !this._core.coreService.isCursorHidden,
126
122
  synchronizedOutputMode: m.synchronizedOutput,
123
+ win32InputMode: m.win32InputMode,
127
124
  wraparoundMode: m.wraparound
128
125
  };
129
126
  }
127
+ public get dimensions(): IRenderDimensions | undefined {
128
+ return this._core.dimensions;
129
+ }
130
130
  public get options(): Required<ITerminalOptions> {
131
131
  return this._publicOptions;
132
132
  }
@@ -161,11 +161,9 @@ export class Terminal extends Disposable implements ITerminalApi {
161
161
  return this._core.registerLinkProvider(linkProvider);
162
162
  }
163
163
  public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
164
- this._checkProposedApi();
165
164
  return this._core.registerCharacterJoiner(handler);
166
165
  }
167
166
  public deregisterCharacterJoiner(joinerId: number): void {
168
- this._checkProposedApi();
169
167
  this._core.deregisterCharacterJoiner(joinerId);
170
168
  }
171
169
  public registerMarker(cursorYOffset: number = 0): IMarker {
@@ -173,7 +171,6 @@ export class Terminal extends Disposable implements ITerminalApi {
173
171
  return this._core.registerMarker(cursorYOffset);
174
172
  }
175
173
  public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
176
- this._checkProposedApi();
177
174
  this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
178
175
  return this._core.registerDecoration(decorationOptions);
179
176
  }
@@ -5,16 +5,18 @@
5
5
 
6
6
  import { DomRendererRowFactory, RowCss } from 'browser/renderer/dom/DomRendererRowFactory';
7
7
  import { WidthCache } from 'browser/renderer/dom/WidthCache';
8
- import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
8
+ import { INVERTED_DEFAULT_COLOR, RendererConstants } from 'browser/renderer/shared/Constants';
9
9
  import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
10
10
  import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
11
+ import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
11
12
  import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from 'browser/renderer/shared/Types';
12
13
  import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
13
14
  import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
14
15
  import { color } from 'common/Color';
15
- import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
16
+ import { Disposable, toDisposable } from 'common/Lifecycle';
16
17
  import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
17
- import { Emitter } from 'vs/base/common/event';
18
+ import { Emitter } from 'common/Event';
19
+ import { addDisposableListener } from 'browser/Dom';
18
20
 
19
21
 
20
22
  const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
@@ -23,6 +25,7 @@ const FG_CLASS_PREFIX = 'xterm-fg-';
23
25
  const BG_CLASS_PREFIX = 'xterm-bg-';
24
26
  const FOCUS_CLASS = 'xterm-focus';
25
27
  const SELECTION_CLASS = 'xterm-selection';
28
+ const CURSOR_BLINK_IDLE_CLASS = 'xterm-cursor-blink-idle';
26
29
 
27
30
  let nextTerminalId = 1;
28
31
 
@@ -42,10 +45,18 @@ export class DomRenderer extends Disposable implements IRenderer {
42
45
  private _selectionContainer: HTMLElement;
43
46
  private _widthCache: WidthCache;
44
47
  private _selectionRenderModel: ISelectionRenderModel = createSelectionRenderModel();
48
+ private _lastSelectionStart: [number, number] | undefined;
49
+ private _lastSelectionEnd: [number, number] | undefined;
50
+ private _lastSelectionColumnMode: boolean = false;
51
+ private _cursorBlinkStateManager: CursorBlinkStateManager;
52
+ private _textBlinkStateManager: TextBlinkStateManager;
53
+ private _rowHasBlinkingCells: boolean[] = [];
54
+ private _rowHasBlinkingCellsCount: number = 0;
45
55
 
46
56
  public dimensions: IRenderDimensions;
47
57
 
48
- public readonly onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>()).event;
58
+ private readonly _onRequestRedraw = this._register(new Emitter<IRequestRedrawEvent>());
59
+ public readonly onRequestRedraw = this._onRequestRedraw.event;
49
60
 
50
61
  constructor(
51
62
  private readonly _terminal: ITerminal,
@@ -89,6 +100,15 @@ export class DomRenderer extends Disposable implements IRenderer {
89
100
  this._register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
90
101
  this._register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
91
102
 
103
+ this._cursorBlinkStateManager = new CursorBlinkStateManager(this._rowContainer, this._coreBrowserService);
104
+ this._register(addDisposableListener(this._document, 'mousedown', () => this._cursorBlinkStateManager.restartBlinkAnimation()));
105
+ this._register(toDisposable(() => this._cursorBlinkStateManager.dispose()));
106
+ this._textBlinkStateManager = this._register(new TextBlinkStateManager(
107
+ () => this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }),
108
+ this._coreBrowserService,
109
+ this._optionsService
110
+ ));
111
+
92
112
  this._register(toDisposable(() => {
93
113
  this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
94
114
 
@@ -101,7 +121,7 @@ export class DomRenderer extends Disposable implements IRenderer {
101
121
  this._dimensionsStyleElement.remove();
102
122
  }));
103
123
 
104
- this._widthCache = new WidthCache(this._document, this._helperContainer);
124
+ this._widthCache = new WidthCache();
105
125
  this._widthCache.setFont(
106
126
  this._optionsService.rawOptions.fontFamily,
107
127
  this._optionsService.rawOptions.fontSize,
@@ -167,6 +187,9 @@ export class DomRenderer extends Disposable implements IRenderer {
167
187
  // refresh() being called during the mousedown handler to start a selection.
168
188
  ` pointer-events: none;` +
169
189
  ` color: ${colors.foreground.css};` +
190
+ `}`;
191
+ styles +=
192
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}, ${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
170
193
  ` font-family: ${this._optionsService.rawOptions.fontFamily};` +
171
194
  ` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
172
195
  ` font-kerning: none;` +
@@ -186,6 +209,9 @@ export class DomRenderer extends Disposable implements IRenderer {
186
209
  `}` +
187
210
  `${this._terminalSelector} span.${RowCss.ITALIC_CLASS} {` +
188
211
  ` font-style: italic;` +
212
+ `}` +
213
+ `${this._terminalSelector} span.${RowCss.BLINK_HIDDEN_CLASS} {` +
214
+ ` visibility: hidden;` +
189
215
  `}`;
190
216
  // Blink animation
191
217
  const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
@@ -225,6 +251,10 @@ export class DomRenderer extends Disposable implements IRenderer {
225
251
  `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` +
226
252
  ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` +
227
253
  `}` +
254
+ // Disable cursor blinking when idle
255
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${CURSOR_BLINK_IDLE_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS} {` +
256
+ ` animation: none !important;` +
257
+ `}` +
228
258
  // !important helps fix an issue where the cursor will not render on top of the selection,
229
259
  // however it's very hard to fix this issue and retain the blink animation without the use of
230
260
  // !important. So this edge case fails when cursor blink is on.
@@ -307,10 +337,14 @@ export class DomRenderer extends Disposable implements IRenderer {
307
337
  const row = this._document.createElement('div');
308
338
  this._rowContainer.appendChild(row);
309
339
  this._rowElements.push(row);
340
+ this._rowHasBlinkingCells.push(false);
310
341
  }
311
342
  // Remove excess elements
312
343
  while (this._rowElements.length > rows) {
313
344
  this._rowContainer.removeChild(this._rowElements.pop()!);
345
+ if (this._rowHasBlinkingCells.pop()) {
346
+ this._rowHasBlinkingCellsCount--;
347
+ }
314
348
  }
315
349
  }
316
350
 
@@ -328,60 +362,104 @@ export class DomRenderer extends Disposable implements IRenderer {
328
362
 
329
363
  public handleBlur(): void {
330
364
  this._rowContainer.classList.remove(FOCUS_CLASS);
365
+ this._cursorBlinkStateManager.pause();
331
366
  this.renderRows(0, this._bufferService.rows - 1);
332
367
  }
333
368
 
334
369
  public handleFocus(): void {
335
370
  this._rowContainer.classList.add(FOCUS_CLASS);
371
+ this._cursorBlinkStateManager.resume();
336
372
  this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y);
337
373
  }
338
374
 
375
+ public handleViewportVisibilityChange(isVisible: boolean): void {
376
+ this._textBlinkStateManager.setViewportVisible(isVisible);
377
+ }
378
+
339
379
  public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
380
+ const rows = this._bufferService.rows;
381
+
340
382
  // Remove all selections
341
383
  this._selectionContainer.replaceChildren();
342
384
  this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
343
- this.renderRows(0, this._bufferService.rows - 1);
344
385
 
345
- // Selection does not exist
386
+ // Determine old selection viewport band
387
+ let oldViewportStart = 0;
388
+ let oldViewportEnd = -1;
389
+ if (this._lastSelectionStart && this._lastSelectionEnd) {
390
+ this._selectionRenderModel.update(this._terminal, this._lastSelectionStart, this._lastSelectionEnd, this._lastSelectionColumnMode);
391
+ if (this._selectionRenderModel.hasSelection) {
392
+ oldViewportStart = this._selectionRenderModel.viewportCappedStartRow;
393
+ oldViewportEnd = this._selectionRenderModel.viewportCappedEndRow;
394
+ }
395
+ }
396
+
397
+ // Determine new selection viewport band and create overlays
398
+ let newViewportStart = 0;
399
+ let newViewportEnd = -1;
346
400
  if (!start || !end) {
347
401
  return;
348
402
  }
349
-
350
403
  this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
351
- if (!this._selectionRenderModel.hasSelection) {
352
- return;
404
+ if (this._selectionRenderModel.hasSelection) {
405
+ const viewportStartRow = this._selectionRenderModel.viewportStartRow;
406
+ const viewportEndRow = this._selectionRenderModel.viewportEndRow;
407
+ const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
408
+ const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
409
+
410
+ newViewportStart = viewportCappedStartRow;
411
+ newViewportEnd = viewportCappedEndRow;
412
+
413
+ // Create the selections
414
+ const documentFragment = this._document.createDocumentFragment();
415
+
416
+ if (columnSelectMode) {
417
+ const isXFlipped = start[0] > end[0];
418
+ documentFragment.appendChild(
419
+ this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
420
+ );
421
+ } else {
422
+ // Draw first row
423
+ const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
424
+ const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
425
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
426
+ // Draw middle rows
427
+ const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
428
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
429
+ // Draw final row
430
+ if (viewportCappedStartRow !== viewportCappedEndRow) {
431
+ // Only draw viewportEndRow if it's not the same as viewporttartRow
432
+ const finalEndCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
433
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, finalEndCol));
434
+ }
435
+ }
436
+ this._selectionContainer.appendChild(documentFragment);
353
437
  }
354
438
 
355
- // Translate from buffer position to viewport position
356
- const viewportStartRow = this._selectionRenderModel.viewportStartRow;
357
- const viewportEndRow = this._selectionRenderModel.viewportEndRow;
358
- const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
359
- const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
360
-
361
- // Create the selections
362
- const documentFragment = this._document.createDocumentFragment();
363
-
364
- if (columnSelectMode) {
365
- const isXFlipped = start[0] > end[0];
366
- documentFragment.appendChild(
367
- this._createSelectionElement(viewportCappedStartRow, isXFlipped ? end[0] : start[0], isXFlipped ? start[0] : end[0], viewportCappedEndRow - viewportCappedStartRow + 1)
368
- );
369
- } else {
370
- // Draw first row
371
- const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
372
- const endCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
373
- documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
374
- // Draw middle rows
375
- const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
376
- documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
377
- // Draw final row
378
- if (viewportCappedStartRow !== viewportCappedEndRow) {
379
- // Only draw viewportEndRow if it's not the same as viewporttartRow
380
- const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
381
- documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
439
+ // Compute minimal row range to redraw
440
+ let renderStartRow = Math.min(oldViewportStart, newViewportStart);
441
+ let renderEndRow = Math.max(oldViewportEnd, newViewportEnd);
442
+
443
+ if (renderEndRow >= 0) {
444
+ // Clamp to viewport
445
+ renderStartRow = Math.max(renderStartRow, 0);
446
+ renderEndRow = Math.min(renderEndRow, rows - 1);
447
+
448
+ // Ensure cursor row is included when a selection is present
449
+ const buffer = this._bufferService.buffer;
450
+ const cursorViewportRow = buffer.y;
451
+ if (this._selectionRenderModel.hasSelection && cursorViewportRow >= 0 && cursorViewportRow < rows) {
452
+ renderStartRow = Math.min(renderStartRow, cursorViewportRow);
453
+ renderEndRow = Math.max(renderEndRow, cursorViewportRow);
382
454
  }
455
+
456
+ this.renderRows(renderStartRow, renderEndRow);
383
457
  }
384
- this._selectionContainer.appendChild(documentFragment);
458
+
459
+ // Update last selection state
460
+ this._lastSelectionStart = start;
461
+ this._lastSelectionEnd = end;
462
+ this._lastSelectionColumnMode = columnSelectMode;
385
463
  }
386
464
 
387
465
  /**
@@ -406,7 +484,8 @@ export class DomRenderer extends Disposable implements IRenderer {
406
484
  }
407
485
 
408
486
  public handleCursorMove(): void {
409
- // No-op, the cursor is drawn when rows are drawn
487
+ // Reset idle timer on cursor movement (which happens on input)
488
+ this._cursorBlinkStateManager.restartBlinkAnimation();
410
489
  }
411
490
 
412
491
  private _handleOptionsChanged(): void {
@@ -436,6 +515,11 @@ export class DomRenderer extends Disposable implements IRenderer {
436
515
  */
437
516
  e.replaceChildren();
438
517
  }
518
+ if (this._rowHasBlinkingCellsCount > 0) {
519
+ this._rowHasBlinkingCells.fill(false);
520
+ this._rowHasBlinkingCellsCount = 0;
521
+ this._textBlinkStateManager.setNeedsBlinkInViewport(false);
522
+ }
439
523
  }
440
524
 
441
525
  public renderRows(start: number, end: number): void {
@@ -445,6 +529,7 @@ export class DomRenderer extends Disposable implements IRenderer {
445
529
  const cursorBlink = this._coreService.decPrivateModes.cursorBlink ?? this._optionsService.rawOptions.cursorBlink;
446
530
  const cursorStyle = this._coreService.decPrivateModes.cursorStyle ?? this._optionsService.rawOptions.cursorStyle;
447
531
  const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
532
+ const rowInfo = { hasBlinkingCells: false };
448
533
 
449
534
  for (let y = start; y <= end; y++) {
450
535
  const row = y + buffer.ydisp;
@@ -462,13 +547,17 @@ export class DomRenderer extends Disposable implements IRenderer {
462
547
  cursorInactiveStyle,
463
548
  cursorX,
464
549
  cursorBlink,
550
+ this._textBlinkStateManager.isBlinkOn,
465
551
  this.dimensions.css.cell.width,
466
552
  this._widthCache,
467
553
  -1,
468
- -1
554
+ -1,
555
+ rowInfo
469
556
  )
470
557
  );
558
+ this._setRowBlinkState(y, rowInfo.hasBlinkingCells);
471
559
  }
560
+ this._updateTextBlinkState();
472
561
  }
473
562
 
474
563
  private get _terminalSelector(): string {
@@ -513,6 +602,7 @@ export class DomRenderer extends Disposable implements IRenderer {
513
602
  const cursorBlink = this._optionsService.rawOptions.cursorBlink;
514
603
  const cursorStyle = this._optionsService.rawOptions.cursorStyle;
515
604
  const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
605
+ const rowInfo = { hasBlinkingCells: false };
516
606
 
517
607
  // refresh rows within link range
518
608
  for (let i = y; i <= y2; ++i) {
@@ -531,12 +621,86 @@ export class DomRenderer extends Disposable implements IRenderer {
531
621
  cursorInactiveStyle,
532
622
  cursorX,
533
623
  cursorBlink,
624
+ this._textBlinkStateManager.isBlinkOn,
534
625
  this.dimensions.css.cell.width,
535
626
  this._widthCache,
536
627
  enabled ? (i === y ? x : 0) : -1,
537
- enabled ? ((i === y2 ? x2 : cols) - 1) : -1
628
+ enabled ? ((i === y2 ? x2 : cols) - 1) : -1,
629
+ rowInfo
538
630
  )
539
631
  );
632
+ this._setRowBlinkState(i, rowInfo.hasBlinkingCells);
633
+ }
634
+ this._updateTextBlinkState();
635
+ }
636
+
637
+ private _setRowBlinkState(row: number, hasBlinkingCells: boolean): void {
638
+ const previous = this._rowHasBlinkingCells[row];
639
+ if (previous === hasBlinkingCells) {
640
+ return;
641
+ }
642
+ this._rowHasBlinkingCells[row] = hasBlinkingCells;
643
+ this._rowHasBlinkingCellsCount += hasBlinkingCells ? 1 : -1;
644
+ }
645
+
646
+ private _updateTextBlinkState(): void {
647
+ this._textBlinkStateManager.setNeedsBlinkInViewport(this._rowHasBlinkingCellsCount > 0);
648
+ }
649
+ }
650
+
651
+ class CursorBlinkStateManager {
652
+ private _idleTimeout: number | undefined;
653
+ private _isIdlePaused: boolean = false;
654
+
655
+ constructor(
656
+ private readonly _rowContainer: HTMLElement,
657
+ private readonly _coreBrowserService: ICoreBrowserService
658
+ ) {
659
+ if (this._coreBrowserService.isFocused) {
660
+ this._resetIdleTimer();
661
+ }
662
+ }
663
+
664
+ public dispose(): void {
665
+ this._clearIdleTimer();
666
+ }
667
+
668
+ public restartBlinkAnimation(): void {
669
+ if (this._isIdlePaused) {
670
+ this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
540
671
  }
672
+ this._resetIdleTimer();
673
+ }
674
+
675
+ public pause(): void {
676
+ this._isIdlePaused = false;
677
+ this._clearIdleTimer();
678
+ }
679
+
680
+ public resume(): void {
681
+ this._isIdlePaused = false;
682
+ this._rowContainer.classList.remove(CURSOR_BLINK_IDLE_CLASS);
683
+ this._resetIdleTimer();
684
+ }
685
+
686
+ private _resetIdleTimer(): void {
687
+ this._isIdlePaused = false;
688
+ this._clearIdleTimer();
689
+ this._idleTimeout = this._coreBrowserService.window.setTimeout(() => {
690
+ this._stopBlinkingDueToIdle();
691
+ }, RendererConstants.CURSOR_BLINK_IDLE_TIMEOUT);
692
+ }
693
+
694
+ private _clearIdleTimer(): void {
695
+ if (this._idleTimeout) {
696
+ this._coreBrowserService.window.clearTimeout(this._idleTimeout);
697
+ this._idleTimeout = undefined;
698
+ }
699
+ }
700
+
701
+ private _stopBlinkingDueToIdle(): void {
702
+ this._rowContainer.classList.add(CURSOR_BLINK_IDLE_CLASS);
703
+ this._isIdlePaused = true;
704
+ this._idleTimeout = undefined;
541
705
  }
542
706
  }