diff --git a/package.json b/package.json index 0414e9df1b04..5c8d9a0ccbc0 100644 --- a/package.json +++ b/package.json @@ -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%", diff --git a/package.nls.json b/package.nls.json index b6ba75b332f2..6c45c43fd3bb 100644 --- a/package.nls.json +++ b/package.nls.json @@ -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.", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 634e0106fe7b..92e00b1267f4 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -378,6 +378,7 @@ export class PythonSettings implements IPythonSettings { activateEnvironment: true, activateEnvInCurrentTerminal: false, enableShellIntegration: false, + reuseActiveTerminal: true, }; this.REPL = pythonSettings.get('REPL')!; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 2cb393d89bdf..33317c24debd 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -189,6 +189,7 @@ export interface ITerminalSettings { readonly activateEnvironment: boolean; readonly activateEnvInCurrentTerminal: boolean; readonly enableShellIntegration: boolean; + readonly reuseActiveTerminal: boolean; } export interface IREPLSettings { diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index ea444af4d89e..584ee53a4901 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -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'; @@ -25,6 +25,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { private hasRanOutsideCurrentDrive = false; protected terminalTitle!: string; private replActive?: Promise; + private existingReplTerminal?: Terminal; constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, @@ -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(); @@ -124,6 +153,41 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { public async getExecuteFileArgs(resource?: Uri, executeArgs: string[] = []): Promise { return this.getExecutableInfo(resource, executeArgs); } + + /** + * Find an existing terminal that has a Python REPL running + */ + private async findExistingPythonTerminal(resource?: Uri): Promise { + 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, diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index b5bcecd971ea..9a481e7055de 100644 --- a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts +++ b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts @@ -675,4 +675,26 @@ suite('Terminal - Code Execution', () => { }); }); }); + + suite('Terminal Reuse Configuration', () => { + let terminalSettings: TypeMoq.IMock; + + 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(); + 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(); + terminalSettings.setup((t) => t.reuseActiveTerminal).returns(() => true); + + expect(terminalSettings.object.reuseActiveTerminal).to.be.true; + }); + }); });