summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>2019-07-17 08:44:55 (GMT)
committerGitHub <noreply@github.com>2019-07-17 08:44:55 (GMT)
commit6cf7c45cb5691b75d9a774c904df02a4f8bfcd04 (patch)
tree5dab274be23a99ab40deae65a997f31df8ed40e5 /Lib
parent35d36dacd410af96d794277c5ae6986d95d50e5a (diff)
downloadcpython-6cf7c45cb5691b75d9a774c904df02a4f8bfcd04.zip
cpython-6cf7c45cb5691b75d9a774c904df02a4f8bfcd04.tar.gz
cpython-6cf7c45cb5691b75d9a774c904df02a4f8bfcd04.tar.bz2
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 7036e1de3a87d36c7ef41b8a2b44ed6fc4d34be2) Co-authored-by: Tal Einat <taleinat@gmail.com>
Diffstat (limited to 'Lib')
-rw-r--r--Lib/idlelib/codecontext.py71
-rw-r--r--Lib/idlelib/editor.py19
-rw-r--r--Lib/idlelib/idle_test/test_codecontext.py144
-rw-r--r--Lib/idlelib/outwin.py2
4 files changed, 137 insertions, 99 deletions
diff --git a/Lib/idlelib/codecontext.py b/Lib/idlelib/codecontext.py
index 2aed76d..9bd0fa1 100644
--- a/Lib/idlelib/codecontext.py
+++ b/Lib/idlelib/codecontext.py
@@ -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)
-
- 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)
+ self.t1 = self.text.after(self.UPDATEINTERVAL, self.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()
diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py
index 9b5364f..b972e3d 100644
--- a/Lib/idlelib/editor.py
+++ b/Lib/idlelib/editor.py
@@ -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."
diff --git a/Lib/idlelib/idle_test/test_codecontext.py b/Lib/idlelib/idle_test/test_codecontext.py
index 6c68935..05d3209 100644
--- a/Lib/idlelib/idle_test/test_codecontext.py
+++ b/Lib/idlelib/idle_test/test_codecontext.py
@@ -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):
diff --git a/Lib/idlelib/outwin.py b/Lib/idlelib/outwin.py
index ecc53ef..38c59bd 100644
--- a/Lib/idlelib/outwin.py
+++ b/Lib/idlelib/outwin.py
@@ -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)