Skip to content

Commit 15f7e0b

Browse files
committed
Fix native repl not using env extension
1 parent 0b477a3 commit 15f7e0b

File tree

5 files changed

+179
-25
lines changed

5 files changed

+179
-25
lines changed

src/client/repl/nativeRepl.ts

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { PVSC_EXTENSION_ID } from '../common/constants';
99
import { showQuickPick } from '../common/vscodeApis/windowApis';
1010
import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis';
1111
import { PythonEnvironment } from '../pythonEnvironments/info';
12-
import { createPythonServer, PythonServer } from './pythonServer';
12+
import { createPythonServer, createPythonServerEnvExt, PythonServer } from './pythonServer';
1313
import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler';
1414
import { createReplController } from './replController';
1515
import { EventName } from '../telemetry/constants';
@@ -18,6 +18,8 @@ import { VariablesProvider } from './variables/variablesProvider';
1818
import { VariableRequester } from './variables/variableRequester';
1919
import { getTabNameForUri } from './replUtils';
2020
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState';
21+
import { getEnvironment, useEnvExtension } from '../envExt/api.internal';
22+
import { traceError, traceVerbose } from '../logging';
2123

2224
export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri';
2325
let nativeRepl: NativeRepl | undefined;
@@ -29,6 +31,8 @@ export class NativeRepl implements Disposable {
2931

3032
private interpreter!: PythonEnvironment;
3133

34+
private resourceUri: Uri | undefined;
35+
3236
private disposables: Disposable[] = [];
3337

3438
private replController!: NotebookController;
@@ -42,12 +46,38 @@ export class NativeRepl implements Disposable {
4246
this.watchNotebookClosed();
4347
}
4448

49+
/**
50+
* Get the interpreter path associated with this REPL instance.
51+
* Used to detect when interpreter has changed and REPL needs to be recreated.
52+
*/
53+
public get interpreterPath(): string {
54+
return this.interpreter.path;
55+
}
56+
4557
// Static async factory method to handle asynchronous initialization
46-
public static async create(interpreter: PythonEnvironment): Promise<NativeRepl> {
58+
public static async create(interpreter: PythonEnvironment, resource?: Uri): Promise<NativeRepl> {
4759
const nativeRepl = new NativeRepl();
4860
nativeRepl.interpreter = interpreter;
61+
nativeRepl.resourceUri = resource;
4962
await nativeRepl.setReplDirectory();
50-
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
63+
64+
// Use env extension's runInBackground if available for proper environment handling
65+
if (useEnvExtension()) {
66+
const pythonEnv = await getEnvironment(resource);
67+
if (pythonEnv) {
68+
traceVerbose(
69+
`Creating REPL server using environment extension for: ${pythonEnv.execInfo.run.executable}`,
70+
);
71+
nativeRepl.pythonServer = await createPythonServerEnvExt(pythonEnv, nativeRepl.cwd);
72+
} else {
73+
traceError('Failed to get environment from env extension, falling back to direct spawn');
74+
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
75+
}
76+
} else {
77+
traceVerbose(`Creating REPL server using direct spawn for: ${interpreter.path}`);
78+
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
79+
}
80+
5181
nativeRepl.setReplController();
5282

5383
return nativeRepl;
@@ -69,7 +99,19 @@ export class NativeRepl implements Disposable {
6999
this.newReplSession = true;
70100
await updateWorkspaceStateValue<string | undefined>(NATIVE_REPL_URI_MEMENTO, undefined);
71101
this.pythonServer.dispose();
72-
this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd);
102+
103+
// Recreate Python server - use env extension if available
104+
if (useEnvExtension()) {
105+
const pythonEnv = await getEnvironment(this.resourceUri);
106+
if (pythonEnv) {
107+
this.pythonServer = await createPythonServerEnvExt(pythonEnv, this.cwd);
108+
} else {
109+
this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd);
110+
}
111+
} else {
112+
this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd);
113+
}
114+
73115
this.disposables.push(this.pythonServer);
74116
if (this.replController) {
75117
this.replController.dispose();
@@ -118,7 +160,12 @@ export class NativeRepl implements Disposable {
118160
*/
119161
public setReplController(): NotebookController {
120162
if (!this.replController) {
121-
this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd);
163+
this.replController = createReplController(
164+
this.interpreter!.path,
165+
this.disposables,
166+
this.cwd,
167+
this.pythonServer,
168+
);
122169
this.replController.variableProvider = new VariablesProvider(
123170
new VariableRequester(this.pythonServer),
124171
() => this.notebookDocument,
@@ -183,13 +230,28 @@ export class NativeRepl implements Disposable {
183230
}
184231

185232
/**
186-
* Get Singleton Native REPL Instance
233+
* Get Singleton Native REPL Instance.
234+
* Recreates the REPL if the interpreter has changed (e.g., when using environment extension).
187235
* @param interpreter
236+
* @param disposables
237+
* @param resource - Optional resource URI used to get the environment from the env extension
188238
* @returns Native REPL instance
189239
*/
190-
export async function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): Promise<NativeRepl> {
240+
export async function getNativeRepl(
241+
interpreter: PythonEnvironment,
242+
disposables: Disposable[],
243+
resource?: Uri,
244+
): Promise<NativeRepl> {
245+
// Check if interpreter has changed - if so, dispose and recreate the REPL
246+
// This serves as a fallback in case the event listener didn't catch the change
247+
if (nativeRepl && nativeRepl.interpreterPath !== interpreter.path) {
248+
traceVerbose(`Interpreter changed to ${interpreter.path}, disposing Native REPL`);
249+
nativeRepl.dispose();
250+
nativeRepl = undefined;
251+
}
252+
191253
if (!nativeRepl) {
192-
nativeRepl = await NativeRepl.create(interpreter);
254+
nativeRepl = await NativeRepl.create(interpreter, resource);
193255
disposables.push(nativeRepl);
194256
}
195257
if (nativeRepl && nativeRepl.newReplSession) {

src/client/repl/pythonServer.ts

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,30 @@ import { EXTENSION_ROOT_DIR } from '../constants';
66
import { traceError, traceLog } from '../logging';
77
import { captureTelemetry } from '../telemetry';
88
import { EventName } from '../telemetry/constants';
9+
import { PythonEnvironment as PythonEnvironmentEnvExt, PythonProcess } from '../envExt/types';
10+
import { runInBackground } from '../envExt/api.internal';
911

1012
const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py');
13+
14+
/**
15+
* Wrapper interface to unify ch.ChildProcess and PythonProcess from env extension
16+
*/
17+
interface PythonProcessWrapper {
18+
readonly stdout: NodeJS.ReadableStream;
19+
readonly stdin: NodeJS.WritableStream;
20+
readonly stderr: NodeJS.ReadableStream;
21+
kill(signal?: string): boolean;
22+
}
23+
1124
let serverInstance: PythonServer | undefined;
25+
26+
/**
27+
* Clear the server instance. Used when the environment changes and we need to recreate the server.
28+
*/
29+
export function clearServerInstance(): void {
30+
serverInstance = undefined;
31+
}
32+
1233
export interface ExecutionResult {
1334
status: boolean;
1435
output: string;
@@ -30,7 +51,7 @@ class PythonServerImpl implements PythonServer, Disposable {
3051

3152
onCodeExecuted = this._onCodeExecuted.event;
3253

33-
constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) {
54+
constructor(private connection: rpc.MessageConnection, private pythonServer: PythonProcessWrapper) {
3455
this.initialize();
3556
this.input();
3657
}
@@ -108,6 +129,33 @@ class PythonServerImpl implements PythonServer, Disposable {
108129
}
109130
}
110131

132+
/**
133+
* Wrap a ch.ChildProcess to match PythonProcessWrapper interface
134+
*/
135+
function wrapChildProcess(proc: ch.ChildProcess): PythonProcessWrapper {
136+
return {
137+
stdout: proc.stdout!,
138+
stdin: proc.stdin!,
139+
stderr: proc.stderr!,
140+
kill: (signal?: string) => proc.kill(signal as NodeJS.Signals),
141+
};
142+
}
143+
144+
/**
145+
* Wrap a PythonProcess from env extension to match PythonProcessWrapper interface
146+
*/
147+
function wrapEnvExtProcess(proc: PythonProcess): PythonProcessWrapper {
148+
return {
149+
stdout: proc.stdout,
150+
stdin: proc.stdin as NodeJS.WritableStream,
151+
stderr: proc.stderr,
152+
kill: () => {
153+
proc.kill();
154+
return true;
155+
},
156+
};
157+
}
158+
111159
export function createPythonServer(interpreter: string[], cwd?: string): PythonServer {
112160
if (serverInstance) {
113161
return serverInstance;
@@ -127,9 +175,41 @@ export function createPythonServer(interpreter: string[], cwd?: string): PythonS
127175
traceError(err);
128176
});
129177
const connection = rpc.createMessageConnection(
130-
new rpc.StreamMessageReader(pythonServer.stdout),
131-
new rpc.StreamMessageWriter(pythonServer.stdin),
178+
new rpc.StreamMessageReader(pythonServer.stdout!),
179+
new rpc.StreamMessageWriter(pythonServer.stdin!),
180+
);
181+
serverInstance = new PythonServerImpl(connection, wrapChildProcess(pythonServer));
182+
return serverInstance;
183+
}
184+
185+
/**
186+
* Create a Python server using the environment extension's runInBackground API.
187+
* This properly handles environment activation and variables.
188+
*/
189+
export async function createPythonServerEnvExt(
190+
pythonEnv: PythonEnvironmentEnvExt,
191+
cwd?: string,
192+
): Promise<PythonServer> {
193+
if (serverInstance) {
194+
return serverInstance;
195+
}
196+
197+
const pythonProcess = await runInBackground(pythonEnv, {
198+
args: [SERVER_PATH],
199+
cwd,
200+
});
201+
202+
pythonProcess.stderr.on('data', (data: Buffer) => {
203+
traceError(data.toString());
204+
});
205+
pythonProcess.onExit((code) => {
206+
traceError(`Python server exited with code ${code}`);
207+
});
208+
209+
const connection = rpc.createMessageConnection(
210+
new rpc.StreamMessageReader(pythonProcess.stdout),
211+
new rpc.StreamMessageWriter(pythonProcess.stdin as NodeJS.WritableStream),
132212
);
133-
serverInstance = new PythonServerImpl(connection, pythonServer);
213+
serverInstance = new PythonServerImpl(connection, wrapEnvExtProcess(pythonProcess));
134214
return serverInstance;
135215
}

src/client/repl/replCommands.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ import { sendTelemetryEvent } from '../telemetry';
1919
import { EventName } from '../telemetry/constants';
2020
import { ReplType } from './types';
2121

22+
/**
23+
* Get the resource URI, falling back to active editor if not provided.
24+
*/
25+
function getResourceUri(uri?: Uri): Uri | undefined {
26+
if (uri) {
27+
return uri;
28+
}
29+
// Fallback to active editor's document URI
30+
return window.activeTextEditor?.document.uri;
31+
}
32+
2233
/**
2334
* Register Start Native REPL command in the command palette
2435
*/
@@ -29,9 +40,10 @@ export async function registerStartNativeReplCommand(
2940
disposables.push(
3041
registerCommand(Commands.Start_Native_REPL, async (uri: Uri) => {
3142
sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' });
32-
const interpreter = await getActiveInterpreter(uri, interpreterService);
43+
const resource = getResourceUri(uri);
44+
const interpreter = await getActiveInterpreter(resource, interpreterService);
3345
if (interpreter) {
34-
const nativeRepl = await getNativeRepl(interpreter, disposables);
46+
const nativeRepl = await getNativeRepl(interpreter, disposables, resource);
3547
await nativeRepl.sendToNativeRepl(undefined, false);
3648
}
3749
}),
@@ -55,10 +67,11 @@ export async function registerReplCommands(
5567
await executeInTerminal();
5668
return;
5769
}
58-
const interpreter = await getActiveInterpreter(uri, interpreterService);
70+
const resource = getResourceUri(uri);
71+
const interpreter = await getActiveInterpreter(resource, interpreterService);
5972

6073
if (interpreter) {
61-
const nativeRepl = await getNativeRepl(interpreter, disposables);
74+
const nativeRepl = await getNativeRepl(interpreter, disposables, resource);
6275
const activeEditor = window.activeTextEditor;
6376
if (activeEditor) {
6477
const code = await getSelectedTextToExecute(activeEditor);
@@ -107,13 +120,14 @@ async function onInputEnter(
107120
interpreterService: IInterpreterService,
108121
disposables: Disposable[],
109122
): Promise<void> {
110-
const interpreter = await interpreterService.getActiveInterpreter(uri);
123+
const resource = getResourceUri(uri);
124+
const interpreter = await interpreterService.getActiveInterpreter(resource);
111125
if (!interpreter) {
112-
commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop);
126+
commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop);
113127
return;
114128
}
115129

116-
const nativeRepl = await getNativeRepl(interpreter, disposables);
130+
const nativeRepl = await getNativeRepl(interpreter, disposables, resource);
117131
const completeCode = await nativeRepl?.checkUserInputCompleteCode(window.activeTextEditor);
118132
const editor = window.activeTextEditor;
119133

src/client/repl/replController.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import * as vscode from 'vscode';
2-
import { createPythonServer } from './pythonServer';
2+
import { PythonServer } from './pythonServer';
33

44
export function createReplController(
55
interpreterPath: string,
66
disposables: vscode.Disposable[],
7-
cwd?: string,
7+
cwd: string | undefined,
8+
server: PythonServer,
89
): vscode.NotebookController {
9-
const server = createPythonServer([interpreterPath], cwd);
10-
disposables.push(server);
11-
1210
const controller = vscode.notebooks.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL');
1311
controller.supportedLanguages = ['python'];
1412

src/client/repl/replUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export function isMultiLineText(textEditor: TextEditor): boolean {
6666
* Function will also return undefined or active interpreter
6767
*/
6868
export async function getActiveInterpreter(
69-
uri: Uri,
69+
uri: Uri | undefined,
7070
interpreterService: IInterpreterService,
7171
): Promise<PythonEnvironment | undefined> {
7272
const interpreter = await interpreterService.getActiveInterpreter(uri);

0 commit comments

Comments
 (0)