Skip to content

Commit 6b784e5

Browse files
authored
Use sendText to send Python code to Terminal REPL for Python >= 3.13 (#24765)
Resolves: #24674 (comment) Use sendText to send Python code to Terminal REPL for Python >= 3.13 to prevent keyboard interrupt. Relevant file context from VS Code: https://github.com/microsoft/vscode/blob/f9c927cf7a29a59b896b6cdac2d8b5d2d43afea5/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts#L906 It seems like we are on this edge scenario where generic terminal shell integration is enabled (so executeCommand can be used), but we have temporarily disabled Python shell integration for Python >= 3.13 (and of course sending the relevant escape sequences such as the commandLine itself to VS Code). Why? * python/cpython#126131 placing user's mouse cursor position at odd place. Why and where I think the keyboard interrupt is happening: Python extension tries to executeCommand when sending commands to terminal REPL >= Python3.13, where we are not sending shell integration escape sequences from the Python side. * I think this is why it is attaching the keyboard interrupt all the sudden, because VS Code see that Python extension is requesting executeCommand but is not sending the commandLine escape sequence to them. For every other versions < 3.13 (where we send all the shell integration escape sequences including the commandLine), this does not happen.
1 parent e9b4b7b commit 6b784e5

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

src/client/common/terminal/service.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { useEnvExtension } from '../../envExt/api.internal';
2525
import { ensureTerminalLegacy } from '../../envExt/api.legacy';
2626
import { sleep } from '../utils/async';
2727
import { isWindows } from '../utils/platform';
28+
import { getPythonMinorVersion } from '../../repl/replUtils';
2829

2930
@injectable()
3031
export class TerminalService implements ITerminalService, Disposable {
@@ -108,7 +109,15 @@ export class TerminalService implements ITerminalService, Disposable {
108109

109110
const config = getConfiguration('python');
110111
const pythonrcSetting = config.get<boolean>('terminal.shellIntegration.enabled');
111-
if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows())) {
112+
113+
const minorVersion = this.options?.resource
114+
? await getPythonMinorVersion(
115+
this.options.resource,
116+
this.serviceContainer.get<IInterpreterService>(IInterpreterService),
117+
)
118+
: undefined;
119+
120+
if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows()) || (minorVersion ?? 0) >= 13) {
112121
// If user has explicitly disabled SI for Python, use sendText for inside Terminal REPL.
113122
terminal.sendText(commandLine);
114123
return undefined;

src/client/repl/replUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,17 @@ export function getTabNameForUri(uri: Uri): string | undefined {
118118

119119
return undefined;
120120
}
121+
122+
/**
123+
* Function that will return the minor version of current active Python interpreter.
124+
*/
125+
export async function getPythonMinorVersion(
126+
uri: Uri | undefined,
127+
interpreterService: IInterpreterService,
128+
): Promise<number | undefined> {
129+
if (uri) {
130+
const pythonVersion = await getActiveInterpreter(uri, interpreterService);
131+
return pythonVersion?.version?.minor;
132+
}
133+
return undefined;
134+
}

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,29 @@ import {
1111
TerminalShellExecution,
1212
TerminalShellExecutionEndEvent,
1313
TerminalShellIntegration,
14+
Uri,
1415
Terminal as VSCodeTerminal,
1516
WorkspaceConfiguration,
1617
} from 'vscode';
1718
import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types';
1819
import { EXTENSION_ROOT_DIR } from '../../../client/common/constants';
1920
import { IPlatformService } from '../../../client/common/platform/types';
2021
import { TerminalService } from '../../../client/common/terminal/service';
21-
import { ITerminalActivator, ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types';
22+
import {
23+
ITerminalActivator,
24+
ITerminalHelper,
25+
TerminalCreationOptions,
26+
TerminalShellType,
27+
} from '../../../client/common/terminal/types';
2228
import { IDisposableRegistry } from '../../../client/common/types';
2329
import { IServiceContainer } from '../../../client/ioc/types';
2430
import { ITerminalAutoActivation } from '../../../client/terminals/types';
2531
import { createPythonInterpreter } from '../../utils/interpreters';
2632
import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis';
2733
import * as platform from '../../../client/common/utils/platform';
2834
import * as extapi from '../../../client/envExt/api.internal';
35+
import { IInterpreterService } from '../../../client/interpreter/contracts';
36+
import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
2937

3038
suite('Terminal Service', () => {
3139
let service: TerminalService;
@@ -46,6 +54,8 @@ suite('Terminal Service', () => {
4654
let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>;
4755
let isWindowsStub: sinon.SinonStub;
4856
let useEnvExtensionStub: sinon.SinonStub;
57+
let interpreterService: TypeMoq.IMock<IInterpreterService>;
58+
let options: TypeMoq.IMock<TerminalCreationOptions>;
4959

5060
setup(() => {
5161
useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension');
@@ -92,6 +102,13 @@ suite('Terminal Service', () => {
92102
disposables = [];
93103

94104
mockServiceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
105+
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
106+
interpreterService
107+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
108+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
109+
110+
options = TypeMoq.Mock.ofType<TerminalCreationOptions>();
111+
options.setup((o) => o.resource).returns(() => Uri.parse('a'));
95112

96113
mockServiceContainer.setup((c) => c.get(ITerminalManager)).returns(() => terminalManager.object);
97114
mockServiceContainer.setup((c) => c.get(ITerminalHelper)).returns(() => terminalHelper.object);
@@ -100,6 +117,7 @@ suite('Terminal Service', () => {
100117
mockServiceContainer.setup((c) => c.get(IWorkspaceService)).returns(() => workspaceService.object);
101118
mockServiceContainer.setup((c) => c.get(ITerminalActivator)).returns(() => terminalActivator.object);
102119
mockServiceContainer.setup((c) => c.get(ITerminalAutoActivation)).returns(() => terminalAutoActivator.object);
120+
mockServiceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object);
103121
getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration');
104122
isWindowsStub = sinon.stub(platform, 'isWindows');
105123
pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>();
@@ -117,6 +135,7 @@ suite('Terminal Service', () => {
117135
}
118136
disposables.filter((item) => !!item).forEach((item) => item.dispose());
119137
sinon.restore();
138+
interpreterService.reset();
120139
});
121140

122141
test('Ensure terminal is disposed', async () => {
@@ -239,7 +258,7 @@ suite('Terminal Service', () => {
239258
terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1));
240259
});
241260

242-
test('Ensure sendText is NOT called when Python shell integration and terminal shell integration are both enabled - Mac, Linux', async () => {
261+
test('Ensure sendText is NOT called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => {
243262
isWindowsStub.returns(false);
244263
pythonConfig
245264
.setup((p) => p.get('terminal.shellIntegration.enabled'))
@@ -261,6 +280,36 @@ suite('Terminal Service', () => {
261280
terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.never());
262281
});
263282

283+
test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => {
284+
interpreterService.reset();
285+
286+
interpreterService
287+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
288+
.returns(() =>
289+
Promise.resolve({ path: 'yo', version: { major: 3, minor: 13, patch: 0 } } as PythonEnvironment),
290+
);
291+
292+
isWindowsStub.returns(false);
293+
pythonConfig
294+
.setup((p) => p.get('terminal.shellIntegration.enabled'))
295+
.returns(() => true)
296+
.verifiable(TypeMoq.Times.once());
297+
298+
terminalHelper
299+
.setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
300+
.returns(() => Promise.resolve(undefined));
301+
302+
service = new TerminalService(mockServiceContainer.object, options.object);
303+
const textToSend = 'Some Text';
304+
terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash);
305+
terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object);
306+
307+
await service.ensureTerminal();
308+
await service.executeCommand(textToSend, true);
309+
310+
terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once());
311+
});
312+
264313
test('Ensure sendText IS called even when Python shell integration and terminal shell integration are both enabled - Window', async () => {
265314
isWindowsStub.returns(true);
266315
pythonConfig

0 commit comments

Comments
 (0)