Skip to content

Commit b6af1d8

Browse files
committed
Support for terminal profiles. Fixes eclipse-theia#11503
- UI and service to manage terminal profiles - Handle profiles in preferences according to VS Code schema - API and contribution markup for contributing profiles and activation event handling contributed on behalf of STMicroelectronics Signed-off-by: Thomas Mäder <[email protected]>
1 parent 911c0ca commit b6af1d8

26 files changed

+958
-64
lines changed

packages/core/i18n/nls.de.json

+12
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,24 @@
447447
"confirmCloseNever": "Niemals bestätigen.",
448448
"enableCopy": "Aktivieren von ctrl-c (cmd-c unter macOS) zum Kopieren von markiertem Text",
449449
"enablePaste": "Aktivieren von ctrl-v (cmd-v unter macOS) zum Einfügen aus der Zwischenablage",
450+
"profileNew": "Neues Terminal...",
451+
"profileDefault": "Standardprofil wählen...",
452+
"selectProfile": "Wählen Sie ein Profil für das neue Terminal",
450453
"shellArgsLinux": "Die Befehlszeilenargumente, die im Linux-Terminal zu verwenden sind.",
451454
"shellArgsOsx": "Die Befehlszeilenargumente, die im macOS-Terminal zu verwenden sind.",
452455
"shellArgsWindows": "Die Befehlszeilenargumente, die im Windows-Terminal zu verwenden sind.",
453456
"shellLinux": "Der Pfad der Shell, die das Terminal unter Linux verwendet (Standard: '{0}'}).",
454457
"shellOsx": "Der Pfad der Shell, die das Terminal unter macOS verwendet (Standard: '{0}'}).",
455458
"shellWindows": "Der Pfad der Shell, die das Terminal unter Windows verwendet. (Standard: '{0}').",
459+
"shell.deprecated": "Dies ist veraltet, neu können Sie Ihre Shell konfigurieren, indem Sie ein Profil unter 'terminal.integrated.profiles.{0}' anlegen und dessen Namen in 'terminal.integrated.defaultProfile.{0}' als Standard setzen.",
460+
"defaultProfile": "Das Standardprofil unter {0}",
461+
"profiles": "Die Profile welche zur Erzeugung eines Terminals verwendet werden können. Setzen Sie den Pfad von Hand mit optionalen Parametern.\n\nSezen Sie ein Profile auf `null` um es zu verbergen, z.B.: `{0}: null`.",
462+
"profilePath": "Der Pfad der Shell, den dieses Profil benutzt.",
463+
"profileSource": "Eine Profilquelle, die Shellpfade automatisch erkennt. Unübliche Installationsorte werden nicht unterstützt und müssen manuell erfasst werden",
464+
"profileArgs": "Die Shellparameter, welche dieses Profil verwendet.",
465+
"profileEnv": "Ein Objekt mit Umgebungsvariablen die zum Terminalprozess hinzugefügt werden. Setzen Sie Variablen auf `null` um sie aus der Basisumgebung zu löschen",
466+
"profileIcon": "Eine codicon ID zur Verwendung mit diesem Terminal. \nterminal-tmux:\"$(terminal-tmux)\"",
467+
"profileColor": "ID einer Terminal-Themenfarbe zur Verwendung mit diesem Terminal.",
456468
"terminate": "Ende",
457469
"terminateActive": "Möchten Sie die aktive Terminalsitzung beenden?",
458470
"terminateActiveMultiple": "Sollen die {0} aktiven Terminalsitzungen beendet werden?"

packages/core/i18n/nls.json

+12
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,24 @@
445445
"confirmCloseNever": "Never confirm.",
446446
"enableCopy": "Enable ctrl-c (cmd-c on macOS) to copy selected text",
447447
"enablePaste": "Enable ctrl-v (cmd-v on macOS) to paste from clipboard",
448+
"profileNew": "New Terminal...",
449+
"profileDefault": "Choose Default Profile...",
450+
"selectProfile": "Select a profile for the new terminal",
448451
"shellArgsLinux": "The command line arguments to use when on the Linux terminal.",
449452
"shellArgsOsx": "The command line arguments to use when on the macOS terminal.",
450453
"shellArgsWindows": "The command line arguments to use when on the Windows terminal.",
451454
"shellLinux": "The path of the shell that the terminal uses on Linux (default: '{0}'}).",
452455
"shellOsx": "The path of the shell that the terminal uses on macOS (default: '{0}'}).",
453456
"shellWindows": "The path of the shell that the terminal uses on Windows. (default: '{0}').",
457+
"shell.deprecated": "This is deprecated, the new recommended way to configure your default shell is by creating a terminal profile in 'terminal.integrated.profiles.{0}' and setting its profile name as the default in 'terminal.integrated.defaultProfile.{0}'.",
458+
"defaultProfile": "The default profile used on {0}",
459+
"profiles": "The profiles to present when creating a new terminal. Set the path property manually with optional args. \n\nSet an existing profile to `null` to hide the profile from the list, for example: `{0}: null`.",
460+
"profilePath": "The path of the shell that this profile uses.",
461+
"profileSource": "A profile source that will auto detect the paths to the shell. Note that non-standard executable locations are not supported and must be created manually in a new profile.",
462+
"profileArgs": "The shell arguments that this profile uses.",
463+
"profileEnv": "An object with environment variables that will be added to the terminal profile process. Set to `null` to delete environment variables from the base environment.",
464+
"profileIcon": "A codicon ID to associate with the terminal icon. \nterminal-tmux:\"$(terminal-tmux)\"",
465+
"profileColor": "A terminal theme color ID to associate with the terminal.",
454466
"terminate": "Terminate",
455467
"terminateActive": "Do you want to terminate the active terminal session?",
456468
"terminateActiveMultiple": "Do you want to terminate the {0} active terminal sessions?"

packages/core/src/common/uri.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class URI {
2323
return new URI(Uri.revive(components));
2424
}
2525

26+
public static fromFilePath(path: string): URI {
27+
return new URI(Uri.file(path));
28+
}
29+
2630
private readonly codeUri: Uri;
2731
private _path: Path | undefined;
2832

packages/plugin-ext/src/common/plugin-api-rpc.ts

+1
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ export interface CommandRegistryExt {
269269
}
270270

271271
export interface TerminalServiceExt {
272+
$startProfile(providerId: string, cancellationToken: theia.CancellationToken): Promise<string>;
272273
$terminalCreated(id: string, name: string): void;
273274
$terminalNameChanged(id: string, name: string): void;
274275
$terminalOpened(id: string, processId: number, terminalId: number, cols: number, rows: number): void;

packages/plugin-ext/src/common/plugin-protocol.ts

+18
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ export interface PluginPackageContribution {
9595
jsonValidation?: PluginJsonValidationContribution[];
9696
resourceLabelFormatters?: ResourceLabelFormatter[];
9797
localizations?: PluginPackageLocalization[];
98+
terminal?: PluginPackageTerminal;
99+
}
100+
101+
export interface PluginPackageTerminalProfile {
102+
title: string,
103+
id: string,
104+
icon?: string
105+
}
106+
107+
export interface PluginPackageTerminal {
108+
profiles: PluginPackageTerminalProfile[]
98109
}
99110

100111
export interface PluginPackageLocalization {
@@ -555,6 +566,13 @@ export interface PluginContribution {
555566
problemPatterns?: ProblemPatternContribution[];
556567
resourceLabelFormatters?: ResourceLabelFormatter[];
557568
localizations?: Localization[];
569+
terminalProfiles?: TerminalProfile[];
570+
}
571+
572+
export interface TerminalProfile {
573+
title: string,
574+
id: string,
575+
icon?: string
558576
}
559577

560578
export interface Localization {

packages/plugin-ext/src/hosted/browser/hosted-plugin.ts

+4
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,10 @@ export class HostedPluginSupport {
616616
return this.activateByEvent(`onFileSystem:${event.scheme}`);
617617
}
618618

619+
activateByTerminalProfile(profileId: string): Promise<void> {
620+
return this.activateByEvent(`onTerminalProfile:${profileId}`);
621+
}
622+
619623
protected ensureFileSystemActivation(event: FileSystemProviderActivationEvent): void {
620624
event.waitUntil(this.activateByFileSystem(event));
621625
}

packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ import {
5959
Localization,
6060
PluginPackageTranslation,
6161
Translation,
62-
PluginIdentifiers
62+
PluginIdentifiers,
63+
TerminalProfile
6364
} from '../../../common/plugin-protocol';
6465
import * as fs from 'fs';
6566
import * as path from 'path';
@@ -358,9 +359,22 @@ export class TheiaPluginScanner implements PluginScanner {
358359
console.error(`Could not read '${rawPlugin.name}' contribution 'localizations'.`, rawPlugin.contributes.colors, err);
359360
}
360361

362+
try {
363+
contributions.terminalProfiles = this.readTerminals(rawPlugin);
364+
} catch (err) {
365+
console.error(`Could not read '${rawPlugin.name}' contribution 'terminals'.`, rawPlugin.contributes.terminal, err);
366+
}
367+
361368
return contributions;
362369
}
363370

371+
protected readTerminals(pck: PluginPackage): TerminalProfile[] | undefined {
372+
if (!pck?.contributes?.terminal?.profiles) {
373+
return undefined;
374+
}
375+
return pck.contributes.terminal.profiles.filter(profile => profile.id && profile.title);
376+
}
377+
364378
protected readLocalizations(pck: PluginPackage): Localization[] | undefined {
365379
if (!pck.contributes || !pck.contributes.localizations) {
366380
return undefined;

packages/plugin-ext/src/main/browser/plugin-contribution-handler.ts

+35
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ import { PluginIconThemeService } from './plugin-icon-theme-service';
4343
import { ContributionProvider } from '@theia/core/lib/common';
4444
import * as monaco from '@theia/monaco-editor-core';
4545
import { ThemeIcon } from '@theia/monaco-editor-core/esm/vs/platform/theme/common/themeService';
46+
import { ContributedTerminalProfileStore, TerminalProfileStore } from '@theia/terminal/lib/browser/terminal-profile-service';
47+
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
48+
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
49+
import { PluginTerminalRegistry } from './plugin-terminal-registry';
4650

4751
@injectable()
4852
export class PluginContributionHandler {
@@ -106,6 +110,15 @@ export class PluginContributionHandler {
106110
@inject(PluginIconThemeService)
107111
protected readonly iconThemeService: PluginIconThemeService;
108112

113+
@inject(TerminalService)
114+
protected readonly terminalService: TerminalService;
115+
116+
@inject(PluginTerminalRegistry)
117+
protected readonly pluginTerminalRegistry: PluginTerminalRegistry;
118+
119+
@inject(ContributedTerminalProfileStore)
120+
protected readonly contributedProfileStore: TerminalProfileStore;
121+
109122
@inject(ContributionProvider) @named(LabelProviderContribution)
110123
protected readonly contributionProvider: ContributionProvider<LabelProviderContribution>;
111124

@@ -356,6 +369,28 @@ export class PluginContributionHandler {
356369
}
357370
}
358371

372+
const self = this;
373+
if (contributions.terminalProfiles) {
374+
for (const profile of contributions.terminalProfiles) {
375+
pushContribution(`terminalProfiles.${profile.id}`, () => {
376+
this.contributedProfileStore.registerTerminalProfile(profile.title, {
377+
async start(): Promise<TerminalWidget> {
378+
const terminalId = await self.pluginTerminalRegistry.start(profile.id);
379+
const result = self.terminalService.getById(terminalId);
380+
if (!result) {
381+
throw new Error(`Error starting terminal from profile ${profile.id}`);
382+
}
383+
return result;
384+
385+
}
386+
});
387+
return Disposable.create(() => {
388+
this.contributedProfileStore.unregisterTerminalProfile(profile.id);
389+
});
390+
});
391+
}
392+
}
393+
359394
return toDispose;
360395
}
361396

packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import { bindTreeViewDecoratorUtilities, TreeViewDecoratorService } from './view
8080
import { CodeEditorWidgetUtil } from './menus/vscode-theia-menu-mappings';
8181
import { PluginMenuCommandAdapter } from './menus/plugin-menu-command-adapter';
8282
import './theme-icon-override';
83+
import { PluginTerminalRegistry } from './plugin-terminal-registry';
8384

8485
export default new ContainerModule((bind, unbind, isBound, rebind) => {
8586

@@ -240,4 +241,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
240241

241242
bind(PluginAuthenticationServiceImpl).toSelf().inSingletonScope();
242243
rebind(AuthenticationService).toService(PluginAuthenticationServiceImpl);
244+
245+
bind(PluginTerminalRegistry).toSelf().inSingletonScope();
243246
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2022 STMicroelectronics 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 WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { injectable } from '@theia/core/shared/inversify';
18+
19+
@injectable()
20+
export class PluginTerminalRegistry {
21+
22+
startCallback: (id: string) => Promise<string>;
23+
24+
start(profileId: string): Promise<string> {
25+
return this.startCallback(profileId);
26+
}
27+
}

packages/plugin-ext/src/main/browser/terminal-main.ts

+34-25
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
import { interfaces } from '@theia/core/shared/inversify';
1818
import { ApplicationShell, WidgetOpenerOptions } from '@theia/core/lib/browser';
19-
import { CancellationToken } from '@theia/core/shared/vscode-languageserver-protocol';
2019
import { TerminalEditorLocationOptions, TerminalOptions } from '@theia/plugin';
2120
import { TerminalLocation, TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
2221
import { TerminalService } from '@theia/terminal/lib/browser/base/terminal-service';
@@ -27,13 +26,18 @@ import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/c
2726
import { ShellTerminalServerProxy } from '@theia/terminal/lib/common/shell-terminal-protocol';
2827
import { TerminalLink, TerminalLinkProvider } from '@theia/terminal/lib/browser/terminal-link-provider';
2928
import { URI } from '@theia/core/lib/common/uri';
29+
import { PluginTerminalRegistry } from './plugin-terminal-registry';
30+
import { CancellationToken } from '@theia/core';
31+
import { HostedPluginSupport } from '../../hosted/browser/hosted-plugin';
3032

3133
/**
3234
* Plugin api service allows working with terminal emulator.
3335
*/
3436
export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLinkProvider, Disposable {
3537

3638
private readonly terminals: TerminalService;
39+
private readonly pluginTerminalRegistry: PluginTerminalRegistry;
40+
private readonly hostedPluginSupport: HostedPluginSupport;
3741
private readonly shell: ApplicationShell;
3842
private readonly extProxy: TerminalServiceExt;
3943
private readonly shellTerminalServer: ShellTerminalServerProxy;
@@ -43,6 +47,8 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
4347

4448
constructor(rpc: RPCProtocol, container: interfaces.Container) {
4549
this.terminals = container.get(TerminalService);
50+
this.pluginTerminalRegistry = container.get(PluginTerminalRegistry);
51+
this.hostedPluginSupport = container.get(HostedPluginSupport);
4652
this.shell = container.get(ApplicationShell);
4753
this.shellTerminalServer = container.get(ShellTerminalServerProxy);
4854
this.extProxy = rpc.getProxy(MAIN_RPC_CONTEXT.TERMINAL_EXT);
@@ -58,9 +64,16 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
5864
this.extProxy.$initEnvironmentVariableCollections(serializedCollections);
5965
}
6066

67+
this.pluginTerminalRegistry.startCallback = id => this.startProfile(id);
68+
6169
container.bind(TerminalLinkProvider).toDynamicValue(() => this);
6270
}
6371

72+
async startProfile(id: string): Promise<string> {
73+
await this.hostedPluginSupport.activateByTerminalProfile(id);
74+
return this.extProxy.$startProfile(id, CancellationToken.None);
75+
}
76+
6477
$setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: SerializableEnvironmentVariableCollection | undefined): void {
6578
if (collection) {
6679
this.shellTerminalServer.setCollection(extensionIdentifier, persistent, collection);
@@ -123,31 +136,27 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
123136
}
124137

125138
async $createTerminal(id: string, options: TerminalOptions, parentId?: string, isPseudoTerminal?: boolean): Promise<string> {
126-
try {
127-
const terminal = await this.terminals.newTerminal({
128-
id,
129-
title: options.name,
130-
shellPath: options.shellPath,
131-
shellArgs: options.shellArgs,
132-
cwd: options.cwd ? new URI(options.cwd) : undefined,
133-
env: options.env,
134-
strictEnv: options.strictEnv,
135-
destroyTermOnClose: true,
136-
useServerTitle: false,
137-
attributes: options.attributes,
138-
hideFromUser: options.hideFromUser,
139-
location: this.getTerminalLocation(options, parentId),
140-
isPseudoTerminal,
141-
isTransient: options.isTransient
142-
});
143-
if (options.message) {
144-
terminal.writeLine(options.message);
145-
}
146-
terminal.start();
147-
return terminal.id;
148-
} catch (error) {
149-
throw new Error('Failed to create terminal. Cause: ' + error);
139+
const terminal = await this.terminals.newTerminal({
140+
id,
141+
title: options.name,
142+
shellPath: options.shellPath,
143+
shellArgs: options.shellArgs,
144+
cwd: options.cwd ? new URI(options.cwd) : undefined,
145+
env: options.env,
146+
strictEnv: options.strictEnv,
147+
destroyTermOnClose: true,
148+
useServerTitle: false,
149+
attributes: options.attributes,
150+
hideFromUser: options.hideFromUser,
151+
location: this.getTerminalLocation(options, parentId),
152+
isPseudoTerminal,
153+
isTransient: options.isTransient
154+
});
155+
if (options.message) {
156+
terminal.writeLine(options.message);
150157
}
158+
terminal.start();
159+
return terminal.id;
151160
}
152161

153162
protected getTerminalLocation(options: TerminalOptions, parentId?: string): TerminalLocation | TerminalEditorLocationOptions | { parentTerminal: string; } | undefined {

packages/plugin-ext/src/plugin/plugin-context.ts

+5
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ import {
153153
InputBoxValidationSeverity,
154154
TerminalLink,
155155
TerminalLocation,
156+
TerminalProfile,
156157
InlayHint,
157158
InlayHintKind,
158159
InlayHintLabelPart,
@@ -547,6 +548,9 @@ export function createAPIFactory(
547548
registerTerminalLinkProvider(provider: theia.TerminalLinkProvider): theia.Disposable {
548549
return terminalExt.registerTerminalLinkProvider(provider);
549550
},
551+
registerTerminalProfileProvider(id: string, provider: theia.TerminalProfileProvider): theia.Disposable {
552+
return terminalExt.registerTerminalProfileProvider(id, provider);
553+
},
550554
get activeColorTheme(): theia.ColorTheme {
551555
return themingExt.activeColorTheme;
552556
},
@@ -1250,6 +1254,7 @@ export function createAPIFactory(
12501254
SourceControlInputBoxValidationType,
12511255
FileDecoration,
12521256
TerminalLink,
1257+
TerminalProfile,
12531258
CancellationError,
12541259
ExtensionMode,
12551260
LinkedEditingRanges,

packages/plugin-ext/src/plugin/plugin-manager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class PluginManagerExtImpl implements PluginManagerExt, PluginManager {
8989
'workspaceContains',
9090
'onView',
9191
'onUri',
92+
'onTerminalProfile',
9293
'onWebviewPanel',
9394
'onFileSystem',
9495
'onCustomEditor',

0 commit comments

Comments
 (0)