FormatParagraph.py 5.6 KB
Newer Older
David Scherer's avatar
David Scherer committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Extension to format a paragraph

# Does basic, standard text formatting, and also understands Python
# comment blocks.  Thus, for editing Python source code, this
# extension is really only suitable for reformatting these comment
# blocks or triple-quoted strings.

# Known problems with comment reformatting:
# * If there is a selection marked, and the first line of the
#   selection is not complete, the block will probably not be detected
#   as comments, and will have the normal "text formatting" rules
#   applied.
# * If a comment block has leading whitespace that mixes tabs and
#   spaces, they will not be considered part of the same block.
# * Fancy comments, like this bulleted list, arent handled :-)

import re
18
from idlelib.configHandler import idleConf
David Scherer's avatar
David Scherer committed
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

class FormatParagraph:

    menudefs = [
        ('format', [   # /s/edit/format   dscherer@cmu.edu
            ('Format Paragraph', '<<format-paragraph>>'),
         ])
    ]

    def __init__(self, editwin):
        self.editwin = editwin

    def close(self):
        self.editwin = None

    def format_paragraph_event(self, event):
35
        maxformatwidth = int(idleConf.GetOption('main','FormatParagraph','paragraph'))
David Scherer's avatar
David Scherer committed
36 37 38 39 40 41 42 43 44 45
        text = self.editwin.text
        first, last = self.editwin.get_selection_indices()
        if first and last:
            data = text.get(first, last)
            comment_header = ''
        else:
            first, last, comment_header, data = \
                    find_paragraph(text, text.index("insert"))
        if comment_header:
            # Reformat the comment lines - convert to text sans header.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
46
            lines = data.split("\n")
David Scherer's avatar
David Scherer committed
47
            lines = map(lambda st, l=len(comment_header): st[l:], lines)
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
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)
David Scherer's avatar
David Scherer committed
51 52
            newdata = reformat_paragraph(data, format_width)
            # re-split and re-insert the comment header.
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
53
            newdata = newdata.split("\n")
David Scherer's avatar
David Scherer committed
54 55 56
            # If the block ends in a \n, we dont want the comment
            # prefix inserted after it. (Im not sure it makes sense to
            # reformat a comment block that isnt made of complete
57
            # lines, but whatever!)  Can't think of a clean solution,
David Scherer's avatar
David Scherer committed
58 59 60 61 62 63
            # so we hack away
            block_suffix = ""
            if not newdata[-1]:
                block_suffix = "\n"
                newdata = newdata[:-1]
            builder = lambda item, prefix=comment_header: prefix+item
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
64
            newdata = '\n'.join(map(builder, newdata)) + block_suffix
David Scherer's avatar
David Scherer committed
65 66
        else:
            # Just a normal text format
67
            newdata = reformat_paragraph(data, maxformatwidth)
David Scherer's avatar
David Scherer committed
68 69 70 71 72 73 74 75 76 77
        text.tag_remove("sel", "1.0", "end")
        if newdata != data:
            text.mark_set("insert", first)
            text.undo_block_start()
            text.delete(first, last)
            text.insert(first, newdata)
            text.undo_block_stop()
        else:
            text.mark_set("insert", last)
        text.see("insert")
78
        return "break"
David Scherer's avatar
David Scherer committed
79 80

def find_paragraph(text, mark):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
81
    lineno, col = map(int, mark.split("."))
David Scherer's avatar
David Scherer committed
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
    line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
    while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
        lineno = lineno + 1
        line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
    first_lineno = lineno
    comment_header = get_comment_header(line)
    comment_header_len = len(comment_header)
    while get_comment_header(line)==comment_header and \
              not is_all_white(line[comment_header_len:]):
        lineno = lineno + 1
        line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
    last = "%d.0" % lineno
    # Search back to beginning of paragraph
    lineno = first_lineno - 1
    line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
    while lineno > 0 and \
              get_comment_header(line)==comment_header and \
              not is_all_white(line[comment_header_len:]):
        lineno = lineno - 1
        line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
    first = "%d.0" % (lineno+1)
    return first, last, comment_header, text.get(first, last)

105
def reformat_paragraph(data, limit):
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
106
    lines = data.split("\n")
David Scherer's avatar
David Scherer committed
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    i = 0
    n = len(lines)
    while i < n and is_all_white(lines[i]):
        i = i+1
    if i >= n:
        return data
    indent1 = get_indent(lines[i])
    if i+1 < n and not is_all_white(lines[i+1]):
        indent2 = get_indent(lines[i+1])
    else:
        indent2 = indent1
    new = lines[:i]
    partial = indent1
    while i < n and not is_all_white(lines[i]):
        # XXX Should take double space after period (etc.) into account
        words = re.split("(\s+)", lines[i])
        for j in range(0, len(words), 2):
            word = words[j]
            if not word:
                continue # Can happen when line ends in whitespace
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
127
            if len((partial + word).expandtabs()) > limit and \
David Scherer's avatar
David Scherer committed
128
               partial != indent1:
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
129
                new.append(partial.rstrip())
David Scherer's avatar
David Scherer committed
130 131 132 133 134
                partial = indent2
            partial = partial + word + " "
            if j+1 < len(words) and words[j+1] != " ":
                partial = partial + " "
        i = i+1
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
135
    new.append(partial.rstrip())
David Scherer's avatar
David Scherer committed
136 137
    # XXX Should reformat remaining paragraphs as well
    new.extend(lines[i:])
Kurt B. Kaiser's avatar
Kurt B. Kaiser committed
138
    return "\n".join(new)
David Scherer's avatar
David Scherer committed
139 140 141 142 143 144 145 146 147 148 149

def is_all_white(line):
    return re.match(r"^\s*$", line) is not None

def get_indent(line):
    return re.match(r"^(\s*)", line).group()

def get_comment_header(line):
    m = re.match(r"^(\s*#*)", line)
    if m is None: return ""
    return m.group(1)