@theia/terminal 1.70.0-next.71 → 1.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/base/terminal-widget.d.ts +40 -1
- package/lib/browser/base/terminal-widget.d.ts.map +1 -1
- package/lib/browser/base/terminal-widget.js.map +1 -1
- package/lib/browser/terminal-command-history.d.ts +27 -0
- package/lib/browser/terminal-command-history.d.ts.map +1 -0
- package/lib/browser/terminal-command-history.js +76 -0
- package/lib/browser/terminal-command-history.js.map +1 -0
- package/lib/browser/terminal-frontend-contribution.d.ts +1 -0
- package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/terminal-frontend-contribution.js +20 -0
- package/lib/browser/terminal-frontend-contribution.js.map +1 -1
- package/lib/browser/terminal-frontend-module.d.ts.map +1 -1
- package/lib/browser/terminal-frontend-module.js +3 -0
- package/lib/browser/terminal-frontend-module.js.map +1 -1
- package/lib/browser/terminal-widget-impl.d.ts +31 -2
- package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
- package/lib/browser/terminal-widget-impl.js +147 -2
- package/lib/browser/terminal-widget-impl.js.map +1 -1
- package/lib/common/shell-terminal-protocol.d.ts +6 -0
- package/lib/common/shell-terminal-protocol.d.ts.map +1 -1
- package/lib/common/shell-terminal-protocol.js.map +1 -1
- package/lib/common/terminal-preferences.d.ts +2 -0
- package/lib/common/terminal-preferences.d.ts.map +1 -1
- package/lib/common/terminal-preferences.js +12 -0
- package/lib/common/terminal-preferences.js.map +1 -1
- package/lib/node/shell-integration-injector.d.ts +15 -0
- package/lib/node/shell-integration-injector.d.ts.map +1 -0
- package/lib/node/shell-integration-injector.js +97 -0
- package/lib/node/shell-integration-injector.js.map +1 -0
- package/lib/node/shell-process.d.ts +6 -0
- package/lib/node/shell-process.d.ts.map +1 -1
- package/lib/node/shell-process.js.map +1 -1
- package/lib/node/terminal-backend-module.d.ts.map +1 -1
- package/lib/node/terminal-backend-module.js +7 -1
- package/lib/node/terminal-backend-module.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/base/terminal-widget.ts +52 -1
- package/src/browser/style/terminal.css +7 -0
- package/src/browser/terminal-command-history.ts +83 -0
- package/src/browser/terminal-frontend-contribution.ts +20 -0
- package/src/browser/terminal-frontend-module.ts +6 -0
- package/src/browser/terminal-widget-impl.ts +171 -4
- package/src/common/shell-terminal-protocol.ts +6 -0
- package/src/common/terminal-preferences.ts +14 -0
- package/src/node/shell-integration-injector.ts +94 -0
- package/src/node/shell-integrations/bash/bash-integration.bash +86 -0
- package/src/node/shell-integrations/bash/command-block-support.bash +195 -0
- package/src/node/shell-integrations/zsh/command-block-support.zsh +103 -0
- package/src/node/shell-integrations/zsh/zdotdir/.zlogin +45 -0
- package/src/node/shell-integrations/zsh/zdotdir/.zprofile +27 -0
- package/src/node/shell-integrations/zsh/zdotdir/.zshenv +56 -0
- package/src/node/shell-integrations/zsh/zdotdir/.zshrc +46 -0
- package/src/node/shell-integrations/zsh/zdotdir/source-original.zsh +61 -0
- package/src/node/shell-integrations/zsh/zsh-integration.zsh +28 -0
- package/src/node/shell-process.ts +6 -0
- package/src/node/terminal-backend-module.ts +9 -1
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import { Terminal } from 'xterm';
|
|
17
|
+
import { Terminal, IMarker } from 'xterm';
|
|
18
18
|
import { FitAddon } from 'xterm-addon-fit';
|
|
19
19
|
import { WebglAddon } from 'xterm-addon-webgl';
|
|
20
20
|
import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
|
|
@@ -32,7 +32,9 @@ import { TerminalWatcher } from '../common/terminal-watcher';
|
|
|
32
32
|
import {
|
|
33
33
|
TerminalWidgetOptions, TerminalWidget, TerminalDimensions, TerminalExitStatus, TerminalLocationOptions,
|
|
34
34
|
TerminalLocation,
|
|
35
|
-
TerminalBuffer
|
|
35
|
+
TerminalBuffer,
|
|
36
|
+
TerminalBlock,
|
|
37
|
+
TerminalCommandHistoryState
|
|
36
38
|
} from './base/terminal-widget';
|
|
37
39
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
38
40
|
import { TerminalPreferences } from '../common/terminal-preferences';
|
|
@@ -52,6 +54,7 @@ import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/brows
|
|
|
52
54
|
import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
|
|
53
55
|
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
|
|
54
56
|
import { cleanTerminalTitle, guessShellTypeFromExecutable } from '../common/shell-type';
|
|
57
|
+
import { TerminalCommandHistoryStateFactory } from './terminal-command-history';
|
|
55
58
|
|
|
56
59
|
export const TERMINAL_WIDGET_FACTORY_ID = 'terminal';
|
|
57
60
|
|
|
@@ -143,6 +146,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
143
146
|
@inject(ShellCommandBuilder) protected readonly shellCommandBuilder: ShellCommandBuilder;
|
|
144
147
|
@inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
|
|
145
148
|
@inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory;
|
|
149
|
+
@inject(TerminalCommandHistoryStateFactory) protected readonly commandHistoryStateFactory: TerminalCommandHistoryStateFactory;
|
|
146
150
|
|
|
147
151
|
protected _markdownRenderer: MarkdownRenderer | undefined;
|
|
148
152
|
protected get markdownRenderer(): MarkdownRenderer {
|
|
@@ -179,6 +183,12 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
179
183
|
|
|
180
184
|
protected readonly toDisposeOnConnect = new DisposableCollection();
|
|
181
185
|
|
|
186
|
+
protected readonly commandSeparatorDecorations = new DisposableCollection();
|
|
187
|
+
|
|
188
|
+
protected readonly toDisposeOnCommandHistory = new DisposableCollection();
|
|
189
|
+
protected outputStartMarker: IMarker | undefined;
|
|
190
|
+
protected promptStartMarker: IMarker | undefined;
|
|
191
|
+
|
|
182
192
|
private _buffer: TerminalBuffer;
|
|
183
193
|
override get buffer(): TerminalBuffer {
|
|
184
194
|
return this._buffer;
|
|
@@ -186,6 +196,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
186
196
|
|
|
187
197
|
private _currentTerminalOutput: string[];
|
|
188
198
|
|
|
199
|
+
private _commandHistoryState?: TerminalCommandHistoryState;
|
|
200
|
+
override get commandHistoryState(): TerminalCommandHistoryState | undefined {
|
|
201
|
+
return this._commandHistoryState;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
protected enableCommandSeparator: boolean;
|
|
205
|
+
|
|
189
206
|
@postConstruct()
|
|
190
207
|
protected init(): void {
|
|
191
208
|
this.id = this._terminalDOMId;
|
|
@@ -222,7 +239,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
222
239
|
lineHeight: this.preferences['terminal.integrated.lineHeight'],
|
|
223
240
|
scrollback: this.preferences['terminal.integrated.scrollback'],
|
|
224
241
|
fastScrollSensitivity: this.preferences['terminal.integrated.fastScrollSensitivity'],
|
|
225
|
-
theme: this.themeService.theme
|
|
242
|
+
theme: this.themeService.theme,
|
|
243
|
+
// Enables proposed API to allow parsing of OSC 133 sequences for command tracking.
|
|
244
|
+
allowProposedApi: this.preferences['terminal.integrated.enableCommandHistory']
|
|
226
245
|
});
|
|
227
246
|
this._buffer = new TerminalBufferImpl(this.term);
|
|
228
247
|
this._currentTerminalOutput = [];
|
|
@@ -237,9 +256,14 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
237
256
|
|
|
238
257
|
this.toDispose.push(this.preferences.onPreferenceChanged(change => {
|
|
239
258
|
this.updateConfig();
|
|
259
|
+
if (change.preferenceName === 'terminal.integrated.enableCommandHistory') {
|
|
260
|
+
this.updateCommandHistoryHandlers();
|
|
261
|
+
}
|
|
240
262
|
this.needsResize = true;
|
|
241
263
|
this.update();
|
|
242
264
|
}));
|
|
265
|
+
this.updateCommandHistoryConfig();
|
|
266
|
+
this.updateCommandHistoryHandlers();
|
|
243
267
|
|
|
244
268
|
this.toDispose.push(this.themeService.onDidChange(() => {
|
|
245
269
|
this.term.options.theme = this.themeService.theme;
|
|
@@ -282,12 +306,19 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
282
306
|
} else {
|
|
283
307
|
this.exitStatus = { code, reason: TerminalExitReason.Process };
|
|
284
308
|
}
|
|
309
|
+
// Ensure any in-progress command block is closed even if the process exits
|
|
310
|
+
// before its OSC prompt_started bytes are flushed from the ring buffer.
|
|
311
|
+
if (this._commandHistoryState?.currentCommand) {
|
|
312
|
+
this.finishCurrentCommand();
|
|
313
|
+
}
|
|
285
314
|
if (!attached) {
|
|
286
315
|
this.dispose();
|
|
287
316
|
}
|
|
288
317
|
}
|
|
289
318
|
}));
|
|
290
319
|
this.toDispose.push(this.toDisposeOnConnect);
|
|
320
|
+
this.toDispose.push(this.commandSeparatorDecorations);
|
|
321
|
+
this.toDispose.push(this.toDisposeOnCommandHistory);
|
|
291
322
|
this.toDispose.push(this.shellTerminalServer.onDidCloseConnection(() => {
|
|
292
323
|
const disposable = this.shellTerminalServer.onDidOpenConnection(() => {
|
|
293
324
|
disposable.dispose();
|
|
@@ -384,6 +415,135 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
384
415
|
this.term.options.lineHeight = this.preferences.get('terminal.integrated.lineHeight');
|
|
385
416
|
this.term.options.scrollback = this.preferences.get('terminal.integrated.scrollback');
|
|
386
417
|
this.term.options.fastScrollSensitivity = this.preferences.get('terminal.integrated.fastScrollSensitivity');
|
|
418
|
+
this.updateCommandHistoryConfig();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
protected updateCommandHistoryConfig(): void {
|
|
422
|
+
const enabled = this.preferences.get('terminal.integrated.enableCommandHistory', false);
|
|
423
|
+
this.term.options.allowProposedApi = enabled;
|
|
424
|
+
this.enableCommandSeparator = enabled
|
|
425
|
+
? this.preferences.get('terminal.integrated.enableCommandSeparator', false)
|
|
426
|
+
: false;
|
|
427
|
+
|
|
428
|
+
if (enabled && !this._commandHistoryState) {
|
|
429
|
+
this._commandHistoryState = this.commandHistoryStateFactory();
|
|
430
|
+
this.toDispose.push(this._commandHistoryState);
|
|
431
|
+
} else if (!enabled && this._commandHistoryState) {
|
|
432
|
+
this._commandHistoryState.dispose();
|
|
433
|
+
this._commandHistoryState = undefined;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Registers or deregisters command history handlers based on the current preference state.
|
|
439
|
+
*
|
|
440
|
+
* Manages the OSC 133 handler that tracks command lifecycle events. OSC 133 is an iTerm2
|
|
441
|
+
* escape-sequence family marking events such as command start and prompt display
|
|
442
|
+
* (see https://iterm2.com/documentation-escape-codes.html). We use a customized subset:
|
|
443
|
+
*
|
|
444
|
+
* - prompt_started: emitted when the prompt is shown (command output ends)
|
|
445
|
+
* - command_started;<hex-encoded-command>: emitted when a command begins
|
|
446
|
+
*
|
|
447
|
+
* Command output is read directly from xterm's buffer using markers to track line positions,
|
|
448
|
+
* avoiding the need to intercept and sanitize raw terminal data.
|
|
449
|
+
*/
|
|
450
|
+
protected updateCommandHistoryHandlers(): void {
|
|
451
|
+
this.toDisposeOnCommandHistory.dispose();
|
|
452
|
+
this.resetCommandOutputMarker();
|
|
453
|
+
this.resetCommandMarker();
|
|
454
|
+
if (!this._commandHistoryState) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
this.toDisposeOnCommandHistory.push(
|
|
458
|
+
this.term.parser.registerOscHandler(133, (oscPayload: string) => {
|
|
459
|
+
if (!this._commandHistoryState) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
if (oscPayload === 'prompt_started') {
|
|
463
|
+
if (this._commandHistoryState.currentCommand) {
|
|
464
|
+
this.finishCurrentCommand();
|
|
465
|
+
}
|
|
466
|
+
this.promptStartMarker?.dispose();
|
|
467
|
+
this.promptStartMarker = this.term.registerMarker(0);
|
|
468
|
+
if (this.enableCommandSeparator) {
|
|
469
|
+
this.addCommandSeparator();
|
|
470
|
+
}
|
|
471
|
+
} else if (oscPayload.startsWith('command_started')) {
|
|
472
|
+
const command = oscPayload.split(';')[1];
|
|
473
|
+
if (!command) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
this.startNewCommand(command);
|
|
477
|
+
}
|
|
478
|
+
return true;
|
|
479
|
+
})
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
protected resetCommandOutputMarker(): void {
|
|
484
|
+
this.outputStartMarker?.dispose();
|
|
485
|
+
this.outputStartMarker = undefined;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
protected resetCommandMarker(): void {
|
|
489
|
+
this.promptStartMarker?.dispose();
|
|
490
|
+
this.promptStartMarker = undefined;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
protected startNewCommand(hexEncodedCommand: string): void {
|
|
494
|
+
if (!this._commandHistoryState) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
this.outputStartMarker?.dispose();
|
|
498
|
+
this.outputStartMarker = this.term.registerMarker(0);
|
|
499
|
+
this._commandHistoryState.startCommand(hexEncodedCommand);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
protected finishCurrentCommand(): void {
|
|
503
|
+
if (!this._commandHistoryState) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
const startMarker = this.outputStartMarker;
|
|
507
|
+
const endMarker = this.term.registerMarker(0);
|
|
508
|
+
const startLine = startMarker?.line ?? -1;
|
|
509
|
+
const endLine = endMarker?.line ?? -1;
|
|
510
|
+
endMarker.dispose();
|
|
511
|
+
|
|
512
|
+
let output = '';
|
|
513
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
514
|
+
output = this._buffer.getLines(startLine, endLine - startLine, true).join('\n');
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const block: TerminalBlock = {
|
|
518
|
+
command: this._commandHistoryState.currentCommand,
|
|
519
|
+
output,
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
this.logger.debug('Terminal command result captured:', { command: block.command, output: block.output, outputLength: block.output.length });
|
|
523
|
+
this._commandHistoryState.finishCommand(block);
|
|
524
|
+
this.outputStartMarker = undefined;
|
|
525
|
+
this.promptStartMarker = undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private addCommandSeparator(): void {
|
|
529
|
+
const marker = this.term.registerMarker(0);
|
|
530
|
+
if (!marker) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const deco = this.term.registerDecoration({ marker });
|
|
535
|
+
if (!deco) {
|
|
536
|
+
marker.dispose();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const renderListener = deco.onRender(e => {
|
|
541
|
+
e.classList.add('terminal-command-separator');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
this.commandSeparatorDecorations.push(marker);
|
|
545
|
+
this.commandSeparatorDecorations.push(deco);
|
|
546
|
+
this.commandSeparatorDecorations.push(renderListener);
|
|
387
547
|
}
|
|
388
548
|
|
|
389
549
|
protected setIconClass(): void {
|
|
@@ -649,7 +809,8 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
649
809
|
isPseudo: this.options.isPseudoTerminal,
|
|
650
810
|
rootURI,
|
|
651
811
|
cols,
|
|
652
|
-
rows
|
|
812
|
+
rows,
|
|
813
|
+
enableShellIntegration: this.preferences['terminal.integrated.enableCommandHistory'] ?? false,
|
|
653
814
|
});
|
|
654
815
|
if (IBaseTerminalServer.validateId(terminalId)) {
|
|
655
816
|
const processInfo = await this.shellTerminalServer.getProcessInfo(terminalId);
|
|
@@ -847,6 +1008,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
847
1008
|
|
|
848
1009
|
async executeCommand(commandOptions: CommandLineOptions): Promise<void> {
|
|
849
1010
|
this.sendText(this.shellCommandBuilder.buildCommand(await this.processInfo, commandOptions) + OS.backend.EOL);
|
|
1011
|
+
this.resetCommandOutputMarker();
|
|
1012
|
+
if (this._commandHistoryState) {
|
|
1013
|
+
this._commandHistoryState.clearCommandCollectionState();
|
|
1014
|
+
}
|
|
850
1015
|
}
|
|
851
1016
|
|
|
852
1017
|
scrollLineUp(): void {
|
|
@@ -903,6 +1068,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
903
1068
|
}
|
|
904
1069
|
this.styleElement?.remove();
|
|
905
1070
|
this.webglAddon?.dispose();
|
|
1071
|
+
this._commandHistoryState?.dispose();
|
|
906
1072
|
super.dispose();
|
|
907
1073
|
}
|
|
908
1074
|
|
|
@@ -1057,4 +1223,5 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
|
|
|
1057
1223
|
|
|
1058
1224
|
return this.enhancedPreviewNode;
|
|
1059
1225
|
}
|
|
1226
|
+
|
|
1060
1227
|
}
|
|
@@ -58,6 +58,12 @@ export interface IShellTerminalServerOptions extends IBaseTerminalServerOptions
|
|
|
58
58
|
env?: { [key: string]: string | null },
|
|
59
59
|
strictEnv?: boolean,
|
|
60
60
|
isPseudo?: boolean,
|
|
61
|
+
/**
|
|
62
|
+
* Whether to inject shell integration scripts for command tracking.
|
|
63
|
+
* When enabled, shell integration scripts are injected to emit OSC 133 sequences.
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
enableShellIntegration?: boolean;
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export const ShellTerminalServerProxy = Symbol('ShellTerminalServerProxy');
|
|
@@ -372,6 +372,18 @@ export const TerminalConfigSchema: PreferenceSchema = {
|
|
|
372
372
|
description: nls.localizeByDefault('Persist terminal sessions/history for the workspace across window reloads.'),
|
|
373
373
|
default: true
|
|
374
374
|
},
|
|
375
|
+
'terminal.integrated.enableCommandHistory': {
|
|
376
|
+
type: 'boolean',
|
|
377
|
+
markdownDescription: nls.localize('theia/terminal/enableCommandHistory', 'Track terminal commands and their output separately using shell injection. This enables use cases such as visually distinguishing commands in the UI and giving AI agents more structured access to terminals. Toggling this setting will not affect terminals that are already open.\n\n \n\nThis feature is currently only supported by task terminals and user terminals running bash or zsh.'),
|
|
378
|
+
default: false,
|
|
379
|
+
tags: ['experimental']
|
|
380
|
+
},
|
|
381
|
+
'terminal.integrated.enableCommandSeparator': {
|
|
382
|
+
type: 'boolean',
|
|
383
|
+
markdownDescription: nls.localize('theia/terminal/enableCommandSeparator', 'Enable a visual separator between executed commands and their output in the terminal. Changes only apply to commands executed after this setting is modified. Only works when {0} is enabled.', '`#terminal.integrated.enableCommandHistory#`'),
|
|
384
|
+
default: false,
|
|
385
|
+
tags: ['experimental']
|
|
386
|
+
},
|
|
375
387
|
'terminal.integrated.defaultProfile.windows': {
|
|
376
388
|
type: 'string',
|
|
377
389
|
description: nls.localize('theia/terminal/defaultProfile', 'The default profile used on {0}', OS.Type.Windows.toString())
|
|
@@ -556,6 +568,8 @@ export interface TerminalConfiguration {
|
|
|
556
568
|
'terminal.integrated.profiles.osx': Profiles,
|
|
557
569
|
'terminal.integrated.confirmOnExit': ConfirmOnExitType
|
|
558
570
|
'terminal.integrated.enablePersistentSessions': boolean
|
|
571
|
+
'terminal.integrated.enableCommandHistory': boolean
|
|
572
|
+
'terminal.integrated.enableCommandSeparator': boolean
|
|
559
573
|
}
|
|
560
574
|
|
|
561
575
|
type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH 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 path = require('path');
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import { GeneralShellType, guessShellTypeFromExecutable } from '../common/shell-type';
|
|
20
|
+
import { ShellProcess, ShellProcessOptions } from './shell-process';
|
|
21
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
22
|
+
|
|
23
|
+
@injectable()
|
|
24
|
+
export class ShellIntegrationInjector {
|
|
25
|
+
|
|
26
|
+
protected readonly INTEGRATION_ROOT_DIR = 'shell-integrations';
|
|
27
|
+
|
|
28
|
+
protected readonly BASH_RCFILE_FLAG = '--rcfile';
|
|
29
|
+
protected readonly BASH_INTEGRATION_SCRIPT_PATH = 'bash/bash-integration.bash';
|
|
30
|
+
|
|
31
|
+
protected readonly ZSH_INTEGRATION_ENV_VAR = 'THEIA_ZSH_DIR';
|
|
32
|
+
protected readonly ZSH_INTEGRATION_DIR = 'zsh';
|
|
33
|
+
protected readonly ZDOTDIR_ENV_VAR = 'ZDOTDIR';
|
|
34
|
+
protected readonly ZDOTDIR_RELATIVE_DIR = '/zsh/zdotdir/';
|
|
35
|
+
protected readonly ZDOTDIR_ORIGINAL_ENV_VAR = 'THEIA_ORIGINAL_ZDOTDIR';
|
|
36
|
+
|
|
37
|
+
injectShellIntegration(options: ShellProcessOptions): ShellProcessOptions {
|
|
38
|
+
const shellExecutable = options.shell ?? ShellProcess.getShellExecutablePath();
|
|
39
|
+
const shellType = guessShellTypeFromExecutable(shellExecutable);
|
|
40
|
+
if (shellType === GeneralShellType.Bash) {
|
|
41
|
+
const scriptPath = this.getShellIntegrationPath(this.BASH_INTEGRATION_SCRIPT_PATH);
|
|
42
|
+
if (!scriptPath) {
|
|
43
|
+
return options;
|
|
44
|
+
}
|
|
45
|
+
// strips the login flag if present to avoid conflicts with --rcfile
|
|
46
|
+
const filteredArgs = this.stripLoginFlag(options.args);
|
|
47
|
+
return {
|
|
48
|
+
...options,
|
|
49
|
+
args: [
|
|
50
|
+
this.BASH_RCFILE_FLAG, scriptPath,
|
|
51
|
+
...(filteredArgs ?? []),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
} else if (shellType === GeneralShellType.Zsh) {
|
|
55
|
+
const zdotdirPath = this.getShellIntegrationPath(this.ZDOTDIR_RELATIVE_DIR);
|
|
56
|
+
const zshDirPath = this.getShellIntegrationPath(this.ZSH_INTEGRATION_DIR);
|
|
57
|
+
if (!zdotdirPath || !zshDirPath) {
|
|
58
|
+
return options;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...options,
|
|
62
|
+
env: {
|
|
63
|
+
...options.env,
|
|
64
|
+
[this.ZDOTDIR_ENV_VAR]: zdotdirPath,
|
|
65
|
+
[this.ZSH_INTEGRATION_ENV_VAR]: zshDirPath,
|
|
66
|
+
[this.ZDOTDIR_ORIGINAL_ENV_VAR]: options.env?.ZDOTDIR ?? process.env.ZDOTDIR ?? ''
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
return options;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected getShellIntegrationPath(relativePath: string): string | undefined {
|
|
75
|
+
const fullPath = path.join(__dirname, this.INTEGRATION_ROOT_DIR, relativePath);
|
|
76
|
+
if (!fs.existsSync(fullPath)) {
|
|
77
|
+
console.warn(`Shell integration file not found (application may not be bundled correctly): ${fullPath}`);
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
return fullPath;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected stripLoginFlag(args: string | string[] | undefined): string[] | undefined {
|
|
84
|
+
if (args === undefined) {
|
|
85
|
+
return args;
|
|
86
|
+
}
|
|
87
|
+
if (typeof args === 'string') {
|
|
88
|
+
// split string on any amount of whitespace into an array
|
|
89
|
+
return args.trim().split(/\s+/).filter(arg => arg !== '-l' && arg !== '--login');
|
|
90
|
+
}
|
|
91
|
+
return args.filter(arg => arg !== '-l' && arg !== '--login');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# *****************************************************************************
|
|
3
|
+
# Copyright (C) 2000-2025 JetBrains s.r.o.
|
|
4
|
+
# Modifications (C) 2025 EclipseSource GmbH and others.
|
|
5
|
+
#
|
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
# you may not use this file except in compliance with the License.
|
|
8
|
+
# You may obtain a copy of the License at
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions
|
|
15
|
+
# and limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
# Modifications:
|
|
18
|
+
# - Rebranded internals from JetBrains/IntelliJ to Theia (`JETBRAINS_INTELLIJ_BASH_DIR` -> `THEIA_BASH_DIR`,
|
|
19
|
+
# `__jetbrains_intellij_restore_posix_flag` -> `__theia_restore_posix_flag`).
|
|
20
|
+
# - Removed IntelliJ-provided environment injection (`_INTELLIJ_FORCE_SET_*` / `_INTELLIJ_FORCE_PREPEND_*` processing),
|
|
21
|
+
# plus optional sourcing of `JEDITERM_USER_RCFILE` and `JEDITERM_SOURCE` (+ args).
|
|
22
|
+
# - Dropped keybinding setup for Ctrl-left/right word movement.
|
|
23
|
+
# - Dropped IntelliJ command-history hook (`__INTELLIJ_COMMAND_HISTFILE__` + EXIT trap).
|
|
24
|
+
# - Only sources `command-block-support.bash` (no `command-block-support-reworked.bash`), without readability checks.
|
|
25
|
+
#
|
|
26
|
+
# Source:
|
|
27
|
+
# https://github.com/JetBrains/intellij-community/blob/8d02751ced444e5b70784fe0a757f960fe495a67/plugins/terminal/resources/shell-integrations/bash/bash-integration.bash
|
|
28
|
+
# *****************************************************************************
|
|
29
|
+
|
|
30
|
+
if [ -n "$LOGIN_SHELL" ]; then
|
|
31
|
+
unset LOGIN_SHELL
|
|
32
|
+
|
|
33
|
+
# When bash is invoked as an interactive login shell, or as a non-interac-
|
|
34
|
+
# tive shell with the --login option, it first reads and executes commands
|
|
35
|
+
# from the file /etc/profile, if that file exists. After reading that
|
|
36
|
+
# file, it looks for ~/.bash_profile, ~/.bash_login, and ~/.profile, in
|
|
37
|
+
# that order, and reads and executes commands from the first one that
|
|
38
|
+
# exists and is readable.
|
|
39
|
+
|
|
40
|
+
if [ -f /etc/profile ]; then
|
|
41
|
+
source /etc/profile
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if [ -f ~/.bash_profile ]; then
|
|
45
|
+
source ~/.bash_profile
|
|
46
|
+
else
|
|
47
|
+
if [ -f ~/.bash_login ]; then
|
|
48
|
+
source ~/.bash_login
|
|
49
|
+
else
|
|
50
|
+
if [ -f ~/.profile ]; then
|
|
51
|
+
source ~/.profile
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
if [ -f ~/.bashrc ]; then
|
|
57
|
+
source ~/.bashrc
|
|
58
|
+
fi
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
function disable_posix() {
|
|
62
|
+
if shopt -qo posix
|
|
63
|
+
then
|
|
64
|
+
set +o posix
|
|
65
|
+
__theia_restore_posix_flag=1
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function restore_posix() {
|
|
70
|
+
if [ -n "${__theia_restore_posix_flag-}" ]
|
|
71
|
+
then
|
|
72
|
+
set -o posix
|
|
73
|
+
unset __theia_restore_posix_flag
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
disable_posix
|
|
78
|
+
|
|
79
|
+
THEIA_BASH_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
|
80
|
+
source "${THEIA_BASH_DIR}/command-block-support.bash"
|
|
81
|
+
|
|
82
|
+
unset THEIA_BASH_DIR
|
|
83
|
+
|
|
84
|
+
restore_posix
|
|
85
|
+
unset -f disable_posix
|
|
86
|
+
unset -f restore_posix
|