annotate CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/python3.8/idlelib/format.py @ 68:5028fdace37b

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