Skip to content

Commit 9edd2b3

Browse files
Copilotedvilme
andauthored
Show the snake icon on extension-created Python terminal REPLs (#26141)
<img width="2113" height="494" alt="image" src="https://github.com/user-attachments/assets/7dc82d66-60ca-43e8-9cc1-9d3fa44e1a25" /> Fixes #21996 Terminal REPLs launched by the extension only set a title, so VS Code fell back to the default shell/profile icon. This threads an optional `iconPath` through the terminal creation pipeline and sets `ThemeIcon('snake')` for the REPL only. ### Changes - **`common/terminal/types.ts`**: `TerminalCreationOptions` gains `iconPath?: TerminalOptions['iconPath']`, keeping the internal type aligned with the VS Code API. - **`common/terminal/service.ts`**: `ensureTerminal` forwards `iconPath` on both creation paths — `ensureTerminalLegacy(...)` (env extension, whose options extend `TerminalOptions`) and `terminalManager.createTerminal(...)`. - **`terminals/codeExecution/terminalCodeExecution.ts`**: new `protected terminalIcon?: TerminalOptions['iconPath']`, passed to `terminalServiceFactory.getTerminalService(...)` alongside the title. - **`terminals/codeExecution/repl.ts`**: sets the icon, so plain Python terminals, Django shells, native REPL tabs, and user-typed `python` sessions are untouched. ```ts // repl.ts this.terminalTitle = 'REPL'; this.terminalIcon = new ThemeIcon('snake'); ``` ### Tests - `terminalCodeExec.unit.test.ts`: the title matcher now also asserts the expected icon id per suite — `snake` for `ReplProvider`, none for the Terminal and Django providers. - `service.unit.test.ts`: two tests assert `iconPath` reaches `terminalManager.createTerminal(...)` and, with `useEnvExtension()` stubbed on, `ensureTerminalLegacy(...)`. The env-extension test stubs `ensureTerminalLegacy` with a plain object rather than a TypeMoq mock: dynamic mocks answer any property access, so `await`ing one hangs on the phantom `then`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
1 parent 71d2eef commit 9edd2b3

7 files changed

Lines changed: 60 additions & 7 deletions

File tree

src/client/common/terminal/service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,15 @@ export class TerminalService implements ITerminalService, Disposable {
219219
this.terminal = await ensureTerminalLegacy(this.options?.resource, {
220220
name: this.options?.title || 'Python',
221221
hideFromUser: this.options?.hideFromUser,
222+
iconPath: this.options?.iconPath,
222223
});
223224
return;
224225
} else {
225226
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
226227
this.terminal = this.terminalManager.createTerminal({
227228
name: this.options?.title || 'Python',
228229
hideFromUser: this.options?.hideFromUser,
230+
iconPath: this.options?.iconPath,
229231
});
230232
this.terminalAutoActivator.disableAutoActivation(this.terminal);
231233

src/client/common/terminal/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
'use strict';
55

6-
import { CancellationToken, Event, Terminal, Uri, TerminalShellExecution } from 'vscode';
6+
import { CancellationToken, Event, Terminal, TerminalOptions, Uri, TerminalShellExecution } from 'vscode';
77
import { PythonEnvironment } from '../../pythonEnvironments/info';
88
import { IEventNamePropertyMapping } from '../../telemetry/index';
99
import { IDisposable, Resource } from '../types';
@@ -65,6 +65,10 @@ export type TerminalCreationOptions = {
6565
* Object with environment variables that will be added to the Terminal.
6666
*/
6767
env?: { [key: string]: string | null };
68+
/**
69+
* Icon displayed for the terminal.
70+
*/
71+
iconPath?: TerminalOptions['iconPath'];
6872
/**
6973
* Resource identifier. E.g. used to determine python interpreter that needs to be used or environment variables or the like.
7074
*

src/client/terminals/codeExecution/djangoShellCodeExecution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
4646
applicationShell,
4747
);
4848
this.terminalTitle = 'Django Shell';
49+
// The snake icon is reserved for the Python REPL terminal.
50+
this.terminalIcon = undefined;
4951
disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager));
5052
}
5153

src/client/terminals/codeExecution/repl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
'use strict';
55

66
import { inject, injectable } from 'inversify';
7-
import { Disposable } from 'vscode';
7+
import { Disposable, ThemeIcon } from 'vscode';
88
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
99
import { IPlatformService } from '../../common/platform/types';
1010
import { ITerminalServiceFactory } from '../../common/terminal/types';
@@ -35,5 +35,6 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
3535
applicationShell,
3636
);
3737
this.terminalTitle = 'REPL';
38+
this.terminalIcon = new ThemeIcon('snake');
3839
}
3940
}

src/client/terminals/codeExecution/terminalCodeExecution.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { inject, injectable } from 'inversify';
77
import * as path from 'path';
8-
import { Disposable, Uri } from 'vscode';
8+
import { Disposable, TerminalOptions, Uri } from 'vscode';
99
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
1010
import '../../common/extensions';
1111
import { IPlatformService } from '../../common/platform/types';
@@ -24,6 +24,7 @@ import { sendTelemetryEvent } from '../../telemetry';
2424
export class TerminalCodeExecutionProvider implements ICodeExecutionService {
2525
private hasRanOutsideCurrentDrive = false;
2626
protected terminalTitle!: string;
27+
protected terminalIcon?: TerminalOptions['iconPath'];
2728
private replActive?: Promise<boolean>;
2829

2930
constructor(
@@ -128,6 +129,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
128129
return this.terminalServiceFactory.getTerminalService({
129130
resource,
130131
title: this.terminalTitle,
132+
iconPath: this.terminalIcon,
131133
newTerminalPerFile: options?.newTerminalPerFile,
132134
});
133135
}

src/test/common/terminals/service.unit.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { expect } from 'chai';
4+
import { assert, expect } from 'chai';
55
import * as path from 'path';
66
import * as sinon from 'sinon';
77
import * as TypeMoq from 'typemoq';
@@ -11,6 +11,7 @@ import {
1111
TerminalShellExecution,
1212
TerminalShellExecutionEndEvent,
1313
TerminalShellIntegration,
14+
ThemeIcon,
1415
Uri,
1516
Terminal as VSCodeTerminal,
1617
WorkspaceConfiguration,
@@ -29,10 +30,12 @@ import {
2930
import { IDisposableRegistry } from '../../../client/common/types';
3031
import { IServiceContainer } from '../../../client/ioc/types';
3132
import { ITerminalAutoActivation } from '../../../client/terminals/types';
33+
import { noop } from '../../../client/common/utils/misc';
3234
import { createPythonInterpreter } from '../../utils/interpreters';
3335
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
3436
import * as platform from '../../../client/common/utils/platform';
3537
import * as extapi from '../../../client/envExt/api.internal';
38+
import * as extapiLegacy from '../../../client/envExt/api.legacy';
3639
import { IInterpreterService } from '../../../client/interpreter/contracts';
3740
import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
3841

@@ -55,6 +58,7 @@ suite('Terminal Service', () => {
5558
let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>;
5659
let isWindowsStub: sinon.SinonStub;
5760
let useEnvExtensionStub: sinon.SinonStub;
61+
let ensureTerminalLegacyStub: sinon.SinonStub;
5862
let interpreterService: TypeMoq.IMock<IInterpreterService>;
5963
let options: TypeMoq.IMock<TerminalCreationOptions>;
6064
let applicationShell: TypeMoq.IMock<IApplicationShell>;
@@ -64,6 +68,7 @@ suite('Terminal Service', () => {
6468
setup(() => {
6569
useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension');
6670
useEnvExtensionStub.returns(false);
71+
ensureTerminalLegacyStub = sinon.stub(extapiLegacy, 'ensureTerminalLegacy');
6772

6873
terminal = TypeMoq.Mock.ofType<VSCodeTerminal>();
6974
terminalShellIntegration = TypeMoq.Mock.ofType<TerminalShellIntegration>();
@@ -394,6 +399,36 @@ suite('Terminal Service', () => {
394399
terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.never());
395400
});
396401

402+
test('Ensure `iconPath` option is forwarded to the created terminal', async () => {
403+
terminalHelper
404+
.setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
405+
.returns(() => Promise.resolve(undefined));
406+
const iconPath = new ThemeIcon('snake');
407+
service = new TerminalService(mockServiceContainer.object, { iconPath });
408+
terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash);
409+
terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object);
410+
411+
await service.show();
412+
413+
terminalManager.verify(
414+
(t) => t.createTerminal(TypeMoq.It.isObjectWith({ iconPath })),
415+
TypeMoq.Times.atLeastOnce(),
416+
);
417+
});
418+
419+
test('Ensure `iconPath` option is forwarded when the environments extension is used', async () => {
420+
useEnvExtensionStub.returns(true);
421+
const iconPath = new ThemeIcon('snake');
422+
const createdTerminal = ({ show: noop, dispose: noop } as unknown) as VSCodeTerminal;
423+
ensureTerminalLegacyStub.resolves(createdTerminal);
424+
service = new TerminalService(mockServiceContainer.object, { iconPath });
425+
426+
await service.show();
427+
428+
assert.ok(ensureTerminalLegacyStub.calledOnce);
429+
assert.strictEqual(ensureTerminalLegacyStub.firstCall.args[1].iconPath, iconPath);
430+
});
431+
397432
test('Ensure terminal shown otherwise', async () => {
398433
terminalHelper
399434
.setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))

src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { expect } from 'chai';
55
import * as path from 'path';
66
import { SemVer } from 'semver';
77
import * as TypeMoq from 'typemoq';
8-
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
8+
import { Disposable, ThemeIcon, Uri, WorkspaceFolder } from 'vscode';
99
import {
1010
IApplicationShell,
1111
ICommandManager,
@@ -45,6 +45,7 @@ suite('Terminal - Code Execution', () => {
4545
let disposables: Disposable[] = [];
4646
let executor: ICodeExecutionService;
4747
let expectedTerminalTitle: string | undefined;
48+
let expectedTerminalIconId: string | undefined;
4849
let terminalFactory: TypeMoq.IMock<ITerminalServiceFactory>;
4950
let documentManager: TypeMoq.IMock<IDocumentManager>;
5051
let commandManager: TypeMoq.IMock<ICommandManager>;
@@ -81,7 +82,6 @@ suite('Terminal - Code Execution', () => {
8182
settings = TypeMoq.Mock.ofType<IPythonSettings>();
8283
settings.setup((s) => s.terminal).returns(() => terminalSettings.object);
8384
configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object);
84-
8585
switch (testSuiteName) {
8686
case 'Terminal Execution': {
8787
executor = new TerminalCodeExecutionProvider(
@@ -108,6 +108,7 @@ suite('Terminal - Code Execution', () => {
108108
applicationShell.object,
109109
);
110110
expectedTerminalTitle = 'REPL';
111+
expectedTerminalIconId = 'snake';
111112
break;
112113
}
113114
case 'Django Execution': {
@@ -132,6 +133,7 @@ suite('Terminal - Code Execution', () => {
132133
applicationShell.object,
133134
);
134135
expectedTerminalTitle = 'Django Shell';
136+
expectedTerminalIconId = undefined;
135137
break;
136138
}
137139
default: {
@@ -145,7 +147,12 @@ suite('Terminal - Code Execution', () => {
145147
terminalFactory
146148
.setup((f) =>
147149
f.getTerminalService(
148-
TypeMoq.It.is<TerminalCreationOptions>((a) => a.title === expectedTerminalTitle),
150+
TypeMoq.It.is<TerminalCreationOptions>(
151+
(a) =>
152+
a.title === expectedTerminalTitle &&
153+
(a.iconPath instanceof ThemeIcon ? a.iconPath.id : undefined) ===
154+
expectedTerminalIconId,
155+
),
149156
),
150157
)
151158
.returns(() => terminalService.object);

0 commit comments

Comments
 (0)