|
1 # Extension to format a paragraph |
|
2 |
|
3 # Does basic, standard text formatting, and also understands Python |
|
4 # comment blocks. Thus, for editing Python source code, this |
|
5 # extension is really only suitable for reformatting these comment |
|
6 # blocks or triple-quoted strings. |
|
7 |
|
8 # Known 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, arent handled :-) |
|
16 |
|
17 import re |
|
18 from configHandler import idleConf |
|
19 |
|
20 class FormatParagraph: |
|
21 |
|
22 menudefs = [ |
|
23 ('format', [ # /s/edit/format dscherer@cmu.edu |
|
24 ('Format Paragraph', '<<format-paragraph>>'), |
|
25 ]) |
|
26 ] |
|
27 |
|
28 def __init__(self, editwin): |
|
29 self.editwin = editwin |
|
30 |
|
31 def close(self): |
|
32 self.editwin = None |
|
33 |
|
34 def format_paragraph_event(self, event): |
|
35 maxformatwidth = int(idleConf.GetOption('main','FormatParagraph','paragraph')) |
|
36 text = self.editwin.text |
|
37 first, last = self.editwin.get_selection_indices() |
|
38 if first and last: |
|
39 data = text.get(first, last) |
|
40 comment_header = '' |
|
41 else: |
|
42 first, last, comment_header, data = \ |
|
43 find_paragraph(text, text.index("insert")) |
|
44 if comment_header: |
|
45 # Reformat the comment lines - convert to text sans header. |
|
46 lines = data.split("\n") |
|
47 lines = map(lambda st, l=len(comment_header): st[l:], lines) |
|
48 data = "\n".join(lines) |
|
49 # Reformat to maxformatwidth chars or a 20 char width, whichever is greater. |
|
50 format_width = max(maxformatwidth - len(comment_header), 20) |
|
51 newdata = reformat_paragraph(data, format_width) |
|
52 # re-split and re-insert the comment header. |
|
53 newdata = newdata.split("\n") |
|
54 # If the block ends in a \n, we dont want the comment |
|
55 # prefix inserted after it. (Im not sure it makes sense to |
|
56 # reformat a comment block that isnt made of complete |
|
57 # lines, but whatever!) Can't think of a clean soltution, |
|
58 # so we hack away |
|
59 block_suffix = "" |
|
60 if not newdata[-1]: |
|
61 block_suffix = "\n" |
|
62 newdata = newdata[:-1] |
|
63 builder = lambda item, prefix=comment_header: prefix+item |
|
64 newdata = '\n'.join(map(builder, newdata)) + block_suffix |
|
65 else: |
|
66 # Just a normal text format |
|
67 newdata = reformat_paragraph(data, maxformatwidth) |
|
68 text.tag_remove("sel", "1.0", "end") |
|
69 if newdata != data: |
|
70 text.mark_set("insert", first) |
|
71 text.undo_block_start() |
|
72 text.delete(first, last) |
|
73 text.insert(first, newdata) |
|
74 text.undo_block_stop() |
|
75 else: |
|
76 text.mark_set("insert", last) |
|
77 text.see("insert") |
|
78 return "break" |
|
79 |
|
80 def find_paragraph(text, mark): |
|
81 lineno, col = map(int, mark.split(".")) |
|
82 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) |
|
83 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line): |
|
84 lineno = lineno + 1 |
|
85 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) |
|
86 first_lineno = lineno |
|
87 comment_header = get_comment_header(line) |
|
88 comment_header_len = len(comment_header) |
|
89 while get_comment_header(line)==comment_header and \ |
|
90 not is_all_white(line[comment_header_len:]): |
|
91 lineno = lineno + 1 |
|
92 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) |
|
93 last = "%d.0" % lineno |
|
94 # Search back to beginning of paragraph |
|
95 lineno = first_lineno - 1 |
|
96 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) |
|
97 while lineno > 0 and \ |
|
98 get_comment_header(line)==comment_header and \ |
|
99 not is_all_white(line[comment_header_len:]): |
|
100 lineno = lineno - 1 |
|
101 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno) |
|
102 first = "%d.0" % (lineno+1) |
|
103 return first, last, comment_header, text.get(first, last) |
|
104 |
|
105 def reformat_paragraph(data, limit): |
|
106 lines = data.split("\n") |
|
107 i = 0 |
|
108 n = len(lines) |
|
109 while i < n and is_all_white(lines[i]): |
|
110 i = i+1 |
|
111 if i >= n: |
|
112 return data |
|
113 indent1 = get_indent(lines[i]) |
|
114 if i+1 < n and not is_all_white(lines[i+1]): |
|
115 indent2 = get_indent(lines[i+1]) |
|
116 else: |
|
117 indent2 = indent1 |
|
118 new = lines[:i] |
|
119 partial = indent1 |
|
120 while i < n and not is_all_white(lines[i]): |
|
121 # XXX Should take double space after period (etc.) into account |
|
122 words = re.split("(\s+)", lines[i]) |
|
123 for j in range(0, len(words), 2): |
|
124 word = words[j] |
|
125 if not word: |
|
126 continue # Can happen when line ends in whitespace |
|
127 if len((partial + word).expandtabs()) > limit and \ |
|
128 partial != indent1: |
|
129 new.append(partial.rstrip()) |
|
130 partial = indent2 |
|
131 partial = partial + word + " " |
|
132 if j+1 < len(words) and words[j+1] != " ": |
|
133 partial = partial + " " |
|
134 i = i+1 |
|
135 new.append(partial.rstrip()) |
|
136 # XXX Should reformat remaining paragraphs as well |
|
137 new.extend(lines[i:]) |
|
138 return "\n".join(new) |
|
139 |
|
140 def is_all_white(line): |
|
141 return re.match(r"^\s*$", line) is not None |
|
142 |
|
143 def get_indent(line): |
|
144 return re.match(r"^(\s*)", line).group() |
|
145 |
|
146 def get_comment_header(line): |
|
147 m = re.match(r"^(\s*#*)", line) |
|
148 if m is None: return "" |
|
149 return m.group(1) |