bpo-37530: simplify, optimize and clean up IDLE code context (GH-14675)

* Only create CodeContext instances for "real" editors windows, but
  not e.g. shell or output windows.
* Remove configuration update Tk event fired every second, by having
  the editor window ask its code context widget to update when
  necessary, i.e. upon font or highlighting updates.
* When code context isn't being shown, avoid having a Tk event fired
  every 100ms to check whether the code context needs to be updated.
* Use the editor window's getlineno() method where applicable.
* Update font of the code context widget before the main text widget
(cherry picked from commit 7036e1de3a)

Co-authored-by: Tal Einat <taleinat@gmail.com>
This commit is contained in:
Miss Islington (bot) 2019-07-17 01:44:18 -07:00 committed by GitHub
parent ba3c89f42e
commit bb79ab84c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 139 additions and 98 deletions

View File

@ -19,8 +19,6 @@ from idlelib.config import idleConf
BLOCKOPENERS = {"class", "def", "elif", "else", "except", "finally", "for",
"if", "try", "while", "with", "async"}
UPDATEINTERVAL = 100 # millisec
CONFIGUPDATEINTERVAL = 1000 # millisec
def get_spaces_firstword(codeline, c=re.compile(r"^(\s*)(\w*)")):
@ -44,13 +42,13 @@ def get_line_info(codeline):
class CodeContext:
"Display block context above the edit window."
UPDATEINTERVAL = 100 # millisec
def __init__(self, editwin):
"""Initialize settings for context block.
editwin is the Editor window for the context block.
self.text is the editor window text widget.
self.textfont is the editor window font.
self.context displays the code context text above the editor text.
Initially None, it is toggled via <<toggle-code-context>>.
@ -65,29 +63,26 @@ class CodeContext:
"""
self.editwin = editwin
self.text = editwin.text
self.textfont = self.text["font"]
self.contextcolors = CodeContext.colors
self.context = None
self.topvisible = 1
self.info = [(0, -1, "", False)]
# Start two update cycles, one for context lines, one for font changes.
self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)
self.t1 = None
@classmethod
def reload(cls):
"Load class variables from config."
cls.context_depth = idleConf.GetOption("extensions", "CodeContext",
"maxlines", type="int", default=15)
cls.colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
"maxlines", type="int",
default=15)
def __del__(self):
"Cancel scheduled events."
try:
self.text.after_cancel(self.t1)
self.text.after_cancel(self.t2)
except:
pass
if self.t1 is not None:
try:
self.text.after_cancel(self.t1)
except tkinter.TclError:
pass
self.t1 = None
def toggle_code_context_event(self, event=None):
"""Toggle code context display.
@ -96,7 +91,7 @@ class CodeContext:
window text (toggle on). If it does exist, destroy it (toggle off).
Return 'break' to complete the processing of the binding.
"""
if not self.context:
if self.context is None:
# Calculate the border width and horizontal padding required to
# align the context with the text in the main Text widget.
#
@ -111,21 +106,23 @@ class CodeContext:
padx += widget.tk.getint(widget.cget('padx'))
border += widget.tk.getint(widget.cget('border'))
self.context = tkinter.Text(
self.editwin.top, font=self.textfont,
bg=self.contextcolors['background'],
fg=self.contextcolors['foreground'],
height=1,
width=1, # Don't request more than we get.
padx=padx, border=border, relief=SUNKEN, state='disabled')
self.editwin.top, font=self.text['font'],
height=1,
width=1, # Don't request more than we get.
padx=padx, border=border, relief=SUNKEN, state='disabled')
self.update_highlight_colors()
self.context.bind('<ButtonRelease-1>', self.jumptoline)
# Pack the context widget before and above the text_frame widget,
# thus ensuring that it will appear directly above text_frame.
self.context.pack(side=TOP, fill=X, expand=False,
before=self.editwin.text_frame)
before=self.editwin.text_frame)
menu_status = 'Hide'
self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
else:
self.context.destroy()
self.context = None
self.text.after_cancel(self.t1)
self.t1 = None
menu_status = 'Show'
self.editwin.update_menu_label(menu='options', index='* Code Context',
label=f'{menu_status} Code Context')
@ -169,7 +166,7 @@ class CodeContext:
be retrieved and the context area will be updated with the code,
up to the number of maxlines.
"""
new_topvisible = int(self.text.index("@0,0").split('.')[0])
new_topvisible = self.editwin.getlineno("@0,0")
if self.topvisible == new_topvisible: # Haven't scrolled.
return
if self.topvisible < new_topvisible: # Scroll down.
@ -217,21 +214,19 @@ class CodeContext:
def timer_event(self):
"Event on editor text widget triggered every UPDATEINTERVAL ms."
if self.context:
if self.context is not None:
self.update_code_context()
self.t1 = self.text.after(UPDATEINTERVAL, self.timer_event)
self.t1 = self.text.after(self.UPDATEINTERVAL, self.timer_event)
def config_timer_event(self):
"Event on editor text widget triggered every CONFIGUPDATEINTERVAL ms."
newtextfont = self.text["font"]
if (self.context and (newtextfont != self.textfont or
CodeContext.colors != self.contextcolors)):
self.textfont = newtextfont
self.contextcolors = CodeContext.colors
self.context["font"] = self.textfont
self.context['background'] = self.contextcolors['background']
self.context['foreground'] = self.contextcolors['foreground']
self.t2 = self.text.after(CONFIGUPDATEINTERVAL, self.config_timer_event)
def update_font(self, font):
if self.context is not None:
self.context['font'] = font
def update_highlight_colors(self):
if self.context is not None:
colors = idleConf.GetHighlight(idleConf.CurrentTheme(), 'context')
self.context['background'] = colors['background']
self.context['foreground'] = colors['foreground']
CodeContext.reload()

View File

@ -62,6 +62,8 @@ class EditorWindow(object):
filesystemencoding = sys.getfilesystemencoding() # for file names
help_url = None
allow_codecontext = True
def __init__(self, flist=None, filename=None, key=None, root=None):
# Delay import: runscript imports pyshell imports EditorWindow.
from idlelib.runscript import ScriptBinding
@ -247,6 +249,7 @@ class EditorWindow(object):
self.good_load = False
self.set_indentation_params(False)
self.color = None # initialized below in self.ResetColorizer
self.codecontext = None
if filename:
if os.path.exists(filename) and not os.path.isdir(filename):
if io.loadfile(filename):
@ -312,8 +315,10 @@ class EditorWindow(object):
text.bind("<<refresh-calltip>>", ctip.refresh_calltip_event)
text.bind("<<force-open-calltip>>", ctip.force_open_calltip_event)
text.bind("<<zoom-height>>", self.ZoomHeight(self).zoom_height_event)
text.bind("<<toggle-code-context>>",
self.CodeContext(self).toggle_code_context_event)
if self.allow_codecontext:
self.codecontext = self.CodeContext(self)
text.bind("<<toggle-code-context>>",
self.codecontext.toggle_code_context_event)
def _filename_to_unicode(self, filename):
"""Return filename as BMP unicode so displayable in Tk."""
@ -773,6 +778,9 @@ class EditorWindow(object):
self._addcolorizer()
EditorWindow.color_config(self.text)
if self.codecontext is not None:
self.codecontext.update_highlight_colors()
IDENTCHARS = string.ascii_letters + string.digits + "_"
def colorize_syntax_error(self, text, pos):
@ -790,7 +798,12 @@ class EditorWindow(object):
"Update the text widgets' font if it is changed"
# Called from configdialog.py
self.text['font'] = idleConf.GetFont(self.root, 'main','EditorWindow')
new_font = idleConf.GetFont(self.root, 'main', 'EditorWindow')
# Update the code context widget first, since its height affects
# the height of the text widget. This avoids double re-rendering.
if self.codecontext is not None:
self.codecontext.update_font(new_font)
self.text['font'] = new_font
def RemoveKeybindings(self):
"Remove the keybindings before they are changed."

View File

@ -2,6 +2,7 @@
from idlelib import codecontext
import unittest
import unittest.mock
from test.support import requires
from tkinter import Tk, Frame, Text, TclError
@ -42,6 +43,9 @@ class DummyEditwin:
self.text = text
self.label = ''
def getlineno(self, index):
return int(float(self.text.index(index)))
def update_menu_label(self, **kwargs):
self.label = kwargs['label']
@ -75,6 +79,18 @@ class CodeContextTest(unittest.TestCase):
self.text.yview(0)
self.cc = codecontext.CodeContext(self.editor)
self.highlight_cfg = {"background": '#abcdef',
"foreground": '#123456'}
orig_idleConf_GetHighlight = codecontext.idleConf.GetHighlight
def mock_idleconf_GetHighlight(theme, element):
if element == 'context':
return self.highlight_cfg
return orig_idleConf_GetHighlight(theme, element)
patcher = unittest.mock.patch.object(
codecontext.idleConf, 'GetHighlight', mock_idleconf_GetHighlight)
patcher.start()
self.addCleanup(patcher.stop)
def tearDown(self):
if self.cc.context:
self.cc.context.destroy()
@ -89,30 +105,24 @@ class CodeContextTest(unittest.TestCase):
eq(cc.editwin, ed)
eq(cc.text, ed.text)
eq(cc.textfont, ed.text['font'])
eq(cc.text['font'], ed.text['font'])
self.assertIsNone(cc.context)
eq(cc.info, [(0, -1, '', False)])
eq(cc.topvisible, 1)
eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
eq(self.root.tk.call('after', 'info', self.cc.t2)[1], 'timer')
self.assertIsNone(self.cc.t1)
def test_del(self):
self.cc.__del__()
with self.assertRaises(TclError) as msg:
self.root.tk.call('after', 'info', self.cc.t1)
self.assertIn("doesn't exist", msg)
with self.assertRaises(TclError) as msg:
self.root.tk.call('after', 'info', self.cc.t2)
self.assertIn("doesn't exist", msg)
# For coverage on the except. Have to delete because the
# above Tcl error is caught by after_cancel.
del self.cc.t1, self.cc.t2
def test_del_with_timer(self):
timer = self.cc.t1 = self.text.after(10000, lambda: None)
self.cc.__del__()
with self.assertRaises(TclError) as cm:
self.root.tk.call('after', 'info', timer)
self.assertIn("doesn't exist", str(cm.exception))
def test_reload(self):
codecontext.CodeContext.reload()
self.assertEqual(self.cc.colors, {'background': 'lightgray',
'foreground': '#000000'})
self.assertEqual(self.cc.context_depth, 15)
def test_toggle_code_context_event(self):
@ -127,16 +137,18 @@ class CodeContextTest(unittest.TestCase):
# Toggle on.
eq(toggle(), 'break')
self.assertIsNotNone(cc.context)
eq(cc.context['font'], cc.textfont)
eq(cc.context['fg'], cc.colors['foreground'])
eq(cc.context['bg'], cc.colors['background'])
eq(cc.context['font'], self.text['font'])
eq(cc.context['fg'], self.highlight_cfg['foreground'])
eq(cc.context['bg'], self.highlight_cfg['background'])
eq(cc.context.get('1.0', 'end-1c'), '')
eq(cc.editwin.label, 'Hide Code Context')
eq(self.root.tk.call('after', 'info', self.cc.t1)[1], 'timer')
# Toggle off.
eq(toggle(), 'break')
self.assertIsNone(cc.context)
eq(cc.editwin.label, 'Show Code Context')
self.assertIsNone(self.cc.t1)
def test_get_context(self):
eq = self.assertEqual
@ -227,7 +239,7 @@ class CodeContextTest(unittest.TestCase):
(4, 4, ' def __init__(self, a, b):', 'def')])
eq(cc.topvisible, 5)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def __init__(self, a, b):')
' def __init__(self, a, b):')
# Scroll down to line 11. Last 'def' is removed.
cc.text.yview(11)
@ -239,9 +251,9 @@ class CodeContextTest(unittest.TestCase):
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
# No scroll. No update, even though context_depth changed.
cc.update_code_context()
@ -253,9 +265,9 @@ class CodeContextTest(unittest.TestCase):
(10, 8, ' elif a < b:', 'elif')])
eq(cc.topvisible, 12)
eq(cc.context.get('1.0', 'end-1c'), 'class C1():\n'
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
' def compare(self):\n'
' if a > b:\n'
' elif a < b:')
# Scroll up.
cc.text.yview(5)
@ -276,7 +288,7 @@ class CodeContextTest(unittest.TestCase):
cc.toggle_code_context_event()
# Empty context.
cc.text.yview(f'{2}.0')
cc.text.yview('2.0')
cc.update_code_context()
eq(cc.topvisible, 2)
cc.context.mark_set('insert', '1.5')
@ -284,7 +296,7 @@ class CodeContextTest(unittest.TestCase):
eq(cc.topvisible, 1)
# 4 lines of context showing.
cc.text.yview(f'{12}.0')
cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '3.0')
@ -293,7 +305,7 @@ class CodeContextTest(unittest.TestCase):
# More context lines than limit.
cc.context_depth = 2
cc.text.yview(f'{12}.0')
cc.text.yview('12.0')
cc.update_code_context()
eq(cc.topvisible, 12)
cc.context.mark_set('insert', '1.0')
@ -313,56 +325,72 @@ class CodeContextTest(unittest.TestCase):
self.cc.timer_event()
mock_update.assert_called()
def test_config_timer_event(self):
def test_font(self):
eq = self.assertEqual
cc = self.cc
save_font = cc.text['font']
save_colors = codecontext.CodeContext.colors
test_font = 'FakeFont'
test_font = 'TkFixedFont'
# Ensure code context is not active.
if cc.context is not None:
cc.toggle_code_context_event()
# Nothing breaks or changes with inactive code context.
cc.update_font(test_font)
# Activate code context, but no change to font.
cc.toggle_code_context_event()
eq(cc.context['font'], save_font)
# Call font update with the existing font.
cc.update_font(save_font)
eq(cc.context['font'], save_font)
cc.toggle_code_context_event()
# Change text widget font and activate code context.
cc.text['font'] = test_font
cc.toggle_code_context_event(test_font)
eq(cc.context['font'], test_font)
# Just call the font update.
cc.update_font(save_font)
eq(cc.context['font'], save_font)
cc.text['font'] = save_font
def test_highlight_colors(self):
eq = self.assertEqual
cc = self.cc
save_colors = dict(self.highlight_cfg)
test_colors = {'background': '#222222', 'foreground': '#ffff00'}
# Ensure code context is not active.
if cc.context:
cc.toggle_code_context_event()
# Nothing updates on inactive code context.
cc.text['font'] = test_font
codecontext.CodeContext.colors = test_colors
cc.config_timer_event()
eq(cc.textfont, save_font)
eq(cc.contextcolors, save_colors)
# Nothing breaks with inactive code context.
cc.update_highlight_colors()
# Activate code context, but no change to font or color.
# Activate code context, but no change to colors.
cc.toggle_code_context_event()
cc.text['font'] = save_font
codecontext.CodeContext.colors = save_colors
cc.config_timer_event()
eq(cc.textfont, save_font)
eq(cc.contextcolors, save_colors)
eq(cc.context['font'], save_font)
eq(cc.context['background'], save_colors['background'])
eq(cc.context['foreground'], save_colors['foreground'])
# Active code context, change font.
cc.text['font'] = test_font
cc.config_timer_event()
eq(cc.textfont, test_font)
eq(cc.contextcolors, save_colors)
eq(cc.context['font'], test_font)
# Call colors update, but no change to font.
cc.update_highlight_colors()
eq(cc.context['background'], save_colors['background'])
eq(cc.context['foreground'], save_colors['foreground'])
cc.toggle_code_context_event()
# Active code context, change color.
cc.text['font'] = save_font
codecontext.CodeContext.colors = test_colors
cc.config_timer_event()
eq(cc.textfont, save_font)
eq(cc.contextcolors, test_colors)
eq(cc.context['font'], save_font)
# Change colors and activate code context.
self.highlight_cfg = test_colors
cc.toggle_code_context_event()
eq(cc.context['background'], test_colors['background'])
eq(cc.context['foreground'], test_colors['foreground'])
codecontext.CodeContext.colors = save_colors
cc.config_timer_event()
# Change colors and call highlight colors update.
self.highlight_cfg = save_colors
cc.update_highlight_colors()
eq(cc.context['background'], save_colors['background'])
eq(cc.context['foreground'], save_colors['foreground'])
class HelperFunctionText(unittest.TestCase):

View File

@ -74,6 +74,8 @@ class OutputWindow(EditorWindow):
("Go to file/line", "<<goto-file-line>>", None),
]
allow_codecontext = False
def __init__(self, *args):
EditorWindow.__init__(self, *args)
self.text.bind("<<goto-file-line>>", self.goto_file_line)

View File

@ -0,0 +1,3 @@
Optimize code context to reduce unneeded background activity.
Font and highlight changes now occur along with text changes
instead of after a random delay.