blob: eb0e69392acc4ee9f8cdcd4e475bc402bb2b60f0 [file] [log] [blame]
David Scherer7aced172000-08-15 01:13:23 +00001# 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
David Scherer7aced172000-08-15 01:13:23 +000017import re
18
19class FormatParagraph:
20
21 menudefs = [
22 ('format', [ # /s/edit/format dscherer@cmu.edu
23 ('Format Paragraph', '<<format-paragraph>>'),
24 ])
25 ]
26
David Scherer7aced172000-08-15 01:13:23 +000027 def __init__(self, editwin):
28 self.editwin = editwin
29
30 def close(self):
31 self.editwin = None
32
33 def format_paragraph_event(self, event):
34 text = self.editwin.text
35 first, last = self.editwin.get_selection_indices()
36 if first and last:
37 data = text.get(first, last)
38 comment_header = ''
39 else:
40 first, last, comment_header, data = \
41 find_paragraph(text, text.index("insert"))
42 if comment_header:
43 # Reformat the comment lines - convert to text sans header.
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000044 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +000045 lines = map(lambda st, l=len(comment_header): st[l:], lines)
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000046 data = "\n".join(lines)
David Scherer7aced172000-08-15 01:13:23 +000047 # Reformat to 70 chars or a 20 char width, whichever is greater.
48 format_width = max(70-len(comment_header), 20)
49 newdata = reformat_paragraph(data, format_width)
50 # re-split and re-insert the comment header.
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000051 newdata = newdata.split("\n")
David Scherer7aced172000-08-15 01:13:23 +000052 # If the block ends in a \n, we dont want the comment
53 # prefix inserted after it. (Im not sure it makes sense to
54 # reformat a comment block that isnt made of complete
55 # lines, but whatever!) Can't think of a clean soltution,
56 # so we hack away
57 block_suffix = ""
58 if not newdata[-1]:
59 block_suffix = "\n"
60 newdata = newdata[:-1]
61 builder = lambda item, prefix=comment_header: prefix+item
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000062 newdata = '\n'.join(map(builder, newdata)) + block_suffix
David Scherer7aced172000-08-15 01:13:23 +000063 else:
64 # Just a normal text format
65 newdata = reformat_paragraph(data)
66 text.tag_remove("sel", "1.0", "end")
67 if newdata != data:
68 text.mark_set("insert", first)
69 text.undo_block_start()
70 text.delete(first, last)
71 text.insert(first, newdata)
72 text.undo_block_stop()
73 else:
74 text.mark_set("insert", last)
75 text.see("insert")
76
77def find_paragraph(text, mark):
Kurt B. Kaiser75e37902002-09-16 02:22:19 +000078 lineno, col = map(int, mark.split("."))
David Scherer7aced172000-08-15 01:13:23 +000079 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
80 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
81 lineno = lineno + 1
82 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
83 first_lineno = lineno
84 comment_header = get_comment_header(line)
85 comment_header_len = len(comment_header)
86 while get_comment_header(line)==comment_header and \
87 not is_all_white(line[comment_header_len:]):
88 lineno = lineno + 1
89 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
90 last = "%d.0" % lineno
91 # Search back to beginning of paragraph
92 lineno = first_lineno - 1
93 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
94 while lineno > 0 and \
95 get_comment_header(line)==comment_header and \
96 not is_all_white(line[comment_header_len:]):
97 lineno = lineno - 1
98 line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
99 first = "%d.0" % (lineno+1)
100 return first, last, comment_header, text.get(first, last)
101
102def reformat_paragraph(data, limit=70):
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000103 lines = data.split("\n")
David Scherer7aced172000-08-15 01:13:23 +0000104 i = 0
105 n = len(lines)
106 while i < n and is_all_white(lines[i]):
107 i = i+1
108 if i >= n:
109 return data
110 indent1 = get_indent(lines[i])
111 if i+1 < n and not is_all_white(lines[i+1]):
112 indent2 = get_indent(lines[i+1])
113 else:
114 indent2 = indent1
115 new = lines[:i]
116 partial = indent1
117 while i < n and not is_all_white(lines[i]):
118 # XXX Should take double space after period (etc.) into account
119 words = re.split("(\s+)", lines[i])
120 for j in range(0, len(words), 2):
121 word = words[j]
122 if not word:
123 continue # Can happen when line ends in whitespace
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000124 if len((partial + word).expandtabs()) > limit and \
David Scherer7aced172000-08-15 01:13:23 +0000125 partial != indent1:
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000126 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000127 partial = indent2
128 partial = partial + word + " "
129 if j+1 < len(words) and words[j+1] != " ":
130 partial = partial + " "
131 i = i+1
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000132 new.append(partial.rstrip())
David Scherer7aced172000-08-15 01:13:23 +0000133 # XXX Should reformat remaining paragraphs as well
134 new.extend(lines[i:])
Kurt B. Kaiser75e37902002-09-16 02:22:19 +0000135 return "\n".join(new)
David Scherer7aced172000-08-15 01:13:23 +0000136
137def is_all_white(line):
138 return re.match(r"^\s*$", line) is not None
139
140def get_indent(line):
141 return re.match(r"^(\s*)", line).group()
142
143def get_comment_header(line):
144 m = re.match(r"^(\s*#*)", line)
145 if m is None: return ""
146 return m.group(1)