Skip to content

Commit 5b76e4a

Browse files
committed
replCommands, replController
1 parent 8dbbd05 commit 5b76e4a

File tree

6 files changed

+250
-1
lines changed

6 files changed

+250
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1334,7 +1334,7 @@
13341334
"category": "Python",
13351335
"command": "python.execInREPL",
13361336
"title": "%python.command.python.execInREPL.title%",
1337-
"when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python"
1337+
"when": "false"
13381338
},
13391339
{
13401340
"category": "Python",

pythonFiles/python_server.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# import debugpy
2+
# debugpy.connect(5678)
3+
import sys
4+
import json
5+
import contextlib
6+
import io
7+
import traceback
8+
9+
10+
def send_message(msg: str):
11+
length_msg = len(msg)
12+
sys.stdout.buffer.write(
13+
f"Content-Length: {length_msg}\r\n\r\n{msg}".encode(encoding="utf-8")
14+
)
15+
sys.stdout.buffer.flush()
16+
17+
18+
def print_log(msg: str):
19+
send_message(json.dumps({"jsonrpc": "2.0", "method": "log", "params": msg}))
20+
21+
22+
def send_response(response: dict, response_id: int):
23+
send_message(json.dumps({"jsonrpc": "2.0", "id": response_id, "result": response}))
24+
25+
26+
def exec_function(user_input):
27+
try:
28+
compile(user_input, "<stdin>", "eval")
29+
except SyntaxError:
30+
return exec
31+
return eval
32+
33+
34+
def execute(user_globals, request):
35+
str_output = CustomIO("<stdout>", encoding="utf-8")
36+
str_error = CustomIO("<stderr>", encoding="utf-8")
37+
38+
with redirect_io("stdout", str_output):
39+
with redirect_io("stderr", str_error):
40+
str_input = CustomIO("<stdin>", encoding="utf-8", newline="\n")
41+
with redirect_io("stdin", str_input):
42+
user_output_globals = exec_user_input(
43+
request["id"], request["params"], user_globals
44+
)
45+
send_response(str_output.get_value(), request["id"])
46+
return user_output_globals
47+
48+
49+
def exec_user_input(request_id, user_input, user_globals):
50+
# have to do redirection
51+
user_input = user_input[0] if isinstance(user_input, list) else user_input
52+
user_globals = user_globals.copy()
53+
54+
try:
55+
callable = exec_function(user_input)
56+
retval = callable(user_input, user_globals)
57+
if retval is not None:
58+
print(retval)
59+
except Exception as e:
60+
send_response(
61+
{
62+
"error": {
63+
"code": -32603,
64+
"message": str(e),
65+
"data": traceback.format_exc(),
66+
},
67+
"id": request_id,
68+
}
69+
)
70+
return user_globals
71+
72+
73+
class CustomIO(io.TextIOWrapper):
74+
"""Custom stream object to replace stdio."""
75+
76+
name = None
77+
78+
def __init__(self, name, encoding="utf-8", newline=None):
79+
self._buffer = io.BytesIO()
80+
self._buffer.name = name
81+
super().__init__(self._buffer, encoding=encoding, newline=newline)
82+
83+
def close(self):
84+
"""Provide this close method which is used by some tools."""
85+
# This is intentionally empty.
86+
87+
def get_value(self) -> str:
88+
"""Returns value from the buffer as string."""
89+
self.seek(0)
90+
return self.read()
91+
92+
93+
@contextlib.contextmanager
94+
def redirect_io(stream: str, new_stream):
95+
"""Redirect stdio streams to a custom stream."""
96+
old_stream = getattr(sys, stream)
97+
setattr(sys, stream, new_stream)
98+
yield
99+
setattr(sys, stream, old_stream)
100+
101+
102+
def get_headers():
103+
headers = {}
104+
while line := sys.stdin.readline().strip():
105+
name, value = line.split(":", 1)
106+
headers[name] = value.strip()
107+
return headers
108+
109+
110+
if __name__ == "__main__":
111+
user_globals = {}
112+
while not sys.stdin.closed:
113+
try:
114+
headers = get_headers()
115+
content_length = int(headers.get("Content-Length", 0))
116+
117+
if content_length:
118+
request_json = json.loads(sys.stdin.read(content_length))
119+
if request_json["method"] == "execute":
120+
user_globals = execute(user_globals, request_json)
121+
elif request_json["method"] == "exit":
122+
sys.exit(0)
123+
124+
except Exception as e:
125+
print_log(str(e))

src/client/extensionActivation.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +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 } from './repl/replCommands';
5556

5657
export async function activateComponents(
5758
// `ext` is passed to any extra activation funcs.
@@ -105,6 +106,7 @@ export function activateFeatures(ext: ExtensionState, _components: Components):
105106
interpreterService,
106107
pathUtils,
107108
);
109+
registerReplCommands(ext.disposables, interpreterService);
108110
}
109111

110112
/// //////////////////////////

src/client/repl/pythonServer.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as path from 'path';
2+
import * as ch from 'child_process';
3+
import * as rpc from 'vscode-jsonrpc/node';
4+
import { Disposable } from 'vscode';
5+
6+
const SERVER_PATH = path.join(__dirname, '...', 'python_files', 'python_server.py');
7+
8+
export interface PythonServer extends Disposable {
9+
execute(code: string): Promise<string>;
10+
}
11+
12+
class PythonServerImpl implements Disposable {
13+
constructor(private connection: rpc.MessageConnection) {
14+
this.initialize();
15+
}
16+
17+
private initialize(): void {
18+
this.connection.onNotification('log', (message: string) => {
19+
console.log('Log:', message);
20+
});
21+
this.connection.listen();
22+
}
23+
24+
public execute(code: string): Promise<string> {
25+
return this.connection.sendRequest('execute', code);
26+
}
27+
28+
public dispose(): void {
29+
this.connection.sendNotification('exit');
30+
this.connection.dispose();
31+
}
32+
}
33+
34+
export function createPythonServer(interpreter: string[]): PythonServer {
35+
const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH]);
36+
pythonServer.stderr.on('data', (data) => {
37+
console.error(data.toString());
38+
});
39+
pythonServer.on('exit', (code) => {
40+
console.error(`Python server exited with code ${code}`);
41+
});
42+
pythonServer.on('error', (err) => {
43+
console.error(err);
44+
});
45+
const connection = rpc.createMessageConnection(
46+
new rpc.StreamMessageReader(pythonServer.stdout),
47+
new rpc.StreamMessageWriter(pythonServer.stdin),
48+
);
49+
return new PythonServerImpl(connection);
50+
}

src/client/repl/replCommands.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { commands, Uri } from 'vscode';
2+
import { Disposable } from 'vscode-jsonrpc';
3+
import { Commands } from '../common/constants';
4+
import { IInterpreterService } from '../interpreter/contracts';
5+
import { startRepl } from './replController';
6+
7+
export function registerReplCommands(disposables: Disposable[], interpreterService: IInterpreterService): void {
8+
disposables.push(
9+
commands.registerCommand(Commands.Exec_In_REPL, async (uri: Uri) => {
10+
const interpreter = await interpreterService.getActiveInterpreter(uri);
11+
if (interpreter) {
12+
const interpreterPath = interpreter.path;
13+
// How do we get instance of interactive window from Python extension?
14+
// How to go from user clicking Run Python --> Run selection/line via Python REPL -> IW opening
15+
16+
// TODO: Find interactive window, or open it
17+
18+
// TODO: Add new cell to interactive window document
19+
20+
// TODO: Set REPL server on interactive window. Make sure REPL server is running
21+
22+
// TODO: execute the cell
23+
}
24+
}),
25+
);
26+
}
27+
28+
// debugger
29+
// read code, use tools like hover, doc
30+
// think through
31+
32+
// ask questions
33+
// write down what you know
34+
// write down what you don't know
35+
// write down what you think you know

src/client/repl/replController.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as vscode from 'vscode';
2+
import { createPythonServer } from './pythonServer';
3+
4+
export function createReplController(interpreterPath: string): vscode.NotebookController {
5+
const server = createPythonServer([interpreterPath]);
6+
const controller = vscode.notebooks.createNotebookController('pythonREPL', 'interactive', 'Python REPL');
7+
controller.supportedLanguages = ['python'];
8+
controller.supportsExecutionOrder = true;
9+
controller.description = 'GitHub';
10+
11+
controller.executeHandler = async (cells) => {
12+
for (const cell of cells) {
13+
const exec = controller.createNotebookCellExecution(cell);
14+
exec.start(Date.now());
15+
try {
16+
const result = await server.execute(cell.document.getText());
17+
exec.replaceOutput([
18+
new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result, 'text/plain')]),
19+
]);
20+
exec.end(true);
21+
} catch (err) {
22+
const error = err as Error;
23+
exec.replaceOutput([
24+
new vscode.NotebookCellOutput([
25+
vscode.NotebookCellOutputItem.error({
26+
name: error.name,
27+
message: error.message,
28+
stack: error.stack,
29+
}),
30+
]),
31+
]);
32+
exec.end(false);
33+
}
34+
}
35+
};
36+
return controller;
37+
}

0 commit comments

Comments
 (0)