@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.
- package/lib/browser/base/terminal-service.d.ts +1 -1
- package/lib/browser/base/terminal-service.d.ts.map +1 -1
- package/lib/browser/base/terminal-widget.d.ts +6 -0
- package/lib/browser/base/terminal-widget.d.ts.map +1 -1
- package/lib/browser/base/terminal-widget.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/search/terminal-search-widget.js +2 -2
- package/lib/browser/search/terminal-search-widget.js.map +1 -1
- package/lib/browser/terminal-creation-handler.d.ts +35 -0
- package/lib/browser/terminal-creation-handler.d.ts.map +1 -0
- package/lib/browser/terminal-creation-handler.js +33 -0
- package/lib/browser/terminal-creation-handler.js.map +1 -0
- package/lib/browser/terminal-file-link-provider.d.ts +2 -1
- package/lib/browser/terminal-file-link-provider.d.ts.map +1 -1
- package/lib/browser/terminal-file-link-provider.js +7 -2
- package/lib/browser/terminal-file-link-provider.js.map +1 -1
- package/lib/browser/terminal-frontend-contribution.d.ts +10 -1
- package/lib/browser/terminal-frontend-contribution.d.ts.map +1 -1
- package/lib/browser/terminal-frontend-contribution.js +76 -53
- 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 +5 -0
- package/lib/browser/terminal-frontend-module.js.map +1 -1
- package/lib/browser/terminal-shell-handler.d.ts +19 -0
- package/lib/browser/terminal-shell-handler.d.ts.map +1 -0
- package/lib/browser/terminal-shell-handler.js +95 -0
- package/lib/browser/terminal-shell-handler.js.map +1 -0
- package/lib/browser/terminal-widget-impl.d.ts +21 -0
- package/lib/browser/terminal-widget-impl.d.ts.map +1 -1
- package/lib/browser/terminal-widget-impl.js +59 -7
- package/lib/browser/terminal-widget-impl.js.map +1 -1
- package/lib/node/buffering-stream.d.ts +2 -0
- package/lib/node/buffering-stream.d.ts.map +1 -1
- package/lib/node/buffering-stream.js +12 -0
- package/lib/node/buffering-stream.js.map +1 -1
- package/lib/node/buffering-stream.spec.js +27 -0
- package/lib/node/buffering-stream.spec.js.map +1 -1
- package/lib/node/shell-terminal-server.d.ts +7 -0
- package/lib/node/shell-terminal-server.d.ts.map +1 -1
- package/lib/node/shell-terminal-server.js +69 -0
- package/lib/node/shell-terminal-server.js.map +1 -1
- package/lib/node/shell-terminal-server.spec.js +99 -0
- package/lib/node/shell-terminal-server.spec.js.map +1 -1
- package/lib/node/terminal-backend-contribution.d.ts.map +1 -1
- package/lib/node/terminal-backend-contribution.js +5 -0
- package/lib/node/terminal-backend-contribution.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/base/terminal-service.ts +1 -1
- package/src/browser/base/terminal-widget.ts +7 -0
- package/src/browser/index.ts +1 -0
- package/src/browser/search/terminal-search-widget.tsx +2 -2
- package/src/browser/terminal-creation-handler.ts +53 -0
- package/src/browser/terminal-file-link-provider.ts +6 -4
- package/src/browser/terminal-frontend-contribution.ts +79 -57
- package/src/browser/terminal-frontend-module.ts +5 -0
- package/src/browser/terminal-shell-handler.ts +89 -0
- package/src/browser/terminal-widget-impl.ts +64 -8
- package/src/node/buffering-stream.spec.ts +30 -0
- package/src/node/buffering-stream.ts +13 -0
- package/src/node/shell-terminal-server.spec.ts +119 -1
- package/src/node/shell-terminal-server.ts +70 -0
- 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
|
|
3
|
+
"version": "1.73.0",
|
|
4
4
|
"description": "Theia - Terminal Extension",
|
|
5
5
|
"dependencies": {
|
|
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
|
|
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.
|
|
54
|
+
"@theia/ext-scripts": "1.73.0"
|
|
55
55
|
},
|
|
56
56
|
"nyc": {
|
|
57
57
|
"extends": "../../configs/nyc.json"
|
|
58
58
|
},
|
|
59
|
-
"gitHead": "
|
|
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;
|
package/src/browser/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
676
|
-
if (terminal
|
|
677
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
1007
|
+
created: nextTerminalCreationToken(),
|
|
950
1008
|
...options
|
|
951
1009
|
});
|
|
952
1010
|
return widget;
|
|
953
1011
|
}
|
|
954
1012
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
const
|
|
959
|
-
|
|
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
|
-
|
|
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.
|
|
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(() =>
|
|
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);
|