diff --git a/python_files/normalizeSelection.py b/python_files/normalizeSelection.py index 981251289e57..3d5137fe4aeb 100644 --- a/python_files/normalizeSelection.py +++ b/python_files/normalizeSelection.py @@ -8,6 +8,8 @@ import textwrap from typing import Iterable +attach_bracket_paste = sys.version_info >= (3, 13) + def split_lines(source): """ @@ -279,14 +281,20 @@ def get_next_block_lineno(which_line_next): normalized = result["normalized_smart_result"] which_line_next = result["which_line_next"] if normalized == "deprecated": - data = json.dumps({"normalized": normalized}) + data = json.dumps( + {"normalized": normalized, "attach_bracket_paste": attach_bracket_paste} + ) else: data = json.dumps( - {"normalized": normalized, "nextBlockLineno": result["which_line_next"]} + { + "normalized": normalized, + "nextBlockLineno": result["which_line_next"], + "attach_bracket_paste": attach_bracket_paste, + } ) else: normalized = normalize_lines(contents["code"]) - data = json.dumps({"normalized": normalized}) + data = json.dumps({"normalized": normalized, "attach_bracket_paste": attach_bracket_paste}) stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer stdout.write(data.encode("utf-8")) diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index ff1c4f218f8d..49fdd59a00c0 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -118,6 +118,15 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; await this.moveToNextBlock(lineOffset, activeEditor); } + // For new _pyrepl for Python3.13 and above, we need to send code via bracketed paste mode. + if (object.attach_bracket_paste) { + let trimmedNormalized = object.normalized.replace(/\n$/, ''); + if (trimmedNormalized.endsWith(':\n')) { + // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. + trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); + } + return `\u001b[200~${trimmedNormalized}\u001b[201~`; + } return parse(object.normalized); } catch (ex) { diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index ebadd153705e..7a3171ccf836 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -8,6 +8,7 @@ import * as path from 'path'; import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; import { Position, Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import * as sinon from 'sinon'; import * as fs from '../../../client/common/platform/fs-paths'; import { IActiveResourceService, @@ -49,6 +50,7 @@ suite('Terminal - Code Execution Helper', () => { let workspaceService: TypeMoq.IMock; let configurationService: TypeMoq.IMock; let pythonSettings: TypeMoq.IMock; + let jsonParseStub: sinon.SinonStub; const workingPython: PythonEnvironment = { path: PYTHON_PATH, version: new SemVer('3.6.6-final'), @@ -134,7 +136,68 @@ suite('Terminal - Code Execution Helper', () => { editor.setup((e) => e.document).returns(() => document.object); }); + test('normalizeLines should handle attach_bracket_paste correctly', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are on 3.13")', + attach_bracket_paste: true, + }; + jsonParseStub.returns(mockResult); + + const result = await helper.normalizeLines('print("Looks like you are on 3.13")'); + + expect(result).to.equal(`\u001b[200~print("Looks like you are on 3.13")\u001b[201~`); + jsonParseStub.restore(); + }); + + test('normalizeLines should not attach bracketed paste for < 3.13', async () => { + jsonParseStub = sinon.stub(JSON, 'parse'); + const mockResult = { + normalized: 'print("Looks like you are not on 3.13")', + attach_bracket_paste: false, + }; + jsonParseStub.returns(mockResult); + + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + EnableREPLSmartSend: false, + REPLSmartSend: false, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + const actualProcessService = new ProcessService(); + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => + actualProcessService.execObservable.apply(actualProcessService, [file, args, options]), + ); + + const result = await helper.normalizeLines('print("Looks like you are not on 3.13")'); + + expect(result).to.equal('print("Looks like you are not on 3.13")'); + jsonParseStub.restore(); + }); + test('normalizeLines should call normalizeSelection.py', async () => { + jsonParseStub.restore(); let execArgs = ''; processService @@ -186,7 +249,6 @@ suite('Terminal - Code Execution Helper', () => { path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized_selection.py`), 'utf8', ); - await ensureCodeIsNormalized(code, expectedCode); }); });