Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Show snake icon for extension-created Python terminal REPL
Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com>
  • Loading branch information
Copilot and edvilme authored Sep 3, 2026
commit fb11ccb7802b1873ceeec6a954566035fa43cf5a
2 changes: 2 additions & 0 deletions src/client/common/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,15 @@ export class TerminalService implements ITerminalService, Disposable {
this.terminal = await ensureTerminalLegacy(this.options?.resource, {
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
iconPath: this.options?.iconPath,
});
return;
} else {
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
this.terminal = this.terminalManager.createTerminal({
name: this.options?.title || 'Python',
hideFromUser: this.options?.hideFromUser,
iconPath: this.options?.iconPath,
});
this.terminalAutoActivator.disableAutoActivation(this.terminal);

Expand Down
6 changes: 5 additions & 1 deletion src/client/common/terminal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

'use strict';

import { CancellationToken, Event, Terminal, Uri, TerminalShellExecution } from 'vscode';
import { CancellationToken, Event, Terminal, TerminalOptions, Uri, TerminalShellExecution } from 'vscode';
import { PythonEnvironment } from '../../pythonEnvironments/info';
import { IEventNamePropertyMapping } from '../../telemetry/index';
import { IDisposable, Resource } from '../types';
Expand Down Expand Up @@ -65,6 +65,10 @@ export type TerminalCreationOptions = {
* Object with environment variables that will be added to the Terminal.
*/
env?: { [key: string]: string | null };
/**
* Icon displayed for the terminal.
*/
iconPath?: TerminalOptions['iconPath'];
/**
* Resource identifier. E.g. used to determine python interpreter that needs to be used or environment variables or the like.
*
Expand Down
3 changes: 2 additions & 1 deletion src/client/terminals/codeExecution/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'use strict';

import { inject, injectable } from 'inversify';
import { Disposable } from 'vscode';
import { Disposable, ThemeIcon } from 'vscode';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
import { IPlatformService } from '../../common/platform/types';
import { ITerminalServiceFactory } from '../../common/terminal/types';
Expand Down Expand Up @@ -35,5 +35,6 @@ export class ReplProvider extends TerminalCodeExecutionProvider {
applicationShell,
);
this.terminalTitle = 'REPL';
this.terminalIcon = new ThemeIcon('snake');
}
}
4 changes: 3 additions & 1 deletion src/client/terminals/codeExecution/terminalCodeExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Disposable, Uri } from 'vscode';
import { Disposable, TerminalOptions, Uri } from 'vscode';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { IPlatformService } from '../../common/platform/types';
Expand All @@ -24,6 +24,7 @@ import { sendTelemetryEvent } from '../../telemetry';
export class TerminalCodeExecutionProvider implements ICodeExecutionService {
private hasRanOutsideCurrentDrive = false;
protected terminalTitle!: string;
protected terminalIcon?: TerminalOptions['iconPath'];
private replActive?: Promise<boolean>;
Comment thread
edvilme marked this conversation as resolved.

constructor(
Expand Down Expand Up @@ -128,6 +129,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
return this.terminalServiceFactory.getTerminalService({
resource,
title: this.terminalTitle,
iconPath: this.terminalIcon,
newTerminalPerFile: options?.newTerminalPerFile,
});
}
Expand Down
35 changes: 34 additions & 1 deletion src/test/common/terminals/service.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { expect } from 'chai';
import { assert, expect } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import * as TypeMoq from 'typemoq';
Expand All @@ -11,6 +11,7 @@ import {
TerminalShellExecution,
TerminalShellExecutionEndEvent,
TerminalShellIntegration,
ThemeIcon,
Uri,
Terminal as VSCodeTerminal,
WorkspaceConfiguration,
Expand All @@ -33,6 +34,7 @@ import { createPythonInterpreter } from '../../utils/interpreters';
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
import * as platform from '../../../client/common/utils/platform';
import * as extapi from '../../../client/envExt/api.internal';
import * as extapiLegacy from '../../../client/envExt/api.legacy';
import { IInterpreterService } from '../../../client/interpreter/contracts';
import { PythonEnvironment } from '../../../client/pythonEnvironments/info';

Expand All @@ -55,6 +57,7 @@ suite('Terminal Service', () => {
let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>;
let isWindowsStub: sinon.SinonStub;
let useEnvExtensionStub: sinon.SinonStub;
let ensureTerminalLegacyStub: sinon.SinonStub;
let interpreterService: TypeMoq.IMock<IInterpreterService>;
let options: TypeMoq.IMock<TerminalCreationOptions>;
let applicationShell: TypeMoq.IMock<IApplicationShell>;
Expand All @@ -64,6 +67,7 @@ suite('Terminal Service', () => {
setup(() => {
useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension');
useEnvExtensionStub.returns(false);
ensureTerminalLegacyStub = sinon.stub(extapiLegacy, 'ensureTerminalLegacy');

terminal = TypeMoq.Mock.ofType<VSCodeTerminal>();
terminalShellIntegration = TypeMoq.Mock.ofType<TerminalShellIntegration>();
Expand Down Expand Up @@ -394,6 +398,35 @@ suite('Terminal Service', () => {
terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.never());
});

test('Ensure `iconPath` option is forwarded to the created terminal', async () => {
terminalHelper
.setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(() => Promise.resolve(undefined));
const iconPath = new ThemeIcon('snake');
service = new TerminalService(mockServiceContainer.object, { iconPath });
terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash);
terminalManager
.setup((t) => t.createTerminal(TypeMoq.It.is((o) => o.iconPath === iconPath)))
.returns(() => terminal.object)
.verifiable(TypeMoq.Times.once());

await service.show();

terminalManager.verifyAll();
});

test('Ensure `iconPath` option is forwarded when the environments extension is used', async () => {
useEnvExtensionStub.returns(true);
const iconPath = new ThemeIcon('snake');
ensureTerminalLegacyStub.returns(Promise.resolve(terminal.object));
service = new TerminalService(mockServiceContainer.object, { iconPath });

await service.show();

assert.ok(ensureTerminalLegacyStub.calledOnce);
assert.strictEqual(ensureTerminalLegacyStub.firstCall.args[1].iconPath, iconPath);
});

test('Ensure terminal shown otherwise', async () => {
terminalHelper
.setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
Expand Down
12 changes: 10 additions & 2 deletions src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { expect } from 'chai';
import * as path from 'path';
import { SemVer } from 'semver';
import * as TypeMoq from 'typemoq';
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
import { Disposable, ThemeIcon, Uri, WorkspaceFolder } from 'vscode';
import {
IApplicationShell,
ICommandManager,
Expand Down Expand Up @@ -45,6 +45,7 @@ suite('Terminal - Code Execution', () => {
let disposables: Disposable[] = [];
let executor: ICodeExecutionService;
let expectedTerminalTitle: string | undefined;
let expectedTerminalIconId: string | undefined;
let terminalFactory: TypeMoq.IMock<ITerminalServiceFactory>;
let documentManager: TypeMoq.IMock<IDocumentManager>;
let commandManager: TypeMoq.IMock<ICommandManager>;
Expand Down Expand Up @@ -81,6 +82,7 @@ suite('Terminal - Code Execution', () => {
settings = TypeMoq.Mock.ofType<IPythonSettings>();
settings.setup((s) => s.terminal).returns(() => terminalSettings.object);
configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object);
expectedTerminalIconId = undefined;

switch (testSuiteName) {
case 'Terminal Execution': {
Expand Down Expand Up @@ -108,6 +110,7 @@ suite('Terminal - Code Execution', () => {
applicationShell.object,
);
expectedTerminalTitle = 'REPL';
expectedTerminalIconId = 'snake';
break;
}
case 'Django Execution': {
Expand Down Expand Up @@ -145,7 +148,12 @@ suite('Terminal - Code Execution', () => {
terminalFactory
.setup((f) =>
f.getTerminalService(
TypeMoq.It.is<TerminalCreationOptions>((a) => a.title === expectedTerminalTitle),
TypeMoq.It.is<TerminalCreationOptions>(
(a) =>
a.title === expectedTerminalTitle &&
(a.iconPath instanceof ThemeIcon ? a.iconPath.id : undefined) ===
expectedTerminalIconId,
),
),
)
.returns(() => terminalService.object);
Expand Down
Loading