blob: 2ac87e45ea1a7216f3b708d50f796438cfc9eafc [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
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 Reedyd5d4c4e2014-04-22 01:11:03 -040047 if limit == None:
48 limit = idleConf.GetOption(
49 'main', 'FormatParagraph', 'paragraph', type='int')
David Scherer7aced172000-08-15 01:13:23 +000050 text = self.editwin.text
51 first, last = self.editwin.get_selection_indices()
52 if first and last:
53 data = text.get(first, last)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040054 comment_header = get_comment_header(data)
David Scherer7aced172000-08-15 01:13:23 +000055 else:
56 first, last, comment_header, data = \
57 find_paragraph(text, text.index("insert"))
58 if comment_header:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040059 newdata = reformat_comment(data, limit, comment_header)
David Scherer7aced172000-08-15 01:13:23 +000060 else:
Terry Jan Reedyd5d4c4e2014-04-22 01:11:03 -040061 newdata = reformat_paragraph(data, limit)
David Scherer7aced172000-08-15 01:13:23 +000062 text.tag_remove("sel", "1.0", "end")
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040063
David Scherer7aced172000-08-15 01:13:23 +000064 if newdata != data:
65 text.mark_set("insert", first)
66 text.undo_block_start()
67 text.delete(first, last)
68 text.insert(first, newdata)
69 text.undo_block_stop()
70 else:
71 text.mark_set("insert", last)
72 text.see("insert")
Christian Heimesb76922a2007-12-11 01:06:40 +000073 return "break"
David Scherer7aced172000-08-15 01:13:23 +000074
75def find_paragraph(text, mark):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040076 """Returns the start/stop indices enclosing the paragraph that mark is in.
77
78 Also returns the comment format string, if any, and paragraph of text
79 between the start/stop indices.
80 """
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000081 lineno, col = map(int, mark.split("."))
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040082 line = text.get("%d.0" % lineno, "%d.end" % lineno)
83
84 # Look for start of next paragraph if the index passed in is a blank line
David Scherer7aced172000-08-15 01:13:23 +000085 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
86 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040087 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000088 first_lineno = lineno
89 comment_header = get_comment_header(line)
90 comment_header_len = len(comment_header)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040091
92 # Once start line found, search for end of paragraph (a blank line)
David Scherer7aced172000-08-15 01:13:23 +000093 while get_comment_header(line)==comment_header and \
94 not is_all_white(line[comment_header_len:]):
95 lineno = lineno + 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040096 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +000097 last = "%d.0" % lineno
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -040098
99 # Search back to beginning of paragraph (first blank line before)
David Scherer7aced172000-08-15 01:13:23 +0000100 lineno = first_lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400101 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000102 while lineno > 0 and \
103 get_comment_header(line)==comment_header and \
104 not is_all_white(line[comment_header_len:]):
105 lineno = lineno - 1
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400106 line = text.get("%d.0" % lineno, "%d.end" % lineno)
David Scherer7aced172000-08-15 01:13:23 +0000107 first = "%d.0" % (lineno+1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400108
David Scherer7aced172000-08-15 01:13:23 +0000109 return first, last, comment_header, text.get(first, last)
110
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400111# This should perhaps be replaced with textwrap.wrap
Raymond Hettinger4e49b832004-06-04 06:31:08 +0000112def reformat_paragraph(data, limit):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400113 """Return data reformatted to specified width (limit)."""
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000114 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +0000115 i = 0
116 n = len(lines)
117 while i < n and is_all_white(lines[i]):
118 i = i+1
119 if i >= n:
120 return data
121 indent1 = get_indent(lines[i])
122 if i+1 < n and not is_all_white(lines[i+1]):
123 indent2 = get_indent(lines[i+1])
124 else:
125 indent2 = indent1
126 new = lines[:i]
127 partial = indent1
128 while i < n and not is_all_white(lines[i]):
129 # XXX Should take double space after period (etc.) into account
130 words = re.split("(\s+)", lines[i])
131 for j in range(0, len(words), 2):
132 word = words[j]
133 if not word:
134 continue # Can happen when line ends in whitespace
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000135 if len((partial + word).expandtabs()) > limit and \
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400136 partial != indent1:
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000137 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000138 partial = indent2
139 partial = partial + word + " "
140 if j+1 < len(words) and words[j+1] != " ":
141 partial = partial + " "
142 i = i+1
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000143 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000144 # XXX Should reformat remaining paragraphs as well
145 new.extend(lines[i:])
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000146 return "\n".join(new)
David Scherer7aced172000-08-15 01:13:23 +0000147
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400148def reformat_comment(data, limit, comment_header):
149 """Return data reformatted to specified width with comment header."""
150
151 # Remove header from the comment lines
152 lc = len(comment_header)
153 data = "\n".join(line[lc:] for line in data.split("\n"))
154 # Reformat to maxformatwidth chars or a 20 char width,
155 # whichever is greater.
156 format_width = max(limit - len(comment_header), 20)
157 newdata = reformat_paragraph(data, format_width)
158 # re-split and re-insert the comment header.
159 newdata = newdata.split("\n")
160 # If the block ends in a \n, we dont want the comment prefix
161 # inserted after it. (Im not sure it makes sense to reformat a
162 # comment block that is not made of complete lines, but whatever!)
163 # Can't think of a clean solution, so we hack away
164 block_suffix = ""
165 if not newdata[-1]:
166 block_suffix = "\n"
167 newdata = newdata[:-1]
168 return '\n'.join(comment_header+line for line in newdata) + block_suffix
169
David Scherer7aced172000-08-15 01:13:23 +0000170def is_all_white(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400171 """Return True if line is empty or all whitespace."""
172
David Scherer7aced172000-08-15 01:13:23 +0000173 return re.match(r"^\s*$", line) is not None
174
175def get_indent(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400176 """Return the initial space or tab indent of line."""
177 return re.match(r"^([ \t]*)", line).group()
David Scherer7aced172000-08-15 01:13:23 +0000178
179def get_comment_header(line):
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400180 """Return string with leading whitespace and '#' from line or ''.
181
182 A null return indicates that the line is not a comment line. A non-
183 null return, such as ' #', will be used to find the other lines of
184 a comment block with the same indent.
185 """
186 m = re.match(r"^([ \t]*#*)", line)
David Scherer7aced172000-08-15 01:13:23 +0000187 if m is None: return ""
188 return m.group(1)
Terry Jan Reedy7c64aad2013-08-10 16:56:28 -0400189
190if __name__ == "__main__":
191 from test import support; support.use_resources = ['gui']
192 import unittest
193 unittest.main('idlelib.idle_test.test_formatparagraph',
194 verbosity=2, exit=False)