source: python/trunk/Lib/idlelib/FormatParagraph.py

Last change on this file was 391, checked in by dmik, 11 years ago

python: Merge vendor 2.7.6 to trunk.

  • Property svn:eol-style set to native
File size: 7.0 KB
Line 
1"""Extension to format a paragraph or selection to a max width.
2
3Does 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.
7
8Known 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"""
17
18import re
19from idlelib.configHandler import idleConf
20
21class FormatParagraph:
22
23 menudefs = [
24 ('format', [ # /s/edit/format dscherer@cmu.edu
25 ('Format Paragraph', '<<format-paragraph>>'),
26 ])
27 ]
28
29 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):
36 """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')
47 text = self.editwin.text
48 first, last = self.editwin.get_selection_indices()
49 if first and last:
50 data = text.get(first, last)
51 comment_header = get_comment_header(data)
52 else:
53 first, last, comment_header, data = \
54 find_paragraph(text, text.index("insert"))
55 if comment_header:
56 newdata = reformat_comment(data, maxformatwidth, comment_header)
57 else:
58 newdata = reformat_paragraph(data, maxformatwidth)
59 text.tag_remove("sel", "1.0", "end")
60
61 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")
70 return "break"
71
72def find_paragraph(text, mark):
73 """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 """
78 lineno, col = map(int, mark.split("."))
79 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
82 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
83 lineno = lineno + 1
84 line = text.get("%d.0" % lineno, "%d.end" % lineno)
85 first_lineno = lineno
86 comment_header = get_comment_header(line)
87 comment_header_len = len(comment_header)
88
89 # Once start line found, search for end of paragraph (a blank line)
90 while get_comment_header(line)==comment_header and \
91 not is_all_white(line[comment_header_len:]):
92 lineno = lineno + 1
93 line = text.get("%d.0" % lineno, "%d.end" % lineno)
94 last = "%d.0" % lineno
95
96 # Search back to beginning of paragraph (first blank line before)
97 lineno = first_lineno - 1
98 line = text.get("%d.0" % lineno, "%d.end" % lineno)
99 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
103 line = text.get("%d.0" % lineno, "%d.end" % lineno)
104 first = "%d.0" % (lineno+1)
105
106 return first, last, comment_header, text.get(first, last)
107
108# This should perhaps be replaced with textwrap.wrap
109def reformat_paragraph(data, limit):
110 """Return data reformatted to specified width (limit)."""
111 lines = data.split("\n")
112 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
132 if len((partial + word).expandtabs()) > limit and \
133 partial != indent1:
134 new.append(partial.rstrip())
135 partial = indent2
136 partial = partial + word + " "
137 if j+1 < len(words) and words[j+1] != " ":
138 partial = partial + " "
139 i = i+1
140 new.append(partial.rstrip())
141 # XXX Should reformat remaining paragraphs as well
142 new.extend(lines[i:])
143 return "\n".join(new)
144
145def 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
167def is_all_white(line):
168 """Return True if line is empty or all whitespace."""
169
170 return re.match(r"^\s*$", line) is not None
171
172def get_indent(line):
173 """Return the initial space or tab indent of line."""
174 return re.match(r"^([ \t]*)", line).group()
175
176def get_comment_header(line):
177 """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)
184 if m is None: return ""
185 return m.group(1)
186
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)
Note: See TracBrowser for help on using the repository browser.