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