Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow execute on enter and Intellisense for native REPL with notebook UI #23442

Merged
merged 33 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5f279f1
start adding command
anthonykim1 May 16, 2024
7992d4b
allow execute on enter and execute on blank space with hard coded bool
anthonykim1 May 16, 2024
31f7db4
remove unintended enum to experiment
anthonykim1 May 17, 2024
4523502
change to TODO
anthonykim1 May 17, 2024
6defed9
compile() arg 1 must be a string, bytes or AST object
anthonykim1 May 17, 2024
ac7cc8c
WORKING --PROPERLY CHECKING IF SOMETHING IS VALID AND COMPLETE
anthonykim1 May 17, 2024
6389fdb
Try to handle interrupt on windows
anthonykim1 May 20, 2024
e3b974c
hide execute on enter from command palette
anthonykim1 May 20, 2024
3039670
add execInREPLEnter to package.nls.json
anthonykim1 May 20, 2024
e6034b7
temp save interactive.open and multi-line
anthonykim1 May 21, 2024
ab490dd
temp save --> sending to IW, intellisense pt2
anthonykim1 May 21, 2024
e25ec45
refactoring, adding disposables
anthonykim1 May 21, 2024
58b7772
cleanup
anthonykim1 May 21, 2024
931f3bf
remove handling Windows interrupt for now
anthonykim1 May 21, 2024
f1dd8a0
more cleanup
anthonykim1 May 21, 2024
55cad61
rename variable
anthonykim1 May 21, 2024
0593349
shift enter sends to REPL or Terminal depending on setting
anthonykim1 May 22, 2024
dbf48e6
hide context menu depending on setting
anthonykim1 May 22, 2024
cfed9d7
fixing test
anthonykim1 May 22, 2024
76d6164
fix more test
anthonykim1 May 22, 2024
c0bc3b8
remove execInREPLEnter from package.json and nls.json
anthonykim1 May 22, 2024
f512670
Update package.nls.json
anthonykim1 May 23, 2024
1b2c451
match setting python.sendToNativeREPL.description
anthonykim1 May 23, 2024
d369f11
Update package.nls.json
anthonykim1 May 23, 2024
8b27c2f
Update src/client/repl/pythonServer.ts
anthonykim1 May 23, 2024
a745cb1
switch checkValidCommand to return boolean
anthonykim1 May 23, 2024
393d2fd
Update src/client/repl/replCommands.ts
anthonykim1 May 23, 2024
0cffa5a
Stop using var!
anthonykim1 May 23, 2024
a6036cd
delete stale experiment, properly get setting
anthonykim1 May 23, 2024
57b2bdd
refactor registerReplCommands
anthonykim1 May 23, 2024
0788e99
clean up more
anthonykim1 May 23, 2024
910783b
allow users with shift+enter functional
anthonykim1 May 23, 2024
e270705
properly add to disposables
anthonykim1 May 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,11 @@
"command": "python.execInREPL",
"title": "%python.command.python.execInREPL.title%"
},
{
"category": "Python",
"command": "python.execInREPLEnter",
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
"title": "%python.command.python.execInREPLEnter.title%"
},
{
"category": "Python",
"command": "python.launchTensorBoard",
Expand Down Expand Up @@ -1110,6 +1115,11 @@
"key": "shift+enter",
"when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !jupyter.ownsSelection && !notebookEditorFocused && activeEditor != 'workbench.editor.interactive'"
},
{
"command": "python.execInREPLEnter",
"key": "enter",
"when": "activeEditor == 'workbench.editor.interactive'"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should I also be adding !config.interactiveWindow.executeWithShiftEnter
here in the when clause? @amunger

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, adding this when clause in makes the whole command get ignored when user presses enter. (I have not personally set up any shift enter keybinding override on my machine).

Copy link
Author

@anthonykim1 anthonykim1 May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found why it is not working..(As in figured out why after adding !config.interactiveWindow.executeWithShiftEnter to when clause would not let user execute with enter by default. Basically user has to have explicit "interactiveWindow.executeWithShiftEnter": false, in their settings.json.

Value for "interactiveWindow.executeWithShiftEnter" seems to be true by default, but Python extension would ideally want the default to be false. Should Python extension somehow override the value of this?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was planning to switch the default, but I didn't get around to putting in the migration logic for jupyter IW users. So you should be able to leave it as is for now (and manually set the setting), and I'll still try to make the switch

Copy link
Author

@anthonykim1 anthonykim1 May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@amunger When you say "leave it as is for now" and update the setting, do you still want the !config.interactiveWindow.executeWithShiftEnter clauses in the "when" condition, and also have Python extension manually set the "interactiveWindow.executeWithShiftEnter": false, for users?
OR
just exclude using !config.interactiveWindow.executeWithShiftEnter in when clause

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need the when clause here, otherwise users who want to use shift+enter to execute will not be able to use enter for a newline.
By manually, I meant users will individually need to change that setting until the default is switched over, don't change anyone's configuration since we were planning on doing that in the other direction for jupyter IW users.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, it should be resolved with: 910783b

},
{
"command": "python.refreshTensorBoard",
"key": "ctrl+r",
Expand Down Expand Up @@ -1269,6 +1279,12 @@
"title": "%python.command.python.execInREPL.title%",
"when": "false"
},
{
"category": "Python",
"command": "python.execInREPLEnter",
"title": "%python.command.python.execInREPLEnter.title%",
"when": "false"
},
{
"category": "Python",
"command": "python.launchTensorBoard",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"python.command.testing.rerunFailedTests.title": "Rerun Failed Tests",
"python.command.python.execSelectionInTerminal.title": "Run Selection/Line in Python Terminal",
"python.command.python.execInREPL.title": "Run Selection/Line in Python REPL",
"python.command.python.execInREPLEnter": "Execute on REPL Enter",
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
"python.command.python.execSelectionInDjangoShell.title": "Run Selection/Line in Django Shell",
"python.command.python.reportIssue.title": "Report Issue...",
"python.command.python.enableSourceMapSupport.title": "Enable Source Map Support For Extension Debugging",
Expand Down
12 changes: 12 additions & 0 deletions python_files/ctrlc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This file sends keyboard interrupt to Windows Python REPL instance
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
import ctypes
import sys

kernel = ctypes.windll.kernel32

Check failure on line 5 in python_files/ctrlc.py

View workflow job for this annotation

GitHub Actions / Check Python types

"windll" is not a known member of module "ctypes" (reportGeneralTypeIssues)

pid = int(sys.argv[1])
kernel.FreeConsole()
kernel.AttachConsole(pid)
kernel.SetConsoleCtrlHandler(None, 1)
kernel.GenerateConsoleCtrlEvent(0, 0)
sys.exit(0)
16 changes: 16 additions & 0 deletions python_files/python_server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from typing import Dict, List, Optional, Union
# import debugpy

# debugpy.connect(5678)
import sys
import json
import contextlib
import io
import traceback
import uuid
import ast

STDIN = sys.stdin
STDOUT = sys.stdout
Expand Down Expand Up @@ -88,6 +91,17 @@ def exec_function(user_input):
return eval


def check_valid_command(request):
try:
user_input = request["params"]
ast.parse(user_input[0])
send_response("True", request["id"])
# return True
except SyntaxError:
send_response("False", request["id"])
# return False


def execute(request, user_globals):
str_output = CustomIO("<stdout>", encoding="utf-8")
str_error = CustomIO("<stderr>", encoding="utf-8")
Expand Down Expand Up @@ -160,6 +174,8 @@ def get_headers():
request_json = json.loads(request_text)
if request_json["method"] == "execute":
execute(request_json, USER_GLOBALS)
if request_json["method"] == "check_valid_command":
check_valid_command(request_json)
elif request_json["method"] == "exit":
sys.exit(0)

Expand Down
1 change: 1 addition & 0 deletions src/client/common/application/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ interface ICommandNameWithoutArgumentTypeMapping {
[Commands.Exec_Selection_In_Terminal]: [];
[Commands.Exec_Selection_In_Django_Shell]: [];
[Commands.Exec_In_REPL]: [];
[Commands.Exec_In_REPL_Enter]: [];
[Commands.Create_Terminal]: [];
[Commands.PickLocalProcess]: [];
[Commands.ClearStorage]: [];
Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export namespace Commands {
export const Exec_In_Separate_Terminal = 'python.execInDedicatedTerminal';
export const Exec_In_REPL = 'python.execInREPL';
export const Exec_Selection_In_Django_Shell = 'python.execSelectionInDjangoShell';
export const Exec_In_REPL_Enter = 'python.execInREPLEnter';
export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal';
export const GetSelectedInterpreterPath = 'python.interpreterPath';
export const InstallJupyter = 'python.installJupyter';
Expand Down
3 changes: 2 additions & 1 deletion src/client/extensionActivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import { initializePersistentStateForTriggers } from './common/persistentState';
import { logAndNotifyOnLegacySettings } from './logging/settingLogs';
import { DebuggerTypeName } from './debugger/constants';
import { StopWatch } from './common/utils/stopWatch';
import { registerReplCommands } from './repl/replCommands';
import { registerReplCommands, registerReplExecuteOnEnter } from './repl/replCommands';
import { EnableRunREPL } from './common/experiments/groups';

export async function activateComponents(
Expand Down Expand Up @@ -117,6 +117,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
if (replExperimentValue) {
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
registerReplCommands(ext.disposables, interpreterService);
commands.executeCommand('setContext', 'pythonRunREPL', true);
registerReplExecuteOnEnter(ext.disposables, interpreterService);
}
}
}
Expand Down
43 changes: 38 additions & 5 deletions src/client/repl/pythonServer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line max-classes-per-file
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
import * as path from 'path';
import * as ch from 'child_process';
import * as rpc from 'vscode-jsonrpc/node';
Expand All @@ -6,15 +7,23 @@ import { EXTENSION_ROOT_DIR } from '../constants';
import { traceError, traceLog } from '../logging';

const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py');

// export let serverInstance: PythonServer | undefined;
export class pythonServerInstance {
public static serverInstance: PythonServer | undefined;
}
export interface PythonServer extends Disposable {
execute(code: string): Promise<string>;
interrupt(): void;
input(): void;
checkValidCommand(code: string): Promise<string>;
}

class PythonServerImpl implements Disposable {
constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) {
constructor(
private connection: rpc.MessageConnection,
private pythonServer: ch.ChildProcess,
private interpreter: string,
) {
this.initialize();
this.input();
}
Expand Down Expand Up @@ -49,18 +58,41 @@ class PythonServerImpl implements Disposable {
}

public interrupt(): void {
// Passing SIGINT to interrupt only would work for Mac and Linux
if (this.pythonServer.kill('SIGINT')) {
traceLog('Python server interrupted');
traceLog('Python REPL server interrupted');
} else {
// Handle interrupt for windows
// Run python_files/ctrlc.py with 12345 as argument
const ctrlc = ch.spawn(this.interpreter, [
path.join(EXTENSION_ROOT_DIR, 'python_files', 'ctrlc.py'),
'12345',
]);
ctrlc.on('exit', (code) => {
if (code === 0) {
traceLog('Windows Python REPL server interrupted successfully with exit code 0');
} else {
traceLog('Windows Python REPL interrupt may have failed');
}
});
}
}

public async checkValidCommand(code: string): Promise<string> {
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
return this.connection.sendRequest('check_valid_command', code);
}

public dispose(): void {
this.connection.sendNotification('exit');
this.connection.dispose();
}
}

export function createPythonServer(interpreter: string[]): PythonServer {
if (pythonServerInstance.serverInstance) {
return pythonServerInstance.serverInstance;
}

const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH]);

pythonServer.stderr.on('data', (data) => {
Expand All @@ -76,6 +108,7 @@ export function createPythonServer(interpreter: string[]): PythonServer {
new rpc.StreamMessageReader(pythonServer.stdout),
new rpc.StreamMessageWriter(pythonServer.stdin),
);

return new PythonServerImpl(connection, pythonServer);
const ourPythonServerImpl = new PythonServerImpl(connection, pythonServer, interpreter[0]);
pythonServerInstance.serverInstance = ourPythonServerImpl;
return ourPythonServerImpl;
}
62 changes: 62 additions & 0 deletions src/client/repl/replCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import {
WorkspaceEdit,
NotebookEditor,
TextEditor,
Selection,
} from 'vscode';
import { Disposable } from 'vscode-jsonrpc';
import { Commands, PVSC_EXTENSION_ID } from '../common/constants';
import { noop } from '../common/utils/misc';
import { IInterpreterService } from '../interpreter/contracts';
import { getMultiLineSelectionText, getSingleLineSelectionText } from '../terminals/codeExecution/helper';
import { createPythonServer } from './pythonServer';
import { createReplController } from './replController';

let notebookController: NotebookController | undefined;
Expand Down Expand Up @@ -102,3 +104,63 @@ export async function registerReplCommands(
}),
);
}

// TODO: Register Python execute command for keybinding 'Enter'
// TODO: Conditionally call interactive.execute OR insert \n in text input box.
export async function registerReplExecuteOnEnter(
disposables: Disposable[],
interpreterService: IInterpreterService,
): Promise<void> {
disposables.push(
commands.registerCommand(Commands.Exec_In_REPL_Enter, async (uri: Uri) => {
const interpreter = await interpreterService.getActiveInterpreter(uri);
if (!interpreter) {
commands.executeCommand(Commands.TriggerEnvironmentSelection, uri).then(noop, noop);
return;
}

// Create Separate Python server to check valid command
const pythonServer = createPythonServer([interpreter!.path! as string]);

const activeEditor = window.activeTextEditor;
let userTextInput;
let completeCode = false;

if (activeEditor) {
const { document } = activeEditor;
userTextInput = document.getText();
}

// Check if userTextInput is a complete Python command
if (userTextInput) {
const stringBoolean = await pythonServer.checkValidCommand(userTextInput);
anthonykim1 marked this conversation as resolved.
Show resolved Hide resolved
if (stringBoolean === 'True') {
completeCode = true;
}
}

if (completeCode) {
await commands.executeCommand('interactive.execute');
} else {
// Insert new line on behalf of user. "Regular" monaco editor behavior
const editor = window.activeTextEditor;

if (editor) {
const position = editor.selection.active;
// move cursor to end of line and also add newline character
const newPosition = position.with(position.line, editor.document.lineAt(position.line).text.length);
editor.selection = new Selection(newPosition, newPosition);
// add newline character
editor.edit((editBuilder) => {
editBuilder.insert(newPosition, '\n');
});
}

// Handle case when user enters on blank line, just trigger interactive.execute
if (editor && editor.document.lineAt(editor.selection.active.line).text === '') {
await commands.executeCommand('interactive.execute');
}
}
}),
);
}
Loading