diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index b58257b2d6bb..2e9f9a7a4dc3 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -90,7 +90,7 @@ export class TerminalService implements ITerminalService, Disposable { ): Promise { if (isPythonShell) { if (this.isReplReady) { - this.terminal?.sendText(commandLine); + this.sendPythonShellCommand(commandLine); traceVerbose(`Python REPL sendText: ${commandLine}`); } else { // Queue command to run once REPL is ready. @@ -156,11 +156,21 @@ export class TerminalService implements ITerminalService, Disposable { const commandLine = this.pythonReplCommandQueue.shift(); if (commandLine) { traceVerbose(`Executing queued REPL command: ${commandLine}`); - this.terminal?.sendText(commandLine); + this.sendPythonShellCommand(commandLine); } } } + // Send PyREPL's carriage-return binding for Enter separately from the bracketed-paste sequence. + private sendPythonShellCommand(commandLine: string): void { + if (commandLine.startsWith('\u001b[200~') && commandLine.endsWith('\u001b[201~')) { + this.terminal?.sendText(commandLine, false); + this.terminal?.sendText('\r', false); + } else { + this.terminal?.sendText(commandLine); + } + } + private async executeCommandInternal(commandLine: string): Promise { const terminal = this.terminal; if (!terminal) { diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index 4efad5ee174e..097a0d5d76df 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -99,12 +99,10 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const endLineVal = activeEditor?.selection?.end.line ?? 0; const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; let smartSendSettingsEnabledVal = true; - let shellIntegrationEnabled = false; const configuration = this.serviceContainer.get(IConfigurationService); if (configuration) { const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; - shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled; } const input = JSON.stringify({ @@ -127,8 +125,12 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { await this.moveToNextBlock(lineOffset, activeEditor); } - // For new _pyrepl for Python3.13+ && !shellIntegration, we need to send code via bracketed paste mode. - if (object.attach_bracket_paste && !shellIntegrationEnabled && _replType === ReplType.terminal) { + // For the new _pyrepl in Python 3.13+, send code via bracketed paste mode so the REPL + // preserves the code's indentation instead of applying its own auto-indentation. + // This must happen regardless of the `terminal.shellIntegration.enabled` setting: terminal + // REPL code is always sent through `sendText` (never `shellIntegration.executeCommand`), so + // bracketed paste is the only thing preventing progressively increasing indentation. + if (object.attach_bracket_paste && _replType === ReplType.terminal) { let trimmedNormalized = object.normalized.replace(/\n$/, ''); if (trimmedNormalized.endsWith(':\n')) { // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. diff --git a/src/test/common/terminals/service.unit.test.ts b/src/test/common/terminals/service.unit.test.ts index d703f547d14e..bc81d773abcf 100644 --- a/src/test/common/terminals/service.unit.test.ts +++ b/src/test/common/terminals/service.unit.test.ts @@ -338,6 +338,27 @@ suite('Terminal Service', () => { terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); }); + test('Ensure bracketed paste and Enter are sent separately to the Python shell', async () => { + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = '\u001b[200~print("hello")\u001b[201~'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify( + (t) => t.sendText(TypeMoq.It.isValue(textToSend), TypeMoq.It.isValue(false)), + TypeMoq.Times.once(), + ); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue('\r'), TypeMoq.It.isValue(false)), TypeMoq.Times.once()); + }); + test('Ensure sendText IS called even when Python shell integration and terminal shell integration are both enabled - Window', async () => { isWindowsStub.returns(true); pythonConfig diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index b7e0d1617884..dfb277062910 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -137,14 +137,23 @@ suite('Terminal - Code Execution Helper', async () => { editor.setup((e) => e.document).returns(() => document.object); }); - test('normalizeLines with BASIC_REPL does not attach bracketed paste mode', async () => { + test('normalizeLines attaches bracketed paste for 3.13+ terminal even when shell integration is enabled (#26176)', async () => { + // The new PyREPL (3.13+) auto-indents typed input. Terminal REPL code is sent via sendText + // (never shellIntegration.executeCommand), so bracketed paste must be attached regardless of + // the terminal.shellIntegration.enabled setting; otherwise multiline indentation grows + // progressively and eventually raises IndentationError. configurationService .setup((c) => c.getSettings(TypeMoq.It.isAny())) .returns({ REPL: { - EnableREPLSmartSend: false, + enableREPLSmartSend: false, REPLSmartSend: false, }, + terminal: { + shellIntegration: { + enabled: true, + }, + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as any); const actualProcessService = new ProcessService(); @@ -155,15 +164,13 @@ suite('Terminal - Code Execution Helper', async () => { ); jsonParseStub = sinon.stub(JSON, 'parse'); - const mockResult = { - normalized: 'print("Looks like you are on 3.13")', - attach_bracket_paste: true, - }; - jsonParseStub.returns(mockResult); + const normalized = 'valid = (\n isinstance(x)\n and y\n)\n'; + jsonParseStub.returns({ normalized, attach_bracket_paste: true }); - const result = await helper.normalizeLines('print("Looks like you are on 3.13")', ReplType.terminal); + const result = await helper.normalizeLines('valid = (\n isinstance(x)\n and y\n)', ReplType.terminal); - expect(result).to.equal(`print("Looks like you are on 3.13")`); + // Indentation is preserved verbatim inside the bracketed-paste markers. + expect(result).to.equal('\u001b[200~valid = (\n isinstance(x)\n and y\n)\u001b[201~'); jsonParseStub.restore(); });