@theia/terminal 1.73.0-next.9 → 1.73.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.
Files changed (65) hide show
  1. package/lib/browser/base/terminal-service.d.ts +1 -1
  2. package/lib/browser/base/terminal-service.d.ts.map +1 -1
  3. package/lib/browser/base/terminal-widget.d.ts +6 -0
  4. package/lib/browser/base/terminal-widget.d.ts.map +1 -1
  5. package/lib/browser/base/terminal-widget.js.map +1 -1
  6. package/lib/browser/index.d.ts +1 -0
  7. package/lib/browser/index.d.ts.map +1 -1
  8. package/lib/browser/index.js +1 -0
  9. package/lib/browser/index.js.map +1 -1
  10. package/lib/browser/search/terminal-search-widget.js +2 -2
  11. package/lib/browser/search/terminal-search-widget.js.map +1 -1
  12. package/lib/browser/terminal-creation-handler.d.ts +35 -0
  13. package/lib/browser/terminal-creation-handler.d.ts.map +1 -0
  14. package/lib/browser/terminal-creation-handler.js +33 -0
  15. package/lib/browser/terminal-creation-handler.js.map +1 -0
  16. package/lib/browser/terminal-file-link-provider.d.ts +2 -1
  17. package/lib/browser/terminal-file-link-provider.d.ts.map +1 -1
  18. package/lib/browser/terminal-file-link-provider.js +7 -2
  19. package/lib/browser/terminal-file-link-provider.js.map +1 -1
  20. package/lib/browser/terminal-frontend-contribution.d.ts +10 -1
  21. package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
  22. package/lib/browser/terminal-frontend-contribution.js +76 -53
  23. package/lib/browser/terminal-frontend-contribution.js.map +1 -1
  24. package/lib/browser/terminal-frontend-module.d.ts.map +1 -1
  25. package/lib/browser/terminal-frontend-module.js +5 -0
  26. package/lib/browser/terminal-frontend-module.js.map +1 -1
  27. package/lib/browser/terminal-shell-handler.d.ts +19 -0
  28. package/lib/browser/terminal-shell-handler.d.ts.map +1 -0
  29. package/lib/browser/terminal-shell-handler.js +95 -0
  30. package/lib/browser/terminal-shell-handler.js.map +1 -0
  31. package/lib/browser/terminal-widget-impl.d.ts +21 -0
  32. package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
  33. package/lib/browser/terminal-widget-impl.js +59 -7
  34. package/lib/browser/terminal-widget-impl.js.map +1 -1
  35. package/lib/node/buffering-stream.d.ts +2 -0
  36. package/lib/node/buffering-stream.d.ts.map +1 -1
  37. package/lib/node/buffering-stream.js +12 -0
  38. package/lib/node/buffering-stream.js.map +1 -1
  39. package/lib/node/buffering-stream.spec.js +27 -0
  40. package/lib/node/buffering-stream.spec.js.map +1 -1
  41. package/lib/node/shell-terminal-server.d.ts +7 -0
  42. package/lib/node/shell-terminal-server.d.ts.map +1 -1
  43. package/lib/node/shell-terminal-server.js +69 -0
  44. package/lib/node/shell-terminal-server.js.map +1 -1
  45. package/lib/node/shell-terminal-server.spec.js +99 -0
  46. package/lib/node/shell-terminal-server.spec.js.map +1 -1
  47. package/lib/node/terminal-backend-contribution.d.ts.map +1 -1
  48. package/lib/node/terminal-backend-contribution.js +5 -0
  49. package/lib/node/terminal-backend-contribution.js.map +1 -1
  50. package/package.json +10 -10
  51. package/src/browser/base/terminal-service.ts +1 -1
  52. package/src/browser/base/terminal-widget.ts +7 -0
  53. package/src/browser/index.ts +1 -0
  54. package/src/browser/search/terminal-search-widget.tsx +2 -2
  55. package/src/browser/terminal-creation-handler.ts +53 -0
  56. package/src/browser/terminal-file-link-provider.ts +6 -4
  57. package/src/browser/terminal-frontend-contribution.ts +79 -57
  58. package/src/browser/terminal-frontend-module.ts +5 -0
  59. package/src/browser/terminal-shell-handler.ts +89 -0
  60. package/src/browser/terminal-widget-impl.ts +64 -8
  61. package/src/node/buffering-stream.spec.ts +30 -0
  62. package/src/node/buffering-stream.ts +13 -0
  63. package/src/node/shell-terminal-server.spec.ts +119 -1
  64. package/src/node/shell-terminal-server.ts +70 -0
  65. package/src/node/terminal-backend-contribution.ts +5 -0
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@theia/terminal",
3
- "version": "1.73.0-next.9+310484268",
3
+ "version": "1.73.0",
4
4
  "description": "Theia - Terminal Extension",
5
5
  "dependencies": {
6
- "@theia/core": "1.73.0-next.9+310484268",
7
- "@theia/editor": "1.73.0-next.9+310484268",
8
- "@theia/file-search": "1.73.0-next.9+310484268",
9
- "@theia/filesystem": "1.73.0-next.9+310484268",
10
- "@theia/process": "1.73.0-next.9+310484268",
11
- "@theia/variable-resolver": "1.73.0-next.9+310484268",
12
- "@theia/workspace": "1.73.0-next.9+310484268",
6
+ "@theia/core": "1.73.0",
7
+ "@theia/editor": "1.73.0",
8
+ "@theia/file-search": "1.73.0",
9
+ "@theia/filesystem": "1.73.0",
10
+ "@theia/process": "1.73.0",
11
+ "@theia/variable-resolver": "1.73.0",
12
+ "@theia/workspace": "1.73.0",
13
13
  "tslib": "^2.8.1",
14
14
  "xterm": "^5.3.0",
15
15
  "xterm-addon-fit": "^0.8.0",
@@ -51,10 +51,10 @@
51
51
  "watch": "theiaext watch"
52
52
  },
53
53
  "devDependencies": {
54
- "@theia/ext-scripts": "1.72.0"
54
+ "@theia/ext-scripts": "1.73.0"
55
55
  },
56
56
  "nyc": {
57
57
  "extends": "../../configs/nyc.json"
58
58
  },
59
- "gitHead": "31048426875ed92ecdb45c7446e92dddb8048f81"
59
+ "gitHead": "25e1b7fdf4c39ecea7066e1c8be3af1cc02e4d2e"
60
60
  }
@@ -29,7 +29,7 @@ export interface TerminalService {
29
29
  */
30
30
  newTerminal(options: TerminalWidgetOptions): Promise<TerminalWidget>;
31
31
 
32
- open(terminal: TerminalWidget, options?: WidgetOpenerOptions): void;
32
+ open(terminal: TerminalWidget, options?: WidgetOpenerOptions): Promise<void>;
33
33
 
34
34
  readonly all: TerminalWidget[];
35
35
 
@@ -181,6 +181,13 @@ export abstract class TerminalWidget extends BaseWidget {
181
181
  */
182
182
  abstract hasSelection(): boolean;
183
183
 
184
+ /**
185
+ * Paste the given text into the terminal as if it were typed by the user.
186
+ * Honors bracketed paste mode when enabled by the running program.
187
+ * @param text the text to paste
188
+ */
189
+ abstract paste(text: string): void;
190
+
184
191
  abstract writeLine(line: string): void;
185
192
 
186
193
  abstract write(data: string): void;
@@ -15,3 +15,4 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  export * from './terminal-frontend-module';
18
+ export * from './terminal-creation-handler';
@@ -51,13 +51,13 @@ export class TerminalSearchWidget extends ReactWidget {
51
51
 
52
52
  protected render(): React.ReactNode {
53
53
  return <div className='theia-search-terminal-widget'>
54
- <div className='theia-search-elem-box' ref={searchBox => this.searchBox = searchBox} >
54
+ <div className='theia-search-elem-box' ref={searchBox => { this.searchBox = searchBox; }} >
55
55
  <input
56
56
  title={nls.localizeByDefault('Find')}
57
57
  type='text'
58
58
  spellCheck='false'
59
59
  placeholder={nls.localizeByDefault('Find')}
60
- ref={ip => this.searchInput = ip}
60
+ ref={ip => { this.searchInput = ip; }}
61
61
  onKeyUp={this.onInputChanged}
62
62
  onFocus={this.onSearchInputFocus}
63
63
  onBlur={this.onSearchInputBlur}
@@ -0,0 +1,53 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource 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 { MaybePromise } from '@theia/core';
18
+ import { WidgetOpenerOptions } from '@theia/core/lib/browser';
19
+ import { TerminalWidget } from './base/terminal-widget';
20
+
21
+ /**
22
+ * Contribution point that allows extensions to intercept terminal placement
23
+ * and take ownership of how/where terminals are displayed.
24
+ *
25
+ * When a terminal is opened via {@link TerminalService.open}, registered
26
+ * handlers are consulted in priority order. A handler can:
27
+ * - Return `true` to claim the terminal (the default shell placement is skipped)
28
+ * - Return `false` / `undefined` to decline (the next handler or default behavior applies)
29
+ *
30
+ * This is the canonical way for alternative terminal UIs (e.g. the terminal
31
+ * manager's tree view) to take ownership of terminal placement without relying
32
+ * on post-hoc event interception or timing-sensitive side-channel flags.
33
+ */
34
+ export const TerminalCreationHandler = Symbol('TerminalCreationHandler');
35
+ export interface TerminalCreationHandler {
36
+
37
+ /**
38
+ * Called when a terminal is about to be placed in the shell.
39
+ * The handler may take ownership of the terminal (e.g. add it to a custom container)
40
+ * by returning `true`. In that case, the default shell placement will be skipped.
41
+ *
42
+ * @param terminal The terminal widget to be placed.
43
+ * @param options The opener options, if any.
44
+ * @returns `true` if this handler claimed the terminal, `false` or `undefined` otherwise.
45
+ */
46
+ onWillOpenTerminal(terminal: TerminalWidget, options?: WidgetOpenerOptions): MaybePromise<boolean | undefined>;
47
+
48
+ /**
49
+ * The priority of this handler. Higher values are consulted first.
50
+ * Defaults to `0` if not specified.
51
+ */
52
+ readonly priority?: number;
53
+ }
@@ -14,10 +14,10 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { OS, Path, QuickInputService } from '@theia/core';
17
+ import { OS, Path, QuickInputService, ILogger } from '@theia/core';
18
18
  import { OpenerService } from '@theia/core/lib/browser';
19
19
  import URI from '@theia/core/lib/common/uri';
20
- import { inject, injectable } from '@theia/core/shared/inversify';
20
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
21
21
  import { Position } from '@theia/editor/lib/browser';
22
22
  import { FileService } from '@theia/filesystem/lib/browser/file-service';
23
23
  import { TerminalWidget } from './base/terminal-widget';
@@ -33,6 +33,8 @@ export class FileLinkProvider implements TerminalLinkProvider {
33
33
  @inject(FileService) protected fileService: FileService;
34
34
  @inject(FileSearchService) protected searchService: FileSearchService;
35
35
  @inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
36
+ @inject(ILogger) @named('terminal:FileLinkProvider')
37
+ protected readonly logger: ILogger;
36
38
 
37
39
  async provideLinks(line: string, terminal: TerminalWidget): Promise<TerminalLink[]> {
38
40
  const links: TerminalLink[] = [];
@@ -64,7 +66,7 @@ export class FileLinkProvider implements TerminalLinkProvider {
64
66
  return this.isValidFileURI(toOpen);
65
67
  }
66
68
  } catch (err) {
67
- console.trace('Error validating ' + match, err);
69
+ this.logger.error('Error validating ' + match, err);
68
70
  }
69
71
  return false;
70
72
  }
@@ -120,7 +122,7 @@ export class FileLinkProvider implements TerminalLinkProvider {
120
122
  const opener = await this.openerService.getOpener(toOpen, options);
121
123
  opener.open(toOpen, options);
122
124
  } catch (err) {
123
- console.error('Cannot open link ' + toOpen, err);
125
+ this.logger.error('Cannot open link ' + toOpen, err);
124
126
  }
125
127
  }
126
128
 
@@ -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 { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
17
+ import { inject, injectable, named, optional, postConstruct } from '@theia/core/shared/inversify';
18
18
  import {
19
19
  CommandContribution,
20
20
  Command,
@@ -26,7 +26,7 @@ import {
26
26
  SelectionService,
27
27
  Emitter,
28
28
  Event,
29
- ViewColumn,
29
+
30
30
  OS,
31
31
  MAIN_MENU_BAR,
32
32
  PreferenceService,
@@ -37,10 +37,11 @@ import {
37
37
  KeybindingRegistry, LabelProvider, WidgetOpenerOptions, StorageService, QuickInputService,
38
38
  codicon, CommonCommands, FrontendApplicationContribution, OnWillStopAction, Dialog, ConfirmDialog, FrontendApplication, Widget, SHELL_TABBAR_CONTEXT_MENU
39
39
  } from '@theia/core/lib/browser';
40
+ import { ClipboardService } from '@theia/core/lib/browser/clipboard-service';
40
41
  import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
41
- import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions, TerminalWidgetImpl } from './terminal-widget-impl';
42
+ import { TERMINAL_WIDGET_FACTORY_ID, TerminalWidgetFactoryOptions, TerminalWidgetImpl, nextTerminalCreationToken } from './terminal-widget-impl';
42
43
  import { TerminalService } from './base/terminal-service';
43
- import { TerminalWidgetOptions, TerminalWidget, TerminalLocation } from './base/terminal-widget';
44
+ import { TerminalWidgetOptions, TerminalWidget } from './base/terminal-widget';
44
45
  import { ContributedTerminalProfileStore, NULL_PROFILE, TerminalProfile, TerminalProfileService, TerminalProfileStore, UserTerminalProfileStore } from './terminal-profile-service';
45
46
  import { UriAwareCommandHandler } from '@theia/core/lib/common/uri-command-handler';
46
47
  import { ShellTerminalServerProxy } from '../common/shell-terminal-protocol';
@@ -58,6 +59,8 @@ import { Profiles, terminalAnsiColorMap, TerminalPreferences } from '../common/t
58
59
  import { ShellTerminalProfile } from './shell-terminal-profile';
59
60
  import { VariableResolverService } from '@theia/variable-resolver/lib/browser';
60
61
  import { Color } from '@theia/core/lib/common/color';
62
+ import { ContributionProvider } from '@theia/core';
63
+ import { TerminalCreationHandler } from './terminal-creation-handler';
61
64
 
62
65
  export namespace TerminalMenus {
63
66
  export const TERMINAL = [...MAIN_MENU_BAR, '7_terminal'];
@@ -164,6 +167,16 @@ export namespace TerminalCommands {
164
167
  label: 'Select All',
165
168
  category: TERMINAL_CATEGORY,
166
169
  });
170
+ export const PASTE_TERMINAL = Command.toDefaultLocalizedCommand({
171
+ id: 'workbench.action.terminal.paste',
172
+ category: TERMINAL_CATEGORY,
173
+ label: 'Paste into Active Terminal'
174
+ });
175
+ export const COPY_TERMINAL_SELECTION = Command.toDefaultLocalizedCommand({
176
+ id: 'workbench.action.terminal.copySelection',
177
+ category: TERMINAL_CATEGORY,
178
+ label: 'Copy Selection'
179
+ });
167
180
 
168
181
  /**
169
182
  * Command that displays all terminals that are currently opened
@@ -230,9 +243,15 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu
230
243
  @inject(TerminalCopyOnSelectionHandler)
231
244
  protected readonly copyHandler: TerminalCopyOnSelectionHandler;
232
245
 
246
+ @inject(ClipboardService)
247
+ protected readonly clipboardService: ClipboardService;
248
+
233
249
  @inject(ContextKeyService)
234
250
  protected readonly contextKeyService: ContextKeyService;
235
251
 
252
+ @inject(ContributionProvider) @named(TerminalCreationHandler)
253
+ protected readonly terminalCreationHandlers: ContributionProvider<TerminalCreationHandler>;
254
+
236
255
  @postConstruct()
237
256
  protected init(): void {
238
257
  this.shell.onDidChangeCurrentWidget(() => this.updateCurrentTerminal());
@@ -670,20 +689,47 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu
670
689
  isEnabled: () => !!this.currentTerminal,
671
690
  execute: () => this.currentTerminal?.selectAll()
672
691
  });
673
- commands.registerHandler(CommonCommands.COPY.id, {
692
+ commands.registerCommand(TerminalCommands.PASTE_TERMINAL, {
693
+ isEnabled: () => !!this.getPasteTargetTerminal(),
694
+ execute: async () => {
695
+ const terminal = this.getPasteTargetTerminal();
696
+ if (!terminal) {
697
+ return;
698
+ }
699
+ const text = await this.clipboardService.readText();
700
+ if (text) {
701
+ terminal.paste(text);
702
+ }
703
+ }
704
+ });
705
+ commands.registerCommand(TerminalCommands.COPY_TERMINAL_SELECTION, {
706
+ isEnabled: () => !!this.getCopySourceTerminal(),
674
707
  execute: () => {
675
- const terminal = this.shell.activeWidget;
676
- if (terminal instanceof TerminalWidget && terminal.hasSelection()) {
677
- this.copyHandler.syncCopy(terminal.getSelection());
708
+ const terminal = this.getCopySourceTerminal();
709
+ if (!terminal) {
710
+ return;
678
711
  }
679
- },
680
- isEnabled: () => {
681
- const terminal = this.shell.activeWidget;
682
- return terminal instanceof TerminalWidget && terminal.hasSelection();
712
+ this.copyHandler.syncCopy(terminal.getSelection());
683
713
  }
684
714
  });
685
715
  }
686
716
 
717
+ protected getPasteTargetTerminal(): TerminalWidget | undefined {
718
+ const terminal = this.shell.activeWidget;
719
+ if (terminal instanceof TerminalWidget && this.terminalPreferences['terminal.enablePaste']) {
720
+ return terminal;
721
+ }
722
+ return undefined;
723
+ }
724
+
725
+ protected getCopySourceTerminal(): TerminalWidget | undefined {
726
+ const terminal = this.shell.activeWidget;
727
+ if (terminal instanceof TerminalWidget && terminal.hasSelection() && this.terminalPreferences['terminal.enableCopy']) {
728
+ return terminal;
729
+ }
730
+ return undefined;
731
+ }
732
+
687
733
  protected toggleTerminal(): void {
688
734
  const terminals = this.shell.getWidgets('bottom').filter(w => w instanceof TerminalWidget);
689
735
 
@@ -765,10 +811,12 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu
765
811
  commandId: TerminalCommands.SPLIT.id
766
812
  });
767
813
  menus.registerMenuAction([...TerminalMenus.TERMINAL_CONTEXT_MENU, '_2'], {
768
- commandId: CommonCommands.COPY.id
814
+ commandId: TerminalCommands.COPY_TERMINAL_SELECTION.id,
815
+ label: nls.localizeByDefault('Copy')
769
816
  });
770
817
  menus.registerMenuAction([...TerminalMenus.TERMINAL_CONTEXT_MENU, '_2'], {
771
- commandId: CommonCommands.PASTE.id
818
+ commandId: TerminalCommands.PASTE_TERMINAL.id,
819
+ label: nls.localizeByDefault('Paste')
772
820
  });
773
821
  menus.registerMenuAction([...TerminalMenus.TERMINAL_CONTEXT_MENU, '_2'], {
774
822
  commandId: TerminalCommands.SELECT_ALL.id
@@ -898,6 +946,16 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu
898
946
  keybinding: 'ctrlcmd+k',
899
947
  when: 'terminalFocus'
900
948
  });
949
+ keybindings.registerKeybinding({
950
+ command: TerminalCommands.PASTE_TERMINAL.id,
951
+ keybinding: 'ctrlcmd+v',
952
+ when: 'terminalFocus'
953
+ });
954
+ keybindings.registerKeybinding({
955
+ command: TerminalCommands.COPY_TERMINAL_SELECTION.id,
956
+ keybinding: 'ctrlcmd+c',
957
+ when: 'terminalFocus'
958
+ });
901
959
  keybindings.registerKeybinding({
902
960
  command: TerminalCommands.TERMINAL_FIND_TEXT.id,
903
961
  keybinding: 'ctrlcmd+f',
@@ -946,56 +1004,20 @@ export class TerminalFrontendContribution implements FrontendApplicationContribu
946
1004
 
947
1005
  async newTerminal(options: TerminalWidgetOptions): Promise<TerminalWidget> {
948
1006
  const widget = <TerminalWidget>await this.widgetManager.getOrCreateWidget(TERMINAL_WIDGET_FACTORY_ID, <TerminalWidgetFactoryOptions>{
949
- created: new Date().toISOString(),
1007
+ created: nextTerminalCreationToken(),
950
1008
  ...options
951
1009
  });
952
1010
  return widget;
953
1011
  }
954
1012
 
955
- // TODO: reuse WidgetOpenHandler.open
956
- open(widget: TerminalWidget, options?: WidgetOpenerOptions): void {
957
- const area = widget.location === TerminalLocation.Editor ? 'main' : 'bottom';
958
- const widgetOptions: ApplicationShell.WidgetOptions = { area: area, ...options?.widgetOptions };
959
- let preserveFocus = false;
960
-
961
- if (typeof widget.location === 'object') {
962
- if ('parentTerminal' in widget.location) {
963
- widgetOptions.ref = this.getById(widget.location.parentTerminal);
964
- widgetOptions.mode = 'split-right';
965
- } else if ('viewColumn' in widget.location) {
966
- preserveFocus = widget.location.preserveFocus ?? false;
967
- switch (widget.location.viewColumn) {
968
- case ViewColumn.Active:
969
- widgetOptions.ref = this.shell.currentWidget;
970
- widgetOptions.mode = 'tab-after';
971
- break;
972
- case ViewColumn.Beside:
973
- widgetOptions.ref = this.shell.currentWidget;
974
- widgetOptions.mode = 'split-right';
975
- break;
976
- default:
977
- widgetOptions.area = 'main';
978
- const mainAreaTerminals = this.shell.getWidgets('main').filter(w => w instanceof TerminalWidget && w.isVisible);
979
- const column = Math.min(widget.location.viewColumn, mainAreaTerminals.length);
980
- widgetOptions.mode = widget.location.viewColumn <= mainAreaTerminals.length ? 'split-left' : 'split-right';
981
- widgetOptions.ref = mainAreaTerminals[column - 1];
982
- }
1013
+ async open(widget: TerminalWidget, options?: WidgetOpenerOptions): Promise<void> {
1014
+ const handlers = this.terminalCreationHandlers.getContributions(true)
1015
+ .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
1016
+ for (const handler of handlers) {
1017
+ if (await handler.onWillOpenTerminal(widget, options)) {
1018
+ return;
983
1019
  }
984
1020
  }
985
-
986
- const op: WidgetOpenerOptions = {
987
- mode: 'activate',
988
- ...options,
989
- widgetOptions: widgetOptions
990
- };
991
- if (!widget.isAttached) {
992
- this.shell.addWidget(widget, op.widgetOptions);
993
- }
994
- if (op.mode === 'activate' && !preserveFocus) {
995
- this.shell.activateWidget(widget.id);
996
- } else if (op.mode === 'reveal' || preserveFocus) {
997
- this.shell.revealWidget(widget.id);
998
- }
999
1021
  }
1000
1022
 
1001
1023
  protected async selectTerminalCwd(): Promise<string | undefined> {
@@ -31,6 +31,8 @@ import { IShellTerminalServer, shellTerminalPath, ShellTerminalServerProxy } fro
31
31
  import { TerminalService } from './base/terminal-service';
32
32
  import { bindTerminalPreferences } from '../common/terminal-preferences';
33
33
  import { TerminalContribution } from './terminal-contribution';
34
+ import { TerminalCreationHandler } from './terminal-creation-handler';
35
+ import { TerminalShellHandler } from './terminal-shell-handler';
34
36
  import { TerminalSearchWidgetFactory } from './search/terminal-search-widget';
35
37
  import { TerminalQuickOpenService, TerminalQuickOpenContribution } from './terminal-quick-open-service';
36
38
  import { createTerminalSearchFactory } from './search/terminal-search-container';
@@ -105,6 +107,9 @@ export default new ContainerModule(bind => {
105
107
  bind(IShellTerminalServer).toService(ShellTerminalServerProxy);
106
108
 
107
109
  bindRootContributionProvider(bind, TerminalContribution);
110
+ bindRootContributionProvider(bind, TerminalCreationHandler);
111
+ bind(TerminalShellHandler).toSelf().inSingletonScope();
112
+ bind(TerminalCreationHandler).toService(TerminalShellHandler);
108
113
 
109
114
  // terminal link provider contribution point
110
115
  bindRootContributionProvider(bind, TerminalLinkProvider);
@@ -0,0 +1,89 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2017-2026 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 { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { ViewColumn } from '@theia/core/lib/common';
19
+ import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser';
20
+ import { TerminalCreationHandler } from './terminal-creation-handler';
21
+ import { TerminalWidget, TerminalLocation } from './base/terminal-widget';
22
+ import { TerminalService } from './base/terminal-service';
23
+
24
+ /**
25
+ * Default {@link TerminalCreationHandler} that places terminals into the
26
+ * application shell. This handles the standard panel/editor/split placement
27
+ * logic and always claims the terminal (returning `true`).
28
+ *
29
+ * Because it always claims, it should run at the lowest priority so that
30
+ * other handlers (e.g. the terminal manager) get a chance to intercept first.
31
+ */
32
+ @injectable()
33
+ export class TerminalShellHandler implements TerminalCreationHandler {
34
+
35
+ readonly priority = -100;
36
+
37
+ @inject(ApplicationShell)
38
+ protected readonly shell: ApplicationShell;
39
+
40
+ @inject(TerminalService)
41
+ protected readonly terminalService: TerminalService;
42
+
43
+ async onWillOpenTerminal(terminal: TerminalWidget, options?: WidgetOpenerOptions): Promise<boolean> {
44
+ const area = terminal.location === TerminalLocation.Editor ? 'main' : 'bottom';
45
+ const widgetOptions: ApplicationShell.WidgetOptions = { area, ...options?.widgetOptions };
46
+ let preserveFocus = false;
47
+
48
+ if (typeof terminal.location === 'object') {
49
+ if ('parentTerminal' in terminal.location) {
50
+ widgetOptions.ref = this.terminalService.getById(terminal.location.parentTerminal);
51
+ widgetOptions.mode = 'split-right';
52
+ } else if ('viewColumn' in terminal.location) {
53
+ preserveFocus = terminal.location.preserveFocus ?? false;
54
+ switch (terminal.location.viewColumn) {
55
+ case ViewColumn.Active:
56
+ widgetOptions.ref = this.shell.currentWidget;
57
+ widgetOptions.mode = 'tab-after';
58
+ break;
59
+ case ViewColumn.Beside:
60
+ widgetOptions.ref = this.shell.currentWidget;
61
+ widgetOptions.mode = 'split-right';
62
+ break;
63
+ default:
64
+ widgetOptions.area = 'main';
65
+ const mainAreaTerminals = this.shell.getWidgets('main').filter(w => w instanceof TerminalWidget && w.isVisible);
66
+ const column = Math.min(terminal.location.viewColumn, mainAreaTerminals.length);
67
+ widgetOptions.mode = terminal.location.viewColumn <= mainAreaTerminals.length ? 'split-left' : 'split-right';
68
+ widgetOptions.ref = mainAreaTerminals[column - 1];
69
+ }
70
+ }
71
+ }
72
+
73
+ const op: WidgetOpenerOptions = {
74
+ mode: 'activate',
75
+ ...options,
76
+ widgetOptions
77
+ };
78
+ if (!terminal.isAttached) {
79
+ this.shell.addWidget(terminal, op.widgetOptions);
80
+ }
81
+ if (op.mode === 'activate' && !preserveFocus) {
82
+ this.shell.activateWidget(terminal.id);
83
+ } else if (op.mode === 'reveal' || preserveFocus) {
84
+ this.shell.revealWidget(terminal.id);
85
+ }
86
+
87
+ return true;
88
+ }
89
+ }
@@ -53,16 +53,34 @@ import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-
53
53
  import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
54
54
  import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
55
55
  import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
56
+ import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
56
57
  import { cleanTerminalTitle, guessShellTypeFromExecutable } from '../common/shell-type';
57
58
  import { TerminalCommandHistoryStateFactory } from './terminal-command-history';
58
59
 
59
60
  export const TERMINAL_WIDGET_FACTORY_ID = 'terminal';
60
61
 
61
62
  export interface TerminalWidgetFactoryOptions extends Partial<TerminalWidgetOptions> {
62
- /* a unique string per terminal */
63
+ /**
64
+ * An opaque, unique string per terminal. Historically a date string, but
65
+ * it should not be interpreted as a date. Callers should use
66
+ * {@link nextTerminalCreationToken} to obtain a value that is guaranteed
67
+ * unique within the current process.
68
+ */
63
69
  created: string
64
70
  }
65
71
 
72
+ let terminalCreationCounter = 0;
73
+ /**
74
+ * Produce a token suitable for {@link TerminalWidgetFactoryOptions.created}
75
+ * that is guaranteed unique within the current process. Combines the current
76
+ * wall-clock time with a monotonically increasing counter so callers cannot
77
+ * accidentally collide even when constructing terminals within the same
78
+ * millisecond.
79
+ */
80
+ export function nextTerminalCreationToken(): string {
81
+ return `${Date.now()}-${terminalCreationCounter++}`;
82
+ }
83
+
66
84
  export const TerminalContribution = Symbol('TerminalContribution');
67
85
  export interface TerminalContribution {
68
86
  onCreate(term: TerminalWidgetImpl): void;
@@ -147,6 +165,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
147
165
  @inject(ContextMenuRenderer) protected readonly contextMenuRenderer: ContextMenuRenderer;
148
166
  @inject(MarkdownRendererFactory) protected readonly markdownRendererFactory: MarkdownRendererFactory;
149
167
  @inject(TerminalCommandHistoryStateFactory) protected readonly commandHistoryStateFactory: TerminalCommandHistoryStateFactory;
168
+ @inject(ContextKeyService) protected readonly contextKeyService: ContextKeyService;
150
169
 
151
170
  protected _markdownRenderer: MarkdownRenderer | undefined;
152
171
  protected get markdownRenderer(): MarkdownRenderer {
@@ -211,6 +230,14 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
211
230
  this.title.caption = initialTitle;
212
231
  this.setIconClass();
213
232
 
233
+ // Declare 'terminalFocus' as a local context key on this widget's DOM scope so that
234
+ // terminal-scoped keybindings (e.g. ctrlcmd+v) take precedence over global bindings
235
+ // for the same keystroke when focus is inside the terminal.
236
+ // See KeybindingRegistry.selectBindingByLocalContext.
237
+ const localContext = this.contextKeyService.createScoped(this.node);
238
+ localContext.createKey('terminalFocus', true);
239
+ this.toDispose.push(localContext);
240
+
214
241
  if (this.options.kind) {
215
242
  this.terminalKind = this.options.kind;
216
243
  }
@@ -306,13 +333,8 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
306
333
  } else {
307
334
  this.exitStatus = { code, reason: TerminalExitReason.Process };
308
335
  }
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
- }
314
336
  if (!attached) {
315
- this.dispose();
337
+ this.deferredFinalizeCommandHistory();
316
338
  }
317
339
  }
318
340
  }));
@@ -525,6 +547,28 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
525
547
  this.promptStartMarker = undefined;
526
548
  }
527
549
 
550
+ protected connectionClosed = false;
551
+ protected waitingForConnectionCloseToDispose = false;
552
+
553
+ protected finalizeAndDispose(): void {
554
+ // enqueue a callback after all pending xterm writes drain
555
+ this.term.write('', () => {
556
+ if (this.isDisposed) { return; }
557
+ if (this._commandHistoryState?.currentCommand) {
558
+ this.finishCurrentCommand();
559
+ }
560
+ this.dispose();
561
+ });
562
+ }
563
+
564
+ protected deferredFinalizeCommandHistory(): void {
565
+ if (this.connectionClosed) {
566
+ this.finalizeAndDispose();
567
+ } else {
568
+ this.waitingForConnectionCloseToDispose = true;
569
+ }
570
+ }
571
+
528
572
  private addCommandSeparator(): void {
529
573
  const marker = this.term.registerMarker(0);
530
574
  if (!marker) {
@@ -740,6 +784,10 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
740
784
  return this.term.hasSelection();
741
785
  }
742
786
 
787
+ paste(text: string): void {
788
+ this.term.paste(text);
789
+ }
790
+
743
791
  async hasChildProcesses(): Promise<boolean> {
744
792
  return this.shellTerminalServer.hasChildProcesses(await this.processId);
745
793
  }
@@ -902,6 +950,8 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
902
950
  }
903
951
  this.toDisposeOnConnect.dispose();
904
952
  this.toDispose.push(this.toDisposeOnConnect);
953
+ this.connectionClosed = false;
954
+ this.waitingForConnectionCloseToDispose = false;
905
955
  const waitForConnection = this.waitForConnection = new Deferred<Channel>();
906
956
  this.connectionProvider.listen(
907
957
  `${terminalsPath}/${this.terminalId}`,
@@ -921,7 +971,13 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
921
971
  disposable.push(this.term.onData(sendData));
922
972
  disposable.push(this.term.onBinary(sendData));
923
973
 
924
- connection.onClose(() => disposable.dispose());
974
+ connection.onClose(() => {
975
+ disposable.dispose();
976
+ this.connectionClosed = true;
977
+ if (this.waitingForConnectionCloseToDispose) {
978
+ this.finalizeAndDispose();
979
+ }
980
+ });
925
981
 
926
982
  if (waitForConnection) {
927
983
  waitForConnection.resolve(connection);