Skip to content

Add option to reuse existing Python terminals for code execution #25158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,12 @@
"preview"
]
},
"python.terminal.reuseActiveTerminal": {
"default": true,
"description": "%python.terminal.reuseActiveTerminal.description%",
"scope": "resource",
"type": "boolean"
},
"python.REPL.enableREPLSmartSend": {
"default": true,
"description": "%python.EnableREPLSmartSend.description%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"python.terminal.executeInFileDir.description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.",
"python.terminal.focusAfterLaunch.description": "When launching a python terminal, whether to focus the cursor on the terminal.",
"python.terminal.launchArgs.description": "Python launch arguments to use when executing a file in the terminal.",
"python.terminal.reuseActiveTerminal.description": "When running code selections or lines, try to reuse an existing active Python terminal instead of creating a new one.",
"python.testing.autoTestDiscoverOnSaveEnabled.description": "Enable auto run test discovery when saving a test file.",
"python.testing.autoTestDiscoverOnSavePattern.description": "Glob pattern used to determine which files are used by autoTestDiscoverOnSaveEnabled.",
"python.testing.cwd.description": "Optional working directory for tests.",
Expand Down
1 change: 1 addition & 0 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,7 @@ export class PythonSettings implements IPythonSettings {
activateEnvironment: true,
activateEnvInCurrentTerminal: false,
enableShellIntegration: false,
reuseActiveTerminal: true,
};

this.REPL = pythonSettings.get<IREPLSettings>('REPL')!;
Expand Down
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export interface ITerminalSettings {
readonly activateEnvironment: boolean;
readonly activateEnvInCurrentTerminal: boolean;
readonly enableShellIntegration: boolean;
readonly reuseActiveTerminal: boolean;
}

export interface IREPLSettings {
Expand Down
68 changes: 66 additions & 2 deletions 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, Uri, window, Terminal } from 'vscode';
import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types';
import '../../common/extensions';
import { IPlatformService } from '../../common/platform/types';
Expand All @@ -25,6 +25,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
private hasRanOutsideCurrentDrive = false;
protected terminalTitle!: string;
private replActive?: Promise<boolean>;
private existingReplTerminal?: Terminal;

constructor(
@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory,
Expand Down Expand Up @@ -59,11 +60,39 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
this.configurationService.updateSetting('REPL.enableREPLSmartSend', false, resource);
}
} else {
await this.getTerminalService(resource).executeCommand(code, true);
// If we're using an existing terminal, send code directly to it
if (this.existingReplTerminal) {
this.existingReplTerminal.sendText(code);
} else {
await this.getTerminalService(resource).executeCommand(code, true);
}
}
}

public async initializeRepl(resource: Resource) {
// First, try to find and reuse an existing Python terminal
const existingTerminal = await this.findExistingPythonTerminal(resource);
if (existingTerminal) {
// Store the existing terminal reference and show it
this.existingReplTerminal = existingTerminal;
existingTerminal.show();
this.replActive = Promise.resolve(true);

// Listen for terminal close events to clear our reference
const terminalCloseListener = window.onDidCloseTerminal((closedTerminal) => {
if (closedTerminal === this.existingReplTerminal) {
this.existingReplTerminal = undefined;
this.replActive = undefined;
}
});
this.disposables.push(terminalCloseListener);

return;
}

// Clear any existing terminal reference since we're creating a new one
this.existingReplTerminal = undefined;

const terminalService = this.getTerminalService(resource);
if (this.replActive && (await this.replActive)) {
await terminalService.show();
Expand Down Expand Up @@ -124,6 +153,41 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise<PythonExecInfo> {
return this.getExecutableInfo(resource, executeArgs);
}

/**
* Find an existing terminal that has a Python REPL running
*/
private async findExistingPythonTerminal(resource?: Uri): Promise<Terminal | undefined> {
const pythonSettings = this.configurationService.getSettings(resource);

// Check if the feature is enabled
if (!pythonSettings.terminal.reuseActiveTerminal) {
return undefined;
}

// Look through all existing terminals
for (const terminal of window.terminals) {
// Skip terminals that are closed or hidden
if (terminal.exitStatus) {
continue;
}

// Check if this looks like a Python terminal based on name
const terminalName = terminal.name.toLowerCase();
if (terminalName.includes('python') || terminalName.includes('repl')) {
// For now, we'll consider any Python-named terminal as potentially reusable
// In the future, we could add more sophisticated detection
return terminal;
}

// Check if the terminal's detected shell is Python
if (terminal.state?.shell === 'python') {
return terminal;
}
}

return undefined;
}
private getTerminalService(resource: Resource, options?: { newTerminalPerFile: boolean }): ITerminalService {
return this.terminalServiceFactory.getTerminalService({
resource,
Expand Down
22 changes: 22 additions & 0 deletions src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -675,4 +675,26 @@ suite('Terminal - Code Execution', () => {
});
});
});

suite('Terminal Reuse Configuration', () => {
let terminalSettings: TypeMoq.IMock<ITerminalSettings>;

test('Should respect reuseActiveTerminal configuration setting when disabled', () => {
// Test that the setting is properly read
// When reuseActiveTerminal is false, should not attempt to reuse
terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>();
terminalSettings.setup((t) => t.reuseActiveTerminal).returns(() => false);

// This test validates that the configuration is properly integrated
expect(terminalSettings.object.reuseActiveTerminal).to.be.false;
});

test('Should have correct default value for reuseActiveTerminal', () => {
// Test that the default configuration is correct
terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>();
terminalSettings.setup((t) => t.reuseActiveTerminal).returns(() => true);

expect(terminalSettings.object.reuseActiveTerminal).to.be.true;
});
});
});