FormatParagraph.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # Extension to format a paragraph
  2. # Does basic, standard text formatting, and also understands Python
  3. # comment blocks. Thus, for editing Python source code, this
  4. # extension is really only suitable for reformatting these comment
  5. # blocks or triple-quoted strings.
  6. # Known problems with comment reformatting:
  7. # * If there is a selection marked, and the first line of the
  8. # selection is not complete, the block will probably not be detected
  9. # as comments, and will have the normal "text formatting" rules
  10. # applied.
  11. # * If a comment block has leading whitespace that mixes tabs and
  12. # spaces, they will not be considered part of the same block.
  13. # * Fancy comments, like this bulleted list, arent handled :-)
  14. import string
  15. import re
  16. class FormatParagraph:
  17. menudefs = [
  18. ('edit', [
  19. ('Format Paragraph', '<<format-paragraph>>'),
  20. ])
  21. ]
  22. keydefs = {
  23. '<<format-paragraph>>': ['<Alt-q>'],
  24. }
  25. unix_keydefs = {
  26. '<<format-paragraph>>': ['<Meta-q>'],
  27. }
  28. def __init__(self, editwin):
  29. self.editwin = editwin
  30. def close(self):
  31. self.editwin = None
  32. def format_paragraph_event(self, event):
  33. text = self.editwin.text
  34. first, last = self.editwin.get_selection_indices()
  35. if first and last:
  36. data = text.get(first, last)
  37. comment_header = ''
  38. else:
  39. first, last, comment_header, data = \
  40. find_paragraph(text, text.index("insert"))
  41. if comment_header:
  42. # Reformat the comment lines - convert to text sans header.
  43. lines = data.split("\n")
  44. lines = map(lambda st, l=len(comment_header): st[l:], lines)
  45. data = "\n".join(lines)
  46. # Reformat to 70 chars or a 20 char width, whichever is greater.
  47. format_width = max(70-len(comment_header), 20)
  48. newdata = reformat_paragraph(data, format_width)
  49. # re-split and re-insert the comment header.
  50. newdata = newdata.split("\n")
  51. # If the block ends in a \n, we dont want the comment
  52. # prefix inserted after it. (Im not sure it makes sense to
  53. # reformat a comment block that isnt made of complete
  54. # lines, but whatever!) Can't think of a clean soltution,
  55. # so we hack away
  56. block_suffix = ""
  57. if not newdata[-1]:
  58. block_suffix = "\n"
  59. newdata = newdata[:-1]
  60. builder = lambda item, prefix=comment_header: prefix+item
  61. newdata = '\n'.join([builder(d) for d in newdata]) + block_suffix
  62. else:
  63. # Just a normal text format
  64. newdata = reformat_paragraph(data)
  65. text.tag_remove("sel", "1.0", "end")
  66. if newdata != data:
  67. text.mark_set("insert", first)
  68. text.undo_block_start()
  69. text.delete(first, last)
  70. text.insert(first, newdata)
  71. text.undo_block_stop()
  72. else:
  73. text.mark_set("insert", last)
  74. text.see("insert")
  75. def find_paragraph(text, mark):
  76. lineno, col = list(map(int, mark.split(".")))
  77. line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
  78. while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
  79. lineno = lineno + 1
  80. line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
  81. first_lineno = lineno
  82. comment_header = get_comment_header(line)
  83. comment_header_len = len(comment_header)
  84. while get_comment_header(line)==comment_header and \
  85. not is_all_white(line[comment_header_len:]):
  86. lineno = lineno + 1
  87. line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
  88. last = "%d.0" % lineno
  89. # Search back to beginning of paragraph
  90. lineno = first_lineno - 1
  91. line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
  92. while lineno > 0 and \
  93. get_comment_header(line)==comment_header and \
  94. not is_all_white(line[comment_header_len:]):
  95. lineno = lineno - 1
  96. line = text.get("%d.0" % lineno, "%d.0 lineend" % lineno)
  97. first = "%d.0" % (lineno+1)
  98. return first, last, comment_header, text.get(first, last)
  99. def reformat_paragraph(data, limit=70):
  100. lines = data.split("\n")
  101. i = 0
  102. n = len(lines)
  103. while i < n and is_all_white(lines[i]):
  104. i = i+1
  105. if i >= n:
  106. return data
  107. indent1 = get_indent(lines[i])
  108. if i+1 < n and not is_all_white(lines[i+1]):
  109. indent2 = get_indent(lines[i+1])
  110. else:
  111. indent2 = indent1
  112. new = lines[:i]
  113. partial = indent1
  114. while i < n and not is_all_white(lines[i]):
  115. # XXX Should take double space after period (etc.) into account
  116. words = re.split("(\s+)", lines[i])
  117. for j in range(0, len(words), 2):
  118. word = words[j]
  119. if not word:
  120. continue # Can happen when line ends in whitespace
  121. if len((partial + word).expandtabs()) > limit and \
  122. partial != indent1:
  123. new.append(partial.rstrip())
  124. partial = indent2
  125. partial = partial + word + " "
  126. if j+1 < len(words) and words[j+1] != " ":
  127. partial = partial + " "
  128. i = i+1
  129. new.append(partial.rstrip())
  130. # XXX Should reformat remaining paragraphs as well
  131. new.extend(lines[i:])
  132. return "\n".join(new)
  133. def is_all_white(line):
  134. return re.match(r"^\s*$", line) is not None
  135. def get_indent(line):
  136. return re.match(r"^(\s*)", line).group()
  137. def get_comment_header(line):
  138. m = re.match(r"^(\s*#*)", line)
  139. if m is None: return ""
  140. return m.group(1)