Skip to content

Commit 80a7f90

Browse files
anthonykim1cwebster-99karthiknadig
authored andcommitted
Allow execute on enter and Intellisense for native REPL with notebook UI (#23442)
"Smartly" allow execute on enter for the microsoft#23235 experiment. User should be able to execute when they press enter on text input box of interactive window trigger from Python extension, whereas we would "wait" and allow insertion of new line after detecting user's Python command is not complete. When the user finally types enter again on a blank line, we should just proceed to execute whatever code, regardless of whether it is complete/valid or not to replicate Python's original interactive REPL behavior. Basically creating Python command and registering that for keybinding of 'Enter'. This would conditionally call interactive.execute which would then eventually call our execute handler contributed from Python n extension's REPL controller, or go ahead and insert,pass in Enter to the text input box to allow user to type "complete" code. This PR only intends to implement/add changes regarding execute on enter logic, adding Intellisense support, and also adding things into disposables so they can be properly disposed. Trying to also add setting to allow toggling on/off to send Python command to Terminal or IW REPL if the user is in experiment. Handling of interrupt for windows should be on separate PR. Test will be added later as separate PR. --------- Co-authored-by: Courtney Webster <60238438+cwebster-99@users.noreply.github.com> Co-authored-by: Karthik Nadig <kanadig@microsoft.com>
1 parent 43d59c2 commit 80a7f90

File tree

14 files changed

+234
-76
lines changed

14 files changed

+234
-76
lines changed

package.json

+31-11
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,7 @@
442442
"pythonDiscoveryUsingWorkers",
443443
"pythonTestAdapter",
444444
"pythonREPLSmartSend",
445-
"pythonRecommendTensorboardExt",
446-
"pythonRunREPL"
445+
"pythonRecommendTensorboardExt"
447446
],
448447
"enumDescriptions": [
449448
"%python.experiments.All.description%",
@@ -453,8 +452,7 @@
453452
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
454453
"%python.experiments.pythonTestAdapter.description%",
455454
"%python.experiments.pythonREPLSmartSend.description%",
456-
"%python.experiments.pythonRecommendTensorboardExt.description%",
457-
"%python.experiments.pythonRunREPL.description%"
455+
"%python.experiments.pythonRecommendTensorboardExt.description%"
458456
]
459457
},
460458
"scope": "window",
@@ -472,8 +470,7 @@
472470
"pythonTerminalEnvVarActivation",
473471
"pythonDiscoveryUsingWorkers",
474472
"pythonTestAdapter",
475-
"pythonREPLSmartSend",
476-
"pythonRunREPL"
473+
"pythonREPLSmartSend"
477474
],
478475
"enumDescriptions": [
479476
"%python.experiments.All.description%",
@@ -482,8 +479,7 @@
482479
"%python.experiments.pythonTerminalEnvVarActivation.description%",
483480
"%python.experiments.pythonDiscoveryUsingWorkers.description%",
484481
"%python.experiments.pythonTestAdapter.description%",
485-
"%python.experiments.pythonREPLSmartSend.description%",
486-
"%python.experiments.pythonRunREPL.description%"
482+
"%python.experiments.pythonREPLSmartSend.description%"
487483
]
488484
},
489485
"scope": "window",
@@ -628,6 +624,15 @@
628624
"scope": "resource",
629625
"type": "boolean"
630626
},
627+
"python.REPL.sendToNativeREPL": {
628+
"default": false,
629+
"description": "%python.REPL.sendToNativeREPL.description%",
630+
"scope": "resource",
631+
"type": "boolean",
632+
"tags": [
633+
"experimental"
634+
]
635+
},
631636
"python.testing.autoTestDiscoverOnSaveEnabled": {
632637
"default": true,
633638
"description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%",
@@ -1108,7 +1113,22 @@
11081113
{
11091114
"command": "python.execSelectionInTerminal",
11101115
"key": "shift+enter",
1111-
"when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused && activeEditor != 'workbench.editor.interactive'"
1116+
"when": "!config.python.REPL.sendToNativeREPL && editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused && activeEditor != 'workbench.editor.interactive'"
1117+
},
1118+
{
1119+
"command": "python.execInREPL",
1120+
"key": "shift+enter",
1121+
"when": "config.python.REPL.sendToNativeREPL && activeEditor != 'workbench.editor.interactive'"
1122+
},
1123+
{
1124+
"command": "python.execREPLShiftEnter",
1125+
"key": "shift+enter",
1126+
"when": "activeEditor == 'workbench.editor.interactive' && config.interactiveWindow.executeWithShiftEnter"
1127+
},
1128+
{
1129+
"command": "python.execInREPLEnter",
1130+
"key": "enter",
1131+
"when": "!config.interactiveWindow.executeWithShiftEnter && activeEditor == 'workbench.editor.interactive'"
11121132
},
11131133
{
11141134
"command": "python.refreshTensorBoard",
@@ -1367,12 +1387,12 @@
13671387
{
13681388
"command": "python.execSelectionInTerminal",
13691389
"group": "Python",
1370-
"when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported"
1390+
"when": "!config.python.REPL.sendToNativeREPL && editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported"
13711391
},
13721392
{
13731393
"command": "python.execInREPL",
13741394
"group": "Python",
1375-
"when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && pythonRunREPL"
1395+
"when": "editorFocus && editorLangId == python && !virtualWorkspace && shellExecutionSupported && config.python.REPL.sendToNativeREPL"
13761396
}
13771397
],
13781398
"editor/title": [

package.nls.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
4646
"python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
4747
"python.experiments.pythonRecommendTensorboardExt.description": "Denotes the Tensorboard Extension recommendation experiment.",
48-
"python.experiments.pythonRunREPL.description": "Enables users to run code in interactive Python REPL.",
4948
"python.globalModuleInstallation.description": "Whether to install Python modules globally when not using an environment.",
5049
"python.languageServer.description": "Defines type of the language server.",
5150
"python.languageServer.defaultDescription": "Automatically select a language server: Pylance if installed and available, otherwise fallback to Jedi.",
@@ -63,6 +62,7 @@
6362
"python.pipenvPath.description": "Path to the pipenv executable to use for activation.",
6463
"python.poetryPath.description": "Path to the poetry executable.",
6564
"python.EnableREPLSmartSend.description": "Toggle Smart Send for the Python REPL. Smart Send enables sending the smallest runnable block of code to the REPL on Shift+Enter and moves the cursor accordingly.",
65+
"python.REPL.sendToNativeREPL.description": "Toggle to send code to Python REPL instead of the terminal on execution. Turning this on will change the behavior for both Smart Send and Run Selection/Line in the Context Menu.",
6666
"python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.",
6767
"python.tensorBoard.logDirectory.markdownDeprecationMessage": "Tensorboard support has been moved to the extension [Tensorboard extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.tensorboard). Instead use the setting `tensorBoard.logDirectory`.",
6868
"python.tensorBoard.logDirectory.deprecationMessage": "Tensorboard support has been moved to the extension Tensorboard extension. Instead use the setting `tensorBoard.logDirectory`.",

python_files/python_server.py

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
from typing import Dict, List, Optional, Union
2-
32
import sys
43
import json
54
import contextlib
65
import io
76
import traceback
87
import uuid
8+
import ast
99

1010
STDIN = sys.stdin
1111
STDOUT = sys.stdout
@@ -88,6 +88,15 @@ def exec_function(user_input):
8888
return eval
8989

9090

91+
def check_valid_command(request):
92+
try:
93+
user_input = request["params"]
94+
ast.parse(user_input[0])
95+
send_response("True", request["id"])
96+
except SyntaxError:
97+
send_response("False", request["id"])
98+
99+
91100
def execute(request, user_globals):
92101
str_output = CustomIO("<stdout>", encoding="utf-8")
93102
str_error = CustomIO("<stderr>", encoding="utf-8")
@@ -160,6 +169,8 @@ def get_headers():
160169
request_json = json.loads(request_text)
161170
if request_json["method"] == "execute":
162171
execute(request_json, USER_GLOBALS)
172+
if request_json["method"] == "check_valid_command":
173+
check_valid_command(request_json)
163174
elif request_json["method"] == "exit":
164175
sys.exit(0)
165176

src/client/common/application/commands.ts

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
3939
[Commands.Exec_Selection_In_Terminal]: [];
4040
[Commands.Exec_Selection_In_Django_Shell]: [];
4141
[Commands.Exec_In_REPL]: [];
42+
[Commands.Exec_In_REPL_Enter]: [];
4243
[Commands.Create_Terminal]: [];
4344
[Commands.PickLocalProcess]: [];
4445
[Commands.ClearStorage]: [];

src/client/common/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export namespace Commands {
4848
export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal';
4949
export const Exec_In_REPL = 'python.execInREPL';
5050
export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell';
51+
export const Exec_In_REPL_Enter = 'python.execInREPLEnter';
52+
export const Exec_In_REPL_Shift_Enter = 'python.execREPLShiftEnter';
5153
export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal';
5254
export const GetSelectedInterpreterPath = 'python.interpreterPath';
5355
export const InstallJupyter = 'python.installJupyter';

src/client/common/experiments/groups.ts

-5
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,3 @@ export enum RecommendTensobardExtension {
3030
export enum CreateEnvOnPipInstallTrigger {
3131
experiment = 'pythonCreateEnvOnPipInstall',
3232
}
33-
34-
// Experiment to enable running Python REPL using IW.
35-
export enum EnableRunREPL {
36-
experiment = 'pythonRunREPL',
37-
}

src/client/common/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ export interface ITerminalSettings {
200200

201201
export interface IREPLSettings {
202202
readonly enableREPLSmartSend: boolean;
203-
readonly enableIWREPL: boolean;
203+
readonly sendToNativeREPL: boolean;
204204
}
205205

206206
export interface IExperiments {

src/client/common/vscodeApis/windowApis.ts

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
TerminalShellExecutionStartEvent,
2222
} from 'vscode';
2323
import { createDeferred, Deferred } from '../utils/async';
24+
import { Resource } from '../types';
25+
import { getWorkspaceFolders } from './workspaceApis';
2426

2527
export function showTextDocument(uri: Uri): Thenable<TextEditor> {
2628
return window.showTextDocument(uri);
@@ -238,3 +240,12 @@ export function createStepForwardEndNode<T>(deferred?: Deferred<T>, result?: T):
238240
undefined,
239241
);
240242
}
243+
244+
export function getActiveResource(): Resource {
245+
const editor = window.activeTextEditor;
246+
if (editor && !editor.document.isUntitled) {
247+
return editor.document.uri;
248+
}
249+
const workspaces = getWorkspaceFolders();
250+
return Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0].uri : undefined;
251+
}

src/client/extensionActivation.ts

+5-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
'use strict';
55

6-
import { DebugConfigurationProvider, debug, languages, window, commands } from 'vscode';
6+
import { DebugConfigurationProvider, debug, languages, window } from 'vscode';
77

88
import { registerTypes as activationRegisterTypes } from './activation/serviceRegistry';
99
import { IExtensionActivationManager } from './activation/types';
@@ -16,7 +16,6 @@ import { IFileSystem } from './common/platform/types';
1616
import {
1717
IConfigurationService,
1818
IDisposableRegistry,
19-
IExperimentService,
2019
IExtensions,
2120
IInterpreterPathService,
2221
ILogOutputChannel,
@@ -53,8 +52,7 @@ import { initializePersistentStateForTriggers } from './common/persistentState';
5352
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
5453
import { DebuggerTypeName } from './debugger/constants';
5554
import { StopWatch } from './common/utils/stopWatch';
56-
import { registerReplCommands } from './repl/replCommands';
57-
import { EnableRunREPL } from './common/experiments/groups';
55+
import { registerReplCommands, registerReplExecuteOnEnter, registerReplExecuteOnShiftEnter } from './repl/replCommands';
5856

5957
export async function activateComponents(
6058
// `ext` is passed to any extra activation funcs.
@@ -109,16 +107,9 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
109107
pathUtils,
110108
);
111109

112-
// Register native REPL context menu when in experiment
113-
const experimentService = ext.legacyIOC.serviceContainer.get<IExperimentService>(IExperimentService);
114-
commands.executeCommand('setContext', 'pythonRunREPL', false);
115-
if (experimentService) {
116-
const replExperimentValue = experimentService.inExperimentSync(EnableRunREPL.experiment);
117-
if (replExperimentValue) {
118-
registerReplCommands(ext.disposables, interpreterService);
119-
commands.executeCommand('setContext', 'pythonRunREPL', true);
120-
}
121-
}
110+
registerReplCommands(ext.disposables, interpreterService);
111+
registerReplExecuteOnEnter(ext.disposables, interpreterService);
112+
registerReplExecuteOnShiftEnter(ext.disposables);
122113
}
123114

124115
/// //////////////////////////

src/client/repl/pythonServer.ts

+26-7
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,32 @@ import { EXTENSION_ROOT_DIR } from '../constants';
66
import { traceError, traceLog } from '../logging';
77

88
const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py');
9+
let serverInstance: PythonServer | undefined;
910

1011
export interface PythonServer extends Disposable {
1112
execute(code: string): Promise<string>;
1213
interrupt(): void;
1314
input(): void;
15+
checkValidCommand(code: string): Promise<boolean>;
1416
}
1517

1618
class PythonServerImpl implements Disposable {
19+
private readonly disposables: Disposable[] = [];
20+
1721
constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) {
1822
this.initialize();
1923
this.input();
2024
}
2125

2226
private initialize(): void {
23-
this.connection.onNotification('log', (message: string) => {
24-
console.log('Log:', message);
25-
});
27+
this.disposables.push(
28+
this.connection.onNotification('log', (message: string) => {
29+
console.log('Log:', message);
30+
}),
31+
);
2632
this.connection.listen();
2733
}
2834

29-
// Register input handler
3035
public input(): void {
3136
// Register input request handler
3237
this.connection.onRequest('input', async (request) => {
@@ -49,18 +54,32 @@ class PythonServerImpl implements Disposable {
4954
}
5055

5156
public interrupt(): void {
57+
// Passing SIGINT to interrupt only would work for Mac and Linux
5258
if (this.pythonServer.kill('SIGINT')) {
53-
traceLog('Python server interrupted');
59+
traceLog('Python REPL server interrupted');
60+
}
61+
}
62+
63+
public async checkValidCommand(code: string): Promise<boolean> {
64+
const completeCode = await this.connection.sendRequest('check_valid_command', code);
65+
if (completeCode === 'True') {
66+
return new Promise((resolve) => resolve(true));
5467
}
68+
return new Promise((resolve) => resolve(false));
5569
}
5670

5771
public dispose(): void {
5872
this.connection.sendNotification('exit');
73+
this.disposables.forEach((d) => d.dispose());
5974
this.connection.dispose();
6075
}
6176
}
6277

6378
export function createPythonServer(interpreter: string[]): PythonServer {
79+
if (serverInstance) {
80+
return serverInstance;
81+
}
82+
6483
const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH]);
6584

6685
pythonServer.stderr.on('data', (data) => {
@@ -76,6 +95,6 @@ export function createPythonServer(interpreter: string[]): PythonServer {
7695
new rpc.StreamMessageReader(pythonServer.stdout),
7796
new rpc.StreamMessageWriter(pythonServer.stdin),
7897
);
79-
80-
return new PythonServerImpl(connection, pythonServer);
98+
serverInstance = new PythonServerImpl(connection, pythonServer);
99+
return serverInstance;
81100
}

0 commit comments

Comments
 (0)