diff -r 8c1ca6720246 Lib/idlelib/FormatParagraph.py --- a/Lib/idlelib/FormatParagraph.py Wed Jul 17 00:57:58 2013 +0200 +++ b/Lib/idlelib/FormatParagraph.py Wed Jul 17 16:52:16 2013 -0400 @@ -1,18 +1,19 @@ -# Extension to format a paragraph +"""Extension to format a paragraph or selection to a max width. -# Does basic, standard text formatting, and also understands Python -# comment blocks. Thus, for editing Python source code, this -# extension is really only suitable for reformatting these comment -# blocks or triple-quoted strings. +Does basic, standard text formatting, and also understands Python +comment blocks. Thus, for editing Python source code, this +extension is really only suitable for reformatting these comment +blocks or triple-quoted strings. -# Known problems with comment reformatting: -# * If there is a selection marked, and the first line of the -# selection is not complete, the block will probably not be detected -# as comments, and will have the normal "text formatting" rules -# applied. -# * If a comment block has leading whitespace that mixes tabs and -# spaces, they will not be considered part of the same block. -# * Fancy comments, like this bulleted list, arent handled :-) +Known problems with comment reformatting: +* If there is a selection marked, and the first line of the + selection is not complete, the block will probably not be detected + as comments, and will have the normal "text formatting" rules + applied. +* If a comment block has leading whitespace that mixes tabs and + spaces, they will not be considered part of the same block. +* Fancy comments, like this bulleted list, arent handled :-) +""" import re from idlelib.configHandler import idleConf @@ -32,13 +33,29 @@ self.editwin = None def format_paragraph_event(self, event): + """Formats paragraph to a max width specified in idleConf. + + If text is selected, format_paragraph_event will start breaking lines + at the max width, starting from the beginning selection. + + If no text is selected, format_paragraph_event uses the current + cursor location to determine the paragraph (lines of text surrounded + by blank lines) and formats it. + """ maxformatwidth = int(idleConf.GetOption('main', 'FormatParagraph', 'paragraph', type='int')) text = self.editwin.text first, last = self.editwin.get_selection_indices() if first and last: data = text.get(first, last) - comment_header = '' + lineno, col = map(int, first.split(".")) + # If selection starts at the beginning of a line, check to see + # if it is a comment + if col == 0: + line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) + comment_header = get_comment_header(line) + else: + comment_header = '' else: first, last, comment_header, data = \ find_paragraph(text, text.index("insert")) @@ -80,6 +97,11 @@ return "break" def find_paragraph(text, mark): + """Returns the start/stop indices enclosing the paragraph that mark is in. + + Also returns the comment format string and paragraph of text between the + start/stop indices. + """ lineno, col = map(int, mark.split(".")) line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): @@ -140,12 +162,25 @@ return "\n".join(new) def is_all_white(line): + """Return True if line consists entirely of whitespace.""" return re.match(r"^\s*$", line) is not None def get_indent(line): + """Return the initial whitespace substring of line.""" return re.match(r"^(\s*)", line).group() def get_comment_header(line): + """Attempts to return string with leading whitespace and '#' from line. + + For example, the string " #" could be returned, then used to format + the rest of the lines in the paragraph with the same indent. + """ m = re.match(r"^(\s*#*)", line) if m is None: return "" return m.group(1) + +if __name__ == "__main__": + from test import support; support.use_resources = ['gui'] + import unittest + unittest.main('idlelib.idle_test.test_formatparagraph', + verbosity=2, exit=False) diff -r 8c1ca6720246 Lib/idlelib/idle_test/mock_idle.py --- a/Lib/idlelib/idle_test/mock_idle.py Wed Jul 17 00:57:58 2013 +0200 +++ b/Lib/idlelib/idle_test/mock_idle.py Wed Jul 17 16:52:16 2013 -0400 @@ -11,10 +11,12 @@ def __init__(self, flist=None, filename=None, key=None, root=None): self.text = Text() self.undo = UndoDelegator() + self.text.undo_block_start = self.undo.undo_block_start + self.text.undo_block_stop = self.undo.undo_block_stop def get_selection_indices(self): - first = self.text.index('1.0') - last = self.text.index('end') + first = self.text.index('sel.first') + last = self.text.index('sel.last') return first, last class UndoDelegator: diff -r 8c1ca6720246 Lib/idlelib/idle_test/mock_tk.py --- a/Lib/idlelib/idle_test/mock_tk.py Wed Jul 17 00:57:58 2013 +0200 +++ b/Lib/idlelib/idle_test/mock_tk.py Wed Jul 17 16:52:16 2013 -0400 @@ -88,10 +88,20 @@ There are just a few Text-only options that affect text behavior. ''' self.data = ['', '\n'] + # Tags must be in the form line.col or None + self.tags = {'sel.first' : None, + 'sel.last' : None, + 'insert' : '1.0' } def index(self, index): "Return string version of index decoded according to current text." - return "%s.%s" % self._decode(index, endflag=1) + try: + return "%s.%s" % self._decode(index, endflag=1) + except TypeError: + # _decode() probably returned None + # Text widget will return '' if index is non-existent, ie + # 'sel.first' when no text is selected + return '' def _decode(self, index, endflag=0): """Return a (line, char) tuple of int indexes into self.data. @@ -107,21 +117,30 @@ * 'line.end', where end='end' (same as above); * 'insert', the positions before terminal \n; * 'end', whose meaning depends on the endflag passed to ._endex. - * 'sel.first' or 'sel.last', where sel is a tag -- not implemented. + * 'sel.first' or 'sel.last', where sel is a tag. """ if isinstance(index, (float, bytes)): index = str(index) try: - index=index.lower() + index = index.lower() except AttributeError: raise TclError('bad text index "%s"' % index) from None lastline = len(self.data) - 1 # same as number of text lines if index == 'insert': - return lastline, len(self.data[lastline]) - 1 + index = self.index(self.tags['insert']) elif index == 'end': return self._endex(endflag) - + elif index == 'sel.first': + if self.tags['sel.first'] is None: + return None + else: + index = self.tags['sel.first'] + elif index == 'sel.last': + if self.tags['sel.last'] is None: + return None + else: + index = self.tags['sel.last'] line, char = index.split('.') line = int(line) @@ -254,6 +273,12 @@ def mark_unset(self, *markNames): "Delete all marks in markNames." + def tag_add(self, tagName, index1, *args): + if index1 is None: + self.tags[tagName] = None + else: + self.tags[tagName] = self.index(index1) + def tag_remove(self, tagName, index1, index2=None): "Remove tag tagName from all characters between index1 and index2." pass diff -r 8c1ca6720246 Lib/idlelib/idle_test/test_formatparagraph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/idlelib/idle_test/test_formatparagraph.py Wed Jul 17 16:52:16 2013 -0400 @@ -0,0 +1,181 @@ +import unittest +from idlelib import FormatParagraph as fp +from idlelib.idle_test.mock_idle import Editor + +class FormatParagraphTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.editor = Editor() + cls.text = cls.editor.text + cls.formatter = fp.FormatParagraph(cls.editor) + + def test_reformat_paragrah(self): + '''tests the reformat_paragraph function without the editor window''' + result = fp.reformat_paragraph("# hello world", 8) + expected = ("# hello\nworld") + self.assertEqual(result, expected) + + def test_format_paragraph_event(self): + test_string = ( + " \"\"\"this is a test of a reformat for a triple " + "quoted string will it reformat to less than 80 " + "characters for me?\"\"\"") + multiline_test_string = ( + " \"\"\"The first line is under the max width.\n" + " The second line's length is way over the max width. It goes " + "on and on until it is over 100 characters long.\n" + " Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + " The fourth line is short like the first line.\n" + " \"\"\"") + multiline_test_comment = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fourth line is short like the first line.") + multiparagraph_test_comment = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "\n" + "# Same thing with the fourth line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fifth line is short like the first line.") + + # Test formatting a long triple quoted string with no selection + # and cursor ('insert') at '1.0' + self.text.tag_add('sel.first', None) + self.text.tag_add('sel.last', None) + self.text.tag_add('insert', '1.0') + self.text.insert('1.0', test_string) + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0', 'end') + # FormatParagraph adds a newline after formatting and the text + # widget also adds a newline character, hence the two newlines + expected = ( + " \"\"\"this is a test of a reformat for a triple quoted " + "string will it\n" + " reformat to less than 80 characters for me?\"\"\"\n\n") + self.assertEqual(result, expected) + + # Test formatting a long triple quoted string with selection from + # 1.15 to 'end' + self.text.delete('1.0', 'end') + self.text.insert('1.0', test_string) + self.text.tag_add('sel.first', '1.15') # 1.15 right before first 'a' + self.text.tag_add('sel.last', 'end') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + expected = ( + " \"\"\"this is a test of a reformat for a triple " + "quoted string will it reformat to\n" + "less than 80 characters for me?\"\"\"\n\n") + self.assertEqual(result, expected) + + # Test formatting a long triple quoted string when multiple lines + # inside a paragraph are selected + self.text.delete('1.0', 'end') + self.text.insert('1.0', multiline_test_string) + self.text.tag_add('sel.first', '2.0') + self.text.tag_add('sel.last', '4.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + # The expected result moves the three quotation marks to the last line + # It would be a good idea to modify FormatParagraph to leave on their + # own line. + expected = ( + " \"\"\"The first line is under the max width.\n" + " The second line's length is way over the max width. It goes " + "on and\n" + " on until it is over 100 characters long. Same thing with the " + "third\n" + " line. It is also way over the max width, but FormatParagraph " + "will\n" + " fix it.\n" + " The fourth line is short like the first line.\n" + " \"\"\"\n") + self.assertEqual(result, expected) + + # Test formatting a comment block when nothing is selected + # and cursor ('insert') at '1.0' + self.text.delete('1.0', 'end') + self.text.insert('1.0', multiline_test_comment) + self.text.tag_add('sel.first', None) + self.text.tag_add('sel.last', None) + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + expected = ( + "# The first line is under the max width. The second line's " + "length is\n" + "# way over the max width. It goes on and on until it is over " + "100\n" + "# characters long. Same thing with the third line. It is also " + "way over\n" + "# the max width, but FormatParagraph will fix it. The fourth " + "line is\n" + "# short like the first line.\n\n") + self.assertEqual(result, expected) + + # Test formatting a comment block when lines 3 and 4 are selected + self.text.delete('1.0', 'end') + self.text.insert('1.0', multiline_test_comment) + self.text.tag_add('sel.first', '3.0') + self.text.tag_add('sel.last', '5.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + expected = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width,\n" + "# but FormatParagraph will fix it. The fourth line is short like " + "the\n" + "# first line.\n\n") + self.assertEqual(result, expected) + + # Test formatting a single long line in a larger comment block when + # using line 2 + self.text.delete('1.0', 'end') + self.text.insert('1.0', multiline_test_comment) + self.text.tag_add('sel.first', '2.0') + self.text.tag_add('sel.last', '3.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + expected = ( + "# The first line is under the max width.\n" + "# The second line's length is way over the max width. It goes on " + "and\n" + "# on until it is over 100 characters long.\n" + "# Same thing with the third line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fourth line is short like the first line.\n") + self.assertEqual(result, expected) + + # Test formatting a comment block with whitespace after to ensure + # FormatParagraph stops formatting when it sees whitespace + self.text.delete('1.0', 'end') + self.text.insert('1.0', multiparagraph_test_comment) + self.text.tag_add('sel.first', None) + self.text.tag_add('sel.last', None) + self.text.tag_add('insert', '1.0') + self.formatter.format_paragraph_event('ParameterDoesNothing') + result = self.text.get('1.0','end') + expected = ( + "# The first line is under the max width. The second line's " + "length is\n" + "# way over the max width. It goes on and on until it is over " + "100\n" + "# characters long.\n" + "\n" + "# Same thing with the fourth line. It is also way over the max " + "width, but FormatParagraph will fix it.\n" + "# The fifth line is short like the first line.\n") + self.assertEqual(result, expected) + + +if __name__ == '__main__': + unittest.main(verbosity=2, exit=2) diff -r 8c1ca6720246 Lib/idlelib/idle_test/test_rstrip.py --- a/Lib/idlelib/idle_test/test_rstrip.py Wed Jul 17 00:57:58 2013 +0200 +++ b/Lib/idlelib/idle_test/test_rstrip.py Wed Jul 17 16:52:16 2013 -0400 @@ -9,6 +9,7 @@ text = editor.text do_rstrip = rs.RstripExtension(editor).do_rstrip + text.tag_add('insert', '1.0 lineend') do_rstrip() self.assertEqual(text.get('1.0', 'insert'), '') text.insert('1.0', ' ') @@ -16,6 +17,7 @@ self.assertEqual(text.get('1.0', 'insert'), '') text.insert('1.0', ' \n') do_rstrip() + text.tag_add('insert', '2.0') self.assertEqual(text.get('1.0', 'insert'), '\n') def test_rstrip_multiple(self): @@ -39,11 +41,11 @@ "Line ending in 5 spaces\n" "Linewithnospaces\n" " indented line\n" - " indented line with trailing space\n") + " indented line with trailing space\n\n") text.insert('1.0', original) do_rstrip() - self.assertEqual(text.get('1.0', 'insert'), stripped) + self.assertEqual(text.get('1.0', 'end'), stripped) if __name__ == '__main__': unittest.main(verbosity=2, exit=False) diff -r 8c1ca6720246 Lib/idlelib/idle_test/test_text.py --- a/Lib/idlelib/idle_test/test_text.py Wed Jul 17 00:57:58 2013 +0200 +++ b/Lib/idlelib/idle_test/test_text.py Wed Jul 17 16:52:16 2013 -0400 @@ -92,6 +92,7 @@ get = self.text.get Equal = self.assertEqual self.text.insert('1.0', self.hw) + self.text.tag_add('insert', 'end') delete('insert') Equal(get('1.0', 'end'), self.hwn)