3030
3131import bpython
3232from bpython .repl import Repl as BpythonRepl , SourceNotFound
33+ from bpython .repl import LineTypeTranslator as LineType
3334from bpython .config import (
3435 Struct ,
3536 loadini ,
@@ -278,14 +279,26 @@ def __init__(self, watcher, old_meta_path):
278279
279280 def find_distributions (self , context ):
280281 for finder in self .old_meta_path :
281- distribution_finder = getattr (finder , ' find_distributions' , None )
282+ distribution_finder = getattr (finder , " find_distributions" , None )
282283 if distribution_finder is not None :
283284 loader = finder .find_distributions (context )
284285 if loader is not None :
285286 return loader
286287
287288 return None
288289
290+ def find_spec (self , fullname , path , target = None ):
291+ for finder in self .old_meta_path :
292+ # Consider the finder only if it implements find_spec
293+ if not getattr (finder , "find_spec" , None ):
294+ continue
295+ # Attempt to find the spec
296+ spec = finder .find_spec (fullname , path , target )
297+ if spec is not None :
298+ # Patch the loader to enable reloading
299+ spec .__loader__ = ImportLoader (self .watcher , spec .__loader__ )
300+ return spec
301+
289302 def find_module (self , fullname , path = None ):
290303 for finder in self .old_meta_path :
291304 loader = finder .find_module (fullname , path )
@@ -389,20 +402,28 @@ def __init__(
389402 # so we're just using the same object
390403 self .interact = self .status_bar
391404
392- # line currently being edited, without ps1 (usually '>>> ')
405+ # logical line currently being edited, without ps1 (usually '>>> ')
393406 self ._current_line = ""
394407
395408 # current line of output - stdout and stdin go here
396409 self .current_stdouterr_line = ""
397410
398- # lines separated whenever logical line
399- # length goes over what the terminal width
400- # was at the time of original output
411+ # this is every line that's been displayed (input and output)
412+ # as with formatting applied. Logical lines that exceeded the terminal width
413+ # at the time of output are split across multiple entries in this list.
401414 self .display_lines = []
402415
403416 # this is every line that's been executed; it gets smaller on rewind
404417 self .history = []
405418
419+ # This is every logical line that's been displayed, both input and output.
420+ # Like self.history, lines are unwrapped, uncolored, and without prompt.
421+ # Entries are tuples, where
422+ # - the first element the line (string, not fmtsr)
423+ # - the second element is one of 2 global constants: "input" or "output"
424+ # (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings)
425+ self .all_logical_lines = []
426+
406427 # formatted version of lines in the buffer kept around so we can
407428 # unhighlight parens using self.reprint_line as called by bpython.Repl
408429 self .display_buffer = []
@@ -411,7 +432,8 @@ def __init__(
411432 # because there wasn't room to display everything
412433 self .scroll_offset = 0
413434
414- # from the left, 0 means first char
435+
436+ # cursor position relative to start of current_line, 0 is first char
415437 self ._cursor_offset = 0
416438
417439 self .orig_tcattrs = orig_tcattrs
@@ -723,8 +745,9 @@ def process_key_event(self, e):
723745 )
724746 and self .config .curtsies_right_arrow_completion
725747 and self .cursor_offset == len (self .current_line )
748+ # if at end of current line and user presses RIGHT (to autocomplete)
726749 ):
727-
750+ # then autocomplete
728751 self .current_line += self .current_suggestion
729752 self .cursor_offset = len (self .current_line )
730753 elif e in ("<UP>" ,) + key_dispatch [self .config .up_one_line_key ]:
@@ -872,6 +895,7 @@ def on_enter(self, new_code=True, reset_rl_history=True):
872895 self .rl_history .reset ()
873896
874897 self .history .append (self .current_line )
898+ self .all_logical_lines .append ((self .current_line , LineType .INPUT ))
875899 self .push (self .current_line , insert_into_history = new_code )
876900
877901 def on_tab (self , back = False ):
@@ -982,6 +1006,9 @@ def process_simple_keypress(self, e):
9821006 self .add_normal_character (e )
9831007
9841008 def send_current_block_to_external_editor (self , filename = None ):
1009+ """"
1010+ Sends the current code block to external editor to be edited. Usually bound to C-x.
1011+ """
9851012 text = self .send_to_external_editor (self .get_current_block ())
9861013 lines = [line for line in text .split ("\n " )]
9871014 while lines and not lines [- 1 ].split ():
@@ -994,15 +1021,12 @@ def send_current_block_to_external_editor(self, filename=None):
9941021 self .cursor_offset = len (self .current_line )
9951022
9961023 def send_session_to_external_editor (self , filename = None ):
1024+ """
1025+ Sends entire bpython session to external editor to be edited. Usually bound to F7.
1026+ """
9971027 for_editor = EDIT_SESSION_HEADER
998- for_editor += "\n " .join (
999- line [len (self .ps1 ) :]
1000- if line .startswith (self .ps1 )
1001- else line [len (self .ps2 ) :]
1002- if line .startswith (self .ps2 )
1003- else "### " + line
1004- for line in self .getstdout ().split ("\n " )
1005- )
1028+ for_editor += self .get_session_formatted_for_file ()
1029+
10061030 text = self .send_to_external_editor (for_editor )
10071031 if text == for_editor :
10081032 self .status_bar .message (
@@ -1016,7 +1040,7 @@ def send_session_to_external_editor(self, filename=None):
10161040 current_line = lines [- 1 ][4 :]
10171041 else :
10181042 current_line = ""
1019- from_editor = [line for line in lines if line [:3 ] != "###" ]
1043+ from_editor = [line for line in lines if line [:6 ] != "# OUT:" and line [: 3 ] != "###" ]
10201044 if all (not line .strip () for line in from_editor ):
10211045 self .status_bar .message (
10221046 _ ("Session not reevaluated because saved file was blank" )
@@ -1237,13 +1261,17 @@ def clear_current_block(self, remove_from_history=True):
12371261 if remove_from_history :
12381262 for unused in self .buffer :
12391263 self .history .pop ()
1264+ self .all_logical_lines .pop ()
12401265 self .buffer = []
12411266 self .cursor_offset = 0
12421267 self .saved_indent = 0
12431268 self .current_line = ""
12441269 self .cursor_offset = len (self .current_line )
12451270
12461271 def get_current_block (self ):
1272+ """
1273+ Returns the current code block as string (without prompts)
1274+ """
12471275 return "\n " .join (self .buffer + [self .current_line ])
12481276
12491277 def send_to_stdouterr (self , output ):
@@ -1271,6 +1299,13 @@ def send_to_stdouterr(self, output):
12711299 [],
12721300 )
12731301 )
1302+ # These can be FmtStrs, but self.all_logical_lines only wants strings
1303+ for line in [self .current_stdouterr_line ] + lines [1 :- 1 ]:
1304+ if isinstance (line , FmtStr ):
1305+ self .all_logical_lines .append ((line .s , LineType .OUTPUT ))
1306+ else :
1307+ self .all_logical_lines .append ((line , LineType .OUTPUT ))
1308+
12741309 self .current_stdouterr_line = lines [- 1 ]
12751310 logger .debug ("display_lines: %r" , self .display_lines )
12761311
@@ -1402,6 +1437,25 @@ def current_output_line(self, value):
14021437 self .current_stdouterr_line = ""
14031438 self .stdin .current_line = "\n "
14041439
1440+ def number_of_padding_chars_on_current_cursor_line (self ):
1441+ """ To avoid cutting off two-column characters at the end of lines where
1442+ there's only one column left, curtsies adds a padding char (u' ').
1443+ It's important to know about these for cursor positioning.
1444+
1445+ Should return zero unless there are fullwidth characters. """
1446+ full_line = self .current_cursor_line_without_suggestion
1447+ line_with_padding = "" .join (
1448+ [
1449+ line .s
1450+ for line in paint .display_linize (
1451+ self .current_cursor_line_without_suggestion .s , self .width
1452+ )
1453+ ]
1454+ )
1455+
1456+ # the difference in length here is how much padding there is
1457+ return len (line_with_padding ) - len (full_line )
1458+
14051459 def paint (
14061460 self ,
14071461 about_to_exit = False ,
@@ -1581,13 +1635,14 @@ def move_screen_up(current_line_start_row):
15811635 len (self .current_line ),
15821636 self .cursor_offset ,
15831637 )
1584- else : # Common case for determining cursor position
1638+ else : # Common case for determining cursor position
15851639 cursor_row , cursor_column = divmod (
15861640 (
15871641 wcswidth (self .current_cursor_line_without_suggestion .s )
15881642 - wcswidth (self .current_line )
1589- + wcswidth (self .current_line [: self .cursor_offset ])
1590- ),
1643+ + wcswidth (self .current_line [: self .cursor_offset ])
1644+ )
1645+ + self .number_of_padding_chars_on_current_cursor_line (),
15911646 width ,
15921647 )
15931648 assert cursor_column >= 0 , (
@@ -1804,11 +1859,13 @@ def take_back_buffer_line(self):
18041859 self .display_buffer .pop ()
18051860 self .buffer .pop ()
18061861 self .history .pop ()
1862+ self .all_logical_lines .pop ()
18071863
18081864 def take_back_empty_line (self ):
18091865 assert self .history and not self .history [- 1 ]
18101866 self .history .pop ()
18111867 self .display_lines .pop ()
1868+ self .all_logical_lines .pop ()
18121869
18131870 def prompt_undo (self ):
18141871 if self .buffer :
@@ -1824,10 +1881,11 @@ def prompt_for_undo():
18241881 greenlet .greenlet (prompt_for_undo ).switch ()
18251882
18261883 def redo (self ):
1827- if ( self .redo_stack ) :
1884+ if self .redo_stack :
18281885 temp = self .redo_stack .pop ()
1829- self .push (temp )
18301886 self .history .append (temp )
1887+ self .all_logical_lines .append ((temp , LineType .INPUT ))
1888+ self .push (temp )
18311889 else :
18321890 self .status_bar .message ("Nothing to redo." )
18331891
@@ -1839,6 +1897,7 @@ def reevaluate(self, new_code=False):
18391897 old_display_lines = self .display_lines
18401898 self .history = []
18411899 self .display_lines = []
1900+ self .all_logical_lines = []
18421901
18431902 if not self .weak_rewind :
18441903 self .interp = self .interp .__class__ ()
@@ -1903,6 +1962,9 @@ def initialize_interp(self):
19031962 del self .coderunner .interp .locals ["_Helper" ]
19041963
19051964 def getstdout (self ):
1965+ """
1966+ Returns a string of the current bpython session, wrapped, WITH prompts.
1967+ """
19061968 lines = self .lines_for_display + [self .current_line_formatted ]
19071969 s = (
19081970 "\n " .join (x .s if isinstance (x , FmtStr ) else x for x in lines )
0 commit comments