From bacd40bf3d4ee858886351555b713cc8129f4b12 Mon Sep 17 00:00:00 2001 From: Louie Lu Date: Thu, 18 May 2017 13:54:56 +0800 Subject: [PATCH] bpo-23667: IDLE: Add trim extension to trim trailing whitespace and blankline on save --- Lib/idlelib/config-extensions.def | 4 +++ Lib/idlelib/editor.py | 1 + Lib/idlelib/idle_test/mock_tk.py | 4 +++ Lib/idlelib/idle_test/test_trim.py | 53 ++++++++++++++++++++++++++++++ Lib/idlelib/iomenu.py | 13 ++++++++ Lib/idlelib/trim.py | 40 ++++++++++++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 Lib/idlelib/idle_test/test_trim.py create mode 100644 Lib/idlelib/trim.py diff --git a/Lib/idlelib/config-extensions.def b/Lib/idlelib/config-extensions.def index a24b8c9316ba07..648c408b2ea1e7 100644 --- a/Lib/idlelib/config-extensions.def +++ b/Lib/idlelib/config-extensions.def @@ -85,6 +85,10 @@ enable=True enable_shell=False enable_editor=True +[TrimExtension] +enable=True +enable_trim_on_save=True + [ScriptBinding] enable=True enable_shell=False diff --git a/Lib/idlelib/editor.py b/Lib/idlelib/editor.py index ab4f1a37c168c1..63af983decd4f3 100644 --- a/Lib/idlelib/editor.py +++ b/Lib/idlelib/editor.py @@ -976,6 +976,7 @@ def get_standard_extension_names(self): 'ParenMatch': 'parenmatch', 'RstripExtension': 'rstrip', 'ScriptBinding': 'runscript', + 'TrimExtension': 'trim', 'ZoomHeight': 'zoomheight', } diff --git a/Lib/idlelib/idle_test/mock_tk.py b/Lib/idlelib/idle_test/mock_tk.py index 6e351297d75db9..6e6c3491b51bde 100644 --- a/Lib/idlelib/idle_test/mock_tk.py +++ b/Lib/idlelib/idle_test/mock_tk.py @@ -140,6 +140,10 @@ def _decode(self, index, endflag=0): return lastline, len(self.data[lastline]) - 1 elif index == 'end': return self._endex(endflag) + elif index.startswith('end-'): + a, b = index.split() + a = int(a[4: -1]) + index = '%d.0' % (a + 1) line, char = index.split('.') line = int(line) diff --git a/Lib/idlelib/idle_test/test_trim.py b/Lib/idlelib/idle_test/test_trim.py new file mode 100644 index 00000000000000..5405b5b6a207e6 --- /dev/null +++ b/Lib/idlelib/idle_test/test_trim.py @@ -0,0 +1,53 @@ +import unittest +import idlelib.trim as trim +from test.support import requires +requires('gui') +from idlelib.editor import EditorWindow as Editor +from tkinter import Tk + + +class trimTest(unittest.TestCase): + + def test_trim_line(self): + editor = Editor(root=Tk()) + text = editor.text + do_trim = trim.TrimExtension(editor).do_trim + + do_trim() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' ') + do_trim() + self.assertEqual(text.get('1.0', 'insert'), '') + text.insert('1.0', ' \n') + do_trim() + self.assertEqual(text.get('1.0', 'insert'), '') + + def test_trim_multiple(self): + editor = Editor(root=Tk()) + text = editor.text + do_trim = trim.TrimExtension(editor).do_trim + + original = ( + "Line with an ending tab\t\n" + "Line ending in 5 spaces \n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space \n" + " \n" + "\n" + " \n" + "\n") + stripped = ( + "Line with an ending tab\n" + "Line ending in 5 spaces\n" + "Linewithnospaces\n" + " indented line\n" + " indented line with trailing space\n") + + text.insert('1.0', original) + do_trim() + self.assertEqual(text.get('1.0', 'insert'), stripped) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=False) diff --git a/Lib/idlelib/iomenu.py b/Lib/idlelib/iomenu.py index 3414c7b3aff4b3..dc00a2721595b1 100644 --- a/Lib/idlelib/iomenu.py +++ b/Lib/idlelib/iomenu.py @@ -109,6 +109,16 @@ def coding_spec(data): return name +def strip_trailing_whitespace_and_blankline(f): + def _strip(self, event, *args, **kwargs): + if idleConf.GetOption('extensions', 'TrimExtension', + 'enable_trim_on_save', + type='bool', default=False): + self.editwin.text.event_generate('<>') + return f(self, event, *args, **kwargs) + return _strip + + class IOBinding: # One instance per editor Window so methods know which to save, close. # Open returns focus to self.editwin if aborted. @@ -340,6 +350,7 @@ def maybesave(self): self.text.focus_set() return reply + @strip_trailing_whitespace_and_blankline def save(self, event): if not self.filename: self.save_as(event) @@ -353,6 +364,7 @@ def save(self, event): self.text.focus_set() return "break" + @strip_trailing_whitespace_and_blankline def save_as(self, event): filename = self.asksavefile() if filename: @@ -367,6 +379,7 @@ def save_as(self, event): self.updaterecentfileslist(filename) return "break" + @strip_trailing_whitespace_and_blankline def save_a_copy(self, event): filename = self.asksavefile() if filename: diff --git a/Lib/idlelib/trim.py b/Lib/idlelib/trim.py new file mode 100644 index 00000000000000..324db148052bf5 --- /dev/null +++ b/Lib/idlelib/trim.py @@ -0,0 +1,40 @@ +'Provides "Trim trailing whitespace and blank line" option' + + +class TrimExtension: + + def __init__(self, editwin): + self.editwin = editwin + self.editwin.text.bind('<>', self.do_trim) + + def do_trim(self, event=None): + text = self.editwin.text + undo = self.editwin.undo + + undo.undo_block_start() + + last = 0 + end_line = int(float(text.index('end'))) + for cur in range(1, end_line): + txt = text.get('%i.0' % cur, '%i.end' % cur) + raw = len(txt) + cut = len(txt.rstrip()) + + # Get the last non-blank line of code + if cut: + last = cur + + # Since text.delete() marks file as changed, even if not, + # only call it when needed to actually delete something. + if cut < raw: + text.delete('%i.%i' % (cur, cut), '%i.end' % cur) + + # Trim trailing blank line + text.delete('end-%ic linestart' % (end_line - last - 1), 'end') + + undo.undo_block_stop() + + +if __name__ == "__main__": + import unittest + unittest.main('idlelib.idle_test.test_trim', verbosity=2, exit=False)