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 idlelib.configHandler import idleConf 19 20 class FormatParagraph: 21 22 menudefs = [ 23 ('format', [ # /s/edit/format dscherer (at] 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 81 def 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 106 def 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 141 def is_all_white(line): 142 return re.match(r"^\s*$", line) is not None 143 144 def get_indent(line): 145 return re.match(r"^(\s*)", line).group() 146 147 def get_comment_header(line): 148 m = re.match(r"^(\s*#*)", line) 149 if m is None: return "" 150 return m.group(1) 151