diff -r 39c67beb483a Doc/library/idle.rst --- a/Doc/library/idle.rst Fri Aug 15 01:23:02 2014 -0400 +++ b/Doc/library/idle.rst Sat Aug 16 11:50:25 2014 +0530 @@ -235,6 +235,10 @@ Open a pane at the top of the edit window which shows the block context of the section of code which is scrolling off the top of the window. +Line Numbers (Editor Window only) + Open a column to the left of the edit window which shows the linenumber + of each line of text. + Windows menu (Shell and Editor) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -550,3 +554,4 @@ * CodeContext +* LineNumber diff -r 39c67beb483a Lib/idlelib/EditorWindow.py --- a/Lib/idlelib/EditorWindow.py Fri Aug 15 01:23:02 2014 -0400 +++ b/Lib/idlelib/EditorWindow.py Sat Aug 16 11:50:25 2014 +0530 @@ -805,6 +805,7 @@ idleConf.GetOption('main','EditorWindow','font-size', type='int'), fontWeight)) + self.text.event_generate('<>') def RemoveKeybindings(self): "Remove the keybindings before they are changed." diff -r 39c67beb483a Lib/idlelib/LineNumber.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/LineNumber.py Sat Aug 16 11:50:25 2014 +0530 @@ -0,0 +1,171 @@ +"""Linenumbering implementation for IDLE as an extension. +Includes BaseSideBar which can be extended for other sidebar based extensions +""" +import tkinter as tk +from idlelib.Delegator import Delegator +from idlelib.configHandler import idleConf + +DISABLED = False +ENABLED = True + +get_end = lambda text: int(float(text.index('end-1c'))) + + +class BaseSideBar: + """ + The base class for extensions which require a sidebar. + """ + def __init__(self, editwin): + self.editwin = editwin + self.text = editwin.text + self.text.bind('<>', self.update_sidebar_text_font) + self.parent = self.text.nametowidget(self.text.winfo_parent()) + self.sidebar_text = tk.Text(self.parent, width=1, wrap=tk.NONE) + self.sidebar_text.config(state=tk.DISABLED) + self.text['yscrollcommand'] = self.vbar_set + self.sidebar_text['yscrollcommand'] = self.vbar_set + + def update_sidebar_text_font(self, event=''): + """ + Implement in subclass to update font config values of sidebar_text + when font config values of editwin.text changes + """ + + def show_sidebar(self, side=tk.LEFT): + """ + side - Valid values are tk.LEFT, tk.RIGHT, tk.TOP, tk.BOTTOM + """ + try: + self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y, before=self.text) + except tk.TclError: + self.sidebar_text.pack(side=tk.LEFT, fill=tk.Y) + self.state = ENABLED + + def hide_sidebar(self): + self.sidebar_text.pack_forget() + self.state = DISABLED + + def vbar_set(self, *args, **kwargs): + """Redirect scrollbar's set command to editwin.text and sidebar_text + """ + self.editwin.vbar.set(*args) + self.sidebar_text.yview_moveto(args[0]) + self.text.yview_moveto(args[0]) + + def redirect_event(self, event, event_name): + """Set focus to editwin.text and redirect 'event' to editwin.text. + """ + self.text.focus_set() + if event_name == '': + self.text.event_generate(event_name, x=event.x, y=event.y, + delta=event.delta) + else: + self.text.event_generate(event_name, x=event.x, y=event.y) + + +class EndLineDelegator(Delegator): + """Generate callbacks with the current end line number after + insert or delete operations""" + def __init__(self, changed_callback, end=1): + """ + changed_callback - Callable, will be called after insert + or delete operations with the current + end line number. + end - int, inital value of the end line number""" + Delegator.__init__(self) + self.changed_callback = changed_callback + self.changed_callback(end) + + def insert(self, index, chars, tags=None): + self.delegate.insert(index, chars, tags) + self.changed_callback(get_end(self.delegate)) + + def delete(self, index1, index2=None): + self.delegate.delete(index1, index2) + self.changed_callback(get_end(self.delegate)) + + +class LineNumber(BaseSideBar): + + menudefs = [ + ('options', [ + ("Line Numbers", "<>"), + ]) + ] + + def __init__(self, editwin): + BaseSideBar.__init__(self, editwin) + self.prev_end = 1 + end = get_end(self.text) + self.update_sidebar_text_font() + self.sidebar_text.config(state=tk.NORMAL) + self.sidebar_text.insert('insert', '1', 'linenumber') + self.sidebar_text.config(state=tk.DISABLED) + for event_name in ('', '', '', + '', '', '', + '', '', + '', '', + '', '', '', + '<2>', '<3>', '', + ''): + self.sidebar_text.bind(event_name, + lambda event, event_name=event_name: + self.redirect_event(event, event_name)) + self.end_line_delegator = EndLineDelegator(self.update_sidebar_text, + end) + self.editwin.per.insertfilter(self.end_line_delegator) + self.state = idleConf.GetOption('extensions', 'LineNumber', 'visible', + type='bool') + # Note : We invert state here, and call toggle_linenumbering_event + # to get our desired state + self.state = not self.state + self.toggle_linenumbering_event('') + + def update_sidebar_text_font(self, event=''): + """Update the font of sidebar_text when font of editwin.font + changes + """ + bg = idleConf.GetOption('extensions', 'LineNumber', 'bgcolor') + fg = idleConf.GetOption('extensions', 'LineNumber', 'fgcolor') + self.sidebar_text.tag_config('linenumber', justify=tk.RIGHT) + config = {'fg': fg, 'bg': bg, 'font': self.text['font'], + 'relief': tk.FLAT, 'selectforeground': fg, + 'selectbackground': bg} + if tk.TkVersion >= 8.5: + config['inactiveselectbackground'] = bg + self.sidebar_text.config(**config) + # The below lines below are required to allow tk to "catch up" with + # changes in font to the main text widget + # + sidebar_text = self.sidebar_text.get('1.0', 'end') + self.sidebar_text.delete('1.0', 'end') + self.sidebar_text.insert('1.0', sidebar_text) + self.text.update_idletasks() + self.sidebar_text.update_idletasks() + + def toggle_linenumbering_event(self, event): + self.show_sidebar() if self.state == DISABLED else self.hide_sidebar() + + 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 + width_difference = len(str(end)) - len(str(self.prev_end)) + self.sidebar_text['width'] += width_difference + self.sidebar_text.config(state=tk.NORMAL) + if end > self.prev_end: + for i in range(self.prev_end + 1, end + 1): + self.sidebar_text.insert('{}.0'.format(i), '\n{}'.format(i), + 'linenumber') + else: + self.sidebar_text.delete('{}.0'.format(end+1), 'end') + self.sidebar_text.config(state=tk.DISABLED) + self.prev_end = end + +if __name__ == '__main__': + from unittest import main + main('idlelib.idle_test.test_linenumber', verbosity=2) diff -r 39c67beb483a Lib/idlelib/config-extensions.def --- a/Lib/idlelib/config-extensions.def Fri Aug 15 01:23:02 2014 -0400 +++ b/Lib/idlelib/config-extensions.def Sat Aug 16 11:50:25 2014 +0530 @@ -94,3 +94,12 @@ enable_shell=0 enable_editor=1 +[LineNumber] +enable=1 +enable_editor=1 +# Should LineNumbering be visible on EditorWindow startup? +visible=0 +bgcolor=Gray +fgcolor=Black +[LineNumber_bindings] +toggle-linenumbering= diff -r 39c67beb483a Lib/idlelib/help.txt --- a/Lib/idlelib/help.txt Fri Aug 15 01:23:02 2014 -0400 +++ b/Lib/idlelib/help.txt Sat Aug 16 11:50:25 2014 +0530 @@ -366,3 +366,4 @@ ParenMatch AutoComplete CodeContext + LineNumber diff -r 39c67beb483a Lib/idlelib/idle_test/test_linenumber.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_linenumber.py Sat Aug 16 11:50:25 2014 +0530 @@ -0,0 +1,138 @@ +"""Unittest for idlelib.LineNumber""" +import unittest +from test.support import requires +import tkinter as tk +from idlelib.Percolator import Percolator +from idlelib.LineNumber import LineNumber + + +class Dummy_editwin: + def __init__(self, text): + self.text = text + self.per = Percolator(text) + + +class LineNumberTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + requires('gui') + cls.root = tk.Tk() + cls.text = tk.Text(cls.root) + cls.editwin = Dummy_editwin(cls.text) + cls.editwin.vbar = tk.Scrollbar(cls.root) + + @classmethod + def tearDownClass(cls): + cls.editwin.per.close() + cls.text.destroy() + cls.root.destroy() + del cls.text, cls.root + + def setUp(self): + self.linenumber = LineNumber(self.editwin) + + def tearDown(self): + self.text.delete('1.0', 'end') + + def test_init_empty(self): + get = self.linenumber.sidebar_text.get + self.assertEqual(get('1.0', 'end'), '1\n') + + def test_init_not_empty(self): + self.text.insert('insert', 'foo bar\n'*3) + linenumber = LineNumber(self.editwin) + get = linenumber.sidebar_text.get + self.assertEqual(get('1.0', 'end'), '1\n2\n3\n4\n') + + def test_toggle_linenumbering(self): + ENABLED = True + DISABLED = False + + self.linenumber.state = ENABLED + self.linenumber.toggle_linenumbering_event('') + self.assertEqual(self.linenumber.state, DISABLED) + + self.linenumber.state = DISABLED + self.linenumber.toggle_linenumbering_event('') + self.assertEqual(self.linenumber.state, ENABLED) + + def test_insert(self): + text = self.editwin.text + get = self.linenumber.sidebar_text.get + config = self.linenumber.sidebar_text.config + equal = self.assertEqual + + text.insert('insert', 'foobar') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', '\nfoo') + equal(get('1.0', 'end'), '1\n2\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', 'hello\n'*2) + equal(get('1.0', 'end'), '1\n2\n3\n4\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', '\nworld') + equal(get('1.0', 'end'), '1\n2\n3\n4\n5\n') + equal(config()['state'][-1], tk.DISABLED) + + def test_delete(self): + text = self.editwin.text + get = self.linenumber.sidebar_text.get + config = self.linenumber.sidebar_text.config + equal = self.assertEqual + + text.insert('insert', 'foobar') + text.delete('1.1', '1.3') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.insert('insert', 'foo\n'*2) + text.delete('3.1') + text.delete('2.0', '2.end') + equal(get('1.0', 'end'), '1\n2\n') + equal(config()['state'][-1], tk.DISABLED) + + text.delete('1.3', 'end') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + text.delete('1.0', 'end') + equal(get('1.0', 'end'), '1\n') + equal(config()['state'][-1], tk.DISABLED) + + def test_sidebar_text_width(self): + """ + Test that linenumber text widget is always at the minimum + width + """ + def get_width(): + return self.linenumber.sidebar_text.config()['width'][-1] + text = self.text + equal = self.assertEqual + + equal(get_width(), 1) + + text.insert('insert', 'foo') + equal(get_width(), 1) + + text.insert('insert', 'foo\n'*10) + equal(get_width(), 2) + + text.insert('insert', 'foo\n'*100) + equal(get_width(), 3) + + text.delete('50.0', 'end') + equal(get_width(), 2) + + text.delete('5.0', 'end') + equal(get_width(), 1) + + text.delete('1.0', 'end') + equal(get_width(), 1) + +if __name__ == '__main__': + unittest.main(verbosity=2)