Skip to content

Commit 75fc451

Browse files
committed
Allow switching interpreter mid-native repl session
1 parent cd4529c commit 75fc451

3 files changed

Lines changed: 116 additions & 15 deletions

File tree

src/client/repl/nativeRepl.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
// Native Repl class that holds instance of pythonServer and replController
55

66
import { NotebookController, NotebookDocument, QuickPickItem, TextEditor, Uri, WorkspaceFolder } from 'vscode';
7+
import * as path from 'path';
78
import { Disposable } from 'vscode-jsonrpc';
89
import { PVSC_EXTENSION_ID } from '../common/constants';
9-
import { showQuickPick } from '../common/vscodeApis/windowApis';
10+
import { showNotebookDocument, showQuickPick } from '../common/vscodeApis/windowApis';
1011
import { getWorkspaceFolders, onDidCloseNotebookDocument } from '../common/vscodeApis/workspaceApis';
1112
import { PythonEnvironment } from '../pythonEnvironments/info';
1213
import { createPythonServer, PythonServer } from './pythonServer';
@@ -18,6 +19,8 @@ import { VariablesProvider } from './variables/variablesProvider';
1819
import { VariableRequester } from './variables/variableRequester';
1920
import { getTabNameForUri } from './replUtils';
2021
import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../common/persistentState';
22+
import { onDidChangeEnvironmentEnvExt, useEnvExtension } from '../envExt/api.internal';
23+
import { getActiveInterpreterLegacy } from '../envExt/api.legacy';
2124

2225
export const NATIVE_REPL_URI_MEMENTO = 'nativeReplUri';
2326
let nativeRepl: NativeRepl | undefined;
@@ -37,6 +40,10 @@ export class NativeRepl implements Disposable {
3740

3841
public newReplSession: boolean | undefined = true;
3942

43+
private envChangeListenerRegistered = false;
44+
45+
private pendingInterpreterChangeTimer?: NodeJS.Timeout;
46+
4047
// TODO: In the future, could also have attribute of URI for file specific REPL.
4148
private constructor() {
4249
this.watchNotebookClosed();
@@ -49,6 +56,7 @@ export class NativeRepl implements Disposable {
4956
await nativeRepl.setReplDirectory();
5057
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
5158
nativeRepl.setReplController();
59+
nativeRepl.registerInterpreterChangeHandler();
5260

5361
return nativeRepl;
5462
}
@@ -116,8 +124,8 @@ export class NativeRepl implements Disposable {
116124
/**
117125
* Function that check if NotebookController for REPL exists, and returns it in Singleton manner.
118126
*/
119-
public setReplController(): NotebookController {
120-
if (!this.replController) {
127+
public setReplController(force: boolean = false): NotebookController {
128+
if (!this.replController || force) {
121129
this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd);
122130
this.replController.variableProvider = new VariablesProvider(
123131
new VariableRequester(this.pythonServer),
@@ -128,6 +136,64 @@ export class NativeRepl implements Disposable {
128136
return this.replController;
129137
}
130138

139+
private registerInterpreterChangeHandler(): void {
140+
if (!useEnvExtension() || this.envChangeListenerRegistered) {
141+
return;
142+
}
143+
this.envChangeListenerRegistered = true;
144+
this.disposables.push(
145+
onDidChangeEnvironmentEnvExt((event) => {
146+
this.updateInterpreterForChange(event.uri).catch(() => undefined);
147+
}),
148+
);
149+
}
150+
151+
private async updateInterpreterForChange(resource?: Uri): Promise<void> {
152+
if (this.pythonServer?.isExecuting) {
153+
this.scheduleInterpreterUpdate(resource);
154+
return;
155+
}
156+
if (!this.shouldApplyInterpreterChange(resource)) {
157+
return;
158+
}
159+
const scope = resource ?? (this.cwd ? Uri.file(this.cwd) : undefined);
160+
const interpreter = await getActiveInterpreterLegacy(scope);
161+
if (!interpreter || interpreter.path === this.interpreter?.path) {
162+
return;
163+
}
164+
165+
this.interpreter = interpreter;
166+
this.pythonServer.dispose();
167+
this.pythonServer = createPythonServer([interpreter.path as string], this.cwd);
168+
if (this.replController) {
169+
this.replController.dispose();
170+
}
171+
this.setReplController(true);
172+
173+
if (this.notebookDocument) {
174+
const notebookEditor = await showNotebookDocument(this.notebookDocument, { preserveFocus: true });
175+
await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID);
176+
}
177+
}
178+
179+
private scheduleInterpreterUpdate(resource?: Uri): void {
180+
if (this.pendingInterpreterChangeTimer) {
181+
clearTimeout(this.pendingInterpreterChangeTimer);
182+
}
183+
this.pendingInterpreterChangeTimer = setTimeout(() => {
184+
this.pendingInterpreterChangeTimer = undefined;
185+
this.updateInterpreterForChange(resource).catch(() => undefined);
186+
}, 200);
187+
}
188+
189+
private shouldApplyInterpreterChange(resource?: Uri): boolean {
190+
if (!resource || !this.cwd) {
191+
return true;
192+
}
193+
const relative = path.relative(this.cwd, resource.fsPath);
194+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
195+
}
196+
131197
/**
132198
* Function that checks if native REPL's text input box contains complete code.
133199
* @returns Promise<boolean> - True if complete/Valid code is present, False otherwise.

src/client/repl/pythonServer.ts

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export interface ExecutionResult {
1616

1717
export interface PythonServer extends Disposable {
1818
onCodeExecuted: Event<void>;
19+
readonly isExecuting: boolean;
20+
readonly isDisposed: boolean;
1921
execute(code: string): Promise<ExecutionResult | undefined>;
2022
executeSilently(code: string): Promise<ExecutionResult | undefined>;
2123
interrupt(): void;
@@ -30,6 +32,18 @@ class PythonServerImpl implements PythonServer, Disposable {
3032

3133
onCodeExecuted = this._onCodeExecuted.event;
3234

35+
private inFlightRequests = 0;
36+
37+
private disposed = false;
38+
39+
public get isExecuting(): boolean {
40+
return this.inFlightRequests > 0;
41+
}
42+
43+
public get isDisposed(): boolean {
44+
return this.disposed;
45+
}
46+
3347
constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) {
3448
this.initialize();
3549
this.input();
@@ -41,6 +55,14 @@ class PythonServerImpl implements PythonServer, Disposable {
4155
traceLog('Log:', message);
4256
}),
4357
);
58+
this.pythonServer.on('exit', (code) => {
59+
traceError(`Python server exited with code ${code}`);
60+
this.markDisposed();
61+
});
62+
this.pythonServer.on('error', (err) => {
63+
traceError(err);
64+
this.markDisposed();
65+
});
4466
this.connection.listen();
4567
}
4668

@@ -75,12 +97,15 @@ class PythonServerImpl implements PythonServer, Disposable {
7597
}
7698

7799
private async executeCode(code: string): Promise<ExecutionResult | undefined> {
100+
this.inFlightRequests += 1;
78101
try {
79102
const result = await this.connection.sendRequest('execute', code);
80103
return result as ExecutionResult;
81104
} catch (err) {
82105
const error = err as Error;
83106
traceError(`Error getting response from REPL server:`, error);
107+
} finally {
108+
this.inFlightRequests -= 1;
84109
}
85110
return undefined;
86111
}
@@ -93,39 +118,47 @@ class PythonServerImpl implements PythonServer, Disposable {
93118
}
94119

95120
public async checkValidCommand(code: string): Promise<boolean> {
96-
const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code);
97-
if (completeCode.output === 'True') {
98-
return new Promise((resolve) => resolve(true));
121+
this.inFlightRequests += 1;
122+
try {
123+
const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code);
124+
return completeCode.output === 'True';
125+
} finally {
126+
this.inFlightRequests -= 1;
99127
}
100-
return new Promise((resolve) => resolve(false));
101128
}
102129

103130
public dispose(): void {
131+
if (this.disposed) {
132+
return;
133+
}
134+
this.disposed = true;
104135
this.connection.sendNotification('exit');
105136
this.disposables.forEach((d) => d.dispose());
106137
this.connection.dispose();
107138
serverInstance = undefined;
108139
}
140+
141+
private markDisposed(): void {
142+
if (this.disposed) {
143+
return;
144+
}
145+
this.disposed = true;
146+
this.connection.dispose();
147+
serverInstance = undefined;
148+
}
109149
}
110150

111151
export function createPythonServer(interpreter: string[], cwd?: string): PythonServer {
112-
if (serverInstance) {
152+
if (serverInstance && !serverInstance.isDisposed) {
113153
return serverInstance;
114154
}
115155

116156
const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], {
117157
cwd, // Launch with correct workspace directory
118158
});
119-
120159
pythonServer.stderr.on('data', (data) => {
121160
traceError(data.toString());
122161
});
123-
pythonServer.on('exit', (code) => {
124-
traceError(`Python server exited with code ${code}`);
125-
});
126-
pythonServer.on('error', (err) => {
127-
traceError(err);
128-
});
129162
const connection = rpc.createMessageConnection(
130163
new rpc.StreamMessageReader(pythonServer.stdout),
131164
new rpc.StreamMessageWriter(pythonServer.stdin),

src/test/repl/nativeRepl.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ suite('REPL - Native REPL', () => {
129129
input: sinon.stub(),
130130
checkValidCommand: sinon.stub().resolves(true),
131131
dispose: sinon.stub(),
132+
isExecuting: false,
133+
isDisposed: false,
132134
};
133135

134136
// Track the number of times createPythonServer was called

0 commit comments

Comments
 (0)