Skip to content

Commit 56661a1

Browse files
REPL Smart Shift+Enter and Dynamic Smart Cursor (#21779)
There are two Feature Requests from: #18105 #21838 They are grouped together to provide the smoothest experience: when user wants to press shift+enter and smoothly move between each executable Python code block without having to manually move their cursor. #19955 (For Execute line/selection and advance to next line, referred to as dynamic smart cursor hereby) Open Issue: #21778 #21838 Steps in implementing REPL Smart Send (smart shift+enter to the REPL) and dynamic cursor move aka. Move to Next Line (next executable line of code to be more precise): 1. Figure out the workflow of where things start and run when user clicks on run selection/line 2. Send the content of selection & document to the Python Side from Typescript side. 3. Respect and follow previous logic/code for EXPLICIT selection (user has highlighting particular text they want to send to REPL), but otherwise, use newly created smart send code. 4. Receive content of document & selection in Python Side 5. Use AST (From Python standard library) to figure out if selection if selection is part of, for example, dictionary, but look for nodes and how each relates to the top level. If some selection is, for example part of a dictionary, we should run the whole dictionary. Look at how to do this for all top level, so that we run the Minimum Viable Block possible. (For example, if user selects part of a dictionary to run in REPL, it will select and send only the dictionary not the whole class or file, etc) 6. Receive the commands to run in typescript side and send it to the REPL 7. After the user has ran shift+enter(non highlight, meaning there was no explicit highlight of text), thus the incurring of smart send, and we have processed the smart selection, figure out the "next" executable line of code in the currently opened Python file. 8. After figuring out the "next" line number, we will move user's cursor to that line number. - [x] Additional scope for telemetry EventName.EXECUTION_CODE with the scope of 'line' in addition to differentiate the explicit selection usage compared to line or executable block. - [x] Drop 3.7 support before merging since end_line attribute of the AST module is only supported for Python version 3.8 and above. - [x] Python tests for both smart selection, dynamic cursor move. - [x] TypeScript tests for smart selection, dynamic cursor move. Notes: * To be shipped after dropping Python3.7 support, since end_lineno, which is critical in smart shift+enter logic, is only for Python version GREATER than 3.7 Update (9/14/23: Python 3.7 support is dropped from the VS Code Python extension: #21962) * Code in regards to this feature(s) should be wrapped in standard experiment (not setting based experiment) * Respective Telemetry should also be attached * EXPLICIT (highlight) selection of the text, and shift+enter/run selection should respect user's selection and send AS IT IS. (When the user selects/highlight specifically what they want to send, we should respect user's selection and send the selection as they are selected) * Smart Shift+Enter should be shipped together with dynamic smart cursor movement for smoothest experience. This way user could shift+enter line by line (or more accurately top block after another top block) as they shift+enter their code. * Be careful with line_no usage between vscode and python as vscode counts line number starting from 0 and python ast start as normal (starts from line 1)) So vscode_lineno + 1 = python_ast_lineno --------- Co-authored-by: Karthik Nadig <[email protected]>
1 parent 0665506 commit 56661a1

File tree

16 files changed

+1148
-15
lines changed

16 files changed

+1148
-15
lines changed

package.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,16 @@
536536
"pythonSurveyNotification",
537537
"pythonPromptNewToolsExt",
538538
"pythonTerminalEnvVarActivation",
539-
"pythonTestAdapter"
539+
"pythonTestAdapter",
540+
"pythonREPLSmartSend"
540541
],
541542
"enumDescriptions": [
542543
"%python.experiments.All.description%",
543544
"%python.experiments.pythonSurveyNotification.description%",
544545
"%python.experiments.pythonPromptNewToolsExt.description%",
545546
"%python.experiments.pythonTerminalEnvVarActivation.description%",
546-
"%python.experiments.pythonTestAdapter.description%"
547+
"%python.experiments.pythonTestAdapter.description%",
548+
"%python.experiments.pythonREPLSmartSend.description%"
547549
]
548550
},
549551
"scope": "machine",
@@ -559,14 +561,16 @@
559561
"pythonSurveyNotification",
560562
"pythonPromptNewToolsExt",
561563
"pythonTerminalEnvVarActivation",
562-
"pythonTestAdapter"
564+
"pythonTestAdapter",
565+
"pythonREPLSmartSend"
563566
],
564567
"enumDescriptions": [
565568
"%python.experiments.All.description%",
566569
"%python.experiments.pythonSurveyNotification.description%",
567570
"%python.experiments.pythonPromptNewToolsExt.description%",
568571
"%python.experiments.pythonTerminalEnvVarActivation.description%",
569-
"%python.experiments.pythonTestAdapter.description%"
572+
"%python.experiments.pythonTestAdapter.description%",
573+
"%python.experiments.pythonREPLSmartSend.description%"
570574
]
571575
},
572576
"scope": "machine",

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.",
4242
"python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.",
4343
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
44+
"python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
4445
"python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.",
4546
"python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8). <br>Learn more [here](https://aka.ms/AAlgvkb).",
4647
"python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.",

pythonFiles/normalizeSelection.py

+147-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import re
77
import sys
88
import textwrap
9+
from typing import Iterable
910

1011

1112
def split_lines(source):
@@ -118,6 +119,8 @@ def normalize_lines(selection):
118119

119120
# Insert a newline between each top-level statement, and append a newline to the selection.
120121
source = "\n".join(statements) + "\n"
122+
if selection[-2] == "}":
123+
source = source[:-1]
121124
except Exception:
122125
# If there's a problem when parsing statements,
123126
# append a blank line to end the block and send it as-is.
@@ -126,17 +129,159 @@ def normalize_lines(selection):
126129
return source
127130

128131

132+
top_level_nodes = []
133+
min_key = None
134+
135+
136+
def check_exact_exist(top_level_nodes, start_line, end_line):
137+
exact_nodes = []
138+
for node in top_level_nodes:
139+
if node.lineno == start_line and node.end_lineno == end_line:
140+
exact_nodes.append(node)
141+
142+
return exact_nodes
143+
144+
145+
def traverse_file(wholeFileContent, start_line, end_line, was_highlighted):
146+
"""
147+
Intended to traverse through a user's given file content and find, collect all appropriate lines
148+
that should be sent to the REPL in case of smart selection.
149+
This could be exact statement such as just a single line print statement,
150+
or a multiline dictionary, or differently styled multi-line list comprehension, etc.
151+
Then call the normalize_lines function to normalize our smartly selected code block.
152+
"""
153+
154+
parsed_file_content = ast.parse(wholeFileContent)
155+
smart_code = ""
156+
should_run_top_blocks = []
157+
158+
# Purpose of this loop is to fetch and collect all the
159+
# AST top level nodes, and its node.body as child nodes.
160+
# Individual nodes will contain information like
161+
# the start line, end line and get source segment information
162+
# that will be used to smartly select, and send normalized code.
163+
for node in ast.iter_child_nodes(parsed_file_content):
164+
top_level_nodes.append(node)
165+
166+
ast_types_with_nodebody = (
167+
ast.Module,
168+
ast.Interactive,
169+
ast.Expression,
170+
ast.FunctionDef,
171+
ast.AsyncFunctionDef,
172+
ast.ClassDef,
173+
ast.For,
174+
ast.AsyncFor,
175+
ast.While,
176+
ast.If,
177+
ast.With,
178+
ast.AsyncWith,
179+
ast.Try,
180+
ast.Lambda,
181+
ast.IfExp,
182+
ast.ExceptHandler,
183+
)
184+
if isinstance(node, ast_types_with_nodebody) and isinstance(
185+
node.body, Iterable
186+
):
187+
for child_nodes in node.body:
188+
top_level_nodes.append(child_nodes)
189+
190+
exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line)
191+
192+
# Just return the exact top level line, if present.
193+
if len(exact_nodes) > 0:
194+
which_line_next = 0
195+
for same_line_node in exact_nodes:
196+
should_run_top_blocks.append(same_line_node)
197+
smart_code += (
198+
f"{ast.get_source_segment(wholeFileContent, same_line_node)}\n"
199+
)
200+
which_line_next = get_next_block_lineno(should_run_top_blocks)
201+
return {
202+
"normalized_smart_result": smart_code,
203+
"which_line_next": which_line_next,
204+
}
205+
206+
# For each of the nodes in the parsed file content,
207+
# add the appropriate source code line(s) to be sent to the REPL, dependent on
208+
# user is trying to send and execute single line/statement or multiple with smart selection.
209+
for top_node in ast.iter_child_nodes(parsed_file_content):
210+
if start_line == top_node.lineno and end_line == top_node.end_lineno:
211+
should_run_top_blocks.append(top_node)
212+
213+
smart_code += f"{ast.get_source_segment(wholeFileContent, top_node)}\n"
214+
break # If we found exact match, don't waste computation in parsing extra nodes.
215+
elif start_line >= top_node.lineno and end_line <= top_node.end_lineno:
216+
# Case to apply smart selection for multiple line.
217+
# This is the case for when we have to add multiple lines that should be included in the smart send.
218+
# For example:
219+
# 'my_dictionary': {
220+
# 'Audi': 'Germany',
221+
# 'BMW': 'Germany',
222+
# 'Genesis': 'Korea',
223+
# }
224+
# with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary.
225+
226+
should_run_top_blocks.append(top_node)
227+
228+
smart_code += str(ast.get_source_segment(wholeFileContent, top_node))
229+
smart_code += "\n"
230+
231+
normalized_smart_result = normalize_lines(smart_code)
232+
which_line_next = get_next_block_lineno(should_run_top_blocks)
233+
return {
234+
"normalized_smart_result": normalized_smart_result,
235+
"which_line_next": which_line_next,
236+
}
237+
238+
239+
# Look at the last top block added, find lineno for the next upcoming block,
240+
# This will be used in calculating lineOffset to move cursor in VS Code.
241+
def get_next_block_lineno(which_line_next):
242+
last_ran_lineno = int(which_line_next[-1].end_lineno)
243+
next_lineno = int(which_line_next[-1].end_lineno)
244+
245+
for reverse_node in top_level_nodes:
246+
if reverse_node.lineno > last_ran_lineno:
247+
next_lineno = reverse_node.lineno
248+
break
249+
return next_lineno
250+
251+
129252
if __name__ == "__main__":
130253
# Content is being sent from the extension as a JSON object.
131254
# Decode the data from the raw bytes.
132255
stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer
133256
raw = stdin.read()
134257
contents = json.loads(raw.decode("utf-8"))
258+
# Empty highlight means user has not explicitly selected specific text.
259+
empty_Highlight = contents.get("emptyHighlight", False)
135260

136-
normalized = normalize_lines(contents["code"])
261+
# We also get the activeEditor selection start line and end line from the typescript VS Code side.
262+
# Remember to add 1 to each of the received since vscode starts line counting from 0 .
263+
vscode_start_line = contents["startLine"] + 1
264+
vscode_end_line = contents["endLine"] + 1
137265

138266
# Send the normalized code back to the extension in a JSON object.
139-
data = json.dumps({"normalized": normalized})
267+
data = None
268+
which_line_next = 0
269+
270+
if empty_Highlight and contents.get("smartSendExperimentEnabled"):
271+
result = traverse_file(
272+
contents["wholeFileContent"],
273+
vscode_start_line,
274+
vscode_end_line,
275+
not empty_Highlight,
276+
)
277+
normalized = result["normalized_smart_result"]
278+
which_line_next = result["which_line_next"]
279+
data = json.dumps(
280+
{"normalized": normalized, "nextBlockLineno": result["which_line_next"]}
281+
)
282+
else:
283+
normalized = normalize_lines(contents["code"])
284+
data = json.dumps({"normalized": normalized})
140285

141286
stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer
142287
stdout.write(data.encode("utf-8"))

0 commit comments

Comments
 (0)