codekingpro/portable-devtools
114k
1"""Format all or a selected region (line slice) of text.2 3Region formatting options: paragraph, comment block, indent, deindent,4comment, uncomment, tabify, and untabify.5 6File renamed from paragraph.py with functions added from editor.py.7"""8import re9from tkinter.messagebox import askyesno10from tkinter.simpledialog import askinteger11from idlelib.config import idleConf12 13 14class FormatParagraph:15 """Format a paragraph, comment block, or selection to a max width.16 17 Does basic, standard text formatting, and also understands Python18 comment blocks. Thus, for editing Python source code, this19 extension is really only suitable for reformatting these comment20 blocks or triple-quoted strings.21 22 Known problems with comment reformatting:23 * If there is a selection marked, and the first line of the24 selection is not complete, the block will probably not be detected25 as comments, and will have the normal "text formatting" rules26 applied.27 * If a comment block has leading whitespace that mixes tabs and28 spaces, they will not be considered part of the same block.29 * Fancy comments, like this bulleted list, aren't handled :-)30 """31 def __init__(self, editwin):32 self.editwin = editwin33 34 @classmethod35 def reload(cls):36 cls.max_width = idleConf.GetOption('extensions', 'FormatParagraph',37 'max-width', type='int', default=72)38 39 def close(self):40 self.editwin = None41 42 def format_paragraph_event(self, event, limit=None):43 """Formats paragraph to a max width specified in idleConf.44 45 If text is selected, format_paragraph_event will start breaking lines46 at the max width, starting from the beginning selection.47 48 If no text is selected, format_paragraph_event uses the current49 cursor location to determine the paragraph (lines of text surrounded50 by blank lines) and formats it.51 52 The length limit parameter is for testing with a known value.53 """54 limit = self.max_width if limit is None else limit55 text = self.editwin.text56 first, last = self.editwin.get_selection_indices()57 if first and last:58 data = text.get(first, last)59 comment_header = get_comment_header(data)60 else:61 first, last, comment_header, data = \62 find_paragraph(text, text.index("insert"))63 if comment_header:64 newdata = reformat_comment(data, limit, comment_header)65 else:66 newdata = reformat_paragraph(data, limit)67 text.tag_remove("sel", "1.0", "end")68 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 return "break"79 80 81FormatParagraph.reload()82 83def find_paragraph(text, mark):84 """Returns the start/stop indices enclosing the paragraph that mark is in.85 86 Also returns the comment format string, if any, and paragraph of text87 between the start/stop indices.88 """89 lineno, col = map(int, mark.split("."))90 line = text.get("%d.0" % lineno, "%d.end" % lineno)91 92 # Look for start of next paragraph if the index passed in is a blank line93 while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):94 lineno = lineno + 195 line = text.get("%d.0" % lineno, "%d.end" % lineno)96 first_lineno = lineno97 comment_header = get_comment_header(line)98 comment_header_len = len(comment_header)99 100 # Once start line found, search for end of paragraph (a blank line)101 while get_comment_header(line)==comment_header and \102 not is_all_white(line[comment_header_len:]):103 lineno = lineno + 1104 line = text.get("%d.0" % lineno, "%d.end" % lineno)105 last = "%d.0" % lineno106 107 # Search back to beginning of paragraph (first blank line before)108 lineno = first_lineno - 1109 line = text.get("%d.0" % lineno, "%d.end" % lineno)110 while lineno > 0 and \111 get_comment_header(line)==comment_header and \112 not is_all_white(line[comment_header_len:]):113 lineno = lineno - 1114 line = text.get("%d.0" % lineno, "%d.end" % lineno)115 first = "%d.0" % (lineno+1)116 117 return first, last, comment_header, text.get(first, last)118 119# This should perhaps be replaced with textwrap.wrap120def reformat_paragraph(data, limit):121 """Return data reformatted to specified width (limit)."""122 lines = data.split("\n")123 i = 0124 n = len(lines)125 while i < n and is_all_white(lines[i]):126 i = i+1127 if i >= n:128 return data129 indent1 = get_indent(lines[i])130 if i+1 < n and not is_all_white(lines[i+1]):131 indent2 = get_indent(lines[i+1])132 else:133 indent2 = indent1134 new = lines[:i]135 partial = indent1136 while i < n and not is_all_white(lines[i]):137 # XXX Should take double space after period (etc.) into account138 words = re.split(r"(\s+)", lines[i])139 for j in range(0, len(words), 2):140 word = words[j]141 if not word:142 continue # Can happen when line ends in whitespace143 if len((partial + word).expandtabs()) > limit and \144 partial != indent1:145 new.append(partial.rstrip())146 partial = indent2147 partial = partial + word + " "148 if j+1 < len(words) and words[j+1] != " ":149 partial = partial + " "150 i = i+1151 new.append(partial.rstrip())152 # XXX Should reformat remaining paragraphs as well153 new.extend(lines[i:])154 return "\n".join(new)155 156def reformat_comment(data, limit, comment_header):157 """Return data reformatted to specified width with comment header."""158 159 # Remove header from the comment lines160 lc = len(comment_header)161 data = "\n".join(line[lc:] for line in data.split("\n"))162 # Reformat to maxformatwidth chars or a 20 char width,163 # whichever is greater.164 format_width = max(limit - len(comment_header), 20)165 newdata = reformat_paragraph(data, format_width)166 # re-split and re-insert the comment header.167 newdata = newdata.split("\n")168 # If the block ends in a \n, we don't want the comment prefix169 # inserted after it. (Im not sure it makes sense to reformat a170 # comment block that is not made of complete lines, but whatever!)171 # Can't think of a clean solution, so we hack away172 block_suffix = ""173 if not newdata[-1]:174 block_suffix = "\n"175 newdata = newdata[:-1]176 return '\n'.join(comment_header+line for line in newdata) + block_suffix177 178def is_all_white(line):179 """Return True if line is empty or all whitespace."""180 181 return re.match(r"^\s*$", line) is not None182 183def get_indent(line):184 """Return the initial space or tab indent of line."""185 return re.match(r"^([ \t]*)", line).group()186 187def get_comment_header(line):188 """Return string with leading whitespace and '#' from line or ''.189 190 A null return indicates that the line is not a comment line. A non-191 null return, such as ' #', will be used to find the other lines of192 a comment block with the same indent.193 """194 m = re.match(r"^([ \t]*#*)", line)195 if m is None: return ""196 return m.group(1)197 198 199# Copied from editor.py; importing it would cause an import cycle.200_line_indent_re = re.compile(r'[ \t]*')201 202def get_line_indent(line, tabwidth):203 """Return a line's indentation as (# chars, effective # of spaces).204 205 The effective # of spaces is the length after properly "expanding"206 the tabs into spaces, as done by str.expandtabs(tabwidth).207 """208 m = _line_indent_re.match(line)209 return m.end(), len(m.group().expandtabs(tabwidth))210 211 212class FormatRegion:213 "Format selected text (region)."214 215 def __init__(self, editwin):216 self.editwin = editwin217 218 def get_region(self):219 """Return line information about the selected text region.220 221 If text is selected, the first and last indices will be222 for the selection. If there is no text selected, the223 indices will be the current cursor location.224 225 Return a tuple containing (first index, last index,226 string representation of text, list of text lines).227 """228 text = self.editwin.text229 first, last = self.editwin.get_selection_indices()230 if first and last:231 head = text.index(first + " linestart")232 tail = text.index(last + "-1c lineend +1c")233 else:234 head = text.index("insert linestart")235 tail = text.index("insert lineend +1c")236 chars = text.get(head, tail)237 lines = chars.split("\n")238 return head, tail, chars, lines239 240 def set_region(self, head, tail, chars, lines):241 """Replace the text between the given indices.242 243 Args:244 head: Starting index of text to replace.245 tail: Ending index of text to replace.246 chars: Expected to be string of current text247 between head and tail.248 lines: List of new lines to insert between head249 and tail.250 """251 text = self.editwin.text252 newchars = "\n".join(lines)253 if newchars == chars:254 text.bell()255 return256 text.tag_remove("sel", "1.0", "end")257 text.mark_set("insert", head)258 text.undo_block_start()259 text.delete(head, tail)260 text.insert(head, newchars)261 text.undo_block_stop()262 text.tag_add("sel", head, "insert")263 264 def indent_region_event(self, event=None):265 "Indent region by indentwidth spaces."266 head, tail, chars, lines = self.get_region()267 for pos in range(len(lines)):268 line = lines[pos]269 if line:270 raw, effective = get_line_indent(line, self.editwin.tabwidth)271 effective = effective + self.editwin.indentwidth272 lines[pos] = self.editwin._make_blanks(effective) + line[raw:]273 self.set_region(head, tail, chars, lines)274 return "break"275 276 def dedent_region_event(self, event=None):277 "Dedent region by indentwidth spaces."278 head, tail, chars, lines = self.get_region()279 for pos in range(len(lines)):280 line = lines[pos]281 if line:282 raw, effective = get_line_indent(line, self.editwin.tabwidth)283 effective = max(effective - self.editwin.indentwidth, 0)284 lines[pos] = self.editwin._make_blanks(effective) + line[raw:]285 self.set_region(head, tail, chars, lines)286 return "break"287 288 def comment_region_event(self, event=None):289 """Comment out each line in region.290 291 ## is appended to the beginning of each line to comment it out.292 """293 head, tail, chars, lines = self.get_region()294 for pos in range(len(lines) - 1):295 line = lines[pos]296 lines[pos] = '##' + line297 self.set_region(head, tail, chars, lines)298 return "break"299 300 def uncomment_region_event(self, event=None):301 """Uncomment each line in region.302 303 Remove ## or # in the first positions of a line. If the comment304 is not in the beginning position, this command will have no effect.305 """306 head, tail, chars, lines = self.get_region()307 for pos in range(len(lines)):308 line = lines[pos]309 if not line:310 continue311 if line[:2] == '##':312 line = line[2:]313 elif line[:1] == '#':314 line = line[1:]315 lines[pos] = line316 self.set_region(head, tail, chars, lines)317 return "break"318 319 def tabify_region_event(self, event=None):320 "Convert leading spaces to tabs for each line in selected region."321 head, tail, chars, lines = self.get_region()322 tabwidth = self._asktabwidth()323 if tabwidth is None:324 return325 for pos in range(len(lines)):326 line = lines[pos]327 if line:328 raw, effective = get_line_indent(line, tabwidth)329 ntabs, nspaces = divmod(effective, tabwidth)330 lines[pos] = '\t' * ntabs + ' ' * nspaces + line[raw:]331 self.set_region(head, tail, chars, lines)332 return "break"333 334 def untabify_region_event(self, event=None):335 "Expand tabs to spaces for each line in region."336 head, tail, chars, lines = self.get_region()337 tabwidth = self._asktabwidth()338 if tabwidth is None:339 return340 for pos in range(len(lines)):341 lines[pos] = lines[pos].expandtabs(tabwidth)342 self.set_region(head, tail, chars, lines)343 return "break"344 345 def _asktabwidth(self):346 "Return value for tab width."347 return askinteger(348 "Tab width",349 "Columns per tab? (2-16)",350 parent=self.editwin.text,351 initialvalue=self.editwin.indentwidth,352 minvalue=2,353 maxvalue=16)354 355 356class Indents:357 "Change future indents."358 359 def __init__(self, editwin):360 self.editwin = editwin361 362 def toggle_tabs_event(self, event):363 editwin = self.editwin364 usetabs = editwin.usetabs365 if askyesno(366 "Toggle tabs",367 "Turn tabs " + ("on", "off")[usetabs] +368 "?\nIndent width " +369 ("will be", "remains at")[usetabs] + " 8." +370 "\n Note: a tab is always 8 columns",371 parent=editwin.text):372 editwin.usetabs = not usetabs373 # Try to prevent inconsistent indentation.374 # User must change indent width manually after using tabs.375 editwin.indentwidth = 8376 return "break"377 378 def change_indentwidth_event(self, event):379 editwin = self.editwin380 new = askinteger(381 "Indent width",382 "New indent width (2-16)\n(Always use 8 when using tabs)",383 parent=editwin.text,384 initialvalue=editwin.indentwidth,385 minvalue=2,386 maxvalue=16)387 if new and new != editwin.indentwidth and not editwin.usetabs:388 editwin.indentwidth = new389 return "break"390 391 392class Rstrip: # 'Strip Trailing Whitespace" on "Format" menu.393 def __init__(self, editwin):394 self.editwin = editwin395 396 def do_rstrip(self, event=None):397 text = self.editwin.text398 undo = self.editwin.undo399 undo.undo_block_start()400 401 end_line = int(float(text.index('end')))402 for cur in range(1, end_line):403 txt = text.get('%i.0' % cur, '%i.end' % cur)404 raw = len(txt)405 cut = len(txt.rstrip())406 # Since text.delete() marks file as changed, even if not,407 # only call it when needed to actually delete something.408 if cut < raw:409 text.delete('%i.%i' % (cur, cut), '%i.end' % cur)410 411 if (text.get('end-2c') == '\n' # File ends with at least 1 newline;412 and not hasattr(self.editwin, 'interp')): # & is not Shell.413 # Delete extra user endlines.414 while (text.index('end-1c') > '1.0' # Stop if file empty.415 and text.get('end-3c') == '\n'):416 text.delete('end-3c')417 # Because tk indexes are slice indexes and never raise,418 # a file with only newlines will be emptied.419 # patchcheck.py does the same.420 421 undo.undo_block_stop()422 423 424if __name__ == "__main__":425 from unittest import main426 main('idlelib.idle_test.test_format', verbosity=2, exit=False)427 