blob: f11bdaeb77ac38beb420366597c89ff90ac062fe [file] [log] [blame]
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -04001"""Extension to format a paragraph or selection to a max width.
David Scherer7aced172000-08-15 01:13:23 +00002
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -04003Does basic, standard text formatting, and also understands Python
4comment blocks. Thus, for editing Python source code, this
5extension is really only suitable for reformatting these comment
6blocks or triple-quoted strings.
David Scherer7aced172000-08-15 01:13:23 +00007
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -04008Known problems with comment reformatting:
9* If there is a selection marked, and the first line of the
10 selection is not complete, the block will probably not be detected
11 as comments, and will have the normal "text formatting" rules
12 applied.
13* If a comment block has leading whitespace that mixes tabs and
14 spaces, they will not be considered part of the same block.
15* Fancy comments, like this bulleted list, aren't handled :-)
16"""
David Scherer7aced172000-08-15 01:13:23 +000017import re
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040018
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040019from idlelib.config import idleConf
David Scherer7aced172000-08-15 01:13:23 +000020
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -040021
David Scherer7aced172000-08-15 01:13:23 +000022class FormatParagraph:
23
24 menudefs = [
25 ('format', [ # /s/edit/format dscherer@cmu.edu
26 ('Format Paragraph', '<<format-paragraph>>'),
27 ])
28 ]
29
David Scherer7aced172000-08-15 01:13:23 +000030 def __init__(self, editwin):
31 self.editwin = editwin
32
33 def close(self):
34 self.editwin = None
35
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040036 def format_paragraph_event(self, event, limit=None):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040037 """Formats paragraph to a max width specified in idleConf.
38
39 If text is selected, format_paragraph_event will start breaking lines
40 at the max width, starting from the beginning selection.
41
42 If no text is selected, format_paragraph_event uses the current
43 cursor location to determine the paragraph (lines of text surrounded
44 by blank lines) and formats it.
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040045
46 The length limit parameter is for testing with a known value.
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040047 """
Terry Jan Reedydf938692014-12-16 03:21:26 -050048 if limit is None:
49 # The default length limit is that defined by pep8
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040050 limit = idleConf.GetOption(
Terry Jan Reedydf938692014-12-16 03:21:26 -050051 'extensions', 'FormatParagraph', 'max-width',
52 type='int', default=72)
David Scherer7aced172000-08-15 01:13:23 +000053 text = self.editwin.text
54 first, last = self.editwin.get_selection_indices()
55 if first and last:
56 data = text.get(first, last)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040057 comment_header = get_comment_header(data)
David Scherer7aced172000-08-15 01:13:23 +000058 else:
59 first, last, comment_header, data = \
60 find_paragraph(text, text.index("insert"))
61 if comment_header:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040062 newdata = reformat_comment(data, limit, comment_header)
David Scherer7aced172000-08-15 01:13:23 +000063 else:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040064 newdata = reformat_paragraph(data, limit)
David Scherer7aced172000-08-15 01:13:23 +000065 text.tag_remove("sel", "1.0", "end")
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040066
David Scherer7aced172000-08-15 01:13:23 +000067 if newdata != data:
68 text.mark_set("insert", first)
69 text.undo_block_start()
70 text.delete(first, last)
71 text.insert(first, newdata)
72 text.undo_block_stop()
73 else:
74 text.mark_set("insert", last)
75 text.see("insert")
Christian Heimesb76922a2007-12-11 01:06:40 +000076 return "break"
David Scherer7aced172000-08-15 01:13:23 +000077
78def find_paragraph(text, mark):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040079 """Returns the start/stop indices enclosing the paragraph that mark is in.
80
81 Also returns the comment format string, if any, and paragraph of text
82 between the start/stop indices.
83 """
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000084 lineno, col = map(int, mark.split("."))
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040085 line = text.get("%d.0" % lineno, "%d.end" % lineno)
86
87 # Look for start of next paragraph if the index passed in is a blank line
David Scherer7aced172000-08-15 01:13:23 +000088 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
89 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040090 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000091 first_lineno = lineno
92 comment_header = get_comment_header(line)
93 comment_header_len = len(comment_header)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040094
95 # Once start line found, search for end of paragraph (a blank line)
David Scherer7aced172000-08-15 01:13:23 +000096 while get_comment_header(line)==comment_header and \
97 not is_all_white(line[comment_header_len:]):
98 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040099 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000100 last = "%d.0" % lineno
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400101
102 # Search back to beginning of paragraph (first blank line before)
David Scherer7aced172000-08-15 01:13:23 +0000103 lineno = first_lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400104 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000105 while lineno > 0 and \
106 get_comment_header(line)==comment_header and \
107 not is_all_white(line[comment_header_len:]):
108 lineno = lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400109 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000110 first = "%d.0" % (lineno+1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400111
David Scherer7aced172000-08-15 01:13:23 +0000112 return first, last, comment_header, text.get(first, last)
113
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400114# This should perhaps be replaced with textwrap.wrap
Raymond Hettinger4e49b832004-06-04 06:31:08 +0000115def reformat_paragraph(data, limit):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400116 """Return data reformatted to specified width (limit)."""
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000117 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +0000118 i = 0
119 n = len(lines)
120 while i < n and is_all_white(lines[i]):
121 i = i+1
122 if i >= n:
123 return data
124 indent1 = get_indent(lines[i])
125 if i+1 < n and not is_all_white(lines[i+1]):
126 indent2 = get_indent(lines[i+1])
127 else:
128 indent2 = indent1
129 new = lines[:i]
130 partial = indent1
131 while i < n and not is_all_white(lines[i]):
132 # XXX Should take double space after period (etc.) into account
R David Murray44b548d2016-09-08 13:59:53 -0400133 words = re.split(r"(\s+)", lines[i])
David Scherer7aced172000-08-15 01:13:23 +0000134 for j in range(0, len(words), 2):
135 word = words[j]
136 if not word:
137 continue # Can happen when line ends in whitespace
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000138 if len((partial + word).expandtabs()) > limit and \
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400139 partial != indent1:
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000140 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000141 partial = indent2
142 partial = partial + word + " "
143 if j+1 < len(words) and words[j+1] != " ":
144 partial = partial + " "
145 i = i+1
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000146 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000147 # XXX Should reformat remaining paragraphs as well
148 new.extend(lines[i:])
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000149 return "\n".join(new)
David Scherer7aced172000-08-15 01:13:23 +0000150
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400151def reformat_comment(data, limit, comment_header):
152 """Return data reformatted to specified width with comment header."""
153
154 # Remove header from the comment lines
155 lc = len(comment_header)
156 data = "\n".join(line[lc:] for line in data.split("\n"))
157 # Reformat to maxformatwidth chars or a 20 char width,
158 # whichever is greater.
159 format_width = max(limit - len(comment_header), 20)
160 newdata = reformat_paragraph(data, format_width)
161 # re-split and re-insert the comment header.
162 newdata = newdata.split("\n")
163 # If the block ends in a \n, we dont want the comment prefix
164 # inserted after it. (Im not sure it makes sense to reformat a
165 # comment block that is not made of complete lines, but whatever!)
166 # Can't think of a clean solution, so we hack away
167 block_suffix = ""
168 if not newdata[-1]:
169 block_suffix = "\n"
170 newdata = newdata[:-1]
171 return '\n'.join(comment_header+line for line in newdata) + block_suffix
172
David Scherer7aced172000-08-15 01:13:23 +0000173def is_all_white(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400174 """Return True if line is empty or all whitespace."""
175
David Scherer7aced172000-08-15 01:13:23 +0000176 return re.match(r"^\s*$", line) is not None
177
178def get_indent(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400179 """Return the initial space or tab indent of line."""
180 return re.match(r"^([ \t]*)", line).group()
David Scherer7aced172000-08-15 01:13:23 +0000181
182def get_comment_header(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400183 """Return string with leading whitespace and '#' from line or ''.
184
185 A null return indicates that the line is not a comment line. A non-
186 null return, such as ' #', will be used to find the other lines of
187 a comment block with the same indent.
188 """
189 m = re.match(r"^([ \t]*#*)", line)
David Scherer7aced172000-08-15 01:13:23 +0000190 if m is None: return ""
191 return m.group(1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400192
Terry Jan Reedybfbaa6b2016-08-31 00:50:55 -0400193
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400194if __name__ == "__main__":
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400195 import unittest
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400196 unittest.main('idlelib.idle_test.test_paragraph',
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400197 verbosity=2, exit=False)