From 7adb9add69ecfb217ffc301d937db65bf2a356fe Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 5 Aug 2019 18:18:19 +0300 Subject: [PATCH 01/62] initial working shell sidebar --- Lib/idlelib/pyshell.py | 4 + Lib/idlelib/sidebar.py | 244 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 236 insertions(+), 12 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 66ae0f7435daba..f579ae77792c27 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -858,6 +858,7 @@ class PyShell(OutputWindow): # New classes from idlelib.history import History + from idlelib.sidebar import ShellSidebar def __init__(self, flist=None): if use_subprocess: @@ -926,6 +927,9 @@ def __init__(self, flist=None): # self.pollinterval = 50 # millisec + self.shell_sidebar = self.ShellSidebar(self) + self.shell_sidebar.show_sidebar() + def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 41c09684a20251..75f2b45ef26228 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -1,10 +1,12 @@ """Line numbering implementation for IDLE as an extension. Includes BaseSideBar which can be extended for other sidebar based extensions """ +import contextlib import functools import itertools import tkinter as tk +from tkinter.font import Font from idlelib.config import idleConf from idlelib.delegator import Delegator @@ -40,6 +42,15 @@ def get_widget_padding(widget): return padx, pady +@contextlib.contextmanager +def temp_enable_text_widget(text): + text.configure(state=tk.NORMAL) + try: + yield + finally: + text.configure(state=tk.DISABLED) + + class BaseSideBar: """ The base class for extensions which require a sidebar. @@ -168,8 +179,6 @@ def __init__(self, editwin): delegator.resetcache() delegator = delegator.delegate - self.is_shown = False - def bind_events(self): # Ensure focus is always redirected to the main editor text widget. self.sidebar_text.bind('', self.redirect_focusin_event) @@ -297,20 +306,231 @@ def update_sidebar_text(self, end): new_width = cur_width + width_difference self.sidebar_text['width'] = self._sidebar_width_type(new_width) - self.sidebar_text.config(state=tk.NORMAL) - if end > self.prev_end: - new_text = '\n'.join(itertools.chain( - [''], - map(str, range(self.prev_end + 1, end + 1)), - )) - self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') - else: - self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') - self.sidebar_text.config(state=tk.DISABLED) + with temp_enable_text_widget(self.sidebar_text): + if end > self.prev_end: + new_text = '\n'.join(itertools.chain( + [''], + map(str, range(self.prev_end + 1, end + 1)), + )) + self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') + else: + self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') self.prev_end = end +# class ShellSidebar(LineNumbers): +# """Show shell prompts in a sidebar.""" +# def __init__(self, editwin): +# super().__init__(editwin) +# self.sidebar_text.delete('1.0', 'end-1c') +# self.sidebar_text.config(width=3) +# self.sidebar_text.tag_config('linenumber', justify=tk.LEFT) +# +# def update_sidebar_text(self, end): +# """ +# Perform the following action: +# Each line sidebar_text contains the linenumber for that line +# Synchronize with editwin.text so that both sidebar_text and +# editwin.text contain the same number of lines""" +# if end == self.prev_end: +# return +# +# with temp_enable_text_widget(self.sidebar_text): +# # if end > self.prev_end: +# # new_text = '\n'.join(itertools.chain( +# # [''], +# # itertools.repeat('...', end - self.prev_end), +# # )) +# # self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') +# # elif self.prev_end > self.editwin.getlineno("iomark"): +# # self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') +# # else: +# import sys +# for i in range(1, self.editwin.getlineno('end')): +# print(i, self.text.tag_names(f'{i}.0'), file=sys.stderr) +# new_text = '\n'.join(itertools.chain( +# # [''], +# ('>>>' if 'console' in self.text.tag_names(f'{line}.0') else '---' +# # for line in range(self.prev_end + 1, end + 1)), +# for line in range(1, end + 1)), +# )) +# self.sidebar_text.delete('1.0', 'end-1c') +# self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') +# +# self.prev_end = end + + +class ChangeOrScrollDelegator(Delegator): + """Generate callbacks with the current end line number after + insert or delete operations""" + def __init__(self, changed_callback): + """ + changed_callback - Callable, will be called after insert + or delete operations with the current + end line number. + """ + Delegator.__init__(self) + self.changed_callback = changed_callback + + def insert(self, index, chars, tags=None): + self.delegate.insert(index, chars, tags) + self.changed_callback("insert") + + def replace(self, index1, index2, chars, *args): + self.delegate.replace(index1, index2, chars, *args) + self.changed_callback("replace") + + def delete(self, index1, index2=None): + self.delegate.delete(index1, index2) + self.changed_callback("delete") + + # def yview(self, *args): + # print(f'yview {args=}') + # self.delete.yview(*args) + # if args: + # self.changed_callback("yview") + # + # def yview_moveto(self, fraction): + # self.delegate.yview_moveto(fraction) + # self.changed_callback("yview_moveto") + # + # def yview_scroll(self, number, what): + # self.delegate.yview_moveto(number, what) + # self.changed_callback("yview_scroll") + + +class ShellSidebar: + def __init__(self, editwin): + self.editwin = editwin + self.parent = editwin.text_frame + self.text = editwin.text + + self.canvas = tk.Canvas(self.parent, width=30, + borderwidth=0, highlightthickness=0, + takefocus=False) + + self.bind_events() + + change_scroll_delegator = ChangeOrScrollDelegator(self.change_callback) + # Insert the delegator after the undo delegator, so that line numbers + # are properly updated after undo and redo actions. + change_scroll_delegator.setdelegate(self.editwin.undo.delegate) + self.editwin.undo.setdelegate(change_scroll_delegator) + # Reset the delegator caches of the delegators "above" the + # end line delegator we just inserted. + delegator = self.editwin.per.top + while delegator is not change_scroll_delegator: + delegator.resetcache() + delegator = delegator.delegate + + self.text['yscrollcommand'] = self.yscroll_event + + self.is_shown = False + + self.update_font() + self.update_colors() + + def show_sidebar(self): + if not self.is_shown: + self.update_sidebar() + _padx, pady = get_widget_padding(self.text) + self.canvas.grid(row=1, column=0, sticky=tk.NSEW, + # padx=2, pady=pady) + padx=2, pady=0) + self.is_shown = True + + def hide_sidebar(self): + if self.is_shown: + self.canvas.grid_forget() + self.canvas.delete(tk.ALL) + self.is_shown = False + + def change_callback(self, change_type): + if self.is_shown: + self.text.after_idle(self.update_sidebar) + + def update_sidebar(self): + text = self.text + canvas = self.canvas + + canvas.delete(tk.ALL) + + index = text.index("@0,0") + while True: + lineinfo = text.dlineinfo(index) + if lineinfo is None: + break + y = lineinfo[1] + tags = self.text.tag_names(f'{index} linestart') + prompt = '>>>' if 'console' in tags else '...' + canvas.create_text(2, y, anchor=tk.NW, text=prompt, + font=self.font, fill=self.colors[0]) + index = text.index(f'{index}+1line') + + # import sys + # for i in range(1, self.editwin.getlineno('end')): + # print(i, self.text.tag_names(f'{i}.0'), file=sys.stderr) + + def yscroll_event(self, *args, **kwargs): + """Redirect vertical scrolling to the main editor text widget. + + The scroll bar is also updated. + """ + self.editwin.vbar.set(*args) + self.change_callback('yview') + return 'break' + + def update_font(self): + """Update the sidebar text font, usually after config changes.""" + font = idleConf.GetFont(self.text, 'main', 'EditorWindow') + tk_font = Font(self.text, font=font) + char_width = max(tk_font.measure(char) for char in ['>', '.']) + self.canvas.configure(width=char_width * 3 + 4) + self._update_font(font) + + def _update_font(self, font): + self.font = font + self.change_callback("font") + + def update_colors(self): + """Update the sidebar text colors, usually after config changes.""" + colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') + self._update_colors(foreground=colors['foreground'], + background=colors['background']) + + def _update_colors(self, foreground, background): + self.colors = (foreground, background) + self.canvas.configure(background=self.colors[1]) + self.change_callback("colors") + + def redirect_focusin_event(self, event): + """Redirect focus-in events to the main editor text widget.""" + self.text.focus_set() + return 'break' + + def redirect_mousebutton_event(self, event, event_name): + """Redirect mouse button events to the main editor text widget.""" + self.text.focus_set() + self.text.event_generate(event_name, x=0, y=event.y) + return 'break' + + def redirect_mousewheel_event(self, event): + """Redirect mouse wheel events to the editwin text widget.""" + self.text.event_generate('', + x=0, y=event.y, delta=event.delta) + return 'break' + + def bind_events(self): + # Ensure focus is always redirected to the main editor text widget. + self.canvas.bind('', self.redirect_focusin_event) + + # Redirect mouse scrolling to the main editor text widget. + # + # Note that without this, scrolling with the mouse only scrolls + # the line numbers. + self.canvas.bind('', self.redirect_mousewheel_event) + def _linenumbers_drag_scrolling(parent): # htest # from idlelib.idle_test.test_sidebar import Dummy_editwin From 26bbaec0a50f70f8b641e5699c6984de8a1d2824 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 23 Aug 2019 18:19:59 +0300 Subject: [PATCH 02/62] fix recall and handling of prompt and input lines --- Lib/idlelib/pyshell.py | 42 ++++++++++++++++++++++++++++++--- Lib/idlelib/sidebar.py | 53 +++++++++++++++++------------------------- 2 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index f579ae77792c27..1736fb89f86d74 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -927,6 +927,9 @@ def __init__(self, flist=None): # self.pollinterval = 50 # millisec + self.input_lines = set() + self.prompt_lines = set() + self.shell_sidebar = self.ShellSidebar(self) self.shell_sidebar.show_sidebar() @@ -1167,10 +1170,22 @@ def enter_callback(self, event): # Check if there's a relevant stdin range -- if so, use it prev = self.text.tag_prevrange("stdin", "insert") if prev and self.text.compare("insert", "<", prev[1]): + for line in reversed(range(self.getlineno(prev[0]), + self.getlineno(prev[1]))): + if line in self.prompt_lines: + break + if self.text.compare(f"{line}.0", ">", prev[0]): + prev = (f"{line}.0", prev[1]) self.recall(self.text.get(prev[0], prev[1]), event) return "break" next = self.text.tag_nextrange("stdin", "insert") if next and self.text.compare("insert lineend", ">=", next[0]): + for line in range(self.getlineno(next[0]), + self.getlineno(next[1])): + if line + 1 in self.prompt_lines: + break + if self.text.compare(f"{line}.end", "<", next[1]): + next = (next[0], f"{line}.end") self.recall(self.text.get(next[0], next[1]), event) return "break" # No stdin mark -- just get the current line, less any prompt @@ -1212,7 +1227,7 @@ def enter_callback(self, event): def recall(self, s, event): # remove leading and trailing empty or whitespace lines - s = re.sub(r'^\s*\n', '' , s) + s = re.sub(r'^\s*\n', '', s) s = re.sub(r'\n\s*$', '', s) lines = s.split('\n') self.text.undo_block_start() @@ -1223,6 +1238,7 @@ def recall(self, s, event): if prefix.rstrip().endswith(':'): self.newline_and_indent_event(event) prefix = self.text.get("insert linestart", "insert") + first_line = self.getlineno("insert") self.text.insert("insert", lines[0].strip()) if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) @@ -1232,6 +1248,9 @@ def recall(self, s, event): # replace orig base indentation with new indentation line = new_base_indent + line[len(orig_base_indent):] self.text.insert('insert', '\n'+line.rstrip()) + self.input_lines.update( + range(first_line, self.getlineno("insert") + 1)) + self.shell_sidebar.update_sidebar() finally: self.text.see("insert") self.text.undo_block_stop() @@ -1248,7 +1267,13 @@ def runit(self): while i > 0 and line[i-1] in " \t": i = i-1 line = line[:i] - self.interp.runsource(line) + input_is_incomplete = self.interp.runsource(line) + # If the input is a complete statement, the sidebar updates will be + # handled elsewhere. + if input_is_incomplete: + self.input_lines.update(range(self.getlineno("iomark"), + self.getlineno("end-1c") + 1)) + self.shell_sidebar.update_sidebar() def open_stack_viewer(self, event=None): if self.interp.rpcclt: @@ -1274,7 +1299,18 @@ def restart_shell(self, event=None): def showprompt(self): self.resetoutput() - self.console.write(self.prompt) + + prompt_lineno = self.getlineno("iomark") + prompt = self.prompt + if self.sys_ps1 and prompt.endswith(self.sys_ps1): + prompt = prompt[:-len(self.sys_ps1)] + self.console.write(prompt) + + n_prompt_lines = prompt.count('\n') + 1 + self.prompt_lines.update( + range(prompt_lineno, prompt_lineno + n_prompt_lines)) + self.shell_sidebar.update_sidebar() + self.text.mark_set("insert", "end-1c") self.set_line_and_column() self.io.reset_undo() diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 75f2b45ef26228..5a604d84b0e605 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -361,9 +361,8 @@ def update_sidebar_text(self, end): # self.prev_end = end -class ChangeOrScrollDelegator(Delegator): - """Generate callbacks with the current end line number after - insert or delete operations""" +class TextChangeDelegator(Delegator): + """Generate callbacks upon insert, replace and delete operations.""" def __init__(self, changed_callback): """ changed_callback - Callable, will be called after insert @@ -385,22 +384,9 @@ def delete(self, index1, index2=None): self.delegate.delete(index1, index2) self.changed_callback("delete") - # def yview(self, *args): - # print(f'yview {args=}') - # self.delete.yview(*args) - # if args: - # self.changed_callback("yview") - # - # def yview_moveto(self, fraction): - # self.delegate.yview_moveto(fraction) - # self.changed_callback("yview_moveto") - # - # def yview_scroll(self, number, what): - # self.delegate.yview_moveto(number, what) - # self.changed_callback("yview_scroll") - class ShellSidebar: + """Sidebar for the PyShell window, for prompts etc.""" def __init__(self, editwin): self.editwin = editwin self.parent = editwin.text_frame @@ -412,15 +398,15 @@ def __init__(self, editwin): self.bind_events() - change_scroll_delegator = ChangeOrScrollDelegator(self.change_callback) - # Insert the delegator after the undo delegator, so that line numbers - # are properly updated after undo and redo actions. - change_scroll_delegator.setdelegate(self.editwin.undo.delegate) - self.editwin.undo.setdelegate(change_scroll_delegator) + change_delegator = TextChangeDelegator(self.change_callback) + # Insert the TextChangeDelegator after the undo delegator, so that + # the sidebar is properly updated after undo and redo actions. + change_delegator.setdelegate(self.editwin.undo.delegate) + self.editwin.undo.setdelegate(change_delegator) # Reset the delegator caches of the delegators "above" the - # end line delegator we just inserted. + # TextChangeDelegator we just inserted. delegator = self.editwin.per.top - while delegator is not change_scroll_delegator: + while delegator is not change_delegator: delegator.resetcache() delegator = delegator.delegate @@ -434,7 +420,7 @@ def __init__(self, editwin): def show_sidebar(self): if not self.is_shown: self.update_sidebar() - _padx, pady = get_widget_padding(self.text) + # _padx, pady = get_widget_padding(self.text) self.canvas.grid(row=1, column=0, sticky=tk.NSEW, # padx=2, pady=pady) padx=2, pady=0) @@ -447,8 +433,10 @@ def hide_sidebar(self): self.is_shown = False def change_callback(self, change_type): + if change_type == "insert": + return if self.is_shown: - self.text.after_idle(self.update_sidebar) + self.update_sidebar() def update_sidebar(self): text = self.text @@ -462,16 +450,16 @@ def update_sidebar(self): if lineinfo is None: break y = lineinfo[1] - tags = self.text.tag_names(f'{index} linestart') - prompt = '>>>' if 'console' in tags else '...' + lineno = self.editwin.getlineno(index) + prompt = ( + '>>>' if lineno in self.editwin.prompt_lines else + '...' if lineno in self.editwin.input_lines else + ' ' + ) canvas.create_text(2, y, anchor=tk.NW, text=prompt, font=self.font, fill=self.colors[0]) index = text.index(f'{index}+1line') - # import sys - # for i in range(1, self.editwin.getlineno('end')): - # print(i, self.text.tag_names(f'{i}.0'), file=sys.stderr) - def yscroll_event(self, *args, **kwargs): """Redirect vertical scrolling to the main editor text widget. @@ -531,6 +519,7 @@ def bind_events(self): # the line numbers. self.canvas.bind('', self.redirect_mousewheel_event) + def _linenumbers_drag_scrolling(parent): # htest # from idlelib.idle_test.test_sidebar import Dummy_editwin From 9076da5661c46bfe49617d643fbbbd97cfb236f0 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 23 Aug 2019 23:59:30 +0300 Subject: [PATCH 03/62] update only as needed during editing, accounting for line wrapping --- Lib/idlelib/sidebar.py | 103 ++++++++++++++++++++++++++++++----------- 1 file changed, 76 insertions(+), 27 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 5a604d84b0e605..8b54db0a7413d2 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -11,10 +11,22 @@ from idlelib.delegator import Delegator +def get_lineno(text, index): + """Utility to get the line number of an index in a Tk text widget.""" + return int(float(text.index(index))) + + def get_end_linenumber(text): """Utility to get the last line's number in a Tk text widget.""" - return int(float(text.index('end-1c'))) + return get_lineno(text, 'end-1c') + +def get_displaylines(text, index): + """Display height, in lines, of a logical line in a Tk text widget.""" + res = text.count(f"{index} linestart", + f"{index} lineend", + "displaylines") + return res[0] if res else 0 def get_widget_padding(widget): """Get the total padding of a Tk widget, including its border.""" @@ -361,28 +373,63 @@ def update_sidebar_text(self, end): # self.prev_end = end -class TextChangeDelegator(Delegator): - """Generate callbacks upon insert, replace and delete operations.""" - def __init__(self, changed_callback): +class WrappedLineHeightChangeDelegator(Delegator): + def __init__(self, callback): """ - changed_callback - Callable, will be called after insert - or delete operations with the current - end line number. + callback - Callable, will be called when an insert, delete or replace + action on the text widget requires updating the shell + sidebar. """ Delegator.__init__(self) - self.changed_callback = changed_callback + self.callback = callback def insert(self, index, chars, tags=None): + is_single_line = '\n' not in chars + if not is_single_line: + before_displaylines = get_displaylines(self, index) + self.delegate.insert(index, chars, tags) - self.changed_callback("insert") + + if not is_single_line: + after_displaylines = get_displaylines(self, index) + if after_displaylines == before_displaylines: + return # no need to update the sidebar + + self.callback() def replace(self, index1, index2, chars, *args): + is_single_line = ( + '\n' not in chars and + get_lineno(self, index1) == get_lineno(self, index2) + ) + if is_single_line: + before_displaylines = get_displaylines(self, index1) + self.delegate.replace(index1, index2, chars, *args) - self.changed_callback("replace") + + if is_single_line: + after_displaylines = get_displaylines(self, index1) + if after_displaylines == before_displaylines: + return # no need to update the sidebar + + self.callback() def delete(self, index1, index2=None): + if index2 is None: + index2 = index1 + "+1c" + is_single_line = get_lineno(self, index1) == get_lineno(self, index2) + if is_single_line: + before_displaylines = get_displaylines(self, index1) + self.delegate.delete(index1, index2) - self.changed_callback("delete") + + if is_single_line: + after_displaylines = get_displaylines(self, index1) + print("displaylines", before_displaylines, after_displaylines) + if after_displaylines == before_displaylines: + return # no need to update the sidebar + + self.callback() class ShellSidebar: @@ -398,17 +445,21 @@ def __init__(self, editwin): self.bind_events() - change_delegator = TextChangeDelegator(self.change_callback) - # Insert the TextChangeDelegator after the undo delegator, so that - # the sidebar is properly updated after undo and redo actions. - change_delegator.setdelegate(self.editwin.undo.delegate) - self.editwin.undo.setdelegate(change_delegator) - # Reset the delegator caches of the delegators "above" the - # TextChangeDelegator we just inserted. + change_delegator = \ + WrappedLineHeightChangeDelegator(self.change_callback) + + # Insert the TextChangeDelegator after the last delegator, so that + # the sidebar reflects final changes to the text widget contents. delegator = self.editwin.per.top - while delegator is not change_delegator: - delegator.resetcache() - delegator = delegator.delegate + if delegator.delegate != self.text: + while delegator.delegate != self.editwin.per.bottom: + # Reset the delegator caches of the delegators "above" the + # TextChangeDelegator we will insert. + delegator.resetcache() + delegator = delegator.delegate + last_delegator = delegator + change_delegator.setdelegate(last_delegator.delegate) + last_delegator.setdelegate(change_delegator) self.text['yscrollcommand'] = self.yscroll_event @@ -432,9 +483,7 @@ def hide_sidebar(self): self.canvas.delete(tk.ALL) self.is_shown = False - def change_callback(self, change_type): - if change_type == "insert": - return + def change_callback(self): if self.is_shown: self.update_sidebar() @@ -466,7 +515,7 @@ def yscroll_event(self, *args, **kwargs): The scroll bar is also updated. """ self.editwin.vbar.set(*args) - self.change_callback('yview') + self.change_callback() return 'break' def update_font(self): @@ -479,7 +528,7 @@ def update_font(self): def _update_font(self, font): self.font = font - self.change_callback("font") + self.change_callback() def update_colors(self): """Update the sidebar text colors, usually after config changes.""" @@ -490,7 +539,7 @@ def update_colors(self): def _update_colors(self, foreground, background): self.colors = (foreground, background) self.canvas.configure(background=self.colors[1]) - self.change_callback("colors") + self.change_callback() def redirect_focusin_event(self, event): """Redirect focus-in events to the main editor text widget.""" From cad2cd1496480d238ef92bd56dfa6ca5208d23df Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 18:34:11 +0300 Subject: [PATCH 04/62] properly insert delegators into their proper place in the percolator Specifically, avoid EditorWindow.ResetColorizer() moving the undo and colorizer delegators to the top of the percolator, by removing and inserting them. --- Lib/idlelib/editor.py | 4 +--- Lib/idlelib/percolator.py | 15 +++++++++++++++ Lib/idlelib/sidebar.py | 25 +++++++------------------ 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index a178eaf93c013a..764c5b07f022ff 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -782,9 +782,7 @@ def _addcolorizer(self): self.color = self.ColorDelegator() # can add more colorizers here... if self.color: - self.per.removefilter(self.undo) - self.per.insertfilter(self.color) - self.per.insertfilter(self.undo) + self.per.insertfilterafter(filter=self.color, after=self.undo) def _rmcolorizer(self): if not self.color: diff --git a/Lib/idlelib/percolator.py b/Lib/idlelib/percolator.py index db70304f589159..1fe34d29f54eb2 100644 --- a/Lib/idlelib/percolator.py +++ b/Lib/idlelib/percolator.py @@ -38,6 +38,21 @@ def insertfilter(self, filter): filter.setdelegate(self.top) self.top = filter + def insertfilterafter(self, filter, after): + assert isinstance(filter, Delegator) + assert isinstance(after, Delegator) + assert filter.delegate is None + + f = self.top + f.resetcache() + while f is not after: + assert f is not self.bottom + f = f.delegate + f.resetcache() + + filter.setdelegate(f.delegate) + f.setdelegate(filter) + def removefilter(self, filter): # XXX Perhaps should only support popfilter()? assert isinstance(filter, Delegator) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 8b54db0a7413d2..5e98cfe8170226 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -182,14 +182,8 @@ def __init__(self, editwin): end_line_delegator = EndLineDelegator(self.update_sidebar_text) # Insert the delegator after the undo delegator, so that line numbers # are properly updated after undo and redo actions. - end_line_delegator.setdelegate(self.editwin.undo.delegate) - self.editwin.undo.setdelegate(end_line_delegator) - # Reset the delegator caches of the delegators "above" the - # end line delegator we just inserted. - delegator = self.editwin.per.top - while delegator is not end_line_delegator: - delegator.resetcache() - delegator = delegator.delegate + self.editwin.per.insertfilterafter(filter=end_line_delegator, + after=self.editwin.undo) def bind_events(self): # Ensure focus is always redirected to the main editor text widget. @@ -450,16 +444,11 @@ def __init__(self, editwin): # Insert the TextChangeDelegator after the last delegator, so that # the sidebar reflects final changes to the text widget contents. - delegator = self.editwin.per.top - if delegator.delegate != self.text: - while delegator.delegate != self.editwin.per.bottom: - # Reset the delegator caches of the delegators "above" the - # TextChangeDelegator we will insert. - delegator.resetcache() - delegator = delegator.delegate - last_delegator = delegator - change_delegator.setdelegate(last_delegator.delegate) - last_delegator.setdelegate(change_delegator) + d = self.editwin.per.top + if d.delegate is not self.text: + while d.delegate is not self.editwin.per.bottom: + d = d.delegate + self.editwin.per.insertfilterafter(change_delegator, d) self.text['yscrollcommand'] = self.yscroll_event From 4040e26c4c1e2b46c731b97d6625ee35b454ccfc Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 18:39:39 +0300 Subject: [PATCH 05/62] move non-syntax tag highlighting out of ColorDelegator This avoids ColorDelegator removing unrelated tags, specifically "stdin", when re-colorizing. --- Lib/idlelib/colorizer.py | 8 +++----- Lib/idlelib/configdialog.py | 2 +- Lib/idlelib/editor.py | 30 ++++++++++++++++++++++++++---- Lib/idlelib/pyshell.py | 27 ++++++++++++--------------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py index db1266fed3b691..e28fbbe9200257 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/colorizer.py @@ -120,12 +120,10 @@ def LoadTagDefs(self): "BUILTIN": idleConf.GetHighlight(theme, "builtin"), "STRING": idleConf.GetHighlight(theme, "string"), "DEFINITION": idleConf.GetHighlight(theme, "definition"), - "SYNC": {'background':None,'foreground':None}, - "TODO": {'background':None,'foreground':None}, + "SYNC": {'background': None, 'foreground': None}, + "TODO": {'background': None, 'foreground': None}, "ERROR": idleConf.GetHighlight(theme, "error"), - # The following is used by ReplaceDialog: - "hit": idleConf.GetHighlight(theme, "hit"), - } + } if DEBUG: print('tagdefs',self.tagdefs) diff --git a/Lib/idlelib/configdialog.py b/Lib/idlelib/configdialog.py index 82596498d34611..44cf8339386192 100644 --- a/Lib/idlelib/configdialog.py +++ b/Lib/idlelib/configdialog.py @@ -233,7 +233,7 @@ def activate_config_changes(self): """ win_instances = self.parent.instance_dict.keys() for instance in win_instances: - instance.ResetColorizer() + instance.update_colors() instance.ResetFont() instance.set_notabs_indentwidth() instance.ApplyKeybindings() diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 764c5b07f022ff..035d716c1eef4e 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -266,7 +266,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): io.set_filename_change_hook(self.filename_change_hook) self.good_load = False self.set_indentation_params(False) - self.color = None # initialized below in self.ResetColorizer + self.color = None # initialized below in self.update_colors() self.code_context = None # optionally initialized later below self.line_numbers = None # optionally initialized later below if filename: @@ -279,7 +279,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): io.set_filename(filename) self.good_load = True - self.ResetColorizer() + self.update_colors() self.saved_change_hook() self.update_recent_files_list() self.load_extensions() @@ -792,18 +792,40 @@ def _rmcolorizer(self): self.color = None def ResetColorizer(self): - "Update the color theme" - # Called from self.filename_change_hook and from configdialog.py + """Reset the syntax colorizer. + + For example, this is called after filename changes, since the file + type can affect the highlighting. + """ self._rmcolorizer() self._addcolorizer() + + def update_colors(self): + """Update the color theme. + + For example, this is called after highlighting config changes. + """ EditorWindow.color_config(self.text) + tag_colors = self.get_tag_colors() + for tag, tag_colors_config in tag_colors.items(): + self.text.tag_configure(tag, **tag_colors_config) + + self.ResetColorizer() + if self.code_context is not None: self.code_context.update_highlight_colors() if self.line_numbers is not None: self.line_numbers.update_colors() + def get_tag_colors(self): + theme = idleConf.CurrentTheme() + return { + # The following is used by ReplaceDialog: + "hit": idleConf.GetHighlight(theme, "hit"), + } + IDENTCHARS = string.ascii_letters + string.digits + "_" def colorize_syntax_error(self, text, pos): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 1736fb89f86d74..09cfa2ced25008 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -335,26 +335,11 @@ def open_shell(self, event=None): class ModifiedColorDelegator(ColorDelegator): "Extend base class: colorizer for the shell window itself" - - def __init__(self): - ColorDelegator.__init__(self) - self.LoadTagDefs() - def recolorize_main(self): self.tag_remove("TODO", "1.0", "iomark") self.tag_add("SYNC", "1.0", "iomark") ColorDelegator.recolorize_main(self) - def LoadTagDefs(self): - ColorDelegator.LoadTagDefs(self) - theme = idleConf.CurrentTheme() - self.tagdefs.update({ - "stdin": {'background':None,'foreground':None}, - "stdout": idleConf.GetHighlight(theme, "stdout"), - "stderr": idleConf.GetHighlight(theme, "stderr"), - "console": idleConf.GetHighlight(theme, "console"), - }) - def removecolors(self): # Don't remove shell color tags before "iomark" for tag in self.tagdefs: @@ -933,6 +918,18 @@ def __init__(self, flist=None): self.shell_sidebar = self.ShellSidebar(self) self.shell_sidebar.show_sidebar() + def get_tag_colors(self): + tag_colors = super().get_tag_colors() + + theme = idleConf.CurrentTheme() + tag_colors.update({ + "stdin": {'background': None, 'foreground': None}, + "stdout": idleConf.GetHighlight(theme, "stdout"), + "stderr": idleConf.GetHighlight(theme, "stderr"), + "console": idleConf.GetHighlight(theme, "console"), + }) + return tag_colors + def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) From 9b31031896964f5853d0f7d63ba83ac7bffc23c7 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 19:04:10 +0300 Subject: [PATCH 06/62] replace shell sidebar prompt and input line recognition method Prompts are now marked in the text by adding the "console" tag to the newline character ('\n') before the beginning of the line. This works around issues with the prompt itself usually no longer being written in the text widget. With another small fix to the PyShell.recall() method, input lines can now be recognized consistently by checking for the "stdin" tag. This is much better than attempting to separately track the prompt and input lines, since those are difficult to keep up to date after undo/redo actions and squeezing/unsqueezing of previous output. --- Lib/idlelib/pyshell.py | 37 ++++++++++++++++++++----------------- Lib/idlelib/sidebar.py | 8 +++++--- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 09cfa2ced25008..8f9c3bacbf9fad 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -48,6 +48,7 @@ from idlelib.colorizer import ColorDelegator from idlelib.config import idleConf +from idlelib.delegator import Delegator from idlelib import debugger from idlelib import debugger_r from idlelib.editor import EditorWindow, fixwordbreaks @@ -367,6 +368,14 @@ def delete(self, index1, index2=None): UndoDelegator.delete(self, index1, index2) +class UserInputTaggingDelegator(Delegator): + """Delegator used to tag user input with "stdin".""" + def insert(self, index, chars, tags=None): + if tags is None: + tags = "stdin" + self.delegate.insert(index, chars, tags) + + class MyRPCClient(rpc.RPCClient): def handle_EOF(self): @@ -912,12 +921,15 @@ def __init__(self, flist=None): # self.pollinterval = 50 # millisec - self.input_lines = set() - self.prompt_lines = set() - self.shell_sidebar = self.ShellSidebar(self) self.shell_sidebar.show_sidebar() + # Insert UserInputTaggingDelegator at the top of the percolator, + # but make calls to text.insert() skip it. This causes only insert + # events generated in Tcl/Tk to go through this delegator. + self.text.insert = self.per.top.insert + self.per.insertfilter(UserInputTaggingDelegator()) + def get_tag_colors(self): tag_colors = super().get_tag_colors() @@ -1169,7 +1181,7 @@ def enter_callback(self, event): if prev and self.text.compare("insert", "<", prev[1]): for line in reversed(range(self.getlineno(prev[0]), self.getlineno(prev[1]))): - if line in self.prompt_lines: + if "console" in self.text.tag_names(f"{line}.0-1c"): break if self.text.compare(f"{line}.0", ">", prev[0]): prev = (f"{line}.0", prev[1]) @@ -1179,7 +1191,7 @@ def enter_callback(self, event): if next and self.text.compare("insert lineend", ">=", next[0]): for line in range(self.getlineno(next[0]), self.getlineno(next[1])): - if line + 1 in self.prompt_lines: + if "console" in self.text.tag_names(f"{line+1}.0-1c"): break if self.text.compare(f"{line}.end", "<", next[1]): next = (next[0], f"{line}.end") @@ -1236,7 +1248,7 @@ def recall(self, s, event): self.newline_and_indent_event(event) prefix = self.text.get("insert linestart", "insert") first_line = self.getlineno("insert") - self.text.insert("insert", lines[0].strip()) + self.text.insert("insert", lines[0].strip(), "stdin") if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) @@ -1244,9 +1256,7 @@ def recall(self, s, event): if line.startswith(orig_base_indent): # replace orig base indentation with new indentation line = new_base_indent + line[len(orig_base_indent):] - self.text.insert('insert', '\n'+line.rstrip()) - self.input_lines.update( - range(first_line, self.getlineno("insert") + 1)) + self.text.insert('insert', '\n'+line.rstrip(), "stdin") self.shell_sidebar.update_sidebar() finally: self.text.see("insert") @@ -1268,8 +1278,6 @@ def runit(self): # If the input is a complete statement, the sidebar updates will be # handled elsewhere. if input_is_incomplete: - self.input_lines.update(range(self.getlineno("iomark"), - self.getlineno("end-1c") + 1)) self.shell_sidebar.update_sidebar() def open_stack_viewer(self, event=None): @@ -1297,17 +1305,12 @@ def restart_shell(self, event=None): def showprompt(self): self.resetoutput() - prompt_lineno = self.getlineno("iomark") prompt = self.prompt if self.sys_ps1 and prompt.endswith(self.sys_ps1): prompt = prompt[:-len(self.sys_ps1)] + self.text.tag_add("console", "iomark-1c") self.console.write(prompt) - n_prompt_lines = prompt.count('\n') + 1 - self.prompt_lines.update( - range(prompt_lineno, prompt_lineno + n_prompt_lines)) - self.shell_sidebar.update_sidebar() - self.text.mark_set("insert", "end-1c") self.set_line_and_column() self.io.reset_undo() diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 5e98cfe8170226..8611d29ffeea5a 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -478,6 +478,7 @@ def change_callback(self): def update_sidebar(self): text = self.text + text_tagnames = text.tag_names canvas = self.canvas canvas.delete(tk.ALL) @@ -488,10 +489,11 @@ def update_sidebar(self): if lineinfo is None: break y = lineinfo[1] - lineno = self.editwin.getlineno(index) + is_prompt = "console" in text_tagnames(f"{index} linestart -1c") + is_input = "stdin" in text_tagnames(f"{index} lineend -1c") prompt = ( - '>>>' if lineno in self.editwin.prompt_lines else - '...' if lineno in self.editwin.input_lines else + '>>>' if is_prompt else + '...' if is_input else ' ' ) canvas.create_text(2, y, anchor=tk.NW, text=prompt, From 37a0db3c9c3981557e2bffff5146e0f14609c81b Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 19:04:23 +0300 Subject: [PATCH 07/62] minor code cleanup --- Lib/idlelib/sidebar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 8611d29ffeea5a..acb2bb6c2370c9 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -371,7 +371,7 @@ class WrappedLineHeightChangeDelegator(Delegator): def __init__(self, callback): """ callback - Callable, will be called when an insert, delete or replace - action on the text widget requires updating the shell + action on the text widget may require updating the shell sidebar. """ Delegator.__init__(self) @@ -419,7 +419,6 @@ def delete(self, index1, index2=None): if is_single_line: after_displaylines = get_displaylines(self, index1) - print("displaylines", before_displaylines, after_displaylines) if after_displaylines == before_displaylines: return # no need to update the sidebar From c7668222b524d89c0f55a5db82969ea3dfbcdbad Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 23:22:01 +0300 Subject: [PATCH 08/62] remove unnecessary .replace() delegator method --- Lib/idlelib/sidebar.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index acb2bb6c2370c9..e4580a36c5d79c 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -391,23 +391,6 @@ def insert(self, index, chars, tags=None): self.callback() - def replace(self, index1, index2, chars, *args): - is_single_line = ( - '\n' not in chars and - get_lineno(self, index1) == get_lineno(self, index2) - ) - if is_single_line: - before_displaylines = get_displaylines(self, index1) - - self.delegate.replace(index1, index2, chars, *args) - - if is_single_line: - after_displaylines = get_displaylines(self, index1) - if after_displaylines == before_displaylines: - return # no need to update the sidebar - - self.callback() - def delete(self, index1, index2=None): if index2 is None: index2 = index1 + "+1c" From 50bd0820bc12c1477321859b27e8ab89e5afcd03 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 23:22:59 +0300 Subject: [PATCH 09/62] fix inverted condition in deciding whether to update sidebar (oops!) --- Lib/idlelib/sidebar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index e4580a36c5d79c..d660632665a37e 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -379,12 +379,12 @@ def __init__(self, callback): def insert(self, index, chars, tags=None): is_single_line = '\n' not in chars - if not is_single_line: + if is_single_line: before_displaylines = get_displaylines(self, index) self.delegate.insert(index, chars, tags) - if not is_single_line: + if is_single_line: after_displaylines = get_displaylines(self, index) if after_displaylines == before_displaylines: return # no need to update the sidebar @@ -430,7 +430,7 @@ def __init__(self, editwin): if d.delegate is not self.text: while d.delegate is not self.editwin.per.bottom: d = d.delegate - self.editwin.per.insertfilterafter(change_delegator, d) + self.editwin.per.insertfilterafter(change_delegator, after=d) self.text['yscrollcommand'] = self.yscroll_event From 6f186f09c1325a1e2a0d3f0d2f8292f917c5c69d Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 23:34:20 +0300 Subject: [PATCH 10/62] fix "stdin" tag removed when undo-ing a deletion --- Lib/idlelib/pyshell.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 8f9c3bacbf9fad..852327c2f0627d 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -346,9 +346,9 @@ def removecolors(self): for tag in self.tagdefs: self.tag_remove(tag, "iomark", "end") + class ModifiedUndoDelegator(UndoDelegator): "Extend base class: forbid insert/delete before the I/O mark" - def insert(self, index, chars, tags=None): try: if self.delegate.compare(index, "<", "iomark"): @@ -367,6 +367,19 @@ def delete(self, index1, index2=None): pass UndoDelegator.delete(self, index1, index2) + def undo_event(self, event): + # Temporarily monkey-patch the delegate's .insert() method to + # always use the "stdin" tag. This is needed for undo-ing + # deletions to preserve the "stdin" tag, because UndoDelegator + # doesn't preserve tags for deleted text. + orig_insert = self.delegate.insert + self.delegate.insert = \ + lambda index, chars: orig_insert(index, chars, "stdin") + try: + super().undo_event(event) + finally: + self.delegate.insert = orig_insert + class UserInputTaggingDelegator(Delegator): """Delegator used to tag user input with "stdin".""" From f7f3de9964d65d0a59cfd3c46886e7a54a0774e1 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 23:34:38 +0300 Subject: [PATCH 11/62] fix sidebar update on new prompt --- Lib/idlelib/pyshell.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 852327c2f0627d..b7057e5cbcb96b 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1324,6 +1324,7 @@ def showprompt(self): self.text.tag_add("console", "iomark-1c") self.console.write(prompt) + self.shell_sidebar.update_sidebar() self.text.mark_set("insert", "end-1c") self.set_line_and_column() self.io.reset_undo() From e929ea763488418e60ba28af5964252ade5150c6 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sat, 24 Aug 2019 23:49:50 +0300 Subject: [PATCH 12/62] add a NEWS entry --- Misc/NEWS.d/next/IDLE/2019-08-24-23-49-36.bpo-37903.4xjast.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/IDLE/2019-08-24-23-49-36.bpo-37903.4xjast.rst diff --git a/Misc/NEWS.d/next/IDLE/2019-08-24-23-49-36.bpo-37903.4xjast.rst b/Misc/NEWS.d/next/IDLE/2019-08-24-23-49-36.bpo-37903.4xjast.rst new file mode 100644 index 00000000000000..56b50e2e91e467 --- /dev/null +++ b/Misc/NEWS.d/next/IDLE/2019-08-24-23-49-36.bpo-37903.4xjast.rst @@ -0,0 +1 @@ +IDLE's shell now shows prompts in a separate side-bar. From d04bfa31b4b867eef943d9b2efa0608fcf652b39 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 25 Aug 2019 00:42:55 +0300 Subject: [PATCH 13/62] fix recall handling with multiple successive statements --- Lib/idlelib/pyshell.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index b7057e5cbcb96b..e6887f778ac0b2 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1189,25 +1189,30 @@ def enter_callback(self, event): # the current line, less a leading prompt, less leading or # trailing whitespace if self.text.compare("insert", "<", "iomark linestart"): - # Check if there's a relevant stdin range -- if so, use it + # Check if there's a relevant stdin range -- if so, use it. + # Note: "stdin" blocks may include several successive statements, + # so look for "console" tags on the newline before each statement + # (and possibly on prompts). prev = self.text.tag_prevrange("stdin", "insert") - if prev and self.text.compare("insert", "<", prev[1]): - for line in reversed(range(self.getlineno(prev[0]), - self.getlineno(prev[1]))): - if "console" in self.text.tag_names(f"{line}.0-1c"): - break - if self.text.compare(f"{line}.0", ">", prev[0]): - prev = (f"{line}.0", prev[1]) + if ( + prev and + self.text.compare("insert", "<", prev[1]) and + # The following is needed to handle empty statements. + "console" not in self.text.tag_names("insert") + ): + prev_cons = self.text.tag_prevrange("console", "insert") + if prev_cons and self.text.compare(prev_cons[1], ">=", prev[0]): + prev = (prev_cons[1], prev[1]) + next_cons = self.text.tag_nextrange("console", "insert") + if next_cons and self.text.compare(next_cons[0], "<", prev[1]): + prev = (prev[0], self.text.index(next_cons[0] + "+1c")) self.recall(self.text.get(prev[0], prev[1]), event) return "break" next = self.text.tag_nextrange("stdin", "insert") if next and self.text.compare("insert lineend", ">=", next[0]): - for line in range(self.getlineno(next[0]), - self.getlineno(next[1])): - if "console" in self.text.tag_names(f"{line+1}.0-1c"): - break - if self.text.compare(f"{line}.end", "<", next[1]): - next = (next[0], f"{line}.end") + next_cons = self.text.tag_nextrange("console", "insert lineend") + if next_cons and self.text.compare(next_cons[0], "<", next[1]): + next = (next[0], self.text.index(next_cons[0] + "+1c")) self.recall(self.text.get(next[0], next[1]), event) return "break" # No stdin mark -- just get the current line, less any prompt From 81f48522b8764636014c8bec6d96c6e9f5146678 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 25 Aug 2019 00:43:08 +0300 Subject: [PATCH 14/62] remove a dead line --- Lib/idlelib/pyshell.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index e6887f778ac0b2..82d4b07db65ea2 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1265,7 +1265,6 @@ def recall(self, s, event): if prefix.rstrip().endswith(':'): self.newline_and_indent_event(event) prefix = self.text.get("insert linestart", "insert") - first_line = self.getlineno("insert") self.text.insert("insert", lines[0].strip(), "stdin") if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) From 46be85e1c26d7f93d14b4d3215842cc3d9b4b59b Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 25 Aug 2019 00:45:07 +0300 Subject: [PATCH 15/62] remove commented out initial attempt --- Lib/idlelib/sidebar.py | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index d660632665a37e..0504c80819494d 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -325,48 +325,6 @@ def update_sidebar_text(self, end): self.prev_end = end -# class ShellSidebar(LineNumbers): -# """Show shell prompts in a sidebar.""" -# def __init__(self, editwin): -# super().__init__(editwin) -# self.sidebar_text.delete('1.0', 'end-1c') -# self.sidebar_text.config(width=3) -# self.sidebar_text.tag_config('linenumber', justify=tk.LEFT) -# -# def update_sidebar_text(self, end): -# """ -# Perform the following action: -# Each line sidebar_text contains the linenumber for that line -# Synchronize with editwin.text so that both sidebar_text and -# editwin.text contain the same number of lines""" -# if end == self.prev_end: -# return -# -# with temp_enable_text_widget(self.sidebar_text): -# # if end > self.prev_end: -# # new_text = '\n'.join(itertools.chain( -# # [''], -# # itertools.repeat('...', end - self.prev_end), -# # )) -# # self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') -# # elif self.prev_end > self.editwin.getlineno("iomark"): -# # self.sidebar_text.delete(f'{end+1}.0 -1c', 'end -1c') -# # else: -# import sys -# for i in range(1, self.editwin.getlineno('end')): -# print(i, self.text.tag_names(f'{i}.0'), file=sys.stderr) -# new_text = '\n'.join(itertools.chain( -# # [''], -# ('>>>' if 'console' in self.text.tag_names(f'{line}.0') else '---' -# # for line in range(self.prev_end + 1, end + 1)), -# for line in range(1, end + 1)), -# )) -# self.sidebar_text.delete('1.0', 'end-1c') -# self.sidebar_text.insert(f'end -1c', new_text, 'linenumber') -# -# self.prev_end = end - - class WrappedLineHeightChangeDelegator(Delegator): def __init__(self, callback): """ From 1fc4bcbc998fef39163f7a1945ec347b882c45f6 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 25 Aug 2019 08:47:01 +0300 Subject: [PATCH 16/62] update shell sidebar font upon font config changes --- Lib/idlelib/pyshell.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 82d4b07db65ea2..b04ebceb5d08b9 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -955,6 +955,12 @@ def get_tag_colors(self): }) return tag_colors + def ResetFont(self): + # Update the sidebar widget, since its width affects + # the width of the text widget. + self.shell_sidebar.update_font() + super().ResetFont() + def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) From 6f1c9758a3029ab080d76667626d2f4001163050 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 13:01:54 +0300 Subject: [PATCH 17/62] update sidebar text colors upon highlight config changes --- Lib/idlelib/pyshell.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index b04ebceb5d08b9..a607e362ea66e3 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -879,6 +879,8 @@ def __init__(self, flist=None): root.withdraw() flist = PyShellFileList(root) + self.shell_sidebar = None # initialized below + OutputWindow.__init__(self, flist, None, None) self.usetabs = True @@ -956,10 +958,16 @@ def get_tag_colors(self): return tag_colors def ResetFont(self): + super().ResetFont() # Update the sidebar widget, since its width affects # the width of the text widget. self.shell_sidebar.update_font() - super().ResetFont() + + def update_colors(self): + super().update_colors() + # During __init__, update_colors() is called before the sidebar is created. + if self.shell_sidebar is not None: + self.shell_sidebar.update_colors() def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) From 083fedeea74774a1b3422b0824125eb7ac8f9d4f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 13:41:55 +0300 Subject: [PATCH 18/62] use the prompt ("console") foreground color for the shell sidebar text The background color still uses that configured for line numbers. --- Lib/idlelib/sidebar.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 0504c80819494d..430a70c60a41cc 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -463,9 +463,10 @@ def _update_font(self, font): def update_colors(self): """Update the sidebar text colors, usually after config changes.""" - colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') - self._update_colors(foreground=colors['foreground'], - background=colors['background']) + linenumbers_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'linenumber') + prompt_colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'console') + self._update_colors(foreground=prompt_colors['foreground'], + background=linenumbers_colors['background']) def _update_colors(self, foreground, background): self.colors = (foreground, background) From 4ad72a1cd94e9d6f985cfb5f99124cebfb17d35e Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 13:58:54 +0300 Subject: [PATCH 19/62] show continuation prompts ("...") for multi-line history recall The fix needed was for History to properly set the "stdin" tag on text it inserts. --- Lib/idlelib/history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/history.py b/Lib/idlelib/history.py index ad44a96a9de2c0..7ce09253eff5c9 100644 --- a/Lib/idlelib/history.py +++ b/Lib/idlelib/history.py @@ -74,13 +74,13 @@ def fetch(self, reverse): else: if self.text.get("iomark", "end-1c") != prefix: self.text.delete("iomark", "end-1c") - self.text.insert("iomark", prefix) + self.text.insert("iomark", prefix, "stdin") pointer = prefix = None break item = self.history[pointer] if item[:nprefix] == prefix and len(item) > nprefix: self.text.delete("iomark", "end-1c") - self.text.insert("iomark", item) + self.text.insert("iomark", item, "stdin") break self.text.see("insert") self.text.tag_remove("sel", "1.0", "end") From 009e08d0493dd56657dc091a080aa19c2c3386f4 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 14:38:06 +0300 Subject: [PATCH 20/62] show continuation prompts ("...") after multi-line replace in the shell --- Lib/idlelib/pyshell.py | 5 +++++ Lib/idlelib/replace.py | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index a607e362ea66e3..aa6000036982fc 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -54,6 +54,7 @@ from idlelib.editor import EditorWindow, fixwordbreaks from idlelib.filelist import FileList from idlelib.outwin import OutputWindow +from idlelib import replace from idlelib import rpc from idlelib.run import idle_formatwarning, StdInputFile, StdOutputFile from idlelib.undo import UndoDelegator @@ -969,6 +970,10 @@ def update_colors(self): if self.shell_sidebar is not None: self.shell_sidebar.update_colors() + def replace_event(self, event): + replace.replace(self.text, insert_tags="stdin") + return "break" + def get_standard_extension_names(self): return idleConf.GetExtensions(shell_only=True) diff --git a/Lib/idlelib/replace.py b/Lib/idlelib/replace.py index 6be034af9626b3..2f9ca231a05e49 100644 --- a/Lib/idlelib/replace.py +++ b/Lib/idlelib/replace.py @@ -11,7 +11,7 @@ from idlelib import searchengine -def replace(text): +def replace(text, insert_tags=None): """Create or reuse a singleton ReplaceDialog instance. The singleton dialog saves user entries and preferences @@ -25,7 +25,7 @@ def replace(text): if not hasattr(engine, "_replacedialog"): engine._replacedialog = ReplaceDialog(root, engine) dialog = engine._replacedialog - dialog.open(text) + dialog.open(text, insert_tags=insert_tags) class ReplaceDialog(SearchDialogBase): @@ -49,8 +49,9 @@ def __init__(self, root, engine): """ super().__init__(root, engine) self.replvar = StringVar(root) + self.insert_tags = None - def open(self, text): + def open(self, text, insert_tags=None): """Make dialog visible on top of others and ready to use. Also, highlight the currently selected text and set the @@ -72,6 +73,7 @@ def open(self, text): last = last or first self.show_hit(first, last) self.ok = True + self.insert_tags = insert_tags def create_entries(self): "Create base and additional label and text entry widgets." @@ -177,7 +179,7 @@ def replace_all(self, event=None): if first != last: text.delete(first, last) if new: - text.insert(first, new) + text.insert(first, new, self.insert_tags) col = i + len(new) ok = False text.undo_block_stop() @@ -231,7 +233,7 @@ def do_replace(self): if m.group(): text.delete(first, last) if new: - text.insert(first, new) + text.insert(first, new, self.insert_tags) text.undo_block_stop() self.show_hit(first, text.index("insert")) self.ok = False @@ -264,6 +266,7 @@ def close(self, event=None): "Close the dialog and remove hit tags." SearchDialogBase.close(self, event) self.text.tag_remove("hit", "1.0", "end") + self.insert_tags = None def _replace_dialog(parent): # htest # From e524cceec38c60afd79c9d34a97f466c0571a555 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 15:21:29 +0300 Subject: [PATCH 21/62] show continuation prompts ("...") after multi-line undo/redo in the shell --- Lib/idlelib/editor.py | 17 +++++++++-------- Lib/idlelib/pyshell.py | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 035d716c1eef4e..1ecafbb05fa870 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -68,6 +68,7 @@ class EditorWindow(object): allow_code_context = True allow_line_numbers = True + user_input_insert_tags = None def __init__(self, flist=None, filename=None, key=None, root=None): # Delay import: runscript imports pyshell imports EditorWindow. @@ -1331,7 +1332,7 @@ def smart_backspace_event(self, event): text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: - text.insert("insert", ' ' * (want - have)) + text.insert("insert", ' ' * (want - have), self.user_input_insert_tags) text.undo_block_stop() return "break" @@ -1364,7 +1365,7 @@ def smart_indent_event(self, event): effective = len(prefix.expandtabs(self.tabwidth)) n = self.indentwidth pad = ' ' * (n - effective % n) - text.insert("insert", pad) + text.insert("insert", pad, self.user_input_insert_tags) text.see("insert") return "break" finally: @@ -1395,7 +1396,7 @@ def newline_and_indent_event(self, event): if i == n: # The cursor is in or at leading indentation in a continuation # line; just inject an empty line at the start. - text.insert("insert linestart", '\n') + text.insert("insert linestart", '\n', self.user_input_insert_tags) return "break" indent = line[:i] @@ -1412,7 +1413,7 @@ def newline_and_indent_event(self, event): text.delete("insert") # Insert new line. - text.insert("insert", '\n') + text.insert("insert", '\n', self.user_input_insert_tags) # Adjust indentation for continuations and block open/close. # First need to find the last statement. @@ -1448,7 +1449,7 @@ def newline_and_indent_event(self, event): elif c == pyparse.C_STRING_NEXT_LINES: # Inside a string which started before this line; # just mimic the current indent. - text.insert("insert", indent) + text.insert("insert", indent, self.user_input_insert_tags) elif c == pyparse.C_BRACKET: # Line up with the first (if any) element of the # last open bracket structure; else indent one @@ -1462,7 +1463,7 @@ def newline_and_indent_event(self, event): # beyond leftmost =; else to beyond first chunk of # non-whitespace on initial line. if y.get_num_lines_in_stmt() > 1: - text.insert("insert", indent) + text.insert("insert", indent, self.user_input_insert_tags) else: self.reindent_to(y.compute_backslash_indent()) else: @@ -1473,7 +1474,7 @@ def newline_and_indent_event(self, event): # indentation of initial line of closest preceding # interesting statement. indent = y.get_base_indent_string() - text.insert("insert", indent) + text.insert("insert", indent, self.user_input_insert_tags) if y.is_block_opener(): self.smart_indent_event(event) elif indent and y.is_block_closer(): @@ -1520,7 +1521,7 @@ def reindent_to(self, column): if text.compare("insert linestart", "!=", "insert"): text.delete("insert linestart", "insert") if column: - text.insert("insert", self._make_blanks(column)) + text.insert("insert", self._make_blanks(column), self.user_input_insert_tags) text.undo_block_stop() # Guess indentwidth from text content. diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index aa6000036982fc..d00887075bf91e 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -863,6 +863,7 @@ class PyShell(OutputWindow): ] allow_line_numbers = False + user_input_insert_tags = "stdin" # New classes from idlelib.history import History @@ -1284,7 +1285,7 @@ def recall(self, s, event): if prefix.rstrip().endswith(':'): self.newline_and_indent_event(event) prefix = self.text.get("insert linestart", "insert") - self.text.insert("insert", lines[0].strip(), "stdin") + self.text.insert("insert", lines[0].strip(), self.user_input_insert_tags) if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) @@ -1292,8 +1293,7 @@ def recall(self, s, event): if line.startswith(orig_base_indent): # replace orig base indentation with new indentation line = new_base_indent + line[len(orig_base_indent):] - self.text.insert('insert', '\n'+line.rstrip(), "stdin") - self.shell_sidebar.update_sidebar() + self.text.insert('insert', '\n'+line.rstrip(), self.user_input_insert_tags) finally: self.text.see("insert") self.text.undo_block_stop() From 3ea29865162ed5aacb82ee189c66c8ebbf3f8b3f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 18:43:57 +0300 Subject: [PATCH 22/62] don't show continuation prompts ("...") for print() outputs --- Lib/idlelib/pyshell.py | 18 +++++------------- Lib/idlelib/sidebar.py | 7 +++---- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index d00887075bf91e..9019bbda78c157 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1264,7 +1264,6 @@ def enter_callback(self, event): self.text.see("insert") else: self.newline_and_indent_event(event) - self.text.tag_add("stdin", "iomark", "end-1c") self.text.update_idletasks() if self.reading: self.top.quit() # Break out of recursive mainloop() @@ -1299,22 +1298,15 @@ def recall(self, s, event): self.text.undo_block_stop() def runit(self): + index_before = self.text.index("end-2c") line = self.text.get("iomark", "end-1c") # Strip off last newline and surrounding whitespace. # (To allow you to hit return twice to end a statement.) - i = len(line) - while i > 0 and line[i-1] in " \t": - i = i-1 - if i > 0 and line[i-1] == "\n": - i = i-1 - while i > 0 and line[i-1] in " \t": - i = i-1 - line = line[:i] + line = re.sub(r"[ \t]*\n?[ \t]*$", "", line) input_is_incomplete = self.interp.runsource(line) - # If the input is a complete statement, the sidebar updates will be - # handled elsewhere. - if input_is_incomplete: - self.shell_sidebar.update_sidebar() + if not input_is_incomplete: + if self.user_input_insert_tags: + self.text.tag_remove(self.user_input_insert_tags, index_before) def open_stack_viewer(self, event=None): if self.interp.rpcclt: diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 430a70c60a41cc..69b917aabe6ff1 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -429,11 +429,10 @@ def update_sidebar(self): if lineinfo is None: break y = lineinfo[1] - is_prompt = "console" in text_tagnames(f"{index} linestart -1c") - is_input = "stdin" in text_tagnames(f"{index} lineend -1c") + prev_newline_tagnames = text_tagnames(f"{index} linestart -1c") prompt = ( - '>>>' if is_prompt else - '...' if is_input else + '>>>' if "console" in prev_newline_tagnames else + '...' if "stdin" in prev_newline_tagnames else ' ' ) canvas.create_text(2, y, anchor=tk.NW, text=prompt, From 7f9182f7c15d52acc0721777e9e961f4efd61952 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 22:00:46 +0300 Subject: [PATCH 23/62] allow deleting ">>> " at the beginning of a line in shell windows --- Lib/idlelib/editor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 1ecafbb05fa870..2ea5967b19a927 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1322,8 +1322,6 @@ def smart_backspace_event(self, event): # Debug prompt is multilined.... ncharsdeleted = 0 while 1: - if chars == self.prompt_last_line: # '' unless PyShell - break chars = chars[:-1] ncharsdeleted = ncharsdeleted + 1 have = len(chars.expandtabs(tabwidth)) @@ -1402,7 +1400,7 @@ def newline_and_indent_event(self, event): # Strip whitespace before insert point unless it's in the prompt. i = 0 - while line and line[-1] in " \t" and line != self.prompt_last_line: + while line and line[-1] in " \t": line = line[:-1] i += 1 if i: From 10aaf1d96dd59f136e5447ca82dcf7e3ef3cbe5f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 22:43:42 +0300 Subject: [PATCH 24/62] update shell sidebar on shell output squeeze/unsqueeze --- Lib/idlelib/editor.py | 1 - Lib/idlelib/idle_test/test_squeezer.py | 32 +++++++++----------------- Lib/idlelib/pyshell.py | 12 ++++++++-- Lib/idlelib/squeezer.py | 6 +++-- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 2ea5967b19a927..4ccd0b8158db4c 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -60,7 +60,6 @@ class EditorWindow(object): from idlelib.sidebar import LineNumbers from idlelib.format import FormatParagraph, FormatRegion, Indents, Rstrip from idlelib.parenmatch import ParenMatch - from idlelib.squeezer import Squeezer from idlelib.zoomheight import ZoomHeight filesystemencoding = sys.getfilesystemencoding() # for file names diff --git a/Lib/idlelib/idle_test/test_squeezer.py b/Lib/idlelib/idle_test/test_squeezer.py index e3912f4bbbec89..697a7e3d6813b7 100644 --- a/Lib/idlelib/idle_test/test_squeezer.py +++ b/Lib/idlelib/idle_test/test_squeezer.py @@ -7,13 +7,12 @@ from test.support import requires from idlelib.config import idleConf +from idlelib.percolator import Percolator from idlelib.squeezer import count_lines_with_wrapping, ExpandingButton, \ Squeezer from idlelib import macosx from idlelib.textview import view_text from idlelib.tooltip import Hovertip -from idlelib.pyshell import PyShell - SENTINEL_VALUE = sentinel.SENTINEL_VALUE @@ -205,8 +204,8 @@ def test_auto_squeeze(self): self.assertEqual(text_widget.get('1.0', 'end'), '\n') self.assertEqual(len(squeezer.expandingbuttons), 1) - def test_squeeze_current_text_event(self): - """Test the squeeze_current_text event.""" + def test_squeeze_current_text(self): + """Test the squeeze_current_text method.""" # Squeezing text should work for both stdout and stderr. for tag_name in ["stdout", "stderr"]: editwin = self.make_mock_editor_window(with_text_widget=True) @@ -222,7 +221,7 @@ def test_squeeze_current_text_event(self): self.assertEqual(len(squeezer.expandingbuttons), 0) # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), '\n\n') self.assertEqual(len(squeezer.expandingbuttons), 1) @@ -230,11 +229,11 @@ def test_squeeze_current_text_event(self): # Test that expanding the squeezed text works and afterwards # the Text widget contains the original text. - squeezer.expandingbuttons[0].expand(event=Mock()) + squeezer.expandingbuttons[0].expand() self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') self.assertEqual(len(squeezer.expandingbuttons), 0) - def test_squeeze_current_text_event_no_allowed_tags(self): + def test_squeeze_current_text_no_allowed_tags(self): """Test that the event doesn't squeeze text without a relevant tag.""" editwin = self.make_mock_editor_window(with_text_widget=True) text_widget = editwin.text @@ -249,7 +248,7 @@ def test_squeeze_current_text_event_no_allowed_tags(self): self.assertEqual(len(squeezer.expandingbuttons), 0) # Test squeezing the current text. - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), 'SOME\nTEXT\n\n') self.assertEqual(len(squeezer.expandingbuttons), 0) @@ -264,13 +263,13 @@ def test_squeeze_text_before_existing_squeezed_text(self): # Prepare some text in the Text widget and squeeze it. text_widget.insert("1.0", "SOME\nTEXT\n", "stdout") text_widget.mark_set("insert", "1.0") - squeezer.squeeze_current_text_event(event=Mock()) + squeezer.squeeze_current_text() self.assertEqual(len(squeezer.expandingbuttons), 1) # Test squeezing the current text. text_widget.insert("1.0", "MORE\nSTUFF\n", "stdout") text_widget.mark_set("insert", "1.0") - retval = squeezer.squeeze_current_text_event(event=Mock()) + retval = squeezer.squeeze_current_text() self.assertEqual(retval, "break") self.assertEqual(text_widget.get('1.0', 'end'), '\n\n\n') self.assertEqual(len(squeezer.expandingbuttons), 2) @@ -311,6 +310,7 @@ def make_mock_squeezer(self): root = get_test_tk_root(self) squeezer = Mock() squeezer.editwin.text = Text(root) + squeezer.editwin.per = Percolator(squeezer.editwin.text) # Set default values for the configuration settings. squeezer.auto_squeeze_min_lines = 50 @@ -352,14 +352,9 @@ def test_expand(self): # Insert the button into the text widget # (this is normally done by the Squeezer class). - text_widget = expandingbutton.text + text_widget = squeezer.editwin.text text_widget.window_create("1.0", window=expandingbutton) - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - # trigger the expand event retval = expandingbutton.expand(event=Mock()) self.assertEqual(retval, None) @@ -390,11 +385,6 @@ def test_expand_dangerous_oupput(self): text_widget = expandingbutton.text text_widget.window_create("1.0", window=expandingbutton) - # Set base_text to the text widget, so that changes are actually - # made to it (by ExpandingButton) and we can inspect these - # changes afterwards. - expandingbutton.base_text = expandingbutton.text - # Patch the message box module to always return False. with patch('idlelib.squeezer.tkMessageBox') as mock_msgbox: mock_msgbox.askokcancel.return_value = False diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 9019bbda78c157..11c6a563c7cb03 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -840,6 +840,7 @@ def display_executing_dialog(self): class PyShell(OutputWindow): + from idlelib.squeezer import Squeezer shell_title = "Python " + python_version() + " Shell" @@ -905,9 +906,9 @@ def __init__(self, flist=None): if use_subprocess: text.bind("<>", self.view_restart_mark) text.bind("<>", self.restart_shell) - squeezer = self.Squeezer(self) + self.squeezer = self.Squeezer(self) text.bind("<>", - squeezer.squeeze_current_text_event) + self.squeeze_current_text_event) self.save_stdout = sys.stdout self.save_stderr = sys.stderr @@ -1389,6 +1390,13 @@ def rmenu_check_paste(self): return 'disabled' return super().rmenu_check_paste() + def squeeze_current_text_event(self, event=None): + self.squeezer.squeeze_current_text() + self.shell_sidebar.update_sidebar() + + def on_squeezed_expand(self, index, text, tags): + self.shell_sidebar.update_sidebar() + def fix_x11_paste(root): "Make paste replace selection on x11. See issue #5124." diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py index be1538a25fdedf..12ee5895b373ca 100644 --- a/Lib/idlelib/squeezer.py +++ b/Lib/idlelib/squeezer.py @@ -160,8 +160,10 @@ def expand(self, event=None): if not confirm: return "break" - self.base_text.insert(self.text.index(self), self.s, self.tags) + index = self.text.index(self) + self.base_text.insert(index, self.s, self.tags) self.base_text.delete(self) + self.editwin.on_squeezed_expand(index, self.s, self.tags) self.squeezer.expandingbuttons.remove(self) def copy(self, event=None): @@ -285,7 +287,7 @@ def count_lines(self, s): """ return count_lines_with_wrapping(s, self.editwin.width) - def squeeze_current_text_event(self, event): + def squeeze_current_text(self): """squeeze-current-text event handler Squeeze the block of text inside which contains the "insert" cursor. From 7c919018b56ec0fc49eaf890fa071ffd6e6ab52b Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 17 Sep 2020 22:48:31 +0300 Subject: [PATCH 25/62] remove failing editor window test that is no longer relevant --- Lib/idlelib/idle_test/test_editor.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/idlelib/idle_test/test_editor.py b/Lib/idlelib/idle_test/test_editor.py index 443dcf021679fc..8665d680c0118f 100644 --- a/Lib/idlelib/idle_test/test_editor.py +++ b/Lib/idlelib/idle_test/test_editor.py @@ -167,7 +167,6 @@ def test_indent_and_newline_event(self): '2.end'), ) - w.prompt_last_line = '' for test in tests: with self.subTest(label=test.label): insert(text, test.text) @@ -182,13 +181,6 @@ def test_indent_and_newline_event(self): # Deletes selected text before adding new line. eq(get('1.0', 'end'), ' def f1(self, a,\n \n return a + b\n') - # Preserves the whitespace in shell prompt. - w.prompt_last_line = '>>> ' - insert(text, '>>> \t\ta =') - text.mark_set('insert', '1.5') - nl(None) - eq(get('1.0', 'end'), '>>> \na =\n') - class RMenuTest(unittest.TestCase): From 50ca9c6b1e68f39ab6b83bae900c21d8d550ef19 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 18 Sep 2020 22:15:24 +0300 Subject: [PATCH 26/62] add several tests for the shell sidebar --- Lib/idlelib/idle_test/test_sidebar.py | 152 +++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 2974a9a7b09874..ed57b572aa20cb 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,7 @@ """Test sidebar, coverage 93%""" +from textwrap import dedent +from time import sleep + import idlelib.sidebar from itertools import chain import unittest @@ -7,7 +10,12 @@ import tkinter as tk from idlelib.delegator import Delegator +from idlelib.editor import fixwordbreaks +from idlelib import macosx from idlelib.percolator import Percolator +import idlelib.pyshell +from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList +from idlelib.run import fix_scaling class Dummy_editwin: @@ -353,7 +361,7 @@ def assert_colors_are_equal(colors): ln.hide_sidebar() self.highlight_cfg = test_colors - # Nothing breaks with inactive code context. + # Nothing breaks with inactive line numbers. ln.update_colors() # Show line numbers, previous colors change is immediately effective. @@ -370,5 +378,147 @@ def assert_colors_are_equal(colors): assert_colors_are_equal(orig_colors) +class TestShellSidebar(unittest.TestCase): + root: tk.Tk = None + shell: PyShell = None + + @classmethod + def setUpClass(cls): + requires('gui') + + idlelib.pyshell.use_subprocess = True + + cls.root = root = tk.Tk() + + fix_scaling(root) + fixwordbreaks(root) + fix_x11_paste(root) + + cls.flist = flist = PyShellFileList(root) + macosx.setupApp(root, flist) + root.update_idletasks() + + @classmethod + def tearDownClass(cls): + cls.root.update_idletasks() + cls.root.destroy() + + def setUp(self): + self.shell = self.flist.open_shell() + self.root.update_idletasks() + + def tearDown(self): + self.shell.close() + + def get_sidebar_lines(self): + canvas = self.shell.shell_sidebar.canvas + sleep(0.1) + self.root.update() + texts = list(canvas.find(tk.ALL)) + texts.sort(key=lambda text: canvas.bbox(text)[1]) + return [canvas.itemcget(text, 'text') for text in texts] + + def assertSidebarLinesEndWith(self, expected_lines): + self.assertEqual( + self.get_sidebar_lines()[-len(expected_lines):], + expected_lines, + ) + + def getShellLineYCoords(self): + text = self.shell.text + y_coords = [] + index = text.index("@0,0") + while True: + lineinfo = text.dlineinfo(index) + if lineinfo is None: + break + y_coords.append(lineinfo[1]) + index = text.index(f"{index} +1line") + return y_coords + + def getSidebarLineYCoords(self): + canvas = self.shell.shell_sidebar.canvas + texts = list(canvas.find(tk.ALL)) + texts.sort(key=lambda text: canvas.bbox(text)[1]) + return [canvas.bbox(text)[1] for text in texts] + + def assertSidebarLinesSynced(self): + self.assertEqual( + self.getSidebarLineYCoords(), + self.getShellLineYCoords(), + ) + + def doInput(self, input): + root = self.root + shell = self.shell + text = shell.text + for line_index, line in enumerate(input.split('\n')): + if line_index > 0: + text.event_generate('') + text.event_generate('') + sleep(0.1) + root.update() + for char in line: + text.event_generate(char) + + def testInitialState(self): + sidebar_lines = self.get_sidebar_lines() + self.assertEqual( + sidebar_lines, + [' '] * (len(sidebar_lines) - 1) + ['>>>'], + ) + + def testSingleEmptyInput(self): + self.doInput('\n') + self.assertSidebarLinesEndWith(['>>>', '>>>']) + + def testSingleLineCommand(self): + self.doInput('1\n') + self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + + def testMultiLineCommand(self): + # note: block statements are not indented because IDLE auto-indents + self.doInput(dedent('''\ + if True: + print(1) + + ''')) + self.assertSidebarLinesEndWith([ + '>>>', + '...', + '...', + '...', + ' ', + '>>>', + ]) + + def testSingleLongLineWraps(self): + self.doInput('1' * 200 + '\n') + self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + self.assertSidebarLinesSynced() + + def testSqueezeSingleLineCommand(self): + root = self.root + shell = self.shell + text = shell.text + + self.doInput('1\n') + self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + + line = int(shell.text.index('insert -1line').split('.')[0]) + text.mark_set('insert', f"{line}.0") + text.event_generate('<>') + sleep(0.1) + root.update() + self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + self.assertSidebarLinesSynced() + + shell.squeezer.expandingbuttons[0].expand() + sleep(0.1) + root.update() + self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + self.assertSidebarLinesSynced() + + if __name__ == '__main__': unittest.main(verbosity=2) From 23c218f93c865c648fec0aaf4bbb348fb7d6682c Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Fri, 18 Sep 2020 23:41:56 +0300 Subject: [PATCH 27/62] reduce shell sidebar test run time by reusing the PyShell --- Lib/idlelib/idle_test/test_sidebar.py | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index ed57b572aa20cb..f2fbdeaec194b7 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -400,15 +400,32 @@ def setUpClass(cls): @classmethod def tearDownClass(cls): + if cls.shell is not None: + cls.shell.executing = False + cls.shell.close() + cls.shell = None + cls.flist = None cls.root.update_idletasks() cls.root.destroy() + cls.root = None - def setUp(self): - self.shell = self.flist.open_shell() - self.root.update_idletasks() + @classmethod + def init_shell(cls): + cls.shell = cls.flist.open_shell() + cls.root.update() + cls.n_preface_lines = int(cls.shell.text.index('end-1c').split('.')[0]) - 1 - def tearDown(self): - self.shell.close() + @classmethod + def reset_shell(cls): + cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c') + cls.shell.shell_sidebar.update_sidebar() + cls.root.update() + + def setUp(self): + if self.shell is None: + self.init_shell() + else: + self.reset_shell() def get_sidebar_lines(self): canvas = self.shell.shell_sidebar.canvas @@ -449,17 +466,16 @@ def assertSidebarLinesSynced(self): ) def doInput(self, input): - root = self.root shell = self.shell text = shell.text for line_index, line in enumerate(input.split('\n')): if line_index > 0: text.event_generate('') text.event_generate('') - sleep(0.1) - root.update() for char in line: text.event_generate(char) + sleep(0.1) + self.root.update() def testInitialState(self): sidebar_lines = self.get_sidebar_lines() @@ -467,6 +483,7 @@ def testInitialState(self): sidebar_lines, [' '] * (len(sidebar_lines) - 1) + ['>>>'], ) + self.assertSidebarLinesSynced() def testSingleEmptyInput(self): self.doInput('\n') From 6cef0a934e6012b08848eea8ca01c40bd4d9b875 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 11 Oct 2020 22:57:43 +0300 Subject: [PATCH 28/62] revert removal of the "hit" tag from Colorizer.tag_defs --- Lib/idlelib/colorizer.py | 5 +++++ Lib/idlelib/editor.py | 11 ----------- Lib/idlelib/pyshell.py | 24 +++++++++++------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py index e28fbbe9200257..be81e8a48cbbc3 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/colorizer.py @@ -123,6 +123,11 @@ def LoadTagDefs(self): "SYNC": {'background': None, 'foreground': None}, "TODO": {'background': None, 'foreground': None}, "ERROR": idleConf.GetHighlight(theme, "error"), + # "hit" is used by ReplaceDialog to mark matches. It shouldn't be changed by Colorizer, but + # that currently isn't technically possible. This should be moved elsewhere in the future + # when fixing the "hit" tag's visibility, or when the replace dialog is replaced with a + # non-modal alternative. + "hit": idleConf.GetHighlight(theme, "hit"), } if DEBUG: print('tagdefs',self.tagdefs) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 4ccd0b8158db4c..187c67de7f087e 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -807,10 +807,6 @@ def update_colors(self): """ EditorWindow.color_config(self.text) - tag_colors = self.get_tag_colors() - for tag, tag_colors_config in tag_colors.items(): - self.text.tag_configure(tag, **tag_colors_config) - self.ResetColorizer() if self.code_context is not None: @@ -819,13 +815,6 @@ def update_colors(self): if self.line_numbers is not None: self.line_numbers.update_colors() - def get_tag_colors(self): - theme = idleConf.CurrentTheme() - return { - # The following is used by ReplaceDialog: - "hit": idleConf.GetHighlight(theme, "hit"), - } - IDENTCHARS = string.ascii_letters + string.digits + "_" def colorize_syntax_error(self, text, pos): diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 11c6a563c7cb03..3b589278b0cbc2 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -948,18 +948,6 @@ def __init__(self, flist=None): self.text.insert = self.per.top.insert self.per.insertfilter(UserInputTaggingDelegator()) - def get_tag_colors(self): - tag_colors = super().get_tag_colors() - - theme = idleConf.CurrentTheme() - tag_colors.update({ - "stdin": {'background': None, 'foreground': None}, - "stdout": idleConf.GetHighlight(theme, "stdout"), - "stderr": idleConf.GetHighlight(theme, "stderr"), - "console": idleConf.GetHighlight(theme, "console"), - }) - return tag_colors - def ResetFont(self): super().ResetFont() # Update the sidebar widget, since its width affects @@ -968,10 +956,20 @@ def ResetFont(self): def update_colors(self): super().update_colors() + + theme = idleConf.CurrentTheme() + tag_colors = { + "stdin": {'background': None, 'foreground': None}, + "stdout": idleConf.GetHighlight(theme, "stdout"), + "stderr": idleConf.GetHighlight(theme, "stderr"), + "console": idleConf.GetHighlight(theme, "console"), + } + for tag, tag_colors_config in tag_colors.items(): + self.text.tag_configure(tag, **tag_colors_config) + # During __init__, update_colors() is called before the sidebar is created. if self.shell_sidebar is not None: self.shell_sidebar.update_colors() - def replace_event(self, event): replace.replace(self.text, insert_tags="stdin") return "break" From a01813543f3d350d7d0f94e054008053206b22e0 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 11 Oct 2020 23:15:54 +0300 Subject: [PATCH 29/62] hide unnecessary extra "root" Tk window in tests --- Lib/idlelib/idle_test/test_sidebar.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index f2fbdeaec194b7..000d7491aa75ed 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -389,6 +389,7 @@ def setUpClass(cls): idlelib.pyshell.use_subprocess = True cls.root = root = tk.Tk() + root.withdraw() fix_scaling(root) fixwordbreaks(root) From 525fe440dcc59c295f3875024716a55d2050c808 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 11 Oct 2020 23:21:19 +0300 Subject: [PATCH 30/62] fix test method names --- Lib/idlelib/idle_test/test_sidebar.py | 58 +++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 000d7491aa75ed..c52b675b1a6cfa 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -436,13 +436,13 @@ def get_sidebar_lines(self): texts.sort(key=lambda text: canvas.bbox(text)[1]) return [canvas.itemcget(text, 'text') for text in texts] - def assertSidebarLinesEndWith(self, expected_lines): + def assert_sidebar_lines_end_with(self, expected_lines): self.assertEqual( self.get_sidebar_lines()[-len(expected_lines):], expected_lines, ) - def getShellLineYCoords(self): + def get_shell_line_y_coords(self): text = self.shell.text y_coords = [] index = text.index("@0,0") @@ -454,19 +454,19 @@ def getShellLineYCoords(self): index = text.index(f"{index} +1line") return y_coords - def getSidebarLineYCoords(self): + def get_sidebar_line_y_coords(self): canvas = self.shell.shell_sidebar.canvas texts = list(canvas.find(tk.ALL)) texts.sort(key=lambda text: canvas.bbox(text)[1]) return [canvas.bbox(text)[1] for text in texts] - def assertSidebarLinesSynced(self): + def assert_sidebar_lines_synced(self): self.assertEqual( - self.getSidebarLineYCoords(), - self.getShellLineYCoords(), + self.get_sidebar_line_y_coords(), + self.get_shell_line_y_coords(), ) - def doInput(self, input): + def do_input(self, input): shell = self.shell text = shell.text for line_index, line in enumerate(input.split('\n')): @@ -478,30 +478,30 @@ def doInput(self, input): sleep(0.1) self.root.update() - def testInitialState(self): + def test_initial_state(self): sidebar_lines = self.get_sidebar_lines() self.assertEqual( sidebar_lines, [' '] * (len(sidebar_lines) - 1) + ['>>>'], ) - self.assertSidebarLinesSynced() + self.assert_sidebar_lines_synced() - def testSingleEmptyInput(self): - self.doInput('\n') - self.assertSidebarLinesEndWith(['>>>', '>>>']) + def test_single_empty_input(self): + self.do_input('\n') + self.assert_sidebar_lines_end_with(['>>>', '>>>']) - def testSingleLineCommand(self): - self.doInput('1\n') - self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + def test_single_line_command(self): + self.do_input('1\n') + self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) - def testMultiLineCommand(self): + def test_multi_line_command(self): # note: block statements are not indented because IDLE auto-indents - self.doInput(dedent('''\ + self.do_input(dedent('''\ if True: print(1) ''')) - self.assertSidebarLinesEndWith([ + self.assert_sidebar_lines_end_with([ '>>>', '...', '...', @@ -510,32 +510,32 @@ def testMultiLineCommand(self): '>>>', ]) - def testSingleLongLineWraps(self): - self.doInput('1' * 200 + '\n') - self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) - self.assertSidebarLinesSynced() + def test_single_long_line_wraps(self): + self.do_input('1' * 200 + '\n') + self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_synced() - def testSqueezeSingleLineCommand(self): + def test_squeeze_single_line_command(self): root = self.root shell = self.shell text = shell.text - self.doInput('1\n') - self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) + self.do_input('1\n') + self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) line = int(shell.text.index('insert -1line').split('.')[0]) text.mark_set('insert', f"{line}.0") text.event_generate('<>') sleep(0.1) root.update() - self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) - self.assertSidebarLinesSynced() + self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_synced() shell.squeezer.expandingbuttons[0].expand() sleep(0.1) root.update() - self.assertSidebarLinesEndWith(['>>>', ' ', '>>>']) - self.assertSidebarLinesSynced() + self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_synced() if __name__ == '__main__': From 7b8913d4506699e3cf18f4f033372e2cf005b786 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 11 Oct 2020 23:23:34 +0300 Subject: [PATCH 31/62] fix test comments according to pep-8 style --- Lib/idlelib/idle_test/test_sidebar.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index c52b675b1a6cfa..6af010a24a331f 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -162,7 +162,7 @@ def test_delete(self): self.assert_sidebar_n_lines(3) self.assert_state_disabled() - # Note: deleting up to "2.end" doesn't delete the final newline. + # Deleting up to "2.end" doesn't delete the final newline. self.text.delete('2.0', '2.end') self.assert_text_equals('fbarfoo\n\n\n') self.assert_sidebar_n_lines(3) @@ -173,7 +173,7 @@ def test_delete(self): self.assert_sidebar_n_lines(1) self.assert_state_disabled() - # Note: Text widgets always keep a single '\n' character at the end. + # Text widgets always keep a single '\n' character at the end. self.text.delete('1.0', 'end') self.assert_text_equals('\n') self.assert_sidebar_n_lines(1) @@ -242,7 +242,7 @@ def get_width(): self.assert_sidebar_n_lines(4) self.assertEqual(get_width(), 1) - # Note: Text widgets always keep a single '\n' character at the end. + # Text widgets always keep a single '\n' character at the end. self.text.delete('1.0', 'end -1c') self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) @@ -495,7 +495,7 @@ def test_single_line_command(self): self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) def test_multi_line_command(self): - # note: block statements are not indented because IDLE auto-indents + # Block statements are not indented because IDLE auto-indents. self.do_input(dedent('''\ if True: print(1) From 8c8d0c0ab5878af3cc1b71530e0a2e83e9dffe8e Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 11 Oct 2020 23:26:22 +0300 Subject: [PATCH 32/62] rename sidebar test class according to convention Co-authored-by: Terry Jan Reedy --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 6af010a24a331f..9c1179e4d25b64 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -378,7 +378,7 @@ def assert_colors_are_equal(colors): assert_colors_are_equal(orig_colors) -class TestShellSidebar(unittest.TestCase): +class ShellSidebarTest(unittest.TestCase): root: tk.Tk = None shell: PyShell = None From 7ecc84374a91b7f752992a94a023c51fdecf55eb Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 10:33:30 +0300 Subject: [PATCH 33/62] re-word doc-strings for BaseSideBar and EndLineDelegator --- Lib/idlelib/sidebar.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 69b917aabe6ff1..103aa4489b7874 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -64,9 +64,7 @@ def temp_enable_text_widget(text): class BaseSideBar: - """ - The base class for extensions which require a sidebar. - """ + """A base class for sidebars using Text.""" def __init__(self, editwin): self.editwin = editwin self.parent = editwin.text_frame @@ -142,14 +140,11 @@ def redirect_mousewheel_event(self, event): class EndLineDelegator(Delegator): - """Generate callbacks with the current end line number after - insert or delete operations""" + """Generate callbacks with the current end line number. + + The provided callback is called after every insert and delete. + """ def __init__(self, changed_callback): - """ - changed_callback - Callable, will be called after insert - or delete operations with the current - end line number. - """ Delegator.__init__(self) self.changed_callback = changed_callback From bedb1cb4511a0c342782ed6988e24124b590335f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 10:36:57 +0300 Subject: [PATCH 34/62] re-word doc-strings for get_lineno and get_end_linenumber --- Lib/idlelib/sidebar.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 103aa4489b7874..fb4b3b9c548afa 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -12,12 +12,12 @@ def get_lineno(text, index): - """Utility to get the line number of an index in a Tk text widget.""" + """Return the line number of an index in a Tk text widget.""" return int(float(text.index(index))) def get_end_linenumber(text): - """Utility to get the last line's number in a Tk text widget.""" + """Return the number of the last line in a Tk text widget.""" return get_lineno(text, 'end-1c') From eee045381231e8a3a5c4886f2a159440ad2e51ba Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 13:36:08 +0300 Subject: [PATCH 35/62] optimize tests and make them more robust --- Lib/idlelib/idle_test/test_sidebar.py | 56 +++++++++++++++++++++------ 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 9c1179e4d25b64..7f3030b7c95968 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -2,7 +2,6 @@ from textwrap import dedent from time import sleep -import idlelib.sidebar from itertools import chain import unittest import unittest.mock @@ -16,6 +15,8 @@ import idlelib.pyshell from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList from idlelib.run import fix_scaling +import idlelib.sidebar +from idlelib.sidebar import get_lineno class Dummy_editwin: @@ -378,6 +379,12 @@ def assert_colors_are_equal(colors): assert_colors_are_equal(orig_colors) +def test_coroutine(test_method): + def new_method(self): + return self.run_test_coroutine(test_method(self)) + return new_method + + class ShellSidebarTest(unittest.TestCase): root: tk.Tk = None shell: PyShell = None @@ -413,8 +420,9 @@ def tearDownClass(cls): @classmethod def init_shell(cls): cls.shell = cls.flist.open_shell() + cls.shell.pollinterval = 10 cls.root.update() - cls.n_preface_lines = int(cls.shell.text.index('end-1c').split('.')[0]) - 1 + cls.n_preface_lines = get_lineno(cls.shell.text, 'end-1c') - 1 @classmethod def reset_shell(cls): @@ -430,13 +438,12 @@ def setUp(self): def get_sidebar_lines(self): canvas = self.shell.shell_sidebar.canvas - sleep(0.1) - self.root.update() texts = list(canvas.find(tk.ALL)) texts.sort(key=lambda text: canvas.bbox(text)[1]) return [canvas.itemcget(text, 'text') for text in texts] def assert_sidebar_lines_end_with(self, expected_lines): + self.shell.shell_sidebar.update_sidebar() self.assertEqual( self.get_sidebar_lines()[-len(expected_lines):], expected_lines, @@ -475,8 +482,28 @@ def do_input(self, input): text.event_generate('') for char in line: text.event_generate(char) - sleep(0.1) - self.root.update() + + def run_test_coroutine(self, coroutine): + root = self.root + # Exceptions raised by self.assert...() need to be raised outside of + # the after callback in order for the test harness to capture them. + exception = None + def after_callback(): + nonlocal exception + try: + interval_multiplier = next(coroutine) or 1 + except StopIteration: + root.quit() + except Exception as exc: + exception = exc + root.quit() + else: + root.after(100 * interval_multiplier, after_callback) + root.after(0, after_callback) + root.mainloop() + + if exception: + raise exception def test_initial_state(self): sidebar_lines = self.get_sidebar_lines() @@ -486,14 +513,19 @@ def test_initial_state(self): ) self.assert_sidebar_lines_synced() + @test_coroutine def test_single_empty_input(self): self.do_input('\n') + yield 0 self.assert_sidebar_lines_end_with(['>>>', '>>>']) + @test_coroutine def test_single_line_command(self): self.do_input('1\n') + yield 2 self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + @test_coroutine def test_multi_line_command(self): # Block statements are not indented because IDLE auto-indents. self.do_input(dedent('''\ @@ -501,6 +533,7 @@ def test_multi_line_command(self): print(1) ''')) + yield 2 self.assert_sidebar_lines_end_with([ '>>>', '...', @@ -510,30 +543,31 @@ def test_multi_line_command(self): '>>>', ]) + @test_coroutine def test_single_long_line_wraps(self): self.do_input('1' * 200 + '\n') + yield 2 self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() + @test_coroutine def test_squeeze_single_line_command(self): - root = self.root shell = self.shell text = shell.text self.do_input('1\n') + yield 2 self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) line = int(shell.text.index('insert -1line').split('.')[0]) text.mark_set('insert', f"{line}.0") text.event_generate('<>') - sleep(0.1) - root.update() + yield 0 self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() shell.squeezer.expandingbuttons[0].expand() - sleep(0.1) - root.update() + yield 0 self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() From 7afcb57faaa21f6e0f474ff31c512350026ef615 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 14:22:02 +0300 Subject: [PATCH 36/62] handle mouse buttons except for button 1 (left-click) --- Lib/idlelib/pyshell.py | 1 + Lib/idlelib/sidebar.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 3b589278b0cbc2..8aa0aefc29fec2 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -970,6 +970,7 @@ def update_colors(self): # During __init__, update_colors() is called before the sidebar is created. if self.shell_sidebar is not None: self.shell_sidebar.update_colors() + def replace_event(self, event): replace.replace(self.text, insert_tags="stdin") return "break" diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index fb4b3b9c548afa..35d58c78434869 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -494,6 +494,30 @@ def bind_events(self): # the line numbers. self.canvas.bind('', self.redirect_mousewheel_event) + # Redirect mouse button events to the main editor text widget, + # except for the left mouse button (1). + # + # Note: X-11 sends Button-4 and Button-5 events for the scroll wheel. + def bind_mouse_event(event_name, target_event_name): + handler = functools.partial(self.redirect_mousebutton_event, + event_name=target_event_name) + self.canvas.bind(event_name, handler) + + for button in [2, 3, 4, 5]: + for event_name in (f'', + f'', + f'', + ): + bind_mouse_event(event_name, target_event_name=event_name) + + # Convert double- and triple-click events to normal click events, + # since event_generate() doesn't allow generating such events. + for event_name in (f'', + f'', + ): + bind_mouse_event(event_name, + target_event_name=f'') + def _linenumbers_drag_scrolling(parent): # htest # from idlelib.idle_test.test_sidebar import Dummy_editwin From 8a7dd607216826b275af204e2f6fb62b9151363a Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 16:55:19 +0300 Subject: [PATCH 37/62] add tests for text font and color configs --- Lib/idlelib/idle_test/test_sidebar.py | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 7f3030b7c95968..f6dc077815c84a 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -571,6 +571,64 @@ def test_squeeze_single_line_command(self): self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() + def test_font(self): + sidebar = self.shell.shell_sidebar + + test_font = 'TkTextFont' + + def mock_idleconf_GetFont(root, configType, section): + return test_font + GetFont_patcher = unittest.mock.patch.object( + idlelib.sidebar.idleConf, 'GetFont', mock_idleconf_GetFont) + GetFont_patcher.start() + def cleanup(): + GetFont_patcher.stop() + sidebar.update_font() + self.addCleanup(cleanup) + + def get_sidebar_font(): + canvas = sidebar.canvas + texts = list(canvas.find(tk.ALL)) + fonts = {canvas.itemcget(text, 'font') for text in texts} + self.assertEqual(len(fonts), 1) + return next(iter(fonts)) + + self.assertNotEqual(get_sidebar_font(), test_font) + sidebar.update_font() + self.assertEqual(get_sidebar_font(), test_font) + + def test_highlight_colors(self): + sidebar = self.shell.shell_sidebar + + test_colors = {"background": '#abcdef', "foreground": '#123456'} + + orig_idleConf_GetHighlight = idlelib.sidebar.idleConf.GetHighlight + def mock_idleconf_GetHighlight(theme, element): + if element in ['linenumber', 'console']: + return test_colors + return orig_idleConf_GetHighlight(theme, element) + GetHighlight_patcher = unittest.mock.patch.object( + idlelib.sidebar.idleConf, 'GetHighlight', + mock_idleconf_GetHighlight) + GetHighlight_patcher.start() + def cleanup(): + GetHighlight_patcher.stop() + sidebar.update_colors() + self.addCleanup(cleanup) + + def get_sidebar_colors(): + canvas = sidebar.canvas + texts = list(canvas.find(tk.ALL)) + fgs = {canvas.itemcget(text, 'fill') for text in texts} + self.assertEqual(len(fgs), 1) + fg = next(iter(fgs)) + bg = canvas.cget('background') + return {"background": bg, "foreground": fg} + + self.assertNotEqual(get_sidebar_colors(), test_colors) + sidebar.update_colors() + self.assertEqual(get_sidebar_colors(), test_colors) + if __name__ == '__main__': unittest.main(verbosity=2) From 6efb9685d2c483bda3cb95333be24670e92b6539 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 20:52:05 +0300 Subject: [PATCH 38/62] remove show/hide_sidebar from the shell sidebar --- Lib/idlelib/pyshell.py | 1 - Lib/idlelib/sidebar.py | 18 +++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 8aa0aefc29fec2..512ea70424cab7 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -940,7 +940,6 @@ def __init__(self, flist=None): self.pollinterval = 50 # millisec self.shell_sidebar = self.ShellSidebar(self) - self.shell_sidebar.show_sidebar() # Insert UserInputTaggingDelegator at the top of the percolator, # but make calls to text.insert() skip it. This causes only insert diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index 35d58c78434869..e71001092c6aa2 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -391,21 +391,9 @@ def __init__(self, editwin): self.update_font() self.update_colors() - - def show_sidebar(self): - if not self.is_shown: - self.update_sidebar() - # _padx, pady = get_widget_padding(self.text) - self.canvas.grid(row=1, column=0, sticky=tk.NSEW, - # padx=2, pady=pady) - padx=2, pady=0) - self.is_shown = True - - def hide_sidebar(self): - if self.is_shown: - self.canvas.grid_forget() - self.canvas.delete(tk.ALL) - self.is_shown = False + self.update_sidebar() + self.canvas.grid(row=1, column=0, sticky=tk.NSEW, padx=2, pady=0) + self.is_shown = True def change_callback(self): if self.is_shown: From cc3792415760a72e2759a8913028ad3e7aee93b1 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 20:52:47 +0300 Subject: [PATCH 39/62] add more test cases --- Lib/idlelib/idle_test/test_sidebar.py | 76 ++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index f6dc077815c84a..1f708606da032f 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,6 +1,6 @@ """Test sidebar, coverage 93%""" from textwrap import dedent -from time import sleep +import sys from itertools import chain import unittest @@ -16,7 +16,7 @@ from idlelib.pyshell import fix_x11_paste, PyShell, PyShellFileList from idlelib.run import fix_scaling import idlelib.sidebar -from idlelib.sidebar import get_lineno +from idlelib.sidebar import get_end_linenumber, get_lineno class Dummy_editwin: @@ -571,6 +571,53 @@ def test_squeeze_single_line_command(self): self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() + @test_coroutine + def test_interrupt_recall_undo_redo(self): + event_generate = self.shell.text.event_generate + # Block statements are not indented because IDLE auto-indents. + initial_sidebar_lines = self.get_sidebar_lines() + + self.do_input(dedent('''\ + if True: + print(1) + ''')) + yield 0 + self.assert_sidebar_lines_end_with(['>>>', '...', '...']) + with_block_sidebar_lines = self.get_sidebar_lines() + self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) + + # Control-C + event_generate('<>') + yield 0 + self.assert_sidebar_lines_end_with(['>>>', '...', '...', ' ', '>>>']) + + # Recall previous via history + event_generate('<>') + event_generate('<>') + yield 0 + self.assert_sidebar_lines_end_with(['>>>', '...', ' ', '>>>']) + + # Recall previous via recall + event_generate('') + event_generate('') + event_generate('') + yield 0 + + event_generate('<>') + yield 0 + self.assert_sidebar_lines_end_with(['>>>']) + + event_generate('<>') + yield 0 + self.assert_sidebar_lines_end_with(['>>>', '...']) + + event_generate('') + event_generate('') + yield 2 + self.assert_sidebar_lines_end_with( + ['>>>', '...', '...', '...', ' ', '>>>'] + ) + def test_font(self): sidebar = self.shell.shell_sidebar @@ -629,6 +676,31 @@ def get_sidebar_colors(): sidebar.update_colors() self.assertEqual(get_sidebar_colors(), test_colors) + @test_coroutine + def test_mousewheel(self): + sidebar = self.shell.shell_sidebar + text = self.shell.text + + # Press Return 50 times to get the shell screen to scroll down. + for _i in range(50): + self.do_input('\n') + yield 0 + self.assertGreater(get_lineno(text, '@0,0'), 1) + + last_lineno = get_end_linenumber(text) + self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + + # Scroll up using the event with a positive delta. + delta = -1 if sys.platform == 'darwin' else 120 + sidebar.canvas.event_generate('', x=0, y=0, delta=delta) + yield 0 + self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + + # Scroll back down using the event. + sidebar.canvas.event_generate('', x=0, y=0) + yield 0 + self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) + if __name__ == '__main__': unittest.main(verbosity=2) From 97576daf75a1146d3e09901f698faa0f571e4e5f Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 20:53:43 +0300 Subject: [PATCH 40/62] update coverage percentage --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 1f708606da032f..c8abc08ebf5216 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,4 @@ -"""Test sidebar, coverage 93%""" +"""Test sidebar, coverage 96%""" from textwrap import dedent import sys From 2aa4e1abe308c7e396b44f6ceba75140e3c6fddb Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Mon, 12 Oct 2020 21:29:11 +0300 Subject: [PATCH 41/62] re-word squeeze_current_text() doc-string --- Lib/idlelib/squeezer.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/squeezer.py b/Lib/idlelib/squeezer.py index 12ee5895b373ca..6a0b2ed5e91809 100644 --- a/Lib/idlelib/squeezer.py +++ b/Lib/idlelib/squeezer.py @@ -288,11 +288,9 @@ def count_lines(self, s): return count_lines_with_wrapping(s, self.editwin.width) def squeeze_current_text(self): - """squeeze-current-text event handler + """Squeeze the text block where the insertion cursor is. - Squeeze the block of text inside which contains the "insert" cursor. - - If the insert cursor is not in a squeezable block of text, give the + If the cursor is not in a squeezable block of text, give the user a small warning and do nothing. """ # Set tag_name to the first valid tag found on the "insert" cursor. From 71f343eaa0555a04cde2ded75a7920f59b0205da Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Tue, 13 Oct 2020 11:10:22 +0300 Subject: [PATCH 42/62] update coverage percentage --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index c8abc08ebf5216..1f708606da032f 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,4 @@ -"""Test sidebar, coverage 96%""" +"""Test sidebar, coverage 93%""" from textwrap import dedent import sys From dbe931c6ed7e2a63864ed79de8ff62f3678cf489 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 14 Oct 2020 11:02:41 +0300 Subject: [PATCH 43/62] fix wrong colors at ends of input lines/blocks --- Lib/idlelib/pyshell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index cb48b9d7e5d897..12b8893ebe60d5 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -961,7 +961,7 @@ def update_colors(self): "stdin": {'background': None, 'foreground': None}, "stdout": idleConf.GetHighlight(theme, "stdout"), "stderr": idleConf.GetHighlight(theme, "stderr"), - "console": idleConf.GetHighlight(theme, "console"), + "console": idleConf.GetHighlight(theme, "normal"), } for tag, tag_colors_config in tag_colors.items(): self.text.tag_configure(tag, **tag_colors_config) From 779adc462cbc205fed7bb798678c6742a33a2c7b Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Wed, 14 Oct 2020 19:21:04 +0300 Subject: [PATCH 44/62] fix some sidebar tests affected by losing window focus Also comment out some existing line numbers tests which are also similarly fragile, but are harder to fix. --- Lib/idlelib/idle_test/test_sidebar.py | 163 +++++++++++++------------- 1 file changed, 81 insertions(+), 82 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 1f708606da032f..a9e688fa98abf2 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -40,6 +40,7 @@ class LineNumbersTest(unittest.TestCase): def setUpClass(cls): requires('gui') cls.root = tk.Tk() + cls.root.withdraw() cls.text_frame = tk.Frame(cls.root) cls.text_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) @@ -248,61 +249,61 @@ def get_width(): self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) - def test_click_selection(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') - self.root.update() - - # Click on the second line. - x, y = self.get_line_screen_position(2) - self.linenumber.sidebar_text.event_generate('', x=x, y=y) - self.linenumber.sidebar_text.update() - self.root.update() - - self.assertEqual(self.get_selection(), ('2.0', '3.0')) - - def simulate_drag(self, start_line, end_line): - start_x, start_y = self.get_line_screen_position(start_line) - end_x, end_y = self.get_line_screen_position(end_line) - - self.linenumber.sidebar_text.event_generate('', - x=start_x, y=start_y) - self.root.update() - - def lerp(a, b, steps): - """linearly interpolate from a to b (inclusive) in equal steps""" - last_step = steps - 1 - for i in range(steps): - yield ((last_step - i) / last_step) * a + (i / last_step) * b - - for x, y in zip( - map(int, lerp(start_x, end_x, steps=11)), - map(int, lerp(start_y, end_y, steps=11)), - ): - self.linenumber.sidebar_text.event_generate('', x=x, y=y) - self.root.update() - - self.linenumber.sidebar_text.event_generate('', - x=end_x, y=end_y) - self.root.update() - - def test_drag_selection_down(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - self.root.update() - - # Drag from the second line to the fourth line. - self.simulate_drag(2, 4) - self.assertEqual(self.get_selection(), ('2.0', '5.0')) - - def test_drag_selection_up(self): - self.linenumber.show_sidebar() - self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - self.root.update() - - # Drag from the fourth line to the second line. - self.simulate_drag(4, 2) - self.assertEqual(self.get_selection(), ('2.0', '5.0')) + # def test_click_selection(self): + # self.linenumber.show_sidebar() + # self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') + # self.root.update() + # + # # Click on the second line. + # x, y = self.get_line_screen_position(2) + # self.linenumber.sidebar_text.event_generate('', x=x, y=y) + # self.linenumber.sidebar_text.update() + # self.root.update() + # + # self.assertEqual(self.get_selection(), ('2.0', '3.0')) + # + # def simulate_drag(self, start_line, end_line): + # start_x, start_y = self.get_line_screen_position(start_line) + # end_x, end_y = self.get_line_screen_position(end_line) + # + # self.linenumber.sidebar_text.event_generate('', + # x=start_x, y=start_y) + # self.root.update() + # + # def lerp(a, b, steps): + # """linearly interpolate from a to b (inclusive) in equal steps""" + # last_step = steps - 1 + # for i in range(steps): + # yield ((last_step - i) / last_step) * a + (i / last_step) * b + # + # for x, y in zip( + # map(int, lerp(start_x, end_x, steps=11)), + # map(int, lerp(start_y, end_y, steps=11)), + # ): + # self.linenumber.sidebar_text.event_generate('', x=x, y=y) + # self.root.update() + # + # self.linenumber.sidebar_text.event_generate('', + # x=end_x, y=end_y) + # self.root.update() + # + # def test_drag_selection_down(self): + # self.linenumber.show_sidebar() + # self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') + # self.root.update() + # + # # Drag from the second line to the fourth line. + # self.simulate_drag(2, 4) + # self.assertEqual(self.get_selection(), ('2.0', '5.0')) + # + # def test_drag_selection_up(self): + # self.linenumber.show_sidebar() + # self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') + # self.root.update() + # + # # Drag from the fourth line to the second line. + # self.simulate_drag(4, 2) + # self.assertEqual(self.get_selection(), ('2.0', '5.0')) def test_scroll(self): self.linenumber.show_sidebar() @@ -315,15 +316,15 @@ def test_scroll(self): self.assertEqual(self.text.index('@0,0'), '11.0') self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') - # Generate a mouse-wheel event and make sure it scrolled up or down. - # The meaning of the "delta" is OS-dependant, so this just checks for - # any change. - self.linenumber.sidebar_text.event_generate('', - x=0, y=0, - delta=10) - self.root.update() - self.assertNotEqual(self.text.index('@0,0'), '11.0') - self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') + # # Generate a mouse-wheel event and make sure it scrolled up or down. + # # The meaning of the "delta" is OS-dependant, so this just checks for + # # any change. + # self.linenumber.sidebar_text.event_generate('', + # x=0, y=0, + # delta=10) + # self.root.update() + # self.assertNotEqual(self.text.index('@0,0'), '11.0') + # self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') def test_font(self): ln = self.linenumber @@ -478,10 +479,9 @@ def do_input(self, input): text = shell.text for line_index, line in enumerate(input.split('\n')): if line_index > 0: - text.event_generate('') - text.event_generate('') + text.event_generate('<>') for char in line: - text.event_generate(char) + text.insert('insert', char) def run_test_coroutine(self, coroutine): root = self.root @@ -573,7 +573,7 @@ def test_squeeze_single_line_command(self): @test_coroutine def test_interrupt_recall_undo_redo(self): - event_generate = self.shell.text.event_generate + text = self.shell.text # Block statements are not indented because IDLE auto-indents. initial_sidebar_lines = self.get_sidebar_lines() @@ -587,32 +587,31 @@ def test_interrupt_recall_undo_redo(self): self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) # Control-C - event_generate('<>') + text.event_generate('<>') yield 0 self.assert_sidebar_lines_end_with(['>>>', '...', '...', ' ', '>>>']) # Recall previous via history - event_generate('<>') - event_generate('<>') + text.event_generate('<>') + text.event_generate('<>') yield 0 self.assert_sidebar_lines_end_with(['>>>', '...', ' ', '>>>']) # Recall previous via recall - event_generate('') - event_generate('') - event_generate('') + text.mark_set('insert', text.index('insert -2l')) + text.event_generate('<>') yield 0 - event_generate('<>') + text.event_generate('<>') yield 0 self.assert_sidebar_lines_end_with(['>>>']) - event_generate('<>') + text.event_generate('<>') yield 0 self.assert_sidebar_lines_end_with(['>>>', '...']) - event_generate('') - event_generate('') + text.event_generate('<>') + text.event_generate('<>') yield 2 self.assert_sidebar_lines_end_with( ['>>>', '...', '...', '...', ' ', '>>>'] @@ -681,16 +680,16 @@ def test_mousewheel(self): sidebar = self.shell.shell_sidebar text = self.shell.text - # Press Return 50 times to get the shell screen to scroll down. - for _i in range(50): - self.do_input('\n') + # Enter a 100-line string to scroll the shell screen down. + self.do_input('x = """' + ('\n'*100) + '"""\n') yield 0 self.assertGreater(get_lineno(text, '@0,0'), 1) last_lineno = get_end_linenumber(text) self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) - # Scroll up using the event with a positive delta. + # Scroll up using the event. + # The meaning delta is platform-dependant. delta = -1 if sys.platform == 'darwin' else 120 sidebar.canvas.event_generate('', x=0, y=0, delta=delta) yield 0 From 8f81915b5e8260486017f65a7fff2a16764283a5 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 15 Oct 2020 17:34:57 +0300 Subject: [PATCH 45/62] fix highlighting and sidebar prompts after syntax errors --- Lib/code.py | 14 +++++++------- Lib/idlelib/pyshell.py | 16 +++++++++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/Lib/code.py b/Lib/code.py index 76000f8c8b2c1e..fb0ae703ad955b 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -53,10 +53,10 @@ def runsource(self, source, filename="", symbol="single"): object. The code is executed by calling self.runcode() (which also handles run-time exceptions, except for SystemExit). - The return value is True in case 2, False in the other cases (unless - an exception is raised). The return value can be used to - decide whether to use sys.ps1 or sys.ps2 to prompt the next - line. + Return whether the code was complete and successfully compiled: True + if complete, False if incomplete, None if there was a complication + error. The return value can be used to decide whether to use sys.ps1 + or sys.ps2 to prompt the next line. """ try: @@ -64,15 +64,15 @@ def runsource(self, source, filename="", symbol="single"): except (OverflowError, SyntaxError, ValueError): # Case 1 self.showsyntaxerror(filename) - return False + return None if code is None: # Case 2 - return True + return False # Case 3 self.runcode(code) - return False + return True def runcode(self, code): """Execute a code object. diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 12b8893ebe60d5..26a3f46cd57ed9 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1283,7 +1283,8 @@ def recall(self, s, event): if prefix.rstrip().endswith(':'): self.newline_and_indent_event(event) prefix = self.text.get("insert linestart", "insert") - self.text.insert("insert", lines[0].strip(), self.user_input_insert_tags) + self.text.insert("insert", lines[0].strip(), + self.user_input_insert_tags) if len(lines) > 1: orig_base_indent = re.search(r'^([ \t]*)', lines[0]).group(0) new_base_indent = re.search(r'^([ \t]*)', prefix).group(0) @@ -1291,7 +1292,8 @@ def recall(self, s, event): if line.startswith(orig_base_indent): # replace orig base indentation with new indentation line = new_base_indent + line[len(orig_base_indent):] - self.text.insert('insert', '\n'+line.rstrip(), self.user_input_insert_tags) + self.text.insert('insert', '\n' + line.rstrip(), + self.user_input_insert_tags) finally: self.text.see("insert") self.text.undo_block_stop() @@ -1302,10 +1304,14 @@ def runit(self): # Strip off last newline and surrounding whitespace. # (To allow you to hit return twice to end a statement.) line = re.sub(r"[ \t]*\n?[ \t]*$", "", line) - input_is_incomplete = self.interp.runsource(line) - if not input_is_incomplete: - if self.user_input_insert_tags: + input_is_complete = self.interp.runsource(line) + if input_is_complete or input_is_complete is None: + if ( + self.user_input_insert_tags and + self.text.get(index_before) == '\n' + ): self.text.tag_remove(self.user_input_insert_tags, index_before) + self.shell_sidebar.update_sidebar() def open_stack_viewer(self, event=None): if self.interp.rpcclt: From ddc00264f7fb6b734a092270d0183663dd444dde Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 15 Oct 2020 17:46:23 +0300 Subject: [PATCH 46/62] better long-term disabling of line numbers tests --- Lib/idlelib/idle_test/test_sidebar.py | 137 ++++++++++++++------------ 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index a9e688fa98abf2..96e8194c4488b4 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -249,61 +249,72 @@ def get_width(): self.assert_sidebar_n_lines(1) self.assertEqual(get_width(), 1) - # def test_click_selection(self): - # self.linenumber.show_sidebar() - # self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') - # self.root.update() + # The following tests are temporarily disabled due to relying on + # simulated user input and inspecting which text is selected, which + # are fragile and can fail when several GUI tests are run in parallel + # or when the windows created by the test lose focus. # - # # Click on the second line. - # x, y = self.get_line_screen_position(2) - # self.linenumber.sidebar_text.event_generate('', x=x, y=y) - # self.linenumber.sidebar_text.update() - # self.root.update() - # - # self.assertEqual(self.get_selection(), ('2.0', '3.0')) - # - # def simulate_drag(self, start_line, end_line): - # start_x, start_y = self.get_line_screen_position(start_line) - # end_x, end_y = self.get_line_screen_position(end_line) - # - # self.linenumber.sidebar_text.event_generate('', - # x=start_x, y=start_y) - # self.root.update() - # - # def lerp(a, b, steps): - # """linearly interpolate from a to b (inclusive) in equal steps""" - # last_step = steps - 1 - # for i in range(steps): - # yield ((last_step - i) / last_step) * a + (i / last_step) * b - # - # for x, y in zip( - # map(int, lerp(start_x, end_x, steps=11)), - # map(int, lerp(start_y, end_y, steps=11)), - # ): - # self.linenumber.sidebar_text.event_generate('', x=x, y=y) - # self.root.update() - # - # self.linenumber.sidebar_text.event_generate('', - # x=end_x, y=end_y) - # self.root.update() - # - # def test_drag_selection_down(self): - # self.linenumber.show_sidebar() - # self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - # self.root.update() - # - # # Drag from the second line to the fourth line. - # self.simulate_drag(2, 4) - # self.assertEqual(self.get_selection(), ('2.0', '5.0')) - # - # def test_drag_selection_up(self): - # self.linenumber.show_sidebar() - # self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') - # self.root.update() - # - # # Drag from the fourth line to the second line. - # self.simulate_drag(4, 2) - # self.assertEqual(self.get_selection(), ('2.0', '5.0')) + # TODO: Re-work these tests or remove them from the test suite. + + @unittest.skip('test disabled') + def test_click_selection(self): + self.linenumber.show_sidebar() + self.text.insert('1.0', 'one\ntwo\nthree\nfour\n') + self.root.update() + + # Click on the second line. + x, y = self.get_line_screen_position(2) + self.linenumber.sidebar_text.event_generate('', x=x, y=y) + self.linenumber.sidebar_text.update() + self.root.update() + + self.assertEqual(self.get_selection(), ('2.0', '3.0')) + + @unittest.skip('test disabled') + def simulate_drag(self, start_line, end_line): + start_x, start_y = self.get_line_screen_position(start_line) + end_x, end_y = self.get_line_screen_position(end_line) + + self.linenumber.sidebar_text.event_generate('', + x=start_x, y=start_y) + self.root.update() + + def lerp(a, b, steps): + """linearly interpolate from a to b (inclusive) in equal steps""" + last_step = steps - 1 + for i in range(steps): + yield ((last_step - i) / last_step) * a + (i / last_step) * b + + for x, y in zip( + map(int, lerp(start_x, end_x, steps=11)), + map(int, lerp(start_y, end_y, steps=11)), + ): + self.linenumber.sidebar_text.event_generate('', x=x, y=y) + self.root.update() + + self.linenumber.sidebar_text.event_generate('', + x=end_x, y=end_y) + self.root.update() + + @unittest.skip('test disabled') + def test_drag_selection_down(self): + self.linenumber.show_sidebar() + self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') + self.root.update() + + # Drag from the second line to the fourth line. + self.simulate_drag(2, 4) + self.assertEqual(self.get_selection(), ('2.0', '5.0')) + + @unittest.skip('test disabled') + def test_drag_selection_up(self): + self.linenumber.show_sidebar() + self.text.insert('1.0', 'one\ntwo\nthree\nfour\nfive\n') + self.root.update() + + # Drag from the fourth line to the second line. + self.simulate_drag(4, 2) + self.assertEqual(self.get_selection(), ('2.0', '5.0')) def test_scroll(self): self.linenumber.show_sidebar() @@ -316,15 +327,15 @@ def test_scroll(self): self.assertEqual(self.text.index('@0,0'), '11.0') self.assertEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') - # # Generate a mouse-wheel event and make sure it scrolled up or down. - # # The meaning of the "delta" is OS-dependant, so this just checks for - # # any change. - # self.linenumber.sidebar_text.event_generate('', - # x=0, y=0, - # delta=10) - # self.root.update() - # self.assertNotEqual(self.text.index('@0,0'), '11.0') - # self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') + # Generate a mouse-wheel event and make sure it scrolled up or down. + # The meaning of the "delta" is OS-dependant, so this just checks for + # any change. + self.linenumber.sidebar_text.event_generate('', + x=0, y=0, + delta=10) + self.root.update() + self.assertNotEqual(self.text.index('@0,0'), '11.0') + self.assertNotEqual(self.linenumber.sidebar_text.index('@0,0'), '11.0') def test_font(self): ln = self.linenumber From d1e0d3291be578b03ed087a8989b25874916c2c4 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 15 Oct 2020 17:50:57 +0300 Subject: [PATCH 47/62] update unittest coverage percentage --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 96e8194c4488b4..b3e267855aeaf6 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -1,4 +1,4 @@ -"""Test sidebar, coverage 93%""" +"""Test sidebar, coverage 85%""" from textwrap import dedent import sys From 0516db9ed1d802f601bc1739694207e3554a67e9 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 22 Oct 2020 15:41:45 +0300 Subject: [PATCH 48/62] run sidebar tests with PyShell without a sub-process --- Lib/idlelib/idle_test/test_sidebar.py | 40 +++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index b3e267855aeaf6..5f05368b108333 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -405,7 +405,7 @@ class ShellSidebarTest(unittest.TestCase): def setUpClass(cls): requires('gui') - idlelib.pyshell.use_subprocess = True + idlelib.pyshell.use_subprocess = False cls.root = root = tk.Tk() root.withdraw() @@ -502,14 +502,14 @@ def run_test_coroutine(self, coroutine): def after_callback(): nonlocal exception try: - interval_multiplier = next(coroutine) or 1 + next(coroutine) except StopIteration: root.quit() except Exception as exc: exception = exc root.quit() else: - root.after(100 * interval_multiplier, after_callback) + root.after_idle(after_callback) root.after(0, after_callback) root.mainloop() @@ -527,13 +527,13 @@ def test_initial_state(self): @test_coroutine def test_single_empty_input(self): self.do_input('\n') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', '>>>']) @test_coroutine def test_single_line_command(self): self.do_input('1\n') - yield 2 + yield self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) @test_coroutine @@ -544,7 +544,7 @@ def test_multi_line_command(self): print(1) ''')) - yield 2 + yield self.assert_sidebar_lines_end_with([ '>>>', '...', @@ -557,7 +557,7 @@ def test_multi_line_command(self): @test_coroutine def test_single_long_line_wraps(self): self.do_input('1' * 200 + '\n') - yield 2 + yield self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() @@ -567,18 +567,18 @@ def test_squeeze_single_line_command(self): text = shell.text self.do_input('1\n') - yield 2 + yield self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) line = int(shell.text.index('insert -1line').split('.')[0]) text.mark_set('insert', f"{line}.0") text.event_generate('<>') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() shell.squeezer.expandingbuttons[0].expand() - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) self.assert_sidebar_lines_synced() @@ -592,38 +592,38 @@ def test_interrupt_recall_undo_redo(self): if True: print(1) ''')) - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', '...', '...']) with_block_sidebar_lines = self.get_sidebar_lines() self.assertNotEqual(with_block_sidebar_lines, initial_sidebar_lines) # Control-C text.event_generate('<>') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', '...', '...', ' ', '>>>']) # Recall previous via history text.event_generate('<>') text.event_generate('<>') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', '...', ' ', '>>>']) # Recall previous via recall text.mark_set('insert', text.index('insert -2l')) text.event_generate('<>') - yield 0 + yield text.event_generate('<>') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>']) text.event_generate('<>') - yield 0 + yield self.assert_sidebar_lines_end_with(['>>>', '...']) text.event_generate('<>') text.event_generate('<>') - yield 2 + yield self.assert_sidebar_lines_end_with( ['>>>', '...', '...', '...', ' ', '>>>'] ) @@ -693,7 +693,7 @@ def test_mousewheel(self): # Enter a 100-line string to scroll the shell screen down. self.do_input('x = """' + ('\n'*100) + '"""\n') - yield 0 + yield self.assertGreater(get_lineno(text, '@0,0'), 1) last_lineno = get_end_linenumber(text) @@ -703,12 +703,12 @@ def test_mousewheel(self): # The meaning delta is platform-dependant. delta = -1 if sys.platform == 'darwin' else 120 sidebar.canvas.event_generate('', x=0, y=0, delta=delta) - yield 0 + yield self.assertIsNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) # Scroll back down using the event. sidebar.canvas.event_generate('', x=0, y=0) - yield 0 + yield self.assertIsNotNone(text.dlineinfo(text.index(f'{last_lineno}.0'))) From afa66690ef1dcd32b54ad26871206e088b4314e3 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 22 Oct 2020 15:57:47 +0300 Subject: [PATCH 49/62] avoid creating text widgets for whitespace shell prompts --- Lib/idlelib/idle_test/test_sidebar.py | 34 +++++++++++++++------------ Lib/idlelib/sidebar.py | 7 +++--- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 5f05368b108333..95c7a3acd07c35 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -451,8 +451,12 @@ def setUp(self): def get_sidebar_lines(self): canvas = self.shell.shell_sidebar.canvas texts = list(canvas.find(tk.ALL)) - texts.sort(key=lambda text: canvas.bbox(text)[1]) - return [canvas.itemcget(text, 'text') for text in texts] + texts_by_y_coords = { + canvas.bbox(text)[1]: canvas.itemcget(text, 'text') + for text in texts + } + line_y_coords = self.get_shell_line_y_coords() + return [texts_by_y_coords.get(y, None) for y in line_y_coords] def assert_sidebar_lines_end_with(self, expected_lines): self.shell.shell_sidebar.update_sidebar() @@ -480,9 +484,9 @@ def get_sidebar_line_y_coords(self): return [canvas.bbox(text)[1] for text in texts] def assert_sidebar_lines_synced(self): - self.assertEqual( - self.get_sidebar_line_y_coords(), - self.get_shell_line_y_coords(), + self.assertLessEqual( + set(self.get_sidebar_line_y_coords()), + set(self.get_shell_line_y_coords()), ) def do_input(self, input): @@ -520,7 +524,7 @@ def test_initial_state(self): sidebar_lines = self.get_sidebar_lines() self.assertEqual( sidebar_lines, - [' '] * (len(sidebar_lines) - 1) + ['>>>'], + [None] * (len(sidebar_lines) - 1) + ['>>>'], ) self.assert_sidebar_lines_synced() @@ -534,7 +538,7 @@ def test_single_empty_input(self): def test_single_line_command(self): self.do_input('1\n') yield - self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) @test_coroutine def test_multi_line_command(self): @@ -550,7 +554,7 @@ def test_multi_line_command(self): '...', '...', '...', - ' ', + None, '>>>', ]) @@ -558,7 +562,7 @@ def test_multi_line_command(self): def test_single_long_line_wraps(self): self.do_input('1' * 200 + '\n') yield - self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) self.assert_sidebar_lines_synced() @test_coroutine @@ -568,18 +572,18 @@ def test_squeeze_single_line_command(self): self.do_input('1\n') yield - self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) line = int(shell.text.index('insert -1line').split('.')[0]) text.mark_set('insert', f"{line}.0") text.event_generate('<>') yield - self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) self.assert_sidebar_lines_synced() shell.squeezer.expandingbuttons[0].expand() yield - self.assert_sidebar_lines_end_with(['>>>', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) self.assert_sidebar_lines_synced() @test_coroutine @@ -600,13 +604,13 @@ def test_interrupt_recall_undo_redo(self): # Control-C text.event_generate('<>') yield - self.assert_sidebar_lines_end_with(['>>>', '...', '...', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', '...', '...', None, '>>>']) # Recall previous via history text.event_generate('<>') text.event_generate('<>') yield - self.assert_sidebar_lines_end_with(['>>>', '...', ' ', '>>>']) + self.assert_sidebar_lines_end_with(['>>>', '...', None, '>>>']) # Recall previous via recall text.mark_set('insert', text.index('insert -2l')) @@ -625,7 +629,7 @@ def test_interrupt_recall_undo_redo(self): text.event_generate('<>') yield self.assert_sidebar_lines_end_with( - ['>>>', '...', '...', '...', ' ', '>>>'] + ['>>>', '...', '...', '...', None, '>>>'] ) def test_font(self): diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index e71001092c6aa2..f27774b7ecfa75 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -416,10 +416,11 @@ def update_sidebar(self): prompt = ( '>>>' if "console" in prev_newline_tagnames else '...' if "stdin" in prev_newline_tagnames else - ' ' + None ) - canvas.create_text(2, y, anchor=tk.NW, text=prompt, - font=self.font, fill=self.colors[0]) + if prompt: + canvas.create_text(2, y, anchor=tk.NW, text=prompt, + font=self.font, fill=self.colors[0]) index = text.index(f'{index}+1line') def yscroll_event(self, *args, **kwargs): From 9d06f26d733aad39220caa5e975bad97a326e150 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 22 Oct 2020 16:01:48 +0300 Subject: [PATCH 50/62] simplify some test code --- Lib/idlelib/idle_test/test_sidebar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 95c7a3acd07c35..a064330e93cf90 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -574,8 +574,7 @@ def test_squeeze_single_line_command(self): yield self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) - line = int(shell.text.index('insert -1line').split('.')[0]) - text.mark_set('insert', f"{line}.0") + text.mark_set('insert', f'insert -1line linestart') text.event_generate('<>') yield self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) From 73146fb30df1aabdab569646ffe34689d7695e5a Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 22 Oct 2020 16:02:43 +0300 Subject: [PATCH 51/62] remove unnecessary parentheses --- Lib/idlelib/idle_test/test_sidebar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index a064330e93cf90..5b8e6554d9acf7 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -695,7 +695,7 @@ def test_mousewheel(self): text = self.shell.text # Enter a 100-line string to scroll the shell screen down. - self.do_input('x = """' + ('\n'*100) + '"""\n') + self.do_input('x = """' + '\n'*100 + '"""\n') yield self.assertGreater(get_lineno(text, '@0,0'), 1) From 19b16a49fe53c391fb9b451de8d614a933d451db Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Thu, 22 Oct 2020 16:27:19 +0300 Subject: [PATCH 52/62] fix first prompt when shown text starts with the end of a wrapped line --- Lib/idlelib/idle_test/test_sidebar.py | 14 +++++++++++--- Lib/idlelib/sidebar.py | 2 ++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 5b8e6554d9acf7..478849c49fbd9a 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -5,7 +5,7 @@ from itertools import chain import unittest import unittest.mock -from test.support import requires +from test.support import requires, swap_attr import tkinter as tk from idlelib.delegator import Delegator @@ -469,6 +469,8 @@ def get_shell_line_y_coords(self): text = self.shell.text y_coords = [] index = text.index("@0,0") + if index.split('.', 1)[1] != '0': + index = text.index(f"{index} +1line linestart") while True: lineinfo = text.dlineinfo(index) if lineinfo is None: @@ -495,8 +497,7 @@ def do_input(self, input): for line_index, line in enumerate(input.split('\n')): if line_index > 0: text.event_generate('<>') - for char in line: - text.insert('insert', char) + text.insert('insert', line) def run_test_coroutine(self, coroutine): root = self.root @@ -631,6 +632,13 @@ def test_interrupt_recall_undo_redo(self): ['>>>', '...', '...', '...', None, '>>>'] ) + @test_coroutine + def test_very_long_wrapped_line(self): + with swap_attr(self.shell, 'squeezer', None): + self.do_input('x = ' + '1'*10_000 + '\n') + yield + self.assertEqual(self.get_sidebar_lines(), ['>>>']) + def test_font(self): sidebar = self.shell.shell_sidebar diff --git a/Lib/idlelib/sidebar.py b/Lib/idlelib/sidebar.py index f27774b7ecfa75..a947961b858d68 100644 --- a/Lib/idlelib/sidebar.py +++ b/Lib/idlelib/sidebar.py @@ -407,6 +407,8 @@ def update_sidebar(self): canvas.delete(tk.ALL) index = text.index("@0,0") + if index.split('.', 1)[1] != '0': + index = text.index(f'{index}+1line linestart') while True: lineinfo = text.dlineinfo(index) if lineinfo is None: From ecab884f5608af771cc09ef513263da8548285a3 Mon Sep 17 00:00:00 2001 From: Tal Einat Date: Sun, 1 Nov 2020 13:01:06 +0200 Subject: [PATCH 53/62] fix failing tests on Azure Devops --- Lib/idlelib/idle_test/test_sidebar.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index 478849c49fbd9a..a2129b4686a9b5 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -405,7 +405,17 @@ class ShellSidebarTest(unittest.TestCase): def setUpClass(cls): requires('gui') + try: + orig_use_subprocess = idlelib.pyshell.use_subprocess + except AttributeError: + orig_use_subprocess = None idlelib.pyshell.use_subprocess = False + def cleanup_use_subprocess(): + if orig_use_subprocess is not None: + idlelib.pyshell.use_subprocess = orig_use_subprocess + else: + del idlelib.pyshell.use_subprocess + cls.addClassCleanup(cleanup_use_subprocess) cls.root = root = tk.Tk() root.withdraw() @@ -497,7 +507,7 @@ def do_input(self, input): for line_index, line in enumerate(input.split('\n')): if line_index > 0: text.event_generate('<>') - text.insert('insert', line) + text.insert('insert', line, 'stdin') def run_test_coroutine(self, coroutine): root = self.root @@ -514,8 +524,8 @@ def after_callback(): exception = exc root.quit() else: - root.after_idle(after_callback) - root.after(0, after_callback) + root.after(1, root.after_idle, after_callback) + root.after(0, root.after_idle, after_callback) root.mainloop() if exception: From f6c9a7ca9cd46910c08f134de2500feccb0ec8eb Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 25 Apr 2021 14:29:00 +0300 Subject: [PATCH 54/62] revert indentation change Co-authored-by: Terry Jan Reedy --- Lib/idlelib/colorizer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/idlelib/colorizer.py b/Lib/idlelib/colorizer.py index 58484c51572add..3c527409731afa 100644 --- a/Lib/idlelib/colorizer.py +++ b/Lib/idlelib/colorizer.py @@ -132,8 +132,7 @@ def LoadTagDefs(self): # when fixing the "hit" tag's visibility, or when the replace dialog is replaced with a # non-modal alternative. "hit": idleConf.GetHighlight(theme, "hit"), - } - + } if DEBUG: print('tagdefs', self.tagdefs) def insert(self, index, chars, tags=None): From 990cef47acc923aaa4db480727a7d15263df9988 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 25 Apr 2021 15:05:37 +0300 Subject: [PATCH 55/62] revert InteractiveInterpreter.runsource return values --- Lib/code.py | 14 +++++++------- Lib/idlelib/pyshell.py | 7 ++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Lib/code.py b/Lib/code.py index fb0ae703ad955b..76000f8c8b2c1e 100644 --- a/Lib/code.py +++ b/Lib/code.py @@ -53,10 +53,10 @@ def runsource(self, source, filename="", symbol="single"): object. The code is executed by calling self.runcode() (which also handles run-time exceptions, except for SystemExit). - Return whether the code was complete and successfully compiled: True - if complete, False if incomplete, None if there was a complication - error. The return value can be used to decide whether to use sys.ps1 - or sys.ps2 to prompt the next line. + The return value is True in case 2, False in the other cases (unless + an exception is raised). The return value can be used to + decide whether to use sys.ps1 or sys.ps2 to prompt the next + line. """ try: @@ -64,15 +64,15 @@ def runsource(self, source, filename="", symbol="single"): except (OverflowError, SyntaxError, ValueError): # Case 1 self.showsyntaxerror(filename) - return None + return False if code is None: # Case 2 - return False + return True # Case 3 self.runcode(code) - return True + return False def runcode(self, code): """Execute a code object. diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index f1dbd1aa1c319d..572551af01e916 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1311,11 +1311,8 @@ def runit(self): # (To allow you to hit return twice to end a statement.) line = re.sub(r"[ \t]*\n?[ \t]*$", "", line) input_is_complete = self.interp.runsource(line) - if input_is_complete or input_is_complete is None: - if ( - self.user_input_insert_tags and - self.text.get(index_before) == '\n' - ): + if not input_is_complete: + if self.text.get(index_before) == '\n': self.text.tag_remove(self.user_input_insert_tags, index_before) self.shell_sidebar.update_sidebar() From e86353b6defb0088b8c9d8a23afebde93d3dedf1 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 25 Apr 2021 16:17:53 +0300 Subject: [PATCH 56/62] fix removal of final newline and surrounding whitespace --- Lib/idlelib/idle_test/test_pyshell.py | 84 +++++++++++++++++++++++++++ Lib/idlelib/pyshell.py | 11 +++- 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index 4a096676f25796..56042a3b972f30 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -60,5 +60,89 @@ def test_init(self): ## self.assertIsInstance(ps, pyshell.PyShell) +class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase): + func = pyshell.PyShell._remove_last_newline_and_surrounding_whitespace + + def all_removed(self, text): + self.assertEqual('', self.func(text)) + + def none_removed(self, text): + self.assertEqual(text, self.func(text)) + + def check_result(self, text, expected): + self.assertEqual(expected, self.func(text)) + + def test_empty(self): + self.all_removed('') + + def test_newline(self): + self.all_removed('\n') + + def test_whitespace_no_newline(self): + self.all_removed(' ') + self.all_removed(' ') + self.all_removed(' ') + self.all_removed(' ' * 20) + self.all_removed('\t') + self.all_removed('\t\t') + self.all_removed('\t\t\t') + self.all_removed('\t' * 20) + self.all_removed('\t ') + self.all_removed(' \t') + self.all_removed(' \t \t ') + self.all_removed('\t \t \t') + + def test_newline_with_whitespace(self): + self.all_removed(' \n') + self.all_removed('\t\n') + self.all_removed(' \t\n') + self.all_removed('\t \n') + self.all_removed('\n ') + self.all_removed('\n\t') + self.all_removed('\n \t') + self.all_removed('\n\t ') + self.all_removed(' \n ') + self.all_removed('\t\n ') + self.all_removed(' \n\t') + self.all_removed('\t\n\t') + self.all_removed('\t \t \t\n') + self.all_removed(' \t \t \n') + self.all_removed('\n\t \t \t') + self.all_removed('\n \t \t ') + + def test_multiple_newlines(self): + self.check_result('\n\n', '\n') + self.check_result('\n' * 5, '\n' * 4) + self.check_result('\n' * 5 + '\t', '\n' * 4) + self.check_result('\n' * 20, '\n' * 19) + self.check_result('\n' * 20 + ' ', '\n' * 19) + self.check_result(' \n \n ', ' \n') + self.check_result(' \n\n ', ' \n') + self.check_result(' \n\n', ' \n') + self.check_result('\t\n\n', '\t\n') + self.check_result('\n\n ', '\n') + self.check_result('\n\n\t', '\n') + self.check_result(' \n \n ', ' \n') + self.check_result('\t\n\t\n\t', '\t\n') + + def test_non_whitespace(self): + self.none_removed('a') + self.check_result('a\n', 'a') + self.check_result('a\n ', 'a') + self.check_result('a \n ', 'a') + self.check_result('a \n\t', 'a') + self.none_removed('-') + self.check_result('-\n', '-') + self.none_removed('.') + self.check_result('.\n', '.') + + def test_unsupported_whitespace(self): + self.none_removed('\v') + self.none_removed('\n\v') + self.check_result('\v\n', '\v') + self.none_removed(' \n\v') + self.check_result('\v\n ', '\v') + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 572551af01e916..928a9d76c1c863 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1304,12 +1304,21 @@ def recall(self, s, event): self.text.see("insert") self.text.undo_block_stop() + _last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?$") + @classmethod + def _remove_last_newline_and_surrounding_whitespace(cls, line): + "Strip off last newline and surrounding whitespace." + # Add an extra newline at the end due to the way the re module + # treats the '$' symbol: "Matches the end of the string or just + # before the newline at the end of the string, ..." + return cls._last_newline_re.sub("", line + '\n') + def runit(self): index_before = self.text.index("end-2c") line = self.text.get("iomark", "end-1c") # Strip off last newline and surrounding whitespace. # (To allow you to hit return twice to end a statement.) - line = re.sub(r"[ \t]*\n?[ \t]*$", "", line) + line = self._remove_last_newline_and_surrounding_whitespace(line) input_is_complete = self.interp.runsource(line) if not input_is_complete: if self.text.get(index_before) == '\n': From a1891a4ad061996674a21ff1236787fb9b009868 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Sun, 25 Apr 2021 17:19:31 +0300 Subject: [PATCH 57/62] simplify fixed newline+whitespace removal using \Z --- Lib/idlelib/idle_test/test_pyshell.py | 8 ++++---- Lib/idlelib/pyshell.py | 12 ++---------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/Lib/idlelib/idle_test/test_pyshell.py b/Lib/idlelib/idle_test/test_pyshell.py index 56042a3b972f30..706703965bffd6 100644 --- a/Lib/idlelib/idle_test/test_pyshell.py +++ b/Lib/idlelib/idle_test/test_pyshell.py @@ -61,16 +61,16 @@ def test_init(self): class PyShellRemoveLastNewlineAndSurroundingWhitespaceTest(unittest.TestCase): - func = pyshell.PyShell._remove_last_newline_and_surrounding_whitespace + regexp = pyshell.PyShell._last_newline_re def all_removed(self, text): - self.assertEqual('', self.func(text)) + self.assertEqual('', self.regexp.sub('', text)) def none_removed(self, text): - self.assertEqual(text, self.func(text)) + self.assertEqual(text, self.regexp.sub('', text)) def check_result(self, text, expected): - self.assertEqual(expected, self.func(text)) + self.assertEqual(expected, self.regexp.sub('', text)) def test_empty(self): self.all_removed('') diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index 928a9d76c1c863..dff70200383452 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -1304,21 +1304,13 @@ def recall(self, s, event): self.text.see("insert") self.text.undo_block_stop() - _last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?$") - @classmethod - def _remove_last_newline_and_surrounding_whitespace(cls, line): - "Strip off last newline and surrounding whitespace." - # Add an extra newline at the end due to the way the re module - # treats the '$' symbol: "Matches the end of the string or just - # before the newline at the end of the string, ..." - return cls._last_newline_re.sub("", line + '\n') - + _last_newline_re = re.compile(r"[ \t]*(\n[ \t]*)?\Z") def runit(self): index_before = self.text.index("end-2c") line = self.text.get("iomark", "end-1c") # Strip off last newline and surrounding whitespace. # (To allow you to hit return twice to end a statement.) - line = self._remove_last_newline_and_surrounding_whitespace(line) + line = self._last_newline_re.sub("", line) input_is_complete = self.interp.runsource(line) if not input_is_complete: if self.text.get(index_before) == '\n': From 3e52c5d7148701710c098840f41329c483ade252 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 26 Apr 2021 09:03:55 +0300 Subject: [PATCH 58/62] revert unneeded changes to ResetColorizer --- Lib/idlelib/editor.py | 19 ++++--------------- Lib/idlelib/pyshell.py | 11 +++++------ 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 622d82676a9d11..2362286fe71923 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -266,7 +266,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): io.set_filename_change_hook(self.filename_change_hook) self.good_load = False self.set_indentation_params(False) - self.color = None # initialized below in self.update_colors() + self.color = None # initialized below in self.ResetColorizer self.code_context = None # optionally initialized later below self.line_numbers = None # optionally initialized later below if filename: @@ -279,7 +279,7 @@ def __init__(self, flist=None, filename=None, key=None, root=None): io.set_filename(filename) self.good_load = True - self.update_colors() + self.ResetColorizer() self.saved_change_hook() self.update_recent_files_list() self.load_extensions() @@ -794,23 +794,12 @@ def _rmcolorizer(self): self.color = None def ResetColorizer(self): - """Reset the syntax colorizer. - - For example, this is called after filename changes, since the file - type can affect the highlighting. - """ + "Update the color theme" + # Called from self.filename_change_hook and from configdialog.py self._rmcolorizer() self._addcolorizer() - - def update_colors(self): - """Update the color theme. - - For example, this is called after highlighting config changes. - """ EditorWindow.color_config(self.text) - self.ResetColorizer() - if self.code_context is not None: self.code_context.update_highlight_colors() diff --git a/Lib/idlelib/pyshell.py b/Lib/idlelib/pyshell.py index dff70200383452..fe5dc861c04162 100755 --- a/Lib/idlelib/pyshell.py +++ b/Lib/idlelib/pyshell.py @@ -949,12 +949,12 @@ def __init__(self, flist=None): def ResetFont(self): super().ResetFont() - # Update the sidebar widget, since its width affects - # the width of the text widget. - self.shell_sidebar.update_font() - def update_colors(self): - super().update_colors() + if self.shell_sidebar is not None: + self.shell_sidebar.update_font() + + def ResetColorizer(self): + super().ResetColorizer() theme = idleConf.CurrentTheme() tag_colors = { @@ -966,7 +966,6 @@ def update_colors(self): for tag, tag_colors_config in tag_colors.items(): self.text.tag_configure(tag, **tag_colors_config) - # During __init__, update_colors() is called before the sidebar is created. if self.shell_sidebar is not None: self.shell_sidebar.update_colors() From cc709ab319a2c8ac5cfb80c66b48e718534204b6 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Mon, 26 Apr 2021 09:04:42 +0300 Subject: [PATCH 59/62] wrap some overly long lines of code --- Lib/idlelib/editor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index 2362286fe71923..8b544407da2e0d 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -1309,7 +1309,8 @@ def smart_backspace_event(self, event): text.undo_block_start() text.delete("insert-%dc" % ncharsdeleted, "insert") if have < want: - text.insert("insert", ' ' * (want - have), self.user_input_insert_tags) + text.insert("insert", ' ' * (want - have), + self.user_input_insert_tags) text.undo_block_stop() return "break" @@ -1373,7 +1374,8 @@ def newline_and_indent_event(self, event): if i == n: # The cursor is in or at leading indentation in a continuation # line; just inject an empty line at the start. - text.insert("insert linestart", '\n', self.user_input_insert_tags) + text.insert("insert linestart", '\n', + self.user_input_insert_tags) return "break" indent = line[:i] @@ -1440,7 +1442,8 @@ def newline_and_indent_event(self, event): # beyond leftmost =; else to beyond first chunk of # non-whitespace on initial line. if y.get_num_lines_in_stmt() > 1: - text.insert("insert", indent, self.user_input_insert_tags) + text.insert("insert", indent, + self.user_input_insert_tags) else: self.reindent_to(y.compute_backslash_indent()) else: @@ -1498,7 +1501,8 @@ def reindent_to(self, column): if text.compare("insert linestart", "!=", "insert"): text.delete("insert linestart", "insert") if column: - text.insert("insert", self._make_blanks(column), self.user_input_insert_tags) + text.insert("insert", self._make_blanks(column), + self.user_input_insert_tags) text.undo_block_stop() # Guess indentwidth from text content. From c4e79f3bf864c88366e3047d4ba7fc5e2fd28b3c Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Mon, 26 Apr 2021 22:59:25 -0400 Subject: [PATCH 60/62] Print saved debug info (can't print while shell exists) --- Lib/idlelib/idle_test/test_sidebar.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index a2129b4686a9b5..fe7631581f4b93 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -427,6 +427,7 @@ def cleanup_use_subprocess(): cls.flist = flist = PyShellFileList(root) macosx.setupApp(root, flist) root.update_idletasks() + cls.savetext = [] @classmethod def tearDownClass(cls): @@ -438,6 +439,11 @@ def tearDownClass(cls): cls.root.update_idletasks() cls.root.destroy() cls.root = None + for item in cls.savetext: + print('***') + for line in item.splitlines(): + print(line) + print('***') @classmethod def init_shell(cls): @@ -548,6 +554,7 @@ def test_single_empty_input(self): @test_coroutine def test_single_line_command(self): self.do_input('1\n') + self.savetext.append(self.shell.text.get(1.0, 'end-1c')) yield self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) @@ -560,6 +567,7 @@ def test_multi_line_command(self): ''')) yield + self.savetext.append(self.shell.text.get(1.0, 'end-1c')) self.assert_sidebar_lines_end_with([ '>>>', '...', From 42886dc6abad53e0d1ab1ecc7f87fc39af97c78d Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Tue, 27 Apr 2021 10:56:46 +0300 Subject: [PATCH 61/62] try resetting sys.stdout --- Lib/idlelib/idle_test/test_sidebar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index fe7631581f4b93..ef84cc668f1528 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -454,6 +454,7 @@ def init_shell(cls): @classmethod def reset_shell(cls): + sys.stdout = cls.shell.stdout cls.shell.per.bottom.delete(f'{cls.n_preface_lines+1}.0', 'end-1c') cls.shell.shell_sidebar.update_sidebar() cls.root.update() @@ -553,6 +554,7 @@ def test_single_empty_input(self): @test_coroutine def test_single_line_command(self): + sys.stdout = self.shell.stdout self.do_input('1\n') self.savetext.append(self.shell.text.get(1.0, 'end-1c')) yield From e292d62cbbb727dcd91c009ef76c35f357c3d060 Mon Sep 17 00:00:00 2001 From: Tal Einat <532281+taleinat@users.noreply.github.com> Date: Tue, 27 Apr 2021 12:29:09 +0300 Subject: [PATCH 62/62] cleanup --- Lib/idlelib/idle_test/test_sidebar.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Lib/idlelib/idle_test/test_sidebar.py b/Lib/idlelib/idle_test/test_sidebar.py index ef84cc668f1528..6ba10240c7f662 100644 --- a/Lib/idlelib/idle_test/test_sidebar.py +++ b/Lib/idlelib/idle_test/test_sidebar.py @@ -427,7 +427,6 @@ def cleanup_use_subprocess(): cls.flist = flist = PyShellFileList(root) macosx.setupApp(root, flist) root.update_idletasks() - cls.savetext = [] @classmethod def tearDownClass(cls): @@ -439,11 +438,6 @@ def tearDownClass(cls): cls.root.update_idletasks() cls.root.destroy() cls.root = None - for item in cls.savetext: - print('***') - for line in item.splitlines(): - print(line) - print('***') @classmethod def init_shell(cls): @@ -554,9 +548,7 @@ def test_single_empty_input(self): @test_coroutine def test_single_line_command(self): - sys.stdout = self.shell.stdout self.do_input('1\n') - self.savetext.append(self.shell.text.get(1.0, 'end-1c')) yield self.assert_sidebar_lines_end_with(['>>>', None, '>>>']) @@ -569,7 +561,6 @@ def test_multi_line_command(self): ''')) yield - self.savetext.append(self.shell.text.get(1.0, 'end-1c')) self.assert_sidebar_lines_end_with([ '>>>', '...',