Skip to content

Commit 0cfd96d

Browse files
authored
Merge branch 'main' into test-project-support
2 parents f048ef4 + bd9b412 commit 0cfd96d

File tree

6 files changed

+169
-23
lines changed

6 files changed

+169
-23
lines changed

src/client/repl/nativeRepl.ts

Lines changed: 70 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 pendingInterpreterChange?: { resource?: Uri };
46+
4047
// TODO: In the future, could also have attribute of URI for file specific REPL.
4148
private constructor() {
4249
this.watchNotebookClosed();
@@ -48,7 +55,9 @@ export class NativeRepl implements Disposable {
4855
nativeRepl.interpreter = interpreter;
4956
await nativeRepl.setReplDirectory();
5057
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
58+
nativeRepl.disposables.push(nativeRepl.pythonServer);
5159
nativeRepl.setReplController();
60+
nativeRepl.registerInterpreterChangeHandler();
5261

5362
return nativeRepl;
5463
}
@@ -116,8 +125,8 @@ export class NativeRepl implements Disposable {
116125
/**
117126
* Function that check if NotebookController for REPL exists, and returns it in Singleton manner.
118127
*/
119-
public setReplController(): NotebookController {
120-
if (!this.replController) {
128+
public setReplController(force: boolean = false): NotebookController {
129+
if (!this.replController || force) {
121130
this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd);
122131
this.replController.variableProvider = new VariablesProvider(
123132
new VariableRequester(this.pythonServer),
@@ -128,6 +137,64 @@ export class NativeRepl implements Disposable {
128137
return this.replController;
129138
}
130139

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { commands, Uri, window } from 'vscode';
22
import { Disposable } from 'vscode-jsonrpc';
33
import { ICommandManager } from '../common/application/types';
44
import { Commands } from '../common/constants';
5-
import { noop } from '../common/utils/misc';
65
import { IInterpreterService } from '../interpreter/contracts';
76
import { ICodeExecutionHelper } from '../terminals/types';
87
import { getNativeRepl } from './nativeRepl';
@@ -102,14 +101,13 @@ export async function registerReplExecuteOnEnter(
102101
}
103102

104103
async function onInputEnter(
105-
uri: Uri,
104+
uri: Uri | undefined,
106105
commandName: string,
107106
interpreterService: IInterpreterService,
108107
disposables: Disposable[],
109108
): Promise<void> {
110-
const interpreter = await interpreterService.getActiveInterpreter(uri);
109+
const interpreter = await getActiveInterpreter(uri, interpreterService);
111110
if (!interpreter) {
112-
commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop);
113111
return;
114112
}
115113

src/client/repl/replUtils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ 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> {
72-
const interpreter = await interpreterService.getActiveInterpreter(uri);
72+
const resource = uri ?? getActiveResource();
73+
const interpreter = await interpreterService.getActiveInterpreter(resource);
7374
if (!interpreter) {
74-
commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop);
75+
commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop);
7576
return undefined;
7677
}
7778
return interpreter;

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

src/test/repl/replCommand.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Create test suite and test cases for the `replUtils` module
22
import * as TypeMoq from 'typemoq';
3-
import { Disposable } from 'vscode';
3+
import { commands, Disposable, Uri } from 'vscode';
44
import * as sinon from 'sinon';
55
import { expect } from 'chai';
66
import { IInterpreterService } from '../../client/interpreter/contracts';
@@ -9,6 +9,7 @@ import { ICodeExecutionHelper } from '../../client/terminals/types';
99
import * as replCommands from '../../client/repl/replCommands';
1010
import * as replUtils from '../../client/repl/replUtils';
1111
import * as nativeRepl from '../../client/repl/nativeRepl';
12+
import * as windowApis from '../../client/common/vscodeApis/windowApis';
1213
import { Commands } from '../../client/common/constants';
1314
import { PythonEnvironment } from '../../client/pythonEnvironments/info';
1415

@@ -203,3 +204,47 @@ suite('REPL - register native repl command', () => {
203204
sinon.assert.notCalled(getNativeReplStub);
204205
});
205206
});
207+
208+
suite('Native REPL getActiveInterpreter', () => {
209+
let interpreterService: TypeMoq.IMock<IInterpreterService>;
210+
let executeCommandStub: sinon.SinonStub;
211+
let getActiveResourceStub: sinon.SinonStub;
212+
213+
setup(() => {
214+
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
215+
executeCommandStub = sinon.stub(commands, 'executeCommand').resolves(undefined);
216+
getActiveResourceStub = sinon.stub(windowApis, 'getActiveResource');
217+
});
218+
219+
teardown(() => {
220+
sinon.restore();
221+
});
222+
223+
test('Uses active resource when uri is undefined', async () => {
224+
const resource = Uri.file('/workspace/app.py');
225+
const expected = ({ path: 'ps' } as unknown) as PythonEnvironment;
226+
getActiveResourceStub.returns(resource);
227+
interpreterService
228+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)))
229+
.returns(() => Promise.resolve(expected));
230+
231+
const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object);
232+
233+
expect(result).to.equal(expected);
234+
interpreterService.verify((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)), TypeMoq.Times.once());
235+
sinon.assert.notCalled(executeCommandStub);
236+
});
237+
238+
test('Triggers environment selection using active resource when interpreter is missing', async () => {
239+
const resource = Uri.file('/workspace/app.py');
240+
getActiveResourceStub.returns(resource);
241+
interpreterService
242+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)))
243+
.returns(() => Promise.resolve(undefined));
244+
245+
const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object);
246+
247+
expect(result).to.equal(undefined);
248+
sinon.assert.calledWith(executeCommandStub, Commands.TriggerEnvironmentSelection, resource);
249+
});
250+
});

0 commit comments

Comments
 (0)