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 f30b8d9cbf6f..7d5ede2367b4 100644 --- a/src/client/repl/replController.ts +++ b/src/client/repl/replController.ts @@ -23,12 +23,13 @@ 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); + 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 2cf18cefe1f7..0e674aa80d4d 100644 --- a/src/test/repl/nativeRepl.test.ts +++ b/src/test/repl/nativeRepl.test.ts @@ -5,6 +5,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as TypeMoq from 'typemoq'; import * as sinon from 'sinon'; +import * as vscode from 'vscode'; import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; import { expect } from 'chai'; @@ -114,6 +115,63 @@ 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; + + sinon.stub(vscode.notebooks, 'createNotebookController').returns(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.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 { + disposables.forEach((disposable) => disposable.dispose()); + } + }); + 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/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts index 0b5edda863f9..f0bc2dcbb10f 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,6 +163,7 @@ suite('REPL - register native repl command', () => { expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + sinon.stub(window, 'activeTextEditor').get(() => undefined); await commandHandler!('uri'); sinon.assert.calledOnce(getNativeReplStub); }); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index ac64384520cf..d644db906e54 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -41,6 +41,32 @@ 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 = { + 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();