Skip to content

Commit e90b95d

Browse files
authored
Allow native REPL launch from command palette (#23912)
Resolves: #23727 Allow users to launch Native REPL via command palette. Will also be handling #23821 in this PR. -- setting proper workspace directory. Related: #23656 Covering scenarios: - Provide selection option if user is in multi-workspace scenario (already included in PR) - Automatically pick workspace as directory for context of REPL if user is in single-workspace scenario (already included in PR) - Handle case where user does not open any workspace and attempt to launch native REPL from plain/empty VS Code instance via command palette option (already included in PR)
1 parent 48e277a commit e90b95d

11 files changed

+191
-34
lines changed

package-lock.json

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,12 @@
344344
{
345345
"category": "Python",
346346
"command": "python.startREPL",
347-
"title": "%python.command.python.startREPL.title%"
347+
"title": "%python.command.python.startTerminalREPL.title%"
348+
},
349+
{
350+
"category": "Python",
351+
"command": "python.startNativeREPL",
352+
"title": "%python.command.python.startNativeREPL.title%"
348353
},
349354
{
350355
"category": "Python",
@@ -1328,7 +1333,13 @@
13281333
{
13291334
"category": "Python",
13301335
"command": "python.startREPL",
1331-
"title": "%python.command.python.startREPL.title%",
1336+
"title": "%python.command.python.startTerminalREPL.title%",
1337+
"when": "!virtualWorkspace && shellExecutionSupported"
1338+
},
1339+
{
1340+
"category": "Python",
1341+
"command": "python.startNativeREPL",
1342+
"title": "%python.command.python.startNativeREPL.title%",
13321343
"when": "!virtualWorkspace && shellExecutionSupported"
13331344
},
13341345
{

package.nls.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"python.command.python.startREPL.title": "Start Terminal REPL",
2+
"python.command.python.startTerminalREPL.title": "Start Terminal REPL",
3+
"python.command.python.startNativeREPL.title": "Start Native Python REPL",
34
"python.command.python.createEnvironment.title": "Create Environment...",
45
"python.command.python.createNewFile.title": "New Python File",
56
"python.command.python.createTerminal.title": "Create Terminal",

src/client/common/application/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
9696
['workbench.action.openIssueReporter']: [{ extensionId: string; issueBody: string }];
9797
[Commands.GetSelectedInterpreterPath]: [{ workspaceFolder: string } | string[]];
9898
[Commands.TriggerEnvironmentSelection]: [undefined | Uri];
99+
[Commands.Start_Native_REPL]: [undefined | Uri];
99100
[Commands.Exec_In_REPL]: [undefined | Uri];
100101
[Commands.Exec_In_REPL_Enter]: [undefined | Uri];
101102
[Commands.Exec_In_Terminal]: [undefined, Uri];

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export namespace Commands {
6262
export const Set_Interpreter = 'python.setInterpreter';
6363
export const Set_ShebangInterpreter = 'python.setShebangInterpreter';
6464
export const Start_REPL = 'python.startREPL';
65+
export const Start_Native_REPL = 'python.startNativeREPL';
6566
export const Tests_Configure = 'python.configureTests';
6667
export const TriggerEnvironmentSelection = 'python.triggerEnvSelection';
6768
export const ViewOutput = 'python.viewOutput';

src/client/extensionActivation.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import { initializePersistentStateForTriggers } from './common/persistentState';
5252
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
5353
import { DebuggerTypeName } from './debugger/constants';
5454
import { StopWatch } from './common/utils/stopWatch';
55-
import { registerReplCommands, registerReplExecuteOnEnter } from './repl/replCommands';
55+
import { registerReplCommands, registerReplExecuteOnEnter, registerStartNativeReplCommand } from './repl/replCommands';
5656

5757
export async function activateComponents(
5858
// `ext` is passed to any extra activation funcs.
@@ -108,6 +108,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
108108
);
109109
const executionHelper = ext.legacyIOC.serviceContainer.get<ICodeExecutionHelper>(ICodeExecutionHelper);
110110
const commandManager = ext.legacyIOC.serviceContainer.get<ICommandManager>(ICommandManager);
111+
registerStartNativeReplCommand(ext.disposables, interpreterService);
111112
registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager);
112113
registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager);
113114
}

src/client/repl/nativeRepl.ts

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,51 @@
11
// Native Repl class that holds instance of pythonServer and replController
22

3-
import { NotebookController, NotebookControllerAffinity, NotebookDocument, TextEditor, workspace } from 'vscode';
3+
import {
4+
NotebookController,
5+
NotebookControllerAffinity,
6+
NotebookDocument,
7+
QuickPickItem,
8+
TextEditor,
9+
workspace,
10+
WorkspaceFolder,
11+
} from 'vscode';
412
import { Disposable } from 'vscode-jsonrpc';
513
import { PVSC_EXTENSION_ID } from '../common/constants';
14+
import { showQuickPick } from '../common/vscodeApis/windowApis';
15+
import { getWorkspaceFolders } from '../common/vscodeApis/workspaceApis';
616
import { PythonEnvironment } from '../pythonEnvironments/info';
717
import { createPythonServer, PythonServer } from './pythonServer';
818
import { executeNotebookCell, openInteractiveREPL, selectNotebookKernel } from './replCommandHandler';
919
import { createReplController } from './replController';
1020

1121
export class NativeRepl implements Disposable {
12-
private pythonServer: PythonServer;
22+
// Adding ! since it will get initialized in create method, not the constructor.
23+
private pythonServer!: PythonServer;
1324

14-
private interpreter: PythonEnvironment;
25+
private cwd: string | undefined;
26+
27+
private interpreter!: PythonEnvironment;
1528

1629
private disposables: Disposable[] = [];
1730

18-
private replController: NotebookController;
31+
private replController!: NotebookController;
1932

2033
private notebookDocument: NotebookDocument | undefined;
2134

2235
// TODO: In the future, could also have attribute of URI for file specific REPL.
23-
constructor(interpreter: PythonEnvironment) {
24-
this.interpreter = interpreter;
36+
private constructor() {
37+
this.watchNotebookClosed();
38+
}
2539

26-
this.pythonServer = createPythonServer([interpreter.path as string]);
27-
this.replController = this.setReplController();
40+
// Static async factory method to handle asynchronous initialization
41+
public static async create(interpreter: PythonEnvironment): Promise<NativeRepl> {
42+
const nativeRepl = new NativeRepl();
43+
nativeRepl.interpreter = interpreter;
44+
await nativeRepl.setReplDirectory();
45+
nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd);
46+
nativeRepl.replController = nativeRepl.setReplController();
2847

29-
this.watchNotebookClosed();
48+
return nativeRepl;
3049
}
3150

3251
dispose(): void {
@@ -47,13 +66,46 @@ export class NativeRepl implements Disposable {
4766
);
4867
}
4968

69+
/**
70+
* Function that set up desired directory for REPL.
71+
* If there is multiple workspaces, prompt the user to choose
72+
* which directory we should set in context of native REPL.
73+
*/
74+
private async setReplDirectory(): Promise<void> {
75+
// Figure out uri via workspaceFolder as uri parameter always
76+
// seem to be undefined from parameter when trying to access from replCommands.ts
77+
const workspaces: readonly WorkspaceFolder[] | undefined = getWorkspaceFolders();
78+
79+
if (workspaces) {
80+
// eslint-disable-next-line no-shadow
81+
const workspacesQuickPickItems: QuickPickItem[] = workspaces.map((workspace) => ({
82+
label: workspace.name,
83+
description: workspace.uri.fsPath,
84+
}));
85+
86+
if (workspacesQuickPickItems.length === 0) {
87+
this.cwd = process.cwd(); // Yields '/' on no workspace scenario.
88+
} else if (workspacesQuickPickItems.length === 1) {
89+
this.cwd = workspacesQuickPickItems[0].description;
90+
} else {
91+
// Show choices of workspaces for user to choose from.
92+
const selection = (await showQuickPick(workspacesQuickPickItems, {
93+
placeHolder: 'Select current working directory for new REPL',
94+
matchOnDescription: true,
95+
ignoreFocusOut: true,
96+
})) as QuickPickItem;
97+
this.cwd = selection?.description;
98+
}
99+
}
100+
}
101+
50102
/**
51103
* Function that check if NotebookController for REPL exists, and returns it in Singleton manner.
52104
* @returns NotebookController
53105
*/
54106
public setReplController(): NotebookController {
55107
if (!this.replController) {
56-
return createReplController(this.interpreter.path, this.disposables);
108+
return createReplController(this.interpreter!.path, this.disposables, this.cwd);
57109
}
58110
return this.replController;
59111
}
@@ -84,14 +136,16 @@ export class NativeRepl implements Disposable {
84136
* Function that opens interactive repl, selects kernel, and send/execute code to the native repl.
85137
* @param code
86138
*/
87-
public async sendToNativeRepl(code: string): Promise<void> {
139+
public async sendToNativeRepl(code?: string): Promise<void> {
88140
const notebookEditor = await openInteractiveREPL(this.replController, this.notebookDocument);
89141
this.notebookDocument = notebookEditor.notebook;
90142

91143
if (this.notebookDocument) {
92144
this.replController.updateNotebookAffinity(this.notebookDocument, NotebookControllerAffinity.Default);
93145
await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID);
94-
await executeNotebookCell(this.notebookDocument, code);
146+
if (code) {
147+
await executeNotebookCell(this.notebookDocument, code);
148+
}
95149
}
96150
}
97151
}
@@ -103,9 +157,9 @@ let nativeRepl: NativeRepl | undefined; // In multi REPL scenario, hashmap of UR
103157
* @param interpreter
104158
* @returns Native REPL instance
105159
*/
106-
export function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): NativeRepl {
160+
export async function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): Promise<NativeRepl> {
107161
if (!nativeRepl) {
108-
nativeRepl = new NativeRepl(interpreter);
162+
nativeRepl = await NativeRepl.create(interpreter);
109163
disposables.push(nativeRepl);
110164
}
111165
return nativeRepl;

src/client/repl/pythonServer.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,14 @@ class PythonServerImpl implements Disposable {
8989
}
9090
}
9191

92-
export function createPythonServer(interpreter: string[]): PythonServer {
92+
export function createPythonServer(interpreter: string[], cwd?: string): PythonServer {
9393
if (serverInstance) {
9494
return serverInstance;
9595
}
9696

97-
const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH]);
97+
const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], {
98+
cwd, // Launch with correct workspace directory
99+
});
98100

99101
pythonServer.stderr.on('data', (data) => {
100102
traceError(data.toString());

src/client/repl/replCommands.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,32 @@ import {
1414
insertNewLineToREPLInput,
1515
isMultiLineText,
1616
} from './replUtils';
17+
import { registerCommand } from '../common/vscodeApis/commandApis';
18+
19+
/**
20+
* Register Start Native REPL command in the command palette
21+
*
22+
* @param disposables
23+
* @param interpreterService
24+
* @param commandManager
25+
* @returns Promise<void>
26+
*/
27+
export async function registerStartNativeReplCommand(
28+
disposables: Disposable[],
29+
interpreterService: IInterpreterService,
30+
): Promise<void> {
31+
disposables.push(
32+
registerCommand(Commands.Start_Native_REPL, async (uri: Uri) => {
33+
const interpreter = await getActiveInterpreter(uri, interpreterService);
34+
if (interpreter) {
35+
if (interpreter) {
36+
const nativeRepl = await getNativeRepl(interpreter, disposables);
37+
await nativeRepl.sendToNativeRepl();
38+
}
39+
}
40+
}),
41+
);
42+
}
1743

1844
/**
1945
* Registers REPL command for shift+enter if sendToNativeREPL setting is enabled.
@@ -39,7 +65,7 @@ export async function registerReplCommands(
3965
const interpreter = await getActiveInterpreter(uri, interpreterService);
4066

4167
if (interpreter) {
42-
const nativeRepl = getNativeRepl(interpreter, disposables);
68+
const nativeRepl = await getNativeRepl(interpreter, disposables);
4369
const activeEditor = window.activeTextEditor;
4470
if (activeEditor) {
4571
const code = await getSelectedTextToExecute(activeEditor);
@@ -76,7 +102,7 @@ export async function registerReplExecuteOnEnter(
76102
return;
77103
}
78104

79-
const nativeRepl = getNativeRepl(interpreter, disposables);
105+
const nativeRepl = await getNativeRepl(interpreter, disposables);
80106
const completeCode = await nativeRepl?.checkUserInputCompleteCode(window.activeTextEditor);
81107
const editor = window.activeTextEditor;
82108

src/client/repl/replController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { createPythonServer } from './pythonServer';
44
export function createReplController(
55
interpreterPath: string,
66
disposables: vscode.Disposable[],
7+
cwd?: string,
78
): vscode.NotebookController {
8-
const server = createPythonServer([interpreterPath]);
9+
const server = createPythonServer([interpreterPath], cwd);
910
disposables.push(server);
1011

1112
const controller = vscode.notebooks.createNotebookController('pythonREPL', 'interactive', 'Python REPL');

src/test/repl/nativeRepl.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/* eslint-disable no-unused-expressions */
2+
/* eslint-disable @typescript-eslint/no-explicit-any */
3+
import * as TypeMoq from 'typemoq';
4+
import * as sinon from 'sinon';
5+
import { Disposable } from 'vscode';
6+
import { expect } from 'chai';
7+
8+
import { IInterpreterService } from '../../client/interpreter/contracts';
9+
import { PythonEnvironment } from '../../client/pythonEnvironments/info';
10+
import { getNativeRepl, NativeRepl } from '../../client/repl/nativeRepl';
11+
12+
suite('REPL - Native REPL', () => {
13+
let interpreterService: TypeMoq.IMock<IInterpreterService>;
14+
15+
let disposable: TypeMoq.IMock<Disposable>;
16+
let disposableArray: Disposable[] = [];
17+
18+
let setReplDirectoryStub: sinon.SinonStub;
19+
let setReplControllerSpy: sinon.SinonSpy;
20+
21+
setup(() => {
22+
interpreterService = TypeMoq.Mock.ofType<IInterpreterService>();
23+
interpreterService
24+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
25+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
26+
disposable = TypeMoq.Mock.ofType<Disposable>();
27+
disposableArray = [disposable.object];
28+
29+
setReplDirectoryStub = sinon.stub(NativeRepl.prototype as any, 'setReplDirectory').resolves(); // Stubbing private method
30+
// Use a spy instead of a stub for setReplController
31+
setReplControllerSpy = sinon.spy(NativeRepl.prototype, 'setReplController');
32+
});
33+
34+
teardown(() => {
35+
disposableArray.forEach((d) => {
36+
if (d) {
37+
d.dispose();
38+
}
39+
});
40+
41+
disposableArray = [];
42+
sinon.restore();
43+
});
44+
45+
test('getNativeRepl should call create constructor', async () => {
46+
const createMethodStub = sinon.stub(NativeRepl, 'create');
47+
interpreterService
48+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
49+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
50+
const interpreter = await interpreterService.object.getActiveInterpreter();
51+
await getNativeRepl(interpreter as PythonEnvironment, disposableArray);
52+
53+
expect(createMethodStub.calledOnce).to.be.true;
54+
});
55+
56+
test('create should call setReplDirectory, setReplController', async () => {
57+
const interpreter = await interpreterService.object.getActiveInterpreter();
58+
interpreterService
59+
.setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
60+
.returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment));
61+
62+
await NativeRepl.create(interpreter as PythonEnvironment);
63+
64+
expect(setReplDirectoryStub.calledOnce).to.be.true;
65+
expect(setReplControllerSpy.calledOnce).to.be.true;
66+
67+
setReplDirectoryStub.restore();
68+
setReplControllerSpy.restore();
69+
});
70+
});

0 commit comments

Comments
 (0)