From a7bd46c41ddc45120dc9f6db5fd963435c3dfa88 Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Mon, 3 Aug 2026 19:54:21 -0400 Subject: [PATCH 1/7] fix scrollable repl --- src/client/repl/replController.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts index f30b8d9cbf6f..63f9e9c4cac0 100644 --- a/src/client/repl/replController.ts +++ b/src/client/repl/replController.ts @@ -23,12 +23,14 @@ export function createReplController( const exec = controller.createNotebookCellExecution(cell); exec.start(Date.now()); - const result = await server.execute(cell.document.getText()); + const code = cell.document.getText(); + const result = await server.execute(code); if (result?.output) { - exec.replaceOutput([ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]), - ]); + const stdoutItem = vscode.NotebookCellOutputItem.stdout(result.output); + (stdoutItem as any).metadata = { scrollable: false }; + const output = new vscode.NotebookCellOutput([stdoutItem], { scrollable: false }); + exec.replaceOutput([output]); // TODO: Properly update via NotebookCellOutputItem.error later. } From 6f60c00eac016eee18be705d108f1caab08202c3 Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Mon, 3 Aug 2026 19:54:21 -0400 Subject: [PATCH 2/7] fix scrollable repl --- src/client/repl/replController.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts index f30b8d9cbf6f..63f9e9c4cac0 100644 --- a/src/client/repl/replController.ts +++ b/src/client/repl/replController.ts @@ -23,12 +23,14 @@ export function createReplController( const exec = controller.createNotebookCellExecution(cell); exec.start(Date.now()); - const result = await server.execute(cell.document.getText()); + const code = cell.document.getText(); + const result = await server.execute(code); if (result?.output) { - exec.replaceOutput([ - new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]), - ]); + const stdoutItem = vscode.NotebookCellOutputItem.stdout(result.output); + (stdoutItem as any).metadata = { scrollable: false }; + const output = new vscode.NotebookCellOutput([stdoutItem], { scrollable: false }); + exec.replaceOutput([output]); // TODO: Properly update via NotebookCellOutputItem.error later. } From 684a949aff1070e37992e91d980b78f5f9016501 Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Thu, 3 Sep 2026 19:01:43 -0400 Subject: [PATCH 3/7] adding tests --- src/test/repl/nativeRepl.test.ts | 63 ++++++++++++++++++++++++++++++++ src/test/vscode-mock.ts | 13 +++++++ 2 files changed, 76 insertions(+) diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts index 2cf18cefe1f7..114ab40e2475 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -5,6 +5,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; +import { reset, when } from 'ts-mockito'; +import * as vscode from 'vscode'; import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; import { expect } from 'chai'; @@ -16,6 +18,7 @@ import * as PythonServer from '../../client/repl/pythonServer'; import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; import * as replController from '../../client/repl/replController'; import { executeCommand } from '../../client/common/vscodeApis/commandApis'; +import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('REPL - Native REPL', () => { let interpreterService: TypeMoq.IMock; @@ -114,6 +117,66 @@ suite('REPL - Native REPL', () => { expect(createReplControllerStub.calledOnce).to.be.true; }); + test('createReplController should publish stdout notebook output for REPL execution', async () => { + const mockServer = { + interrupt: sinon.stub(), + execute: sinon.stub().resolves({ status: true, output: 'hello\nworld' }), + dispose: sinon.stub(), + } as any; + const createPythonServerStub = sinon.stub(PythonServer, 'createPythonServer').returns(mockServer as any); + + const replaceOutputStub = sinon.stub(); + const execStub = { + start: sinon.stub(), + replaceOutput: replaceOutputStub, + end: sinon.stub(), + } as any; + + const createNotebookCellExecutionStub = sinon.stub().returns(execStub); + const mockNotebookController = { + id: 'mockController', + dispose: sinon.stub(), + updateNotebookAffinity: sinon.stub(), + createNotebookCellExecution: createNotebookCellExecutionStub, + supportedLanguages: [] as string[], + description: '', + interruptHandler: undefined, + executeHandler: undefined, + } as any as vscode.NotebookController; + + when(mockedVSCodeNamespaces.notebooks!.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL')).thenReturn( + mockNotebookController, + ); + createReplControllerStub.restore(); + + const disposables: Disposable[] = []; + try { + const controller = replController.createReplController('python', disposables, '/cwd'); + + const mockTextDocument = { getText: sinon.stub().returns('print("hi")') } as any; + const mockCell = { document: mockTextDocument } as any; + await (controller.executeHandler as any)([mockCell]); + + expect(createPythonServerStub.calledOnce).to.be.true; + expect(mockServer.execute.calledOnceWithExactly('print("hi")')).to.be.true; + expect(createNotebookCellExecutionStub.calledOnce).to.be.true; + expect(replaceOutputStub.calledOnce).to.be.true; + + const outputs = replaceOutputStub.firstCall.args[0] as vscode.NotebookCellOutput[]; + expect(outputs).to.have.lengthOf(1); + const output = outputs[0]; + expect(output.items).to.have.lengthOf(1); + expect(output.items[0].mime).to.equal('application/vnd.code.notebook.stdout'); + expect((output.items[0] as any).metadata).to.deep.equal({ scrollable: false }); + expect(output.metadata).to.deep.equal({ scrollable: false }); + const outputText = Buffer.from((output.items[0] as any).data).toString(); + expect(outputText).to.equal('hello\nworld'); + } finally { + disposables.forEach((disposable) => disposable.dispose()); + reset(mockedVSCodeNamespaces.notebooks!); + } + }); + test('watchNotebookClosed should clean up resources when notebook is closed', async () => { const notebookCloseEmitter = new EventEmitter(); sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ac64384520cf..b856fe13ab43 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -42,6 +42,19 @@ export function initialize() { generateMock('scm'); generateMock('notebooks'); + // Notebook output helpers used by REPL tests. + (mockedVSCode as any).NotebookCellOutputItem = { + stdout: (value: string) => ({ mime: 'application/vnd.code.notebook.stdout', data: Buffer.from(value) }), + }; + (mockedVSCode as any).NotebookCellOutput = class { + public items: any[]; + public metadata?: any; + constructor(items: any[], metadata?: any) { + this.items = items; + this.metadata = metadata; + } + }; + // Use mock clipboard fo testing purposes. const clipboard = new MockClipboard(); when(mockedVSCodeNamespaces.env!.clipboard).thenReturn(clipboard); From 60b2861ece3af1d3323c4247b03cd1450a985483 Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Thu, 17 Sep 2026 20:13:23 -0400 Subject: [PATCH 4/7] prettier and fix tets --- src/client/repl/nativeRepl.ts | 2 +- src/client/repl/replController.ts | 3 +-- src/test/repl/nativeRepl.test.ts | 14 +++++++------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/client/repl/nativeRepl.ts b/src/client/repl/nativeRepl.ts index 3f8a085da467..7f82edc9dda1 100644 --- a/src/client/repl/nativeRepl.ts +++ b/src/client/repl/nativeRepl.ts @@ -63,7 +63,7 @@ export class NativeRepl implements Disposable { } dispose(): void { - this.disposables.forEach((d) => d.dispose()); + this.disposables.forEach((d) => d?.dispose()); } /** diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts index 63f9e9c4cac0..7d5ede2367b4 100644 --- a/src/client/repl/replController.ts +++ b/src/client/repl/replController.ts @@ -28,8 +28,7 @@ export function createReplController( if (result?.output) { const stdoutItem = vscode.NotebookCellOutputItem.stdout(result.output); - (stdoutItem as any).metadata = { scrollable: false }; - const output = new vscode.NotebookCellOutput([stdoutItem], { scrollable: false }); + const output = new vscode.NotebookCellOutput([stdoutItem], {}); exec.replaceOutput([output]); // TODO: Properly update via NotebookCellOutputItem.error later. } diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts index 114ab40e2475..f655c84dc705 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -133,7 +133,7 @@ suite('REPL - Native REPL', () => { } as any; const createNotebookCellExecutionStub = sinon.stub().returns(execStub); - const mockNotebookController = { + const mockNotebookController = ({ id: 'mockController', dispose: sinon.stub(), updateNotebookAffinity: sinon.stub(), @@ -142,11 +142,11 @@ suite('REPL - Native REPL', () => { description: '', interruptHandler: undefined, executeHandler: undefined, - } as any as vscode.NotebookController; + } as any) as vscode.NotebookController; - when(mockedVSCodeNamespaces.notebooks!.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL')).thenReturn( - mockNotebookController, - ); + when( + mockedVSCodeNamespaces.notebooks!.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'), + ).thenReturn(mockNotebookController); createReplControllerStub.restore(); const disposables: Disposable[] = []; @@ -167,8 +167,8 @@ suite('REPL - Native REPL', () => { const output = outputs[0]; expect(output.items).to.have.lengthOf(1); expect(output.items[0].mime).to.equal('application/vnd.code.notebook.stdout'); - expect((output.items[0] as any).metadata).to.deep.equal({ scrollable: false }); - expect(output.metadata).to.deep.equal({ scrollable: false }); + expect((output.items[0] as any).metadata).to.be.undefined; + expect(output.metadata).to.deep.equal({}); const outputText = Buffer.from((output.items[0] as any).data).toString(); expect(outputText).to.equal('hello\nworld'); } finally { From df9f4e57416993851060d0e5013fb3f9482246af Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Wed, 7 Oct 2026 20:26:02 -0400 Subject: [PATCH 5/7] Merge branch 'main' of https://github.com/microsoft/vscode-python into vscode-python-issue-25609-scrollable-repl From 4eddf1165165c8726db5ec117acac698ea016359 Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Wed, 7 Oct 2026 20:40:46 -0400 Subject: [PATCH 6/7] fix tests --- src/test/repl/nativeRepl.test.ts | 7 +------ src/test/repl/replCommand.test.ts | 10 ++++++++-- src/test/vscode-mock.ts | 13 +++++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts index f655c84dc705..0e674aa80d4d 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -5,7 +5,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; -import { reset, when } from 'ts-mockito'; import * as vscode from 'vscode'; import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; import { expect } from 'chai'; @@ -18,7 +17,6 @@ import * as PythonServer from '../../client/repl/pythonServer'; import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; import * as replController from '../../client/repl/replController'; import { executeCommand } from '../../client/common/vscodeApis/commandApis'; -import { mockedVSCodeNamespaces } from '../vscode-mock'; suite('REPL - Native REPL', () => { let interpreterService: TypeMoq.IMock; @@ -144,9 +142,7 @@ suite('REPL - Native REPL', () => { executeHandler: undefined, } as any) as vscode.NotebookController; - when( - mockedVSCodeNamespaces.notebooks!.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'), - ).thenReturn(mockNotebookController); + sinon.stub(vscode.notebooks, 'createNotebookController').returns(mockNotebookController); createReplControllerStub.restore(); const disposables: Disposable[] = []; @@ -173,7 +169,6 @@ suite('REPL - Native REPL', () => { expect(outputText).to.equal('hello\nworld'); } finally { disposables.forEach((disposable) => disposable.dispose()); - reset(mockedVSCodeNamespaces.notebooks!); } }); diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts index 0b5edda863f9..8f8c313ee344 100644 --- a/src/test/repl/replCommand.test.ts +++ b/src/test/repl/replCommand.test.ts @@ -1,6 +1,6 @@ // Create test suite and test cases for the `replUtils` module import * as TypeMoq from 'typemoq'; -import { commands, Disposable, Uri } from 'vscode'; +import { commands, Disposable, Uri, window } from 'vscode'; import * as sinon from 'sinon'; import { expect } from 'chai'; import { IInterpreterService } from '../../client/interpreter/contracts'; @@ -163,7 +163,13 @@ suite('REPL - register native repl command', () => { expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); - await commandHandler!('uri'); + const originalActiveTextEditor = window.activeTextEditor; + (window as any).activeTextEditor = undefined; + try { + await commandHandler!('uri'); + } finally { + (window as any).activeTextEditor = originalActiveTextEditor; + } sinon.assert.calledOnce(getNativeReplStub); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index b856fe13ab43..d644db906e54 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -41,6 +41,19 @@ export function initialize() { generateMock('debug'); generateMock('scm'); generateMock('notebooks'); + (mockedVSCode.commands as any).executeCommand = () => Promise.resolve(undefined); + (mockedVSCode.notebooks as any).createNotebookController = () => { + throw new Error('createNotebookController must be stubbed in tests that use it.'); + }; + when(mockedVSCodeNamespaces.window!.tabGroups).thenReturn({ + all: [], + get activeTabGroup(): vscode.TabGroup { + throw new Error('activeTabGroup must be stubbed in tests that use it.'); + }, + onDidChangeTabGroups: new vscodeMocks.EventEmitter().event, + onDidChangeTabs: new vscodeMocks.EventEmitter().event, + close: () => Promise.resolve(true), + }); // Notebook output helpers used by REPL tests. (mockedVSCode as any).NotebookCellOutputItem = { From dc303177a33074a6fcfed0ae80250a8dca0d315e Mon Sep 17 00:00:00 2001 From: Paul Donnelly Date: Thu, 8 Oct 2026 20:06:52 -0400 Subject: [PATCH 7/7] Fix REPL test stubbing of activeTextEditor --- src/test/repl/replCommand.test.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts index 8f8c313ee344..f0bc2dcbb10f 100644 --- a/src/test/repl/replCommand.test.ts +++ b/src/test/repl/replCommand.test.ts @@ -163,13 +163,8 @@ suite('REPL - register native repl command', () => { expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); - const originalActiveTextEditor = window.activeTextEditor; - (window as any).activeTextEditor = undefined; - try { - await commandHandler!('uri'); - } finally { - (window as any).activeTextEditor = originalActiveTextEditor; - } + sinon.stub(window, 'activeTextEditor').get(() => undefined); + await commandHandler!('uri'); sinon.assert.calledOnce(getNativeReplStub); });