| 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 [email protected]
|
|---|
| 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 |
|
|---|
| 79 | def find_paragraph(text, mark):
|
|---|
| 80 | lineno, col = map(int, mark.split("."))
|
|---|
| 81 | line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
|
|---|
| 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.0 lineend" % lineno)
|
|---|
| 85 | first_lineno = lineno
|
|---|
| 86 | comment_header = get_comment_header(line)
|
|---|
| 87 | comment_header_len = len(comment_header)
|
|---|
| 88 | while get_comment_header(line)==comment_header and \
|
|---|
| 89 | not is_all_white(line[comment_header_len:]):
|
|---|
| 90 | lineno = lineno + 1
|
|---|
| 91 | line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
|
|---|
| 92 | last = "%d.0" % lineno
|
|---|
| 93 | # Search back to beginning of paragraph
|
|---|
| 94 | lineno = first_lineno - 1
|
|---|
| 95 | line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
|
|---|
| 96 | while lineno > 0 and \
|
|---|
| 97 | get_comment_header(line)==comment_header and \
|
|---|
| 98 | not is_all_white(line[comment_header_len:]):
|
|---|
| 99 | lineno = lineno - 1
|
|---|
| 100 | line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
|
|---|
| 101 | first = "%d.0" % (lineno+1)
|
|---|
| 102 | return first, last, comment_header, text.get(first, last)
|
|---|
| 103 |
|
|---|
| 104 | def reformat_paragraph(data, limit):
|
|---|
| 105 | lines = data.split("\n")
|
|---|
| 106 | i = 0
|
|---|
| 107 | n = len(lines)
|
|---|
| 108 | while i < n and is_all_white(lines[i]):
|
|---|
| 109 | i = i+1
|
|---|
| 110 | if i >= n:
|
|---|
| 111 | return data
|
|---|
| 112 | indent1 = get_indent(lines[i])
|
|---|
| 113 | if i+1 < n and not is_all_white(lines[i+1]):
|
|---|
| 114 | indent2 = get_indent(lines[i+1])
|
|---|
| 115 | else:
|
|---|
| 116 | indent2 = indent1
|
|---|
| 117 | new = lines[:i]
|
|---|
| 118 | partial = indent1
|
|---|
| 119 | while i < n and not is_all_white(lines[i]):
|
|---|
| 120 | # XXX Should take double space after period (etc.) into account
|
|---|
| 121 | words = re.split("(\s+)", lines[i])
|
|---|
| 122 | for j in range(0, len(words), 2):
|
|---|
| 123 | word = words[j]
|
|---|
| 124 | if not word:
|
|---|
| 125 | continue # Can happen when line ends in whitespace
|
|---|
| 126 | if len((partial + word).expandtabs()) > limit and \
|
|---|
| 127 | partial != indent1:
|
|---|
| 128 | new.append(partial.rstrip())
|
|---|
| 129 | partial = indent2
|
|---|
| 130 | partial = partial + word + " "
|
|---|
| 131 | if j+1 < len(words) and words[j+1] != " ":
|
|---|
| 132 | partial = partial + " "
|
|---|
| 133 | i = i+1
|
|---|
| 134 | new.append(partial.rstrip())
|
|---|
| 135 | # XXX Should reformat remaining paragraphs as well
|
|---|
| 136 | new.extend(lines[i:])
|
|---|
| 137 | return "\n".join(new)
|
|---|
| 138 |
|
|---|
| 139 | def is_all_white(line):
|
|---|
| 140 | return re.match(r"^\s*$", line) is not None
|
|---|
| 141 |
|
|---|
| 142 | def get_indent(line):
|
|---|
| 143 | return re.match(r"^(\s*)", line).group()
|
|---|
| 144 |
|
|---|
| 145 | def get_comment_header(line):
|
|---|
| 146 | m = re.match(r"^(\s*#*)", line)
|
|---|
| 147 | if m is None: return ""
|
|---|
| 148 | return m.group(1)
|
|---|