@theia/terminal 1.45.0 → 1.46.0-next.72

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 (126) hide show
  1. package/README.md +30 -30
  2. package/lib/browser/base/terminal-service.d.ts +34 -34
  3. package/lib/browser/base/terminal-service.js +7 -7
  4. package/lib/browser/base/terminal-widget.d.ts +192 -192
  5. package/lib/browser/base/terminal-widget.js +34 -34
  6. package/lib/browser/index.d.ts +1 -1
  7. package/lib/browser/index.js +28 -28
  8. package/lib/browser/search/terminal-search-container.d.ts +3 -3
  9. package/lib/browser/search/terminal-search-container.js +28 -28
  10. package/lib/browser/search/terminal-search-widget.d.ts +30 -30
  11. package/lib/browser/search/terminal-search-widget.js +147 -147
  12. package/lib/browser/shell-terminal-profile.d.ts +20 -20
  13. package/lib/browser/shell-terminal-profile.js +42 -42
  14. package/lib/browser/terminal-contribution.d.ts +3 -3
  15. package/lib/browser/terminal-contribution.js +20 -20
  16. package/lib/browser/terminal-copy-on-selection-handler.d.ts +10 -10
  17. package/lib/browser/terminal-copy-on-selection-handler.js +103 -103
  18. package/lib/browser/terminal-file-link-provider.d.ts +24 -24
  19. package/lib/browser/terminal-file-link-provider.js +200 -200
  20. package/lib/browser/terminal-frontend-contribution.d.ts +115 -115
  21. package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
  22. package/lib/browser/terminal-frontend-contribution.js +1078 -1056
  23. package/lib/browser/terminal-frontend-contribution.js.map +1 -1
  24. package/lib/browser/terminal-frontend-module.d.ts +5 -5
  25. package/lib/browser/terminal-frontend-module.js +117 -117
  26. package/lib/browser/terminal-link-helpers.d.ts +27 -27
  27. package/lib/browser/terminal-link-helpers.js +155 -155
  28. package/lib/browser/terminal-link-provider.d.ts +51 -51
  29. package/lib/browser/terminal-link-provider.js +197 -197
  30. package/lib/browser/terminal-preferences.d.ts +61 -61
  31. package/lib/browser/terminal-preferences.d.ts.map +1 -1
  32. package/lib/browser/terminal-preferences.js +357 -356
  33. package/lib/browser/terminal-preferences.js.map +1 -1
  34. package/lib/browser/terminal-profile-service.d.ts +58 -58
  35. package/lib/browser/terminal-profile-service.js +158 -158
  36. package/lib/browser/terminal-quick-open-service.d.ts +36 -36
  37. package/lib/browser/terminal-quick-open-service.js +137 -137
  38. package/lib/browser/terminal-theme-service.d.ts +21 -21
  39. package/lib/browser/terminal-theme-service.d.ts.map +1 -1
  40. package/lib/browser/terminal-theme-service.js +222 -218
  41. package/lib/browser/terminal-theme-service.js.map +1 -1
  42. package/lib/browser/terminal-url-link-provider.d.ts +11 -11
  43. package/lib/browser/terminal-url-link-provider.js +76 -76
  44. package/lib/browser/terminal-widget-impl.d.ts +180 -187
  45. package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
  46. package/lib/browser/terminal-widget-impl.js +857 -867
  47. package/lib/browser/terminal-widget-impl.js.map +1 -1
  48. package/lib/common/base-terminal-protocol.d.ts +55 -55
  49. package/lib/common/base-terminal-protocol.js +84 -84
  50. package/lib/common/shell-terminal-protocol.d.ts +66 -66
  51. package/lib/common/shell-terminal-protocol.js +35 -35
  52. package/lib/common/terminal-common-module.d.ts +7 -7
  53. package/lib/common/terminal-common-module.js +31 -31
  54. package/lib/common/terminal-protocol.d.ts +12 -12
  55. package/lib/common/terminal-protocol.js +21 -21
  56. package/lib/common/terminal-watcher.d.ts +13 -13
  57. package/lib/common/terminal-watcher.js +70 -70
  58. package/lib/node/base-terminal-server.d.ts +24 -24
  59. package/lib/node/base-terminal-server.js +168 -168
  60. package/lib/node/buffering-stream.d.ts +39 -39
  61. package/lib/node/buffering-stream.js +75 -75
  62. package/lib/node/buffering-stream.spec.d.ts +1 -1
  63. package/lib/node/buffering-stream.spec.js +43 -43
  64. package/lib/node/index.d.ts +1 -1
  65. package/lib/node/index.js +28 -28
  66. package/lib/node/shell-process.d.ts +27 -27
  67. package/lib/node/shell-process.js +107 -107
  68. package/lib/node/shell-process.js.map +1 -1
  69. package/lib/node/shell-terminal-server.d.ts +30 -30
  70. package/lib/node/shell-terminal-server.js +212 -212
  71. package/lib/node/shell-terminal-server.spec.d.ts +1 -1
  72. package/lib/node/shell-terminal-server.spec.js +35 -35
  73. package/lib/node/terminal-backend-contribution.d.ts +9 -9
  74. package/lib/node/terminal-backend-contribution.js +75 -75
  75. package/lib/node/terminal-backend-contribution.slow-spec.d.ts +1 -1
  76. package/lib/node/terminal-backend-contribution.slow-spec.js +54 -54
  77. package/lib/node/terminal-backend-module.d.ts +11 -11
  78. package/lib/node/terminal-backend-module.js +69 -69
  79. package/lib/node/terminal-server.d.ts +9 -9
  80. package/lib/node/terminal-server.js +64 -64
  81. package/lib/node/terminal-server.spec.d.ts +1 -1
  82. package/lib/node/terminal-server.spec.js +41 -41
  83. package/lib/node/test/terminal-test-container.d.ts +2 -2
  84. package/lib/node/test/terminal-test-container.js +40 -40
  85. package/lib/package.spec.js +25 -25
  86. package/package.json +12 -12
  87. package/src/browser/base/terminal-service.ts +60 -60
  88. package/src/browser/base/terminal-widget.ts +254 -254
  89. package/src/browser/index.ts +17 -17
  90. package/src/browser/search/terminal-search-container.ts +28 -28
  91. package/src/browser/search/terminal-search-widget.tsx +161 -161
  92. package/src/browser/shell-terminal-profile.ts +45 -45
  93. package/src/browser/style/terminal-search.css +99 -99
  94. package/src/browser/style/terminal.css +32 -32
  95. package/src/browser/terminal-contribution.ts +19 -19
  96. package/src/browser/terminal-copy-on-selection-handler.ts +92 -92
  97. package/src/browser/terminal-file-link-provider.ts +200 -200
  98. package/src/browser/terminal-frontend-contribution.ts +1120 -1098
  99. package/src/browser/terminal-frontend-module.ts +136 -136
  100. package/src/browser/terminal-link-helpers.ts +187 -187
  101. package/src/browser/terminal-link-provider.ts +203 -203
  102. package/src/browser/terminal-preferences.ts +428 -427
  103. package/src/browser/terminal-profile-service.ts +180 -180
  104. package/src/browser/terminal-quick-open-service.ts +132 -132
  105. package/src/browser/terminal-theme-service.ts +213 -209
  106. package/src/browser/terminal-url-link-provider.ts +66 -66
  107. package/src/browser/terminal-widget-impl.ts +939 -936
  108. package/src/common/base-terminal-protocol.ts +125 -125
  109. package/src/common/shell-terminal-protocol.ts +103 -103
  110. package/src/common/terminal-common-module.ts +30 -30
  111. package/src/common/terminal-protocol.ts +32 -32
  112. package/src/common/terminal-watcher.ts +69 -69
  113. package/src/node/base-terminal-server.ts +173 -173
  114. package/src/node/buffering-stream.spec.ts +46 -46
  115. package/src/node/buffering-stream.ts +95 -95
  116. package/src/node/index.ts +17 -17
  117. package/src/node/shell-process.ts +101 -101
  118. package/src/node/shell-terminal-server.spec.ts +40 -40
  119. package/src/node/shell-terminal-server.ts +221 -221
  120. package/src/node/terminal-backend-contribution.slow-spec.ts +63 -63
  121. package/src/node/terminal-backend-contribution.ts +60 -60
  122. package/src/node/terminal-backend-module.ts +82 -82
  123. package/src/node/terminal-server.spec.ts +47 -47
  124. package/src/node/terminal-server.ts +52 -52
  125. package/src/node/test/terminal-test-container.ts +39 -39
  126. package/src/package.spec.ts +28 -28
@@ -1,936 +1,939 @@
1
- // *****************************************************************************
2
- // Copyright (C) 2017 TypeFox and others.
3
- //
4
- // This program and the accompanying materials are made available under the
5
- // terms of the Eclipse Public License v. 2.0 which is available at
6
- // http://www.eclipse.org/legal/epl-2.0.
7
- //
8
- // This Source Code may also be made available under the following Secondary
9
- // Licenses when the conditions for such availability set forth in the Eclipse
10
- // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
- // with the GNU Classpath Exception which is available at
12
- // https://www.gnu.org/software/classpath/license.html.
13
- //
14
- // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
- // *****************************************************************************
16
-
17
- import { Terminal, RendererType } from 'xterm';
18
- import { FitAddon } from 'xterm-addon-fit';
19
- import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
20
- import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS } from '@theia/core';
21
- import {
22
- Widget, Message, StatefulWidget, isFirefox, MessageLoop, KeyCode, codicon, ExtractableWidget, ContextMenuRenderer
23
- } from '@theia/core/lib/browser';
24
- import { isOSX } from '@theia/core/lib/common';
25
- import { WorkspaceService } from '@theia/workspace/lib/browser';
26
- import { ShellTerminalServerProxy, IShellTerminalPreferences } from '../common/shell-terminal-protocol';
27
- import { terminalsPath } from '../common/terminal-protocol';
28
- import { IBaseTerminalServer, TerminalProcessInfo, TerminalExitReason } from '../common/base-terminal-protocol';
29
- import { TerminalWatcher } from '../common/terminal-watcher';
30
- import {
31
- TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions,
32
- TerminalLocation
33
- } from './base/terminal-widget';
34
- import { Deferred } from '@theia/core/lib/common/promise-util';
35
- import { TerminalPreferences, TerminalRendererType, isTerminalRendererType, DEFAULT_TERMINAL_RENDERER_TYPE, CursorStyle } from './terminal-preferences';
36
- import URI from '@theia/core/lib/common/uri';
37
- import { TerminalService } from './base/terminal-service';
38
- import { TerminalSearchWidgetFactory, TerminalSearchWidget } from './search/terminal-search-widget';
39
- import { TerminalCopyOnSelectionHandler } from './terminal-copy-on-selection-handler';
40
- import { TerminalThemeService } from './terminal-theme-service';
41
- import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder';
42
- import { Key } from '@theia/core/lib/browser/keys';
43
- import { nls } from '@theia/core/lib/common/nls';
44
- import { TerminalMenus } from './terminal-frontend-contribution';
45
- import debounce = require('p-debounce');
46
- import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
47
- import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-preview-widget';
48
- import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
49
- import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
50
-
51
- export const TERMINAL_WIDGET_FACTORY_ID = 'terminal';
52
-
53
- export interface TerminalWidgetFactoryOptions extends Partial<TerminalWidgetOptions> {
54
- /* a unique string per terminal */
55
- created: string
56
- }
57
-
58
- export const TerminalContribution = Symbol('TerminalContribution');
59
- export interface TerminalContribution {
60
- onCreate(term: TerminalWidgetImpl): void;
61
- }
62
-
63
- @injectable()
64
- export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget, EnhancedPreviewWidget {
65
- readonly isExtractable: boolean = true;
66
- secondaryWindow: Window | undefined;
67
- location: TerminalLocationOptions;
68
-
69
- static LABEL = nls.localizeByDefault('Terminal');
70
-
71
- exitStatus: TerminalExitStatus | undefined;
72
-
73
- protected terminalKind = 'user';
74
- protected _terminalId = -1;
75
- protected readonly onTermDidClose = new Emitter<TerminalWidget>();
76
- protected fitAddon: FitAddon;
77
- protected term: Terminal;
78
- protected searchBox: TerminalSearchWidget;
79
- protected restored = false;
80
- protected closeOnDispose = true;
81
- protected waitForConnection: Deferred<Channel> | undefined;
82
- protected linkHover: HTMLDivElement;
83
- protected linkHoverButton: HTMLAnchorElement;
84
- protected lastTouchEnd: TouchEvent | undefined;
85
- protected lastMousePosition: { x: number, y: number } | undefined;
86
- protected isAttachedCloseListener: boolean = false;
87
- protected shown = false;
88
- protected enhancedPreviewNode: Node | undefined;
89
- override lastCwd = new URI();
90
-
91
- @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
92
- @inject(RemoteConnectionProvider) protected readonly connectionProvider: ServiceConnectionProvider;
93
- @inject(TerminalWidgetOptions) options: TerminalWidgetOptions;
94
- @inject(ShellTerminalServerProxy) protected readonly shellTerminalServer: ShellTerminalServerProxy;
95
- @inject(TerminalWatcher) protected readonly terminalWatcher: TerminalWatcher;
96
- @inject(ILogger) @named('terminal') protected readonly logger: ILogger;
97
- @inject('terminal-dom-id') override readonly id: string;
98
- @inject(TerminalPreferences) protected readonly preferences: TerminalPreferences;
99
- @inject(ContributionProvider) @named(TerminalContribution) protected readonly terminalContributionProvider: ContributionProvider<TerminalContribution>;
100
- @inject(TerminalService) protected readonly terminalService: TerminalService;
101
- @inject(TerminalSearchWidgetFactory) protected readonly terminalSearchBoxFactory: TerminalSearchWidgetFactory;
102
- @inject(TerminalCopyOnSelectionHandler) protected readonly copyOnSelectionHandler: TerminalCopyOnSelectionHandler;
103
- @inject(TerminalThemeService) protected readonly themeService: TerminalThemeService;
104
- @inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder;
105
- @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
106
- @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory;
107
-
108
- protected _markdownRenderer: MarkdownRenderer | undefined;
109
- protected get markdownRenderer(): MarkdownRenderer {
110
- this._markdownRenderer ||= this.markdownRendererFactory();
111
- return this._markdownRenderer;
112
- }
113
-
114
- protected readonly onDidOpenEmitter = new Emitter<void>();
115
- readonly onDidOpen: Event<void> = this.onDidOpenEmitter.event;
116
-
117
- protected readonly onDidOpenFailureEmitter = new Emitter<void>();
118
- readonly onDidOpenFailure: Event<void> = this.onDidOpenFailureEmitter.event;
119
-
120
- protected readonly onSizeChangedEmitter = new Emitter<{ cols: number; rows: number; }>();
121
- readonly onSizeChanged: Event<{ cols: number; rows: number; }> = this.onSizeChangedEmitter.event;
122
-
123
- protected readonly onDataEmitter = new Emitter<string>();
124
- readonly onData: Event<string> = this.onDataEmitter.event;
125
-
126
- protected readonly onKeyEmitter = new Emitter<{ key: string, domEvent: KeyboardEvent }>();
127
- readonly onKey: Event<{ key: string, domEvent: KeyboardEvent }> = this.onKeyEmitter.event;
128
-
129
- protected readonly onMouseEnterLinkHoverEmitter = new Emitter<MouseEvent>();
130
- readonly onMouseEnterLinkHover: Event<MouseEvent> = this.onMouseEnterLinkHoverEmitter.event;
131
-
132
- protected readonly onMouseLeaveLinkHoverEmitter = new Emitter<MouseEvent>();
133
- readonly onMouseLeaveLinkHover: Event<MouseEvent> = this.onMouseLeaveLinkHoverEmitter.event;
134
-
135
- protected readonly toDisposeOnConnect = new DisposableCollection();
136
-
137
- @postConstruct()
138
- protected init(): void {
139
- this.setTitle(this.options.title || TerminalWidgetImpl.LABEL);
140
-
141
- if (this.options.iconClass) {
142
- this.title.iconClass = this.options.iconClass;
143
- } else {
144
- this.title.iconClass = codicon('terminal');
145
- }
146
-
147
- if (this.options.kind) {
148
- this.terminalKind = this.options.kind;
149
- }
150
-
151
- if (this.options.destroyTermOnClose === true) {
152
- this.toDispose.push(Disposable.create(() =>
153
- this.term.dispose()
154
- ));
155
- }
156
-
157
- this.location = this.options.location || TerminalLocation.Panel;
158
-
159
- this.title.closable = true;
160
- this.addClass('terminal-container');
161
-
162
- this.term = new Terminal({
163
- cursorBlink: this.preferences['terminal.integrated.cursorBlinking'],
164
- cursorStyle: this.getCursorStyle(),
165
- cursorWidth: this.preferences['terminal.integrated.cursorWidth'],
166
- fontFamily: this.preferences['terminal.integrated.fontFamily'],
167
- fontSize: this.preferences['terminal.integrated.fontSize'],
168
- fontWeight: this.preferences['terminal.integrated.fontWeight'],
169
- fontWeightBold: this.preferences['terminal.integrated.fontWeightBold'],
170
- drawBoldTextInBrightColors: this.preferences['terminal.integrated.drawBoldTextInBrightColors'],
171
- letterSpacing: this.preferences['terminal.integrated.letterSpacing'],
172
- lineHeight: this.preferences['terminal.integrated.lineHeight'],
173
- scrollback: this.preferences['terminal.integrated.scrollback'],
174
- fastScrollSensitivity: this.preferences['terminal.integrated.fastScrollSensitivity'],
175
- rendererType: this.getTerminalRendererType(this.preferences['terminal.integrated.rendererType']),
176
- theme: this.themeService.theme
177
- });
178
-
179
- this.fitAddon = new FitAddon();
180
- this.term.loadAddon(this.fitAddon);
181
-
182
- this.initializeLinkHover();
183
-
184
- this.toDispose.push(this.preferences.onPreferenceChanged(change => {
185
- const lastSeparator = change.preferenceName.lastIndexOf('.');
186
- if (lastSeparator > 0) {
187
- let preferenceName = change.preferenceName.substring(lastSeparator + 1);
188
- let preferenceValue = change.newValue;
189
-
190
- if (preferenceName === 'rendererType') {
191
- const newRendererType = preferenceValue as string;
192
- if (newRendererType !== this.getTerminalRendererType(newRendererType)) {
193
- // Given terminal renderer type is not supported or invalid
194
- preferenceValue = DEFAULT_TERMINAL_RENDERER_TYPE;
195
- }
196
- } else if (preferenceName === 'cursorBlinking') {
197
- // Convert the terminal preference into a valid `xterm` option
198
- preferenceName = 'cursorBlink';
199
- } else if (preferenceName === 'cursorStyle') {
200
- preferenceValue = this.getCursorStyle();
201
- }
202
- try {
203
- this.term.setOption(preferenceName, preferenceValue);
204
- } catch (e) {
205
- console.debug(`xterm configuration: '${preferenceName}' with value '${preferenceValue}' is not valid.`);
206
- }
207
- this.needsResize = true;
208
- this.update();
209
- }
210
- }));
211
-
212
- this.toDispose.push(this.themeService.onDidChange(() => this.term.setOption('theme', this.themeService.theme)));
213
- this.attachCustomKeyEventHandler();
214
- const titleChangeListenerDispose = this.term.onTitleChange((title: string) => {
215
- if (this.options.useServerTitle) {
216
- this.title.label = title;
217
- }
218
- });
219
- this.toDispose.push(titleChangeListenerDispose);
220
-
221
- this.toDispose.push(this.terminalWatcher.onTerminalError(({ terminalId, error, attached }) => {
222
- if (terminalId === this.terminalId) {
223
- this.exitStatus = { code: undefined, reason: TerminalExitReason.Process };
224
- this.logger.error(`The terminal process terminated. Cause: ${error}`);
225
- if (!attached) {
226
- this.dispose();
227
- }
228
- }
229
- }));
230
- this.toDispose.push(this.terminalWatcher.onTerminalExit(({ terminalId, code, reason, attached }) => {
231
- if (terminalId === this.terminalId) {
232
- if (reason) {
233
- this.exitStatus = { code, reason };
234
- } else {
235
- this.exitStatus = { code, reason: TerminalExitReason.Process };
236
- }
237
- if (!attached) {
238
- this.dispose();
239
- }
240
- }
241
- }));
242
- this.toDispose.push(this.toDisposeOnConnect);
243
- this.toDispose.push(this.shellTerminalServer.onDidCloseConnection(() => {
244
- const disposable = this.shellTerminalServer.onDidOpenConnection(() => {
245
- disposable.dispose();
246
- this.reconnectTerminalProcess();
247
- });
248
- this.toDispose.push(disposable);
249
- }));
250
- this.toDispose.push(this.onTermDidClose);
251
- this.toDispose.push(this.onDidOpenEmitter);
252
- this.toDispose.push(this.onDidOpenFailureEmitter);
253
- this.toDispose.push(this.onSizeChangedEmitter);
254
- this.toDispose.push(this.onDataEmitter);
255
- this.toDispose.push(this.onKeyEmitter);
256
-
257
- const touchEndListener = (event: TouchEvent) => {
258
- if (this.node.contains(event.target as Node)) {
259
- this.lastTouchEnd = event;
260
- }
261
- };
262
- document.addEventListener('touchend', touchEndListener, { passive: true });
263
- this.onDispose(() => {
264
- document.removeEventListener('touchend', touchEndListener);
265
- });
266
-
267
- const mouseListener = (event: MouseEvent) => {
268
- this.lastMousePosition = { x: event.x, y: event.y };
269
- };
270
- this.node.addEventListener('mousemove', mouseListener);
271
- this.onDispose(() => {
272
- this.node.removeEventListener('mousemove', mouseListener);
273
- });
274
-
275
- const contextMenuListener = (event: MouseEvent) => {
276
- event.preventDefault();
277
- event.stopPropagation();
278
- this.contextMenuRenderer.render({ menuPath: TerminalMenus.TERMINAL_CONTEXT_MENU, anchor: event });
279
- };
280
- this.node.addEventListener('contextmenu', contextMenuListener);
281
- this.onDispose(() => this.node.removeEventListener('contextmenu', contextMenuListener));
282
-
283
- this.toDispose.push(this.term.onSelectionChange(() => {
284
- if (this.copyOnSelection) {
285
- this.copyOnSelectionHandler.copy(this.term.getSelection());
286
- }
287
- }));
288
-
289
- this.toDispose.push(this.term.onResize(data => {
290
- this.onSizeChangedEmitter.fire(data);
291
- }));
292
-
293
- this.toDispose.push(this.term.onData(data => {
294
- this.onDataEmitter.fire(data);
295
- }));
296
-
297
- this.toDispose.push(this.term.onBinary(data => {
298
- this.onDataEmitter.fire(data);
299
- }));
300
-
301
- this.toDispose.push(this.term.onKey(data => {
302
- this.onKeyEmitter.fire(data);
303
- }));
304
-
305
- for (const contribution of this.terminalContributionProvider.getContributions()) {
306
- contribution.onCreate(this);
307
- }
308
-
309
- this.searchBox = this.terminalSearchBoxFactory(this.term);
310
- this.toDispose.push(this.searchBox);
311
- }
312
-
313
- get kind(): 'user' | string {
314
- return this.terminalKind;
315
- }
316
-
317
- /**
318
- * Get the cursor style compatible with `xterm`.
319
- * @returns CursorStyle
320
- */
321
- private getCursorStyle(): CursorStyle {
322
- const value = this.preferences['terminal.integrated.cursorStyle'];
323
- return value === 'line' ? 'bar' : value;
324
- }
325
-
326
- /**
327
- * Returns given renderer type if it is valid and supported or default renderer otherwise.
328
- *
329
- * @param terminalRendererType desired terminal renderer type
330
- */
331
- private getTerminalRendererType(terminalRendererType?: string | TerminalRendererType): RendererType {
332
- if (terminalRendererType && isTerminalRendererType(terminalRendererType)) {
333
- return terminalRendererType;
334
- }
335
- return DEFAULT_TERMINAL_RENDERER_TYPE;
336
- }
337
-
338
- protected initializeLinkHover(): void {
339
- this.linkHover = document.createElement('div');
340
- this.linkHover.style.position = 'fixed';
341
- this.linkHover.style.color = 'var(--theia-editorHoverWidget-foreground)';
342
- this.linkHover.style.backgroundColor = 'var(--theia-editorHoverWidget-background)';
343
- this.linkHover.style.borderColor = 'var(--theia-editorHoverWidget-border)';
344
- this.linkHover.style.borderWidth = '0.5px';
345
- this.linkHover.style.borderStyle = 'solid';
346
- this.linkHover.style.padding = '5px';
347
- // Above the xterm.js canvas layers:
348
- // https://github.com/xtermjs/xterm.js/blob/ff790236c1b205469f17a21246141f512d844295/src/renderer/Renderer.ts#L41-L46
349
- this.linkHover.style.zIndex = '10';
350
- // Initially invisible:
351
- this.linkHover.style.display = 'none';
352
-
353
- this.linkHoverButton = document.createElement('a');
354
- this.linkHoverButton.textContent = this.linkHoverMessage();
355
- this.linkHoverButton.style.cursor = 'pointer';
356
- this.linkHover.appendChild(this.linkHoverButton);
357
-
358
- const cmdCtrl = isOSX ? 'cmd' : 'ctrl';
359
- const cmdHint = document.createTextNode(` (${nls.localizeByDefault(`${cmdCtrl} + click`)})`);
360
- this.linkHover.appendChild(cmdHint);
361
-
362
- const onMouseEnter = (mouseEvent: MouseEvent) => this.onMouseEnterLinkHoverEmitter.fire(mouseEvent);
363
- this.linkHover.addEventListener('mouseenter', onMouseEnter);
364
- this.toDispose.push(Disposable.create(() => this.linkHover.removeEventListener('mouseenter', onMouseEnter)));
365
-
366
- const onMouseLeave = (mouseEvent: MouseEvent) => this.onMouseLeaveLinkHoverEmitter.fire(mouseEvent);
367
- this.linkHover.addEventListener('mouseleave', onMouseLeave);
368
- this.toDispose.push(Disposable.create(() => this.linkHover.removeEventListener('mouseleave', onMouseLeave)));
369
-
370
- this.node.appendChild(this.linkHover);
371
- }
372
-
373
- showLinkHover(invokeAction: (event: MouseEvent) => void, x: number, y: number, message?: string): void {
374
- const mouseY = this.lastMousePosition?.y ?? y;
375
- const mouseX = this.lastMousePosition?.x ?? x;
376
- this.linkHoverButton.textContent = this.linkHoverMessage(message);
377
- this.linkHoverButton.onclick = (mouseEvent: MouseEvent) => invokeAction(mouseEvent);
378
- this.linkHover.style.display = 'inline';
379
- this.linkHover.style.top = `${mouseY - 30}px`;
380
- this.linkHover.style.left = `${mouseX - 60}px`;
381
- }
382
-
383
- protected linkHoverMessage(message?: string): string {
384
- return message ?? nls.localizeByDefault('Follow link');
385
- }
386
-
387
- hideLinkHover(): void {
388
- this.linkHover.style.display = 'none';
389
- // eslint-disable-next-line no-null/no-null
390
- this.linkHoverButton.onclick = null;
391
- }
392
-
393
- getTerminal(): Terminal {
394
- return this.term;
395
- }
396
-
397
- getSearchBox(): TerminalSearchWidget {
398
- return this.searchBox;
399
- }
400
-
401
- protected override onCloseRequest(msg: Message): void {
402
- this.exitStatus = { code: undefined, reason: TerminalExitReason.User };
403
- super.onCloseRequest(msg);
404
- }
405
-
406
- get dimensions(): TerminalDimensions {
407
- return {
408
- cols: this.term.cols,
409
- rows: this.term.rows,
410
- };
411
- }
412
-
413
- get cwd(): Promise<URI> {
414
- if (!IBaseTerminalServer.validateId(this.terminalId)) {
415
- return Promise.reject(new Error('terminal is not started'));
416
- }
417
- if (this.terminalService.getById(this.id)) {
418
- return this.shellTerminalServer.getCwdURI(this.terminalId)
419
- .then(cwdUrl => {
420
- this.lastCwd = new URI(cwdUrl);
421
- return this.lastCwd;
422
- }).catch(() => this.lastCwd);
423
- }
424
- return Promise.resolve(new URI());
425
- }
426
-
427
- get processId(): Promise<number> {
428
- if (!IBaseTerminalServer.validateId(this.terminalId)) {
429
- return Promise.reject(new Error('terminal is not started'));
430
- }
431
- return this.shellTerminalServer.getProcessId(this.terminalId);
432
- }
433
-
434
- get processInfo(): Promise<TerminalProcessInfo> {
435
- if (!IBaseTerminalServer.validateId(this.terminalId)) {
436
- return Promise.reject(new Error('terminal is not started'));
437
- }
438
- return this.shellTerminalServer.getProcessInfo(this.terminalId);
439
- }
440
-
441
- get envVarCollectionDescriptionsByExtension(): Promise<Map<string, (string | MarkdownString | undefined)[]>> {
442
- if (!IBaseTerminalServer.validateId(this.terminalId)) {
443
- return Promise.reject(new Error('terminal is not started'));
444
- }
445
- return this.shellTerminalServer.getEnvVarCollectionDescriptionsByExtension(this.terminalId);
446
- }
447
-
448
- get terminalId(): number {
449
- return this._terminalId;
450
- }
451
-
452
- get lastTouchEndEvent(): TouchEvent | undefined {
453
- return this.lastTouchEnd;
454
- }
455
-
456
- get hiddenFromUser(): boolean {
457
- if (this.shown) {
458
- return false;
459
- }
460
- return this.options.hideFromUser ?? false;
461
- }
462
-
463
- get transient(): boolean {
464
- // The terminal is transient if session persistence is disabled or it's explicitly marked as transient
465
- return !this.preferences['terminal.integrated.enablePersistentSessions'] || !!this.options.isTransient;
466
- }
467
-
468
- onDispose(onDispose: () => void): void {
469
- this.toDispose.push(Disposable.create(onDispose));
470
- }
471
-
472
- clearOutput(): void {
473
- this.term.clear();
474
- }
475
-
476
- selectAll(): void {
477
- this.term.selectAll();
478
- }
479
-
480
- async hasChildProcesses(): Promise<boolean> {
481
- return this.shellTerminalServer.hasChildProcesses(await this.processId);
482
- }
483
-
484
- storeState(): object {
485
- this.closeOnDispose = false;
486
- if (this.transient || this.options.isPseudoTerminal) {
487
- return {};
488
- }
489
- return { terminalId: this.terminalId, titleLabel: this.title.label };
490
- }
491
-
492
- restoreState(oldState: object): void {
493
- // transient terminals and pseudo terminals are not restored
494
- if (this.transient || this.options.isPseudoTerminal) {
495
- this.dispose();
496
- return;
497
- }
498
- if (this.restored === false) {
499
- const state = oldState as { terminalId: number, titleLabel: string };
500
- /* This is a workaround to issue #879 */
501
- this.restored = true;
502
- this.title.label = state.titleLabel;
503
- this.start(state.terminalId);
504
- }
505
- }
506
-
507
- /**
508
- * Create a new shell terminal in the back-end and attach it to a
509
- * new terminal widget.
510
- * If id is provided attach to the terminal for this id.
511
- */
512
- async start(id?: number): Promise<number> {
513
- this._terminalId = typeof id !== 'number' ? await this.createTerminal() : await this.attachTerminal(id);
514
- this.resizeTerminalProcess();
515
- this.connectTerminalProcess();
516
- if (IBaseTerminalServer.validateId(this.terminalId)) {
517
- this.onDidOpenEmitter.fire(undefined);
518
- await this.shellTerminalServer.onAttachAttempted(this._terminalId);
519
- return this.terminalId;
520
- }
521
- this.onDidOpenFailureEmitter.fire(undefined);
522
- throw new Error('Failed to start terminal' + (id ? ` for id: ${id}.` : '.'));
523
- }
524
-
525
- protected async attachTerminal(id: number): Promise<number> {
526
- const terminalId = await this.shellTerminalServer.attach(id);
527
- if (IBaseTerminalServer.validateId(terminalId)) {
528
- // reset exit status if a new terminal process is attached
529
- this.exitStatus = undefined;
530
- return terminalId;
531
- }
532
- this.logger.warn(`Failed attaching to terminal id ${id}, the terminal is most likely gone. Starting up a new terminal instead.`);
533
- if (this.kind === 'user') {
534
- return this.createTerminal();
535
- } else {
536
- return -1;
537
- }
538
- }
539
-
540
- protected async createTerminal(): Promise<number> {
541
- let rootURI = this.options.cwd?.toString();
542
- if (!rootURI) {
543
- const root = (await this.workspaceService.roots)[0];
544
- rootURI = root?.resource?.toString();
545
- }
546
- const { cols, rows } = this.term;
547
-
548
- const terminalId = await this.shellTerminalServer.create({
549
- shell: this.options.shellPath || this.shellPreferences.shell[OS.backend.type()],
550
- args: this.options.shellArgs || this.shellPreferences.shellArgs[OS.backend.type()],
551
- env: this.options.env,
552
- strictEnv: this.options.strictEnv,
553
- isPseudo: this.options.isPseudoTerminal,
554
- rootURI,
555
- cols,
556
- rows
557
- });
558
- if (IBaseTerminalServer.validateId(terminalId)) {
559
- return terminalId;
560
- }
561
- throw new Error('Error creating terminal widget, see the backend error log for more information.');
562
- }
563
-
564
- override processMessage(msg: Message): void {
565
- super.processMessage(msg);
566
- switch (msg.type) {
567
- case 'fit-request':
568
- this.onFitRequest(msg);
569
- break;
570
- default:
571
- break;
572
- }
573
- }
574
- protected override onFitRequest(msg: Message): void {
575
- super.onFitRequest(msg);
576
- MessageLoop.sendMessage(this, Widget.ResizeMessage.UnknownSize);
577
- }
578
- protected override onActivateRequest(msg: Message): void {
579
- super.onActivateRequest(msg);
580
- this.term.focus();
581
- }
582
- protected override onAfterShow(msg: Message): void {
583
- super.onAfterShow(msg);
584
- this.update();
585
- this.shown = true;
586
- }
587
- protected override onAfterAttach(msg: Message): void {
588
- Widget.attach(this.searchBox, this.node);
589
- super.onAfterAttach(msg);
590
- this.update();
591
- }
592
- protected override onBeforeDetach(msg: Message): void {
593
- Widget.detach(this.searchBox);
594
- super.onBeforeDetach(msg);
595
- }
596
- protected override onResize(msg: Widget.ResizeMessage): void {
597
- super.onResize(msg);
598
- this.needsResize = true;
599
- this.update();
600
- }
601
-
602
- protected needsResize = true;
603
- protected override onUpdateRequest(msg: Message): void {
604
- super.onUpdateRequest(msg);
605
- if (!this.isVisible || !this.isAttached) {
606
- return;
607
- }
608
-
609
- this.open();
610
-
611
- if (this.needsResize) {
612
- this.resizeTerminal();
613
- this.needsResize = false;
614
-
615
- this.resizeTerminalProcess();
616
- }
617
- }
618
-
619
- // Device status code emitted by Xterm.js
620
- // Check: https://github.com/xtermjs/xterm.js/blob/release/3.14/src/InputHandler.ts#L1055-L1082
621
- protected readonly deviceStatusCodes = new Set(['\u001B[>0;276;0c', '\u001B[>85;95;0c', '\u001B[>83;40003;0c', '\u001B[?1;2c', '\u001B[?6c']);
622
-
623
- protected connectTerminalProcess(): void {
624
- if (typeof this.terminalId !== 'number') {
625
- return;
626
- }
627
- if (this.options.isPseudoTerminal) {
628
- return;
629
- }
630
- this.toDisposeOnConnect.dispose();
631
- this.toDispose.push(this.toDisposeOnConnect);
632
- const waitForConnection = this.waitForConnection = new Deferred<Channel>();
633
- this.connectionProvider.listen(
634
- `${terminalsPath}/${this.terminalId}`,
635
- (path, connection) => {
636
- connection.onMessage(e => {
637
- this.write(e().readString());
638
- });
639
-
640
- // Excludes the device status code emitted by Xterm.js
641
- const sendData = (data?: string) => {
642
- if (data && !this.deviceStatusCodes.has(data) && !this.disableEnterWhenAttachCloseListener()) {
643
- connection.getWriteBuffer().writeString(data).commit();
644
- }
645
- };
646
-
647
- const disposable = new DisposableCollection();
648
- disposable.push(this.term.onData(sendData));
649
- disposable.push(this.term.onBinary(sendData));
650
-
651
- connection.onClose(() => disposable.dispose());
652
-
653
- if (waitForConnection) {
654
- waitForConnection.resolve(connection);
655
- }
656
- }, false);
657
- }
658
- protected async reconnectTerminalProcess(): Promise<void> {
659
- if (this.options.isPseudoTerminal) {
660
- return;
661
- }
662
- if (typeof this.terminalId === 'number') {
663
- await this.start(this.terminalId);
664
- }
665
- }
666
-
667
- protected termOpened = false;
668
- protected initialData = '';
669
- protected open(): void {
670
- if (this.termOpened) {
671
- return;
672
- }
673
- this.term.open(this.node);
674
-
675
- if (isFirefox) {
676
- // monkey patching intersection observer handling for secondary window support
677
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
678
- const renderService: any = (this.term as any)._core._renderService;
679
- const originalFunc: (entry: IntersectionObserverEntry) => void = renderService._onIntersectionChange.bind(renderService);
680
- const replacement = function (entry: IntersectionObserverEntry): void {
681
- if (entry.target.ownerDocument !== document) {
682
- // in Firefox, the intersection observer always reports the widget as non-intersecting if the dom element
683
- // is in a different document from when the IntersectionObserver started observing. Since we know
684
- // that the widget is always "visible" when in a secondary window, so we mark the entry as "intersecting"
685
- const patchedEvent: IntersectionObserverEntry = {
686
- ...entry,
687
- isIntersecting: true,
688
- };
689
- originalFunc(patchedEvent);
690
- } else {
691
- originalFunc(entry);
692
- }
693
- };
694
-
695
- renderService._onIntersectionChange = replacement;
696
- }
697
-
698
- if (this.initialData) {
699
- this.term.write(this.initialData);
700
- }
701
- this.termOpened = true;
702
- this.initialData = '';
703
-
704
- if (isFirefox) {
705
- // The software scrollbars don't work with xterm.js, so we disable the scrollbar if we are on firefox.
706
- if (this.term.element) {
707
- (this.term.element.children.item(0) as HTMLElement).style.overflow = 'hidden';
708
- }
709
- }
710
- }
711
-
712
- write(data: string): void {
713
- if (this.termOpened) {
714
- this.term.write(data);
715
- } else {
716
- this.initialData += data;
717
- }
718
- }
719
-
720
- resize(cols: number, rows: number): void {
721
- this.term.resize(cols, rows);
722
- }
723
-
724
- sendText(text: string): void {
725
- if (this.waitForConnection) {
726
- this.waitForConnection.promise.then(connection =>
727
- connection.getWriteBuffer().writeString(text).commit()
728
- );
729
- }
730
- }
731
-
732
- async executeCommand(commandOptions: CommandLineOptions): Promise<void> {
733
- this.sendText(this.shellCommandBuilder.buildCommand(await this.processInfo, commandOptions) + OS.backend.EOL);
734
- }
735
-
736
- scrollLineUp(): void {
737
- this.term.scrollLines(-1);
738
- }
739
-
740
- scrollLineDown(): void {
741
- this.term.scrollLines(1);
742
- }
743
-
744
- scrollToTop(): void {
745
- this.term.scrollToTop();
746
- }
747
-
748
- scrollToBottom(): void {
749
- this.term.scrollToBottom();
750
- }
751
-
752
- scrollPageUp(): void {
753
- this.term.scrollPages(-1);
754
- }
755
-
756
- scrollPageDown(): void {
757
- this.term.scrollPages(1);
758
- }
759
-
760
- resetTerminal(): void {
761
- this.term.reset();
762
- }
763
-
764
- writeLine(text: string): void {
765
- this.term.writeln(text);
766
- }
767
-
768
- get onTerminalDidClose(): Event<TerminalWidget> {
769
- return this.onTermDidClose.event;
770
- }
771
-
772
- override dispose(): void {
773
- if (this.closeOnDispose === true && typeof this.terminalId === 'number' && !this.exitStatus) {
774
- // Close the backend terminal only when explicitly closing the terminal
775
- // a refresh for example won't close it.
776
- this.shellTerminalServer.close(this.terminalId);
777
- // Exit status is set when terminal is closed by user or by process, so most likely an extension closed it.
778
- this.exitStatus = { code: undefined, reason: TerminalExitReason.Extension };
779
- }
780
- if (this.exitStatus) {
781
- this.onTermDidClose.fire(this);
782
- }
783
- if (this.enhancedPreviewNode) {
784
- // don't use preview node anymore. rendered markdown will be disposed on super call
785
- this.enhancedPreviewNode = undefined;
786
- }
787
- super.dispose();
788
- }
789
-
790
- protected resizeTerminal = debounce(() => this.doResizeTerminal(), 50);
791
-
792
- protected doResizeTerminal(): void {
793
- if (this.isDisposed) {
794
- return;
795
- }
796
- const geo = this.fitAddon.proposeDimensions();
797
- const cols = geo.cols;
798
- const rows = geo.rows - 1; // subtract one row for margin
799
- this.term.resize(cols, rows);
800
- }
801
-
802
- protected resizeTerminalProcess(): void {
803
- if (this.options.isPseudoTerminal) {
804
- return;
805
- }
806
- if (!IBaseTerminalServer.validateId(this.terminalId)
807
- || this.exitStatus
808
- || !this.terminalService.getById(this.id)
809
- ) {
810
- return;
811
- }
812
- const { cols, rows } = this.term;
813
- this.shellTerminalServer.resize(this.terminalId, cols, rows);
814
- }
815
-
816
- protected get enableCopy(): boolean {
817
- return this.preferences['terminal.enableCopy'];
818
- }
819
-
820
- protected get enablePaste(): boolean {
821
- return this.preferences['terminal.enablePaste'];
822
- }
823
-
824
- protected get shellPreferences(): IShellTerminalPreferences {
825
- return {
826
- shell: {
827
- Windows: this.preferences['terminal.integrated.shell.windows'] ?? undefined,
828
- Linux: this.preferences['terminal.integrated.shell.linux'] ?? undefined,
829
- OSX: this.preferences['terminal.integrated.shell.osx'] ?? undefined,
830
- },
831
- shellArgs: {
832
- Windows: this.preferences['terminal.integrated.shellArgs.windows'],
833
- Linux: this.preferences['terminal.integrated.shellArgs.linux'],
834
- OSX: this.preferences['terminal.integrated.shellArgs.osx'],
835
- }
836
- };
837
- }
838
-
839
- protected customKeyHandler(event: KeyboardEvent): boolean {
840
- const keyBindings = KeyCode.createKeyCode(event).toString();
841
- const ctrlCmdCopy = (isOSX && keyBindings === 'meta+c') || (!isOSX && keyBindings === 'ctrl+c');
842
- const ctrlCmdPaste = (isOSX && keyBindings === 'meta+v') || (!isOSX && keyBindings === 'ctrl+v');
843
- if (ctrlCmdCopy && this.enableCopy && this.term.hasSelection()) {
844
- return false;
845
- }
846
- if (ctrlCmdPaste && this.enablePaste) {
847
- return false;
848
- }
849
- return true;
850
- }
851
-
852
- protected get copyOnSelection(): boolean {
853
- return this.preferences['terminal.integrated.copyOnSelection'];
854
- }
855
-
856
- protected attachCustomKeyEventHandler(): void {
857
- this.term.attachCustomKeyEventHandler(e => this.customKeyHandler(e));
858
- }
859
-
860
- setTitle(title: string): void {
861
- this.title.caption = title;
862
- this.title.label = title;
863
- }
864
-
865
- waitOnExit(waitOnExit?: boolean | string): void {
866
- if (waitOnExit) {
867
- if (typeof waitOnExit === 'string') {
868
- let message = waitOnExit;
869
- // Bold the message and add an extra new line to make it stand out from the rest of the output
870
- message = `\r\n\x1b[1m${message}\x1b[0m`;
871
- this.write(message);
872
- }
873
- this.attachPressEnterKeyToCloseListener(this.term);
874
- return;
875
- }
876
- this.dispose();
877
- }
878
-
879
- private attachPressEnterKeyToCloseListener(term: Terminal): void {
880
- if (term.textarea) {
881
- this.isAttachedCloseListener = true;
882
- this.addKeyListener(term.textarea, Key.ENTER, (event: KeyboardEvent) => {
883
- this.dispose();
884
- this.isAttachedCloseListener = false;
885
- });
886
- }
887
- }
888
-
889
- private disableEnterWhenAttachCloseListener(): boolean {
890
- return this.isAttachedCloseListener;
891
- }
892
-
893
- getEnhancedPreviewNode(): Node | undefined {
894
- if (this.enhancedPreviewNode) {
895
- return this.enhancedPreviewNode;
896
- }
897
-
898
- this.enhancedPreviewNode = document.createElement('div');
899
-
900
- Promise.all([this.envVarCollectionDescriptionsByExtension, this.processId, this.processInfo])
901
- .then((values: [Map<string, (string | MarkdownString | undefined)[]>, number, TerminalProcessInfo]) => {
902
- const extensions = values[0];
903
- const processId = values[1];
904
- const processInfo = values[2];
905
-
906
- const markdown = new MarkdownStringImpl();
907
- markdown.appendMarkdown('Process ID: ' + processId + '\\\n');
908
- markdown.appendMarkdown('Command line: ' +
909
- processInfo.executable +
910
- ' ' +
911
- processInfo.arguments.join(' ') +
912
- '\n\n---\n\n');
913
- markdown.appendMarkdown('The following extensions have contributed to this terminal\'s environment:\n');
914
- extensions.forEach((arr, key) => {
915
- arr.forEach(value => {
916
- if (value === undefined) {
917
- markdown.appendMarkdown('* ' + key + '\n');
918
- } else if (typeof value === 'string') {
919
- markdown.appendMarkdown('* ' + key + ': ' + value + '\n');
920
- } else {
921
- markdown.appendMarkdown('* ' + key + ': ' + value.value + '\n');
922
- }
923
- });
924
- });
925
-
926
- const enhancedPreviewNode = this.enhancedPreviewNode;
927
- if (!this.isDisposed && enhancedPreviewNode) {
928
- const result = this.markdownRenderer.render(markdown);
929
- this.toDispose.push(result);
930
- enhancedPreviewNode.appendChild(result.element);
931
- }
932
- });
933
-
934
- return this.enhancedPreviewNode;
935
- }
936
- }
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017 TypeFox and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { Terminal } from 'xterm';
18
+ import { FitAddon } from 'xterm-addon-fit';
19
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
20
+ import { ContributionProvider, Disposable, Event, Emitter, ILogger, DisposableCollection, Channel, OS } from '@theia/core';
21
+ import {
22
+ Widget, Message, StatefulWidget, isFirefox, MessageLoop, KeyCode, codicon, ExtractableWidget, ContextMenuRenderer
23
+ } from '@theia/core/lib/browser';
24
+ import { isOSX } from '@theia/core/lib/common';
25
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
26
+ import { ShellTerminalServerProxy, IShellTerminalPreferences } from '../common/shell-terminal-protocol';
27
+ import { terminalsPath } from '../common/terminal-protocol';
28
+ import { IBaseTerminalServer, TerminalProcessInfo, TerminalExitReason } from '../common/base-terminal-protocol';
29
+ import { TerminalWatcher } from '../common/terminal-watcher';
30
+ import {
31
+ TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions,
32
+ TerminalLocation
33
+ } from './base/terminal-widget';
34
+ import { Deferred } from '@theia/core/lib/common/promise-util';
35
+ import { TerminalPreferences } from './terminal-preferences';
36
+ import URI from '@theia/core/lib/common/uri';
37
+ import { TerminalService } from './base/terminal-service';
38
+ import { TerminalSearchWidgetFactory, TerminalSearchWidget } from './search/terminal-search-widget';
39
+ import { TerminalCopyOnSelectionHandler } from './terminal-copy-on-selection-handler';
40
+ import { TerminalThemeService } from './terminal-theme-service';
41
+ import { CommandLineOptions, ShellCommandBuilder } from '@theia/process/lib/common/shell-command-builder';
42
+ import { Key } from '@theia/core/lib/browser/keys';
43
+ import { nls } from '@theia/core/lib/common/nls';
44
+ import { TerminalMenus } from './terminal-frontend-contribution';
45
+ import debounce = require('p-debounce');
46
+ import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
47
+ import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-preview-widget';
48
+ import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
49
+ import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
50
+
51
+ export const TERMINAL_WIDGET_FACTORY_ID = 'terminal';
52
+
53
+ export interface TerminalWidgetFactoryOptions extends Partial<TerminalWidgetOptions> {
54
+ /* a unique string per terminal */
55
+ created: string
56
+ }
57
+
58
+ export const TerminalContribution = Symbol('TerminalContribution');
59
+ export interface TerminalContribution {
60
+ onCreate(term: TerminalWidgetImpl): void;
61
+ }
62
+
63
+ @injectable()
64
+ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget, ExtractableWidget, EnhancedPreviewWidget {
65
+ readonly isExtractable: boolean = true;
66
+ secondaryWindow: Window | undefined;
67
+ location: TerminalLocationOptions;
68
+
69
+ static LABEL = nls.localizeByDefault('Terminal');
70
+
71
+ exitStatus: TerminalExitStatus | undefined;
72
+
73
+ protected terminalKind = 'user';
74
+ protected _terminalId = -1;
75
+ protected readonly onTermDidClose = new Emitter<TerminalWidget>();
76
+ protected fitAddon: FitAddon;
77
+ protected term: Terminal;
78
+ protected searchBox: TerminalSearchWidget;
79
+ protected restored = false;
80
+ protected closeOnDispose = true;
81
+ protected waitForConnection: Deferred<Channel> | undefined;
82
+ protected linkHover: HTMLDivElement;
83
+ protected linkHoverButton: HTMLAnchorElement;
84
+ protected lastTouchEnd: TouchEvent | undefined;
85
+ protected lastMousePosition: { x: number, y: number } | undefined;
86
+ protected isAttachedCloseListener: boolean = false;
87
+ protected shown = false;
88
+ protected enhancedPreviewNode: Node | undefined;
89
+ override lastCwd = new URI();
90
+
91
+ @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
92
+ @inject(RemoteConnectionProvider) protected readonly connectionProvider: ServiceConnectionProvider;
93
+ @inject(TerminalWidgetOptions) options: TerminalWidgetOptions;
94
+ @inject(ShellTerminalServerProxy) protected readonly shellTerminalServer: ShellTerminalServerProxy;
95
+ @inject(TerminalWatcher) protected readonly terminalWatcher: TerminalWatcher;
96
+ @inject(ILogger) @named('terminal') protected readonly logger: ILogger;
97
+ @inject('terminal-dom-id') override readonly id: string;
98
+ @inject(TerminalPreferences) protected readonly preferences: TerminalPreferences;
99
+ @inject(ContributionProvider) @named(TerminalContribution) protected readonly terminalContributionProvider: ContributionProvider<TerminalContribution>;
100
+ @inject(TerminalService) protected readonly terminalService: TerminalService;
101
+ @inject(TerminalSearchWidgetFactory) protected readonly terminalSearchBoxFactory: TerminalSearchWidgetFactory;
102
+ @inject(TerminalCopyOnSelectionHandler) protected readonly copyOnSelectionHandler: TerminalCopyOnSelectionHandler;
103
+ @inject(TerminalThemeService) protected readonly themeService: TerminalThemeService;
104
+ @inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder;
105
+ @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
106
+ @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory;
107
+
108
+ protected _markdownRenderer: MarkdownRenderer | undefined;
109
+ protected get markdownRenderer(): MarkdownRenderer {
110
+ this._markdownRenderer ||= this.markdownRendererFactory();
111
+ return this._markdownRenderer;
112
+ }
113
+
114
+ protected readonly onDidOpenEmitter = new Emitter<void>();
115
+ readonly onDidOpen: Event<void> = this.onDidOpenEmitter.event;
116
+
117
+ protected readonly onDidOpenFailureEmitter = new Emitter<void>();
118
+ readonly onDidOpenFailure: Event<void> = this.onDidOpenFailureEmitter.event;
119
+
120
+ protected readonly onSizeChangedEmitter = new Emitter<{ cols: number; rows: number; }>();
121
+ readonly onSizeChanged: Event<{ cols: number; rows: number; }> = this.onSizeChangedEmitter.event;
122
+
123
+ protected readonly onDataEmitter = new Emitter<string>();
124
+ readonly onData: Event<string> = this.onDataEmitter.event;
125
+
126
+ protected readonly onKeyEmitter = new Emitter<{ key: string, domEvent: KeyboardEvent }>();
127
+ readonly onKey: Event<{ key: string, domEvent: KeyboardEvent }> = this.onKeyEmitter.event;
128
+
129
+ protected readonly onMouseEnterLinkHoverEmitter = new Emitter<MouseEvent>();
130
+ readonly onMouseEnterLinkHover: Event<MouseEvent> = this.onMouseEnterLinkHoverEmitter.event;
131
+
132
+ protected readonly onMouseLeaveLinkHoverEmitter = new Emitter<MouseEvent>();
133
+ readonly onMouseLeaveLinkHover: Event<MouseEvent> = this.onMouseLeaveLinkHoverEmitter.event;
134
+
135
+ protected readonly toDisposeOnConnect = new DisposableCollection();
136
+
137
+ @postConstruct()
138
+ protected init(): void {
139
+ this.setTitle(this.options.title || TerminalWidgetImpl.LABEL);
140
+
141
+ if (this.options.iconClass) {
142
+ this.title.iconClass = this.options.iconClass;
143
+ } else {
144
+ this.title.iconClass = codicon('terminal');
145
+ }
146
+
147
+ if (this.options.kind) {
148
+ this.terminalKind = this.options.kind;
149
+ }
150
+
151
+ if (this.options.destroyTermOnClose === true) {
152
+ this.toDispose.push(Disposable.create(() =>
153
+ this.term.dispose()
154
+ ));
155
+ }
156
+
157
+ this.location = this.options.location || TerminalLocation.Panel;
158
+
159
+ this.title.closable = true;
160
+ this.addClass('terminal-container');
161
+
162
+ this.term = new Terminal({
163
+ cursorBlink: this.preferences['terminal.integrated.cursorBlinking'],
164
+ cursorStyle: this.preferences['terminal.integrated.cursorStyle'] === 'line' ? 'bar' : this.preferences['terminal.integrated.cursorStyle'],
165
+ cursorWidth: this.preferences['terminal.integrated.cursorWidth'],
166
+ fontFamily: this.preferences['terminal.integrated.fontFamily'],
167
+ fontSize: this.preferences['terminal.integrated.fontSize'],
168
+ fontWeight: this.preferences['terminal.integrated.fontWeight'],
169
+ fontWeightBold: this.preferences['terminal.integrated.fontWeightBold'],
170
+ drawBoldTextInBrightColors: this.preferences['terminal.integrated.drawBoldTextInBrightColors'],
171
+ letterSpacing: this.preferences['terminal.integrated.letterSpacing'],
172
+ lineHeight: this.preferences['terminal.integrated.lineHeight'],
173
+ scrollback: this.preferences['terminal.integrated.scrollback'],
174
+ fastScrollSensitivity: this.preferences['terminal.integrated.fastScrollSensitivity'],
175
+ theme: this.themeService.theme
176
+ });
177
+
178
+ this.fitAddon = new FitAddon();
179
+ this.term.loadAddon(this.fitAddon);
180
+
181
+ this.initializeLinkHover();
182
+
183
+ this.toDispose.push(this.preferences.onPreferenceChanged(change => {
184
+ this.updateConfig();
185
+ this.needsResize = true;
186
+ this.update();
187
+ }));
188
+
189
+ this.toDispose.push(this.themeService.onDidChange(() => this.term.options.theme = this.themeService.theme));
190
+ this.attachCustomKeyEventHandler();
191
+ const titleChangeListenerDispose = this.term.onTitleChange((title: string) => {
192
+ if (this.options.useServerTitle) {
193
+ this.title.label = title;
194
+ }
195
+ });
196
+ this.toDispose.push(titleChangeListenerDispose);
197
+
198
+ this.toDispose.push(this.terminalWatcher.onTerminalError(({ terminalId, error, attached }) => {
199
+ if (terminalId === this.terminalId) {
200
+ this.exitStatus = { code: undefined, reason: TerminalExitReason.Process };
201
+ this.logger.error(`The terminal process terminated. Cause: ${error}`);
202
+ if (!attached) {
203
+ this.dispose();
204
+ }
205
+ }
206
+ }));
207
+ this.toDispose.push(this.terminalWatcher.onTerminalExit(({ terminalId, code, reason, attached }) => {
208
+ if (terminalId === this.terminalId) {
209
+ if (reason) {
210
+ this.exitStatus = { code, reason };
211
+ } else {
212
+ this.exitStatus = { code, reason: TerminalExitReason.Process };
213
+ }
214
+ if (!attached) {
215
+ this.dispose();
216
+ }
217
+ }
218
+ }));
219
+ this.toDispose.push(this.toDisposeOnConnect);
220
+ this.toDispose.push(this.shellTerminalServer.onDidCloseConnection(() => {
221
+ const disposable = this.shellTerminalServer.onDidOpenConnection(() => {
222
+ disposable.dispose();
223
+ this.reconnectTerminalProcess();
224
+ });
225
+ this.toDispose.push(disposable);
226
+ }));
227
+ this.toDispose.push(this.onTermDidClose);
228
+ this.toDispose.push(this.onDidOpenEmitter);
229
+ this.toDispose.push(this.onDidOpenFailureEmitter);
230
+ this.toDispose.push(this.onSizeChangedEmitter);
231
+ this.toDispose.push(this.onDataEmitter);
232
+ this.toDispose.push(this.onKeyEmitter);
233
+
234
+ const touchEndListener = (event: TouchEvent) => {
235
+ if (this.node.contains(event.target as Node)) {
236
+ this.lastTouchEnd = event;
237
+ }
238
+ };
239
+ document.addEventListener('touchend', touchEndListener, { passive: true });
240
+ this.onDispose(() => {
241
+ document.removeEventListener('touchend', touchEndListener);
242
+ });
243
+
244
+ const mouseListener = (event: MouseEvent) => {
245
+ this.lastMousePosition = { x: event.x, y: event.y };
246
+ };
247
+ this.node.addEventListener('mousemove', mouseListener);
248
+ this.onDispose(() => {
249
+ this.node.removeEventListener('mousemove', mouseListener);
250
+ });
251
+
252
+ const contextMenuListener = (event: MouseEvent) => {
253
+ event.preventDefault();
254
+ event.stopPropagation();
255
+ this.contextMenuRenderer.render({ menuPath: TerminalMenus.TERMINAL_CONTEXT_MENU, anchor: event });
256
+ };
257
+ this.node.addEventListener('contextmenu', contextMenuListener);
258
+ this.onDispose(() => this.node.removeEventListener('contextmenu', contextMenuListener));
259
+
260
+ this.toDispose.push(this.term.onSelectionChange(() => {
261
+ if (this.copyOnSelection) {
262
+ this.copyOnSelectionHandler.copy(this.term.getSelection());
263
+ }
264
+ }));
265
+
266
+ this.toDispose.push(this.term.onResize(data => {
267
+ this.onSizeChangedEmitter.fire(data);
268
+ }));
269
+
270
+ this.toDispose.push(this.term.onData(data => {
271
+ this.onDataEmitter.fire(data);
272
+ }));
273
+
274
+ this.toDispose.push(this.term.onBinary(data => {
275
+ this.onDataEmitter.fire(data);
276
+ }));
277
+
278
+ this.toDispose.push(this.term.onKey(data => {
279
+ this.onKeyEmitter.fire(data);
280
+ }));
281
+
282
+ for (const contribution of this.terminalContributionProvider.getContributions()) {
283
+ contribution.onCreate(this);
284
+ }
285
+
286
+ this.searchBox = this.terminalSearchBoxFactory(this.term);
287
+ this.toDispose.push(this.searchBox);
288
+ }
289
+
290
+ get kind(): 'user' | string {
291
+ return this.terminalKind;
292
+ }
293
+
294
+ updateConfig(): void {
295
+ this.setCursorBlink(this.preferences.get('terminal.integrated.cursorBlinking'));
296
+ this.setCursorStyle(this.preferences.get('terminal.integrated.cursorStyle'));
297
+ this.setCursorWidth(this.preferences.get('terminal.integrated.cursorWidth'));
298
+ this.term.options.fontFamily = this.preferences.get('terminal.integrated.fontFamily');
299
+ this.term.options.fontSize = this.preferences.get('terminal.integrated.fontSize');
300
+ this.term.options.fontWeight = this.preferences.get('terminal.integrated.fontWeight');
301
+ this.term.options.fontWeightBold = this.preferences.get('terminal.integrated.fontWeightBold');
302
+ this.term.options.drawBoldTextInBrightColors = this.preferences.get('terminal.integrated.drawBoldTextInBrightColors');
303
+ this.term.options.letterSpacing = this.preferences.get('terminal.integrated.letterSpacing');
304
+ this.term.options.lineHeight = this.preferences.get('terminal.integrated.lineHeight');
305
+ this.term.options.scrollback = this.preferences.get('terminal.integrated.scrollback');
306
+ this.term.options.fastScrollSensitivity = this.preferences.get('terminal.integrated.fastScrollSensitivity');
307
+ }
308
+
309
+ private setCursorBlink(blink: boolean): void {
310
+ if (this.term.options.cursorBlink !== blink) {
311
+ this.term.options.cursorBlink = blink;
312
+ this.term.refresh(0, this.term.rows - 1);
313
+ }
314
+ }
315
+
316
+ private setCursorStyle(style: 'block' | 'underline' | 'bar' | 'line'): void {
317
+ if (this.term.options.cursorStyle !== style) {
318
+ this.term.options.cursorStyle = (style === 'line') ? 'bar' : style;
319
+ }
320
+ }
321
+
322
+ private setCursorWidth(width: number): void {
323
+ if (this.term.options.cursorWidth !== width) {
324
+ this.term.options.cursorWidth = width;
325
+ }
326
+ }
327
+
328
+ protected initializeLinkHover(): void {
329
+ this.linkHover = document.createElement('div');
330
+ this.linkHover.style.position = 'fixed';
331
+ this.linkHover.style.color = 'var(--theia-editorHoverWidget-foreground)';
332
+ this.linkHover.style.backgroundColor = 'var(--theia-editorHoverWidget-background)';
333
+ this.linkHover.style.borderColor = 'var(--theia-editorHoverWidget-border)';
334
+ this.linkHover.style.borderWidth = '0.5px';
335
+ this.linkHover.style.borderStyle = 'solid';
336
+ this.linkHover.style.padding = '5px';
337
+ // Above the xterm.js canvas layers:
338
+ // https://github.com/xtermjs/xterm.js/blob/ff790236c1b205469f17a21246141f512d844295/src/renderer/Renderer.ts#L41-L46
339
+ this.linkHover.style.zIndex = '10';
340
+ // Initially invisible:
341
+ this.linkHover.style.display = 'none';
342
+
343
+ this.linkHoverButton = document.createElement('a');
344
+ this.linkHoverButton.textContent = this.linkHoverMessage();
345
+ this.linkHoverButton.style.cursor = 'pointer';
346
+ this.linkHover.appendChild(this.linkHoverButton);
347
+
348
+ const cmdCtrl = isOSX ? 'cmd' : 'ctrl';
349
+ const cmdHint = document.createTextNode(` (${nls.localizeByDefault(`${cmdCtrl} + click`)})`);
350
+ this.linkHover.appendChild(cmdHint);
351
+
352
+ const onMouseEnter = (mouseEvent: MouseEvent) => this.onMouseEnterLinkHoverEmitter.fire(mouseEvent);
353
+ this.linkHover.addEventListener('mouseenter', onMouseEnter);
354
+ this.toDispose.push(Disposable.create(() => this.linkHover.removeEventListener('mouseenter', onMouseEnter)));
355
+
356
+ const onMouseLeave = (mouseEvent: MouseEvent) => this.onMouseLeaveLinkHoverEmitter.fire(mouseEvent);
357
+ this.linkHover.addEventListener('mouseleave', onMouseLeave);
358
+ this.toDispose.push(Disposable.create(() => this.linkHover.removeEventListener('mouseleave', onMouseLeave)));
359
+
360
+ this.node.appendChild(this.linkHover);
361
+ }
362
+
363
+ showLinkHover(invokeAction: (event: MouseEvent) => void, x: number, y: number, message?: string): void {
364
+ const mouseY = this.lastMousePosition?.y ?? y;
365
+ const mouseX = this.lastMousePosition?.x ?? x;
366
+ this.linkHoverButton.textContent = this.linkHoverMessage(message);
367
+ this.linkHoverButton.onclick = (mouseEvent: MouseEvent) => invokeAction(mouseEvent);
368
+ this.linkHover.style.display = 'inline';
369
+ this.linkHover.style.top = `${mouseY - 30}px`;
370
+ this.linkHover.style.left = `${mouseX - 60}px`;
371
+ }
372
+
373
+ protected linkHoverMessage(message?: string): string {
374
+ return message ?? nls.localizeByDefault('Follow link');
375
+ }
376
+
377
+ hideLinkHover(): void {
378
+ this.linkHover.style.display = 'none';
379
+ // eslint-disable-next-line no-null/no-null
380
+ this.linkHoverButton.onclick = null;
381
+ }
382
+
383
+ getTerminal(): Terminal {
384
+ return this.term;
385
+ }
386
+
387
+ getSearchBox(): TerminalSearchWidget {
388
+ return this.searchBox;
389
+ }
390
+
391
+ protected override onCloseRequest(msg: Message): void {
392
+ this.exitStatus = { code: undefined, reason: TerminalExitReason.User };
393
+ super.onCloseRequest(msg);
394
+ }
395
+
396
+ get dimensions(): TerminalDimensions {
397
+ return {
398
+ cols: this.term.cols,
399
+ rows: this.term.rows,
400
+ };
401
+ }
402
+
403
+ get cwd(): Promise<URI> {
404
+ if (!IBaseTerminalServer.validateId(this.terminalId)) {
405
+ return Promise.reject(new Error('terminal is not started'));
406
+ }
407
+ if (this.terminalService.getById(this.id)) {
408
+ return this.shellTerminalServer.getCwdURI(this.terminalId)
409
+ .then(cwdUrl => {
410
+ this.lastCwd = new URI(cwdUrl);
411
+ return this.lastCwd;
412
+ }).catch(() => this.lastCwd);
413
+ }
414
+ return Promise.resolve(new URI());
415
+ }
416
+
417
+ get processId(): Promise<number> {
418
+ if (!IBaseTerminalServer.validateId(this.terminalId)) {
419
+ return Promise.reject(new Error('terminal is not started'));
420
+ }
421
+ return this.shellTerminalServer.getProcessId(this.terminalId);
422
+ }
423
+
424
+ get processInfo(): Promise<TerminalProcessInfo> {
425
+ if (!IBaseTerminalServer.validateId(this.terminalId)) {
426
+ return Promise.reject(new Error('terminal is not started'));
427
+ }
428
+ return this.shellTerminalServer.getProcessInfo(this.terminalId);
429
+ }
430
+
431
+ get envVarCollectionDescriptionsByExtension(): Promise<Map<string, (string | MarkdownString | undefined)[]>> {
432
+ if (!IBaseTerminalServer.validateId(this.terminalId)) {
433
+ return Promise.reject(new Error('terminal is not started'));
434
+ }
435
+ return this.shellTerminalServer.getEnvVarCollectionDescriptionsByExtension(this.terminalId);
436
+ }
437
+
438
+ get terminalId(): number {
439
+ return this._terminalId;
440
+ }
441
+
442
+ get lastTouchEndEvent(): TouchEvent | undefined {
443
+ return this.lastTouchEnd;
444
+ }
445
+
446
+ get hiddenFromUser(): boolean {
447
+ if (this.shown) {
448
+ return false;
449
+ }
450
+ return this.options.hideFromUser ?? false;
451
+ }
452
+
453
+ get transient(): boolean {
454
+ // The terminal is transient if session persistence is disabled or it's explicitly marked as transient
455
+ return !this.preferences['terminal.integrated.enablePersistentSessions'] || !!this.options.isTransient;
456
+ }
457
+
458
+ onDispose(onDispose: () => void): void {
459
+ this.toDispose.push(Disposable.create(onDispose));
460
+ }
461
+
462
+ clearOutput(): void {
463
+ this.term.clear();
464
+ }
465
+
466
+ selectAll(): void {
467
+ this.term.selectAll();
468
+ }
469
+
470
+ async hasChildProcesses(): Promise<boolean> {
471
+ return this.shellTerminalServer.hasChildProcesses(await this.processId);
472
+ }
473
+
474
+ storeState(): object {
475
+ this.closeOnDispose = false;
476
+ if (this.transient || this.options.isPseudoTerminal) {
477
+ return {};
478
+ }
479
+ return { terminalId: this.terminalId, titleLabel: this.title.label };
480
+ }
481
+
482
+ restoreState(oldState: object): void {
483
+ // transient terminals and pseudo terminals are not restored
484
+ if (this.transient || this.options.isPseudoTerminal) {
485
+ this.dispose();
486
+ return;
487
+ }
488
+ if (this.restored === false) {
489
+ const state = oldState as { terminalId: number, titleLabel: string };
490
+ /* This is a workaround to issue #879 */
491
+ this.restored = true;
492
+ this.title.label = state.titleLabel;
493
+ this.start(state.terminalId);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Create a new shell terminal in the back-end and attach it to a
499
+ * new terminal widget.
500
+ * If id is provided attach to the terminal for this id.
501
+ */
502
+ async start(id?: number): Promise<number> {
503
+ this._terminalId = typeof id !== 'number' ? await this.createTerminal() : await this.attachTerminal(id);
504
+ this.resizeTerminalProcess();
505
+ this.connectTerminalProcess();
506
+ if (IBaseTerminalServer.validateId(this.terminalId)) {
507
+ this.onDidOpenEmitter.fire(undefined);
508
+ await this.shellTerminalServer.onAttachAttempted(this._terminalId);
509
+ return this.terminalId;
510
+ }
511
+ this.onDidOpenFailureEmitter.fire(undefined);
512
+ throw new Error('Failed to start terminal' + (id ? ` for id: ${id}.` : '.'));
513
+ }
514
+
515
+ protected async attachTerminal(id: number): Promise<number> {
516
+ const terminalId = await this.shellTerminalServer.attach(id);
517
+ if (IBaseTerminalServer.validateId(terminalId)) {
518
+ // reset exit status if a new terminal process is attached
519
+ this.exitStatus = undefined;
520
+ return terminalId;
521
+ }
522
+ this.logger.warn(`Failed attaching to terminal id ${id}, the terminal is most likely gone. Starting up a new terminal instead.`);
523
+ if (this.kind === 'user') {
524
+ return this.createTerminal();
525
+ } else {
526
+ return -1;
527
+ }
528
+ }
529
+
530
+ protected async createTerminal(): Promise<number> {
531
+ let rootURI = this.options.cwd?.toString();
532
+ if (!rootURI) {
533
+ const root = (await this.workspaceService.roots)[0];
534
+ rootURI = root?.resource?.toString();
535
+ }
536
+ const { cols, rows } = this.term;
537
+
538
+ const terminalId = await this.shellTerminalServer.create({
539
+ shell: this.options.shellPath || this.shellPreferences.shell[OS.backend.type()],
540
+ args: this.options.shellArgs || this.shellPreferences.shellArgs[OS.backend.type()],
541
+ env: this.options.env,
542
+ strictEnv: this.options.strictEnv,
543
+ isPseudo: this.options.isPseudoTerminal,
544
+ rootURI,
545
+ cols,
546
+ rows
547
+ });
548
+ if (IBaseTerminalServer.validateId(terminalId)) {
549
+ return terminalId;
550
+ }
551
+ throw new Error('Error creating terminal widget, see the backend error log for more information.');
552
+ }
553
+
554
+ override processMessage(msg: Message): void {
555
+ super.processMessage(msg);
556
+ switch (msg.type) {
557
+ case 'fit-request':
558
+ this.onFitRequest(msg);
559
+ break;
560
+ default:
561
+ break;
562
+ }
563
+ }
564
+ protected override onFitRequest(msg: Message): void {
565
+ super.onFitRequest(msg);
566
+ MessageLoop.sendMessage(this, Widget.ResizeMessage.UnknownSize);
567
+ }
568
+ protected override onActivateRequest(msg: Message): void {
569
+ super.onActivateRequest(msg);
570
+ this.term.focus();
571
+ }
572
+ protected override onAfterShow(msg: Message): void {
573
+ super.onAfterShow(msg);
574
+ this.update();
575
+ this.shown = true;
576
+ }
577
+ protected override onAfterAttach(msg: Message): void {
578
+ Widget.attach(this.searchBox, this.node);
579
+ super.onAfterAttach(msg);
580
+ this.update();
581
+ }
582
+ protected override onBeforeDetach(msg: Message): void {
583
+ Widget.detach(this.searchBox);
584
+ super.onBeforeDetach(msg);
585
+ }
586
+ protected override onResize(msg: Widget.ResizeMessage): void {
587
+ super.onResize(msg);
588
+ this.needsResize = true;
589
+ this.update();
590
+ }
591
+
592
+ protected needsResize = true;
593
+ protected override onUpdateRequest(msg: Message): void {
594
+ super.onUpdateRequest(msg);
595
+ if (!this.isVisible || !this.isAttached) {
596
+ return;
597
+ }
598
+
599
+ this.open();
600
+
601
+ if (this.needsResize) {
602
+ this.resizeTerminal();
603
+ this.needsResize = false;
604
+ }
605
+ }
606
+
607
+ // Device status code emitted by Xterm.js
608
+ // Check: https://github.com/xtermjs/xterm.js/blob/release/3.14/src/InputHandler.ts#L1055-L1082
609
+ protected readonly deviceStatusCodes = new Set(['\u001B[>0;276;0c', '\u001B[>85;95;0c', '\u001B[>83;40003;0c', '\u001B[?1;2c', '\u001B[?6c']);
610
+
611
+ protected connectTerminalProcess(): void {
612
+ if (typeof this.terminalId !== 'number') {
613
+ return;
614
+ }
615
+ if (this.options.isPseudoTerminal) {
616
+ return;
617
+ }
618
+ this.toDisposeOnConnect.dispose();
619
+ this.toDispose.push(this.toDisposeOnConnect);
620
+ const waitForConnection = this.waitForConnection = new Deferred<Channel>();
621
+ this.connectionProvider.listen(
622
+ `${terminalsPath}/${this.terminalId}`,
623
+ (path, connection) => {
624
+ connection.onMessage(e => {
625
+ this.write(e().readString());
626
+ });
627
+
628
+ // Excludes the device status code emitted by Xterm.js
629
+ const sendData = (data?: string) => {
630
+ if (data && !this.deviceStatusCodes.has(data) && !this.disableEnterWhenAttachCloseListener()) {
631
+ connection.getWriteBuffer().writeString(data).commit();
632
+ }
633
+ };
634
+
635
+ const disposable = new DisposableCollection();
636
+ disposable.push(this.term.onData(sendData));
637
+ disposable.push(this.term.onBinary(sendData));
638
+
639
+ connection.onClose(() => disposable.dispose());
640
+
641
+ if (waitForConnection) {
642
+ waitForConnection.resolve(connection);
643
+ }
644
+ }, false);
645
+ }
646
+ protected async reconnectTerminalProcess(): Promise<void> {
647
+ if (this.options.isPseudoTerminal) {
648
+ return;
649
+ }
650
+ if (typeof this.terminalId === 'number') {
651
+ await this.start(this.terminalId);
652
+ }
653
+ }
654
+
655
+ protected termOpened = false;
656
+ protected initialData = '';
657
+ protected open(): void {
658
+ if (this.termOpened) {
659
+ return;
660
+ }
661
+ this.term.open(this.node);
662
+
663
+ interface ViewportType {
664
+ register(d: Disposable): void;
665
+ _refreshAnimationFrame: number | null;
666
+ _coreBrowserService: {
667
+ window: Window;
668
+ }
669
+ }
670
+
671
+ // Workaround for https://github.com/xtermjs/xterm.js/issues/4775. Can be removed for releases > 5.3.0
672
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
673
+ const viewPort: ViewportType = (this.term as any)._core.viewport;
674
+ viewPort.register(Disposable.create(() => {
675
+ if (typeof viewPort._refreshAnimationFrame === 'number') {
676
+ viewPort._coreBrowserService.window.cancelAnimationFrame(viewPort._refreshAnimationFrame);
677
+ }
678
+ }));
679
+
680
+ if (isFirefox) {
681
+ // monkey patching intersection observer handling for secondary window support
682
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
683
+ const renderService: any = (this.term as any)._core._renderService;
684
+
685
+ const originalFunc: (entry: IntersectionObserverEntry) => void = renderService._handleIntersectionChange.bind(renderService);
686
+ const replacement = function (entry: IntersectionObserverEntry): void {
687
+ if (entry.target.ownerDocument !== document) {
688
+ // in Firefox, the intersection observer always reports the widget as non-intersecting if the dom element
689
+ // is in a different document from when the IntersectionObserver started observing. Since we know
690
+ // that the widget is always "visible" when in a secondary window, so we refresh the rows ourselves
691
+ const patchedEvent: IntersectionObserverEntry = {
692
+ ...entry,
693
+ isIntersecting: true,
694
+ };
695
+ originalFunc(patchedEvent);
696
+ } else {
697
+ originalFunc(entry);
698
+ }
699
+ };
700
+
701
+ renderService._handleIntersectionChange = replacement.bind(renderService);
702
+ }
703
+
704
+ if (this.initialData) {
705
+ this.term.write(this.initialData);
706
+ }
707
+ this.termOpened = true;
708
+ this.initialData = '';
709
+ }
710
+
711
+ write(data: string): void {
712
+ if (this.termOpened) {
713
+ this.term.write(data);
714
+ } else {
715
+ this.initialData += data;
716
+ }
717
+ }
718
+
719
+ resize(cols: number, rows: number): void {
720
+ this.term.resize(cols, rows);
721
+ }
722
+
723
+ sendText(text: string): void {
724
+ if (this.waitForConnection) {
725
+ this.waitForConnection.promise.then(connection =>
726
+ connection.getWriteBuffer().writeString(text).commit()
727
+ );
728
+ }
729
+ }
730
+
731
+ async executeCommand(commandOptions: CommandLineOptions): Promise<void> {
732
+ this.sendText(this.shellCommandBuilder.buildCommand(await this.processInfo, commandOptions) + OS.backend.EOL);
733
+ }
734
+
735
+ scrollLineUp(): void {
736
+ this.term.scrollLines(-1);
737
+ }
738
+
739
+ scrollLineDown(): void {
740
+ this.term.scrollLines(1);
741
+ }
742
+
743
+ scrollToTop(): void {
744
+ this.term.scrollToTop();
745
+ }
746
+
747
+ scrollToBottom(): void {
748
+ this.term.scrollToBottom();
749
+ }
750
+
751
+ scrollPageUp(): void {
752
+ this.term.scrollPages(-1);
753
+ }
754
+
755
+ scrollPageDown(): void {
756
+ this.term.scrollPages(1);
757
+ }
758
+
759
+ resetTerminal(): void {
760
+ this.term.reset();
761
+ }
762
+
763
+ writeLine(text: string): void {
764
+ this.term.writeln(text);
765
+ }
766
+
767
+ get onTerminalDidClose(): Event<TerminalWidget> {
768
+ return this.onTermDidClose.event;
769
+ }
770
+
771
+ override dispose(): void {
772
+ if (this.closeOnDispose === true && typeof this.terminalId === 'number' && !this.exitStatus) {
773
+ // Close the backend terminal only when explicitly closing the terminal
774
+ // a refresh for example won't close it.
775
+ this.shellTerminalServer.close(this.terminalId);
776
+ // Exit status is set when terminal is closed by user or by process, so most likely an extension closed it.
777
+ this.exitStatus = { code: undefined, reason: TerminalExitReason.Extension };
778
+ }
779
+ if (this.exitStatus) {
780
+ this.onTermDidClose.fire(this);
781
+ }
782
+ if (this.enhancedPreviewNode) {
783
+ // don't use preview node anymore. rendered markdown will be disposed on super call
784
+ this.enhancedPreviewNode = undefined;
785
+ }
786
+ super.dispose();
787
+ }
788
+
789
+ protected resizeTerminal = debounce(() => this.doResizeTerminal(), 50);
790
+
791
+ protected doResizeTerminal(): void {
792
+ if (this.isDisposed) {
793
+ return;
794
+ }
795
+ const geo = this.fitAddon.proposeDimensions();
796
+ if (geo) {
797
+ const cols = geo.cols;
798
+ const rows = geo.rows - 1; // subtract one row for margin
799
+ this.term.resize(cols, rows);
800
+
801
+ this.resizeTerminalProcess();
802
+ }
803
+ }
804
+
805
+ protected resizeTerminalProcess(): void {
806
+ if (this.options.isPseudoTerminal) {
807
+ return;
808
+ }
809
+ if (!IBaseTerminalServer.validateId(this.terminalId)
810
+ || this.exitStatus
811
+ || !this.terminalService.getById(this.id)
812
+ ) {
813
+ return;
814
+ }
815
+ const { cols, rows } = this.term;
816
+ this.shellTerminalServer.resize(this.terminalId, cols, rows);
817
+ }
818
+
819
+ protected get enableCopy(): boolean {
820
+ return this.preferences['terminal.enableCopy'];
821
+ }
822
+
823
+ protected get enablePaste(): boolean {
824
+ return this.preferences['terminal.enablePaste'];
825
+ }
826
+
827
+ protected get shellPreferences(): IShellTerminalPreferences {
828
+ return {
829
+ shell: {
830
+ Windows: this.preferences['terminal.integrated.shell.windows'] ?? undefined,
831
+ Linux: this.preferences['terminal.integrated.shell.linux'] ?? undefined,
832
+ OSX: this.preferences['terminal.integrated.shell.osx'] ?? undefined,
833
+ },
834
+ shellArgs: {
835
+ Windows: this.preferences['terminal.integrated.shellArgs.windows'],
836
+ Linux: this.preferences['terminal.integrated.shellArgs.linux'],
837
+ OSX: this.preferences['terminal.integrated.shellArgs.osx'],
838
+ }
839
+ };
840
+ }
841
+
842
+ protected customKeyHandler(event: KeyboardEvent): boolean {
843
+ const keyBindings = KeyCode.createKeyCode(event).toString();
844
+ const ctrlCmdCopy = (isOSX && keyBindings === 'meta+c') || (!isOSX && keyBindings === 'ctrl+c');
845
+ const ctrlCmdPaste = (isOSX && keyBindings === 'meta+v') || (!isOSX && keyBindings === 'ctrl+v');
846
+ if (ctrlCmdCopy && this.enableCopy && this.term.hasSelection()) {
847
+ return false;
848
+ }
849
+ if (ctrlCmdPaste && this.enablePaste) {
850
+ return false;
851
+ }
852
+ return true;
853
+ }
854
+
855
+ protected get copyOnSelection(): boolean {
856
+ return this.preferences['terminal.integrated.copyOnSelection'];
857
+ }
858
+
859
+ protected attachCustomKeyEventHandler(): void {
860
+ this.term.attachCustomKeyEventHandler(e => this.customKeyHandler(e));
861
+ }
862
+
863
+ setTitle(title: string): void {
864
+ this.title.caption = title;
865
+ this.title.label = title;
866
+ }
867
+
868
+ waitOnExit(waitOnExit?: boolean | string): void {
869
+ if (waitOnExit) {
870
+ if (typeof waitOnExit === 'string') {
871
+ let message = waitOnExit;
872
+ // Bold the message and add an extra new line to make it stand out from the rest of the output
873
+ message = `\r\n\x1b[1m${message}\x1b[0m`;
874
+ this.write(message);
875
+ }
876
+ this.attachPressEnterKeyToCloseListener(this.term);
877
+ return;
878
+ }
879
+ this.dispose();
880
+ }
881
+
882
+ private attachPressEnterKeyToCloseListener(term: Terminal): void {
883
+ if (term.textarea) {
884
+ this.isAttachedCloseListener = true;
885
+ this.addKeyListener(term.textarea, Key.ENTER, (event: KeyboardEvent) => {
886
+ this.dispose();
887
+ this.isAttachedCloseListener = false;
888
+ });
889
+ }
890
+ }
891
+
892
+ private disableEnterWhenAttachCloseListener(): boolean {
893
+ return this.isAttachedCloseListener;
894
+ }
895
+
896
+ getEnhancedPreviewNode(): Node | undefined {
897
+ if (this.enhancedPreviewNode) {
898
+ return this.enhancedPreviewNode;
899
+ }
900
+
901
+ this.enhancedPreviewNode = document.createElement('div');
902
+
903
+ Promise.all([this.envVarCollectionDescriptionsByExtension, this.processId, this.processInfo])
904
+ .then((values: [Map<string, (string | MarkdownString | undefined)[]>, number, TerminalProcessInfo]) => {
905
+ const extensions = values[0];
906
+ const processId = values[1];
907
+ const processInfo = values[2];
908
+
909
+ const markdown = new MarkdownStringImpl();
910
+ markdown.appendMarkdown('Process ID: ' + processId + '\\\n');
911
+ markdown.appendMarkdown('Command line: ' +
912
+ processInfo.executable +
913
+ ' ' +
914
+ processInfo.arguments.join(' ') +
915
+ '\n\n---\n\n');
916
+ markdown.appendMarkdown('The following extensions have contributed to this terminal\'s environment:\n');
917
+ extensions.forEach((arr, key) => {
918
+ arr.forEach(value => {
919
+ if (value === undefined) {
920
+ markdown.appendMarkdown('* ' + key + '\n');
921
+ } else if (typeof value === 'string') {
922
+ markdown.appendMarkdown('* ' + key + ': ' + value + '\n');
923
+ } else {
924
+ markdown.appendMarkdown('* ' + key + ': ' + value.value + '\n');
925
+ }
926
+ });
927
+ });
928
+
929
+ const enhancedPreviewNode = this.enhancedPreviewNode;
930
+ if (!this.isDisposed && enhancedPreviewNode) {
931
+ const result = this.markdownRenderer.render(markdown);
932
+ this.toDispose.push(result);
933
+ enhancedPreviewNode.appendChild(result.element);
934
+ }
935
+ });
936
+
937
+ return this.enhancedPreviewNode;
938
+ }
939
+ }