Home | History | Annotate | Download | only in idlelib
      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