blob: ae4e6e7b91153ee58afda51c5fb5e0235ce4c06b [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
Kurt B. Kaiser2d7f6a02007-08-22 23:01:33 +000019from idlelib.configHandler 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
35 def format_paragraph_event(self, event):
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.
44 """
45 maxformatwidth = idleConf.GetOption(
46 'main', 'FormatParagraph', 'paragraph', type='int')
David Scherer7aced172000-08-15 01:13:23 +000047 text = self.editwin.text
48 first, last = self.editwin.get_selection_indices()
49 if first and last:
50 data = text.get(first, last)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040051 comment_header = get_comment_header(data)
David Scherer7aced172000-08-15 01:13:23 +000052 else:
53 first, last, comment_header, data = \
54 find_paragraph(text, text.index("insert"))
55 if comment_header:
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040056 newdata = reformat_comment(data, maxformatwidth, comment_header)
David Scherer7aced172000-08-15 01:13:23 +000057 else:
Raymond Hettinger4e49b832004-06-04 06:31:08 +000058 newdata = reformat_paragraph(data, maxformatwidth)
David Scherer7aced172000-08-15 01:13:23 +000059 text.tag_remove("sel", "1.0", "end")
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040060
David Scherer7aced172000-08-15 01:13:23 +000061 if newdata != data:
62 text.mark_set("insert", first)
63 text.undo_block_start()
64 text.delete(first, last)
65 text.insert(first, newdata)
66 text.undo_block_stop()
67 else:
68 text.mark_set("insert", last)
69 text.see("insert")
Christian Heimesb76922a2007-12-11 01:06:40 +000070 return "break"
David Scherer7aced172000-08-15 01:13:23 +000071
72def find_paragraph(text, mark):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040073 """Returns the start/stop indices enclosing the paragraph that mark is in.
74
75 Also returns the comment format string, if any, and paragraph of text
76 between the start/stop indices.
77 """
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000078 lineno, col = map(int, mark.split("."))
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040079 line = text.get("%d.0" % lineno, "%d.end" % lineno)
80
81 # Look for start of next paragraph if the index passed in is a blank line
David Scherer7aced172000-08-15 01:13:23 +000082 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
83 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040084 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000085 first_lineno = lineno
86 comment_header = get_comment_header(line)
87 comment_header_len = len(comment_header)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040088
89 # Once start line found, search for end of paragraph (a blank line)
David Scherer7aced172000-08-15 01:13:23 +000090 while get_comment_header(line)==comment_header and \
91 not is_all_white(line[comment_header_len:]):
92 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040093 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000094 last = "%d.0" % lineno
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040095
96 # Search back to beginning of paragraph (first blank line before)
David Scherer7aced172000-08-15 01:13:23 +000097 lineno = first_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 while lineno > 0 and \
100 get_comment_header(line)==comment_header and \
101 not is_all_white(line[comment_header_len:]):
102 lineno = 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 first = "%d.0" % (lineno+1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400105
David Scherer7aced172000-08-15 01:13:23 +0000106 return first, last, comment_header, text.get(first, last)
107
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400108# This should perhaps be replaced with textwrap.wrap
Raymond Hettinger4e49b832004-06-04 06:31:08 +0000109def reformat_paragraph(data, limit):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400110 """Return data reformatted to specified width (limit)."""
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000111 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +0000112 i = 0
113 n = len(lines)
114 while i < n and is_all_white(lines[i]):
115 i = i+1
116 if i >= n:
117 return data
118 indent1 = get_indent(lines[i])
119 if i+1 < n and not is_all_white(lines[i+1]):
120 indent2 = get_indent(lines[i+1])
121 else:
122 indent2 = indent1
123 new = lines[:i]
124 partial = indent1
125 while i < n and not is_all_white(lines[i]):
126 # XXX Should take double space after period (etc.) into account
127 words = re.split("(\s+)", lines[i])
128 for j in range(0, len(words), 2):
129 word = words[j]
130 if not word:
131 continue # Can happen when line ends in whitespace
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000132 if len((partial + word).expandtabs()) > limit and \
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400133 partial != indent1:
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000134 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000135 partial = indent2
136 partial = partial + word + " "
137 if j+1 < len(words) and words[j+1] != " ":
138 partial = partial + " "
139 i = i+1
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000140 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000141 # XXX Should reformat remaining paragraphs as well
142 new.extend(lines[i:])
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000143 return "\n".join(new)
David Scherer7aced172000-08-15 01:13:23 +0000144
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400145def reformat_comment(data, limit, comment_header):
146 """Return data reformatted to specified width with comment header."""
147
148 # Remove header from the comment lines
149 lc = len(comment_header)
150 data = "\n".join(line[lc:] for line in data.split("\n"))
151 # Reformat to maxformatwidth chars or a 20 char width,
152 # whichever is greater.
153 format_width = max(limit - len(comment_header), 20)
154 newdata = reformat_paragraph(data, format_width)
155 # re-split and re-insert the comment header.
156 newdata = newdata.split("\n")
157 # If the block ends in a \n, we dont want the comment prefix
158 # inserted after it. (Im not sure it makes sense to reformat a
159 # comment block that is not made of complete lines, but whatever!)
160 # Can't think of a clean solution, so we hack away
161 block_suffix = ""
162 if not newdata[-1]:
163 block_suffix = "\n"
164 newdata = newdata[:-1]
165 return '\n'.join(comment_header+line for line in newdata) + block_suffix
166
David Scherer7aced172000-08-15 01:13:23 +0000167def is_all_white(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400168 """Return True if line is empty or all whitespace."""
169
David Scherer7aced172000-08-15 01:13:23 +0000170 return re.match(r"^\s*$", line) is not None
171
172def get_indent(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400173 """Return the initial space or tab indent of line."""
174 return re.match(r"^([ \t]*)", line).group()
David Scherer7aced172000-08-15 01:13:23 +0000175
176def get_comment_header(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400177 """Return string with leading whitespace and '#' from line or ''.
178
179 A null return indicates that the line is not a comment line. A non-
180 null return, such as ' #', will be used to find the other lines of
181 a comment block with the same indent.
182 """
183 m = re.match(r"^([ \t]*#*)", line)
David Scherer7aced172000-08-15 01:13:23 +0000184 if m is None: return ""
185 return m.group(1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400186
187if __name__ == "__main__":
188 from test import support; support.use_resources = ['gui']
189 import unittest
190 unittest.main('idlelib.idle_test.test_formatparagraph',
191 verbosity=2, exit=False)