blob: 0323b53d863b658263a19a02ab4e23603f550a85 [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 +000017
David Scherer7aced172000-08-15 01:13:23 +000018import re
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -040019from idlelib.config import idleConf
David Scherer7aced172000-08-15 01:13:23 +000020
21class FormatParagraph:
22
23 menudefs = [
24 ('format', [ # /s/edit/format dscherer@cmu.edu
25 ('Format Paragraph', '<<format-paragraph>>'),
26 ])
27 ]
28
David Scherer7aced172000-08-15 01:13:23 +000029 def __init__(self, editwin):
30 self.editwin = editwin
31
32 def close(self):
33 self.editwin = None
34
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040035 def format_paragraph_event(self, event, limit=None):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040036 """Formats paragraph to a max width specified in idleConf.
37
38 If text is selected, format_paragraph_event will start breaking lines
39 at the max width, starting from the beginning selection.
40
41 If no text is selected, format_paragraph_event uses the current
42 cursor location to determine the paragraph (lines of text surrounded
43 by blank lines) and formats it.
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040044
45 The length limit parameter is for testing with a known value.
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040046 """
Terry Jan Reedydf938692014-12-16 03:21:26 -050047 if limit is None:
48 # The default length limit is that defined by pep8
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040049 limit = idleConf.GetOption(
Terry Jan Reedydf938692014-12-16 03:21:26 -050050 'extensions', 'FormatParagraph', 'max-width',
51 type='int', default=72)
David Scherer7aced172000-08-15 01:13:23 +000052 text = self.editwin.text
53 first, last = self.editwin.get_selection_indices()
54 if first and last:
55 data = text.get(first, last)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040056 comment_header = get_comment_header(data)
David Scherer7aced172000-08-15 01:13:23 +000057 else:
58 first, last, comment_header, data = \
59 find_paragraph(text, text.index("insert"))
60 if comment_header:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040061 newdata = reformat_comment(data, limit, comment_header)
David Scherer7aced172000-08-15 01:13:23 +000062 else:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040063 newdata = reformat_paragraph(data, limit)
David Scherer7aced172000-08-15 01:13:23 +000064 text.tag_remove("sel", "1.0", "end")
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040065
David Scherer7aced172000-08-15 01:13:23 +000066 if newdata != data:
67 text.mark_set("insert", first)
68 text.undo_block_start()
69 text.delete(first, last)
70 text.insert(first, newdata)
71 text.undo_block_stop()
72 else:
73 text.mark_set("insert", last)
74 text.see("insert")
Christian Heimesb76922a2007-12-11 01:06:40 +000075 return "break"
David Scherer7aced172000-08-15 01:13:23 +000076
77def find_paragraph(text, mark):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040078 """Returns the start/stop indices enclosing the paragraph that mark is in.
79
80 Also returns the comment format string, if any, and paragraph of text
81 between the start/stop indices.
82 """
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000083 lineno, col = map(int, mark.split("."))
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040084 line = text.get("%d.0" % lineno, "%d.end" % lineno)
85
86 # Look for start of next paragraph if the index passed in is a blank line
David Scherer7aced172000-08-15 01:13:23 +000087 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
88 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040089 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000090 first_lineno = lineno
91 comment_header = get_comment_header(line)
92 comment_header_len = len(comment_header)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040093
94 # Once start line found, search for end of paragraph (a blank line)
David Scherer7aced172000-08-15 01:13:23 +000095 while get_comment_header(line)==comment_header and \
96 not is_all_white(line[comment_header_len:]):
97 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040098 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000099 last = "%d.0" % lineno
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400100
101 # Search back to beginning of paragraph (first blank line before)
David Scherer7aced172000-08-15 01:13:23 +0000102 lineno = first_lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400103 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000104 while lineno > 0 and \
105 get_comment_header(line)==comment_header and \
106 not is_all_white(line[comment_header_len:]):
107 lineno = lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400108 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000109 first = "%d.0" % (lineno+1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400110
David Scherer7aced172000-08-15 01:13:23 +0000111 return first, last, comment_header, text.get(first, last)
112
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400113# This should perhaps be replaced with textwrap.wrap
Raymond Hettinger4e49b832004-06-04 06:31:08 +0000114def reformat_paragraph(data, limit):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400115 """Return data reformatted to specified width (limit)."""
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000116 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +0000117 i = 0
118 n = len(lines)
119 while i < n and is_all_white(lines[i]):
120 i = i+1
121 if i >= n:
122 return data
123 indent1 = get_indent(lines[i])
124 if i+1 < n and not is_all_white(lines[i+1]):
125 indent2 = get_indent(lines[i+1])
126 else:
127 indent2 = indent1
128 new = lines[:i]
129 partial = indent1
130 while i < n and not is_all_white(lines[i]):
131 # XXX Should take double space after period (etc.) into account
132 words = re.split("(\s+)", lines[i])
133 for j in range(0, len(words), 2):
134 word = words[j]
135 if not word:
136 continue # Can happen when line ends in whitespace
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000137 if len((partial + word).expandtabs()) > limit and \
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400138 partial != indent1:
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000139 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000140 partial = indent2
141 partial = partial + word + " "
142 if j+1 < len(words) and words[j+1] != " ":
143 partial = partial + " "
144 i = i+1
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000145 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000146 # XXX Should reformat remaining paragraphs as well
147 new.extend(lines[i:])
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000148 return "\n".join(new)
David Scherer7aced172000-08-15 01:13:23 +0000149
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400150def reformat_comment(data, limit, comment_header):
151 """Return data reformatted to specified width with comment header."""
152
153 # Remove header from the comment lines
154 lc = len(comment_header)
155 data = "\n".join(line[lc:] for line in data.split("\n"))
156 # Reformat to maxformatwidth chars or a 20 char width,
157 # whichever is greater.
158 format_width = max(limit - len(comment_header), 20)
159 newdata = reformat_paragraph(data, format_width)
160 # re-split and re-insert the comment header.
161 newdata = newdata.split("\n")
162 # If the block ends in a \n, we dont want the comment prefix
163 # inserted after it. (Im not sure it makes sense to reformat a
164 # comment block that is not made of complete lines, but whatever!)
165 # Can't think of a clean solution, so we hack away
166 block_suffix = ""
167 if not newdata[-1]:
168 block_suffix = "\n"
169 newdata = newdata[:-1]
170 return '\n'.join(comment_header+line for line in newdata) + block_suffix
171
David Scherer7aced172000-08-15 01:13:23 +0000172def is_all_white(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400173 """Return True if line is empty or all whitespace."""
174
David Scherer7aced172000-08-15 01:13:23 +0000175 return re.match(r"^\s*$", line) is not None
176
177def get_indent(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400178 """Return the initial space or tab indent of line."""
179 return re.match(r"^([ \t]*)", line).group()
David Scherer7aced172000-08-15 01:13:23 +0000180
181def get_comment_header(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400182 """Return string with leading whitespace and '#' from line or ''.
183
184 A null return indicates that the line is not a comment line. A non-
185 null return, such as ' #', will be used to find the other lines of
186 a comment block with the same indent.
187 """
188 m = re.match(r"^([ \t]*#*)", line)
David Scherer7aced172000-08-15 01:13:23 +0000189 if m is None: return ""
190 return m.group(1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400191
192if __name__ == "__main__":
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400193 import unittest
Terry Jan Reedy6fa5bdc2016-05-28 13:22:31 -0400194 unittest.main('idlelib.idle_test.test_paragraph',
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400195 verbosity=2, exit=False)