(root)/
Python-3.11.7/
Lib/
idlelib/
format.py
       1  """Format all or a selected region (line slice) of text.
       2  
       3  Region formatting options: paragraph, comment block, indent, deindent,
       4  comment, uncomment, tabify, and untabify.
       5  
       6  File renamed from paragraph.py with functions added from editor.py.
       7  """
       8  import re
       9  from tkinter.messagebox import askyesno
      10  from tkinter.simpledialog import askinteger
      11  from idlelib.config import idleConf
      12  
      13  
      14  class ESC[4;38;5;81mFormatParagraph:
      15      """Format a paragraph, comment block, or selection to a max width.
      16  
      17      Does basic, standard text formatting, and also understands Python
      18      comment blocks. Thus, for editing Python source code, this
      19      extension is really only suitable for reformatting these comment
      20      blocks or triple-quoted strings.
      21  
      22      Known problems with comment reformatting:
      23      * If there is a selection marked, and the first line of the
      24        selection is not complete, the block will probably not be detected
      25        as comments, and will have the normal "text formatting" rules
      26        applied.
      27      * If a comment block has leading whitespace that mixes tabs and
      28        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 = editwin
      33  
      34      @classmethod
      35      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 = None
      41  
      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 lines
      46          at the max width, starting from the beginning selection.
      47  
      48          If no text is selected, format_paragraph_event uses the current
      49          cursor location to determine the paragraph (lines of text surrounded
      50          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 limit
      55          text = self.editwin.text
      56          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  
      81  FormatParagraph.reload()
      82  
      83  def 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 text
      87      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 line
      93      while text.compare("%d.0" % lineno, "<", "end") and is_all_white(line):
      94          lineno = lineno + 1
      95          line = text.get("%d.0" % lineno, "%d.end" % lineno)
      96      first_lineno = lineno
      97      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 + 1
     104          line = text.get("%d.0" % lineno, "%d.end" % lineno)
     105      last = "%d.0" % lineno
     106  
     107      # Search back to beginning of paragraph (first blank line before)
     108      lineno = first_lineno - 1
     109      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 - 1
     114          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.wrap
     120  def reformat_paragraph(data, limit):
     121      """Return data reformatted to specified width (limit)."""
     122      lines = data.split("\n")
     123      i = 0
     124      n = len(lines)
     125      while i < n and is_all_white(lines[i]):
     126          i = i+1
     127      if i >= n:
     128          return data
     129      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 = indent1
     134      new = lines[:i]
     135      partial = indent1
     136      while i < n and not is_all_white(lines[i]):
     137          # XXX Should take double space after period (etc.) into account
     138          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 whitespace
     143              if len((partial + word).expandtabs()) > limit and \
     144                     partial != indent1:
     145                  new.append(partial.rstrip())
     146                  partial = indent2
     147              partial = partial + word + " "
     148              if j+1 < len(words) and words[j+1] != " ":
     149                  partial = partial + " "
     150          i = i+1
     151      new.append(partial.rstrip())
     152      # XXX Should reformat remaining paragraphs as well
     153      new.extend(lines[i:])
     154      return "\n".join(new)
     155  
     156  def reformat_comment(data, limit, comment_header):
     157      """Return data reformatted to specified width with comment header."""
     158  
     159      # Remove header from the comment lines
     160      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 prefix
     169      # inserted after it. (Im not sure it makes sense to reformat a
     170      # comment block that is not made of complete lines, but whatever!)
     171      # Can't think of a clean solution, so we hack away
     172      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_suffix
     177  
     178  def is_all_white(line):
     179      """Return True if line is empty or all whitespace."""
     180  
     181      return re.match(r"^\s*$", line) is not None
     182  
     183  def get_indent(line):
     184      """Return the initial space or tab indent of line."""
     185      return re.match(r"^([ \t]*)", line).group()
     186  
     187  def 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 of
     192      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  
     202  def 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  
     212  class ESC[4;38;5;81mFormatRegion:
     213      "Format selected text (region)."
     214  
     215      def __init__(self, editwin):
     216          self.editwin = editwin
     217  
     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 be
     222          for the selection.  If there is no text selected, the
     223          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.text
     229          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, lines
     239  
     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 text
     247                  between head and tail.
     248              lines: List of new lines to insert between head
     249                  and tail.
     250          """
     251          text = self.editwin.text
     252          newchars = "\n".join(lines)
     253          if newchars == chars:
     254              text.bell()
     255              return
     256          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.indentwidth
     272                  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] = '##' + line
     297          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 comment
     304          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                  continue
     311              if line[:2] == '##':
     312                  line = line[2:]
     313              elif line[:1] == '#':
     314                  line = line[1:]
     315              lines[pos] = line
     316          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              return
     325          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              return
     340          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  
     356  class ESC[4;38;5;81mIndents:
     357      "Change future indents."
     358  
     359      def __init__(self, editwin):
     360          self.editwin = editwin
     361  
     362      def toggle_tabs_event(self, event):
     363          editwin = self.editwin
     364          usetabs = editwin.usetabs
     365          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 usetabs
     373              # Try to prevent inconsistent indentation.
     374              # User must change indent width manually after using tabs.
     375              editwin.indentwidth = 8
     376          return "break"
     377  
     378      def change_indentwidth_event(self, event):
     379          editwin = self.editwin
     380          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 = new
     389          return "break"
     390  
     391  
     392  class ESC[4;38;5;81mRstrip:  # 'Strip Trailing Whitespace" on "Format" menu.
     393      def __init__(self, editwin):
     394          self.editwin = editwin
     395  
     396      def do_rstrip(self, event=None):
     397          text = self.editwin.text
     398          undo = self.editwin.undo
     399          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  
     424  if __name__ == "__main__":
     425      from unittest import main
     426      main('idlelib.idle_test.test_format', verbosity=2, exit=False)