Skip to content

Commit

Permalink
Refactor Native REPL code (microsoft#23550)
Browse files Browse the repository at this point in the history
Refactor Native REPL code.
Resolves: microsoft#23632

---------

Co-authored-by: Aaron Munger <[email protected]>
  • Loading branch information
anthonykim1 and amunger authored Jun 18, 2024
1 parent 6af3d03 commit 9abfd30
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 162 deletions.
112 changes: 112 additions & 0 deletions src/client/repl/nativeRepl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Native Repl class that holds instance of pythonServer and replController

import { NotebookController, NotebookControllerAffinity, NotebookDocument, TextEditor, workspace } from 'vscode';
import { Disposable } from 'vscode-jsonrpc';
import { PVSC_EXTENSION_ID } from '../common/constants';
import { PythonEnvironment } from '../pythonEnvironments/info';
import { createPythonServer, PythonServer } from './pythonServer';
import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler';
import { createReplController } from './replController';

export class NativeRepl implements Disposable {
private pythonServer: PythonServer;

private interpreter: PythonEnvironment;

private disposables: Disposable[] = [];

private replController: NotebookController;

private notebookDocument: NotebookDocument | undefined;

// TODO: In the future, could also have attribute of URI for file specific REPL.
constructor(interpreter: PythonEnvironment) {
this.interpreter = interpreter;

this.pythonServer = createPythonServer([interpreter.path as string]);
this.replController = this.setReplController();

this.watchNotebookClosed();
}

dispose(): void {
this.disposables.forEach((d) => d.dispose());
}

/**
* Function that watches for Notebook Closed event.
* This is for the purposes of correctly updating the notebookEditor and notebookDocument on close.
*/
private watchNotebookClosed(): void {
this.disposables.push(
workspace.onDidCloseNotebookDocument((nb) => {
if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) {
this.notebookDocument = undefined;
}
}),
);
}

/**
* Function that check if NotebookController for REPL exists, and returns it in Singleton manner.
* @returns NotebookController
*/
public setReplController(): NotebookController {
if (!this.replController) {
return createReplController(this.interpreter.path, this.disposables);
}
return this.replController;
}

/**
* Function that checks if native REPL's text input box contains complete code.
* @param activeEditor
* @param pythonServer
* @returns Promise<boolean> - True if complete/Valid code is present, False otherwise.
*/
public async checkUserInputCompleteCode(activeEditor: TextEditor | undefined): Promise<boolean> {
let completeCode = false;
let userTextInput;
if (activeEditor) {
const { document } = activeEditor;
userTextInput = document.getText();
}

// Check if userTextInput is a complete Python command
if (userTextInput) {
completeCode = await this.pythonServer.checkValidCommand(userTextInput);
}

return completeCode;
}

/**
* Function that opens interactive repl, selects kernel, and send/execute code to the native repl.
* @param code
*/
public async sendToNativeRepl(code: string): Promise<void> {
const notebookEditor = await openInteractiveREPL(this.replController, this.notebookDocument);
this.notebookDocument = notebookEditor.notebook;

if (this.notebookDocument) {
this.replController.updateNotebookAffinity(this.notebookDocument, NotebookControllerAffinity.Default);
await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID);
await executeNotebookCell(this.notebookDocument, code);
}
}
}

let nativeRepl: NativeRepl | undefined; // In multi REPL scenario, hashmap of URI to Repl.

/**
* Get Singleton Native REPL Instance
* @param interpreter
* @returns Native REPL instance
*/
export function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): NativeRepl {
if (!nativeRepl) {
nativeRepl = new NativeRepl(interpreter);
disposables.push(nativeRepl);
}
return nativeRepl;
}
100 changes: 100 additions & 0 deletions src/client/repl/replCommandHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
commands,
window,
NotebookController,
NotebookEditor,
ViewColumn,
NotebookDocument,
NotebookCellData,
NotebookCellKind,
NotebookEdit,
WorkspaceEdit,
workspace,
} from 'vscode';
import { getExistingReplViewColumn } from './replUtils';

/**
* Function that opens/show REPL using IW UI.
* @param notebookController
* @param notebookEditor
* @returns notebookEditor
*/
export async function openInteractiveREPL(
notebookController: NotebookController,
notebookDocument: NotebookDocument | undefined,
): Promise<NotebookEditor> {
let notebookEditor: NotebookEditor | undefined;

// Case where NotebookDocument (REPL document already exists in the tab)
if (notebookDocument) {
const existingReplViewColumn = getExistingReplViewColumn(notebookDocument);
const replViewColumn = existingReplViewColumn ?? ViewColumn.Beside;
notebookEditor = await window.showNotebookDocument(notebookDocument!, { viewColumn: replViewColumn });
} else if (!notebookDocument) {
// Case where NotebookDocument doesnt exist, open new REPL tab
const interactiveWindowObject = (await commands.executeCommand(
'interactive.open',
{
preserveFocus: true,
viewColumn: ViewColumn.Beside,
},
undefined,
notebookController.id,
'Python REPL',
)) as { notebookEditor: NotebookEditor };
notebookEditor = interactiveWindowObject.notebookEditor;
notebookDocument = interactiveWindowObject.notebookEditor.notebook;
}
return notebookEditor!;
}

/**
* Function that selects notebook Kernel.
* @param notebookEditor
* @param notebookControllerId
* @param extensionId
* @return Promise<void>
*/
export async function selectNotebookKernel(
notebookEditor: NotebookEditor,
notebookControllerId: string,
extensionId: string,
): Promise<void> {
await commands.executeCommand('notebook.selectKernel', {
notebookEditor,
id: notebookControllerId,
extension: extensionId,
});
}

/**
* Function that executes notebook cell given code.
* @param notebookDocument
* @param code
* @return Promise<void>
*/
export async function executeNotebookCell(notebookDocument: NotebookDocument, code: string): Promise<void> {
const { cellCount } = notebookDocument;
await addCellToNotebook(notebookDocument, code);
// Execute the cell
commands.executeCommand('notebook.cell.execute', {
ranges: [{ start: cellCount, end: cellCount + 1 }],
document: notebookDocument.uri,
});
}

/**
* Function that adds cell to notebook.
* This function will only get called when notebook document is defined.
* @param code
*
*/
async function addCellToNotebook(notebookDocument: NotebookDocument, code: string): Promise<void> {
const notebookCellData = new NotebookCellData(NotebookCellKind.Code, code as string, 'python');
const { cellCount } = notebookDocument!;
// Add new cell to interactive window document
const notebookEdit = NotebookEdit.insertCells(cellCount, [notebookCellData]);
const workspaceEdit = new WorkspaceEdit();
workspaceEdit.set(notebookDocument!.uri, [notebookEdit]);
await workspace.applyEdit(workspaceEdit);
}
Loading

0 comments on commit 9abfd30

Please sign in to comment.