diff -r 1f4aed2c914c Lib/idlelib/FormatParagraph.py --- a/Lib/idlelib/FormatParagraph.py Sun Aug 11 20:13:36 2013 +0300 +++ b/Lib/idlelib/FormatParagraph.py Mon Aug 12 18:19:06 2013 -0400 @@ -16,6 +16,7 @@ """ import re +import textwrap from idlelib.configHandler import idleConf class FormatParagraph: @@ -28,6 +29,8 @@ def __init__(self, editwin): self.editwin = editwin + self.wrapper = textwrap.TextWrapper() + self.wrapper.break_long_words = False def close(self): self.editwin = None @@ -45,17 +48,38 @@ maxformatwidth = idleConf.GetOption( 'main', 'FormatParagraph', 'paragraph', type='int') text = self.editwin.text + wrapper = self.wrapper first, last = self.editwin.get_selection_indices() if first and last: + # Modify the first index to begin at the line start + first = first.split(".")[0] + '.0' data = text.get(first, last) comment_header = get_comment_header(data) else: first, last, comment_header, data = \ find_paragraph(text, text.index("insert")) - if comment_header: - newdata = reformat_comment(data, maxformatwidth, comment_header) - else: - newdata = reformat_paragraph(data, maxformatwidth) + + suffix = '\n' + data = list(get_line_content(line) for line in data.split('\n')) + if data[-1:] == '': + del data[-1] + suffix = '\n' + if data[-1:] == '"""' or data[-1:] == "'''": + del data[-1] + suffix += '\n' + last = last.split(".")[0] + '.0' + data = "\n".join(data) + + if comment_header.endswith('#'): + comment_header += ' ' + + wrapper.initial_indent = wrapper.subsequent_indent = comment_header + newdata = [] + paragraphs = re.split(r"\n\s*\n", data) + for p in paragraphs: + newdata.append(wrapper.fill(p)) + newdata = "\n\n".join(newdata) + suffix + text.tag_remove("sel", "1.0", "end") if newdata != data: @@ -75,6 +99,13 @@ Also returns the comment format string, if any, and paragraph of text between the start/stop indices. """ + first, last = get_string_indices(text, mark) + if first and last: + startlineno, startcol = map(int, first.split(".")) + first = "%d.0" % startlineno + comment_header = get_comment_header(text.get(first, first + ' lineend')) + return first, last, comment_header, text.get(first, last) + lineno, col = map(int, mark.split(".")) line = text.get("%d.0" % lineno, "%d.end" % lineno) @@ -105,70 +136,25 @@ return first, last, comment_header, text.get(first, last) -# This should perhaps be replaced with textwrap.wrap -def reformat_paragraph(data, limit): - """Return data reformatted to specified width (limit).""" - lines = data.split("\n") - i = 0 - n = len(lines) - while i < n and is_all_white(lines[i]): - i = i+1 - if i >= n: - return data - indent1 = get_indent(lines[i]) - if i+1 < n and not is_all_white(lines[i+1]): - indent2 = get_indent(lines[i+1]) - else: - indent2 = indent1 - new = lines[:i] - partial = indent1 - while i < n and not is_all_white(lines[i]): - # XXX Should take double space after period (etc.) into account - words = re.split("(\s+)", lines[i]) - for j in range(0, len(words), 2): - word = words[j] - if not word: - continue # Can happen when line ends in whitespace - if len((partial + word).expandtabs()) > limit and \ - partial != indent1: - new.append(partial.rstrip()) - partial = indent2 - partial = partial + word + " " - if j+1 < len(words) and words[j+1] != " ": - partial = partial + " " - i = i+1 - new.append(partial.rstrip()) - # XXX Should reformat remaining paragraphs as well - new.extend(lines[i:]) - return "\n".join(new) - -def reformat_comment(data, limit, comment_header): - """Return data reformatted to specified width with comment header.""" - - # Remove header from the comment lines - lc = len(comment_header) - data = "\n".join(line[lc:] for line in data.split("\n")) - # Reformat to maxformatwidth chars or a 20 char width, - # whichever is greater. - format_width = max(limit - len(comment_header), 20) - newdata = reformat_paragraph(data, format_width) - # re-split and re-insert the comment header. - newdata = newdata.split("\n") - # If the block ends in a \n, we dont want the comment prefix - # inserted after it. (Im not sure it makes sense to reformat a - # comment block that is not made of complete lines, but whatever!) - # Can't think of a clean solution, so we hack away - block_suffix = "" - if not newdata[-1]: - block_suffix = "\n" - newdata = newdata[:-1] - return '\n'.join(comment_header+line for line in newdata) + block_suffix - def is_all_white(line): """Return True if line is empty or all whitespace.""" return re.match(r"^\s*$", line) is not None +def get_string_indices(text, mark): + ranges = text.tag_ranges("STRING") + for i in range(0, len(ranges), 2): + start, stop = ranges[i], ranges[i+1] + if text.compare(start, "<=", mark) and \ + text.compare(mark, "<=", stop): + return text.index(start), text.index(stop) + return '', '' + +def get_line_content(line): + m = re.match(r"^[ \t]*#*[ \t]*(.*)$", line) + if m is None: return "" + return m.group(1) + def get_indent(line): """Return the initial space or tab indent of line.""" return re.match(r"^([ \t]*)", line).group() diff -r 1f4aed2c914c Lib/idlelib/idle_test/test_formatparagraph.py --- a/Lib/idlelib/idle_test/test_formatparagraph.py Sun Aug 11 20:13:36 2013 +0300 +++ b/Lib/idlelib/idle_test/test_formatparagraph.py Mon Aug 12 18:19:06 2013 -0400 @@ -49,8 +49,13 @@ @classmethod def setUpClass(cls): - from idlelib.idle_test.mock_tk import Text - cls.text = Text() + # from idlelib.idle_test.mock_tk import Text + # cls.text = Text() + # Need real text widget because mock does not have tag_ranges + requires('gui') + cls.root = Tk() + editor = Editor(root=cls.root) + cls.text = editor.text def runcase(self, inserttext, stopline, expected): # Check that find_paragraph returns the expected paragraph when @@ -279,9 +284,9 @@ cls.root.destroy() def test_short_line(self): - self.text.insert('1.0', "Short line\n") + self.text.insert('1.0', "Short line") self.formatter("Dummy") - self.assertEqual(self.text.get('1.0', 'insert'), "Short line\n" ) + self.assertEqual(self.text.get('1.0', 'insert'), "Short line" ) self.text.delete('1.0', 'end') def test_long_line(self): @@ -299,17 +304,19 @@ self.assertEqual(result, expected) text.delete('1.0', 'end') - # Select from 1.11 to line end. - text.insert('1.0', self.test_string) - text.tag_add('sel', '1.11', '1.end') - self.formatter('ParameterDoesNothing') - result = text.get('1.0', 'insert') - # selection excludes \n - expected = ( -" '''this is a test of a reformat for a triple quoted string will it reformat\n" -" to less than 70 characters for me?'''") # no - self.assertEqual(result, expected) - text.delete('1.0', 'end') +# The following tests partial line selection behavior which was changed +# to include the beginning of the line before the selection. +# # Select from 1.11 to line end. +# text.insert('1.0', self.test_string) +# text.tag_add('sel', '1.11', '1.end') +# self.formatter('ParameterDoesNothing') +# result = text.get('1.0', 'insert') +# # selection excludes \n +# expected = ( +# " '''this is a test of a reformat for a triple quoted string will it reformat\n" +# " to less than 70 characters for me?'''") # no +# self.assertEqual(result, expected) +# text.delete('1.0', 'end') def test_multiple_lines(self): text = self.text