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:
parent
ba3c89f42e
commit
bb79ab84c2
|
@ -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()
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue