Skip to content
Merged
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
14 changes: 12 additions & 2 deletions src/client/common/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class TerminalService implements ITerminalService, Disposable {
): Promise<TerminalShellExecution | undefined> {
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.
Expand Down Expand Up @@ -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~')) {
Comment thread
edvilme marked this conversation as resolved.
this.terminal?.sendText(commandLine, false);
this.terminal?.sendText('\r', false);
} else {
this.terminal?.sendText(commandLine);
}
}

private async executeCommandInternal(commandLine: string): Promise<TerminalShellExecution | undefined> {
const terminal = this.terminal;
if (!terminal) {
Expand Down
10 changes: 6 additions & 4 deletions src/client/terminals/codeExecution/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(IConfigurationService);
if (configuration) {
const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource());
smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend;
shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled;
}

const input = JSON.stringify({
Expand All @@ -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

@anthonykim1 Anthony Kim (anthonykim1) Oct 1, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't recall exact detail, but the indentation logic has been funky and probably worth revisiting with newer models. Maybe there were some nuances with PY_REPL flag + shell integration combination.

Also recommend these 4 scenarios when it comes to manually testing and ensuring coverage for run selection of Python code (via shift+enter, or UI):

When I say shell integration, I mean both the terminal.integrated.shellIntegration.enabled and python.terminal.shellIntegration.enabled

  1. >= Python3.13 with shell integration enabled.
  2. < Python 3.13 with shell integration enabled.
  3. >= Python3.13 with shell integration disabled.
  4. < Python 3.13 with shell integration disabled.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

>=3.13 with shell integration enabled

image

>=3.13 with shell integration disabled

image

< 3.13 with shell integration disabled

image

< 3.13 with shell integration enabled

image

// 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.
Expand Down
21 changes: 21 additions & 0 deletions src/test/common/terminals/service.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 16 additions & 9 deletions src/test/terminals/codeExecution/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
});

Expand Down
Loading